[iterate-4C] JIT: native update-form loads/stores (lwzu/stwu/ldu/stdu/...)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user