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