From cce35658a5c53c93da1ca95d7514062d447205a3 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 4 Jul 2026 21:34:43 +0200 Subject: [PATCH] [iterate-4C] JIT: native update-form loads/stores (lwzu/stwu/ldu/stdu/...) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Port the D/DS-form update loads (lwzu, lhzu, lhau, lbzu, ldu) and stores (stwu, sthu, stbu, stdu) — ~10% of the boot fallback executions (stwu 6.1% + lwzu 2.4% + ldu/stdu 1.4% + ...). EA = gpr[rA] + EXTS(disp) with rA read directly (update forms are invalid for rA==0, and interp reads gpr[rA] with no 0-substitution); rD/mem then gpr[rA]=ea truncated to u32 and zero-extended (matches interp `... as u32; gpr[ra]=ea as u64`). Stores capture rS into rdx BEFORE the rA writeback so `stXu rS,d(rA)` with rS==rA stores the OLD rA value (interp order). New emit helpers emit_update_ea_writeback / emit_load_update / emit_load64_update / emit_store_update; all cache-aware (gld64/gst64 accessors). New differential test update_loadstore_matches (regcache forced on), incl. the store rS==rA case. 18 tests green. Fallback executions 25.55M -> 22.97M (-10.1%, n=200M). Golden n200m BYTE-IDENTICAL under interp, XENIA_JIT=1, and +XENIA_JIT_REGCACHE=1. Whole-run ratio 1.079x -> 1.077x: as prior evidence predicted, opcode coverage does not flip the JIT on this FP-video-decode-bound boot bench (62% of remaining fallbacks are fmadds/fmuls/fadds/fsubs, expensive in both paths). Correctness/coverage win; real speed lever is dispatch elimination (block-linking), not more opcodes. Co-Authored-By: Claude Opus 4.8 --- crates/xenia-jit/src/emit.rs | 100 ++++++++++++++++++++++++++++++++++ crates/xenia-jit/src/tests.rs | 36 ++++++++++++ 2 files changed, 136 insertions(+) diff --git a/crates/xenia-jit/src/emit.rs b/crates/xenia-jit/src/emit.rs index c963176..0a7bebe 100644 --- a/crates/xenia-jit/src/emit.rs +++ b/crates/xenia-jit/src/emit.rs @@ -585,6 +585,53 @@ pub fn try_emit_native( Emit::Native } + // ===== Update-form D/DS loads: ea = gpr[rA] + EXTS(disp) (rA used + // DIRECTLY — the update forms are invalid for rA==0, and the interp + // reads gpr[rA] without the 0-substitution). rD = EXT(mem[ea]); + // gpr[rA] = ea (truncated to u32 then zero-extended, matching interp's + // `... as u32; gpr[ra] = ea as u64`). Emitting the rA writeback BEFORE + // the load is byte-identical for the architecturally-valid domain + // (rA != rD). ===== + PpcOpcode::lwzu => { + emit_load_update(ops, cache, off, state, helpers.read_u32, ra, rd, instr.d(), Ext::Zx32); + Emit::Native + } + PpcOpcode::lhzu => { + emit_load_update(ops, cache, off, state, helpers.read_u16, ra, rd, instr.d(), Ext::Zx16); + Emit::Native + } + PpcOpcode::lhau => { + emit_load_update(ops, cache, off, state, helpers.read_u16, ra, rd, instr.d(), Ext::Sx16); + Emit::Native + } + PpcOpcode::lbzu => { + emit_load_update(ops, cache, off, state, helpers.read_u8, ra, rd, instr.d(), Ext::Zx8); + Emit::Native + } + PpcOpcode::ldu => { + emit_load64_update(ops, cache, off, state, helpers.read_u64, ra, rd, instr.ds()); + Emit::Native + } + // ===== Update-form stores: mem[ea] = gpr[rS]; gpr[rA] = ea. The store + // data (rS) is read BEFORE the rA writeback so `stXu rS,d(rA)` with + // rS==rA stores the OLD rA value, matching interp order. ===== + PpcOpcode::stwu => { + emit_store_update(ops, cache, off, state, helpers.store_u32, ra, rd, instr.d()); + Emit::Native + } + PpcOpcode::sthu => { + emit_store_update(ops, cache, off, state, helpers.store_u16, ra, rd, instr.d()); + Emit::Native + } + PpcOpcode::stbu => { + emit_store_update(ops, cache, off, state, helpers.store_u8, ra, rd, instr.d()); + Emit::Native + } + PpcOpcode::stdu => { + emit_store_update(ops, cache, off, state, helpers.store_u64, ra, rd, instr.ds()); + 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 @@ -1029,6 +1076,59 @@ fn emit_store(ops: &mut Asm, cache: &RegCache, off: &Offsets, state: &mut EmitSt emit_store_tail(ops, cache, off, state, helper, rs); } +/// Emit the update-form EA + rA writeback shared by `emit_load_update` / +/// `emit_store_update`: `rsi = gpr[rA] + EXTS(disp)` (rA read directly, no +/// 0-substitution), then `gpr[rA] = (rsi as u32)` (truncate + zero-extend, via +/// `mov eax, esi`). Leaves the full 64-bit EA in `rsi` (low 32 = guest addr) +/// for the caller's load/store tail. Uses `rax` as scratch. +#[inline] +fn emit_update_ea_writeback(ops: &mut Asm, cache: &RegCache, off: &Offsets, ra: usize, disp: i32) { + gld64(ops, cache, off, RSI, ra); + if disp != 0 { + dynasm!(ops ; .arch x64 ; add rsi, disp); + } + dynasm!(ops ; .arch x64 ; mov eax, esi); // ea as u32, zero-extended into rax + gst64(ops, cache, off, ra, RAX); +} + +/// Update-form D-load (lwzu/lhzu/lhau/lbzu): rD = EXT(mem[ea]); gpr[rA] = ea. +/// The rA writeback precedes the load — byte-identical for the valid domain +/// (rA != rD). +#[inline] +fn emit_load_update(ops: &mut Asm, cache: &RegCache, off: &Offsets, state: &mut EmitState, helper: i64, ra: usize, rd: usize, disp: i32, ext: Ext) { + emit_update_ea_writeback(ops, cache, off, ra, disp); + emit_load_tail(ops, cache, off, state, helper, rd, ext); +} + +/// Update-form 64-bit load (ldu): rD = mem.read_u64(ea); gpr[rA] = ea. +#[inline] +fn emit_load64_update(ops: &mut Asm, cache: &RegCache, off: &Offsets, state: &mut EmitState, helper: i64, ra: usize, rd: usize, disp: i32) { + emit_update_ea_writeback(ops, cache, off, ra, disp); + emit_int_load64(ops, cache, off, helper, rd); + state.retire(); +} + +/// Update-form store (stwu/sthu/stbu/stdu): mem[ea] = gpr[rS]; gpr[rA] = ea. +/// The store data (rS) is captured into `rdx` BEFORE the rA writeback so +/// `stXu rS,d(rA)` with rS==rA stores the OLD rA value (interp order). +#[inline] +fn emit_store_update(ops: &mut Asm, cache: &RegCache, off: &Offsets, state: &mut EmitState, helper: i64, ra: usize, rs: usize, disp: i32) { + gld64(ops, cache, off, RSI, ra); + if disp != 0 { + dynasm!(ops ; .arch x64 ; add rsi, disp); + } + gld64(ops, cache, off, RDX, rs); // store data first (rs may == ra) + dynasm!(ops ; .arch x64 ; mov eax, esi); + gst64(ops, cache, off, ra, RAX); // gpr[ra] = ea (u32 zext) + dynasm!(ops + ; .arch x64 + ; mov rdi, rbx + ; mov rax, QWORD helper + ; call rax + ); + 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] diff --git a/crates/xenia-jit/src/tests.rs b/crates/xenia-jit/src/tests.rs index 246ba50..261fad0 100644 --- a/crates/xenia-jit/src/tests.rs +++ b/crates/xenia-jit/src/tests.rs @@ -916,3 +916,39 @@ fn stores_match() { check_mem((62 << 26) | (rs << 21) | (ra << 16) | ((disp as u32 & 0x3FFC)), g); } } + +#[test] +fn update_loadstore_matches() { + // Update forms (lwzu/lhzu/lhau/lbzu/ldu, stwu/sthu/stbu/stdu) require a + // non-zero base (invalid + no valid EA otherwise), and for loads the + // architecturally-valid domain is rA != rD (rA==rD is an invalid form). + // The JIT emits the rA writeback before the load, byte-identical to interp + // for that domain, so constrain the fuzz to rA in 1..=31 and rD != rA. + let mut s = 0x33ddu64; + for _ in 0..ITERS { + let ra = 1 + (rng(&mut s) % 31) as u32; // 1..=31 + let mut rd = (rng(&mut s) % 32) as u32; + if rd == ra { + rd = (rd + 1) % 32; + if rd == 0 { rd = 1; } + } + let disp = (rng(&mut s) & 0x7F) as u16; + let g = fuzz_gpr_based(&mut s, ra); + // D-form update loads/stores. + check_mem(enc_d(33, rd, ra, disp), g); // lwzu + check_mem(enc_d(41, rd, ra, disp), g); // lhzu + check_mem(enc_d(43, rd, ra, disp), g); // lhau + check_mem(enc_d(35, rd, ra, disp), g); // lbzu + check_mem(enc_d(37, rd, ra, disp), g); // stwu + check_mem(enc_d(45, rd, ra, disp), g); // sthu + check_mem(enc_d(39, rd, ra, disp), g); // stbu + // DS-form update loads/stores (XO=1 in the low 2 bits). + check_mem((58 << 26) | (rd << 21) | (ra << 16) | (disp as u32 & 0x3FFC) | 1, g); // ldu + check_mem((62 << 26) | (rd << 21) | (ra << 16) | (disp as u32 & 0x3FFC) | 1, g); // stdu + + // Store-with-rS==rA (valid form): the store must use the OLD rA value, + // which is why the JIT captures rdx before the rA writeback. + check_mem(enc_d(37, ra, ra, disp), g); // stwu rA, d(rA) + check_mem((62 << 26) | (ra << 21) | (ra << 16) | (disp as u32 & 0x3FFC) | 1, g); // stdu rA,d(rA) + } +}