wip: extract the xexdb tool closure

This commit is contained in:
MechaCat02
2026-09-13 19:31:49 +02:00
parent 5aeca0a55a
commit 86da451bfe
52 changed files with 19190 additions and 1 deletions

View File

@@ -0,0 +1,10 @@
[package]
name = "sylpheed-ppc"
version = "0.1.0"
edition = "2024"
description = "PowerPC decode and disassembly, for static analysis"
[dependencies]
tracing = "0.1"
bitflags = "2"
thiserror = "1"

View File

@@ -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 -- <table.txt>
//! ```
//! where each line is `0xWORD name`.
use std::io::BufRead;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args().nth(1).ok_or("usage: decode_table_check <table>")?;
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 = sylpheed_ppc::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(())
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
//! PowerPC decode and disassembly, for static analysis.
//!
//! Lifted from `xenia-rs`'s `xenia-cpu` when that emulator was retired. Only
//! three modules came: the interpreter, JIT, scheduler and VMX are the parts
//! this project no longer runs, and `sylpheed-xexdb` never referenced them —
//! it used exactly `decoder::decode`, `disasm::DisasmItem` and `disasm::format`.
//!
//! See `docs/agents/CONSOLIDATION.md` Phase 3.
pub mod decoder;
pub mod disasm;
pub mod opcode;

View File

@@ -0,0 +1,308 @@
/// All PPC opcodes supported by the Xbox 360, including VMX128 extensions.
/// Directly mirrors the C++ PPCOpcode enum from ppc_opcode.h.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
#[allow(non_camel_case_types)]
pub enum PpcOpcode {
// ALU
addcx, addex, addi, addic, addicx, addis, addmex, addx, addzex,
andcx, andisx, andix, andx,
// Branch
bcctrx, bclrx, bcx, bx,
// Compare
cmp, cmpi, cmpl, cmpli,
// Count leading zeros
cntlzdx, cntlzwx,
// Condition register
crand, crandc, creqv, crnand, crnor, cror, crorc, crxor,
// Data cache
dcbf, dcbi, dcbst, dcbt, dcbtst, dcbz, dcbz128,
// Division
divdux, divdx, divwux, divwx,
// Sync/barrier
eieio,
// Logical
eqvx, extsbx, extshx, extswx,
// FPU
fabsx, faddsx, faddx, fcfidx, fcmpo, fcmpu, fctidx, fctidzx, fctiwx, fctiwzx,
fdivsx, fdivx, fmaddsx, fmaddx, fmrx, fmsubsx, fmsubx, fmulsx, fmulx,
fnabsx, fnegx, fnmaddsx, fnmaddx, fnmsubsx, fnmsubx, fresx, frspx, frsqrtex,
fselx, fsqrtsx, fsqrtx, fsubsx, fsubx,
// Instruction cache
icbi, isync,
// Load byte
lbz, lbzu, lbzux, lbzx,
// Load doubleword
ld, ldarx, ldbrx, ldu, ldux, ldx,
// Load float
lfd, lfdu, lfdux, lfdx, lfs, lfsu, lfsux, lfsx,
// Load halfword
lha, lhau, lhaux, lhax, lhbrx, lhz, lhzu, lhzux, lhzx,
// Load multiple/string
lmw, lswi, lswx,
// Load vector
lvebx, lvehx, lvewx, lvewx128, lvlx, lvlx128, lvlxl, lvlxl128,
lvrx, lvrx128, lvrxl, lvrxl128,
lvsl, lvsl128, lvsr, lvsr128,
lvx, lvx128, lvxl, lvxl128,
// Load word
lwa, lwarx, lwaux, lwax, lwbrx, lwz, lwzu, lwzux, lwzx,
// Move CR
mcrf, mcrfs, mcrxr,
// Move from special
mfcr, mffsx, mfmsr, mfspr, mftb, mfvscr,
// Move to special
mtcrf, mtfsb0x, mtfsb1x, mtfsfix, mtfsfx, mtmsr, mtmsrd, mtspr, mtvscr,
// Multiply
mulhdux, mulhdx, mulhwux, mulhwx, mulldx, mulli, mullwx,
// Logical
nandx, negx, norx, orcx, ori, oris, orx,
// Rotate
rldclx, rldcrx, rldiclx, rldicrx, rldicx, rldimix, rlwimix, rlwinmx, rlwnmx,
// System call
sc,
// Shift
sldx, slwx, sradix, sradx, srawix, srawx, srdx, srwx,
// Store byte
stb, stbu, stbux, stbx,
// Store doubleword
std, stdbrx, stdcx, stdu, stdux, stdx,
// Store float
stfd, stfdu, stfdux, stfdx, stfiwx, stfs, stfsu, stfsux, stfsx,
// Store halfword
sth, sthbrx, sthu, sthux, sthx,
// Store multiple/string
stmw, stswi, stswx,
// Store vector
stvebx, stvehx, stvewx, stvewx128, stvlx, stvlx128, stvlxl, stvlxl128,
stvrx, stvrx128, stvrxl, stvrxl128,
stvx, stvx128, stvxl, stvxl128,
// Store word
stw, stwbrx, stwcx, stwu, stwux, stwx,
// Subtract
subfcx, subfex, subficx, subfmex, subfx, subfzex,
// Sync
sync,
// Trap
td, tdi, tw, twi,
// VMX integer
vaddcuw, vaddfp, vaddfp128, vaddsbs, vaddshs, vaddsws,
vaddubm, vaddubs, vadduhm, vadduhs, vadduwm, vadduws,
vand, vand128, vandc, vandc128,
vavgsb, vavgsh, vavgsw, vavgub, vavguh, vavguw,
vcfpsxws128, vcfpuxws128, vcfsx, vcfux,
vcmpbfp, vcmpbfp128, vcmpeqfp, vcmpeqfp128,
vcmpequb, vcmpequh, vcmpequw, vcmpequw128,
vcmpgefp, vcmpgefp128, vcmpgtfp, vcmpgtfp128,
vcmpgtsb, vcmpgtsh, vcmpgtsw, vcmpgtub, vcmpgtuh, vcmpgtuw,
vcsxwfp128, vctsxs, vctuxs, vcuxwfp128,
vexptefp, vexptefp128, vlogefp, vlogefp128,
vmaddcfp128, vmaddfp, vmaddfp128,
vmaxfp, vmaxfp128, vmaxsb, vmaxsh, vmaxsw, vmaxub, vmaxuh, vmaxuw,
vmhaddshs, vmhraddshs,
vminfp, vminfp128, vminsb, vminsh, vminsw, vminub, vminuh, vminuw,
vmladduhm,
vmrghb, vmrghh, vmrghw, vmrghw128, vmrglb, vmrglh, vmrglw, vmrglw128,
vmsum3fp128, vmsum4fp128,
vmsummbm, vmsumshm, vmsumshs, vmsumubm, vmsumuhm, vmsumuhs,
vmulesb, vmulesh, vmuleub, vmuleuh, vmulfp128,
vmulosb, vmulosh, vmuloub, vmulouh,
vnmsubfp, vnmsubfp128, vnor, vnor128,
vor, vor128,
vperm, vperm128, vpermwi128, vpkd3d128,
vpkpx, vpkshss, vpkshss128, vpkshus, vpkshus128,
vpkswss, vpkswss128, vpkswus, vpkswus128,
vpkuhum, vpkuhum128, vpkuhus, vpkuhus128,
vpkuwum, vpkuwum128, vpkuwus, vpkuwus128,
vrefp, vrefp128,
vrfim, vrfim128, vrfin, vrfin128, vrfip, vrfip128, vrfiz, vrfiz128,
vrlb, vrlh, vrlimi128, vrlw, vrlw128,
vrsqrtefp, vrsqrtefp128,
vsel, vsel128,
vsl, vslb, vsldoi, vsldoi128, vslh, vslo, vslo128, vslw, vslw128,
vspltb, vsplth, vspltisb, vspltish, vspltisw, vspltisw128, vspltw, vspltw128,
vsr, vsrab, vsrah, vsraw, vsraw128, vsrb, vsrh, vsro, vsro128, vsrw, vsrw128,
vsubcuw, vsubfp, vsubfp128, vsubsbs, vsubshs, vsubsws,
vsububm, vsububs, vsubuhm, vsubuhs, vsubuwm, vsubuws,
vsum2sws, vsum4sbs, vsum4shs, vsum4ubs, vsumsws,
vupkd3d128, vupkhpx, vupkhsb, vupkhsb128, vupkhsh,
vupklpx, vupklsb, vupklsb128, vupklsh,
vxor, vxor128,
// XOR immediate
xori, xoris, xorx,
// Invalid
Invalid,
}
impl PpcOpcode {
/// Returns true if this opcode is a branch instruction.
pub fn is_branch(&self) -> bool {
matches!(self, Self::bx | Self::bcx | Self::bclrx | Self::bcctrx)
}
/// Returns true if this opcode is a system call.
pub fn is_syscall(&self) -> bool {
matches!(self, Self::sc)
}
/// Returns true if this opcode unconditionally ends a basic block:
/// any branch, system call, trap, or `Invalid` (decoder couldn't
/// recognize the instruction — execution will hit the
/// `Unimplemented` arm and we don't want to swallow the boundary
/// inside a cached block).
///
/// Notably *not* terminating: `mtmsr`/`mtmsrd`/`isync`/`mfmsr`.
/// On real hardware these have synchronization semantics (a context
/// synchronizing event for `isync`, MSR rewrite for the `mt*`s) but
/// our interpreter has no asynchronous-exception model and no
/// out-of-order execution — they execute as plain ALU/move ops and
/// don't change control flow synchronously. Block-cache replay is
/// still bit-for-bit identical to per-instruction dispatch for
/// those.
///
/// Used by the basic-block cache (`block_cache.rs`) to know when to
/// stop accumulating instructions during a forward decode walk.
pub fn terminates_block(&self) -> bool {
matches!(
self,
Self::bx | Self::bcx | Self::bclrx | Self::bcctrx
| Self::sc
| Self::td | Self::tdi | Self::tw | Self::twi
| Self::Invalid
)
}
/// Returns true if this is a load instruction.
pub fn is_load(&self) -> bool {
matches!(self,
Self::lbz | Self::lbzu | Self::lbzux | Self::lbzx |
Self::lhz | Self::lhzu | Self::lhzux | Self::lhzx |
Self::lha | Self::lhau | Self::lhaux | Self::lhax |
Self::lwz | Self::lwzu | Self::lwzux | Self::lwzx |
Self::lwa | Self::lwax | Self::lwaux |
Self::ld | Self::ldu | Self::ldux | Self::ldx |
Self::lfs | Self::lfsu | Self::lfsux | Self::lfsx |
Self::lfd | Self::lfdu | Self::lfdux | Self::lfdx |
Self::lhbrx | Self::lwbrx | Self::ldbrx |
Self::lmw | Self::lswi | Self::lswx |
Self::lwarx | Self::ldarx
)
}
/// Returns true if this is a store instruction.
pub fn is_store(&self) -> bool {
matches!(self,
Self::stb | Self::stbu | Self::stbux | Self::stbx |
Self::sth | Self::sthu | Self::sthux | Self::sthx |
Self::stw | Self::stwu | Self::stwux | Self::stwx |
Self::std | Self::stdu | Self::stdux | Self::stdx |
Self::stfs | Self::stfsu | Self::stfsux | Self::stfsx |
Self::stfd | Self::stfdu | Self::stfdux | Self::stfdx |
Self::sthbrx | Self::stwbrx | Self::stdbrx |
Self::stmw | Self::stswi | Self::stswx |
Self::stwcx | Self::stdcx | Self::stfiwx
)
}
/// Returns true if this opcode is a cross-thread synchronization
/// point at which the superblock runner MUST yield back to the
/// round-robin scheduler so the lockstep interleaving stays
/// fine-grained enough to preserve correct cross-thread ordering:
///
/// - reserved load/store (`lwarx`/`ldarx`/`stwcx.`/`stdcx.`): the
/// atomic primitive other threads race on. Running past one
/// without returning to the scheduler would let a single slot
/// win/lose a reservation across many blocks before any peer
/// observes it.
/// - memory barriers (`sync`/`eieio`/`isync`): the guest explicitly
/// demands a global ordering point here; honour it by ending the
/// superblock so the scheduler re-interleaves.
///
/// Purely a function of the opcode (no guest data), so the yield
/// decision is deterministic and the schedule reproduces byte-identically.
/// Note: `sc` (syscall) and traps already `terminates_block`, and
/// import-thunk / halt-sentinel PCs are handled by the per-block
/// prologue re-check in the superblock loop — they are not listed here.
#[inline]
pub fn is_sync_sensitive(&self) -> bool {
matches!(
self,
Self::lwarx | Self::ldarx | Self::stwcx | Self::stdcx
| Self::sync | Self::eieio | Self::isync
)
}
pub fn name(&self) -> &'static str {
match self {
Self::Invalid => "invalid",
_ => {
// Use debug formatting to get the variant name
// This is a placeholder - in practice we'd have a lookup table
"?"
}
}
}
}
impl std::fmt::Display for PpcOpcode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Debug::fmt(self, f)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn terminates_block_includes_all_branches() {
assert!(PpcOpcode::bx.terminates_block());
assert!(PpcOpcode::bcx.terminates_block());
assert!(PpcOpcode::bclrx.terminates_block());
assert!(PpcOpcode::bcctrx.terminates_block());
}
#[test]
fn terminates_block_includes_sc_and_traps() {
assert!(PpcOpcode::sc.terminates_block());
assert!(PpcOpcode::td.terminates_block());
assert!(PpcOpcode::tdi.terminates_block());
assert!(PpcOpcode::tw.terminates_block());
assert!(PpcOpcode::twi.terminates_block());
}
#[test]
fn terminates_block_includes_invalid() {
// Decoder failure must end the block — otherwise an unknown
// opcode would be replayed inside a cached block without going
// through the per-instruction Unimplemented path.
assert!(PpcOpcode::Invalid.terminates_block());
}
#[test]
fn terminates_block_excludes_straight_line_ops() {
// Common ALU and load/store ops must NOT terminate a block.
assert!(!PpcOpcode::addi.terminates_block());
assert!(!PpcOpcode::addis.terminates_block());
assert!(!PpcOpcode::addx.terminates_block());
assert!(!PpcOpcode::cmpi.terminates_block());
assert!(!PpcOpcode::cmp.terminates_block());
assert!(!PpcOpcode::lwz.terminates_block());
assert!(!PpcOpcode::stw.terminates_block());
assert!(!PpcOpcode::lbzx.terminates_block());
assert!(!PpcOpcode::ori.terminates_block());
assert!(!PpcOpcode::oris.terminates_block());
assert!(!PpcOpcode::rlwinmx.terminates_block());
}
#[test]
fn terminates_block_excludes_msr_and_sync_ops() {
// Documented decision: synchronizing ops execute as ALU within
// a block since the interpreter has no async-exception model.
assert!(!PpcOpcode::mtmsr.terminates_block());
assert!(!PpcOpcode::mtmsrd.terminates_block());
assert!(!PpcOpcode::isync.terminates_block());
assert!(!PpcOpcode::sync.terminates_block());
assert!(!PpcOpcode::mfmsr.terminates_block());
}
}