[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:
MechaCat02
2026-07-04 21:34:43 +02:00
parent 7ec6941fe8
commit cce35658a5
2 changed files with 136 additions and 0 deletions

View File

@@ -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]

View File

@@ -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)
}
}