[iterate-4C] JIT Phase 2b-ii: native branches (bx/bcx/bclrx)
try_emit_native now returns a 3-state Emit enum (Fallback/Native/Branch); compile_block appends the pc==expected_next discontinuity check after a native branch, mirroring step_block. bx (aa/rel, lk), bcx (CTR decrement + CTR/CR condition, cmov-selected target), bclrx (return via lr & !3) emitted natively; bcctrx stays on fallback (dispatch_rec side effect). count_only postlude (counters, no pc+=4) for branches. Differential tests: unconditional/conditional/return branches over exhaustive BO bits, CTR==1->0 boundary, lr alignment mask, both lk states, asserting pc/lr/ctr/cr/counters. Golden n200m BYTE-IDENTICAL with and without XENIA_JIT (11 tests green). Throughput 4.85->4.7s (interp ~3.9s); crossover still pending rotate/shift (Phase 3). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,20 @@ use xenia_cpu::decoder::DecodedInstr;
|
||||
|
||||
use crate::JitEnv;
|
||||
|
||||
/// Outcome of trying to emit one instruction natively.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum Emit {
|
||||
/// Not handled — the caller must emit the interpreter fallback + exit checks.
|
||||
Fallback,
|
||||
/// Fully handled; `pc += 4` and control always continues to the next
|
||||
/// instruction (no exit check needed).
|
||||
Native,
|
||||
/// Handled, but the instruction may change `pc` discontinuously (a branch).
|
||||
/// The caller must append the `pc == expected_next` check so a taken branch
|
||||
/// ends the block and a fall-through continues — exactly like `step_block`.
|
||||
Branch,
|
||||
}
|
||||
|
||||
/// Which byte within a `CrField` — used to address the individual condition
|
||||
/// flags the emitters write with `setcc`.
|
||||
#[derive(Clone, Copy)]
|
||||
@@ -40,6 +54,10 @@ pub struct Offsets {
|
||||
pub pc: i32,
|
||||
pub cycle: i32,
|
||||
pub timebase: i32,
|
||||
/// Link register (`lr: u64`).
|
||||
lr: i32,
|
||||
/// Count register (`ctr: u64`).
|
||||
ctr: i32,
|
||||
/// Base of the `cr: [CrField; 8]` array.
|
||||
cr: i32,
|
||||
/// Stride between adjacent `CrField`s (`size_of::<CrField>()`).
|
||||
@@ -62,6 +80,8 @@ impl Offsets {
|
||||
pc: core::mem::offset_of!(PpcContext, pc) as i32,
|
||||
cycle: core::mem::offset_of!(PpcContext, cycle_count) as i32,
|
||||
timebase: core::mem::offset_of!(PpcContext, timebase) as i32,
|
||||
lr: core::mem::offset_of!(PpcContext, lr) as i32,
|
||||
ctr: core::mem::offset_of!(PpcContext, ctr) as i32,
|
||||
cr: core::mem::offset_of!(PpcContext, cr) as i32,
|
||||
cr_stride: core::mem::size_of::<CrField>() as i32,
|
||||
cr_lt: core::mem::offset_of!(CrField, lt) as i32,
|
||||
@@ -87,6 +107,20 @@ impl Offsets {
|
||||
};
|
||||
self.cr + (field as i32) * self.cr_stride + within
|
||||
}
|
||||
/// Byte offset of absolute CR bit `bit` (0-31), matching
|
||||
/// `PpcContext::get_cr_bit`: `field = bit/4`, `sub = bit%4` →
|
||||
/// `lt/gt/eq/so`. The byte holds the flag as 0/1.
|
||||
#[inline]
|
||||
fn cr_bit_off(&self, bit: u32) -> i32 {
|
||||
let field = (bit / 4) as usize;
|
||||
let which = match bit % 4 {
|
||||
0 => CrByte::Lt,
|
||||
1 => CrByte::Gt,
|
||||
2 => CrByte::Eq,
|
||||
_ => CrByte::So,
|
||||
};
|
||||
self.cr_byte(field, which)
|
||||
}
|
||||
}
|
||||
|
||||
type Asm = dynasmrt::x64::Assembler;
|
||||
@@ -104,15 +138,27 @@ fn advance_and_count(ops: &mut Asm, off: &Offsets) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Try to emit native x64 for `instr`. Returns `true` if it fully handled the
|
||||
/// instruction (computation + `advance_and_count`); `false` if the caller must
|
||||
/// fall back to the interpreter for it.
|
||||
/// 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]
|
||||
);
|
||||
}
|
||||
|
||||
/// Try to emit native x64 for `instr`. Returns [`Emit::Native`] if it fully
|
||||
/// handled the instruction (computation + `advance_and_count`), [`Emit::Branch`]
|
||||
/// if it handled a branch (caller must add the pc-discontinuity exit check), or
|
||||
/// [`Emit::Fallback`] if the caller must fall back to the interpreter.
|
||||
pub fn try_emit_native(
|
||||
ops: &mut Asm,
|
||||
off: &Offsets,
|
||||
helpers: &crate::MemHelpers,
|
||||
instr: &DecodedInstr,
|
||||
) -> bool {
|
||||
) -> Emit {
|
||||
let ra = instr.ra();
|
||||
let rb = instr.rb();
|
||||
let rd = instr.rd(); // == rs()
|
||||
@@ -126,7 +172,7 @@ pub fn try_emit_native(
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// rD = (rA==0 ? 0 : gpr[rA]) + (EXTS(SIMM) << 16) [64-bit]
|
||||
PpcOpcode::addis => {
|
||||
@@ -139,7 +185,7 @@ pub fn try_emit_native(
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// gpr[rA] = gpr[rS] | ZEXT(UIMM) [never records]
|
||||
PpcOpcode::ori => {
|
||||
@@ -150,7 +196,7 @@ pub fn try_emit_native(
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// gpr[rA] = gpr[rS] | (ZEXT(UIMM) << 16)
|
||||
PpcOpcode::oris => {
|
||||
@@ -162,7 +208,7 @@ pub fn try_emit_native(
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// gpr[rA] = gpr[rS] ^ ZEXT(UIMM)
|
||||
PpcOpcode::xori => {
|
||||
@@ -173,7 +219,7 @@ pub fn try_emit_native(
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// gpr[rA] = gpr[rS] ^ (ZEXT(UIMM) << 16)
|
||||
PpcOpcode::xoris => {
|
||||
@@ -185,13 +231,13 @@ pub fn try_emit_native(
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// gpr[rA] = gpr[rS] | gpr[rB] [64-bit]. Skip the db16cyc spin hint
|
||||
// (returns Yield) and the recording form.
|
||||
PpcOpcode::orx => {
|
||||
if instr.rc_bit() || instr.raw == 0x7FFF_FB78 {
|
||||
return false;
|
||||
return Emit::Fallback;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rd)]
|
||||
@@ -199,12 +245,12 @@ pub fn try_emit_native(
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// gpr[rA] = gpr[rS] & gpr[rB] [64-bit]
|
||||
PpcOpcode::andx => {
|
||||
if instr.rc_bit() {
|
||||
return false;
|
||||
return Emit::Fallback;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rd)]
|
||||
@@ -212,12 +258,12 @@ pub fn try_emit_native(
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// gpr[rA] = gpr[rS] ^ gpr[rB] [64-bit]
|
||||
PpcOpcode::xorx => {
|
||||
if instr.rc_bit() {
|
||||
return false;
|
||||
return Emit::Fallback;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rd)]
|
||||
@@ -225,12 +271,12 @@ pub fn try_emit_native(
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// rD = gpr[rA] + gpr[rB] [64-bit]; skip OE/recording forms.
|
||||
PpcOpcode::addx => {
|
||||
if instr.oe() || instr.rc_bit() {
|
||||
return false;
|
||||
return Emit::Fallback;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(ra)]
|
||||
@@ -238,12 +284,12 @@ pub fn try_emit_native(
|
||||
; mov [r15 + off.gpr(rd)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// rD = gpr[rB] - gpr[rA] [64-bit]; skip OE/recording forms.
|
||||
PpcOpcode::subfx => {
|
||||
if instr.oe() || instr.rc_bit() {
|
||||
return false;
|
||||
return Emit::Fallback;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rb)]
|
||||
@@ -251,12 +297,12 @@ pub fn try_emit_native(
|
||||
; mov [r15 + off.gpr(rd)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// rD = 0 - gpr[rA] [64-bit]; skip OE/recording forms.
|
||||
PpcOpcode::negx => {
|
||||
if instr.oe() || instr.rc_bit() {
|
||||
return false;
|
||||
return Emit::Fallback;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(ra)]
|
||||
@@ -264,7 +310,7 @@ pub fn try_emit_native(
|
||||
; mov [r15 + off.gpr(rd)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// ===== Compares: cr[bf] = { lt, gt, eq, so=xer_so!=0 } =====
|
||||
// cr[bf] = signed(ra ? imm) [L: 64-bit, else 32-bit]
|
||||
@@ -278,7 +324,7 @@ pub fn try_emit_native(
|
||||
}
|
||||
emit_cr_from_flags(ops, off, bf, /*signed=*/ true);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// cr[bf] = unsigned(ra ? imm)
|
||||
PpcOpcode::cmpli => {
|
||||
@@ -291,7 +337,7 @@ pub fn try_emit_native(
|
||||
}
|
||||
emit_cr_from_flags(ops, off, bf, /*signed=*/ false);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// cr[bf] = signed(ra ? rb)
|
||||
PpcOpcode::cmp => {
|
||||
@@ -311,7 +357,7 @@ pub fn try_emit_native(
|
||||
}
|
||||
emit_cr_from_flags(ops, off, bf, /*signed=*/ true);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
// cr[bf] = unsigned(ra ? rb)
|
||||
PpcOpcode::cmpl => {
|
||||
@@ -331,50 +377,162 @@ pub fn try_emit_native(
|
||||
}
|
||||
emit_cr_from_flags(ops, off, bf, /*signed=*/ false);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
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);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
PpcOpcode::lhz => {
|
||||
emit_load(ops, off, helpers.read_u16, ra, rd, instr.d(), Ext::Zx16);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
PpcOpcode::lha => {
|
||||
emit_load(ops, off, helpers.read_u16, ra, rd, instr.d(), Ext::Sx16);
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
PpcOpcode::lwz => {
|
||||
emit_load(ops, off, helpers.read_u32, ra, rd, instr.d(), Ext::Zx32);
|
||||
true
|
||||
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());
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
PpcOpcode::sth => {
|
||||
emit_store(ops, off, helpers.store_u16, ra, rd, instr.d());
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
PpcOpcode::stw => {
|
||||
emit_store(ops, off, helpers.store_u32, ra, rd, instr.d());
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
PpcOpcode::std => {
|
||||
// DS-form displacement (14-bit signed << 2).
|
||||
emit_store(ops, off, helpers.store_u64, ra, rd, instr.ds());
|
||||
true
|
||||
Emit::Native
|
||||
}
|
||||
|
||||
_ => false,
|
||||
// ===== Branches (block terminators) =====
|
||||
// Unconditional: target = aa ? LI : pc+LI; lk -> lr = pc+4; pc = target.
|
||||
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);
|
||||
} else {
|
||||
let li = instr.li(); // signed offset (already <<2)
|
||||
dynasm!(ops ; .arch x64 ; add DWORD [r15 + off.pc], li);
|
||||
}
|
||||
count_only(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).
|
||||
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);
|
||||
} else {
|
||||
let bd = instr.bd(); // signed offset (already <<2)
|
||||
dynasm!(ops ; .arch x64 ; lea edx, [rdx + bd]);
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; test al, al
|
||||
; cmovne ecx, edx // taken -> target, else stays pc+4
|
||||
; mov [r15 + off.pc], ecx
|
||||
);
|
||||
count_only(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).
|
||||
PpcOpcode::bclrx => {
|
||||
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]
|
||||
; lea r8d, [rdx + 4] // next_pc = pc+4, preserved in r8
|
||||
; mov edx, [r15 + off.lr] // edx = lr low 32
|
||||
; and edx, -4 // & !3
|
||||
; mov ecx, r8d // not-taken default = next_pc
|
||||
; 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);
|
||||
}
|
||||
count_only(ops, off);
|
||||
Emit::Branch
|
||||
}
|
||||
|
||||
_ => Emit::Fallback,
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit the `bcx`/`bclrx` condition evaluation, leaving `taken` (0/1) in `al`.
|
||||
/// Mirrors the interpreter: optionally decrement CTR (when `BO2` is clear), then
|
||||
/// `ctr_ok = BO2 || (((ctr as u32)!=0) ^ BO3)` and
|
||||
/// `cond_ok = BO0 || (get_cr_bit(bi) == BO1)`; `taken = ctr_ok && cond_ok`.
|
||||
/// (`BO` bits are numbered from the MSB: BO0=0x10, BO1=0x08, BO2=0x04,
|
||||
/// BO3=0x02.) Everything folds to constants at compile time except the runtime
|
||||
/// CTR/CR reads. Clobbers rax, rcx.
|
||||
fn emit_branch_cond(ops: &mut Asm, off: &Offsets, bo: u32, bi: u32) {
|
||||
// ctr_ok -> al
|
||||
if bo & 0b00100 != 0 {
|
||||
dynasm!(ops ; .arch x64 ; mov al, 1); // CTR ignored: always ctr_ok
|
||||
} else {
|
||||
// Decrement CTR (u64), then test its low 32 bits against 0.
|
||||
dynasm!(ops ; .arch x64
|
||||
; dec QWORD [r15 + off.ctr]
|
||||
; mov ecx, [r15 + off.ctr]
|
||||
; test ecx, ecx
|
||||
);
|
||||
if bo & 0b00010 != 0 {
|
||||
dynasm!(ops ; .arch x64 ; sete al); // branch if CTR == 0
|
||||
} else {
|
||||
dynasm!(ops ; .arch x64 ; setne al); // branch if CTR != 0
|
||||
}
|
||||
}
|
||||
// cond_ok -> cl
|
||||
if bo & 0b10000 != 0 {
|
||||
dynasm!(ops ; .arch x64 ; mov cl, 1); // condition ignored: always cond_ok
|
||||
} else {
|
||||
let crbit = off.cr_bit_off(bi);
|
||||
dynasm!(ops ; .arch x64 ; mov cl, [r15 + crbit]); // crbit is 0/1
|
||||
if bo & 0b01000 == 0 {
|
||||
// expected bit == 0: cond_ok = !crbit
|
||||
dynasm!(ops ; .arch x64 ; xor cl, 1);
|
||||
}
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; and al, cl); // taken = ctr_ok & cond_ok
|
||||
}
|
||||
|
||||
/// Result-extension mode for a native load.
|
||||
#[derive(Clone, Copy)]
|
||||
enum Ext {
|
||||
|
||||
@@ -242,11 +242,24 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock {
|
||||
);
|
||||
|
||||
for instr in instrs.iter() {
|
||||
// Native fast path: emitters fully handle the instruction (compute +
|
||||
// pc+=4 + counter bumps) and never diverge control flow, so no exit
|
||||
// checks are needed after them.
|
||||
if emit::try_emit_native(&mut ops, &off, &mem_helpers, instr) {
|
||||
continue;
|
||||
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).
|
||||
emit::Emit::Branch => {
|
||||
let expected_next = instr.addr.wrapping_add(4) as i32;
|
||||
dynasm!(ops
|
||||
; .arch x64
|
||||
; cmp DWORD [r15 + off.pc], expected_next
|
||||
; jne =>l_cont
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// Un-ported opcode: emit the interpreter fallback below.
|
||||
emit::Emit::Fallback => {}
|
||||
}
|
||||
// Interpreter fallback for un-ported opcodes.
|
||||
let instr_ptr = instr as *const DecodedInstr as usize as i64;
|
||||
|
||||
@@ -148,6 +148,20 @@ fn enc_x(op: u32, b6_10: u32, b11_15: u32, b16_20: u32, xo: u32, rc: u32) -> u32
|
||||
fn enc_xo(op: u32, b6_10: u32, b11_15: u32, b16_20: u32, oe: u32, xo: u32, rc: u32) -> u32 {
|
||||
(op << 26) | (b6_10 << 21) | (b11_15 << 16) | (b16_20 << 11) | (oe << 10) | (xo << 1) | rc
|
||||
}
|
||||
/// I-form `bx` (op 18): 24-bit LI (bits 6-29, target offset >> 2), AA, LK.
|
||||
fn enc_bx(off_bytes: i32, aa: u32, lk: u32) -> u32 {
|
||||
let li24 = ((off_bytes >> 2) as u32) & 0x00FF_FFFF;
|
||||
(18 << 26) | (li24 << 2) | (aa << 1) | lk
|
||||
}
|
||||
/// B-form `bcx` (op 16): BO, BI, 14-bit BD (bits 16-29, offset >> 2), AA, LK.
|
||||
fn enc_bcx(bo: u32, bi: u32, off_bytes: i32, aa: u32, lk: u32) -> u32 {
|
||||
let bd14 = ((off_bytes >> 2) as u32) & 0x0000_3FFF;
|
||||
(16 << 26) | (bo << 21) | (bi << 16) | (bd14 << 2) | (aa << 1) | lk
|
||||
}
|
||||
/// XL-form `bclrx` (op 19, XO 16): BO, BI, LK.
|
||||
fn enc_bclrx(bo: u32, bi: u32, lk: u32) -> u32 {
|
||||
(19 << 26) | (bo << 21) | (bi << 16) | (16 << 1) | lk
|
||||
}
|
||||
|
||||
/// Deterministic PRNG (splitmix64-ish) so failures are reproducible.
|
||||
fn rng(state: &mut u64) -> u64 {
|
||||
@@ -177,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::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback,
|
||||
"opcode not natively emitted for raw={raw:#010x} ({:?})",
|
||||
instr.opcode
|
||||
);
|
||||
@@ -224,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::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback,
|
||||
"compare not natively emitted raw={raw:#010x} ({:?})",
|
||||
instr.opcode
|
||||
);
|
||||
@@ -358,6 +372,109 @@ fn compares_match() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Branch differential check: seed pc/lr/ctr/cr identically, run the branch
|
||||
/// through the interpreter and the JIT (a 1-instruction block), and assert
|
||||
/// pc/lr/ctr/cr/counters/StepResult all match. Branches must be emitted as
|
||||
/// [`emit::Emit::Branch`] specifically.
|
||||
fn check_branch(raw: u32, lr: u64, ctr: u64, cr_seed: u8) {
|
||||
let pc = 0x8200_1000u32;
|
||||
let instr: DecodedInstr = decode(raw, pc);
|
||||
let off = emit::Offsets::resolve();
|
||||
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::Emit::Branch,
|
||||
"branch not emitted as Emit::Branch raw={raw:#010x} ({:?})",
|
||||
instr.opcode
|
||||
);
|
||||
|
||||
let seed = |c: &mut PpcContext| {
|
||||
c.lr = lr;
|
||||
c.ctr = ctr;
|
||||
for (i, f) in c.cr.iter_mut().enumerate() {
|
||||
*f = xenia_cpu::context::CrField::from_u8(cr_seed.wrapping_add(i as u8) & 0xF);
|
||||
}
|
||||
};
|
||||
|
||||
let mem = NoMem;
|
||||
let mut a = ctx_from_gpr([0u64; 32], pc);
|
||||
seed(&mut a);
|
||||
let ra = interpret_one(&mut a, &mem, &instr);
|
||||
a.cycle_count += 1;
|
||||
a.timebase += 1;
|
||||
|
||||
let mut b = ctx_from_gpr([0u64; 32], pc);
|
||||
seed(&mut b);
|
||||
let block = DecodedBlock {
|
||||
start_pc: pc,
|
||||
end_pc: pc.wrapping_add(4),
|
||||
page_version: 0,
|
||||
instrs: vec![instr],
|
||||
sync_sensitive: false,
|
||||
};
|
||||
let cb: CompiledBlock = compile_block(&block);
|
||||
let rb = run_jit_block(&cb, &mut b, &mem);
|
||||
|
||||
assert_eq!(a.pc, b.pc, "pc mismatch raw={raw:#010x} ({:?})", instr.opcode);
|
||||
assert_eq!(a.lr, b.lr, "lr mismatch raw={raw:#010x} ({:?})", instr.opcode);
|
||||
assert_eq!(a.ctr, b.ctr, "ctr mismatch raw={raw:#010x} ({:?})", instr.opcode);
|
||||
assert_eq!(a.cycle_count, b.cycle_count, "cycle mismatch raw={raw:#010x}");
|
||||
assert_eq!(a.timebase, b.timebase, "timebase mismatch raw={raw:#010x}");
|
||||
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 raw={raw:#010x}");
|
||||
assert_eq!(ra, rb, "StepResult mismatch raw={raw:#010x}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unconditional_branch_matches() {
|
||||
let mut s = 0xb00u64;
|
||||
for _ in 0..ITERS {
|
||||
// Non-negative and negative offsets (multiples of 4), both lk states.
|
||||
let off = ((rng(&mut s) & 0x3FFF) as i32) * 4 - 0x8000;
|
||||
for lk in [0u32, 1u32] {
|
||||
check_branch(enc_bx(off, 0, lk), 0xdead_beef_1234_0000, 0x55, 0);
|
||||
}
|
||||
// Absolute form: small positive absolute target.
|
||||
let abs = ((rng(&mut s) & 0x3FFF) as i32) * 4;
|
||||
check_branch(enc_bx(abs, 1, 0), 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conditional_branch_matches() {
|
||||
let mut s = 0xbc0u64;
|
||||
for _ in 0..ITERS {
|
||||
let bo = (rng(&mut s) % 32) as u32; // exhaustive over the 5 BO bits
|
||||
let bi = (rng(&mut s) % 32) as u32; // any CR bit
|
||||
let off = ((rng(&mut s) & 0x1FFF) as i32) * 4 - 0x4000;
|
||||
let ctr = rng(&mut s); // hits ctr==1 boundary (dec ->0) sometimes
|
||||
let cr_seed = rng(&mut s) as u8;
|
||||
for lk in [0u32, 1u32] {
|
||||
check_branch(enc_bcx(bo, bi, off, 0, lk), 0, ctr, cr_seed);
|
||||
}
|
||||
// Force the CTR==1 -> 0 boundary explicitly.
|
||||
check_branch(enc_bcx(bo, bi, off, 0, 0), 0, 1, cr_seed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn return_branch_matches() {
|
||||
let mut s = 0xb1cu64;
|
||||
for _ in 0..ITERS {
|
||||
let bo = (rng(&mut s) % 32) as u32;
|
||||
let bi = (rng(&mut s) % 32) as u32;
|
||||
let lr = rng(&mut s); // exercises the & !3 alignment mask
|
||||
let ctr = rng(&mut s);
|
||||
let cr_seed = rng(&mut s) as u8;
|
||||
for lk in [0u32, 1u32] {
|
||||
check_branch(enc_bclrx(bo, bi, lk), lr, ctr, cr_seed);
|
||||
}
|
||||
check_branch(enc_bclrx(bo, bi, 0), lr, 1, cr_seed);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
@@ -375,7 +492,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::try_emit_native(&mut probe, &off, &helpers, &instr) == emit::Emit::Fallback,
|
||||
"raw={raw:#010x} ({:?}) should fall back, not native",
|
||||
instr.opcode
|
||||
);
|
||||
@@ -392,7 +509,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::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback,
|
||||
"mem opcode not natively emitted raw={raw:#010x} ({:?})",
|
||||
instr.opcode
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user