Files
xenia-rs/crates/xenia-cpu/src/disasm.rs
MechaCat02 2be25bdd41 fix(disasm): PPCBUG-641+649 sync/lwsync L-field discrimination
PPCBUG-641: PpcOpcode::sync emitted "sync" regardless of the L-field at
PPC bit 10. The Xbox 360 acquire barrier (encoding 0x7C2004AC, L=1) is
lwsync, used in every spinlock. The disassembly DB stored every lwsync
as `mnemonic='sync'`, so `SELECT WHERE mnemonic='lwsync'` returned zero
rows regardless of binary content.

PPCBUG-649 (companion): the golden fixture for lwsync had no ext_mnemonic
field, pinning the wrong output and defeating regression detection.

Fix: in disasm.rs, gate on `(instr.raw >> 21) & 1` (PPC bit 10) — when
set, emit the lwsync extended form. Update extended_mnemonics.json
fixture to expect `ext_mnemonic: "lwsync"`.

Note: this is the disassembler-side fix only. The interpreter-side
PPCBUG-088 (lwsync vs sync semantics) is separate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-02 10:43:24 +02:00

1879 lines
77 KiB
Rust

//! PowerPC (Xbox 360 Xenon) text disassembler.
//!
//! Single source of truth for assembly text formatting. Sits on top of the
//! canonical decoder in [`crate::decoder`] and consumes [`DecodedInstr`]
//! (8-byte `Copy`, no allocations) so the interpreter's decode cache stays
//! lean — formatting allocates, but only when a sink calls [`format`].
//!
//! [`format`] returns a [`DisasmText`] carrying both base and extended
//! (simplified) mnemonic forms. Callers (text printer, JSON sink, DuckDB
//! row writer) consume the fields directly instead of re-parsing.
use crate::decoder::{DecodedInstr, extract_vx128_uimm5};
use crate::opcode::PpcOpcode;
/// Formatted disassembly of a single instruction.
///
/// Owns its strings. `mnemonic`/`operands` are the structured base form
/// (e.g. `"addi"`, `"r3, r1, 16"`); `disasm` is the legacy padded display
/// form (e.g. `"addi r3, r1, 16"`). The `ext_*` triple is `Some` when
/// a simplified/extended mnemonic applies (e.g. `addi r3,0,imm` →
/// `li r3, imm`). `branch_target` is the resolved absolute target for
/// direct branches (`b`/`bl`/`bc`/`bcl`); `None` for indirect branches
/// and non-branches.
#[derive(Debug, Clone)]
pub struct DisasmText {
pub mnemonic: String,
pub operands: String,
pub disasm: String,
pub ext_mnemonic: Option<String>,
pub ext_operands: Option<String>,
pub ext_disasm: Option<String>,
pub branch_target: Option<u32>,
}
impl DisasmText {
/// Preferred display form: extended if present, else base.
#[inline]
pub fn display(&self) -> &str {
self.ext_disasm.as_deref().unwrap_or(&self.disasm)
}
}
// ── Internal builders ───────────────────────────────────────────────────────
#[inline]
fn pad_into(mnem: &str, operands: &str, width: usize) -> String {
if width <= mnem.len() + 1 {
// No padding fits — fall back to single-space join.
if operands.is_empty() { mnem.to_string() }
else { format!("{mnem} {operands}") }
} else {
format!("{:<width$}{}", mnem, operands, width = width)
}
}
fn base(mnem: &str, operands: String, pad: usize) -> DisasmText {
let disasm = pad_into(mnem, &operands, pad);
DisasmText {
mnemonic: mnem.to_string(),
operands,
disasm,
ext_mnemonic: None,
ext_operands: None,
ext_disasm: None,
branch_target: None,
}
}
fn with_ext(
base_mnem: &str, base_ops: String, base_pad: usize,
ext_mnem: &str, ext_ops: String, ext_pad: usize,
) -> DisasmText {
let disasm = pad_into(base_mnem, &base_ops, base_pad);
let ext_disasm = pad_into(ext_mnem, &ext_ops, ext_pad);
DisasmText {
mnemonic: base_mnem.to_string(),
operands: base_ops,
disasm,
ext_mnemonic: Some(ext_mnem.to_string()),
ext_operands: Some(ext_ops),
ext_disasm: Some(ext_disasm),
branch_target: None,
}
}
fn with_target(mut t: DisasmText, target: u32) -> DisasmText {
t.branch_target = Some(target);
t
}
fn long_word(raw: u32) -> DisasmText {
let operands = format!("0x{raw:08X}");
base(".long", operands, 8)
}
// ── Helpers (register names, sign extension, condition decoding) ────────────
#[inline] fn gpr(r: usize) -> String { format!("r{r}") }
#[inline] fn fpr(r: usize) -> String { format!("f{r}") }
#[inline] fn vr(r: usize) -> String { format!("v{r}") }
fn crb(b: u32) -> String {
let cr = b / 4;
let bit = b % 4;
let bit_name = ["lt", "gt", "eq", "so"][bit as usize];
if cr == 0 { bit_name.to_string() } else { format!("4*cr{cr}+{bit_name}") }
}
fn spr_name(spr: u32) -> String {
match spr {
1 => "XER".into(),
8 => "LR".into(),
9 => "CTR".into(),
_ => format!("spr{spr}"),
}
}
#[inline] fn sign_ext(val: u32, bits: u32) -> i32 {
let shift = 32 - bits;
((val << shift) as i32) >> shift
}
/// Map trap TO field to condition suffix (e.g. 16 → "lt", 4 → "eq").
/// Unsigned variants (`lgt`/`llt`/`lge`/`lle`) cover bits 1-3 of the TO
/// encoding which `tw`/`td` use for logical-compare conditions.
fn trap_cond(to: u32) -> Option<&'static str> {
match to {
1 => Some("lgt"),
2 => Some("llt"),
4 => Some("eq"),
5 => Some("lge"),
6 => Some("lle"),
8 => Some("gt"),
12 => Some("ge"),
16 => Some("lt"),
20 => Some("le"),
24 => Some("ne"),
31 => Some(""), // unconditional
_ => None,
}
}
/// For non-decrementing conditional branches: returns Some((cond_name, cr_prefix))
/// where cr_prefix is e.g. "" or "cr2, ".
fn cond_branch_ext(bo: u32, bi: u32) -> Option<(&'static str, String)> {
let cond_true = bo & 0x08 != 0;
let no_cond = bo & 0x10 != 0;
let decr = bo & 0x04 == 0;
if no_cond || decr { return None; }
let cr_field = bi / 4;
let cr_bit = bi % 4;
let cond_name = match (cr_bit, cond_true) {
(0, true) => "lt", (0, false) => "ge",
(1, true) => "gt", (1, false) => "le",
(2, true) => "eq", (2, false) => "ne",
(3, true) => "so", (3, false) => "ns",
_ => return None,
};
let cr = if cr_field == 0 { String::new() } else { format!("cr{cr_field}, ") };
Some((cond_name, cr))
}
#[inline] fn rc_dot(instr: &DecodedInstr) -> &'static str {
if instr.rc_bit() { "." } else { "" }
}
// ── Public entrypoints ──────────────────────────────────────────────────────
/// Format a decoded instruction into structured disassembly text.
pub fn format(instr: &DecodedInstr) -> DisasmText {
match instr.opcode {
// ── Branch ──────────────────────────────────────────────────────────
PpcOpcode::bx => fmt_b(instr),
PpcOpcode::bcx => fmt_bc(instr),
PpcOpcode::bclrx => fmt_bclr(instr),
PpcOpcode::bcctrx => fmt_bcctr(instr),
PpcOpcode::sc => base("sc", String::new(), 0),
// ── Trap ────────────────────────────────────────────────────────────
PpcOpcode::tdi => fmt_trap_imm(instr, "tdi", "td"),
PpcOpcode::twi => fmt_trap_imm(instr, "twi", "tw"),
PpcOpcode::td => fmt_trap_reg(instr, "td"),
PpcOpcode::tw => fmt_trap_reg(instr, "tw"),
// ── D-form ALU/logical ──────────────────────────────────────────────
PpcOpcode::addi => fmt_addi(instr),
PpcOpcode::addis => fmt_addis(instr),
PpcOpcode::addic => fmt_d_add(instr, "addic"),
PpcOpcode::addicx => fmt_d_add(instr, "addic."),
PpcOpcode::subficx => fmt_d_imm_simple(instr, "subfic"),
PpcOpcode::mulli => fmt_d_imm_simple(instr, "mulli"),
PpcOpcode::cmpi => fmt_cmp_imm(instr, "cmpi", true),
PpcOpcode::cmpli => fmt_cmp_imm(instr, "cmpli", false),
PpcOpcode::ori => fmt_ori(instr),
PpcOpcode::oris => fmt_d_logic(instr, "oris"),
PpcOpcode::xori => fmt_d_logic(instr, "xori"),
PpcOpcode::xoris => fmt_d_logic(instr, "xoris"),
PpcOpcode::andix => fmt_d_logic(instr, "andi."),
PpcOpcode::andisx => fmt_d_logic(instr, "andis."),
// ── D-form load/store ───────────────────────────────────────────────
PpcOpcode::lwz => fmt_ld(instr, "lwz", false),
PpcOpcode::lwzu => fmt_ld(instr, "lwzu", false),
PpcOpcode::lbz => fmt_ld(instr, "lbz", false),
PpcOpcode::lbzu => fmt_ld(instr, "lbzu", false),
PpcOpcode::lhz => fmt_ld(instr, "lhz", false),
PpcOpcode::lhzu => fmt_ld(instr, "lhzu", false),
PpcOpcode::lha => fmt_ld(instr, "lha", false),
PpcOpcode::lhau => fmt_ld(instr, "lhau", false),
PpcOpcode::lmw => fmt_ld(instr, "lmw", false),
PpcOpcode::lfs => fmt_ld(instr, "lfs", true),
PpcOpcode::lfsu => fmt_ld(instr, "lfsu", true),
PpcOpcode::lfd => fmt_ld(instr, "lfd", true),
PpcOpcode::lfdu => fmt_ld(instr, "lfdu", true),
PpcOpcode::stw => fmt_st(instr, "stw", false),
PpcOpcode::stwu => fmt_st(instr, "stwu", false),
PpcOpcode::stb => fmt_st(instr, "stb", false),
PpcOpcode::stbu => fmt_st(instr, "stbu", false),
PpcOpcode::sth => fmt_st(instr, "sth", false),
PpcOpcode::sthu => fmt_st(instr, "sthu", false),
PpcOpcode::stmw => fmt_st(instr, "stmw", false),
PpcOpcode::stfs => fmt_st(instr, "stfs", true),
PpcOpcode::stfsu => fmt_st(instr, "stfsu", true),
PpcOpcode::stfd => fmt_st(instr, "stfd", true),
PpcOpcode::stfdu => fmt_st(instr, "stfdu", true),
// ── DS-form load/store ──────────────────────────────────────────────
PpcOpcode::ld => fmt_ds(instr, "ld"),
PpcOpcode::ldu => fmt_ds(instr, "ldu"),
PpcOpcode::lwa => fmt_ds(instr, "lwa"),
PpcOpcode::std => fmt_ds(instr, "std"),
PpcOpcode::stdu => fmt_ds(instr, "stdu"),
// ── Rotate ─────────────────────────────────────────────────────────
PpcOpcode::rlwimix => fmt_rlwimi(instr),
PpcOpcode::rlwinmx => fmt_rlwinm(instr),
PpcOpcode::rlwnmx => fmt_rlwnm(instr),
PpcOpcode::rldiclx => fmt_rldicl(instr),
PpcOpcode::rldicrx => fmt_rldicr(instr),
PpcOpcode::rldicx => fmt_rldic(instr),
PpcOpcode::rldimix => fmt_rldimi(instr),
PpcOpcode::rldclx => fmt_rldcl(instr),
PpcOpcode::rldcrx => fmt_rldcr(instr),
// ── Compare (X-form) ───────────────────────────────────────────────
PpcOpcode::cmp => fmt_cmp_reg(instr, "cmp"),
PpcOpcode::cmpl => fmt_cmp_reg(instr, "cmpl"),
// ── X-form ALU (3-register) with OE/Rc ─────────────────────────────
PpcOpcode::addx => fmt_xo_3op(instr, "add"),
PpcOpcode::addcx => fmt_xo_3op(instr, "addc"),
PpcOpcode::addex => fmt_xo_3op(instr, "adde"),
PpcOpcode::addmex => fmt_xo_2op(instr, "addme"),
PpcOpcode::addzex => fmt_xo_2op(instr, "addze"),
PpcOpcode::subfx => fmt_subf(instr, "subf", "sub"),
PpcOpcode::subfcx => fmt_subf(instr, "subfc", "subc"),
PpcOpcode::subfex => fmt_xo_3op(instr, "subfe"),
PpcOpcode::subfmex => fmt_xo_2op(instr, "subfme"),
PpcOpcode::subfzex => fmt_xo_2op(instr, "subfze"),
PpcOpcode::negx => fmt_xo_2op(instr, "neg"),
PpcOpcode::mullwx => fmt_xo_3op(instr, "mullw"),
PpcOpcode::mulhwx => fmt_xo_3op_no_oe(instr, "mulhw"),
PpcOpcode::mulhwux => fmt_xo_3op_rc_only(instr, "mulhwu"),
PpcOpcode::divwx => fmt_xo_3op(instr, "divw"),
PpcOpcode::divwux => fmt_xo_3op(instr, "divwu"),
PpcOpcode::mulldx => fmt_xo_3op(instr, "mulld"),
PpcOpcode::mulhdx => fmt_xo_3op_rc_only(instr, "mulhd"),
PpcOpcode::mulhdux => fmt_xo_3op_rc_only(instr, "mulhdu"),
PpcOpcode::divdx => fmt_xo_3op(instr, "divd"),
PpcOpcode::divdux => fmt_xo_3op(instr, "divdu"),
// ── X-form logical (Rc) ────────────────────────────────────────────
PpcOpcode::andx => fmt_logic_and(instr),
PpcOpcode::andcx => fmt_x_logic(instr, "andc"),
PpcOpcode::orx => fmt_logic_or(instr),
PpcOpcode::orcx => fmt_x_logic(instr, "orc"),
PpcOpcode::xorx => fmt_x_logic(instr, "xor"),
PpcOpcode::norx => fmt_logic_nor(instr),
PpcOpcode::nandx => fmt_x_logic(instr, "nand"),
PpcOpcode::eqvx => fmt_x_logic(instr, "eqv"),
PpcOpcode::extsbx => fmt_x_unary_rc(instr, "extsb"),
PpcOpcode::extshx => fmt_x_unary_rc(instr, "extsh"),
PpcOpcode::extswx => fmt_x_unary_rc(instr, "extsw"),
PpcOpcode::cntlzwx => fmt_x_unary_rc(instr, "cntlzw"),
PpcOpcode::cntlzdx => fmt_x_unary_rc(instr, "cntlzd"),
// ── Shift (32 / 64) ─────────────────────────────────────────────────
PpcOpcode::slwx => fmt_x_logic(instr, "slw"),
PpcOpcode::srwx => fmt_x_logic(instr, "srw"),
PpcOpcode::srawx => fmt_x_logic(instr, "sraw"),
PpcOpcode::sldx => fmt_x_logic(instr, "sld"),
PpcOpcode::srdx => fmt_x_logic(instr, "srd"),
PpcOpcode::sradx => fmt_x_logic(instr, "srad"),
PpcOpcode::srawix => fmt_srawi(instr),
PpcOpcode::sradix => fmt_sradi(instr),
// ── Special register moves ─────────────────────────────────────────
PpcOpcode::mfspr => fmt_mfspr(instr),
PpcOpcode::mtspr => fmt_mtspr(instr),
PpcOpcode::mfcr => base("mfcr", gpr(instr.rd()), 8),
PpcOpcode::mtcrf => fmt_mtcrf(instr),
PpcOpcode::mfmsr => base("mfmsr", gpr(instr.rd()), 8),
PpcOpcode::mtmsr => base("mtmsr", gpr(instr.rs()), 8),
PpcOpcode::mtmsrd => base("mtmsrd", gpr(instr.rs()), 8),
PpcOpcode::mftb => fmt_mftb(instr),
PpcOpcode::mcrxr => base("mcrxr", format!("cr{}", instr.crfd()), 8),
PpcOpcode::mcrf => base("mcrf", format!("cr{}, cr{}", instr.crfd(), instr.crfs()), 8),
// ── X-form indexed load/store ──────────────────────────────────────
PpcOpcode::lwzx => fmt_x_load(instr, "lwzx", false),
PpcOpcode::lwzux => fmt_x_load(instr, "lwzux", false),
PpcOpcode::lbzx => fmt_x_load(instr, "lbzx", false),
PpcOpcode::lbzux => fmt_x_load(instr, "lbzux", false),
PpcOpcode::lhzx => fmt_x_load(instr, "lhzx", false),
PpcOpcode::lhzux => fmt_x_load(instr, "lhzux", false),
PpcOpcode::lhax => fmt_x_load(instr, "lhax", false),
PpcOpcode::lhaux => fmt_x_load(instr, "lhaux", false),
PpcOpcode::lwax => fmt_x_load(instr, "lwax", false),
PpcOpcode::lwaux => fmt_x_load(instr, "lwaux", false),
PpcOpcode::ldx => fmt_x_load(instr, "ldx", false),
PpcOpcode::ldux => fmt_x_load(instr, "ldux", false),
PpcOpcode::lwbrx => fmt_x_load(instr, "lwbrx", false),
PpcOpcode::lhbrx => fmt_x_load(instr, "lhbrx", false),
PpcOpcode::ldbrx => fmt_x_load(instr, "ldbrx", false),
PpcOpcode::lwarx => fmt_x_load(instr, "lwarx", false),
PpcOpcode::ldarx => fmt_x_load(instr, "ldarx", false),
PpcOpcode::lswx => fmt_x_load(instr, "lswx", false),
PpcOpcode::lswi => fmt_lswi_stswi(instr, "lswi"),
PpcOpcode::lfsx => fmt_x_load(instr, "lfsx", true),
PpcOpcode::lfsux => fmt_x_load(instr, "lfsux", true),
PpcOpcode::lfdx => fmt_x_load(instr, "lfdx", true),
PpcOpcode::lfdux => fmt_x_load(instr, "lfdux", true),
PpcOpcode::stwx => fmt_x_store(instr, "stwx", false),
PpcOpcode::stwux => fmt_x_store(instr, "stwux", false),
PpcOpcode::stbx => fmt_x_store(instr, "stbx", false),
PpcOpcode::stbux => fmt_x_store(instr, "stbux", false),
PpcOpcode::sthx => fmt_x_store(instr, "sthx", false),
PpcOpcode::sthux => fmt_x_store(instr, "sthux", false),
PpcOpcode::stdx => fmt_x_store(instr, "stdx", false),
PpcOpcode::stdux => fmt_x_store(instr, "stdux", false),
PpcOpcode::stwbrx => fmt_x_store(instr, "stwbrx", false),
PpcOpcode::sthbrx => fmt_x_store(instr, "sthbrx", false),
PpcOpcode::stdbrx => fmt_x_store(instr, "stdbrx", false),
PpcOpcode::stwcx => fmt_x_store(instr, "stwcx.", false),
PpcOpcode::stdcx => fmt_x_store(instr, "stdcx.", false),
PpcOpcode::stswx => fmt_x_store(instr, "stswx", false),
PpcOpcode::stswi => fmt_lswi_stswi(instr, "stswi"),
PpcOpcode::stfsx => fmt_x_store(instr, "stfsx", true),
PpcOpcode::stfsux => fmt_x_store(instr, "stfsux", true),
PpcOpcode::stfdx => fmt_x_store(instr, "stfdx", true),
PpcOpcode::stfdux => fmt_x_store(instr, "stfdux", true),
PpcOpcode::stfiwx => fmt_x_store(instr, "stfiwx", true),
// ── Cache / sync ────────────────────────────────────────────────────
PpcOpcode::dcbf => fmt_cache(instr, "dcbf"),
PpcOpcode::dcbi => fmt_cache(instr, "dcbi"),
PpcOpcode::dcbst => fmt_cache(instr, "dcbst"),
PpcOpcode::dcbt => fmt_cache(instr, "dcbt"),
PpcOpcode::dcbtst => fmt_cache(instr, "dcbtst"),
PpcOpcode::dcbz => fmt_cache(instr, "dcbz"),
PpcOpcode::dcbz128 => fmt_cache(instr, "dcbz128"),
PpcOpcode::icbi => fmt_cache(instr, "icbi"),
PpcOpcode::sync => {
// L-field at PPC bit 10 (host bit 21) selects lwsync (L=1), the
// acquire barrier in every Xbox 360 spinlock. PPCBUG-641.
if (instr.raw >> 21) & 1 == 1 {
with_ext("sync", String::new(), 0, "lwsync", String::new(), 0)
} else {
base("sync", String::new(), 0)
}
}
PpcOpcode::eieio => base("eieio", String::new(), 0),
PpcOpcode::isync => base("isync", String::new(), 0),
// ── CR logical ──────────────────────────────────────────────────────
PpcOpcode::crand => fmt_cr_logic(instr, "crand"),
PpcOpcode::crandc => fmt_cr_logic(instr, "crandc"),
PpcOpcode::creqv => fmt_creqv(instr),
PpcOpcode::crnand => fmt_cr_logic(instr, "crnand"),
PpcOpcode::crnor => fmt_crnor(instr),
PpcOpcode::cror => fmt_cror(instr),
PpcOpcode::crorc => fmt_cr_logic(instr, "crorc"),
PpcOpcode::crxor => fmt_crxor(instr),
// ── FPU (op59 / op63) ──────────────────────────────────────────────
PpcOpcode::fdivsx => fmt_a_3op(instr, "fdivs", false),
PpcOpcode::fsubsx => fmt_a_3op(instr, "fsubs", false),
PpcOpcode::faddsx => fmt_a_3op(instr, "fadds", false),
PpcOpcode::fsqrtsx => fmt_a_unary(instr, "fsqrts"),
PpcOpcode::fresx => fmt_a_unary(instr, "fres"),
PpcOpcode::fmulsx => fmt_a_3op(instr, "fmuls", true),
PpcOpcode::fmsubsx => fmt_a_4op(instr, "fmsubs"),
PpcOpcode::fmaddsx => fmt_a_4op(instr, "fmadds"),
PpcOpcode::fnmsubsx => fmt_a_4op(instr, "fnmsubs"),
PpcOpcode::fnmaddsx => fmt_a_4op(instr, "fnmadds"),
PpcOpcode::fdivx => fmt_a_3op(instr, "fdiv", false),
PpcOpcode::fsubx => fmt_a_3op(instr, "fsub", false),
PpcOpcode::faddx => fmt_a_3op(instr, "fadd", false),
PpcOpcode::fsqrtx => fmt_a_unary(instr, "fsqrt"),
PpcOpcode::fselx => fmt_a_4op(instr, "fsel"),
PpcOpcode::fmulx => fmt_a_3op(instr, "fmul", true),
PpcOpcode::frsqrtex => fmt_a_unary(instr, "frsqrte"),
PpcOpcode::fmsubx => fmt_a_4op(instr, "fmsub"),
PpcOpcode::fmaddx => fmt_a_4op(instr, "fmadd"),
PpcOpcode::fnmsubx => fmt_a_4op(instr, "fnmsub"),
PpcOpcode::fnmaddx => fmt_a_4op(instr, "fnmadd"),
PpcOpcode::fcmpu => fmt_fcmp(instr, "fcmpu"),
PpcOpcode::fcmpo => fmt_fcmp(instr, "fcmpo"),
PpcOpcode::frspx => fmt_x_fpu_unary(instr, "frsp"),
PpcOpcode::fctiwx => fmt_x_fpu_unary(instr, "fctiw"),
PpcOpcode::fctiwzx => fmt_x_fpu_unary(instr, "fctiwz"),
PpcOpcode::fnegx => fmt_x_fpu_unary(instr, "fneg"),
PpcOpcode::fmrx => fmt_x_fpu_unary(instr, "fmr"),
PpcOpcode::fnabsx => fmt_x_fpu_unary(instr, "fnabs"),
PpcOpcode::fabsx => fmt_x_fpu_unary(instr, "fabs"),
PpcOpcode::fctidx => fmt_x_fpu_unary(instr, "fctid"),
PpcOpcode::fctidzx => fmt_x_fpu_unary(instr, "fctidz"),
PpcOpcode::fcfidx => fmt_x_fpu_unary(instr, "fcfid"),
PpcOpcode::mffsx => {
let rc = rc_dot(instr);
base(&format!("mffs{rc}"), fpr(instr.rd()), 8)
}
PpcOpcode::mtfsfx => {
let rc = rc_dot(instr);
let fxm = (instr.raw >> 17) & 0xFF;
let frb = (instr.raw >> 11) & 0x1F;
base(&format!("mtfsf{rc}"), format!("0x{fxm:02X}, {}", fpr(frb as usize)), 8)
}
PpcOpcode::mtfsb1x => fmt_mtfsb(instr, "mtfsb1"),
PpcOpcode::mtfsb0x => fmt_mtfsb(instr, "mtfsb0"),
PpcOpcode::mtfsfix => {
let rc = rc_dot(instr);
let bf = instr.crfd();
let imm = (instr.raw >> 12) & 0xF;
base(&format!("mtfsfi{rc}"), format!("cr{bf}, {imm}"), 8)
}
PpcOpcode::mcrfs => base("mcrfs", format!("cr{}, cr{}", instr.crfd(), instr.crfs()), 8),
// ── Standard VMX (5-bit registers) ────────────────────────────────
// 3-operand VD, VA, VB
PpcOpcode::vaddubm | PpcOpcode::vmaxub | PpcOpcode::vrlb | PpcOpcode::vmuloub |
PpcOpcode::vaddfp | PpcOpcode::vmrghb | PpcOpcode::vpkuhum |
PpcOpcode::vadduhm | PpcOpcode::vmaxuh | PpcOpcode::vrlh | PpcOpcode::vmulouh |
PpcOpcode::vsubfp | PpcOpcode::vmrghh | PpcOpcode::vpkuwum |
PpcOpcode::vadduwm | PpcOpcode::vmaxuw | PpcOpcode::vrlw | PpcOpcode::vmrghw |
PpcOpcode::vpkuhus | PpcOpcode::vpkuwus |
PpcOpcode::vmaxsb | PpcOpcode::vslb | PpcOpcode::vmulosb | PpcOpcode::vmrglb |
PpcOpcode::vpkshus | PpcOpcode::vmaxsh | PpcOpcode::vslh | PpcOpcode::vmulosh |
PpcOpcode::vmrglh | PpcOpcode::vpkswus | PpcOpcode::vaddcuw | PpcOpcode::vmaxsw |
PpcOpcode::vslw | PpcOpcode::vmrglw | PpcOpcode::vpkshss | PpcOpcode::vsl |
PpcOpcode::vpkswss | PpcOpcode::vaddubs | PpcOpcode::vminub | PpcOpcode::vsrb |
PpcOpcode::vmuleub | PpcOpcode::vadduhs | PpcOpcode::vminuh | PpcOpcode::vsrh |
PpcOpcode::vmuleuh | PpcOpcode::vadduws | PpcOpcode::vminuw | PpcOpcode::vsrw |
PpcOpcode::vsr | PpcOpcode::vaddsbs | PpcOpcode::vminsb | PpcOpcode::vsrab |
PpcOpcode::vmulesb | PpcOpcode::vpkpx | PpcOpcode::vaddshs | PpcOpcode::vminsh |
PpcOpcode::vsrah | PpcOpcode::vmulesh | PpcOpcode::vaddsws | PpcOpcode::vminsw |
PpcOpcode::vsraw | PpcOpcode::vsububm | PpcOpcode::vavgub | PpcOpcode::vand |
PpcOpcode::vmaxfp | PpcOpcode::vslo | PpcOpcode::vsubuhm | PpcOpcode::vavguh |
PpcOpcode::vandc | PpcOpcode::vminfp | PpcOpcode::vsro | PpcOpcode::vsubuwm |
PpcOpcode::vavguw | PpcOpcode::vor | PpcOpcode::vxor | PpcOpcode::vavgsb |
PpcOpcode::vnor | PpcOpcode::vavgsh | PpcOpcode::vsubcuw | PpcOpcode::vavgsw |
PpcOpcode::vsububs | PpcOpcode::vsum4ubs| PpcOpcode::vsubuhs | PpcOpcode::vsum4shs |
PpcOpcode::vsubuws | PpcOpcode::vsum2sws| PpcOpcode::vsubsbs | PpcOpcode::vsum4sbs |
PpcOpcode::vsubshs | PpcOpcode::vsubsws | PpcOpcode::vsumsws => {
fmt_vmx_3op(instr, opcode_name(instr.opcode))
}
// VMX unary VD, VB
PpcOpcode::vrefp | PpcOpcode::vrsqrtefp | PpcOpcode::vexptefp |
PpcOpcode::vlogefp | PpcOpcode::vrfin | PpcOpcode::vrfiz |
PpcOpcode::vrfip | PpcOpcode::vrfim | PpcOpcode::vupkhsb |
PpcOpcode::vupkhsh | PpcOpcode::vupklsb | PpcOpcode::vupklsh |
PpcOpcode::vupkhpx | PpcOpcode::vupklpx => {
fmt_vmx_unary(instr, opcode_name(instr.opcode))
}
// VMX VD, VB, UIMM (VA = uimm field)
PpcOpcode::vspltb | PpcOpcode::vsplth | PpcOpcode::vspltw |
PpcOpcode::vcfux | PpcOpcode::vcfsx |
PpcOpcode::vctuxs | PpcOpcode::vctsxs => {
fmt_vmx_uimm(instr, opcode_name(instr.opcode))
}
// VMX VD, SIMM (VA field as 5-bit signed immediate)
PpcOpcode::vspltisb => fmt_vmx_simm(instr, "vspltisb"),
PpcOpcode::vspltish => fmt_vmx_simm(instr, "vspltish"),
PpcOpcode::vspltisw => fmt_vmx_simm(instr, "vspltisw"),
PpcOpcode::mfvscr => base("mfvscr", vr(instr.rd()), 8),
PpcOpcode::mtvscr => base("mtvscr", vr(instr.rb()), 8),
// VMX compare (Rc bit at bit 21)
PpcOpcode::vcmpequb | PpcOpcode::vcmpequh | PpcOpcode::vcmpequw |
PpcOpcode::vcmpeqfp | PpcOpcode::vcmpgefp | PpcOpcode::vcmpgtub |
PpcOpcode::vcmpgtuh | PpcOpcode::vcmpgtuw | PpcOpcode::vcmpgtfp |
PpcOpcode::vcmpgtsb | PpcOpcode::vcmpgtsh | PpcOpcode::vcmpgtsw |
PpcOpcode::vcmpbfp => fmt_vmx_cmp(instr, opcode_name(instr.opcode)),
// VMX 4-operand VD, VA, VB, VC
PpcOpcode::vmhaddshs | PpcOpcode::vmhraddshs | PpcOpcode::vmladduhm |
PpcOpcode::vmsumubm | PpcOpcode::vmsummbm | PpcOpcode::vmsumuhm |
PpcOpcode::vmsumuhs | PpcOpcode::vmsumshm | PpcOpcode::vmsumshs |
PpcOpcode::vsel | PpcOpcode::vperm => {
fmt_vmx_4op(instr, opcode_name(instr.opcode))
}
PpcOpcode::vsldoi => fmt_vsldoi(instr),
PpcOpcode::vmaddfp => fmt_vmx_4op_swap(instr, "vmaddfp"),
PpcOpcode::vnmsubfp => fmt_vmx_4op_swap(instr, "vnmsubfp"),
// ── VMX128 load/store (uses GPR addressing + vd128 dest) ───────────
PpcOpcode::lvsl128 => fmt_vmx128_ls(instr, "lvsl128"),
PpcOpcode::lvsr128 => fmt_vmx128_ls(instr, "lvsr128"),
PpcOpcode::lvewx128 => fmt_vmx128_ls(instr, "lvewx128"),
PpcOpcode::lvx128 => fmt_vmx128_ls(instr, "lvx128"),
PpcOpcode::lvxl128 => fmt_vmx128_ls(instr, "lvxl128"),
PpcOpcode::lvlx128 => fmt_vmx128_ls(instr, "lvlx128"),
PpcOpcode::lvrx128 => fmt_vmx128_ls(instr, "lvrx128"),
PpcOpcode::lvlxl128 => fmt_vmx128_ls(instr, "lvlxl128"),
PpcOpcode::lvrxl128 => fmt_vmx128_ls(instr, "lvrxl128"),
PpcOpcode::stvewx128 => fmt_vmx128_ls(instr, "stvewx128"),
PpcOpcode::stvx128 => fmt_vmx128_ls(instr, "stvx128"),
PpcOpcode::stvxl128 => fmt_vmx128_ls(instr, "stvxl128"),
PpcOpcode::stvlx128 => fmt_vmx128_ls(instr, "stvlx128"),
PpcOpcode::stvrx128 => fmt_vmx128_ls(instr, "stvrx128"),
PpcOpcode::stvlxl128 => fmt_vmx128_ls(instr, "stvlxl128"),
PpcOpcode::stvrxl128 => fmt_vmx128_ls(instr, "stvrxl128"),
// Standard AltiVec load/store indexed (5-bit vr0-vr31)
PpcOpcode::lvsl => fmt_vmx_ls(instr, "lvsl"),
PpcOpcode::lvsr => fmt_vmx_ls(instr, "lvsr"),
PpcOpcode::lvebx => fmt_vmx_ls(instr, "lvebx"),
PpcOpcode::lvehx => fmt_vmx_ls(instr, "lvehx"),
PpcOpcode::lvewx => fmt_vmx_ls(instr, "lvewx"),
PpcOpcode::lvx => fmt_vmx_ls(instr, "lvx"),
PpcOpcode::lvxl => fmt_vmx_ls(instr, "lvxl"),
PpcOpcode::lvlx => fmt_vmx_ls(instr, "lvlx"),
PpcOpcode::lvrx => fmt_vmx_ls(instr, "lvrx"),
PpcOpcode::lvlxl => fmt_vmx_ls(instr, "lvlxl"),
PpcOpcode::lvrxl => fmt_vmx_ls(instr, "lvrxl"),
PpcOpcode::stvebx => fmt_vmx_ls(instr, "stvebx"),
PpcOpcode::stvehx => fmt_vmx_ls(instr, "stvehx"),
PpcOpcode::stvewx => fmt_vmx_ls(instr, "stvewx"),
PpcOpcode::stvx => fmt_vmx_ls(instr, "stvx"),
PpcOpcode::stvxl => fmt_vmx_ls(instr, "stvxl"),
PpcOpcode::stvlx => fmt_vmx_ls(instr, "stvlx"),
PpcOpcode::stvrx => fmt_vmx_ls(instr, "stvrx"),
PpcOpcode::stvlxl => fmt_vmx_ls(instr, "stvlxl"),
PpcOpcode::stvrxl => fmt_vmx_ls(instr, "stvrxl"),
// ── VMX128 op5 (3-op and 4-op fp/pack/logic) ───────────────────────
PpcOpcode::vaddfp128 => fmt_vmx128_3op(instr, "vaddfp128"),
PpcOpcode::vsubfp128 => fmt_vmx128_3op(instr, "vsubfp128"),
PpcOpcode::vmulfp128 => fmt_vmx128_3op(instr, "vmulfp128"),
PpcOpcode::vmsum3fp128 => fmt_vmx128_3op(instr, "vmsum3fp128"),
PpcOpcode::vmsum4fp128 => fmt_vmx128_3op(instr, "vmsum4fp128"),
PpcOpcode::vpkshss128 => fmt_vmx128_3op(instr, "vpkshss128"),
PpcOpcode::vpkshus128 => fmt_vmx128_3op(instr, "vpkshus128"),
PpcOpcode::vpkswss128 => fmt_vmx128_3op(instr, "vpkswss128"),
PpcOpcode::vpkswus128 => fmt_vmx128_3op(instr, "vpkswus128"),
PpcOpcode::vpkuhum128 => fmt_vmx128_3op(instr, "vpkuhum128"),
PpcOpcode::vpkuhus128 => fmt_vmx128_3op(instr, "vpkuhus128"),
PpcOpcode::vpkuwum128 => fmt_vmx128_3op(instr, "vpkuwum128"),
PpcOpcode::vpkuwus128 => fmt_vmx128_3op(instr, "vpkuwus128"),
PpcOpcode::vand128 => fmt_vmx128_3op(instr, "vand128"),
PpcOpcode::vandc128 => fmt_vmx128_3op(instr, "vandc128"),
PpcOpcode::vnor128 => fmt_vmx128_3op(instr, "vnor128"),
PpcOpcode::vor128 => fmt_vmx128_3op(instr, "vor128"),
PpcOpcode::vxor128 => fmt_vmx128_3op(instr, "vxor128"),
PpcOpcode::vsel128 => fmt_vmx128_3op(instr, "vsel128"),
PpcOpcode::vslo128 => fmt_vmx128_3op(instr, "vslo128"),
PpcOpcode::vsro128 => fmt_vmx128_3op(instr, "vsro128"),
PpcOpcode::vmaddfp128 => fmt_vmaddfp128(instr),
PpcOpcode::vmaddcfp128 => fmt_vmx128_madd_vd_vb(instr, "vmaddcfp128"),
PpcOpcode::vnmsubfp128 => fmt_vmx128_madd_vd_vb(instr, "vnmsubfp128"),
PpcOpcode::vperm128 => fmt_vperm128(instr),
PpcOpcode::vsldoi128 => fmt_vsldoi128(instr),
PpcOpcode::vpermwi128 => fmt_vpermwi128(instr),
// ── VMX128 op6 special ─────────────────────────────────────────────
PpcOpcode::vpkd3d128 => fmt_vmx128_pack_d3d(instr, "vpkd3d128"),
PpcOpcode::vrlimi128 => fmt_vmx128_pack_d3d(instr, "vrlimi128"),
PpcOpcode::vrfim128 => fmt_vmx128_unary(instr, "vrfim128"),
PpcOpcode::vrfin128 => fmt_vmx128_unary(instr, "vrfin128"),
PpcOpcode::vrfip128 => fmt_vmx128_unary(instr, "vrfip128"),
PpcOpcode::vrfiz128 => fmt_vmx128_unary(instr, "vrfiz128"),
PpcOpcode::vrefp128 => fmt_vmx128_unary(instr, "vrefp128"),
PpcOpcode::vrsqrtefp128 => fmt_vmx128_unary(instr, "vrsqrtefp128"),
PpcOpcode::vexptefp128 => fmt_vmx128_unary(instr, "vexptefp128"),
PpcOpcode::vlogefp128 => fmt_vmx128_unary(instr, "vlogefp128"),
PpcOpcode::vcfpsxws128 => fmt_vmx128_uimm(instr, "vcfpsxws128"),
PpcOpcode::vcfpuxws128 => fmt_vmx128_uimm(instr, "vcfpuxws128"),
PpcOpcode::vcsxwfp128 => fmt_vmx128_uimm(instr, "vcsxwfp128"),
PpcOpcode::vcuxwfp128 => fmt_vmx128_uimm(instr, "vcuxwfp128"),
PpcOpcode::vspltw128 => fmt_vmx128_uimm(instr, "vspltw128"),
PpcOpcode::vupkd3d128 => fmt_vmx128_uimm(instr, "vupkd3d128"),
PpcOpcode::vspltisw128 => {
let vd = instr.vd128();
let simm = sign_ext(extract_vx128_uimm5(instr.raw), 5);
base("vspltisw128", format!("{}, {simm}", vr(vd)), 14)
}
PpcOpcode::vcmpeqfp128 => fmt_vmx128_cmp(instr, "vcmpeqfp128"),
PpcOpcode::vcmpgefp128 => fmt_vmx128_cmp(instr, "vcmpgefp128"),
PpcOpcode::vcmpgtfp128 => fmt_vmx128_cmp(instr, "vcmpgtfp128"),
PpcOpcode::vcmpbfp128 => fmt_vmx128_cmp(instr, "vcmpbfp128"),
PpcOpcode::vcmpequw128 => fmt_vmx128_cmp(instr, "vcmpequw128"),
PpcOpcode::vrlw128 => fmt_vmx128_3op(instr, "vrlw128"),
PpcOpcode::vslw128 => fmt_vmx128_3op(instr, "vslw128"),
PpcOpcode::vsraw128 => fmt_vmx128_3op(instr, "vsraw128"),
PpcOpcode::vsrw128 => fmt_vmx128_3op(instr, "vsrw128"),
PpcOpcode::vmaxfp128 => fmt_vmx128_3op(instr, "vmaxfp128"),
PpcOpcode::vminfp128 => fmt_vmx128_3op(instr, "vminfp128"),
PpcOpcode::vmrghw128 => fmt_vmx128_3op(instr, "vmrghw128"),
PpcOpcode::vmrglw128 => fmt_vmx128_3op(instr, "vmrglw128"),
PpcOpcode::vupkhsb128 => fmt_vmx128_3op(instr, "vupkhsb128"),
PpcOpcode::vupklsb128 => fmt_vmx128_3op(instr, "vupklsb128"),
PpcOpcode::Invalid => long_word(instr.raw),
}
}
/// Disassemble a decoded instruction into PPC assembly text.
///
/// Back-compat entry point: returns the same single-string the legacy
/// formatter produced, preferring the extended form when present.
pub fn disassemble(instr: &DecodedInstr) -> String {
format(instr).display().to_string()
}
/// Disassemble a range of instructions from a byte slice.
pub fn disassemble_block(data: &[u8], base_addr: u32, count: usize) -> Vec<(u32, String)> {
let mut result = Vec::new();
for i in 0..count {
let offset = i * 4;
if offset + 4 > data.len() {
break;
}
let raw = u32::from_be_bytes([
data[offset], data[offset + 1], data[offset + 2], data[offset + 3],
]);
let addr = base_addr + offset as u32;
let instr = crate::decoder::decode(raw, addr);
let text = disassemble(&instr);
result.push((addr, text));
}
result
}
/// One yielded instruction from [`iter_disasm`]. Carries the absolute VA,
/// raw word, decoded opcode and the formatted text — everything a sink
/// needs to render or persist a single row without re-parsing.
#[derive(Debug, Clone)]
pub struct DisasmItem {
pub addr: u32,
pub raw: u32,
pub opcode: PpcOpcode,
pub text: DisasmText,
}
/// Iterate over instructions in the VA range `[va_start, va_end)` of an
/// image-mapped byte slice. `image[rva]` must hold the byte at absolute VA
/// `image_base + rva` (the layout produced by [`xenia_xex::loader`]).
///
/// Stops on a truncated tail (less than 4 bytes remaining at the cursor).
/// Yields nothing if `va_start >= va_end` or the start RVA is beyond the
/// image.
pub fn iter_disasm(
image: &[u8],
image_base: u32,
va_start: u32,
va_end: u32,
) -> impl Iterator<Item = DisasmItem> + '_ {
DisasmIter { image, image_base, va: va_start, end: va_end }
}
struct DisasmIter<'a> {
image: &'a [u8],
image_base: u32,
va: u32,
end: u32,
}
impl Iterator for DisasmIter<'_> {
type Item = DisasmItem;
#[inline]
fn next(&mut self) -> Option<DisasmItem> {
if self.va >= self.end {
return None;
}
let rva = self.va.wrapping_sub(self.image_base) as usize;
if rva + 4 > self.image.len() {
return None;
}
let raw = u32::from_be_bytes([
self.image[rva],
self.image[rva + 1],
self.image[rva + 2],
self.image[rva + 3],
]);
let abs = self.va;
let decoded = crate::decoder::decode(raw, abs);
let text = format(&decoded);
self.va = self.va.wrapping_add(4);
Some(DisasmItem { addr: abs, raw, opcode: decoded.opcode, text })
}
}
// ── Per-class formatters ───────────────────────────────────────────────────
fn opcode_name(op: PpcOpcode) -> &'static str {
// Used for VMX where the enum variant name matches the canonical mnemonic.
// For ALU/FPU variants ending in "x", use hardcoded strings instead.
match op {
PpcOpcode::vaddubm => "vaddubm", PpcOpcode::vmaxub => "vmaxub", PpcOpcode::vrlb => "vrlb",
PpcOpcode::vmuloub => "vmuloub", PpcOpcode::vaddfp => "vaddfp", PpcOpcode::vmrghb => "vmrghb",
PpcOpcode::vpkuhum => "vpkuhum", PpcOpcode::vadduhm => "vadduhm", PpcOpcode::vmaxuh => "vmaxuh",
PpcOpcode::vrlh => "vrlh", PpcOpcode::vmulouh => "vmulouh", PpcOpcode::vsubfp => "vsubfp",
PpcOpcode::vmrghh => "vmrghh", PpcOpcode::vpkuwum => "vpkuwum",
PpcOpcode::vadduwm => "vadduwm", PpcOpcode::vmaxuw => "vmaxuw", PpcOpcode::vrlw => "vrlw",
PpcOpcode::vmrghw => "vmrghw", PpcOpcode::vpkuhus => "vpkuhus", PpcOpcode::vpkuwus => "vpkuwus",
PpcOpcode::vmaxsb => "vmaxsb", PpcOpcode::vslb => "vslb", PpcOpcode::vmulosb => "vmulosb",
PpcOpcode::vmrglb => "vmrglb", PpcOpcode::vpkshus => "vpkshus", PpcOpcode::vmaxsh => "vmaxsh",
PpcOpcode::vslh => "vslh", PpcOpcode::vmulosh => "vmulosh", PpcOpcode::vmrglh => "vmrglh",
PpcOpcode::vpkswus => "vpkswus", PpcOpcode::vaddcuw => "vaddcuw", PpcOpcode::vmaxsw => "vmaxsw",
PpcOpcode::vslw => "vslw", PpcOpcode::vmrglw => "vmrglw", PpcOpcode::vpkshss => "vpkshss",
PpcOpcode::vsl => "vsl", PpcOpcode::vpkswss => "vpkswss",
PpcOpcode::vaddubs => "vaddubs", PpcOpcode::vminub => "vminub", PpcOpcode::vsrb => "vsrb",
PpcOpcode::vmuleub => "vmuleub", PpcOpcode::vadduhs => "vadduhs", PpcOpcode::vminuh => "vminuh",
PpcOpcode::vsrh => "vsrh", PpcOpcode::vmuleuh => "vmuleuh",
PpcOpcode::vadduws => "vadduws", PpcOpcode::vminuw => "vminuw", PpcOpcode::vsrw => "vsrw",
PpcOpcode::vsr => "vsr",
PpcOpcode::vaddsbs => "vaddsbs", PpcOpcode::vminsb => "vminsb", PpcOpcode::vsrab => "vsrab",
PpcOpcode::vmulesb => "vmulesb", PpcOpcode::vpkpx => "vpkpx",
PpcOpcode::vaddshs => "vaddshs", PpcOpcode::vminsh => "vminsh", PpcOpcode::vsrah => "vsrah",
PpcOpcode::vmulesh => "vmulesh",
PpcOpcode::vaddsws => "vaddsws", PpcOpcode::vminsw => "vminsw", PpcOpcode::vsraw => "vsraw",
PpcOpcode::vsububm => "vsububm", PpcOpcode::vavgub => "vavgub", PpcOpcode::vand => "vand",
PpcOpcode::vmaxfp => "vmaxfp", PpcOpcode::vslo => "vslo",
PpcOpcode::vsubuhm => "vsubuhm", PpcOpcode::vavguh => "vavguh", PpcOpcode::vandc => "vandc",
PpcOpcode::vminfp => "vminfp", PpcOpcode::vsro => "vsro",
PpcOpcode::vsubuwm => "vsubuwm", PpcOpcode::vavguw => "vavguw", PpcOpcode::vor => "vor",
PpcOpcode::vxor => "vxor", PpcOpcode::vavgsb => "vavgsb", PpcOpcode::vnor => "vnor",
PpcOpcode::vavgsh => "vavgsh", PpcOpcode::vsubcuw => "vsubcuw", PpcOpcode::vavgsw => "vavgsw",
PpcOpcode::vsububs => "vsububs", PpcOpcode::vsum4ubs => "vsum4ubs",
PpcOpcode::vsubuhs => "vsubuhs", PpcOpcode::vsum4shs => "vsum4shs",
PpcOpcode::vsubuws => "vsubuws", PpcOpcode::vsum2sws => "vsum2sws",
PpcOpcode::vsubsbs => "vsubsbs", PpcOpcode::vsum4sbs => "vsum4sbs",
PpcOpcode::vsubshs => "vsubshs", PpcOpcode::vsubsws => "vsubsws",
PpcOpcode::vsumsws => "vsumsws",
PpcOpcode::vrefp => "vrefp", PpcOpcode::vrsqrtefp => "vrsqrtefp",
PpcOpcode::vexptefp => "vexptefp", PpcOpcode::vlogefp => "vlogefp",
PpcOpcode::vrfin => "vrfin", PpcOpcode::vrfiz => "vrfiz",
PpcOpcode::vrfip => "vrfip", PpcOpcode::vrfim => "vrfim",
PpcOpcode::vupkhsb => "vupkhsb", PpcOpcode::vupkhsh => "vupkhsh",
PpcOpcode::vupklsb => "vupklsb", PpcOpcode::vupklsh => "vupklsh",
PpcOpcode::vupkhpx => "vupkhpx", PpcOpcode::vupklpx => "vupklpx",
PpcOpcode::vspltb => "vspltb", PpcOpcode::vsplth => "vsplth", PpcOpcode::vspltw => "vspltw",
PpcOpcode::vcfux => "vcfux", PpcOpcode::vcfsx => "vcfsx",
PpcOpcode::vctuxs => "vctuxs", PpcOpcode::vctsxs => "vctsxs",
PpcOpcode::vcmpequb => "vcmpequb", PpcOpcode::vcmpequh => "vcmpequh",
PpcOpcode::vcmpequw => "vcmpequw", PpcOpcode::vcmpeqfp => "vcmpeqfp",
PpcOpcode::vcmpgefp => "vcmpgefp", PpcOpcode::vcmpgtub => "vcmpgtub",
PpcOpcode::vcmpgtuh => "vcmpgtuh", PpcOpcode::vcmpgtuw => "vcmpgtuw",
PpcOpcode::vcmpgtfp => "vcmpgtfp", PpcOpcode::vcmpgtsb => "vcmpgtsb",
PpcOpcode::vcmpgtsh => "vcmpgtsh", PpcOpcode::vcmpgtsw => "vcmpgtsw",
PpcOpcode::vcmpbfp => "vcmpbfp",
PpcOpcode::vmhaddshs => "vmhaddshs", PpcOpcode::vmhraddshs => "vmhraddshs",
PpcOpcode::vmladduhm => "vmladduhm",
PpcOpcode::vmsumubm => "vmsumubm", PpcOpcode::vmsummbm => "vmsummbm",
PpcOpcode::vmsumuhm => "vmsumuhm", PpcOpcode::vmsumuhs => "vmsumuhs",
PpcOpcode::vmsumshm => "vmsumshm", PpcOpcode::vmsumshs => "vmsumshs",
PpcOpcode::vsel => "vsel", PpcOpcode::vperm => "vperm",
_ => "?",
}
}
// Branches (I-form: b/bl/ba/bla) — produces base + extended forms.
fn fmt_b(instr: &DecodedInstr) -> DisasmText {
let aa = instr.aa();
let lk = instr.lk();
let target = if aa { instr.li() as u32 }
else { instr.addr.wrapping_add(instr.li() as u32) };
let mnem = match (aa, lk) {
(false, false) => "b",
(false, true) => "bl",
(true, false) => "ba",
(true, true) => "bla",
};
let ops = format!("0x{target:08X}");
with_target(base(mnem, ops, 8), target)
}
fn fmt_bc(instr: &DecodedInstr) -> DisasmText {
let bo = instr.bo();
let bi = instr.bi();
let aa = instr.aa();
let lk = instr.lk();
let target = if aa { instr.bd() as u32 }
else { instr.addr.wrapping_add(instr.bd() as u32) };
let a = if aa { "a" } else { "" };
let l = if lk { "l" } else { "" };
let base_mnem = format!("bc{a}{l}");
let base_ops = format!("{bo}, {}, 0x{target:08X}", crb(bi));
// Extended forms.
let cr_field = bi / 4;
let cr_bit = bi % 4;
let decr = bo & 0x04 == 0;
let uncond = bo & 0x10 != 0;
let result = if uncond && !decr {
// Unconditional branch.
let ext_mnem = format!("b{a}{l}");
let ext_ops = format!("0x{target:08X}");
with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8)
} else {
let cond_true = bo & 0x08 != 0;
let cond_name_opt: Option<&'static str> = match (cr_bit, cond_true) {
(0, true) => Some("lt"), (0, false) => Some("ge"),
(1, true) => Some("gt"), (1, false) => Some("le"),
(2, true) => Some("eq"), (2, false) => Some("ne"),
(3, true) => Some("so"), (3, false) => Some("ns"),
_ => None,
};
let cr = if cr_field == 0 { String::new() } else { format!("cr{cr_field}, ") };
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)
} else if let Some(cond_name) = cond_name_opt {
let ext_mnem = format!("b{cond_name}{a}{l}");
let ext_ops = format!("{cr}0x{target:08X}");
with_ext(&base_mnem, base_ops, 8, &ext_mnem, ext_ops, 8)
} else {
base(&base_mnem, base_ops, 8)
}
};
with_target(result, target)
}
fn fmt_bclr(instr: &DecodedInstr) -> DisasmText {
let bo = instr.bo();
let bi = instr.bi();
let lk = instr.lk();
let l = if lk { "l" } else { "" };
let base_mnem = format!("bclr{l}");
let base_ops = format!("{bo}, {}", crb(bi));
// 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
// the simplified `blr`/`blrl` form applies for any BI value.
if bo == 20 {
let ext = if lk { "blrl" } else { "blr" };
return with_ext(&base_mnem, base_ops, 8, ext, String::new(), 0);
}
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}");
if cr_no_comma.is_empty() {
return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0);
} else {
return with_ext(&base_mnem, base_ops, 8, &ext_mnem, cr_no_comma.to_string(), 8);
}
}
let decr = bo & 0x04 == 0;
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}");
return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0);
}
base(&base_mnem, base_ops, 8)
}
fn fmt_bcctr(instr: &DecodedInstr) -> DisasmText {
let bo = instr.bo();
let bi = instr.bi();
let lk = instr.lk();
let l = if lk { "l" } else { "" };
let base_mnem = format!("bcctr{l}");
let base_ops = format!("{bo}, {}", crb(bi));
// BO=20 unconditional pattern: BI is don't-care (see fmt_bclr).
if bo == 20 {
let ext = if lk { "bctrl" } else { "bctr" };
return with_ext(&base_mnem, base_ops, 8, ext, String::new(), 0);
}
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}");
if cr_no_comma.is_empty() {
return with_ext(&base_mnem, base_ops, 8, &ext_mnem, String::new(), 0);
} else {
return with_ext(&base_mnem, base_ops, 8, &ext_mnem, cr_no_comma.to_string(), 8);
}
}
base(&base_mnem, base_ops, 8)
}
// Trap immediate / register
fn fmt_trap_imm(instr: &DecodedInstr, mnem: &str, simplified_prefix: &str) -> DisasmText {
let to = instr.to();
let ra = instr.ra();
let imm = instr.simm16() as i32;
let base_ops = format!("{to}, {}, {imm}", gpr(ra));
if let Some(cond) = trap_cond(to) {
if cond.is_empty() {
base(mnem, base_ops, 8)
} else {
let ext_mnem = format!("{simplified_prefix}{cond}i");
let ext_ops = format!("{}, {imm}", gpr(ra));
with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8)
}
} else {
base(mnem, base_ops, 8)
}
}
fn fmt_trap_reg(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let to = instr.to();
let ra = instr.ra();
let rb = instr.rb();
let base_ops = format!("{to}, {}, {}", gpr(ra), gpr(rb));
if to == 31 && ra == 0 && rb == 0 {
return with_ext(mnem, base_ops, 8, "trap", String::new(), 0);
}
if let Some(cond) = trap_cond(to)
&& !cond.is_empty()
{
let ext_mnem = format!("{mnem}{cond}");
let ext_ops = format!("{}, {}", gpr(ra), gpr(rb));
return with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8);
}
base(mnem, base_ops, 8)
}
// D-form ALU
fn fmt_addi(instr: &DecodedInstr) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let imm = instr.simm16() as i32;
let base_ops = format!("{}, {}, {imm}", gpr(rt), gpr(ra));
if ra == 0 {
with_ext("addi", base_ops, 8, "li", format!("{}, {imm}", gpr(rt)), 8)
} else if imm < 0 {
with_ext("addi", base_ops, 8, "subi", format!("{}, {}, {}", gpr(rt), gpr(ra), -imm), 8)
} else {
base("addi", base_ops, 8)
}
}
fn fmt_addis(instr: &DecodedInstr) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let imm = instr.simm16() as i32;
let imm_u = imm as u16 as u32;
let base_ops = format!("{}, {}, 0x{imm_u:X}", gpr(rt), gpr(ra));
if ra == 0 {
with_ext("addis", base_ops, 8, "lis", format!("{}, 0x{imm_u:X}", gpr(rt)), 8)
} else if imm < 0 {
let neg = (-imm) as u16 as u32;
with_ext("addis", base_ops, 8, "subis", format!("{}, {}, 0x{neg:X}", gpr(rt), gpr(ra)), 8)
} else {
base("addis", base_ops, 8)
}
}
fn fmt_d_add(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let imm = instr.simm16() as i32;
let base_ops = format!("{}, {}, {imm}", gpr(rt), gpr(ra));
if imm < 0 {
let ext_mnem = mnem.replace("addic", "subic");
with_ext(mnem, base_ops, 8, &ext_mnem, format!("{}, {}, {}", gpr(rt), gpr(ra), -imm), 8)
} else {
base(mnem, base_ops, 8)
}
}
fn fmt_d_imm_simple(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let imm = instr.simm16() as i32;
base(mnem, format!("{}, {}, {imm}", gpr(rt), gpr(ra)), 8)
}
fn fmt_cmp_imm(instr: &DecodedInstr, mnem: &str, signed: bool) -> DisasmText {
let bf = instr.crfd();
let l_bit = if instr.l() { 1 } else { 0 };
let ra = instr.ra();
let imm_str = if signed {
format!("{}", instr.simm16() as i32)
} else {
format!("0x{:X}", instr.uimm16())
};
let cr = if bf == 0 { String::new() } else { format!("cr{bf}, ") };
let base_ops = format!("{cr}{l_bit}, {}, {imm_str}", gpr(ra));
let size = if l_bit == 0 { "w" } else { "d" };
let ext_mnem = if mnem == "cmpi" {
format!("cmp{size}i")
} else {
format!("cmpl{size}i")
};
let ext_ops = format!("{cr}{}, {imm_str}", gpr(ra));
with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8)
}
fn fmt_cmp_reg(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let bf = instr.crfd();
let l_bit = if instr.l() { 1 } else { 0 };
let ra = instr.ra();
let rb = instr.rb();
let cr = if bf == 0 { String::new() } else { format!("cr{bf}, ") };
let base_ops = format!("{cr}{l_bit}, {}, {}", gpr(ra), gpr(rb));
let size = if l_bit == 0 { "w" } else { "d" };
let ext_mnem = format!("{mnem}{size}");
let ext_ops = format!("{cr}{}, {}", gpr(ra), gpr(rb));
with_ext(mnem, base_ops, 8, &ext_mnem, ext_ops, 8)
}
fn fmt_ori(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let uimm = instr.uimm16() as u32;
let base_ops = format!("{}, {}, 0x{uimm:X}", gpr(ra), gpr(rs));
if rs == 0 && ra == 0 && uimm == 0 {
with_ext("ori", base_ops, 8, "nop", String::new(), 0)
} else {
base("ori", base_ops, 8)
}
}
fn fmt_d_logic(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let uimm = instr.uimm16() as u32;
base(mnem, format!("{}, {}, 0x{uimm:X}", gpr(ra), gpr(rs)), 8)
}
// D-form load/store. `is_fpr` selects between fX and rX for the data register.
fn fmt_ld(instr: &DecodedInstr, mnem: &str, is_fpr: bool) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let d = instr.d();
let rn = if is_fpr { fpr(rt) } else { gpr(rt) };
base(mnem, format!("{rn}, {d}({})", gpr(ra)), 8)
}
fn fmt_st(instr: &DecodedInstr, mnem: &str, is_fpr: bool) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let d = instr.d();
let rn = if is_fpr { fpr(rs) } else { gpr(rs) };
base(mnem, format!("{rn}, {d}({})", gpr(ra)), 8)
}
// DS-form load/store.
fn fmt_ds(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let r = instr.rd();
let ra = instr.ra();
let ds = instr.ds();
base(mnem, format!("{}, {ds}({})", gpr(r), gpr(ra)), 8)
}
// Rotate (32-bit).
fn fmt_rlwimi(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let sh = instr.sh();
let mb = instr.mb();
let me = instr.me();
let rc = rc_dot(instr);
let mnem = format!("rlwimi{rc}");
let base_ops = format!("{}, {}, {sh}, {mb}, {me}", gpr(ra), gpr(rs));
// inslwi rA, rS, n, b = rlwimi rA, rS, 32-b, b, b+n-1
if mb <= me && sh == (32u32.wrapping_sub(mb)) % 32 && sh != 31u32.wrapping_sub(me) {
let n = me - mb + 1;
let b = mb;
let ext_mnem = format!("inslwi{rc}");
return with_ext(&mnem, base_ops, 8, &ext_mnem, format!("{}, {}, {n}, {b}", gpr(ra), gpr(rs)), 8);
}
// insrwi rA, rS, n, b = rlwimi rA, rS, 32-(b+n), b, b+n-1
if mb <= me && sh == 31u32.wrapping_sub(me) % 32 {
let n = me - mb + 1;
let b = mb;
let ext_mnem = format!("insrwi{rc}");
return with_ext(&mnem, base_ops, 8, &ext_mnem, format!("{}, {}, {n}, {b}", gpr(ra), gpr(rs)), 8);
}
base(&mnem, base_ops, 8)
}
fn fmt_rlwinm(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let sh = instr.sh();
let mb = instr.mb();
let me = instr.me();
let rc = rc_dot(instr);
let mnem = format!("rlwinm{rc}");
let base_ops = format!("{}, {}, {sh}, {mb}, {me}", gpr(ra), gpr(rs));
// Priority-ordered simplified forms.
if sh > 0 && 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);
}
if sh > 0 && me == 31 && sh + mb == 32 {
let ext = format!("srwi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), 32 - sh), 8);
}
if sh > 0 && mb == 0 && me == 31 {
let ext = format!("rotlwi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8);
}
if sh == 0 && me == 31 && mb > 0 {
let ext = format!("clrlwi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {mb}", gpr(ra), gpr(rs)), 8);
}
if sh == 0 && mb == 0 && me < 31 {
let ext = format!("clrrwi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), 31 - me), 8);
}
if mb == 0 && sh > 0 && me < 31 {
let n = me + 1;
let ext = format!("extlwi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {n}, {sh}", gpr(ra), gpr(rs)), 8);
}
if me == 31 && mb > 0 && sh > 0 {
let n = 32 - mb;
let b = sh.wrapping_sub(n) % 32;
let ext = format!("extrwi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {n}, {b}", gpr(ra), gpr(rs)), 8);
}
base(&mnem, base_ops, 8)
}
fn fmt_rlwnm(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rb = instr.rb();
let mb = instr.mb();
let me = instr.me();
let rc = rc_dot(instr);
let mnem = format!("rlwnm{rc}");
let base_ops = format!("{}, {}, {}, {mb}, {me}", gpr(ra), gpr(rs), gpr(rb));
if mb == 0 && me == 31 {
let ext = format!("rotlw{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)), 8);
}
base(&mnem, base_ops, 8)
}
// 64-bit MD/MDS-form rotate.
fn fmt_rldicl(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rc = rc_dot(instr);
let sh = instr.sh64();
let mb = mb_md(instr.raw);
let mnem = format!("rldicl{rc}");
let base_ops = format!("{}, {}, {sh}, {mb}", gpr(ra), gpr(rs));
if sh == 0 && mb > 0 {
let ext = format!("clrldi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {mb}", gpr(ra), gpr(rs)), 8);
}
if mb > 0 && sh == (64u32.wrapping_sub(mb)) & 63 {
let ext = format!("srdi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {mb}", gpr(ra), gpr(rs)), 8);
}
if sh > 0 && mb == 0 {
let ext = format!("rotldi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8);
}
base(&mnem, base_ops, 8)
}
fn fmt_rldicr(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rc = rc_dot(instr);
let sh = instr.sh64();
let me = mb_md(instr.raw);
let mnem = format!("rldicr{rc}");
let base_ops = format!("{}, {}, {sh}, {me}", gpr(ra), gpr(rs));
if sh > 0 && me == (63u32.wrapping_sub(sh)) & 63 {
let ext = format!("sldi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8);
}
if sh == 0 && me < 63 {
let ext = format!("clrrdi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), 63 - me), 8);
}
base(&mnem, base_ops, 8)
}
fn fmt_rldic(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rc = rc_dot(instr);
let sh = instr.sh64();
let mb = mb_md(instr.raw);
base(&format!("rldic{rc}"), format!("{}, {}, {sh}, {mb}", gpr(ra), gpr(rs)), 8)
}
fn fmt_rldimi(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rc = rc_dot(instr);
let sh = instr.sh64();
let mb = mb_md(instr.raw);
let mnem = format!("rldimi{rc}");
let base_ops = format!("{}, {}, {sh}, {mb}", gpr(ra), gpr(rs));
if mb > 0 {
let n = (64u32.wrapping_sub(sh).wrapping_sub(mb)) & 63;
if n > 0 {
let ext = format!("insrdi{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {n}, {mb}", gpr(ra), gpr(rs)), 8);
}
}
base(&mnem, base_ops, 8)
}
fn fmt_rldcl(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let mb = mb_md(instr.raw);
let mnem = format!("rldcl{rc}");
let base_ops = format!("{}, {}, {}, {mb}", gpr(ra), gpr(rs), gpr(rb));
if mb == 0 {
let ext = format!("rotld{rc}");
return with_ext(&mnem, base_ops, 8, &ext, format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)), 8);
}
base(&mnem, base_ops, 8)
}
fn fmt_rldcr(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let me = mb_md(instr.raw);
base(&format!("rldcr{rc}"), format!("{}, {}, {}, {me}", gpr(ra), gpr(rs), gpr(rb)), 8)
}
/// MD/MDS-form mb/me field: 6 bits packed as bits 21-25 + bit 26 (low bit).
#[inline]
fn mb_md(raw: u32) -> u32 {
let lo5 = (raw >> 6) & 0x1F; // bits 21-25
let hi = (raw >> 5) & 0x1; // bit 26
lo5 | (hi << 5)
}
// XO-form ALU
fn fmt_xo_3op(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let oe = if instr.oe() { "o" } else { "" };
let full = format!("{mnem}{oe}{rc}");
base(&full, format!("{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)), 8)
}
fn fmt_xo_2op(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let rc = rc_dot(instr);
let oe = if instr.oe() { "o" } else { "" };
let full = format!("{mnem}{oe}{rc}");
base(&full, format!("{}, {}", gpr(rt), gpr(ra)), 8)
}
fn fmt_xo_3op_no_oe(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let oe = if instr.oe() { "o" } else { "" };
let full = format!("{mnem}{oe}{rc}");
base(&full, format!("{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)), 8)
}
fn fmt_xo_3op_rc_only(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let full = format!("{mnem}{rc}");
base(&full, format!("{}, {}, {}", gpr(rt), gpr(ra), gpr(rb)), 8)
}
fn fmt_subf(instr: &DecodedInstr, base_mnem: &str, ext_mnem: &str) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let oe = if instr.oe() { "o" } else { "" };
let bm = format!("{base_mnem}{oe}{rc}");
let em = format!("{ext_mnem}{oe}{rc}");
let bo = format!("{}, {}, {}", gpr(rt), gpr(ra), gpr(rb));
let eo = format!("{}, {}, {}", gpr(rt), gpr(rb), gpr(ra));
with_ext(&bm, bo, 8, &em, eo, 8)
}
// X-form logical
fn fmt_x_logic(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let full = format!("{mnem}{rc}");
base(&full, format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb)), 8)
}
fn fmt_x_unary_rc(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rc = rc_dot(instr);
let full = format!("{mnem}{rc}");
base(&full, format!("{}, {}", gpr(ra), gpr(rs)), 8)
}
fn fmt_logic_and(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let bm = format!("and{rc}");
let bo = format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb));
if rs == rb {
let em = format!("mr{rc}");
with_ext(&bm, bo, 8, &em, format!("{}, {}", gpr(ra), gpr(rs)), 8)
} else {
base(&bm, bo, 8)
}
}
fn fmt_logic_or(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let bm = format!("or{rc}");
let bo = format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb));
if rs == rb {
let em = format!("mr{rc}");
with_ext(&bm, bo, 8, &em, format!("{}, {}", gpr(ra), gpr(rs)), 8)
} else {
base(&bm, bo, 8)
}
}
fn fmt_logic_nor(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rb = instr.rb();
let rc = rc_dot(instr);
let bm = format!("nor{rc}");
let bo = format!("{}, {}, {}", gpr(ra), gpr(rs), gpr(rb));
if rs == rb {
let em = format!("not{rc}");
with_ext(&bm, bo, 8, &em, format!("{}, {}", gpr(ra), gpr(rs)), 8)
} else {
base(&bm, bo, 8)
}
}
// Shift immediate
fn fmt_srawi(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let sh = instr.sh();
let rc = rc_dot(instr);
base(&format!("srawi{rc}"), format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8)
}
fn fmt_sradi(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let sh = instr.sh64();
let rc = rc_dot(instr);
base(&format!("sradi{rc}"), format!("{}, {}, {sh}", gpr(ra), gpr(rs)), 8)
}
// Special-purpose register moves
fn fmt_mfspr(instr: &DecodedInstr) -> DisasmText {
let rd = instr.rd();
let spr = instr.spr();
let base_ops = format!("{}, {}", gpr(rd), spr_name(spr));
let ext = match spr {
8 => Some(("mflr", format!("{}", gpr(rd)))),
9 => Some(("mfctr", format!("{}", gpr(rd)))),
1 => Some(("mfxer", format!("{}", gpr(rd)))),
_ => None,
};
match ext {
Some((em, eo)) => with_ext("mfspr", base_ops, 8, em, eo, 8),
None => base("mfspr", base_ops, 8),
}
}
fn fmt_mtspr(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let spr = instr.spr();
let base_ops = format!("{}, {}", spr_name(spr), gpr(rs));
let ext = match spr {
8 => Some(("mtlr", format!("{}", gpr(rs)))),
9 => Some(("mtctr", format!("{}", gpr(rs)))),
1 => Some(("mtxer", format!("{}", gpr(rs)))),
_ => None,
};
match ext {
Some((em, eo)) => with_ext("mtspr", base_ops, 8, em, eo, 8),
None => base("mtspr", base_ops, 8),
}
}
fn fmt_mtcrf(instr: &DecodedInstr) -> DisasmText {
let rs = instr.rs();
let fxm = (instr.raw >> 12) & 0xFF;
let bo = format!("0x{fxm:02X}, {}", gpr(rs));
if fxm == 0xFF {
with_ext("mtcrf", bo, 8, "mtcr", gpr(rs), 8)
} else {
base("mtcrf", bo, 8)
}
}
fn fmt_mftb(instr: &DecodedInstr) -> DisasmText {
let rd = instr.rd();
let tbr = instr.spr();
let base_ops = format!("{}, {tbr}", gpr(rd));
match tbr {
268 => with_ext("mftb", base_ops, 8, "mftb", gpr(rd), 8),
269 => with_ext("mftb", base_ops, 8, "mftbu", gpr(rd), 8),
_ => base("mftb", base_ops, 8),
}
}
// X-form indexed load/store.
fn fmt_x_load(instr: &DecodedInstr, mnem: &str, is_fpr: bool) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let rb = instr.rb();
let rn = if is_fpr { fpr(rt) } else { gpr(rt) };
base(mnem, format!("{rn}, {}, {}", gpr(ra), gpr(rb)), 8)
}
fn fmt_x_store(instr: &DecodedInstr, mnem: &str, is_fpr: bool) -> DisasmText {
let rs = instr.rs();
let ra = instr.ra();
let rb = instr.rb();
let rn = if is_fpr { fpr(rs) } else { gpr(rs) };
base(mnem, format!("{rn}, {}, {}", gpr(ra), gpr(rb)), 8)
}
fn fmt_lswi_stswi(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let rt = instr.rd();
let ra = instr.ra();
let nb = instr.nb();
base(mnem, format!("{}, {}, {nb}", gpr(rt), gpr(ra)), 8)
}
fn fmt_cache(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let ra = instr.ra();
let rb = instr.rb();
base(mnem, format!("{}, {}", gpr(ra), gpr(rb)), 8)
}
// CR logical
fn fmt_cr_logic(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let bt = instr.crbd();
let ba = instr.crba();
let bb = instr.crbb();
base(mnem, format!("{}, {}, {}", crb(bt), crb(ba), crb(bb)), 8)
}
fn fmt_crnor(instr: &DecodedInstr) -> DisasmText {
let bt = instr.crbd();
let ba = instr.crba();
let bb = instr.crbb();
let bo = format!("{}, {}, {}", crb(bt), crb(ba), crb(bb));
if ba == bb {
with_ext("crnor", bo, 8, "crnot", format!("{}, {}", crb(bt), crb(ba)), 8)
} else {
base("crnor", bo, 8)
}
}
fn fmt_crxor(instr: &DecodedInstr) -> DisasmText {
let bt = instr.crbd();
let ba = instr.crba();
let bb = instr.crbb();
let bo = format!("{}, {}, {}", crb(bt), crb(ba), crb(bb));
if bt == ba && ba == bb {
with_ext("crxor", bo, 8, "crclr", crb(bt), 8)
} else {
base("crxor", bo, 8)
}
}
fn fmt_creqv(instr: &DecodedInstr) -> DisasmText {
let bt = instr.crbd();
let ba = instr.crba();
let bb = instr.crbb();
let bo = format!("{}, {}, {}", crb(bt), crb(ba), crb(bb));
if bt == ba && ba == bb {
with_ext("creqv", bo, 8, "crset", crb(bt), 8)
} else {
base("creqv", bo, 8)
}
}
fn fmt_cror(instr: &DecodedInstr) -> DisasmText {
let bt = instr.crbd();
let ba = instr.crba();
let bb = instr.crbb();
let bo = format!("{}, {}, {}", crb(bt), crb(ba), crb(bb));
if ba == bb {
with_ext("cror", bo, 8, "crmove", format!("{}, {}", crb(bt), crb(ba)), 8)
} else {
base("cror", bo, 8)
}
}
// FPU
fn fmt_a_3op(instr: &DecodedInstr, mnem: &str, use_frc: bool) -> DisasmText {
let frt = instr.rd();
let fra = instr.ra();
let frb = instr.rb();
let frc = instr.rc();
let rc = rc_dot(instr);
let full = format!("{mnem}{rc}");
let ops = if use_frc {
format!("{}, {}, {}", fpr(frt), fpr(fra), fpr(frc))
} else {
format!("{}, {}, {}", fpr(frt), fpr(fra), fpr(frb))
};
base(&full, ops, 8)
}
fn fmt_a_unary(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let frt = instr.rd();
let frb = instr.rb();
let rc = rc_dot(instr);
base(&format!("{mnem}{rc}"), format!("{}, {}", fpr(frt), fpr(frb)), 8)
}
fn fmt_a_4op(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let frt = instr.rd();
let fra = instr.ra();
let frb = instr.rb();
let frc = instr.rc();
let rc = rc_dot(instr);
base(&format!("{mnem}{rc}"),
format!("{}, {}, {}, {}", fpr(frt), fpr(fra), fpr(frc), fpr(frb)), 8)
}
fn fmt_fcmp(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let bf = instr.crfd();
let fra = instr.ra();
let frb = instr.rb();
base(mnem, format!("cr{bf}, {}, {}", fpr(fra), fpr(frb)), 8)
}
fn fmt_x_fpu_unary(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let frt = instr.rd();
let frb = instr.rb();
let rc = rc_dot(instr);
base(&format!("{mnem}{rc}"), format!("{}, {}", fpr(frt), fpr(frb)), 8)
}
fn fmt_mtfsb(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let bt = instr.crbd();
let rc = rc_dot(instr);
base(&format!("{mnem}{rc}"), format!("{bt}"), 8)
}
// VMX (5-bit registers).
fn fmt_vmx_3op(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.rd();
let va = instr.ra();
let vb = instr.rb();
base(mnem, format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 8)
}
fn fmt_vmx_unary(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.rd();
let vb = instr.rb();
base(mnem, format!("{}, {}", vr(vd), vr(vb)), 8)
}
fn fmt_vmx_uimm(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.rd();
let vb = instr.rb();
let uimm = instr.ra() as u32;
base(mnem, format!("{}, {}, {uimm}", vr(vd), vr(vb)), 8)
}
fn fmt_vmx_simm(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.rd();
let simm = sign_ext(instr.ra() as u32, 5);
base(mnem, format!("{}, {simm}", vr(vd)), 9)
}
fn fmt_vmx_cmp(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.rd();
let va = instr.ra();
let vb = instr.rb();
// Rc bit at position 22 (0-indexed from MSB)
let rc = if (instr.raw >> 10) & 1 != 0 { "." } else { "" };
let full = format!("{mnem}{rc}");
base(&full, format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 12)
}
fn fmt_vmx_4op(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.rd();
let va = instr.ra();
let vb = instr.rb();
let vc = instr.rc();
base(mnem, format!("{}, {}, {}, {}", vr(vd), vr(va), vr(vb), vr(vc)), 12)
}
fn fmt_vmx_4op_swap(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.rd();
let va = instr.ra();
let vb = instr.rb();
let vc = instr.rc();
base(mnem, format!("{}, {}, {}, {}", vr(vd), vr(va), vr(vc), vr(vb)), 9)
}
fn fmt_vsldoi(instr: &DecodedInstr) -> DisasmText {
let vd = instr.rd();
let va = instr.ra();
let vb = instr.rb();
let sh = (instr.raw >> 6) & 0xF;
base("vsldoi", format!("{}, {}, {}, {sh}", vr(vd), vr(va), vr(vb)), 8)
}
fn fmt_vmx_ls(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.rd();
let ra = instr.ra();
let rb = instr.rb();
base(mnem, format!("{}, {}, {}", vr(vd), gpr(ra), gpr(rb)), 8)
}
// VMX128 — uses canonical va128/vb128/vd128 accessors from decoder.rs.
// (Silently fixes the prior ppc.rs bug where these used wrong bit positions.)
fn fmt_vmx128_ls(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.vd128();
let ra = instr.ra();
let rb = instr.rb();
base(mnem, format!("{}, {}, {}", vr(vd), gpr(ra), gpr(rb)), 12)
}
fn fmt_vmx128_3op(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.vd128();
let va = instr.va128();
let vb = instr.vb128();
base(mnem, format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 12)
}
// VMX128 multiply-add forms (VX128_2): the addend is the VD register
// re-used, not a separate VC field. Operand order differs between
// `vmaddfp128` (VD, VA, VB, VD) and the `vmaddcfp128`/`vnmsubfp128`
// pair (VD, VA, VD, VB), per canary's authoritative formatters in
// xenia-canary/src/xenia/cpu/ppc/ppc_opcode_disasm_gen.cc.
fn fmt_vmaddfp128(instr: &DecodedInstr) -> DisasmText {
let vd = instr.vd128();
let va = instr.va128();
let vb = instr.vb128();
base(
"vmaddfp128",
format!("{}, {}, {}, {}", vr(vd), vr(va), vr(vb), vr(vd)),
12,
)
}
fn fmt_vmx128_madd_vd_vb(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.vd128();
let va = instr.va128();
let vb = instr.vb128();
base(
mnem,
format!("{}, {}, {}, {}", vr(vd), vr(va), vr(vd), vr(vb)),
12,
)
}
fn fmt_vperm128(instr: &DecodedInstr) -> DisasmText {
let vd = instr.vd128();
let va = instr.va128();
let vb = instr.vb128();
let vc = (instr.raw >> 6) & 0x7;
base("vperm128", format!("{}, {}, {}, {vc}", vr(vd), vr(va), vr(vb)), 9)
}
fn fmt_vsldoi128(instr: &DecodedInstr) -> DisasmText {
let vd = instr.vd128();
let va = instr.va128();
let vb = instr.vb128();
let sh = (instr.raw >> 6) & 0xF;
base("vsldoi128", format!("{}, {}, {}, {sh}", vr(vd), vr(va), vr(vb)), 10)
}
fn fmt_vpermwi128(instr: &DecodedInstr) -> DisasmText {
let vd = instr.vd128();
let vb = instr.vb128();
// UIMM combines bits 11-15 (low 5) with bits 23-25 (upper 3).
let lo = (instr.raw >> 16) & 0x1F;
let hi = (instr.raw >> 6) & 0x7;
let uimm = lo | (hi << 5);
base("vpermwi128", format!("{}, {}, 0x{uimm:X}", vr(vd), vr(vb)), 11)
}
fn fmt_vmx128_pack_d3d(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.vd128();
let vb = instr.vb128();
let imm = (instr.raw >> 16) & 0x1F;
let z = (instr.raw >> 6) & 0x3;
base(mnem, format!("{}, {}, {imm}, {z}", vr(vd), vr(vb)), 10)
}
fn fmt_vmx128_unary(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.vd128();
let vb = instr.vb128();
base(mnem, format!("{}, {}", vr(vd), vr(vb)), 12)
}
fn fmt_vmx128_uimm(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.vd128();
let vb = instr.vb128();
let uimm = extract_vx128_uimm5(instr.raw);
base(mnem, format!("{}, {}, {uimm}", vr(vd), vr(vb)), 12)
}
fn fmt_vmx128_cmp(instr: &DecodedInstr, mnem: &str) -> DisasmText {
let vd = instr.vd128();
let va = instr.va128();
let vb = instr.vb128();
// Rc bit at position 25 in VMX128 cmp form.
let rc = if (instr.raw >> 6) & 1 != 0 { "." } else { "" };
let full = format!("{mnem}{rc}");
base(&full, format!("{}, {}, {}", vr(vd), vr(va), vr(vb)), 14)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::decoder::decode;
#[test]
fn nop_collapses_via_extended() {
let instr = decode(0x60000000, 0);
let t = format(&instr);
assert_eq!(t.mnemonic, "ori");
assert_eq!(t.ext_mnemonic.as_deref(), Some("nop"));
assert_eq!(t.display(), "nop");
}
#[test]
fn addi_to_li_when_ra_zero() {
// addi r3, r0, 16
let raw = (14u32 << 26) | (3 << 21) | (0 << 16) | 16;
let instr = decode(raw, 0);
let t = format(&instr);
assert_eq!(t.mnemonic, "addi");
assert_eq!(t.ext_mnemonic.as_deref(), Some("li"));
assert_eq!(t.ext_operands.as_deref(), Some("r3, 16"));
}
#[test]
fn rlwinm_dot_preserves_record_bit() {
// Same pattern as the Sylpheed graphics-callback test:
// rlwinm. r11, r11, 0, 31, 31 with Rc=1
let raw = (21u32 << 26) | (11 << 21) | (11 << 16)
| (0 << 11) | (31 << 6) | (31 << 1) | 1;
let instr = decode(raw, 0);
let t = format(&instr);
assert!(t.disasm.starts_with("rlwinm."), "got: {}", t.disasm);
}
#[test]
fn rlwinm_no_dot_when_rc_unset() {
let raw = (21u32 << 26) | (11 << 21) | (11 << 16)
| (0 << 11) | (31 << 6) | (31 << 1);
let instr = decode(raw, 0);
let t = format(&instr);
assert_eq!(t.mnemonic, "rlwinm");
assert!(!t.mnemonic.ends_with('.'));
}
#[test]
fn or_with_same_source_is_mr() {
// or r3, r4, r4 → mr r3, r4
let raw = (31u32 << 26) | (4 << 21) | (3 << 16) | (4 << 11) | (444 << 1);
let instr = decode(raw, 0);
let t = format(&instr);
assert_eq!(t.ext_mnemonic.as_deref(), Some("mr"));
assert_eq!(t.ext_operands.as_deref(), Some("r3, r4"));
}
#[test]
fn unconditional_branch_resolves_target() {
// b +0x100 with addr=0x82000000
let raw = (18u32 << 26) | (0x40 << 2);
let instr = decode(raw, 0x82000000);
let t = format(&instr);
assert_eq!(t.mnemonic, "b");
assert_eq!(t.branch_target, Some(0x82000100));
assert_eq!(t.operands, "0x82000100");
}
#[test]
fn bclr_unconditional_is_blr() {
// bclr 20, 0
let raw = (19u32 << 26) | (20 << 21) | (0 << 16) | (16 << 1);
let instr = decode(raw, 0);
let t = format(&instr);
assert_eq!(t.ext_mnemonic.as_deref(), Some("blr"));
}
#[test]
fn back_compat_disassemble_returns_display() {
let instr = decode(0x60000000, 0);
assert_eq!(disassemble(&instr), "nop");
}
#[test]
fn iter_disasm_walks_byte_slice_in_order() {
// Three instructions at 0x82000000: nop, addi r3,r0,16, b +0x100.
let mut bytes = Vec::new();
bytes.extend_from_slice(&0x60000000u32.to_be_bytes()); // nop
bytes.extend_from_slice(&((14u32 << 26) | (3 << 21) | (0 << 16) | 16).to_be_bytes()); // addi
bytes.extend_from_slice(&((18u32 << 26) | (0x40 << 2)).to_be_bytes()); // b +0x100
let items: Vec<_> = super::iter_disasm(&bytes, 0x82000000, 0x82000000, 0x82000000 + 12)
.collect();
assert_eq!(items.len(), 3);
assert_eq!(items[0].addr, 0x82000000);
assert_eq!(items[0].text.ext_mnemonic.as_deref(), Some("nop"));
assert_eq!(items[1].addr, 0x82000004);
assert_eq!(items[1].text.ext_mnemonic.as_deref(), Some("li"));
assert_eq!(items[2].addr, 0x82000008);
assert_eq!(items[2].text.branch_target, Some(0x82000108));
}
#[test]
fn iter_disasm_stops_on_truncated_tail() {
// 6 bytes — one full instruction + 2 dangling. Iterator must yield exactly 1.
let mut bytes = Vec::new();
bytes.extend_from_slice(&0x60000000u32.to_be_bytes());
bytes.push(0x60); bytes.push(0x00);
let items: Vec<_> = super::iter_disasm(&bytes, 0, 0, 6).collect();
assert_eq!(items.len(), 1);
}
}