[iterate-4C] JIT Phase 2b-i: native compares + CR field emission

Native cmpi/cmpli/cmp/cmpl via x64 cmp + setcc into cr[bf].{lt,gt,eq},
so from xer_so. CrField flag offsets resolved with offset_of! (the type
is not repr(C), so byte positions are not assumed). Both 32/64-bit (L)
widths handled. Differential test compares_match (2000 seeds x both
widths x both xer_so values) asserts full CR nibble + counters; check()
now also asserts CR for all opcodes. Golden n200m BYTE-IDENTICAL with
and without XENIA_JIT.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-04 19:39:16 +02:00
parent 0f1130e2e6
commit 5e521f7f53
2 changed files with 213 additions and 1 deletions

View File

@@ -18,11 +18,21 @@
use dynasmrt::{DynasmApi, dynasm};
use xenia_cpu::PpcOpcode;
use xenia_cpu::context::PpcContext;
use xenia_cpu::context::{CrField, PpcContext};
use xenia_cpu::decoder::DecodedInstr;
use crate::JitEnv;
/// Which byte within a `CrField` — used to address the individual condition
/// flags the emitters write with `setcc`.
#[derive(Clone, Copy)]
enum CrByte {
Lt,
Gt,
Eq,
So,
}
/// Byte offsets into `JitEnv`/`PpcContext`, resolved once per block compile.
pub struct Offsets {
pub env_ctx: i32,
@@ -30,6 +40,18 @@ pub struct Offsets {
pub pc: i32,
pub cycle: i32,
pub timebase: i32,
/// Base of the `cr: [CrField; 8]` array.
cr: i32,
/// Stride between adjacent `CrField`s (`size_of::<CrField>()`).
cr_stride: i32,
/// In-`CrField` byte offsets of the four flags (layout is NOT `repr(C)`, so
/// these are resolved rather than assumed to be 0/1/2/3).
cr_lt: i32,
cr_gt: i32,
cr_eq: i32,
cr_so: i32,
/// `xer_so` byte — source of every CR field's summary-overflow bit.
xer_so: i32,
}
impl Offsets {
@@ -40,6 +62,13 @@ 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,
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,
cr_gt: core::mem::offset_of!(CrField, gt) as i32,
cr_eq: core::mem::offset_of!(CrField, eq) as i32,
cr_so: core::mem::offset_of!(CrField, so) as i32,
xer_so: core::mem::offset_of!(PpcContext, xer_so) as i32,
}
}
/// Byte offset of guest register `i` (`gpr[i]`, 8 bytes each).
@@ -47,6 +76,17 @@ impl Offsets {
fn gpr(&self, i: usize) -> i32 {
self.gpr + (i as i32) * 8
}
/// Byte offset of one flag inside `cr[field]`.
#[inline]
fn cr_byte(&self, field: usize, which: CrByte) -> i32 {
let within = match which {
CrByte::Lt => self.cr_lt,
CrByte::Gt => self.cr_gt,
CrByte::Eq => self.cr_eq,
CrByte::So => self.cr_so,
};
self.cr + (field as i32) * self.cr_stride + within
}
}
type Asm = dynasmrt::x64::Assembler;
@@ -226,6 +266,74 @@ pub fn try_emit_native(
advance_and_count(ops, off);
true
}
// ===== Compares: cr[bf] = { lt, gt, eq, so=xer_so!=0 } =====
// cr[bf] = signed(ra ? imm) [L: 64-bit, else 32-bit]
PpcOpcode::cmpi => {
let bf = instr.crfd();
let imm = instr.simm16() as i32; // 16-bit sign-extended fits i32; cmp sign-extends to 64
if instr.l() {
dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(ra)] ; cmp rax, imm);
} else {
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);
true
}
// cr[bf] = unsigned(ra ? imm)
PpcOpcode::cmpli => {
let bf = instr.crfd();
let imm = instr.uimm16() as i32; // 0..65535 -> positive i32 (zext == small positive)
if instr.l() {
dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(ra)] ; cmp rax, imm);
} else {
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);
true
}
// cr[bf] = signed(ra ? rb)
PpcOpcode::cmp => {
let bf = instr.crfd();
if instr.l() {
dynasm!(ops ; .arch x64
; mov rax, [r15 + off.gpr(ra)]
; mov rcx, [r15 + off.gpr(rb)]
; cmp rax, rcx
);
} else {
dynasm!(ops ; .arch x64
; mov eax, [r15 + off.gpr(ra)]
; mov ecx, [r15 + off.gpr(rb)]
; cmp eax, ecx
);
}
emit_cr_from_flags(ops, off, bf, /*signed=*/ true);
advance_and_count(ops, off);
true
}
// cr[bf] = unsigned(ra ? rb)
PpcOpcode::cmpl => {
let bf = instr.crfd();
if instr.l() {
dynasm!(ops ; .arch x64
; mov rax, [r15 + off.gpr(ra)]
; mov rcx, [r15 + off.gpr(rb)]
; cmp rax, rcx
);
} else {
dynasm!(ops ; .arch x64
; mov eax, [r15 + off.gpr(ra)]
; mov ecx, [r15 + off.gpr(rb)]
; cmp eax, ecx
);
}
emit_cr_from_flags(ops, off, bf, /*signed=*/ false);
advance_and_count(ops, off);
true
}
// ===== 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);
@@ -328,6 +436,36 @@ fn emit_store(ops: &mut Asm, off: &Offsets, helper: i64, ra: usize, rs: usize, d
advance_and_count(ops, off);
}
/// Emit `cr[field] = { lt, gt, eq, so=xer_so!=0 }` from the flags of a `cmp`
/// that was JUST executed. `setcc` writes a 0/1 byte — the exact `bool` repr the
/// interpreter stores. The lt/gt/eq `setcc`s MUST come before the `so` compare
/// (which clobbers the flags). Signed uses `setl/setg`, unsigned `setb/seta`.
#[inline]
fn emit_cr_from_flags(ops: &mut Asm, off: &Offsets, field: usize, signed: bool) {
let lt = off.cr_byte(field, CrByte::Lt);
let gt = off.cr_byte(field, CrByte::Gt);
let eq = off.cr_byte(field, CrByte::Eq);
let so = off.cr_byte(field, CrByte::So);
if signed {
dynasm!(ops ; .arch x64
; setl BYTE [r15 + lt]
; setg BYTE [r15 + gt]
; sete BYTE [r15 + eq]
);
} else {
dynasm!(ops ; .arch x64
; setb BYTE [r15 + lt]
; seta BYTE [r15 + gt]
; sete BYTE [r15 + eq]
);
}
// so = (xer_so != 0), normalized to 0/1. Clobbers flags — done last.
dynasm!(ops ; .arch x64
; cmp BYTE [r15 + off.xer_so], 0
; setne BYTE [r15 + so]
);
}
/// Load `gpr[ra]` into `rax`, or zero `rax` when `ra == 0` (the PPC
/// arithmetic-D "RA=0 means literal 0" rule, statically known here).
#[inline]

View File

@@ -208,6 +208,59 @@ fn check(raw: u32, gpr: [u64; 32]) {
assert_eq!(a.xer_ca, b.xer_ca, "xer_ca mismatch raw={raw:#010x}");
assert_eq!(a.xer_ov, b.xer_ov, "xer_ov mismatch raw={raw:#010x}");
assert_eq!(a.xer_so, b.xer_so, "xer_so 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} ({:?})", instr.opcode);
assert_eq!(ra, rb, "StepResult mismatch raw={raw:#010x}");
}
/// Compare-opcode differential check: like [`check`] but seeds `xer_so` on both
/// contexts (compares copy it into `cr[bf].so`) and pre-fills every CR field
/// with a distinct nibble so a mis-targeted `bf` write is caught.
fn check_cmp(raw: u32, gpr: [u64; 32], xer_so: 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!(
emit::try_emit_native(&mut probe, &off, &helpers, &instr),
"compare not natively emitted raw={raw:#010x} ({:?})",
instr.opcode
);
let seed_cr = |c: &mut PpcContext| {
c.xer_so = xer_so;
for (i, f) in c.cr.iter_mut().enumerate() {
*f = xenia_cpu::context::CrField::from_u8((i as u8) & 0xF);
}
};
let mem = NoMem;
let mut a = ctx_from_gpr(gpr, pc);
seed_cr(&mut a);
let ra = interpret_one(&mut a, &mem, &instr);
a.cycle_count += 1;
a.timebase += 1;
let mut b = ctx_from_gpr(gpr, pc);
seed_cr(&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}");
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} ({:?})", instr.opcode);
assert_eq!(ra, rb, "StepResult mismatch raw={raw:#010x}");
}
@@ -284,6 +337,27 @@ fn arith_reg_matches() {
}
}
#[test]
fn compares_match() {
let mut s = 0xc0deu64;
for _ in 0..ITERS {
let g = fuzz_gpr(&mut s);
let bf = (rng(&mut s) % 8) as u32;
let ra = (rng(&mut s) % 32) as u32;
let rb = (rng(&mut s) % 32) as u32;
let simm = rng(&mut s) as u16;
let so = (rng(&mut s) & 1) as u8; // exercise both so=0 and so!=0
// L bit: 0 = 32-bit, 1 = 64-bit. Test both widths.
for l in [0u32, 1u32] {
let b6_10 = (bf << 2) | l;
check_cmp(enc_d(11, b6_10, ra, simm), g, so); // cmpi
check_cmp(enc_d(10, b6_10, ra, simm), g, so); // cmpli
check_cmp(enc_x(31, b6_10, ra, rb, 0, 0), g, so); // cmp
check_cmp(enc_x(31, b6_10, ra, rb, 32, 0), g, so); // cmpl
}
}
}
/// 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.