diff --git a/crates/xenia-jit/src/emit.rs b/crates/xenia-jit/src/emit.rs new file mode 100644 index 0000000..3321e61 --- /dev/null +++ b/crates/xenia-jit/src/emit.rs @@ -0,0 +1,237 @@ +//! Native x64 emitters for individual PPC opcodes (context-threading). +//! +//! Each emitter mirrors exactly one interpreter arm +//! (`xenia-cpu/src/interpreter.rs`) and leaves guest state in `PpcContext` +//! identical to what the interpreter would produce, then advances `pc` by 4 and +//! bumps the cycle/timebase counters. Emitters cover only the cases they can +//! reproduce faithfully; any variant they can't (recording `.` forms, `OE` +//! overflow forms, the `db16cyc` spin hint) returns `false` from +//! [`try_emit_native`] so the block falls back to the interpreter for that +//! instruction — keeping every result byte-identical. +//! +//! Register convention (set up by the block prologue): `r15` = `*mut +//! PpcContext`, `rbx` = `*mut JitEnv`. Emitters use `rax`/`rcx` as scratch +//! (caller-saved; no cross-instruction state — all guest state lives in +//! `PpcContext`). PPC operand roles follow the ISA: arithmetic D-forms write +//! `rD` (`instr.rd()`); logical forms write `rA` (`instr.ra()`) from `rS` +//! (`instr.rs()`). + +use dynasmrt::{DynasmApi, dynasm}; +use xenia_cpu::PpcOpcode; +use xenia_cpu::context::PpcContext; +use xenia_cpu::decoder::DecodedInstr; + +use crate::JitEnv; + +/// Byte offsets into `JitEnv`/`PpcContext`, resolved once per block compile. +pub struct Offsets { + pub env_ctx: i32, + gpr: i32, + pub pc: i32, + pub cycle: i32, + pub timebase: i32, +} + +impl Offsets { + pub fn resolve() -> Self { + Offsets { + env_ctx: core::mem::offset_of!(JitEnv, ctx) as i32, + gpr: core::mem::offset_of!(PpcContext, gpr) as i32, + pc: core::mem::offset_of!(PpcContext, pc) as i32, + cycle: core::mem::offset_of!(PpcContext, cycle_count) as i32, + timebase: core::mem::offset_of!(PpcContext, timebase) as i32, + } + } + /// Byte offset of guest register `i` (`gpr[i]`, 8 bytes each). + #[inline] + fn gpr(&self, i: usize) -> i32 { + self.gpr + (i as i32) * 8 + } +} + +type Asm = dynasmrt::x64::Assembler; + +/// Emit `pc += 4; cycle_count += 1; timebase += 1` — the postlude every native +/// (non-branch) opcode shares, matching `interpreter.rs` (`execute` does +/// `ctx.pc += 4`, `step_block` bumps the counters after each instruction). +#[inline] +fn advance_and_count(ops: &mut Asm, off: &Offsets) { + dynasm!(ops + ; .arch x64 + ; add DWORD [r15 + off.pc], 4 + ; inc QWORD [r15 + off.cycle] + ; inc QWORD [r15 + off.timebase] + ); +} + +/// Try to emit native x64 for `instr`. Returns `true` if it fully handled the +/// instruction (computation + `advance_and_count`); `false` if the caller must +/// fall back to the interpreter for it. +pub fn try_emit_native(ops: &mut Asm, off: &Offsets, instr: &DecodedInstr) -> bool { + let ra = instr.ra(); + let rb = instr.rb(); + let rd = instr.rd(); // == rs() + match instr.opcode { + // rD = (rA==0 ? 0 : gpr[rA]) + EXTS(SIMM) [64-bit; never records] + PpcOpcode::addi => { + let simm = instr.simm16() as i32; // fits i32 (16-bit sign-extended) + load_ra_or_zero(ops, off, ra); + if simm != 0 { + dynasm!(ops ; .arch x64 ; add rax, simm); + } + dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax); + advance_and_count(ops, off); + true + } + // rD = (rA==0 ? 0 : gpr[rA]) + (EXTS(SIMM) << 16) [64-bit] + PpcOpcode::addis => { + // (i16 sign-extended) << 16 occupies bits 16..31 with the sign in + // bit 31 — exactly an i32, so it fits an `add r64, imm32`. + let simm = (instr.simm16() as i32) << 16; + load_ra_or_zero(ops, off, ra); + if simm != 0 { + dynasm!(ops ; .arch x64 ; add rax, simm); + } + dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax); + advance_and_count(ops, off); + true + } + // gpr[rA] = gpr[rS] | ZEXT(UIMM) [never records] + PpcOpcode::ori => { + let uimm = instr.uimm16() as i32; // 0..65535 -> positive i32 (zext == sext) + dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)]); + if uimm != 0 { + dynasm!(ops ; .arch x64 ; or rax, uimm); + } + dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax); + advance_and_count(ops, off); + true + } + // gpr[rA] = gpr[rS] | (ZEXT(UIMM) << 16) + PpcOpcode::oris => { + let imm = (instr.uimm16() as u32) << 16; + dynasm!(ops ; .arch x64 + ; mov rax, [r15 + off.gpr(rd)] + ; mov ecx, imm as i32 // zero-extends into rcx + ; or rax, rcx + ; mov [r15 + off.gpr(ra)], rax + ); + advance_and_count(ops, off); + true + } + // gpr[rA] = gpr[rS] ^ ZEXT(UIMM) + PpcOpcode::xori => { + let uimm = instr.uimm16() as i32; + dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)]); + if uimm != 0 { + dynasm!(ops ; .arch x64 ; xor rax, uimm); + } + dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax); + advance_and_count(ops, off); + true + } + // gpr[rA] = gpr[rS] ^ (ZEXT(UIMM) << 16) + PpcOpcode::xoris => { + let imm = (instr.uimm16() as u32) << 16; + dynasm!(ops ; .arch x64 + ; mov rax, [r15 + off.gpr(rd)] + ; mov ecx, imm as i32 + ; xor rax, rcx + ; mov [r15 + off.gpr(ra)], rax + ); + advance_and_count(ops, off); + true + } + // gpr[rA] = gpr[rS] | gpr[rB] [64-bit]. Skip the db16cyc spin hint + // (returns Yield) and the recording form. + PpcOpcode::orx => { + if instr.rc_bit() || instr.raw == 0x7FFF_FB78 { + return false; + } + dynasm!(ops ; .arch x64 + ; mov rax, [r15 + off.gpr(rd)] + ; or rax, [r15 + off.gpr(rb)] + ; mov [r15 + off.gpr(ra)], rax + ); + advance_and_count(ops, off); + true + } + // gpr[rA] = gpr[rS] & gpr[rB] [64-bit] + PpcOpcode::andx => { + if instr.rc_bit() { + return false; + } + dynasm!(ops ; .arch x64 + ; mov rax, [r15 + off.gpr(rd)] + ; and rax, [r15 + off.gpr(rb)] + ; mov [r15 + off.gpr(ra)], rax + ); + advance_and_count(ops, off); + true + } + // gpr[rA] = gpr[rS] ^ gpr[rB] [64-bit] + PpcOpcode::xorx => { + if instr.rc_bit() { + return false; + } + dynasm!(ops ; .arch x64 + ; mov rax, [r15 + off.gpr(rd)] + ; xor rax, [r15 + off.gpr(rb)] + ; mov [r15 + off.gpr(ra)], rax + ); + advance_and_count(ops, off); + true + } + // rD = gpr[rA] + gpr[rB] [64-bit]; skip OE/recording forms. + PpcOpcode::addx => { + if instr.oe() || instr.rc_bit() { + return false; + } + dynasm!(ops ; .arch x64 + ; mov rax, [r15 + off.gpr(ra)] + ; add rax, [r15 + off.gpr(rb)] + ; mov [r15 + off.gpr(rd)], rax + ); + advance_and_count(ops, off); + true + } + // rD = gpr[rB] - gpr[rA] [64-bit]; skip OE/recording forms. + PpcOpcode::subfx => { + if instr.oe() || instr.rc_bit() { + return false; + } + dynasm!(ops ; .arch x64 + ; mov rax, [r15 + off.gpr(rb)] + ; sub rax, [r15 + off.gpr(ra)] + ; mov [r15 + off.gpr(rd)], rax + ); + advance_and_count(ops, off); + true + } + // rD = 0 - gpr[rA] [64-bit]; skip OE/recording forms. + PpcOpcode::negx => { + if instr.oe() || instr.rc_bit() { + return false; + } + dynasm!(ops ; .arch x64 + ; mov rax, [r15 + off.gpr(ra)] + ; neg rax + ; mov [r15 + off.gpr(rd)], rax + ); + advance_and_count(ops, off); + true + } + _ => false, + } +} + +/// Load `gpr[ra]` into `rax`, or zero `rax` when `ra == 0` (the PPC +/// arithmetic-D "RA=0 means literal 0" rule, statically known here). +#[inline] +fn load_ra_or_zero(ops: &mut Asm, off: &Offsets, ra: usize) { + if ra == 0 { + dynasm!(ops ; .arch x64 ; xor eax, eax); + } else { + dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(ra)]); + } +} diff --git a/crates/xenia-jit/src/lib.rs b/crates/xenia-jit/src/lib.rs index 2e19473..512f71a 100644 --- a/crates/xenia-jit/src/lib.rs +++ b/crates/xenia-jit/src/lib.rs @@ -37,6 +37,10 @@ use xenia_cpu::decoder::DecodedInstr; use xenia_cpu::interpreter::{StepResult, interpret_one}; use xenia_memory::MemoryAccess; +mod emit; +#[cfg(test)] +mod tests; + /// Runtime environment handed to a compiled block. The emitted prologue reads /// only `ctx` (via `offset_of!`); `mem` and `last_result` are touched solely by /// the Rust helpers. `mem` is a real fat raw pointer, so no transmute is needed @@ -124,10 +128,7 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock { let instrs: Box<[DecodedInstr]> = block.instrs.clone().into_boxed_slice(); // Field offsets resolved at compile time — robust to struct layout. - let off_ctx = core::mem::offset_of!(JitEnv, ctx) as i32; - let off_cycle = core::mem::offset_of!(PpcContext, cycle_count) as i32; - let off_timebase = core::mem::offset_of!(PpcContext, timebase) as i32; - let off_pc = core::mem::offset_of!(PpcContext, pc) as i32; + let off = emit::Offsets::resolve(); let helper = jit_interpret_one as usize as i64; let mut ops = dynasmrt::x64::Assembler::new().expect("dynasm assembler"); @@ -144,10 +145,17 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock { ; push r15 ; sub rsp, 8 ; mov rbx, rdi - ; mov r15, [rbx + off_ctx] + ; mov r15, [rbx + off.env_ctx] ); for instr in instrs.iter() { + // Native fast path: emitters fully handle the instruction (compute + + // pc+=4 + counter bumps) and never diverge control flow, so no exit + // checks are needed after them. + if emit::try_emit_native(&mut ops, &off, instr) { + continue; + } + // Interpreter fallback for un-ported opcodes. let instr_ptr = instr as *const DecodedInstr as usize as i64; let expected_next = instr.addr.wrapping_add(4) as i32; dynasm!(ops @@ -158,13 +166,13 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock { ; mov rax, QWORD helper ; call rax // determinism postlude: cycle_count += 1; timebase += 1 - ; inc QWORD [r15 + off_cycle] - ; inc QWORD [r15 + off_timebase] + ; inc QWORD [r15 + off.cycle] + ; inc QWORD [r15 + off.timebase] // non-Continue result -> exit returning the discriminant in eax ; test eax, eax ; jnz =>l_exit // taken-branch (pc discontinuity) -> stop the block, return Continue - ; cmp DWORD [r15 + off_pc], expected_next + ; cmp DWORD [r15 + off.pc], expected_next ; jne =>l_cont ); } diff --git a/crates/xenia-jit/src/tests.rs b/crates/xenia-jit/src/tests.rs new file mode 100644 index 0000000..35b953f --- /dev/null +++ b/crates/xenia-jit/src/tests.rs @@ -0,0 +1,223 @@ +//! Differential opcode tests: for each natively-emitted opcode, run a random +//! register state through the interpreter (`interpret_one`, then the same +//! per-instruction counter bump `step_block` does) and through a JIT-compiled +//! one-instruction block, and assert the resulting `PpcContext` (GPRs, PC, +//! XER, CR, cycle/timebase) and `StepResult` are identical. This makes the +//! hand-written x64 as trustworthy as the interpreter. + +use super::{CompiledBlock, compile_block, run_jit_block}; +use crate::emit; +use xenia_cpu::block_cache::DecodedBlock; +use xenia_cpu::context::PpcContext; +use xenia_cpu::decode; +use xenia_cpu::decoder::DecodedInstr; +use xenia_cpu::interpreter::interpret_one; +use xenia_memory::MemoryAccess; + +/// Memory that must never be touched — the ALU opcodes tested here don't +/// access guest memory, so any call is a bug in the emitter. +struct NoMem; +impl MemoryAccess for NoMem { + fn read_u8(&self, _: u32) -> u8 { + unreachable!("ALU emitter touched memory (read_u8)") + } + fn read_u16(&self, _: u32) -> u16 { + unreachable!("ALU emitter touched memory (read_u16)") + } + fn read_u32(&self, _: u32) -> u32 { + unreachable!("ALU emitter touched memory (read_u32)") + } + fn read_u64(&self, _: u32) -> u64 { + unreachable!("ALU emitter touched memory (read_u64)") + } + fn write_u8(&self, _: u32, _: u8) { + unreachable!("ALU emitter touched memory (write_u8)") + } + fn write_u16(&self, _: u32, _: u16) { + unreachable!("ALU emitter touched memory (write_u16)") + } + fn write_u32(&self, _: u32, _: u32) { + unreachable!("ALU emitter touched memory (write_u32)") + } + fn write_u64(&self, _: u32, _: u64) { + unreachable!("ALU emitter touched memory (write_u64)") + } + fn translate(&self, _: u32) -> Option<*const u8> { + None + } + fn translate_mut(&self, _: u32) -> Option<*mut u8> { + None + } +} + +// ---- instruction encoders (PPC big-endian field layout; `raw` is the u32) ---- + +/// D-form: `op | b6_10<<21 | b11_15<<16 | imm16`. +fn enc_d(op: u32, b6_10: u32, b11_15: u32, imm16: u16) -> u32 { + (op << 26) | (b6_10 << 21) | (b11_15 << 16) | (imm16 as u32) +} +/// X-form (logical register): `op | b6_10<<21 | b11_15<<16 | b16_20<<11 | xo<<1 | rc`. +fn enc_x(op: u32, b6_10: u32, b11_15: u32, b16_20: u32, xo: u32, rc: u32) -> u32 { + (op << 26) | (b6_10 << 21) | (b11_15 << 16) | (b16_20 << 11) | (xo << 1) | rc +} +/// XO-form (arithmetic register): X-form plus the OE bit at position 10. +fn enc_xo(op: u32, b6_10: u32, b11_15: u32, b16_20: u32, oe: u32, xo: u32, rc: u32) -> u32 { + (op << 26) | (b6_10 << 21) | (b11_15 << 16) | (b16_20 << 11) | (oe << 10) | (xo << 1) | rc +} + +/// Deterministic PRNG (splitmix64-ish) so failures are reproducible. +fn rng(state: &mut u64) -> u64 { + *state = state.wrapping_add(0x9E37_79B9_7F4A_7C15); + let mut z = *state; + z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9); + z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB); + z ^ (z >> 31) +} + +fn ctx_from_gpr(gpr: [u64; 32], pc: u32) -> PpcContext { + let mut c = PpcContext::new(); + c.gpr = gpr; + c.pc = pc; + c +} + +/// Assert the JIT emits `raw` natively and reproduces the interpreter exactly +/// for the given register seed. +fn check(raw: u32, gpr: [u64; 32]) { + let pc = 0x8200_1000u32; + let instr: DecodedInstr = decode(raw, pc); + let off = emit::Offsets::resolve(); + + // Guard: the opcode must actually be natively emitted (else this test is + // vacuous — a fallback would trivially match). + let mut probe = dynasmrt::x64::Assembler::new().unwrap(); + assert!( + emit::try_emit_native(&mut probe, &off, &instr), + "opcode not natively emitted for raw={raw:#010x} ({:?})", + instr.opcode + ); + + // Interpreter reference: interpret_one, then the counter bump step_block does. + let mem = NoMem; + let mut a = ctx_from_gpr(gpr, pc); + let ra = interpret_one(&mut a, &mem, &instr); + a.cycle_count += 1; + a.timebase += 1; + + // JIT. + let mut b = ctx_from_gpr(gpr, pc); + let block = DecodedBlock { + start_pc: pc, + end_pc: pc.wrapping_add(4), + page_version: 0, + instrs: vec![instr], + sync_sensitive: false, + }; + let cb: CompiledBlock = compile_block(&block); + let rb = run_jit_block(&cb, &mut b, &mem); + + assert_eq!(a.gpr, b.gpr, "gpr mismatch raw={raw:#010x} ({:?})", instr.opcode); + assert_eq!(a.pc, b.pc, "pc mismatch raw={raw:#010x}"); + assert_eq!(a.cycle_count, b.cycle_count, "cycle mismatch raw={raw:#010x}"); + assert_eq!(a.timebase, b.timebase, "timebase mismatch raw={raw:#010x}"); + assert_eq!(a.xer_ca, b.xer_ca, "xer_ca mismatch raw={raw:#010x}"); + assert_eq!(a.xer_ov, b.xer_ov, "xer_ov mismatch raw={raw:#010x}"); + assert_eq!(a.xer_so, b.xer_so, "xer_so mismatch raw={raw:#010x}"); + assert_eq!(ra, rb, "StepResult mismatch raw={raw:#010x}"); +} + +/// Random register seeds, including hard boundary values in a few slots. +fn fuzz_gpr(seed: &mut u64) -> [u64; 32] { + let mut g = [0u64; 32]; + for slot in g.iter_mut() { + *slot = rng(seed); + } + // Sprinkle in edge values. + g[0] = 0; + g[3] = 0xFFFF_FFFF; + g[4] = 0x8000_0000; + g[5] = 0xFFFF_FFFF_FFFF_FFFF; + g[6] = 0x0000_0000_8000_0000; + g +} + +const ITERS: usize = 2000; + +#[test] +fn addi_matches() { + let mut s = 0x1234u64; + for _ in 0..ITERS { + let g = fuzz_gpr(&mut s); + let rd = (rng(&mut s) % 32) as u32; + let ra = (rng(&mut s) % 32) as u32; + let imm = rng(&mut s) as u16; + check(enc_d(14, rd, ra, imm), g); // addi + check(enc_d(15, rd, ra, imm), g); // addis + } +} + +#[test] +fn logical_imm_matches() { + let mut s = 0x5678u64; + for _ in 0..ITERS { + let g = fuzz_gpr(&mut s); + let rs = (rng(&mut s) % 32) as u32; + let ra = (rng(&mut s) % 32) as u32; + let uimm = rng(&mut s) as u16; + check(enc_d(24, rs, ra, uimm), g); // ori + check(enc_d(25, rs, ra, uimm), g); // oris + check(enc_d(26, rs, ra, uimm), g); // xori + check(enc_d(27, rs, ra, uimm), g); // xoris + } +} + +#[test] +fn logical_reg_matches() { + let mut s = 0x9abcu64; + for _ in 0..ITERS { + let g = fuzz_gpr(&mut s); + let rs = (rng(&mut s) % 32) as u32; + let ra = (rng(&mut s) % 32) as u32; + let rb = (rng(&mut s) % 32) as u32; + check(enc_x(31, rs, ra, rb, 444, 0), g); // or + check(enc_x(31, rs, ra, rb, 28, 0), g); // and + check(enc_x(31, rs, ra, rb, 316, 0), g); // xor + } +} + +#[test] +fn arith_reg_matches() { + let mut s = 0xdef0u64; + for _ in 0..ITERS { + let g = fuzz_gpr(&mut s); + let rd = (rng(&mut s) % 32) as u32; + let ra = (rng(&mut s) % 32) as u32; + let rb = (rng(&mut s) % 32) as u32; + check(enc_xo(31, rd, ra, rb, 0, 266, 0), g); // add + check(enc_xo(31, rd, ra, rb, 0, 40, 0), g); // subf + check(enc_xo(31, rd, ra, rb, 0, 104, 0), g); // neg (rb ignored) + } +} + +/// The recording/OE forms and the db16cyc hint must NOT be natively emitted +/// (they fall back to the interpreter). Guards against a future emitter +/// accidentally handling a form it can't reproduce. +#[test] +fn recording_and_hint_forms_fall_back() { + let off = emit::Offsets::resolve(); + let mut probe = dynasmrt::x64::Assembler::new().unwrap(); + let cases = [ + enc_xo(31, 3, 4, 5, 0, 266, 1), // add. + enc_xo(31, 3, 4, 5, 1, 266, 0), // addo + enc_x(31, 3, 4, 5, 444, 1), // or. + 0x7FFF_FB78u32, // db16cyc (or r31,r31,r31) + ]; + for raw in cases { + let instr = decode(raw, 0x8200_1000); + assert!( + !emit::try_emit_native(&mut probe, &off, &instr), + "raw={raw:#010x} ({:?}) should fall back, not native", + instr.opcode + ); + } +}