diff --git a/crates/xenia-cpu/examples/decode_table_check.rs b/crates/xenia-cpu/examples/decode_table_check.rs new file mode 100644 index 0000000..7445cf6 --- /dev/null +++ b/crates/xenia-cpu/examples/decode_table_check.rs @@ -0,0 +1,37 @@ +//! Cross-check our decoder against xenia-canary's authoritative encoding table. +//! +//! Canary's `ppc_opcode_table_gen.cc` lists, for every opcode it knows, a +//! representative instruction word with the operand fields zeroed. Feeding each +//! word to our decoder must yield the matching opcode — anything else is a hole +//! or a mis-decode in our tables. +//! +//! ```text +//! cargo run --release -p xenia-cpu --example decode_table_check -- +//! ``` +//! where each line is `0xWORD name`. +use std::io::BufRead; + +fn main() -> Result<(), Box> { + let path = std::env::args().nth(1).ok_or("usage: decode_table_check ")?; + let f = std::io::BufReader::new(std::fs::File::open(path)?); + let (mut ok, mut bad, mut invalid) = (0u32, 0u32, 0u32); + for line in f.lines() { + let line = line?; + let mut it = line.split_whitespace(); + let (Some(w), Some(name)) = (it.next(), it.next()) else { continue }; + let word = u32::from_str_radix(w.trim_start_matches("0x"), 16)?; + let d = xenia_cpu::decoder::decode(word, 0x8200_0000); + let got = format!("{:?}", d.opcode); + if got == name { + ok += 1; + } else if got == "Invalid" { + invalid += 1; + println!("MISSING {w} {name:<14} -> Invalid"); + } else { + bad += 1; + println!("MISMATCH {w} {name:<14} -> {got}"); + } + } + println!("\nmatched {ok}, mismatched {bad}, missing {invalid}"); + Ok(()) +} diff --git a/crates/xenia-cpu/src/disasm.rs b/crates/xenia-cpu/src/disasm.rs index 352219c..2fa107a 100644 --- a/crates/xenia-cpu/src/disasm.rs +++ b/crates/xenia-cpu/src/disasm.rs @@ -127,6 +127,7 @@ fn trap_cond(to: u32) -> Option<&'static str> { match to { 1 => Some("lgt"), 2 => Some("llt"), + 3 => Some("lne"), 4 => Some("eq"), 5 => Some("lge"), 6 => Some("lle"), @@ -298,7 +299,7 @@ pub fn format(instr: &DecodedInstr) -> DisasmText { // ── Special register moves ───────────────────────────────────────── PpcOpcode::mfspr => fmt_mfspr(instr), PpcOpcode::mtspr => fmt_mtspr(instr), - PpcOpcode::mfcr => base("mfcr", gpr(instr.rd()), 8), + PpcOpcode::mfcr => fmt_mfcr(instr), PpcOpcode::mtcrf => fmt_mtcrf(instr), PpcOpcode::mfmsr => base("mfmsr", gpr(instr.rd()), 8), PpcOpcode::mtmsr => base("mtmsr", gpr(instr.rs()), 8), @@ -441,6 +442,16 @@ pub fn format(instr: &DecodedInstr) -> DisasmText { // ── Standard VMX (5-bit registers) ──────────────────────────────── // 3-operand VD, VA, VB + // `vor vD,vA,vA` is the canonical vector register move, and + // `vnor vD,vA,vA` the canonical vector complement. Both are extremely + // common (1,535 and 9 sites here) and both read as noise in base form. + PpcOpcode::vor if instr.ra() == instr.rb() => { + fmt_vmx_move(instr, "vor", "vmr") + } + PpcOpcode::vnor if instr.ra() == instr.rb() => { + fmt_vmx_move(instr, "vnor", "vnot") + } + PpcOpcode::vaddubm | PpcOpcode::vmaxub | PpcOpcode::vrlb | PpcOpcode::vmuloub | PpcOpcode::vaddfp | PpcOpcode::vmrghb | PpcOpcode::vpkuhum | PpcOpcode::vadduhm | PpcOpcode::vmaxuh | PpcOpcode::vrlh | PpcOpcode::vmulouh | @@ -468,6 +479,7 @@ pub fn format(instr: &DecodedInstr) -> DisasmText { fmt_vmx_3op(instr, opcode_name(instr.opcode)) } + // VMX unary VD, VB PpcOpcode::vrefp | PpcOpcode::vrsqrtefp | PpcOpcode::vexptefp | PpcOpcode::vlogefp | PpcOpcode::vrfin | PpcOpcode::vrfiz | @@ -799,6 +811,33 @@ fn fmt_b(instr: &DecodedInstr) -> DisasmText { with_target(base(mnem, ops, 8), target) } +/// Static branch-prediction hint suffix for a `BO` field. +/// +/// PowerISA gives several `BO` encodings an `at` pair — `001at`, `011at`, +/// `1a00t`, `1a01t` — where `at=0b10` means "unlikely" (`-`) and `0b11` means +/// "likely" (`+`); `0b00` is "no hint" and `0b01` is reserved. The forms whose +/// low bit is the reserved `z` (`0000z`, `0001z`, `0100z`, `0101z`) carry no +/// hint at all. Dropping the suffix loses the compiler's static prediction, +/// which is the only place it is recorded. +fn hint_suffix(bo: u32) -> &'static str { + let b = |i: u32| (bo >> (4 - i)) & 1; // b(0) is the MSB of the 5-bit field + let at = match (b(0), b(2), b(3)) { + // 1z1zz — branch always, no hint. + (1, 1, _) => return "", + // 1a00t / 1a01t — the `a` bit is b1. + (1, 0, _) => (b(1) << 1) | b(4), + // 001at / 011at — the `a` bit is b3. + (0, 1, _) => (b(3) << 1) | b(4), + // 0000z / 0001z / 0100z / 0101z — low bit reserved, no hint. + _ => return "", + }; + match at { + 0b10 => "-", + 0b11 => "+", + _ => "", + } +} + fn fmt_bc(instr: &DecodedInstr) -> DisasmText { let bo = instr.bo(); let bi = instr.bi(); @@ -818,6 +857,7 @@ fn fmt_bc(instr: &DecodedInstr) -> DisasmText { let decr = bo & 0x04 == 0; let uncond = bo & 0x10 != 0; + let hint = hint_suffix(bo); let result = if uncond && !decr { // Unconditional branch. let ext_mnem = format!("b{a}{l}"); @@ -836,15 +876,26 @@ fn fmt_bc(instr: &DecodedInstr) -> DisasmText { if decr { let z = if bo & 0x02 != 0 { "z" } else { "nz" }; - // BO bit 4 (uncond) means CR is ignored — pure CTR-decrement branch. - // Without this guard, bdnz/bdz would emit a spurious `ge` suffix derived - // from the don't-care BI=0 / cond_true=false pair (PPCBUG-640). - let cond_str = if uncond { "" } else { cond_name_opt.unwrap_or("") }; - let ext_mnem = format!("bd{z}{cond_str}{a}{l}"); - let ext_ops = format!("{cr}0x{target:08X}"); - with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8) + if uncond { + // BO bit 4 set means CR is ignored — a pure CTR-decrement + // branch. Without this guard bdnz/bdz would emit a spurious + // `ge` suffix derived from the don't-care BI=0 / + // cond_true=false pair (PPCBUG-640). + let ext_mnem = format!("bd{z}{a}{l}{hint}"); + let ext_ops = format!("0x{target:08X}"); + with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8) + } else { + // Combined CTR + condition. PowerISA names these `bdnzt` / + // `bdnzf` / `bdzt` / `bdzf` with the CR bit as an operand — + // not a condition-suffixed `bdnzne`, which is an invention no + // assembler accepts. + let t = if cond_true { "t" } else { "f" }; + let ext_mnem = format!("bd{z}{t}{a}{l}{hint}"); + let ext_ops = format!("{}, 0x{target:08X}", crb(bi)); + with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8) + } } else if let Some(cond_name) = cond_name_opt { - let ext_mnem = format!("b{cond_name}{a}{l}"); + let ext_mnem = format!("b{cond_name}{a}{l}{hint}"); let ext_ops = format!("{cr}0x{target:08X}"); with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8) } else { @@ -861,6 +912,7 @@ fn fmt_bclr(instr: &DecodedInstr) -> DisasmText { let l = if lk { "l" } else { "" }; let base_mnem = format!("bclr{l}"); let base_ops = format!("{bo}, {}", crb(bi)); + let hint = hint_suffix(bo); // BO=20 (binary 10100) sets both "ignore CTR" and "ignore CR" bits, making // the branch unconditional regardless of BI. BI is don't-care by spec, so @@ -871,7 +923,7 @@ fn fmt_bclr(instr: &DecodedInstr) -> DisasmText { } if let Some((cond, cr)) = cond_branch_ext(bo, bi) { let cr_no_comma = cr.trim_end_matches(", "); - let ext_mnem = format!("b{cond}lr{l}"); + let ext_mnem = format!("b{cond}lr{l}{hint}"); if cr_no_comma.is_empty() { return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0); } else { @@ -882,7 +934,7 @@ fn fmt_bclr(instr: &DecodedInstr) -> DisasmText { let uncond = bo & 0x10 != 0; if decr && uncond { let z = if bo & 0x02 != 0 { "z" } else { "nz" }; - let ext_mnem = format!("bd{z}lr{l}"); + let ext_mnem = format!("bd{z}lr{l}{hint}"); return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0); } base(&base_mnem, base_ops, 8) @@ -895,6 +947,7 @@ fn fmt_bcctr(instr: &DecodedInstr) -> DisasmText { let l = if lk { "l" } else { "" }; let base_mnem = format!("bcctr{l}"); let base_ops = format!("{bo}, {}", crb(bi)); + let hint = hint_suffix(bo); // BO=20 unconditional pattern: BI is don't-care (see fmt_bclr). if bo == 20 { @@ -903,7 +956,7 @@ fn fmt_bcctr(instr: &DecodedInstr) -> DisasmText { } if let Some((cond, cr)) = cond_branch_ext(bo, bi) { let cr_no_comma = cr.trim_end_matches(", "); - let ext_mnem = format!("b{cond}ctr{l}"); + let ext_mnem = format!("b{cond}ctr{l}{hint}"); if cr_no_comma.is_empty() { return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0); } else { @@ -921,7 +974,12 @@ fn fmt_trap_imm(instr: &DecodedInstr, mnem: &str, simplified_prefix: &str) -> Di let base_ops = format!("{to}, {}, {imm}", gpr(ra)); if let Some(cond) = trap_cond(to) { if cond.is_empty() { - base(mnem, base_ops, 8) + // TO=31 traps unconditionally. The register form has `trap`; the + // immediate form's counterpart is `twui`/`tdui` (binutils), which + // is what every other disassembler prints for these 16 sites. + let ext_mnem = format!("{simplified_prefix}ui"); + let ext_ops = format!("{}, {imm}", gpr(ra)); + with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8) } else { let ext_mnem = format!("{simplified_prefix}{cond}i"); let ext_ops = format!("{}, {imm}", gpr(ra)); @@ -1118,7 +1176,13 @@ fn fmt_rlwinm(instr: &DecodedInstr) -> DisasmText { let base_ops = format!("{}, {}, {sh}, {mb}, {me}", gpr(ra), gpr(rs)); // Priority-ordered simplified forms. - if sh > 0 && mb == 0 && me == 31 - sh { + // + // `slwi` is deliberately not gated on `sh > 0`: `rlwinm rA,rS,0,0,31` is a + // rotate-by-zero under a full mask, which the ISA's table still names + // `slwi rA,rS,0` (and which LLVM/capstone print that way). It is the single + // most common `rlwinm` encoding in this binary — 3,720 sites — so gating it + // away left the largest simplified-mnemonic gap we had. + if mb == 0 && me == 31 - sh { let ext = format!("slwi{rc}"); return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8); } @@ -1435,9 +1499,32 @@ fn fmt_mtspr(instr: &DecodedInstr) -> DisasmText { } } +/// `mfcr` and `mfocrf` share XO=19 and are told apart by bit 11. +/// +/// With bit 11 clear the instruction copies the whole CR into `rD`; with it set +/// this is `mfocrf`, which copies only the single CR field named by `FXM` and +/// leaves the rest of `rD` undefined. Printing the latter as a bare `mfcr rD` +/// loses which field was read — and on this title 163 of 165 sites are the +/// one-field form. (The reference emulator folds both into one handler because +/// the wide read is a safe superset at runtime; a disassembler cannot.) +fn fmt_mfcr(instr: &DecodedInstr) -> DisasmText { + let rd = instr.rd(); + if instr.raw & (1 << 20) != 0 { + let fxm = (instr.raw >> 12) & 0xFF; + base("mfocrf", format!("{}, 0x{fxm:02X}", gpr(rd)), 8) + } else { + base("mfcr", gpr(rd), 8) + } +} + +/// `mtcrf` and `mtocrf` share XO=144, told apart by bit 11 exactly as +/// [`fmt_mfcr`] describes. fn fmt_mtcrf(instr: &DecodedInstr) -> DisasmText { let rs = instr.rs(); let fxm = (instr.raw >> 12) & 0xFF; + if instr.raw & (1 << 20) != 0 { + return base("mtocrf", format!("0x{fxm:02X}, {}", gpr(rs)), 8); + } let bo = format!("0x{fxm:02X}, {}", gpr(rs)); if fxm == 0xFF { with_ext("mtcrf", bo, 8, "mtcr", gpr(rs), 8) @@ -1597,6 +1684,20 @@ fn fmt_mtfsb(instr: &DecodedInstr, mnem: &str) -> DisasmText { } // VMX (5-bit registers). +/// A VX-form op whose two sources are the same register, so it degenerates to +/// a move/complement: emit the base form plus the two-operand simplified one. +fn fmt_vmx_move(instr: &DecodedInstr, base_mnem: &str, ext_mnem: &str) -> DisasmText { + let vd = instr.rd(); + let va = instr.ra(); + let vb = instr.rb(); + with_ext( + base_mnem, + format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 8, + ext_mnem, + format!("{}, {}", vr(vd), vr(va)), 8, + ) +} + fn fmt_vmx_3op(instr: &DecodedInstr, mnem: &str) -> DisasmText { let vd = instr.rd(); let va = instr.ra(); @@ -1875,4 +1976,153 @@ mod tests { let items: Vec<_> = super::iter_disasm(&bytes, 0, 0, 6).collect(); assert_eq!(items.len(), 1); } + /// `mfocrf` shares XO=19 with `mfcr`, differing only in bit 11. Printing it + /// as `mfcr` drops the FXM field naming which CR field is actually read — + /// 163 of 165 sites in the reference title are this form. + #[test] + fn mfocrf_is_distinguished_from_mfcr() { + // 0x7d502026: mfocrf r10, 0x02 (bit 11 set) + let d = crate::decoder::decode(0x7d50_2026, 0x8200_0000); + let t = format(&d); + assert_eq!(t.mnemonic, "mfocrf"); + assert_eq!(t.operands, "r10, 0x02"); + + // Same encoding with bit 11 clear is a plain whole-CR read. + let d = crate::decoder::decode(0x7d50_2026 & !(1 << 20), 0x8200_0000); + let t = format(&d); + assert_eq!(t.mnemonic, "mfcr"); + assert_eq!(t.operands, "r10"); + } + + /// The mirror case on the write side. + #[test] + fn mtocrf_is_distinguished_from_mtcrf() { + let base_word = 0x7c10_1120u32; // mtcrf-form, XO=144 + let d = crate::decoder::decode(base_word | (1 << 20), 0x8200_0000); + assert_eq!(format(&d).mnemonic, "mtocrf"); + let d = crate::decoder::decode(base_word & !(1 << 20), 0x8200_0000); + assert_eq!(format(&d).mnemonic, "mtcrf"); + } + + /// `rlwinm rA,rS,0,0,31` is a rotate-by-zero under a full mask. It is the + /// most common `rlwinm` encoding in the reference title (3,720 sites) and + /// was falling through to the base form because every simplified branch was + /// gated on `sh > 0`. + #[test] + fn rlwinm_shift_zero_still_simplifies() { + // 0x5548003e: rlwinm r8, r10, 0, 0, 31 + let d = crate::decoder::decode(0x5548_003e, 0x8200_0000); + let t = format(&d); + assert_eq!(t.mnemonic, "rlwinm"); + assert_eq!(t.ext_mnemonic.as_deref(), Some("slwi")); + assert_eq!(t.ext_operands.as_deref(), Some("r8, r10, 0")); + + // The record-bit form keeps its dot. + let d = crate::decoder::decode(0x5569_003f, 0x8200_0000); + assert_eq!(format(&d).ext_mnemonic.as_deref(), Some("slwi.")); + } + + /// A genuine bit-extraction has no short name and must stay in base form. + #[test] + fn rlwinm_bit_extract_has_no_simplified_form() { + // rlwinm rA,rS,0,30,30 — extracts one bit; not a shift or clear. + let word = 0x5548_0000 | (0 << 11) | (30 << 6) | (30 << 1); + let t = format(&crate::decoder::decode(word, 0x8200_0000)); + assert_eq!(t.mnemonic, "rlwinm"); + assert_eq!(t.ext_mnemonic, None); + } + + /// `vor vD,vA,vA` is the vector register move; `vnor vD,vA,vA` the vector + /// complement. Both only apply when the two sources are the same register. + #[test] + fn vor_and_vnor_simplify_only_when_sources_match() { + // 0x11800484: vor v12, v0, v0 + let t = format(&crate::decoder::decode(0x1180_0484, 0x8200_0000)); + assert_eq!(t.mnemonic, "vor"); + assert_eq!(t.ext_mnemonic.as_deref(), Some("vmr")); + assert_eq!(t.ext_operands.as_deref(), Some("v12, v0")); + + // 0x10000504: vnor v0, v0, v0 + let t = format(&crate::decoder::decode(0x1000_0504, 0x8200_0000)); + assert_eq!(t.ext_mnemonic.as_deref(), Some("vnot")); + + // Distinct sources: a real bitwise OR, no simplification. + let t = format(&crate::decoder::decode(0x1180_1484, 0x8200_0000)); + assert_eq!(t.mnemonic, "vor"); + assert_eq!(t.ext_mnemonic, None); + } + + /// PowerISA names the combined CTR+condition branches `bdnzt`/`bdnzf` + /// (and `bdzt`/`bdzf`) with the CR bit as an operand. We used to synthesise + /// `bdnzne` by gluing on a condition suffix — readable, but not a mnemonic + /// any assembler accepts. + #[test] + fn bdnz_with_condition_uses_the_isa_t_f_form() { + // 0x4002fff8: BO=00000 (dec CTR, branch if CTR!=0 and CR[BI]==0), BI=eq + let t = format(&crate::decoder::decode(0x4002_fff8, 0x8200_0000)); + assert_eq!(t.ext_mnemonic.as_deref(), Some("bdnzf")); + assert!(t.ext_operands.as_deref().unwrap().starts_with("eq,")); + } + + /// The `at` hint bits are the only record of the compiler's static branch + /// prediction, so they must survive into the text. + #[test] + fn branch_prediction_hints_are_preserved() { + // 0x4320fff0: bdnz with at=0b11 -> "+" + assert_eq!( + format(&crate::decoder::decode(0x4320_fff0, 0x8200_0000)).ext_mnemonic.as_deref(), + Some("bdnz+") + ); + // 0x41c20024: beq with at=0b10 -> "-" + assert_eq!( + format(&crate::decoder::decode(0x41c2_0024, 0x8200_0000)).ext_mnemonic.as_deref(), + Some("beq-") + ); + // 0x4de20020: beqlr with at=0b11 -> "+" + assert_eq!( + format(&crate::decoder::decode(0x4de2_0020, 0x8200_0000)).ext_mnemonic.as_deref(), + Some("beqlr+") + ); + } + + /// A branch with no hint bits set must stay unsuffixed, and `blr` (BO=20, + /// the branch-always form) never takes a hint at all. + #[test] + fn unhinted_branches_gain_no_suffix() { + // 0x4182000c: beq, at=0b00 + let t = format(&crate::decoder::decode(0x4182_000c, 0x8200_0000)); + assert_eq!(t.ext_mnemonic.as_deref(), Some("beq")); + // 0x4e800020: blr + let t = format(&crate::decoder::decode(0x4e80_0020, 0x8200_0000)); + assert_eq!(t.ext_mnemonic.as_deref(), Some("blr")); + } + + /// The whole trap family, checked against the reference table: TO=31 is + /// unconditional, which the register form calls `trap` and the immediate + /// form `twui` — the latter was the one gap. + #[test] + fn trap_extended_mnemonics_cover_the_table() { + // 0x0fe00016: twi 31, r0, 22 -> unconditional + let t = format(&crate::decoder::decode(0x0fe0_0016, 0x8200_0000)); + assert_eq!(t.mnemonic, "twi"); + assert_eq!(t.ext_mnemonic.as_deref(), Some("twui")); + + // TO=6 is "logically less than or equal" — the divide-by-zero guard + // MSVC emits, and the most common trap in the reference title. + let word = 0x0c00_0000 | (6 << 21) | (3 << 16) | 0; + assert_eq!( + format(&crate::decoder::decode(word, 0x8200_0000)).ext_mnemonic.as_deref(), + Some("twllei") + ); + // TO=5 is "logically greater than or equal". + let word = 0x0c00_0000 | (5 << 21) | (3 << 16) | 0; + assert_eq!( + format(&crate::decoder::decode(word, 0x8200_0000)).ext_mnemonic.as_deref(), + Some("twlgei") + ); + // tw 31,0,0 stays the register-form `trap`. + let t = format(&crate::decoder::decode(0x7fe0_0008, 0x8200_0000)); + assert_eq!(t.ext_mnemonic.as_deref(), Some("trap")); + } + }