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