From 5950dc86d50b9b20e3ae7a9648141dd46be2fd02 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 8 Jul 2026 07:06:42 +0200 Subject: [PATCH] =?UTF-8?q?[iterate-4A]=20jit:=20coverage=20=E2=80=94=20up?= =?UTF-8?q?date-form=20loads/stores=20+=20mfspr/mtspr(LR/CTR)=20=E2=86=92?= =?UTF-8?q?=2089.6%=20native?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second coverage batch, data-driven from the histogram. Diff harness validates every op against the interpreter (MISMATCHES=0 over 47.96M blocks). Update-form load/stores (24 ops: lbzu..stdux, lfsu..stfdux and their x-forms): same access as the base form but with EA = gpr[ra] + offset (ra used directly — update forms are illegal with ra==0) and rA := EA written back (zero-extended) after the access. New ea_d_update/ea_x_update/write_ea_back helpers; loads write rD then rA (so rA wins if rd==ra, matching the interpreter). stwu especially is the standard stack-frame push in every function prologue. mfspr/mtspr for LR and CTR only (64-bit field copies): the ubiquitous mflr/mtlr prologue-epilogue pair and mtctr. covered() gates on the compile-time SPR number so the offset always resolves; XER (packs CA/OV/SO) and the modelled SPRs stay uncovered. This is the biggest single jump — mflr/mtlr gate almost every non-leaf function's prologue and epilogue blocks. Coverage (single-block, diff mode): 80.6% -> 82.8% (update forms) -> 89.6% (mfspr/mtspr). 7/7 jit unit tests; foreground-validated per the bg-SIGTERM finding. Carry-setting shifts (sraw*/srad*), rotate-double (rldic*), and XER mfspr/mtspr remain for later batches; VMX128 is the long pole. Co-Authored-By: Claude Opus 4.8 --- crates/xenia-cpu/src/jit.rs | 220 ++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/crates/xenia-cpu/src/jit.rs b/crates/xenia-cpu/src/jit.rs index 74f2355..36ec987 100644 --- a/crates/xenia-cpu/src/jit.rs +++ b/crates/xenia-cpu/src/jit.rs @@ -424,9 +424,14 @@ pub fn covered(instr: &DecodedInstr) -> bool { // (sign-extending) loads are not lowered yet. lbz | lbzx | lhz | lhzx | lwz | lwzx | ld | ldx => true, stb | stbx | sth | sthx | stw | stwx | std | stdx => true, + // Update (`u`) forms: base load/store + write the EA back to rA (zero- + // extended). Algebraic (sign-extending) loads remain uncovered. + lbzu | lbzux | lhzu | lhzux | lwzu | lwzux | ldu | ldux => true, + stbu | stbux | sthu | sthux | stwu | stwux | stdu | stdux => true, // FP loads/stores are pure moves (no fpscr). Single-precision forms // widen/narrow via IEEE round-to-nearest, matching the interpreter. lfs | lfsx | lfd | lfdx | stfs | stfsx | stfd | stfdx => true, + lfsu | lfsux | lfdu | lfdux | stfsu | stfsux | stfdu | stfdux => true, // FP register moves: only the Rc=0 forms (the `.` forms update CR1 from // fpscr). fmr/fabs/fneg are sign-bit ops with no fpscr side effect. // FP *arithmetic* (fadds/fmuls/fmadds…) is NOT covered — it updates @@ -434,6 +439,13 @@ pub fn covered(instr: &DecodedInstr) -> bool { fmrx | fabsx | fnegx => !instr.rc_bit(), // FP arithmetic/compare — lowered by punting to the interpreter. op if is_fp_punt(op) => true, + // mfspr/mtspr for LR and CTR only — trivial 64-bit field copies (the + // ubiquitous mflr/mtlr prologue pair and mtctr). XER (packs the CA/OV/SO + // flags) and the modelled SPRs (timebase, DEC, VRSAVE, …) stay uncovered. + mfspr | mtspr => matches!( + instr.spr(), + crate::context::spr::LR | crate::context::spr::CTR + ), // Branch terminators. bx | bcx | bclrx => true, _ => false, @@ -1247,6 +1259,31 @@ fn ea_x(b: &mut FunctionBuilder, ec: &EmitCtx, ra: usize, rb: usize) -> Value { b.ins().ireduce(types::I32, ea64) } +/// EA for an **update-form** D load/store: `gpr[ra] + disp` truncated to 32 +/// bits. Unlike [`ea_d`], `ra` is used directly (update forms are illegal with +/// `ra==0`). The returned EA is also written back to `rA` by [`write_ea_back`]. +fn ea_d_update(b: &mut FunctionBuilder, ec: &EmitCtx, ra: usize, disp: i32) -> Value { + let base = cache_read_gpr(b, ec, ra); + let ea64 = b.ins().iadd_imm(base, disp as i64); + b.ins().ireduce(types::I32, ea64) +} + +/// EA for an update-form X (indexed) load/store: `gpr[ra] + gpr[rb]` truncated +/// to 32 bits (`ra` used directly). +fn ea_x_update(b: &mut FunctionBuilder, ec: &EmitCtx, ra: usize, rb: usize) -> Value { + let base = cache_read_gpr(b, ec, ra); + let rbv = cache_read_gpr(b, ec, rb); + let ea64 = b.ins().iadd(base, rbv); + b.ins().ireduce(types::I32, ea64) +} + +/// Write an update-form EA (I32) back to `rA`, zero-extended to 64 bits — the +/// register writeback the `u` forms perform after the access. +fn write_ea_back(b: &mut FunctionBuilder, ec: &EmitCtx, ra: usize, ea: Value) { + let ea64 = b.ins().uextend(types::I64, ea); + cache_write_gpr(ec, ra, ea64); +} + /// Emit a load trampoline call `fref(memenv, ea)` and return its result value. fn call_load(b: &mut FunctionBuilder, ec: &EmitCtx, fref: FuncRef, ea: Value) -> Value { let call = b.ins().call(fref, &[ec.memenv, ea]); @@ -1896,6 +1933,168 @@ fn emit_op(b: &mut FunctionBuilder, ec: &EmitCtx, instr: &DecodedInstr) { call_store(b, ec, ec.tr.write64, ea, bits); } + // ---- update-form integer loads: base load into rD, then rA = EA. ---- + PpcOpcode::lbzu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let v = emit_inline_load(b, ec, ea, 1, ec.tr.read8); + cache_write_gpr(ec, instr.rd(), v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::lbzux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let v = emit_inline_load(b, ec, ea, 1, ec.tr.read8); + cache_write_gpr(ec, instr.rd(), v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::lhzu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let v = emit_inline_load(b, ec, ea, 2, ec.tr.read16); + cache_write_gpr(ec, instr.rd(), v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::lhzux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let v = emit_inline_load(b, ec, ea, 2, ec.tr.read16); + cache_write_gpr(ec, instr.rd(), v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::lwzu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let v = emit_inline_load(b, ec, ea, 4, ec.tr.read32); + cache_write_gpr(ec, instr.rd(), v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::lwzux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let v = emit_inline_load(b, ec, ea, 4, ec.tr.read32); + cache_write_gpr(ec, instr.rd(), v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::ldu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.ds()); + let v = emit_inline_load(b, ec, ea, 8, ec.tr.read64); + cache_write_gpr(ec, instr.rd(), v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::ldux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let v = emit_inline_load(b, ec, ea, 8, ec.tr.read64); + cache_write_gpr(ec, instr.rd(), v); + write_ea_back(b, ec, instr.ra(), ea); + } + + // ---- update-form integer stores: base store, then rA = EA. ---- + PpcOpcode::stbu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let v = gpr32(b, ec, instr.rs()); + call_store(b, ec, ec.tr.write8, ea, v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::stbux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let v = gpr32(b, ec, instr.rs()); + call_store(b, ec, ec.tr.write8, ea, v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::sthu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let v = gpr32(b, ec, instr.rs()); + call_store(b, ec, ec.tr.write16, ea, v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::sthux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let v = gpr32(b, ec, instr.rs()); + call_store(b, ec, ec.tr.write16, ea, v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::stwu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let v = gpr32(b, ec, instr.rs()); + call_store(b, ec, ec.tr.write32, ea, v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::stwux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let v = gpr32(b, ec, instr.rs()); + call_store(b, ec, ec.tr.write32, ea, v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::stdu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.ds()); + let v = cache_read_gpr(b, ec, instr.rs()); + call_store(b, ec, ec.tr.write64, ea, v); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::stdux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let v = cache_read_gpr(b, ec, instr.rs()); + call_store(b, ec, ec.tr.write64, ea, v); + write_ea_back(b, ec, instr.ra(), ea); + } + + // ---- update-form FP loads: base load into fpr[rD], then rA = EA. ---- + PpcOpcode::lfsu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let bits64 = emit_inline_load(b, ec, ea, 4, ec.tr.read32); + let bits = b.ins().ireduce(types::I32, bits64); + let f32v = b.ins().bitcast(types::F32, MemFlags::new(), bits); + let f64v = b.ins().fpromote(types::F64, f32v); + b.ins().store(MemFlags::trusted(), f64v, ctxp, fpr_off(instr.rd())); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::lfsux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let bits64 = emit_inline_load(b, ec, ea, 4, ec.tr.read32); + let bits = b.ins().ireduce(types::I32, bits64); + let f32v = b.ins().bitcast(types::F32, MemFlags::new(), bits); + let f64v = b.ins().fpromote(types::F64, f32v); + b.ins().store(MemFlags::trusted(), f64v, ctxp, fpr_off(instr.rd())); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::lfdu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let bits = emit_inline_load(b, ec, ea, 8, ec.tr.read64); + b.ins().store(MemFlags::trusted(), bits, ctxp, fpr_off(instr.rd())); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::lfdux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let bits = emit_inline_load(b, ec, ea, 8, ec.tr.read64); + b.ins().store(MemFlags::trusted(), bits, ctxp, fpr_off(instr.rd())); + write_ea_back(b, ec, instr.ra(), ea); + } + + // ---- update-form FP stores: base store, then rA = EA. ---- + PpcOpcode::stfsu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let f64v = b.ins().load(types::F64, MemFlags::trusted(), ctxp, fpr_off(instr.rs())); + let f32v = b.ins().fdemote(types::F32, f64v); + let bits = b.ins().bitcast(types::I32, MemFlags::new(), f32v); + call_store(b, ec, ec.tr.write32, ea, bits); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::stfsux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let f64v = b.ins().load(types::F64, MemFlags::trusted(), ctxp, fpr_off(instr.rs())); + let f32v = b.ins().fdemote(types::F32, f64v); + let bits = b.ins().bitcast(types::I32, MemFlags::new(), f32v); + call_store(b, ec, ec.tr.write32, ea, bits); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::stfdu => { + let ea = ea_d_update(b, ec, instr.ra(), instr.d()); + let bits = b.ins().load(types::I64, MemFlags::trusted(), ctxp, fpr_off(instr.rs())); + call_store(b, ec, ec.tr.write64, ea, bits); + write_ea_back(b, ec, instr.ra(), ea); + } + PpcOpcode::stfdux => { + let ea = ea_x_update(b, ec, instr.ra(), instr.rb()); + let bits = b.ins().load(types::I64, MemFlags::trusted(), ctxp, fpr_off(instr.rs())); + call_store(b, ec, ec.tr.write64, ea, bits); + write_ea_back(b, ec, instr.ra(), ea); + } + // ---- FP register moves (Rc=0; the `.` forms read fpscr → uncovered). // Done as bit ops on the 64-bit pattern so they exactly match Rust's // f64 copy / sign-bit abs / sign-bit negate. ---- @@ -1914,6 +2113,27 @@ fn emit_op(b: &mut FunctionBuilder, ec: &EmitCtx, instr: &DecodedInstr) { b.ins().store(MemFlags::trusted(), res, ctxp, fpr_off(instr.rd())); } + // ---- mfspr/mtspr for LR and CTR: 64-bit field copies. `covered()` + // gates the SPR to LR/CTR, so the offset is always resolved. ---- + PpcOpcode::mfspr => { + let field = match instr.spr() { + crate::context::spr::LR => offset_of!(PpcContext, lr), + crate::context::spr::CTR => offset_of!(PpcContext, ctr), + _ => unreachable!("mfspr covered only for LR/CTR"), + }; + let v = b.ins().load(types::I64, MemFlags::trusted(), ctxp, off(field)); + cache_write_gpr(ec, instr.rd(), v); + } + PpcOpcode::mtspr => { + let field = match instr.spr() { + crate::context::spr::LR => offset_of!(PpcContext, lr), + crate::context::spr::CTR => offset_of!(PpcContext, ctr), + _ => unreachable!("mtspr covered only for LR/CTR"), + }; + let v = cache_read_gpr(b, ec, instr.rs()); + b.ins().store(MemFlags::trusted(), v, ctxp, off(field)); + } + // Unconditional branch. Target is an immediate; `pc` (= this instr's // address in the interpreter) is `instr.addr` at emit time. PpcOpcode::bx => {