Files
xenia-rs/crates/xenia-cpu/src/decoder.rs
MechaCat02 d51b9346df fix(cpu): PPCBUG-275 276 420 421 422 423 562 600 fix vcmp Rc bit + decode dot forms
PPCBUG-562: Add vc_rc_bit() (PPC bit 21) and vx128r_rc_bit() (PPC bit 27)
to decoder.rs. The generic rc_bit() reads bit 0 (PPC bit 31); all vcmp XO
values are even so bit 0 is always 0, making CR6 permanently dead.

PPCBUG-275/276/420/421: Replace rc_bit() with vc_rc_bit() at all 8 pure
VC-form vcmp arms (vcmpequb, vcmpequh, vcmpgtub, vcmpgtsb, vcmpgtuh,
vcmpgtsh, vcmpgtuw, vcmpgtsw) and with the correct per-form accessor at
the 4 combined arms (vcmpeqfp|128, vcmpgefp|128, vcmpgtfp|128,
vcmpequw|128) and vcmpbfp|128.

PPCBUG-422: VX128_R-form 128-variants in combined arms now use
vx128r_rc_bit() instead of vc_rc_bit().

PPCBUG-423/600: Add 5 dot-form key entries to decode_op6 so
vcmp*fp128./vcmpequw128. decode as the correct opcode instead of Invalid.
Uses a 5-bit key (bits22-24 + bit25 + bit27) for dot-forms to avoid
aliasing against the shift/merge group (which sets bit25=1 when bit27=1).
Interpreter uses vx128r_rc_bit() to conditionally update CR6.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 21:15:06 +02:00

1133 lines
44 KiB
Rust

use crate::opcode::PpcOpcode;
/// Extract bits [a..=b] from a 32-bit value (PPC bit numbering: 0 = MSB).
#[inline(always)]
const fn extract_bits(v: u32, a: u32, b: u32) -> u32 {
(v >> (32 - 1 - b)) & ((1 << (b - a + 1)) - 1)
}
/// Decoded PPC instruction with extracted operand fields.
#[derive(Debug, Clone, Copy)]
pub struct DecodedInstr {
pub opcode: PpcOpcode,
pub raw: u32,
pub addr: u32,
}
impl DecodedInstr {
// Common field extractors (PPC bit numbering)
/// Primary opcode (bits 0-5)
#[inline] pub fn op(&self) -> u32 { extract_bits(self.raw, 0, 5) }
/// rD/rS/rT (bits 6-10) - destination/source register
#[inline] pub fn rd(&self) -> usize { extract_bits(self.raw, 6, 10) as usize }
#[inline] pub fn rs(&self) -> usize { self.rd() }
#[inline] pub fn rt(&self) -> usize { self.rd() }
/// rA (bits 11-15)
#[inline] pub fn ra(&self) -> usize { extract_bits(self.raw, 11, 15) as usize }
/// rB (bits 16-20)
#[inline] pub fn rb(&self) -> usize { extract_bits(self.raw, 16, 20) as usize }
/// rC (bits 21-25) - for 4-operand instructions
#[inline] pub fn rc(&self) -> usize { extract_bits(self.raw, 21, 25) as usize }
/// SIMM/UIMM (bits 16-31) - signed/unsigned immediate
#[inline] pub fn simm16(&self) -> i16 { (self.raw & 0xFFFF) as i16 }
#[inline] pub fn uimm16(&self) -> u16 { (self.raw & 0xFFFF) as u16 }
/// D-form displacement (signed, bits 16-31)
#[inline] pub fn d(&self) -> i32 { self.simm16() as i32 }
/// DS-form displacement (signed, bits 16-29, shifted left 2)
#[inline] pub fn ds(&self) -> i32 { (self.raw & 0xFFFC) as i16 as i32 }
/// LI field for branch (bits 6-29, sign-extended, shifted left 2)
#[inline] pub fn li(&self) -> i32 {
let li = extract_bits(self.raw, 6, 29);
// Sign-extend from 24 bits, then shift left 2
let sign_extended = ((li as i32) << 8) >> 8;
sign_extended << 2
}
/// BD field for conditional branch (bits 16-29, sign-extended, shifted left 2)
#[inline] pub fn bd(&self) -> i32 {
let bd = extract_bits(self.raw, 16, 29);
let sign_extended = ((bd as i32) << 18) >> 18;
sign_extended << 2
}
/// BO field (bits 6-10) - branch options
#[inline] pub fn bo(&self) -> u32 { extract_bits(self.raw, 6, 10) }
/// BI field (bits 11-15) - branch condition
#[inline] pub fn bi(&self) -> u32 { extract_bits(self.raw, 11, 15) }
/// AA bit (bit 30) - absolute address
#[inline] pub fn aa(&self) -> bool { (self.raw >> 1) & 1 != 0 }
/// LK bit (bit 31) - link (update LR)
#[inline] pub fn lk(&self) -> bool { self.raw & 1 != 0 }
/// Rc bit (bit 31) - record CR0
#[inline] pub fn rc_bit(&self) -> bool { self.raw & 1 != 0 }
/// Rc for VC-form vector compare instructions — PPC bit 21 = host bit 10.
#[inline] pub fn vc_rc_bit(&self) -> bool { (self.raw >> 10) & 1 != 0 }
/// Rc for VX128_R-form vector compare instructions — PPC bit 27 = host bit 4.
#[inline] pub fn vx128r_rc_bit(&self) -> bool { (self.raw >> 4) & 1 != 0 }
/// OE bit (bit 21) - overflow enable
#[inline] pub fn oe(&self) -> bool { extract_bits(self.raw, 21, 21) != 0 }
/// TO field (bits 6-10) for tw/twi/td/tdi trap instructions.
#[inline] pub fn to(&self) -> u32 { extract_bits(self.raw, 6, 10) }
/// MB, ME fields for rotate instructions
#[inline] pub fn mb(&self) -> u32 { extract_bits(self.raw, 21, 25) }
#[inline] pub fn me(&self) -> u32 { extract_bits(self.raw, 26, 30) }
/// SH field (bits 16-20) for shift instructions
#[inline] pub fn sh(&self) -> u32 { extract_bits(self.raw, 16, 20) }
/// SH field for 64-bit shifts (bits 16-20 + bit 30)
#[inline] pub fn sh64(&self) -> u32 {
(extract_bits(self.raw, 30, 30) << 5) | extract_bits(self.raw, 16, 20)
}
/// MB/ME field for MD-form and MDS-form instructions (6-bit field, split encoding).
/// MB[4:0] at PPC bits 21-25; MB[5] at PPC bit 26.
#[inline] pub fn mb_md(&self) -> u32 {
extract_bits(self.raw, 21, 25) | (extract_bits(self.raw, 26, 26) << 5)
}
/// SPR field (bits 11-20, swapped halves)
#[inline] pub fn spr(&self) -> u32 {
let spr_raw = extract_bits(self.raw, 11, 20);
((spr_raw & 0x1F) << 5) | ((spr_raw >> 5) & 0x1F)
}
/// CRM field (bits 12-19) for mtcrf
#[inline] pub fn crm(&self) -> u32 { extract_bits(self.raw, 12, 19) }
/// crfD (bits 6-8) - condition register field destination
#[inline] pub fn crfd(&self) -> usize { extract_bits(self.raw, 6, 8) as usize }
/// crfS (bits 11-13)
#[inline] pub fn crfs(&self) -> usize { extract_bits(self.raw, 11, 13) as usize }
/// L bit (bit 10) - 64-bit compare
#[inline] pub fn l(&self) -> bool { extract_bits(self.raw, 10, 10) != 0 }
/// crbD (bits 6-10)
#[inline] pub fn crbd(&self) -> u32 { extract_bits(self.raw, 6, 10) }
/// crbA (bits 11-15)
#[inline] pub fn crba(&self) -> u32 { extract_bits(self.raw, 11, 15) }
/// crbB (bits 16-20)
#[inline] pub fn crbb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
// VMX128 field extractors
/// VA128 (bits 6-10, plus bit from 29)
#[inline] pub fn va128(&self) -> usize {
(extract_bits(self.raw, 6, 10) | (extract_bits(self.raw, 29, 29) << 5)) as usize
}
/// VB128 (bits 16-20, plus bits from 28, 30)
#[inline] pub fn vb128(&self) -> usize {
(extract_bits(self.raw, 16, 20)
| (extract_bits(self.raw, 28, 28) << 5)
| (extract_bits(self.raw, 30, 30) << 6)) as usize
}
/// VD128 (bits 6-10, plus bits from 21, 22)
#[inline] pub fn vd128(&self) -> usize {
(extract_bits(self.raw, 6, 10)
| (extract_bits(self.raw, 21, 21) << 5)
| (extract_bits(self.raw, 22, 22) << 6)) as usize
}
/// VS128 - same encoding as VD128
#[inline] pub fn vs128(&self) -> usize { self.vd128() }
/// NB field (bits 16-20) for lswi/stswi
#[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
}
/// Extract the 5-bit `UIMM` (`VX128_3`) / `IMM` (`VX128_4`) field. Canary
/// packs both formats with LSB-bits 16-20 holding the field, which is
/// MSB bits 11-15 in our `extract_bits` convention. For `vpkd3d128` /
/// `vupkd3d128` the decoded selector is `type = UIMM >> 2` (3 bits; valid
/// values 0-6 per [`crate::vmx::D3dPackType`], 7 is undocumented /
/// undefined in canary) and `pack = UIMM & 0x3` (output-slot layout for
/// `vpkd3d128` only, `vupkd3d128` ignores it).
///
/// First-Pixels M3: the interpreter previously used a hand-rolled
/// `(instr.raw >> 6) & 0x7` that was **LSB-numbered** and extracted
/// bits from a completely different part of the word (the
/// secondary-opcode region). Centralizing the extractor here matches
/// canary's `FormatVX128_{3,4}::{UIMM,IMM}` field semantics exactly.
#[inline]
pub fn extract_vx128_uimm5(raw: u32) -> u32 {
extract_bits(raw, 11, 15)
}
/// Decode a 32-bit PPC instruction into its opcode.
/// Direct translation of the C++ LookupOpcode from ppc_opcode_lookup_gen.cc.
pub fn decode(raw: u32, addr: u32) -> DecodedInstr {
let opcode = lookup_opcode(raw);
DecodedInstr { opcode, raw, addr }
}
// Perf tier-2 — direct-mapped PC-keyed decode cache.
//
// The interpreter hot path spends ~15-25% of its time in `decode()`
// parsing the raw u32 and walking the primary+secondary opcode tables.
// For non-self-modifying guest code — the common case past the XEX
// loader — `decode(raw, pc)` is purely a function of `(raw, pc)` and
// the output is `Copy + 16B`. A direct-mapped cache indexed by
// `(pc >> 2) & MASK` gives the interpreter a 1-comparison fast path,
// at the cost of one branch and a 1.5 MiB region of memory.
//
// Invalidation piggybacks on `xenia_memory::GuestMemory::page_version`
// (P5 texture-cache invalidation): every cache entry carries the page
// version that was active at decode time; on lookup we compare against
// the current version of the containing 4 KiB page. Any write to the
// page bumps the counter, so the next decode on that PC is a miss that
// refills.
/// Number of direct-mapped entries. 2^16 = 65,536 slots, one PPC
/// instruction address per slot — enough for every hot code path in a
/// typical Xbox 360 title to stay resident without collision.
const DECODE_CACHE_SIZE: usize = 1 << 16;
const DECODE_CACHE_MASK: u32 = (DECODE_CACHE_SIZE - 1) as u32;
#[derive(Clone, Copy)]
struct DecodeCacheEntry {
/// Guest PC this entry was decoded at. Used as the tag on lookup; a
/// mismatch means the slot was last populated by a different PC that
/// shares the same low-16 index.
pc: u32,
/// Page version at decode time (from `GuestMemory::page_version(pc)`).
/// Zero means "unused slot" since real page versions start at 1.
page_version: u64,
decoded: DecodedInstr,
}
impl DecodeCacheEntry {
const fn empty() -> Self {
// `Invalid` is the decoder's "unrecognized opcode" sentinel; we
// use it here as the empty-slot marker. Real misses compare `pc`,
// not the opcode, so the sentinel choice is cosmetic.
Self {
pc: 0,
page_version: 0,
decoded: DecodedInstr {
opcode: PpcOpcode::Invalid,
raw: 0,
addr: 0,
},
}
}
}
/// Direct-mapped PC-keyed decode cache. One instance shared across all
/// HW threads (PC is thread-independent; entries are read-only once
/// filled). Not thread-safe — the single scheduler thread owns it.
pub struct DecodeCache {
slots: Box<[DecodeCacheEntry]>,
hits: u64,
misses: u64,
invalidations: u64,
}
impl Default for DecodeCache {
fn default() -> Self {
Self::new()
}
}
impl DecodeCache {
pub fn new() -> Self {
Self {
slots: vec![DecodeCacheEntry::empty(); DECODE_CACHE_SIZE].into_boxed_slice(),
hits: 0,
misses: 0,
invalidations: 0,
}
}
/// Look up (or fill) the decoded form of the instruction at `pc`.
/// `raw` is the fetched instruction word; `current_page_version` is
/// `mem.page_version(pc)` — the caller has it cheaper than we do,
/// since they're already touching `mem` to fetch `raw`.
#[inline]
pub fn lookup(&mut self, pc: u32, raw: u32, current_page_version: u64) -> DecodedInstr {
let idx = ((pc >> 2) & DECODE_CACHE_MASK) as usize;
// Safety: `idx` is masked into `[0, DECODE_CACHE_SIZE)` so the
// slice access is always in-bounds. Opt-out of the bounds check
// for the hot path.
let entry = unsafe { self.slots.get_unchecked_mut(idx) };
if entry.pc == pc && entry.page_version == current_page_version {
self.hits += 1;
return entry.decoded;
}
if entry.pc == pc && entry.page_version != current_page_version {
self.invalidations += 1;
}
self.misses += 1;
let decoded = decode(raw, pc);
*entry = DecodeCacheEntry {
pc,
page_version: current_page_version,
decoded,
};
decoded
}
pub fn hits(&self) -> u64 {
self.hits
}
pub fn misses(&self) -> u64 {
self.misses
}
pub fn invalidations(&self) -> u64 {
self.invalidations
}
}
fn lookup_opcode(code: u32) -> PpcOpcode {
match extract_bits(code, 0, 5) {
2 => PpcOpcode::tdi,
3 => PpcOpcode::twi,
4 => decode_op4(code),
5 => decode_op5(code),
6 => decode_op6(code),
7 => PpcOpcode::mulli,
8 => PpcOpcode::subficx,
10 => PpcOpcode::cmpli,
11 => PpcOpcode::cmpi,
12 => PpcOpcode::addic,
13 => PpcOpcode::addicx,
14 => PpcOpcode::addi,
15 => PpcOpcode::addis,
16 => PpcOpcode::bcx,
17 => PpcOpcode::sc,
18 => PpcOpcode::bx,
19 => decode_op19(code),
20 => PpcOpcode::rlwimix,
21 => PpcOpcode::rlwinmx,
23 => PpcOpcode::rlwnmx,
24 => PpcOpcode::ori,
25 => PpcOpcode::oris,
26 => PpcOpcode::xori,
27 => PpcOpcode::xoris,
28 => PpcOpcode::andix,
29 => PpcOpcode::andisx,
30 => decode_op30(code),
31 => decode_op31(code),
32 => PpcOpcode::lwz,
33 => PpcOpcode::lwzu,
34 => PpcOpcode::lbz,
35 => PpcOpcode::lbzu,
36 => PpcOpcode::stw,
37 => PpcOpcode::stwu,
38 => PpcOpcode::stb,
39 => PpcOpcode::stbu,
40 => PpcOpcode::lhz,
41 => PpcOpcode::lhzu,
42 => PpcOpcode::lha,
43 => PpcOpcode::lhau,
44 => PpcOpcode::sth,
45 => PpcOpcode::sthu,
46 => PpcOpcode::lmw,
47 => PpcOpcode::stmw,
48 => PpcOpcode::lfs,
49 => PpcOpcode::lfsu,
50 => PpcOpcode::lfd,
51 => PpcOpcode::lfdu,
52 => PpcOpcode::stfs,
53 => PpcOpcode::stfsu,
54 => PpcOpcode::stfd,
55 => PpcOpcode::stfdu,
58 => match extract_bits(code, 30, 31) {
0b00 => PpcOpcode::ld,
0b01 => PpcOpcode::ldu,
0b10 => PpcOpcode::lwa,
_ => PpcOpcode::Invalid,
},
59 => match extract_bits(code, 26, 30) {
0b10010 => PpcOpcode::fdivsx,
0b10100 => PpcOpcode::fsubsx,
0b10101 => PpcOpcode::faddsx,
0b10110 => PpcOpcode::fsqrtsx,
0b11000 => PpcOpcode::fresx,
0b11001 => PpcOpcode::fmulsx,
0b11100 => PpcOpcode::fmsubsx,
0b11101 => PpcOpcode::fmaddsx,
0b11110 => PpcOpcode::fnmsubsx,
0b11111 => PpcOpcode::fnmaddsx,
_ => PpcOpcode::Invalid,
},
62 => match extract_bits(code, 30, 31) {
0b00 => PpcOpcode::std,
0b01 => PpcOpcode::stdu,
_ => PpcOpcode::Invalid,
},
63 => decode_op63(code),
_ => PpcOpcode::Invalid,
}
}
fn decode_op4(code: u32) -> PpcOpcode {
// VMX128 load/store (op=4, bits 21-27 << 4 | bits 30-31)
let key1 = (extract_bits(code, 21, 27) << 4) | extract_bits(code, 30, 31);
match key1 {
0b00000000011 => return PpcOpcode::lvsl128,
0b00001000011 => return PpcOpcode::lvsr128,
0b00010000011 => return PpcOpcode::lvewx128,
0b00011000011 => return PpcOpcode::lvx128,
0b00110000011 => return PpcOpcode::stvewx128,
0b00111000011 => return PpcOpcode::stvx128,
0b01011000011 => return PpcOpcode::lvxl128,
0b01111000011 => return PpcOpcode::stvxl128,
0b10000000011 => return PpcOpcode::lvlx128,
0b10001000011 => return PpcOpcode::lvrx128,
0b10100000011 => return PpcOpcode::stvlx128,
0b10101000011 => return PpcOpcode::stvrx128,
0b11000000011 => return PpcOpcode::lvlxl128,
0b11001000011 => return PpcOpcode::lvrxl128,
0b11100000011 => return PpcOpcode::stvlxl128,
0b11101000011 => return PpcOpcode::stvrxl128,
_ => {}
}
// Standard VMX (op=4, bits 21-31)
let key2 = extract_bits(code, 21, 31);
match key2 {
0b00000000000 => return PpcOpcode::vaddubm,
0b00000000010 => return PpcOpcode::vmaxub,
0b00000000100 => return PpcOpcode::vrlb,
0b00000001000 => return PpcOpcode::vmuloub,
0b00000001010 => return PpcOpcode::vaddfp,
0b00000001100 => return PpcOpcode::vmrghb,
0b00000001110 => return PpcOpcode::vpkuhum,
0b00001000000 => return PpcOpcode::vadduhm,
0b00001000010 => return PpcOpcode::vmaxuh,
0b00001000100 => return PpcOpcode::vrlh,
0b00001001000 => return PpcOpcode::vmulouh,
0b00001001010 => return PpcOpcode::vsubfp,
0b00001001100 => return PpcOpcode::vmrghh,
0b00001001110 => return PpcOpcode::vpkuwum,
0b00010000000 => return PpcOpcode::vadduwm,
0b00010000010 => return PpcOpcode::vmaxuw,
0b00010000100 => return PpcOpcode::vrlw,
0b00010001100 => return PpcOpcode::vmrghw,
0b00010001110 => return PpcOpcode::vpkuhus,
0b00011001110 => return PpcOpcode::vpkuwus,
0b00100000010 => return PpcOpcode::vmaxsb,
0b00100000100 => return PpcOpcode::vslb,
0b00100001000 => return PpcOpcode::vmulosb,
0b00100001010 => return PpcOpcode::vrefp,
0b00100001100 => return PpcOpcode::vmrglb,
0b00100001110 => return PpcOpcode::vpkshus,
0b00101000010 => return PpcOpcode::vmaxsh,
0b00101000100 => return PpcOpcode::vslh,
0b00101001000 => return PpcOpcode::vmulosh,
0b00101001010 => return PpcOpcode::vrsqrtefp,
0b00101001100 => return PpcOpcode::vmrglh,
0b00101001110 => return PpcOpcode::vpkswus,
0b00110000000 => return PpcOpcode::vaddcuw,
0b00110000010 => return PpcOpcode::vmaxsw,
0b00110000100 => return PpcOpcode::vslw,
0b00110001010 => return PpcOpcode::vexptefp,
0b00110001100 => return PpcOpcode::vmrglw,
0b00110001110 => return PpcOpcode::vpkshss,
0b00111000100 => return PpcOpcode::vsl,
0b00111001010 => return PpcOpcode::vlogefp,
0b00111001110 => return PpcOpcode::vpkswss,
0b01000000000 => return PpcOpcode::vaddubs,
0b01000000010 => return PpcOpcode::vminub,
0b01000000100 => return PpcOpcode::vsrb,
0b01000001000 => return PpcOpcode::vmuleub,
0b01000001010 => return PpcOpcode::vrfin,
0b01000001100 => return PpcOpcode::vspltb,
0b01000001110 => return PpcOpcode::vupkhsb,
0b01001000000 => return PpcOpcode::vadduhs,
0b01001000010 => return PpcOpcode::vminuh,
0b01001000100 => return PpcOpcode::vsrh,
0b01001001000 => return PpcOpcode::vmuleuh,
0b01001001010 => return PpcOpcode::vrfiz,
0b01001001100 => return PpcOpcode::vsplth,
0b01001001110 => return PpcOpcode::vupkhsh,
0b01010000000 => return PpcOpcode::vadduws,
0b01010000010 => return PpcOpcode::vminuw,
0b01010000100 => return PpcOpcode::vsrw,
0b01010001010 => return PpcOpcode::vrfip,
0b01010001100 => return PpcOpcode::vspltw,
0b01010001110 => return PpcOpcode::vupklsb,
0b01011000100 => return PpcOpcode::vsr,
0b01011001010 => return PpcOpcode::vrfim,
0b01011001110 => return PpcOpcode::vupklsh,
0b01100000000 => return PpcOpcode::vaddsbs,
0b01100000010 => return PpcOpcode::vminsb,
0b01100000100 => return PpcOpcode::vsrab,
0b01100001000 => return PpcOpcode::vmulesb,
0b01100001010 => return PpcOpcode::vcfux,
0b01100001100 => return PpcOpcode::vspltisb,
0b01100001110 => return PpcOpcode::vpkpx,
0b01101000000 => return PpcOpcode::vaddshs,
0b01101000010 => return PpcOpcode::vminsh,
0b01101000100 => return PpcOpcode::vsrah,
0b01101001000 => return PpcOpcode::vmulesh,
0b01101001010 => return PpcOpcode::vcfsx,
0b01101001100 => return PpcOpcode::vspltish,
0b01101001110 => return PpcOpcode::vupkhpx,
0b01110000000 => return PpcOpcode::vaddsws,
0b01110000010 => return PpcOpcode::vminsw,
0b01110000100 => return PpcOpcode::vsraw,
0b01110001010 => return PpcOpcode::vctuxs,
0b01110001100 => return PpcOpcode::vspltisw,
0b01111001010 => return PpcOpcode::vctsxs,
0b01111001110 => return PpcOpcode::vupklpx,
0b10000000000 => return PpcOpcode::vsububm,
0b10000000010 => return PpcOpcode::vavgub,
0b10000000100 => return PpcOpcode::vand,
0b10000001010 => return PpcOpcode::vmaxfp,
0b10000001100 => return PpcOpcode::vslo,
0b10001000000 => return PpcOpcode::vsubuhm,
0b10001000010 => return PpcOpcode::vavguh,
0b10001000100 => return PpcOpcode::vandc,
0b10001001010 => return PpcOpcode::vminfp,
0b10001001100 => return PpcOpcode::vsro,
0b10010000000 => return PpcOpcode::vsubuwm,
0b10010000010 => return PpcOpcode::vavguw,
0b10010000100 => return PpcOpcode::vor,
0b10011000100 => return PpcOpcode::vxor,
0b10100000010 => return PpcOpcode::vavgsb,
0b10100000100 => return PpcOpcode::vnor,
0b10101000010 => return PpcOpcode::vavgsh,
0b10110000000 => return PpcOpcode::vsubcuw,
0b10110000010 => return PpcOpcode::vavgsw,
0b11000000000 => return PpcOpcode::vsububs,
0b11000000100 => return PpcOpcode::mfvscr,
0b11000001000 => return PpcOpcode::vsum4ubs,
0b11001000000 => return PpcOpcode::vsubuhs,
0b11001000100 => return PpcOpcode::mtvscr,
0b11001001000 => return PpcOpcode::vsum4shs,
0b11010000000 => return PpcOpcode::vsubuws,
0b11010001000 => return PpcOpcode::vsum2sws,
0b11100000000 => return PpcOpcode::vsubsbs,
0b11100001000 => return PpcOpcode::vsum4sbs,
0b11101000000 => return PpcOpcode::vsubshs,
0b11110000000 => return PpcOpcode::vsubsws,
0b11110001000 => return PpcOpcode::vsumsws,
_ => {}
}
// VMX compare (op=4, bits 22-31)
let key3 = extract_bits(code, 22, 31);
match key3 {
0b0000000110 => return PpcOpcode::vcmpequb,
0b0001000110 => return PpcOpcode::vcmpequh,
0b0010000110 => return PpcOpcode::vcmpequw,
0b0011000110 => return PpcOpcode::vcmpeqfp,
0b0111000110 => return PpcOpcode::vcmpgefp,
0b1000000110 => return PpcOpcode::vcmpgtub,
0b1001000110 => return PpcOpcode::vcmpgtuh,
0b1010000110 => return PpcOpcode::vcmpgtuw,
0b1011000110 => return PpcOpcode::vcmpgtfp,
0b1100000110 => return PpcOpcode::vcmpgtsb,
0b1101000110 => return PpcOpcode::vcmpgtsh,
0b1110000110 => return PpcOpcode::vcmpgtsw,
0b1111000110 => return PpcOpcode::vcmpbfp,
_ => {}
}
// VMX 4-operand (op=4, bits 26-31)
let key4 = extract_bits(code, 26, 31);
match key4 {
0b100000 => return PpcOpcode::vmhaddshs,
0b100001 => return PpcOpcode::vmhraddshs,
0b100010 => return PpcOpcode::vmladduhm,
0b100100 => return PpcOpcode::vmsumubm,
0b100101 => return PpcOpcode::vmsummbm,
0b100110 => return PpcOpcode::vmsumuhm,
0b100111 => return PpcOpcode::vmsumuhs,
0b101000 => return PpcOpcode::vmsumshm,
0b101001 => return PpcOpcode::vmsumshs,
0b101010 => return PpcOpcode::vsel,
0b101011 => return PpcOpcode::vperm,
0b101100 => return PpcOpcode::vsldoi,
0b101110 => return PpcOpcode::vmaddfp,
0b101111 => return PpcOpcode::vnmsubfp,
_ => {}
}
// vsldoi128 (op=4, bit 27)
if extract_bits(code, 27, 27) == 1 {
return PpcOpcode::vsldoi128;
}
PpcOpcode::Invalid
}
fn decode_op5(code: u32) -> PpcOpcode {
// vperm128 (op=5, bits 22,27)
let key1 = (extract_bits(code, 22, 22) << 5) | extract_bits(code, 27, 27);
if key1 == 0b000000 {
return PpcOpcode::vperm128;
}
let key2 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27);
match key2 {
0b000001 => PpcOpcode::vaddfp128,
0b000101 => PpcOpcode::vsubfp128,
0b001001 => PpcOpcode::vmulfp128,
0b001101 => PpcOpcode::vmaddfp128,
0b010001 => PpcOpcode::vmaddcfp128,
0b010101 => PpcOpcode::vnmsubfp128,
0b011001 => PpcOpcode::vmsum3fp128,
0b011101 => PpcOpcode::vmsum4fp128,
0b100000 => PpcOpcode::vpkshss128,
0b100001 => PpcOpcode::vand128,
0b100100 => PpcOpcode::vpkshus128,
0b100101 => PpcOpcode::vandc128,
0b101000 => PpcOpcode::vpkswss128,
0b101001 => PpcOpcode::vnor128,
0b101100 => PpcOpcode::vpkswus128,
0b101101 => PpcOpcode::vor128,
0b110000 => PpcOpcode::vpkuhum128,
0b110001 => PpcOpcode::vxor128,
0b110100 => PpcOpcode::vpkuhus128,
0b110101 => PpcOpcode::vsel128,
0b111000 => PpcOpcode::vpkuwum128,
0b111001 => PpcOpcode::vslo128,
0b111100 => PpcOpcode::vpkuwus128,
0b111101 => PpcOpcode::vsro128,
_ => PpcOpcode::Invalid,
}
}
fn decode_op6(code: u32) -> PpcOpcode {
// vpermwi128
let key1 = (extract_bits(code, 21, 22) << 5) | extract_bits(code, 26, 27);
if key1 == 0b0100001 {
return PpcOpcode::vpermwi128;
}
// vpkd3d128, vrlimi128
let key2 = (extract_bits(code, 21, 23) << 4) | extract_bits(code, 26, 27);
match key2 {
0b1100001 => return PpcOpcode::vpkd3d128,
0b1110001 => return PpcOpcode::vrlimi128,
_ => {}
}
// Unary VMX128 ops
let key3 = extract_bits(code, 21, 27);
match key3 {
0b0100011 => return PpcOpcode::vcfpsxws128,
0b0100111 => return PpcOpcode::vcfpuxws128,
0b0101011 => return PpcOpcode::vcsxwfp128,
0b0101111 => return PpcOpcode::vcuxwfp128,
0b0110011 => return PpcOpcode::vrfim128,
0b0110111 => return PpcOpcode::vrfin128,
0b0111011 => return PpcOpcode::vrfip128,
0b0111111 => return PpcOpcode::vrfiz128,
0b1100011 => return PpcOpcode::vrefp128,
0b1100111 => return PpcOpcode::vrsqrtefp128,
0b1101011 => return PpcOpcode::vexptefp128,
0b1101111 => return PpcOpcode::vlogefp128,
0b1110011 => return PpcOpcode::vspltw128,
0b1110111 => return PpcOpcode::vspltisw128,
0b1111111 => return PpcOpcode::vupkd3d128,
_ => {}
}
// VMX128 compare (non-dot and dot forms).
// Non-dot: bit 27 = 0. Dot: bit 27 = 1, but bit 25 must also be 0 to
// distinguish from the shift/merge group (which has bit 25 = 1 when bit 27 = 1).
// key4_nd uses bits 22-24 + bit 27 (same as original, covers non-dot).
// key4_dt uses bits 22-24 + bit 25 + bit 27 (narrower, covers dot-only).
let key4_nd = (extract_bits(code, 22, 24) << 3) | extract_bits(code, 27, 27);
match key4_nd {
0b000000 => return PpcOpcode::vcmpeqfp128,
0b001000 => return PpcOpcode::vcmpgefp128,
0b010000 => return PpcOpcode::vcmpgtfp128,
0b011000 => return PpcOpcode::vcmpbfp128,
0b100000 => return PpcOpcode::vcmpequw128,
_ => {}
}
// Dot forms: bit 27 = 1, bit 25 = 0 (key = bits22-24 + bit25 + bit27, low 3 bits)
let key4_dt = (extract_bits(code, 22, 24) << 2) | (extract_bits(code, 25, 25) << 1) | extract_bits(code, 27, 27);
match key4_dt {
0b00001 => return PpcOpcode::vcmpeqfp128, // bits22-24=000, bit25=0, bit27=1
0b00101 => return PpcOpcode::vcmpgefp128, // bits22-24=001, bit25=0, bit27=1
0b01001 => return PpcOpcode::vcmpgtfp128, // bits22-24=010, bit25=0, bit27=1
0b01101 => return PpcOpcode::vcmpbfp128, // bits22-24=011, bit25=0, bit27=1
0b10001 => return PpcOpcode::vcmpequw128, // bits22-24=100, bit25=0, bit27=1
_ => {}
}
// VMX128 shift/merge
let key5 = (extract_bits(code, 22, 25) << 2) | extract_bits(code, 27, 27);
match key5 {
0b000101 => return PpcOpcode::vrlw128,
0b001101 => return PpcOpcode::vslw128,
0b010101 => return PpcOpcode::vsraw128,
0b011101 => return PpcOpcode::vsrw128,
0b101000 => return PpcOpcode::vmaxfp128,
0b101100 => return PpcOpcode::vminfp128,
0b110000 => return PpcOpcode::vmrghw128,
0b110100 => return PpcOpcode::vmrglw128,
0b111000 => return PpcOpcode::vupkhsb128,
0b111100 => return PpcOpcode::vupklsb128,
_ => {}
}
PpcOpcode::Invalid
}
fn decode_op19(code: u32) -> PpcOpcode {
match extract_bits(code, 21, 30) {
0b0000000000 => PpcOpcode::mcrf,
0b0000010000 => PpcOpcode::bclrx,
0b0000100001 => PpcOpcode::crnor,
0b0010000001 => PpcOpcode::crandc,
0b0010010110 => PpcOpcode::isync,
0b0011000001 => PpcOpcode::crxor,
0b0011100001 => PpcOpcode::crnand,
0b0100000001 => PpcOpcode::crand,
0b0100100001 => PpcOpcode::creqv,
0b0110100001 => PpcOpcode::crorc,
0b0111000001 => PpcOpcode::cror,
0b1000010000 => PpcOpcode::bcctrx,
_ => PpcOpcode::Invalid,
}
}
fn decode_op30(code: u32) -> PpcOpcode {
match extract_bits(code, 27, 29) {
0b000 => PpcOpcode::rldiclx,
0b001 => PpcOpcode::rldicrx,
0b010 => PpcOpcode::rldicx,
0b011 => PpcOpcode::rldimix,
_ => match extract_bits(code, 27, 30) {
0b1000 => PpcOpcode::rldclx,
0b1001 => PpcOpcode::rldcrx,
_ => PpcOpcode::Invalid,
},
}
}
fn decode_op31(code: u32) -> PpcOpcode {
// sradix has a unique 10-bit key (bits 21-29)
if extract_bits(code, 21, 29) == 0b110011101 {
return PpcOpcode::sradix;
}
// Main op31 table (bits 21-30)
let key = extract_bits(code, 21, 30);
match key {
0b0000000000 => return PpcOpcode::cmp,
0b0000000100 => return PpcOpcode::tw,
0b0000000110 => return PpcOpcode::lvsl,
0b0000000111 => return PpcOpcode::lvebx,
0b0000010011 => return PpcOpcode::mfcr,
0b0000010100 => return PpcOpcode::lwarx,
0b0000010101 => return PpcOpcode::ldx,
0b0000010111 => return PpcOpcode::lwzx,
0b0000011000 => return PpcOpcode::slwx,
0b0000011010 => return PpcOpcode::cntlzwx,
0b0000011011 => return PpcOpcode::sldx,
0b0000011100 => return PpcOpcode::andx,
0b0000100000 => return PpcOpcode::cmpl,
0b0000100110 => return PpcOpcode::lvsr,
0b0000100111 => return PpcOpcode::lvehx,
0b0000110101 => return PpcOpcode::ldux,
0b0000110110 => return PpcOpcode::dcbst,
0b0000110111 => return PpcOpcode::lwzux,
0b0000111010 => return PpcOpcode::cntlzdx,
0b0000111100 => return PpcOpcode::andcx,
0b0001000100 => return PpcOpcode::td,
0b0001000111 => return PpcOpcode::lvewx,
0b0001010011 => return PpcOpcode::mfmsr,
0b0001010100 => return PpcOpcode::ldarx,
0b0001010110 => return PpcOpcode::dcbf,
0b0001010111 => return PpcOpcode::lbzx,
0b0001100111 => return PpcOpcode::lvx,
0b0001110111 => return PpcOpcode::lbzux,
0b0001111100 => return PpcOpcode::norx,
0b0010000111 => return PpcOpcode::stvebx,
0b0010010000 => return PpcOpcode::mtcrf,
0b0010010010 => return PpcOpcode::mtmsr,
0b0010010101 => return PpcOpcode::stdx,
0b0010010110 => return PpcOpcode::stwcx,
0b0010010111 => return PpcOpcode::stwx,
0b0010100111 => return PpcOpcode::stvehx,
0b0010110010 => return PpcOpcode::mtmsrd,
0b0010110101 => return PpcOpcode::stdux,
0b0010110111 => return PpcOpcode::stwux,
0b0011000111 => return PpcOpcode::stvewx,
0b0011010110 => return PpcOpcode::stdcx,
0b0011010111 => return PpcOpcode::stbx,
0b0011100111 => return PpcOpcode::stvx,
0b0011110110 => return PpcOpcode::dcbtst,
0b0011110111 => return PpcOpcode::stbux,
0b0100010110 => return PpcOpcode::dcbt,
0b0100010111 => return PpcOpcode::lhzx,
0b0100011100 => return PpcOpcode::eqvx,
0b0100110111 => return PpcOpcode::lhzux,
0b0100111100 => return PpcOpcode::xorx,
0b0101010011 => return PpcOpcode::mfspr,
0b0101010101 => return PpcOpcode::lwax,
0b0101010111 => return PpcOpcode::lhax,
0b0101100111 => return PpcOpcode::lvxl,
0b0101110011 => return PpcOpcode::mftb,
0b0101110101 => return PpcOpcode::lwaux,
0b0101110111 => return PpcOpcode::lhaux,
0b0110010111 => return PpcOpcode::sthx,
0b0110011100 => return PpcOpcode::orcx,
0b0110110111 => return PpcOpcode::sthux,
0b0110111100 => return PpcOpcode::orx,
0b0111010011 => return PpcOpcode::mtspr,
0b0111010110 => return PpcOpcode::dcbi,
0b0111011100 => return PpcOpcode::nandx,
0b0111100111 => return PpcOpcode::stvxl,
0b1000000000 => return PpcOpcode::mcrxr,
0b1000000111 => return PpcOpcode::lvlx,
0b1000010100 => return PpcOpcode::ldbrx,
0b1000010101 => return PpcOpcode::lswx,
0b1000010110 => return PpcOpcode::lwbrx,
0b1000010111 => return PpcOpcode::lfsx,
0b1000011000 => return PpcOpcode::srwx,
0b1000011011 => return PpcOpcode::srdx,
0b1000100111 => return PpcOpcode::lvrx,
0b1000110111 => return PpcOpcode::lfsux,
0b1001010101 => return PpcOpcode::lswi,
0b1001010110 => return PpcOpcode::sync,
0b1001010111 => return PpcOpcode::lfdx,
0b1001110111 => return PpcOpcode::lfdux,
0b1010000111 => return PpcOpcode::stvlx,
0b1010010100 => return PpcOpcode::stdbrx,
0b1010010101 => return PpcOpcode::stswx,
0b1010010110 => return PpcOpcode::stwbrx,
0b1010010111 => return PpcOpcode::stfsx,
0b1010100111 => return PpcOpcode::stvrx,
0b1010110111 => return PpcOpcode::stfsux,
0b1011010101 => return PpcOpcode::stswi,
0b1011010111 => return PpcOpcode::stfdx,
0b1011110111 => return PpcOpcode::stfdux,
0b1100000111 => return PpcOpcode::lvlxl,
0b1100010110 => return PpcOpcode::lhbrx,
0b1100011000 => return PpcOpcode::srawx,
0b1100011010 => return PpcOpcode::sradx,
0b1100100111 => return PpcOpcode::lvrxl,
0b1100111000 => return PpcOpcode::srawix,
0b1101010110 => return PpcOpcode::eieio,
0b1110000111 => return PpcOpcode::stvlxl,
0b1110010110 => return PpcOpcode::sthbrx,
0b1110011010 => return PpcOpcode::extshx,
0b1110100111 => return PpcOpcode::stvrxl,
0b1110111010 => return PpcOpcode::extsbx,
0b1111010110 => return PpcOpcode::icbi,
0b1111010111 => return PpcOpcode::stfiwx,
0b1111011010 => return PpcOpcode::extswx,
_ => {}
}
// Arithmetic op31 (bits 22-30)
let key2 = extract_bits(code, 22, 30);
match key2 {
0b000001000 => return PpcOpcode::subfcx,
0b000001001 => return PpcOpcode::mulhdux,
0b000001010 => return PpcOpcode::addcx,
0b000001011 => return PpcOpcode::mulhwux,
0b000101000 => return PpcOpcode::subfx,
0b001001001 => return PpcOpcode::mulhdx,
0b001001011 => return PpcOpcode::mulhwx,
0b001101000 => return PpcOpcode::negx,
0b010001000 => return PpcOpcode::subfex,
0b010001010 => return PpcOpcode::addex,
0b011001000 => return PpcOpcode::subfzex,
0b011001010 => return PpcOpcode::addzex,
0b011101000 => return PpcOpcode::subfmex,
0b011101001 => return PpcOpcode::mulldx,
0b011101010 => return PpcOpcode::addmex,
0b011101011 => return PpcOpcode::mullwx,
0b100001010 => return PpcOpcode::addx,
0b111001001 => return PpcOpcode::divdux,
0b111001011 => return PpcOpcode::divwux,
0b111101001 => return PpcOpcode::divdx,
0b111101011 => return PpcOpcode::divwx,
_ => {}
}
// dcbz/dcbz128 special case
let key3 = (extract_bits(code, 6, 10) << 20) | (extract_bits(code, 21, 30));
match key3 {
0b0000000000000001111110110 => return PpcOpcode::dcbz,
0b0000100000000001111110110 => return PpcOpcode::dcbz128,
_ => {}
}
PpcOpcode::Invalid
}
fn decode_op63(code: u32) -> PpcOpcode {
// Primary op63 table (bits 21-30)
match extract_bits(code, 21, 30) {
0b0000000000 => return PpcOpcode::fcmpu,
0b0000001100 => return PpcOpcode::frspx,
0b0000001110 => return PpcOpcode::fctiwx,
0b0000001111 => return PpcOpcode::fctiwzx,
0b0000100000 => return PpcOpcode::fcmpo,
0b0000100110 => return PpcOpcode::mtfsb1x,
0b0000101000 => return PpcOpcode::fnegx,
0b0001000000 => return PpcOpcode::mcrfs,
0b0001000110 => return PpcOpcode::mtfsb0x,
0b0001001000 => return PpcOpcode::fmrx,
0b0010000110 => return PpcOpcode::mtfsfix,
0b0010001000 => return PpcOpcode::fnabsx,
0b0100001000 => return PpcOpcode::fabsx,
0b1001000111 => return PpcOpcode::mffsx,
0b1011000111 => return PpcOpcode::mtfsfx,
0b1100101110 => return PpcOpcode::fctidx,
0b1100101111 => return PpcOpcode::fctidzx,
0b1101001110 => return PpcOpcode::fcfidx,
_ => {}
}
// FPU arithmetic (bits 26-30)
match extract_bits(code, 26, 30) {
0b10010 => PpcOpcode::fdivx,
0b10100 => PpcOpcode::fsubx,
0b10101 => PpcOpcode::faddx,
0b10110 => PpcOpcode::fsqrtx,
0b10111 => PpcOpcode::fselx,
0b11001 => PpcOpcode::fmulx,
0b11010 => PpcOpcode::frsqrtex,
0b11100 => PpcOpcode::fmsubx,
0b11101 => PpcOpcode::fmaddx,
0b11110 => PpcOpcode::fnmsubx,
0b11111 => PpcOpcode::fnmaddx,
_ => PpcOpcode::Invalid,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_decode_addi() {
// addi r3, r1, 0x10 => opcode 14, rD=3, rA=1, SIMM=0x10
let raw: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::addi);
assert_eq!(instr.rd(), 3);
assert_eq!(instr.ra(), 1);
assert_eq!(instr.simm16(), 0x10);
}
#[test]
fn test_decode_lwz() {
// lwz r5, 0x20(r1) => opcode 32
let raw: u32 = (32 << 26) | (5 << 21) | (1 << 16) | 0x20;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::lwz);
assert_eq!(instr.rd(), 5);
assert_eq!(instr.ra(), 1);
assert_eq!(instr.d(), 0x20);
}
#[test]
fn decode_cache_miss_fills_then_hit() {
let mut cache = DecodeCache::new();
let raw: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10;
let pc = 0x8200_0000u32;
let first = cache.lookup(pc, raw, 1);
assert_eq!(first.opcode, PpcOpcode::addi);
assert_eq!(cache.hits(), 0);
assert_eq!(cache.misses(), 1);
// Same pc, same version → cache hit, no new decode.
let second = cache.lookup(pc, raw, 1);
assert_eq!(second.opcode, PpcOpcode::addi);
assert_eq!(cache.hits(), 1);
assert_eq!(cache.misses(), 1);
}
#[test]
fn decode_cache_stale_version_refills() {
let mut cache = DecodeCache::new();
// First fill with an `addi`.
let raw_addi: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10;
let pc = 0x8200_0000u32;
cache.lookup(pc, raw_addi, 1);
// Guest rewrote the page: same pc, different raw + bumped version.
// Cache must refill — not return the stale `addi`.
let raw_lwz: u32 = (32 << 26) | (5 << 21) | (1 << 16) | 0x20;
let refreshed = cache.lookup(pc, raw_lwz, 2);
assert_eq!(refreshed.opcode, PpcOpcode::lwz);
assert_eq!(cache.invalidations(), 1);
assert_eq!(cache.misses(), 2);
}
#[test]
fn decode_cache_pc_collision_refills() {
// Two PCs that hash to the same slot (pc >> 2 low 16 bits equal)
// must not alias. Slot index = ((pc >> 2) & 0xFFFF) — pick two
// PCs 4 * 2^16 bytes apart.
let mut cache = DecodeCache::new();
let pc_a = 0x8200_0000u32;
let pc_b = pc_a.wrapping_add(0x0004_0000u32); // (>> 2) differs by 2^16
let raw_addi: u32 = (14 << 26) | (3 << 21) | (1 << 16) | 0x10;
let raw_lwz: u32 = (32 << 26) | (5 << 21) | (1 << 16) | 0x20;
cache.lookup(pc_a, raw_addi, 1);
// Different pc but same slot → miss + refill.
cache.lookup(pc_b, raw_lwz, 1);
// First pc comes back → miss + refill (slot was taken by pc_b).
let back = cache.lookup(pc_a, raw_addi, 1);
assert_eq!(back.opcode, PpcOpcode::addi);
assert_eq!(cache.misses(), 3);
}
#[test]
fn test_decode_branch() {
// b +0x100 => opcode 18, LI=0x40 (shifted left 2 = 0x100), AA=0, LK=0
let raw: u32 = (18 << 26) | (0x40 << 2);
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::bx);
assert_eq!(instr.li(), 0x100);
assert!(!instr.aa());
assert!(!instr.lk());
}
#[test]
fn test_decode_stw() {
// stw r7, 0x8(r2)
let raw: u32 = (36 << 26) | (7 << 21) | (2 << 16) | 0x8;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::stw);
assert_eq!(instr.rs(), 7);
assert_eq!(instr.ra(), 2);
}
#[test]
fn test_decode_ori_nop() {
// ori r0, r0, 0 = NOP
let raw: u32 = 24 << 26;
let instr = decode(raw, 0);
assert_eq!(instr.opcode, PpcOpcode::ori);
}
#[test]
fn test_extract_bits() {
assert_eq!(extract_bits(0xFFFF_FFFF, 0, 5), 0x3F);
assert_eq!(extract_bits(0x8000_0000, 0, 0), 1);
assert_eq!(extract_bits(0x0000_0001, 31, 31), 1);
}
// VMX128 register-name extraction. Locks the canonical bit positions
// (decoder.rs is the single source of truth — the analysis crate's
// old `ppc.rs` had different positions, which produced wrong printed
// register names; the bug was silent because the interpreter never
// used those extractors). Each test poke-bits exactly the slots the
// accessor reads and asserts the assembled register number.
fn vmx128_test_word(va21: u32, vd6_10: u32, vd21: u32, vd22: u32,
vb16_20: u32, vb28: u32, vb30: u32) -> u32 {
// PPC bit i -> LSB position 31-i.
(vd6_10 << (31 - 10))
| (va21 << (31 - 21)) // va128 high bit at PPC 29 in some forms — kept 0 here
| (vd21 << (31 - 21))
| (vd22 << (31 - 22))
| (vb16_20 << (31 - 20))
| (vb28 << (31 - 28))
| (vb30 << (31 - 30))
}
#[test]
fn vmx128_vd128_low_5_bits_only() {
// vd_lo = 0..31, vd_b21 = 0, vd_b22 = 0 → vd128 = vd_lo
for r in 0..32u32 {
let raw = (r as u32) << (31 - 10);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), r as usize, "vd_lo={r}");
}
}
#[test]
fn vmx128_vd128_bit21_adds_32() {
// vd_lo = 0, vd_b21 = 1, vd_b22 = 0 → vd128 = 32
let raw = (0u32 << (31 - 10)) | (1u32 << (31 - 21));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), 32);
}
#[test]
fn vmx128_vd128_bit22_adds_64() {
// vd_lo = 0, vd_b21 = 0, vd_b22 = 1 → vd128 = 64
let raw = (0u32 << (31 - 10)) | (1u32 << (31 - 22));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), 64);
}
#[test]
fn vmx128_vd128_full_127() {
// vd_lo = 31, vd_b21 = 1, vd_b22 = 1 → vd128 = 127
let raw = (31u32 << (31 - 10))
| (1u32 << (31 - 21))
| (1u32 << (31 - 22));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), 127);
}
#[test]
fn vmx128_va128_uses_bit29() {
// va128 = bits 6-10 + bit 29. va_lo = 7, bit 29 = 1 → va128 = 7 | 32 = 39.
let raw = (7u32 << (31 - 10)) | (1u32 << (31 - 29));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.va128(), 39);
}
#[test]
fn vmx128_vb128_uses_bits28_and_30() {
// vb128 = bits 16-20 + bit 28 + bit 30. Low 5 = 5, bit 28 = 1 → +32, bit 30 = 1 → +64.
let raw = (5u32 << (31 - 20))
| (1u32 << (31 - 28))
| (1u32 << (31 - 30));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vb128(), 5 | 32 | 64);
}
#[test]
fn vmx128_vs128_aliases_vd128() {
// vs128 must always equal vd128.
for r in [0u32, 31, 32, 64, 96, 127] {
let lo = r & 0x1F;
let b21 = (r >> 5) & 1;
let b22 = (r >> 6) & 1;
let raw = (lo << (31 - 10))
| (b21 << (31 - 21))
| (b22 << (31 - 22));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), r as usize, "vd128 mismatch for r={r}");
assert_eq!(d.vs128(), r as usize, "vs128 mismatch for r={r}");
assert_eq!(d.vd128(), d.vs128());
}
}
#[test]
#[allow(dead_code)]
fn _vmx128_test_word_helper_compiles() {
// Keep the helper validated against the real accessor.
let raw = vmx128_test_word(0, 5, 1, 1, 0, 0, 0);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), 5 | 32 | 64);
}
}