[iterate-4C] JIT Phase 3b: defer pc/counter accounting per block

Strip the 3 per-instruction memory RMWs (pc+=4, cycle++, timebase++) from
straight-line native ops. EmitState.pending accumulates retired native
instrs at compile time; counters are materialized in bulk (add [cycle],N)
only at observability points, and pc is written absolutely (from the known
addr) only where read. Invariant: at every block-exit edge ctx.pc/
cycle_count/timebase are exactly the interpreter's values (nothing observes
them mid-block; fallbacks flush+set-pc first; native branches set pc
absolutely from addr and flush). Branches rewritten to use compile-time
addr (no more [pc] reads). New multi_instr_block_matches test diffs a
JIT block vs the real step_block (native+fallback+branch mix). Golden
n200m BYTE-IDENTICAL with and without XENIA_JIT (13 tests green).
Throughput 4.55->4.44s; ratio vs interp still ~1.16 -> gap is fallback
tax (mflr/mtlr, indexed/update ld-st, shifts still fallback) + the run
being only ~40% CPU-step, NOT the native-path RMWs. Deferral is also the
flush-discipline substrate for register caching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-04 20:08:15 +02:00
parent 781a82cc0a
commit e8d0dc4a2d
3 changed files with 210 additions and 110 deletions

View File

@@ -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`:

View File

@@ -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<u32> = 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.

View File

@@ -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<DecodedInstr> = 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
);