From 704e87a2a445f50aede9f689915e9962229b4a39 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Tue, 7 Jul 2026 19:09:15 +0200 Subject: [PATCH] =?UTF-8?q?[iterate-4A]=20jit:=20FP-move=20tier=20?= =?UTF-8?q?=E2=80=94=20lfs/stfs/lfd/stfd=20+=20fmr/fabs/fneg=20(diff-clean?= =?UTF-8?q?,=2039.82%=20native)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cover the fpscr-free FP moves. Validated bit-exact via the in-process differential harness on a full boot+movie run (movie plays, clean exit): checked 146.2M blocks, 39.82% native (up from 38.78%), MISMATCHES=0 Coverage (all fpscr-free — pure data movement, no rounding flags): * FP loads: lfs/lfsx (load single, IEEE-widen to the f64 FPR via read32 -> bitcast F32 -> fpromote F64), lfd/lfdx (pure 64-bit bit copy). * FP stores: stfs/stfsx (fdemote f64 -> f32 -> bitcast i32 -> write32), stfd/stfdx (pure 64-bit bit copy). Stores reuse the reservation-kicking write trampolines. * FP reg moves (Rc=0 only — the `.` forms update CR1 from fpscr): fmr (bit copy), fabs (band_imm i64::MAX, clear sign), fneg (bxor_imm i64::MIN, flip sign) — bit ops that exactly match Rust f64 copy/abs/neg. Note: the +1% lift is small because the movie's FP-heavy blocks almost always mix an FP load with FP *arithmetic* (fadds/fmuls/fmadds), which stays uncovered — it updates fpscr (FPRF/FI/FR, invalid-op flags), not yet lowered. The moves that landed are the pure data-copy blocks. Co-Authored-By: Claude Opus 4.8 --- crates/xenia-cpu/src/jit.rs | 85 +++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/crates/xenia-cpu/src/jit.rs b/crates/xenia-cpu/src/jit.rs index 6d634bd..6365a55 100644 --- a/crates/xenia-cpu/src/jit.rs +++ b/crates/xenia-cpu/src/jit.rs @@ -212,6 +212,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, + // 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, + // 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 + // fpscr (FPRF/FI/FR, invalid-op flags), which is not lowered yet. + fmrx | fabsx | fnegx => !instr.rc_bit(), // Branch terminators. bx | bcx | bclrx => true, _ => false, @@ -391,6 +399,11 @@ fn gpr_off(r: usize) -> i32 { (offset_of!(PpcContext, gpr) + r * 8) as i32 } +#[inline] +fn fpr_off(r: usize) -> i32 { + (offset_of!(PpcContext, fpr) + r * 8) as i32 +} + #[inline] fn load_gpr(b: &mut FunctionBuilder, ctxp: Value, r: usize) -> Value { b.ins().load(types::I64, MemFlags::trusted(), ctxp, gpr_off(r)) @@ -909,6 +922,78 @@ fn emit_op(b: &mut FunctionBuilder, ec: &EmitCtx, instr: &DecodedInstr) { call_store(b, ec, ec.tr.write64, ea, v); } + // ---- FP loads. lfs/lfsx: load single, widen to the f64 FPR. ---- + PpcOpcode::lfs => { + let ea = ea_d(b, ec, instr.ra(), instr.d()); + let bits = call_load(b, ec, ec.tr.read32, ea); + 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())); + } + PpcOpcode::lfsx => { + let ea = ea_x(b, ec, instr.ra(), instr.rb()); + let bits = call_load(b, ec, ec.tr.read32, ea); + 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())); + } + // lfd/lfdx: 64-bit — a pure bit copy into the FPR. + PpcOpcode::lfd => { + let ea = ea_d(b, ec, instr.ra(), instr.d()); + let bits = call_load(b, ec, ec.tr.read64, ea); + b.ins().store(MemFlags::trusted(), bits, ctxp, fpr_off(instr.rd())); + } + PpcOpcode::lfdx => { + let ea = ea_x(b, ec, instr.ra(), instr.rb()); + let bits = call_load(b, ec, ec.tr.read64, ea); + b.ins().store(MemFlags::trusted(), bits, ctxp, fpr_off(instr.rd())); + } + + // ---- FP stores. stfs/stfsx: narrow the f64 FPR to single. ---- + PpcOpcode::stfs => { + let ea = ea_d(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); + } + PpcOpcode::stfsx => { + let ea = ea_x(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); + } + // stfd/stfdx: 64-bit — a pure bit copy out of the FPR. + PpcOpcode::stfd => { + let ea = ea_d(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); + } + PpcOpcode::stfdx => { + let ea = ea_x(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); + } + + // ---- 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. ---- + PpcOpcode::fmrx => { + let bits = b.ins().load(types::I64, MemFlags::trusted(), ctxp, fpr_off(instr.rb())); + b.ins().store(MemFlags::trusted(), bits, ctxp, fpr_off(instr.rd())); + } + PpcOpcode::fabsx => { + let bits = b.ins().load(types::I64, MemFlags::trusted(), ctxp, fpr_off(instr.rb())); + let res = b.ins().band_imm(bits, i64::MAX); // clear sign bit (bit 63) + b.ins().store(MemFlags::trusted(), res, ctxp, fpr_off(instr.rd())); + } + PpcOpcode::fnegx => { + let bits = b.ins().load(types::I64, MemFlags::trusted(), ctxp, fpr_off(instr.rb())); + let res = b.ins().bxor_imm(bits, i64::MIN); // flip sign bit (bit 63) + b.ins().store(MemFlags::trusted(), res, ctxp, fpr_off(instr.rd())); + } + // Unconditional branch. Target is an immediate; `pc` (= this instr's // address in the interpreter) is `instr.addr` at emit time. PpcOpcode::bx => {