diff --git a/crates/xenia-jit/src/emit.rs b/crates/xenia-jit/src/emit.rs index ceb3c57..fe58f69 100644 --- a/crates/xenia-jit/src/emit.rs +++ b/crates/xenia-jit/src/emit.rs @@ -125,28 +125,47 @@ impl Offsets { type Asm = dynasmrt::x64::Assembler; -/// Emit `pc += 4; cycle_count += 1; timebase += 1` — the postlude every native -/// (non-branch) opcode shares, matching `interpreter.rs` (`execute` does -/// `ctx.pc += 4`, `step_block` bumps the counters after each instruction). -#[inline] -fn advance_and_count(ops: &mut Asm, off: &Offsets) { - dynasm!(ops - ; .arch x64 - ; add DWORD [r15 + off.pc], 4 - ; inc QWORD [r15 + off.cycle] - ; inc QWORD [r15 + off.timebase] - ); +/// Deferred per-block accounting (the counter/pc deferral optimization). +/// +/// Instead of writing `pc += 4; cycle_count += 1; timebase += 1` after every +/// native instruction (3 memory RMWs each), we DEFER it. Straight-line native +/// ops just bump a compile-time `pending` counter and emit nothing; the counter +/// bumps are materialized in bulk (`add [cycle], N`) only at observability +/// points, and `pc` is written absolutely (from the known instruction address) +/// only where something reads it. +/// +/// The load-bearing invariant: **at every block-exit edge, `ctx.pc`, +/// `cycle_count`, and `timebase` hold exactly the interpreter's values.** +/// Nothing observes them mid-block — native ops never read pc/counters; before +/// each interpreter fallback (which may `mftb` timebase or branch pc) the caller +/// flushes counters and sets pc; native branches set pc absolutely and flush. +#[derive(Default)] +pub struct EmitState { + pending: u32, } -/// Emit `cycle_count += 1; timebase += 1` WITHOUT touching `pc` — the postlude -/// for native branches, which set `pc` themselves. -#[inline] -fn count_only(ops: &mut Asm, off: &Offsets) { - dynasm!(ops - ; .arch x64 - ; inc QWORD [r15 + off.cycle] - ; inc QWORD [r15 + off.timebase] - ); +impl EmitState { + pub fn new() -> Self { + Self { pending: 0 } + } + /// A native instruction retired: defer its counter bump. + #[inline] + pub fn retire(&mut self) { + self.pending += 1; + } + /// Materialize the deferred counter bumps: `cycle_count += pending; + /// timebase += pending`, then reset. A no-op when nothing is pending. + pub fn flush_counters(&mut self, ops: &mut Asm, off: &Offsets) { + if self.pending > 0 { + let n = self.pending as i32; + dynasm!(ops + ; .arch x64 + ; add QWORD [r15 + off.cycle], n + ; add QWORD [r15 + off.timebase], n + ); + self.pending = 0; + } + } } /// Try to emit native x64 for `instr`. Returns [`Emit::Native`] if it fully @@ -157,6 +176,7 @@ pub fn try_emit_native( ops: &mut Asm, off: &Offsets, helpers: &crate::MemHelpers, + state: &mut EmitState, instr: &DecodedInstr, ) -> Emit { let ra = instr.ra(); @@ -171,7 +191,7 @@ pub fn try_emit_native( dynasm!(ops ; .arch x64 ; add rax, simm); } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax); - advance_and_count(ops, off); + state.retire(); Emit::Native } // rD = (rA==0 ? 0 : gpr[rA]) + (EXTS(SIMM) << 16) [64-bit] @@ -184,7 +204,7 @@ pub fn try_emit_native( dynasm!(ops ; .arch x64 ; add rax, simm); } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax); - advance_and_count(ops, off); + state.retire(); Emit::Native } // gpr[rA] = gpr[rS] | ZEXT(UIMM) [never records] @@ -195,7 +215,7 @@ pub fn try_emit_native( dynasm!(ops ; .arch x64 ; or rax, uimm); } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax); - advance_and_count(ops, off); + state.retire(); Emit::Native } // gpr[rA] = gpr[rS] | (ZEXT(UIMM) << 16) @@ -207,7 +227,7 @@ pub fn try_emit_native( ; or rax, rcx ; mov [r15 + off.gpr(ra)], rax ); - advance_and_count(ops, off); + state.retire(); Emit::Native } // gpr[rA] = gpr[rS] ^ ZEXT(UIMM) @@ -218,7 +238,7 @@ pub fn try_emit_native( dynasm!(ops ; .arch x64 ; xor rax, uimm); } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax); - advance_and_count(ops, off); + state.retire(); Emit::Native } // gpr[rA] = gpr[rS] ^ (ZEXT(UIMM) << 16) @@ -230,7 +250,7 @@ pub fn try_emit_native( ; xor rax, rcx ; mov [r15 + off.gpr(ra)], rax ); - advance_and_count(ops, off); + state.retire(); Emit::Native } // gpr[rA] = gpr[rS] | gpr[rB] [64-bit]. Skip the db16cyc spin hint @@ -244,7 +264,7 @@ pub fn try_emit_native( ; or rax, [r15 + off.gpr(rb)] ; mov [r15 + off.gpr(ra)], rax ); - advance_and_count(ops, off); + state.retire(); Emit::Native } // gpr[rA] = gpr[rS] & gpr[rB] [64-bit] @@ -257,7 +277,7 @@ pub fn try_emit_native( ; and rax, [r15 + off.gpr(rb)] ; mov [r15 + off.gpr(ra)], rax ); - advance_and_count(ops, off); + state.retire(); Emit::Native } // gpr[rA] = gpr[rS] ^ gpr[rB] [64-bit] @@ -270,7 +290,7 @@ pub fn try_emit_native( ; xor rax, [r15 + off.gpr(rb)] ; mov [r15 + off.gpr(ra)], rax ); - advance_and_count(ops, off); + state.retire(); Emit::Native } // rD = gpr[rA] + gpr[rB] [64-bit]; skip OE/recording forms. @@ -283,7 +303,7 @@ pub fn try_emit_native( ; add rax, [r15 + off.gpr(rb)] ; mov [r15 + off.gpr(rd)], rax ); - advance_and_count(ops, off); + state.retire(); Emit::Native } // rD = gpr[rB] - gpr[rA] [64-bit]; skip OE/recording forms. @@ -296,7 +316,7 @@ pub fn try_emit_native( ; sub rax, [r15 + off.gpr(ra)] ; mov [r15 + off.gpr(rd)], rax ); - advance_and_count(ops, off); + state.retire(); Emit::Native } // rD = 0 - gpr[rA] [64-bit]; skip OE/recording forms. @@ -309,7 +329,7 @@ pub fn try_emit_native( ; neg rax ; mov [r15 + off.gpr(rd)], rax ); - advance_and_count(ops, off); + state.retire(); Emit::Native } // ===== Compares: cr[bf] = { lt, gt, eq, so=xer_so!=0 } ===== @@ -323,7 +343,7 @@ pub fn try_emit_native( dynasm!(ops ; .arch x64 ; mov eax, [r15 + off.gpr(ra)] ; cmp eax, imm); } emit_cr_from_flags(ops, off, bf, /*signed=*/ true); - advance_and_count(ops, off); + state.retire(); Emit::Native } // cr[bf] = unsigned(ra ? imm) @@ -336,7 +356,7 @@ pub fn try_emit_native( dynasm!(ops ; .arch x64 ; mov eax, [r15 + off.gpr(ra)] ; cmp eax, imm); } emit_cr_from_flags(ops, off, bf, /*signed=*/ false); - advance_and_count(ops, off); + state.retire(); Emit::Native } // cr[bf] = signed(ra ? rb) @@ -356,7 +376,7 @@ pub fn try_emit_native( ); } emit_cr_from_flags(ops, off, bf, /*signed=*/ true); - advance_and_count(ops, off); + state.retire(); Emit::Native } // cr[bf] = unsigned(ra ? rb) @@ -376,44 +396,44 @@ pub fn try_emit_native( ); } emit_cr_from_flags(ops, off, bf, /*signed=*/ false); - advance_and_count(ops, off); + state.retire(); Emit::Native } // ===== Loads (D-form): rD = EXT(mem[(rA==0?0:gpr[rA]) + EXTS(D)]) ===== PpcOpcode::lbz => { - emit_load(ops, off, helpers.read_u8, ra, rd, instr.d(), Ext::Zx8); + emit_load(ops, off, state, helpers.read_u8, ra, rd, instr.d(), Ext::Zx8); Emit::Native } PpcOpcode::lhz => { - emit_load(ops, off, helpers.read_u16, ra, rd, instr.d(), Ext::Zx16); + emit_load(ops, off, state, helpers.read_u16, ra, rd, instr.d(), Ext::Zx16); Emit::Native } PpcOpcode::lha => { - emit_load(ops, off, helpers.read_u16, ra, rd, instr.d(), Ext::Sx16); + emit_load(ops, off, state, helpers.read_u16, ra, rd, instr.d(), Ext::Sx16); Emit::Native } PpcOpcode::lwz => { - emit_load(ops, off, helpers.read_u32, ra, rd, instr.d(), Ext::Zx32); + emit_load(ops, off, state, helpers.read_u32, ra, rd, instr.d(), Ext::Zx32); Emit::Native } // ===== Stores: mem[(rA==0?0:gpr[rA]) + EXTS(D/DS)] = gpr[rS] ===== PpcOpcode::stb => { - emit_store(ops, off, helpers.store_u8, ra, rd, instr.d()); + emit_store(ops, off, state, helpers.store_u8, ra, rd, instr.d()); Emit::Native } PpcOpcode::sth => { - emit_store(ops, off, helpers.store_u16, ra, rd, instr.d()); + emit_store(ops, off, state, helpers.store_u16, ra, rd, instr.d()); Emit::Native } PpcOpcode::stw => { - emit_store(ops, off, helpers.store_u32, ra, rd, instr.d()); + emit_store(ops, off, state, helpers.store_u32, ra, rd, instr.d()); Emit::Native } PpcOpcode::std => { // DS-form displacement (14-bit signed << 2). - emit_store(ops, off, helpers.store_u64, ra, rd, instr.ds()); + emit_store(ops, off, state, helpers.store_u64, ra, rd, instr.ds()); Emit::Native } @@ -431,7 +451,7 @@ pub fn try_emit_native( if instr.rc_bit() { emit_cr0_from_reg(ops, off, false); } - advance_and_count(ops, off); + state.retire(); Emit::Native } // rlwimi: RA = (ROTL32(RS,SH) & MASK) | (RA & ~MASK) [insert] @@ -452,7 +472,7 @@ pub fn try_emit_native( if instr.rc_bit() { emit_cr0_from_reg(ops, off, false); } - advance_and_count(ops, off); + state.retire(); Emit::Native } // rlwnm: like rlwinm but SH = RB[27:31] (runtime, masked to 0x1F). @@ -469,7 +489,7 @@ pub fn try_emit_native( if instr.rc_bit() { emit_cr0_from_reg(ops, off, false); } - advance_and_count(ops, off); + state.retire(); Emit::Native } // rldicl: RA = ROTL64(RS, SH) & mask_left(mb) [64-bit] @@ -488,7 +508,7 @@ pub fn try_emit_native( if instr.rc_bit() { emit_cr0_from_reg(ops, off, true); } - advance_and_count(ops, off); + state.retire(); Emit::Native } // rldicr: RA = ROTL64(RS, SH) & mask_right(me) [64-bit] @@ -507,81 +527,78 @@ pub fn try_emit_native( if instr.rc_bit() { emit_cr0_from_reg(ops, off, true); } - advance_and_count(ops, off); + state.retire(); Emit::Native } // ===== Branches (block terminators) ===== - // Unconditional: target = aa ? LI : pc+LI; lk -> lr = pc+4; pc = target. + // With the pc-deferral model, pc is NOT live in memory here; the branch + // targets are compile-time constants (addr is known), so branches SET pc + // absolutely and never read `[pc]`. `next`/`target` computed like the + // interpreter (u32 wrapping). Each branch flushes the deferred counters + // (incl. its own retire) so counters are current on the exit edge. + // Unconditional: target = aa ? LI : addr+LI; lk -> lr = addr+4. PpcOpcode::bx => { - if instr.lk() { - // lr = (pc+4) as u64, from the ORIGINAL pc (before it changes). - dynasm!(ops ; .arch x64 - ; mov edx, [r15 + off.pc] - ; lea ecx, [rdx + 4] // 64-bit base wraps like (pc+4) as u32 - ; mov [r15 + off.lr], rcx - ); - } - if instr.aa() { - let tgt = instr.li() as u32 as i32; - dynasm!(ops ; .arch x64 ; mov DWORD [r15 + off.pc], tgt); + let next = instr.addr.wrapping_add(4); + let target = if instr.aa() { + instr.li() as u32 } else { - let li = instr.li(); // signed offset (already <<2) - dynasm!(ops ; .arch x64 ; add DWORD [r15 + off.pc], li); + instr.addr.wrapping_add(instr.li() as u32) + }; + if instr.lk() { + dynasm!(ops ; .arch x64 ; mov eax, next as i32 ; mov [r15 + off.lr], rax); } - count_only(ops, off); + dynasm!(ops ; .arch x64 ; mov DWORD [r15 + off.pc], target as i32); + state.retire(); + state.flush_counters(ops, off); Emit::Branch } - // Conditional: optional CTR decrement + CTR/CR test; taken -> pc=target, - // else pc+=4; lk -> lr = pc+4 in both cases (from the original pc). + // Conditional: optional CTR decrement + CTR/CR test; taken -> target, + // else next; lk -> lr = addr+4 (both cases). PpcOpcode::bcx => { let bo = instr.bo(); let bi = instr.bi(); - emit_branch_cond(ops, off, bo, bi); // taken (0/1) -> al - dynasm!(ops ; .arch x64 ; mov edx, [r15 + off.pc]); // edx = original pc - if instr.lk() { - dynasm!(ops ; .arch x64 - ; lea ecx, [rdx + 4] - ; mov [r15 + off.lr], rcx - ); - } - // not-taken candidate (pc+4) in ecx; taken target in edx. - dynasm!(ops ; .arch x64 ; lea ecx, [rdx + 4]); - if instr.aa() { - let tgt = instr.bd() as u32 as i32; - dynasm!(ops ; .arch x64 ; mov edx, tgt); + let next = instr.addr.wrapping_add(4); + let target = if instr.aa() { + instr.bd() as u32 } else { - let bd = instr.bd(); // signed offset (already <<2) - dynasm!(ops ; .arch x64 ; lea edx, [rdx + bd]); + instr.addr.wrapping_add(instr.bd() as u32) + }; + emit_branch_cond(ops, off, bo, bi); // taken (0/1) -> al + if instr.lk() { + dynasm!(ops ; .arch x64 ; mov ecx, next as i32 ; mov [r15 + off.lr], rcx); } dynasm!(ops ; .arch x64 + ; mov ecx, next as i32 // not-taken default + ; mov edx, target as i32 // taken target ; test al, al - ; cmovne ecx, edx // taken -> target, else stays pc+4 + ; cmovne ecx, edx ; mov [r15 + off.pc], ecx ); - count_only(ops, off); + state.retire(); + state.flush_counters(ops, off); Emit::Branch } - // Return via LR: taken -> pc = (lr as u32) & !3, else pc+=4; - // lk -> lr = pc+4 (set AFTER reading lr for the target). + // Return via LR: taken -> pc = (lr as u32) & !3, else next; + // lk -> lr = addr+4 (set AFTER reading lr for the target). PpcOpcode::bclrx => { let bo = instr.bo(); let bi = instr.bi(); + let next = instr.addr.wrapping_add(4); emit_branch_cond(ops, off, bo, bi); // taken (0/1) -> al dynasm!(ops ; .arch x64 - ; mov edx, [r15 + off.pc] - ; lea r8d, [rdx + 4] // next_pc = pc+4, preserved in r8 - ; mov edx, [r15 + off.lr] // edx = lr low 32 + ; mov edx, [r15 + off.lr] // edx = lr low 32 (read before any lk write) ; and edx, -4 // & !3 - ; mov ecx, r8d // not-taken default = next_pc + ; mov ecx, next as i32 // not-taken default ; test al, al ; cmovne ecx, edx // taken -> lr & !3 ; mov [r15 + off.pc], ecx ); if instr.lk() { - dynasm!(ops ; .arch x64 ; mov [r15 + off.lr], r8); + dynasm!(ops ; .arch x64 ; mov eax, next as i32 ; mov [r15 + off.lr], rax); } - count_only(ops, off); + state.retire(); + state.flush_counters(ops, off); Emit::Branch } @@ -654,7 +671,7 @@ fn emit_ea(ops: &mut Asm, off: &Offsets, ra: usize, disp: i32) { /// Emit a native load: `rsi=ea; rdi=env; call read_helper; extend; gpr[rd]=res`. #[inline] -fn emit_load(ops: &mut Asm, off: &Offsets, helper: i64, ra: usize, rd: usize, disp: i32, ext: Ext) { +fn emit_load(ops: &mut Asm, off: &Offsets, state: &mut EmitState, helper: i64, ra: usize, rd: usize, disp: i32, ext: Ext) { emit_ea(ops, off, ra, disp); dynasm!(ops ; .arch x64 @@ -671,12 +688,12 @@ fn emit_load(ops: &mut Asm, off: &Offsets, helper: i64, ra: usize, rd: usize, di Ext::Zx32 => dynasm!(ops ; .arch x64 ; mov eax, eax), } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax); - advance_and_count(ops, off); + state.retire(); } /// Emit a native store: `rsi=ea; rdx=gpr[rs]; rdi=env; call store_helper`. #[inline] -fn emit_store(ops: &mut Asm, off: &Offsets, helper: i64, ra: usize, rs: usize, disp: i32) { +fn emit_store(ops: &mut Asm, off: &Offsets, state: &mut EmitState, helper: i64, ra: usize, rs: usize, disp: i32) { emit_ea(ops, off, ra, disp); dynasm!(ops ; .arch x64 @@ -685,7 +702,7 @@ fn emit_store(ops: &mut Asm, off: &Offsets, helper: i64, ra: usize, rs: usize, d ; mov rax, QWORD helper ; call rax ); - advance_and_count(ops, off); + state.retire(); } /// Emit `cr[0] = update_cr_signed(result)` where the result is in `rax`/`eax`: diff --git a/crates/xenia-jit/src/lib.rs b/crates/xenia-jit/src/lib.rs index d3ed310..caaee6e 100644 --- a/crates/xenia-jit/src/lib.rs +++ b/crates/xenia-jit/src/lib.rs @@ -241,14 +241,23 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock { ; mov r15, [rbx + off.env_ctx] ); + // Counter/pc deferral: native ops just accumulate `state.pending`; pc and the + // counters are materialized only at observability points (fallbacks, branch + // edges, block end). `tail_pc` = Some(next_pc) when the last emitted op was + // straight-line native (its pc write is deferred to the fall-through end); + // None when a branch/fallback already set pc. + let mut state = emit::EmitState::new(); + let mut tail_pc: Option = None; for instr in instrs.iter() { - match emit::try_emit_native(&mut ops, &off, &mem_helpers, instr) { - // Native non-branch: fully handled (compute + pc+=4 + counter - // bumps), control always continues — no exit check needed. - emit::Emit::Native => continue, - // Native branch: it set pc/lr/ctr + bumped counters. Append the same - // pc-discontinuity check the fallback path uses so a taken branch - // ends the block and a fall-through continues (matches step_block). + match emit::try_emit_native(&mut ops, &off, &mem_helpers, &mut state, instr) { + // Native non-branch: computation only; pc/counters deferred. + emit::Emit::Native => { + tail_pc = Some(instr.addr.wrapping_add(4)); + continue; + } + // Native branch: it set pc absolutely and flushed the counters. + // Append the pc-discontinuity check (taken -> exit via l_cont; + // fall-through -> continue), matching step_block. emit::Emit::Branch => { let expected_next = instr.addr.wrapping_add(4) as i32; dynasm!(ops @@ -256,22 +265,28 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock { ; cmp DWORD [r15 + off.pc], expected_next ; jne =>l_cont ); + tail_pc = None; continue; } // Un-ported opcode: emit the interpreter fallback below. emit::Emit::Fallback => {} } - // Interpreter fallback for un-ported opcodes. + // Interpreter fallback for un-ported opcodes. Make counters + pc current + // first (the callee may read timebase via mftb and reads/writes pc), then + // run it and account its own retirement (interpreter order: after execute). + state.flush_counters(&mut ops, &off); + let addr = instr.addr as i32; let instr_ptr = instr as *const DecodedInstr as usize as i64; let expected_next = instr.addr.wrapping_add(4) as i32; dynasm!(ops ; .arch x64 + ; mov DWORD [r15 + off.pc], addr // fallback: eax = jit_interpret_one(env, &instr); env.last_result set ; mov rdi, rbx ; mov rsi, QWORD instr_ptr ; mov rax, QWORD helper ; call rax - // determinism postlude: cycle_count += 1; timebase += 1 + // determinism postlude: this instruction retired. ; inc QWORD [r15 + off.cycle] ; inc QWORD [r15 + off.timebase] // non-Continue result -> exit returning the discriminant in eax @@ -281,6 +296,16 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock { ; cmp DWORD [r15 + off.pc], expected_next ; jne =>l_cont ); + tail_pc = None; + } + + // Block end (fall-through): flush the deferred counters and materialize the + // final pc if the tail was straight-line native. Emitted BEFORE l_cont so a + // taken branch/fallback (which jumps to l_cont) skips it — its pc/counters + // are already current. + state.flush_counters(&mut ops, &off); + if let Some(p) = tail_pc { + dynasm!(ops ; .arch x64 ; mov DWORD [r15 + off.pc], p as i32); } // Natural end / discontinuity exit: Continue (eax=0). Shared epilogue. diff --git a/crates/xenia-jit/src/tests.rs b/crates/xenia-jit/src/tests.rs index c56f618..14e73cb 100644 --- a/crates/xenia-jit/src/tests.rs +++ b/crates/xenia-jit/src/tests.rs @@ -191,7 +191,7 @@ fn check(raw: u32, gpr: [u64; 32]) { let helpers = crate::MemHelpers::resolve(); let mut probe = dynasmrt::x64::Assembler::new().unwrap(); assert!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback, + emit::try_emit_native(&mut probe, &off, &helpers, &mut emit::EmitState::new(), &instr) != emit::Emit::Fallback, "opcode not natively emitted for raw={raw:#010x} ({:?})", instr.opcode ); @@ -238,7 +238,7 @@ fn check_cmp(raw: u32, gpr: [u64; 32], xer_so: u8) { let helpers = crate::MemHelpers::resolve(); let mut probe = dynasmrt::x64::Assembler::new().unwrap(); assert!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback, + emit::try_emit_native(&mut probe, &off, &helpers, &mut emit::EmitState::new(), &instr) != emit::Emit::Fallback, "compare not natively emitted raw={raw:#010x} ({:?})", instr.opcode ); @@ -361,7 +361,7 @@ fn check_rot(raw: u32, gpr: [u64; 32], xer_so: u8) { let helpers = crate::MemHelpers::resolve(); let mut probe = dynasmrt::x64::Assembler::new().unwrap(); assert!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback, + emit::try_emit_native(&mut probe, &off, &helpers, &mut emit::EmitState::new(), &instr) != emit::Emit::Fallback, "rotate not natively emitted raw={raw:#010x} ({:?})", instr.opcode ); @@ -467,7 +467,7 @@ fn check_branch(raw: u32, lr: u64, ctr: u64, cr_seed: u8) { let helpers = crate::MemHelpers::resolve(); let mut probe = dynasmrt::x64::Assembler::new().unwrap(); assert_eq!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr), + emit::try_emit_native(&mut probe, &off, &helpers, &mut emit::EmitState::new(), &instr), emit::Emit::Branch, "branch not emitted as Emit::Branch raw={raw:#010x} ({:?})", instr.opcode @@ -559,6 +559,64 @@ fn return_branch_matches() { } } +/// Multi-instruction block test: exercises the counter/pc DEFERRAL (pending +/// accumulation across native ops, mid-block flush before a fallback, native +/// terminating branch) by comparing a JIT-compiled block against the real +/// `step_block` over the SAME block. A single-instruction test can't catch a +/// deferral bug (pending never accumulates); this can. +#[test] +fn multi_instr_block_matches() { + use xenia_cpu::interpreter::step_block; + let base = 0x8200_1000u32; + // Straight-line natives, a FALLBACK in the middle (mulli, op 7 — not ported), + // more natives incl. a CR-writing compare, terminated by an unconditional bx. + let raws = [ + enc_d(14, 5, 0, 0x100), // addi r5, r0, 0x100 + enc_d(14, 6, 5, 0x001), // addi r6, r5, 1 + (7 << 26) | (7 << 21) | (6 << 16) | 3, // mulli r7, r6, 3 (FALLBACK) + enc_x(31, 5, 8, 6, 444, 0), // or r8, r5, r6 + enc_x(31, 0, 7, 8, 0, 0), // cmp cr0, r7, r8 + enc_bx(0x40, 0, 0), // b +0x40 (native branch, terminator) + ]; + let instrs: Vec = raws + .iter() + .enumerate() + .map(|(i, &raw)| decode(raw, base + (i as u32) * 4)) + .collect(); + let block = DecodedBlock { + start_pc: base, + end_pc: base + (raws.len() as u32) * 4, + page_version: 0, + instrs, + sync_sensitive: false, + }; + + let gpr = { + let mut s = 0x9191u64; + fuzz_gpr(&mut s) + }; + + // Reference: the real interpreter block stepper. + let mem = NoMem; + let mut a = ctx_from_gpr(gpr, base); + let ra = step_block(&mut a, &mem, &block); + + // JIT. + let mut b = ctx_from_gpr(gpr, base); + let cb: CompiledBlock = compile_block(&block); + let rb = run_jit_block(&cb, &mut b, &mem); + + assert_eq!(a.gpr, b.gpr, "gpr mismatch"); + assert_eq!(a.pc, b.pc, "pc mismatch (deferred pc materialization)"); + assert_eq!(a.lr, b.lr, "lr mismatch"); + assert_eq!(a.cycle_count, b.cycle_count, "cycle mismatch (deferred counter)"); + assert_eq!(a.timebase, b.timebase, "timebase mismatch (deferred counter)"); + let cra: [u8; 8] = std::array::from_fn(|i| a.cr[i].as_u8()); + let crb: [u8; 8] = std::array::from_fn(|i| b.cr[i].as_u8()); + assert_eq!(cra, crb, "cr mismatch"); + assert_eq!(ra, rb, "StepResult mismatch"); +} + /// The recording/OE forms and the db16cyc hint must NOT be natively emitted /// (they fall back to the interpreter). Guards against a future emitter /// accidentally handling a form it can't reproduce. @@ -576,7 +634,7 @@ fn recording_and_hint_forms_fall_back() { for raw in cases { let instr = decode(raw, 0x8200_1000); assert!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr) == emit::Emit::Fallback, + emit::try_emit_native(&mut probe, &off, &helpers, &mut emit::EmitState::new(), &instr) == emit::Emit::Fallback, "raw={raw:#010x} ({:?}) should fall back, not native", instr.opcode ); @@ -593,7 +651,7 @@ fn check_mem(raw: u32, gpr: [u64; 32]) { let helpers = crate::MemHelpers::resolve(); let mut probe = dynasmrt::x64::Assembler::new().unwrap(); assert!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback, + emit::try_emit_native(&mut probe, &off, &helpers, &mut emit::EmitState::new(), &instr) != emit::Emit::Fallback, "mem opcode not natively emitted raw={raw:#010x} ({:?})", instr.opcode );