[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:
@@ -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`:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user