From 5e521f7f53925e76d49db2b81d341e0004214b0c Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Sat, 4 Jul 2026 19:39:16 +0200 Subject: [PATCH] [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 --- crates/xenia-jit/src/emit.rs | 140 +++++++++++++++++++++++++++++++++- crates/xenia-jit/src/tests.rs | 74 ++++++++++++++++++ 2 files changed, 213 insertions(+), 1 deletion(-) diff --git a/crates/xenia-jit/src/emit.rs b/crates/xenia-jit/src/emit.rs index f172026..7aea1a1 100644 --- a/crates/xenia-jit/src/emit.rs +++ b/crates/xenia-jit/src/emit.rs @@ -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::()`). + 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::() 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] diff --git a/crates/xenia-jit/src/tests.rs b/crates/xenia-jit/src/tests.rs index 11a1412..a44be5e 100644 --- a/crates/xenia-jit/src/tests.rs +++ b/crates/xenia-jit/src/tests.rs @@ -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.