[iterate-4A] jit: FP arithmetic via interpreter-punt (diff-clean, 73.48% native)

Solve the FP-arithmetic fpscr problem without lowering fpscr into IR. Add a
generic single-instruction shim, xj_interp_op(ctx, env, raw, addr), that decodes
and runs one instruction through the interpreter's execute() on the live
context/memory — bit-exact by construction. This lets a block containing FP math
still be JIT-compiled: the surrounding integer/memory/branch ops run as machine
code and only the FP op calls back into Rust (the technique production JITs use
for complex ops). Validated on a full boot+movie run (movie plays, clean exit):

  checked 147.2M blocks, 73.48% native (108.2M, up from 39.82%!), MISMATCHES=0

The fpscr bookkeeping (rounding-mode-dependent rounding, sticky exception bits,
FPRF classification) was too intricate to emit correctly in IR; punting sidesteps
it entirely while still capturing the coverage.

is_fp_punt allowlist (all verified always-Continue, non-branch):
  faddx/faddsx, fsubx/fsubsx, fmulx/fmulsx, fdivx/fdivsx, fmaddx/fmaddsx,
  fmsubx/fmsubsx, fnmaddx/fnmaddsx, fnmsubx/fnmsubsx, frspx, fsqrtx, fresx,
  frsqrtex, fselx, fnabsx, fcmpu, fcmpo.
covered() returns true for them; emit_op emits `call interp(ctx, env, raw,
addr)`. execute() advances pc by 4 (harmless — the block epilogue stamps end_pc
for non-branch blocks; cycle_count is bumped once per block, not by execute).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-07 19:18:06 +02:00
parent 704e87a2a4
commit d91311c486

View File

@@ -100,6 +100,22 @@ extern "C" fn xj_write64(ctxp: *const PpcContext, env: *const MemEnv, addr: u32,
unsafe { (*env).mem.write_u64(addr, val) }
}
/// Generic single-instruction interpreter shim ("punt"). Decodes `raw` at
/// `addr` and runs it through the interpreter's [`crate::interpreter::execute`],
/// mutating the live context/memory in place. Lets a block containing an op we
/// don't lower natively still be JIT-compiled: the surrounding covered ops run
/// as machine code and only the punted op calls back into Rust — bit-exact by
/// construction. Used for FP arithmetic (whose `fpscr` bookkeeping is too
/// intricate to lower in IR). Only ops that are **always `Continue` and never
/// branch** may be punted (`execute` advances `pc` by 4; the block epilogue then
/// stamps `end_pc` for non-branch blocks, so intermediate `pc` doesn't matter).
extern "C" fn xj_interp_op(ctxp: *mut PpcContext, memenv: *const MemEnv, raw: u32, addr: u32) {
let ctx = unsafe { &mut *ctxp };
let mem = unsafe { (*memenv).mem };
let instr = crate::decoder::decode(raw, addr);
let _ = crate::interpreter::execute(ctx, mem, &instr);
}
/// `FuncRef`s for the eight trampolines, declared into the function being built.
/// Copied into each block's IR so load/store arms can emit calls.
#[derive(Clone, Copy)]
@@ -112,6 +128,7 @@ struct Trampolines {
write16: FuncRef,
write32: FuncRef,
write64: FuncRef,
interp: FuncRef,
}
/// Per-compile emit context: the two entry-block pointer params plus the
@@ -220,6 +237,8 @@ pub fn covered(instr: &DecodedInstr) -> bool {
// FP *arithmetic* (fadds/fmuls/fmadds…) is NOT covered — it updates
// fpscr (FPRF/FI/FR, invalid-op flags), which is not lowered yet.
fmrx | fabsx | fnegx => !instr.rc_bit(),
// FP arithmetic/compare — lowered by punting to the interpreter.
op if is_fp_punt(op) => true,
// Branch terminators.
bx | bcx | bclrx => true,
_ => false,
@@ -232,6 +251,21 @@ fn writes_pc(instr: &DecodedInstr) -> bool {
matches!(instr.opcode, PpcOpcode::bx | PpcOpcode::bcx | PpcOpcode::bclrx)
}
/// FP ops lowered by punting to the interpreter ([`xj_interp_op`]) rather than
/// native IR: their `fpscr` bookkeeping (rounding-mode-dependent rounding,
/// sticky exception bits, FPRF classification) is too intricate to emit in IR,
/// but punting lets a block containing them still compile (surrounding covered
/// ops run native). All are always-`Continue`, non-branch ops.
fn is_fp_punt(op: PpcOpcode) -> bool {
use PpcOpcode::*;
matches!(
op,
faddx | faddsx | fsubx | fsubsx | fmulx | fmulsx | fdivx | fdivsx
| fmaddx | fmaddsx | fmsubx | fmsubsx | fnmaddx | fnmaddsx | fnmsubx | fnmsubsx
| frspx | fsqrtx | fresx | frsqrtex | fselx | fnabsx | fcmpu | fcmpo
)
}
/// True if the whole block can be compiled (all opcodes covered).
pub fn block_covered(block: &DecodedBlock) -> bool {
block.instrs.iter().all(covered)
@@ -258,6 +292,7 @@ struct TrampIds {
write16: FuncId,
write32: FuncId,
write64: FuncId,
interp: FuncId,
}
impl Jit {
@@ -274,6 +309,7 @@ impl Jit {
builder.symbol("xj_write16", xj_write16 as *const u8);
builder.symbol("xj_write32", xj_write32 as *const u8);
builder.symbol("xj_write64", xj_write64 as *const u8);
builder.symbol("xj_interp_op", xj_interp_op as *const u8);
let mut module = JITModule::new(builder);
let ptr = module.target_config().pointer_type();
@@ -307,6 +343,15 @@ impl Jit {
write16: declare("xj_write16", &store_sig(types::I32))?,
write32: declare("xj_write32", &store_sig(types::I32))?,
write64: declare("xj_write64", &store_sig(types::I64))?,
interp: {
// interp(ctx: ptr, env: ptr, raw: i32, addr: i32)
let mut s = Signature::new(cc);
s.params.push(AbiParam::new(ptr));
s.params.push(AbiParam::new(ptr));
s.params.push(AbiParam::new(types::I32));
s.params.push(AbiParam::new(types::I32));
declare("xj_interp_op", &s)?
},
};
let ctx = module.make_context();
@@ -337,6 +382,7 @@ impl Jit {
write16: self.module.declare_func_in_func(self.tramp_ids.write16, &mut self.ctx.func),
write32: self.module.declare_func_in_func(self.tramp_ids.write32, &mut self.ctx.func),
write64: self.module.declare_func_in_func(self.tramp_ids.write64, &mut self.ctx.func),
interp: self.module.declare_func_in_func(self.tramp_ids.interp, &mut self.ctx.func),
};
{
@@ -1047,6 +1093,13 @@ fn emit_op(b: &mut FunctionBuilder, ec: &EmitCtx, instr: &DecodedInstr) {
store_lr(b, ctxp, next as u64);
}
}
// FP arithmetic/compare: punt to the interpreter via xj_interp_op.
// execute() runs the exact op (result + fpscr) on the live ctx/mem.
op if is_fp_punt(op) => {
let raw = b.ins().iconst(types::I32, instr.raw as i64);
let addr = b.ins().iconst(types::I32, instr.addr as i64);
b.ins().call(ec.tr.interp, &[ec.ctxp, ec.memenv, raw, addr]);
}
// covered() gates this — unreachable for uncovered opcodes.
_ => unreachable!("emit_op called on uncovered opcode {:?}", instr.opcode),
}