[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 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-04 20:20:55 +02:00
parent afc3692223
commit 4194ed77a3
3 changed files with 262 additions and 0 deletions

View File

@@ -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.

View File

@@ -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,
}
}
}

View File

@@ -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;