Files
xenia-rs/crates/xenia-cpu/src/decoder.rs
MechaCat02 7609dcd406 fix(cpu): PPCBUG-700 VMX128 register accessors match canary bitfield layout
Independent review of P3 batch 2 (52ece4b) found that all three VMX128
register accessors disagreed with canary's FormatVX128/VX128_R bitfield
struct (`xenia-canary/src/xenia/cpu/ppc/ppc_decode_data.h:484-663`). The
audit at line 2958 had marked these "confirmed-clean" but had miscounted
LSB-first bitfield offsets.

Canary's actual layout (LSB-first, GCC/Clang/MSVC on x86):
  VA128 = VA128l(5) | VA128h(1)<<5 | VA128H(1)<<6
        = PPC[11:15] | PPC[26]<<5 | PPC[21]<<6  (7-bit selector, 3 fields)
  VB128 = VB128l(5) | VB128h(2)<<5
        = PPC[16:20] | PPC[30:31]<<5            (7-bit selector, 2 fields)
  VD128 = VD128l(5) | VD128h(2)<<5
        = PPC[6:10]  | PPC[28:29]<<5            (7-bit selector, 2 fields)
  VX128_R Rc = PPC[25]  (host bit 6)             not PPC[27] as prior fix had

The buggy convention was internally consistent with hand-crafted test
fixtures (which set bits 29/21/22 to encode the high registers, matching
the buggy accessor). Real Xbox 360 game code follows canary's convention,
so any production VMX128 instruction with VR >= 32 was silently mis-decoded
— but no unit test exercised that path until the va128 fix in 52ece4b
exposed the inconsistency.

Changes:
- decoder.rs: rewrite va128/vb128/vd128/vx128r_rc_bit to canary positions.
  Drop the speculative `key4_dt` dot-form dispatch in decode_op6 — canary
  has no separate dot-form opcodes for VX128_R compute ops; Rc is a
  runtime modifier read by the interpreter via vx128r_rc_bit().
- decoder.rs tests: rewrite vmx128_test_word helper for canary layout;
  rename/re-encode vmx128_vd128_*, vmx128_va128_*, vmx128_vb128_* tests.
- interpreter.rs: update encode_vpkd3d128 test helper to encode VD via
  canary's VD128h field; tests now pass vd=96 explicitly.
- tests/disasm_goldens.rs: replace the vrlimi128/vsrw128/vpermwi128/
  vperm128 hand-encoded raws with canary-compliant encodings; introduce
  a shared `encode_vx128` helper.
- tests/golden/vmx128_registers.json: re-encode 9 entries (vperm128,
  vsrw128 ×2, vpermwi128, vrlimi128 ×2, vmaddfp128, vmaddcfp128,
  vnmsubfp128) to canary-compliant raws preserving the same expected
  operand strings.
- audit-findings.md: new PPCBUG-700 entry documenting the discovery and
  invalidating the audit's "confirmed-clean" assessment.

Affects all VMX128 binary ops (vaddfp128, vsubfp128, vmulfp128, vand128,
vor128, vxor128, vnor128, vandc128, vsel128, vslo128, vsro128, vperm128,
vsrw128, vmaddfp128, vmaddcfp128, vnmsubfp128, vpkd3d128, vpkshss128,
vpkshus128, vpkswss128, vpkswus128, vpkuhum128, vpkuhus128, vpkuwum128,
vpkuwus128, vmsum3fp128, vmsum4fp128, vrlimi128, vpermwi128 — 30+
opcodes), plus VX128_R compare dot-forms.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 11:22:20 +02:00

1246 lines
50 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.
/// VX128_R Rc bit — PPC bit 25 (host bit 6) per canary's FormatVX128_R
/// bitfield layout. PPCBUG-700.
#[inline] pub fn vx128r_rc_bit(&self) -> bool { (self.raw >> 6) & 1 != 0 }
/// IMM field for VX128_4-form instructions (vrlimi128) — 5-bit blend mask at PPC bits 11-15.
#[inline] pub fn vx128_4_imm(&self) -> u32 { extract_bits(self.raw, 11, 15) }
/// z field for VX128_4-form instructions (vrlimi128) — 2-bit rotation index at PPC bits 24-25.
#[inline] pub fn vx128_4_z(&self) -> u32 { extract_bits(self.raw, 24, 25) }
/// 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 — bit positions match canary's
// FormatVX128/VX128_2/VX128_4/VX128_5/VX128_R bitfield layout
// (xenia-canary `ppc_decode_data.h:484-663`, LSB-first packed). PPCBUG-700.
/// VA128 = VA128l(5) | VA128h(1) << 5 | VA128H(1) << 6.
/// Canonical 7-bit register selector: PPC 11-15 (low), PPC 26 (mid), PPC 21 (high).
#[inline] pub fn va128(&self) -> usize {
(extract_bits(self.raw, 11, 15)
| (extract_bits(self.raw, 26, 26) << 5)
| (extract_bits(self.raw, 21, 21) << 6)) as usize
}
/// VB128 = VB128l(5) | VB128h(2) << 5. Canary's VB128h is a 2-bit
/// contiguous field at PPC 30-31 (host bits 0-1).
#[inline] pub fn vb128(&self) -> usize {
(extract_bits(self.raw, 16, 20)
| (extract_bits(self.raw, 30, 31) << 5)) as usize
}
/// VD128 = VD128l(5) | VD128h(2) << 5. Canary's VD128h is a 2-bit
/// contiguous field at PPC 28-29 (host bits 2-3).
#[inline] pub fn vd128(&self) -> usize {
(extract_bits(self.raw, 6, 10)
| (extract_bits(self.raw, 28, 29) << 5)) as usize
}
/// VS128 - same encoding as VD128
#[inline] pub fn vs128(&self) -> usize { self.vd128() }
/// VC register for VX128_2-form instructions (vperm128) — 3-bit at PPC bits 23-25.
#[inline] pub fn vc128_2(&self) -> usize { extract_bits(self.raw, 23, 25) as usize }
/// NB field (bits 16-20) for lswi/stswi
#[inline] pub fn nb(&self) -> u32 { extract_bits(self.raw, 16, 20) }
/// PERM field for VX128_P-form instructions (vpermwi128) — 8-bit split encoding.
/// PERMl (5 bits) at PPC bits 11-15; PERMh (3 bits) at PPC bits 23-25.
#[inline] pub fn vx128_p_perm(&self) -> u32 {
extract_bits(self.raw, 11, 15) | (extract_bits(self.raw, 23, 25) << 5)
}
/// SH field for VX128_5-form instructions (vsldoi128) — 4-bit shift at PPC bits 22-25.
#[inline] pub fn vx128_5_sh(&self) -> u32 { extract_bits(self.raw, 22, 25) }
}
/// 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 (VX128_R form). Single dispatch path: bit 27 = 0 always
// for these opcodes per canary's table (`ppc_opcode_table_gen.cc:295-305`).
// The Rc bit is at PPC 25 (host bit 6) per the FormatVX128_R bitfield —
// it's a runtime modifier read by the interpreter, NOT part of the
// secondary-opcode discrimination. PPCBUG-700.
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,
_ => {}
}
// 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.
/// Build a VMX128 test word for the canary-compliant register layout.
/// `vd128 = vd_lo | (vd_hi << 5)` where vd_lo is 5 bits (PPC 6-10) and
/// vd_hi is 2 bits (PPC 28-29). Same shape for vb128 (vb_lo at PPC 16-20,
/// vb_hi 2 bits at PPC 30-31). va128 = va_lo | (va_h26<<5) | (va_h21<<6)
/// per canary's 7-bit VA selector.
fn vmx128_test_word(vd_lo: u32, vd_hi: u32, va_lo: u32, va_h26: u32, va_h21: u32,
vb_lo: u32, vb_hi: u32) -> u32 {
// PPC bit i -> host bit (31-i).
(vd_lo << (31 - 10)) // VD128l: PPC 6-10 = host 21-25
| (vd_hi << (31 - 29)) // VD128h: PPC 28-29 = host 2-3 (LSB at host 2)
| (va_lo << (31 - 15)) // VA128l: PPC 11-15 = host 16-20
| (va_h26 << (31 - 26)) // VA128h: PPC 26 = host 5
| (va_h21 << (31 - 21)) // VA128H: PPC 21 = host 10
| (vb_lo << (31 - 20)) // VB128l: PPC 16-20 = host 11-15
| (vb_hi << (31 - 31)) // VB128h: PPC 30-31 = host 0-1 (LSB at host 0)
}
#[test]
fn vmx128_vd128_low_5_bits_only() {
// vd_lo = 0..31, vd_hi = 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_high_low_bit_adds_32() {
// vd_lo = 0, VD128h = 0b01 (LSB only at host bit 2 = PPC 29) → vd128 = 32
let raw = (1u32 << (31 - 29));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), 32);
}
#[test]
fn vmx128_vd128_high_high_bit_adds_64() {
// vd_lo = 0, VD128h = 0b10 (MSB only at host bit 3 = PPC 28) → vd128 = 64
let raw = (1u32 << (31 - 28));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), 64);
}
#[test]
fn vmx128_vd128_full_127() {
// vd_lo = 31, VD128h = 0b11 → vd128 = 127
let raw = (31u32 << (31 - 10))
| (1u32 << (31 - 28))
| (1u32 << (31 - 29));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), 127);
}
#[test]
fn vmx128_va128_canary_layout() {
// va_lo = 7 at PPC 11-15, VA128h = 1 at PPC 26 → va128 = 7 | 32 = 39
let raw = (7u32 << (31 - 15)) | (1u32 << (31 - 26));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.va128(), 39);
// VA128H = 1 at PPC 21 → va128 += 64 = 103
let raw = raw | (1u32 << (31 - 21));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.va128(), 7 | 32 | 64);
}
#[test]
fn vmx128_vb128_uses_bits30_31() {
// vb_lo = 5 at PPC 16-20. VB128h = 0b01 (LSB at PPC 31 = host 0) → +32.
// VB128h = 0b11 → +96.
let raw = (5u32 << (31 - 20)) | (1u32 << (31 - 31));
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vb128(), 5 | 32);
let raw = raw | (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 hi = (r >> 5) & 0x3;
let raw = (lo << (31 - 10))
| (hi << (31 - 29));
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.
// vd_lo=5, vd_hi=0b11 → vd128 = 5 | 96 = 101
let raw = vmx128_test_word(5, 3, 0, 0, 0, 0, 0);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vd128(), 5 | 32 | 64);
}
#[test]
fn vx128_5_sh_bit_positions() {
// SH=8 (binary 1000): bit 3 = 1, bits 0-2 = 0.
// Host bit 9 = 1 (PPC bit 22), host bits 6-8 = 0.
// So raw bit 9 set = raw |= 1 << 9 = 0x200
let raw = 0x200u32; // host bit 9 set only
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vx128_5_sh(), 8, "SH=8: MSB at PPC bit 22");
// SH=1 (binary 0001): host bit 6 set = raw |= 1 << 6 = 0x40
let raw = 0x40u32;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vx128_5_sh(), 1, "SH=1: LSB at PPC bit 25");
// SH=15 (binary 1111): host bits 6-9 all set = raw |= 0xF << 6 = 0x3C0
let raw = 0x3C0u32;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vx128_5_sh(), 15, "SH=15: all 4 bits set");
// SH=0: raw=0
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
assert_eq!(d.vx128_5_sh(), 0, "SH=0");
}
#[test]
fn vx128_4_accessors_correct_bit_positions() {
// z=3 (binary 11) at PPC bits 24-25 = host bits 6-7
let raw = 0b11u32 << 6;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vx128_4_z(), 3, "z=3 from host bits 6-7");
// IMM=0x15 (binary 10101) at PPC bits 11-15 = host bits 16-20
let raw2 = 0x15u32 << 16;
let d2 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw2, addr: 0 };
assert_eq!(d2.vx128_4_imm(), 0x15, "IMM=0x15 from host bits 16-20");
// Combined: z=1, IMM=0xA — fields must not bleed into each other
let raw3 = (0x1u32 << 6) | (0xAu32 << 16);
let d3 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw3, addr: 0 };
assert_eq!(d3.vx128_4_z(), 1, "z=1 combined");
assert_eq!(d3.vx128_4_imm(), 0xA, "IMM=0xA combined");
// z=2, IMM=0xF — max 4-bit blend mask, exercises the full lower nibble
let raw4 = (0b10u32 << 6) | (0xFu32 << 16);
let d4 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: raw4, addr: 0 };
assert_eq!(d4.vx128_4_z(), 2, "z=2 from binary 10");
assert_eq!(d4.vx128_4_imm(), 0xF, "IMM=0xF all-ones nibble");
}
#[test]
fn vc128_2_extracts_ppc_bits_23_25() {
// VC=5 (binary 101) at PPC bits 23-25 = host bits 6-8
// extract_bits(raw, 23, 25) = (raw >> (31-25)) & 0x7 = (raw >> 6) & 0x7
let raw = 5u32 << 6; // host bits 6-8 = 5
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vc128_2(), 5);
let d0 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
assert_eq!(d0.vc128_2(), 0);
let d7 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 7u32 << 6, addr: 0 };
assert_eq!(d7.vc128_2(), 7);
let d1 = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 1u32 << 6, addr: 0 };
assert_eq!(d1.vc128_2(), 1);
}
#[test]
fn vx128_p_perm_assembles_correctly() {
// PERMl=0x1F (all 5 bits set) at host bits 16-20: raw = 0x1F << 16
let raw = 0x1Fu32 << 16;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vx128_p_perm(), 0x1F, "PERMl only");
// PERMh=0x7 (all 3 bits set) at host bits 6-8: raw = 0x7 << 6 = 0x1C0
let raw = 0x7u32 << 6;
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vx128_p_perm(), 0x7 << 5, "PERMh only: bits 5-7");
// PERMl=0xA, PERMh=0x5: raw = (0xA << 16) | (0x5 << 6)
let raw = (0xAu32 << 16) | (0x5u32 << 6);
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw, addr: 0 };
assert_eq!(d.vx128_p_perm(), 0xA | (0x5 << 5));
// PERMl and PERMh bits must not bleed into each other
let d = DecodedInstr { opcode: PpcOpcode::Invalid, raw: 0, addr: 0 };
assert_eq!(d.vx128_p_perm(), 0);
}
}