From 4194ed77a3a0f58525e2c8f10f846df27b26ba35 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 4 Jul 2026 20:20:55 +0200 Subject: [PATCH] [iterate-4C] JIT Phase 4a: native FP + 64-bit loads/stores Native lfs/lfsx/lfd/lfdx/stfs/stfsx/stfd/stfdx and ld/ldx. FP goes through dedicated helpers (jit_read_f32_as_f64/read_f64/store_f32/store_f64) that call the EXACT interpreter mem methods + as-casts, so the f32<->f64 conversion is bit-identical by construction (no hand-written cvtss2sd / NaN-payload risk); FP stores replicate the reservation-invalidation. emit_ea_x (indexed EA), emit_fp_load/store, emit_int_load64 helpers; fpr offset added. Differential test fp_loadstore_matches (2000 seeds, FPR edge patterns 0/-0/inf/NaN/denormal/rounding, asserts fpr-by-bits + mem). Golden n200m BYTE-IDENTICAL with XENIA_JIT (14 tests green). Measured (XENIA_JIT_STATS): total fallback execs 29.7M->13.5M (-55%); throughput ratio vs interp 1.16->1.10. Remaining fallback now FP-arith- dominated: fmadds 20% + fmuls 16% + fadds 8% (~44%). Co-Authored-By: Claude Opus 4.8 --- crates/xenia-jit/src/emit.rs | 128 ++++++++++++++++++++++++++++++++++ crates/xenia-jit/src/lib.rs | 41 +++++++++++ crates/xenia-jit/src/tests.rs | 93 ++++++++++++++++++++++++ 3 files changed, 262 insertions(+) diff --git a/crates/xenia-jit/src/emit.rs b/crates/xenia-jit/src/emit.rs index fe58f69..df31e73 100644 --- a/crates/xenia-jit/src/emit.rs +++ b/crates/xenia-jit/src/emit.rs @@ -51,6 +51,8 @@ enum CrByte { pub struct Offsets { pub env_ctx: i32, gpr: i32, + /// Base of the `fpr: [f64; 32]` array (8 bytes each). + fpr: i32, pub pc: i32, pub cycle: i32, pub timebase: i32, @@ -77,6 +79,7 @@ impl Offsets { Offsets { env_ctx: core::mem::offset_of!(JitEnv, ctx) as i32, gpr: core::mem::offset_of!(PpcContext, gpr) as i32, + fpr: core::mem::offset_of!(PpcContext, fpr) 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, @@ -96,6 +99,11 @@ impl Offsets { fn gpr(&self, i: usize) -> i32 { self.gpr + (i as i32) * 8 } + /// Byte offset of FP register `i` (`fpr[i]`, 8 bytes each). + #[inline] + fn fpr(&self, i: usize) -> i32 { + self.fpr + (i as i32) * 8 + } /// Byte offset of one flag inside `cr[field]`. #[inline] fn cr_byte(&self, field: usize, which: CrByte) -> i32 { @@ -436,6 +444,76 @@ pub fn try_emit_native( emit_store(ops, off, state, helpers.store_u64, ra, rd, instr.ds()); Emit::Native } + // ld/ldx: 64-bit integer load. rD = mem.read_u64(ea). (DS-form / indexed.) + PpcOpcode::ld => { + emit_ea(ops, off, ra, instr.ds()); + emit_int_load64(ops, off, helpers.read_u64, rd); + state.retire(); + Emit::Native + } + PpcOpcode::ldx => { + emit_ea_x(ops, off, ra, rb); + emit_int_load64(ops, off, helpers.read_u64, rd); + state.retire(); + Emit::Native + } + + // ===== FP loads/stores (rd()==rs() = the FP register in bits 6-10). The + // helpers call the exact interpreter mem methods + as-casts, so the f32 + // <-> f64 conversion is bit-identical. EA rule is identical to the + // integer forms (RA=0 -> 0). ===== + // lfs/lfsx: fpr[rd] = mem.read_f32(ea) as f64. + PpcOpcode::lfs => { + emit_ea(ops, off, ra, instr.d()); + emit_fp_load(ops, off, helpers.read_f32_as_f64, rd); + state.retire(); + Emit::Native + } + PpcOpcode::lfsx => { + emit_ea_x(ops, off, ra, rb); + emit_fp_load(ops, off, helpers.read_f32_as_f64, rd); + state.retire(); + Emit::Native + } + // lfd/lfdx: fpr[rd] = mem.read_f64(ea). + PpcOpcode::lfd => { + emit_ea(ops, off, ra, instr.d()); + emit_fp_load(ops, off, helpers.read_f64, rd); + state.retire(); + Emit::Native + } + PpcOpcode::lfdx => { + emit_ea_x(ops, off, ra, rb); + emit_fp_load(ops, off, helpers.read_f64, rd); + state.retire(); + Emit::Native + } + // stfs/stfsx: mem.write_f32(ea, fpr[rs] as f32). + PpcOpcode::stfs => { + emit_ea(ops, off, ra, instr.d()); + emit_fp_store(ops, off, helpers.store_f32, rd); + state.retire(); + Emit::Native + } + PpcOpcode::stfsx => { + emit_ea_x(ops, off, ra, rb); + emit_fp_store(ops, off, helpers.store_f32, rd); + state.retire(); + Emit::Native + } + // stfd/stfdx: mem.write_f64(ea, fpr[rs]). + PpcOpcode::stfd => { + emit_ea(ops, off, ra, instr.d()); + emit_fp_store(ops, off, helpers.store_f64, rd); + state.retire(); + Emit::Native + } + PpcOpcode::stfdx => { + emit_ea_x(ops, off, ra, rb); + emit_fp_store(ops, off, helpers.store_f64, rd); + state.retire(); + Emit::Native + } // ===== Rotate/mask (sh/mb/me are compile-time constants -> the 32/64-bit // mask folds to a constant, so these are just rol + and). ===== @@ -705,6 +783,56 @@ fn emit_store(ops: &mut Asm, off: &Offsets, state: &mut EmitState, helper: i64, state.retire(); } +/// Compute an indexed effective address `(rA==0 ? 0 : gpr[rA]) + gpr[rB]` into +/// `rsi` (low 32 = guest EA). Mirrors the interpreter's X-form EA. +#[inline] +fn emit_ea_x(ops: &mut Asm, off: &Offsets, ra: usize, rb: usize) { + if ra == 0 { + dynasm!(ops ; .arch x64 ; mov rsi, [r15 + off.gpr(rb)]); + } else { + dynasm!(ops ; .arch x64 + ; mov rsi, [r15 + off.gpr(ra)] + ; add rsi, [r15 + off.gpr(rb)] + ); + } +} + +/// Emit a 64-bit integer load: `rsi=ea; call read_u64; gpr[rd] = rax`. +/// (Caller sets `rsi` via `emit_ea`/`emit_ea_x` and calls `state.retire()`.) +#[inline] +fn emit_int_load64(ops: &mut Asm, off: &Offsets, helper: i64, rd: usize) { + dynasm!(ops ; .arch x64 + ; mov rdi, rbx + ; mov rax, QWORD helper + ; call rax + ; mov [r15 + off.gpr(rd)], rax + ); +} + +/// Emit an FP load: `rsi=ea; call helper (returns f64 bits in rax); fpr[rd]=rax`. +/// (Caller sets `rsi` and calls `state.retire()`.) +#[inline] +fn emit_fp_load(ops: &mut Asm, off: &Offsets, helper: i64, rd: usize) { + dynasm!(ops ; .arch x64 + ; mov rdi, rbx + ; mov rax, QWORD helper + ; call rax + ; mov [r15 + off.fpr(rd)], rax + ); +} + +/// Emit an FP store: `rsi=ea; rdx=fpr[rs] bits; rdi=env; call helper`. +/// (Caller sets `rsi` and calls `state.retire()`.) +#[inline] +fn emit_fp_store(ops: &mut Asm, off: &Offsets, helper: i64, rs: usize) { + dynasm!(ops ; .arch x64 + ; mov rdx, [r15 + off.fpr(rs)] + ; mov rdi, rbx + ; mov rax, QWORD helper + ; call rax + ); +} + /// Emit `cr[0] = update_cr_signed(result)` where the result is in `rax`/`eax`: /// a signed comparison of the value against 0. `test` sets ZF/SF (OF=0), so the /// signed `setl/setg/sete` in [`emit_cr_from_flags`] give lt/gt/eq vs 0. diff --git a/crates/xenia-jit/src/lib.rs b/crates/xenia-jit/src/lib.rs index b439cc3..b6a6773 100644 --- a/crates/xenia-jit/src/lib.rs +++ b/crates/xenia-jit/src/lib.rs @@ -199,6 +199,39 @@ unsafe extern "C" fn jit_store_u64(env: *mut JitEnv, addr: u32, val: u64) { mem.write_u64(addr, val); } +// ---- floating-point load/store helpers ---- +// +// These call the EXACT interpreter memory methods + `as f32`/`as f64` +// conversions, so results are bit-identical by construction (no hand-written +// cvtss2sd / NaN-payload risk). Loads return the f64 BITS to store into +// `fpr[rd]`; stores take the f64 bits from `fpr[rs]`. Stores replicate the +// reservation-invalidation prologue, like the integer store helpers. + +/// `fpr[rd] = mem.read_f32(ea) as f64` — returns the widened f64 bits. +unsafe extern "C" fn jit_read_f32_as_f64(env: *mut JitEnv, addr: u32) -> u64 { + let mem: &dyn MemoryAccess = unsafe { &*(&*env).mem }; + (mem.read_f32(addr) as f64).to_bits() +} +/// `fpr[rd] = mem.read_f64(ea)` — returns the f64 bits. +unsafe extern "C" fn jit_read_f64(env: *mut JitEnv, addr: u32) -> u64 { + let mem: &dyn MemoryAccess = unsafe { &*(&*env).mem }; + mem.read_f64(addr).to_bits() +} +/// `mem.write_f32(ea, f64::from_bits(bits) as f32)` (matches `fpr[rs] as f32`). +unsafe extern "C" fn jit_store_f32(env: *mut JitEnv, addr: u32, bits: u64) { + let env = unsafe { &*env }; + unsafe { store_reservation_invalidate(env, addr) }; + let mem: &dyn MemoryAccess = unsafe { &*env.mem }; + mem.write_f32(addr, f64::from_bits(bits) as f32); +} +/// `mem.write_f64(ea, f64::from_bits(bits))` (the fpr[rs] value). +unsafe extern "C" fn jit_store_f64(env: *mut JitEnv, addr: u32, bits: u64) { + let env = unsafe { &*env }; + unsafe { store_reservation_invalidate(env, addr) }; + let mem: &dyn MemoryAccess = unsafe { &*env.mem }; + mem.write_f64(addr, f64::from_bits(bits)); +} + /// Absolute addresses of the memory helpers, baked into emitted code. pub(crate) struct MemHelpers { pub read_u8: i64, @@ -209,6 +242,10 @@ pub(crate) struct MemHelpers { pub store_u16: i64, pub store_u32: i64, pub store_u64: i64, + pub read_f32_as_f64: i64, + pub read_f64: i64, + pub store_f32: i64, + pub store_f64: i64, } impl MemHelpers { pub(crate) fn resolve() -> Self { @@ -221,6 +258,10 @@ impl MemHelpers { store_u16: jit_store_u16 as usize as i64, store_u32: jit_store_u32 as usize as i64, store_u64: jit_store_u64 as usize as i64, + read_f32_as_f64: jit_read_f32_as_f64 as usize as i64, + read_f64: jit_read_f64 as usize as i64, + store_f32: jit_store_f32 as usize as i64, + store_f64: jit_store_f64 as usize as i64, } } } diff --git a/crates/xenia-jit/src/tests.rs b/crates/xenia-jit/src/tests.rs index 14e73cb..0fec919 100644 --- a/crates/xenia-jit/src/tests.rs +++ b/crates/xenia-jit/src/tests.rs @@ -697,6 +697,99 @@ fn fuzz_gpr_based(seed: &mut u64, ra: u32) -> [u64; 32] { g } +/// FP-load/store (and ld/ldx) differential check with backed memory: seeds the +/// FPRs, runs interp vs JIT, and asserts GPR + FPR (by bits, so NaN compares) + +/// memory + counters match. The f32<->f64 conversion lives in a shared helper, +/// so this primarily validates EA computation, fpr indexing, and the ABI. +fn check_fp(raw: u32, gpr: [u64; 32], fpr_bits: [u64; 32]) { + let pc = 0x8200_1000u32; + let instr = decode(raw, pc); + let off = emit::Offsets::resolve(); + let helpers = crate::MemHelpers::resolve(); + let mut probe = dynasmrt::x64::Assembler::new().unwrap(); + assert!( + emit::try_emit_native(&mut probe, &off, &helpers, &mut emit::EmitState::new(), &instr) != emit::Emit::Fallback, + "fp opcode not natively emitted raw={raw:#010x} ({:?})", + instr.opcode + ); + let seed = |c: &mut PpcContext| { + for (i, b) in fpr_bits.iter().enumerate() { + c.fpr[i] = f64::from_bits(*b); + } + }; + + let mem_a = VecMem::seeded(); + let mut a = ctx_from_gpr(gpr, pc); + seed(&mut a); + let ra = interpret_one(&mut a, &mem_a, &instr); + a.cycle_count += 1; + a.timebase += 1; + + let mem_b = VecMem::seeded(); + let mut b = ctx_from_gpr(gpr, pc); + seed(&mut b); + 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_b); + + assert_eq!(a.gpr, b.gpr, "gpr mismatch raw={raw:#010x} ({:?})", instr.opcode); + let fa: [u64; 32] = std::array::from_fn(|i| a.fpr[i].to_bits()); + let fb: [u64; 32] = std::array::from_fn(|i| b.fpr[i].to_bits()); + assert_eq!(fa, fb, "fpr 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!(mem_a.snapshot(), mem_b.snapshot(), "memory mismatch raw={raw:#010x} ({:?})", instr.opcode); + assert_eq!(ra, rb, "StepResult mismatch raw={raw:#010x}"); +} + +/// FPR seed with edge bit patterns (0/-0/1/inf/NaN/denormal) plus random bits. +fn fuzz_fpr(seed: &mut u64) -> [u64; 32] { + let mut f = [0u64; 32]; + for slot in f.iter_mut() { + *slot = rng(seed); + } + f[0] = 0.0f64.to_bits(); + f[1] = (-0.0f64).to_bits(); + f[2] = 1.0f64.to_bits(); + f[3] = (-1.0f64).to_bits(); + f[4] = f64::INFINITY.to_bits(); + f[5] = f64::NEG_INFINITY.to_bits(); + f[6] = f64::NAN.to_bits(); + f[7] = 1.0e-300f64.to_bits(); // sub-f32 magnitude -> flushes/rounds on stfs + f[8] = 3.1415926535f64.to_bits(); // rounds on narrowing to f32 + f +} + +#[test] +fn fp_loadstore_matches() { + let mut s = 0xf9a7u64; + for _ in 0..ITERS { + let rd = (rng(&mut s) % 32) as u32; + let ra = (rng(&mut s) % 32) as u32; + let rb = (rng(&mut s) % 32) as u32; + let disp = (rng(&mut s) & 0x7F) as u16 & !0x3; // small aligned displacement + let g = fuzz_gpr_based(&mut s, ra); + let f = fuzz_fpr(&mut s); + check_fp(enc_d(48, rd, ra, disp), g, f); // lfs + check_fp(enc_d(50, rd, ra, disp), g, f); // lfd + check_fp(enc_d(52, rd, ra, disp), g, f); // stfs + check_fp(enc_d(54, rd, ra, disp), g, f); // stfd + check_fp(enc_x(31, rd, ra, rb, 535, 0), g, f); // lfsx + check_fp(enc_x(31, rd, ra, rb, 599, 0), g, f); // lfdx + check_fp(enc_x(31, rd, ra, rb, 663, 0), g, f); // stfsx + check_fp(enc_x(31, rd, ra, rb, 727, 0), g, f); // stfdx + // ld (op 58, DS-form XO 0) and ldx (op 31, XO 21). + check_fp((58 << 26) | (rd << 21) | (ra << 16) | (disp as u32 & 0x3FFC), g, f); + check_fp(enc_x(31, rd, ra, rb, 21, 0), g, f); // ldx + } +} + #[test] fn loads_match() { let mut s = 0x11ffu64;