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); } }