[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:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user