[iterate-4C] JIT Phase 3a: native rotate/mask family

Native rlwinm/rlwimi/rlwnm (32-bit rol+and, mask folded to a compile-time
constant) and rldicl/rldicr (64-bit rol + 64-bit mask), including the Rc
recording forms via emit_cr0_from_reg (test + signed setcc into cr[0],
reusing the compare CR path). rlw_mask/rld_mask replicated as
compile-time helpers. Differential test rotates_match: 2000 seeds x
{rc,non-rc} x {32,64-bit} x xer_so, asserting full GPR+CR+counters.
Golden n200m BYTE-IDENTICAL with and without XENIA_JIT (12 tests green).
Throughput 4.7->4.55s (interp ~3.9s): opcode coverage now at diminishing
returns; the remaining gap is memory-traffic-per-instruction (no register
caching) + fallback tax, not missing opcodes -> Phase 4 (register cache)
is the crossover lever.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-04 19:54:51 +02:00
parent 701e4c399a
commit 781a82cc0a
2 changed files with 208 additions and 0 deletions

View File

@@ -417,6 +417,100 @@ pub fn try_emit_native(
Emit::Native
}
// ===== Rotate/mask (sh/mb/me are compile-time constants -> the 32/64-bit
// mask folds to a constant, so these are just rol + and). =====
// rlwinm: RA = ROTL32(RS, SH) & MASK(mb,me)
PpcOpcode::rlwinmx => {
let sh = instr.sh();
let mask = rlw_mask(instr.mb(), instr.me()) as i32;
dynasm!(ops ; .arch x64 ; mov eax, [r15 + off.gpr(rd)]);
if sh != 0 {
dynasm!(ops ; .arch x64 ; rol eax, sh as i8);
}
dynasm!(ops ; .arch x64 ; and eax, mask ; mov [r15 + off.gpr(ra)], rax);
if instr.rc_bit() {
emit_cr0_from_reg(ops, off, false);
}
advance_and_count(ops, off);
Emit::Native
}
// rlwimi: RA = (ROTL32(RS,SH) & MASK) | (RA & ~MASK) [insert]
PpcOpcode::rlwimix => {
let sh = instr.sh();
let mask = rlw_mask(instr.mb(), instr.me());
dynasm!(ops ; .arch x64 ; mov eax, [r15 + off.gpr(rd)]);
if sh != 0 {
dynasm!(ops ; .arch x64 ; rol eax, sh as i8);
}
dynasm!(ops ; .arch x64
; and eax, mask as i32
; mov ecx, [r15 + off.gpr(ra)]
; and ecx, !mask as i32
; or eax, ecx
; mov [r15 + off.gpr(ra)], rax
);
if instr.rc_bit() {
emit_cr0_from_reg(ops, off, false);
}
advance_and_count(ops, off);
Emit::Native
}
// rlwnm: like rlwinm but SH = RB[27:31] (runtime, masked to 0x1F).
PpcOpcode::rlwnmx => {
let mask = rlw_mask(instr.mb(), instr.me()) as i32;
dynasm!(ops ; .arch x64
; mov eax, [r15 + off.gpr(rd)]
; mov ecx, [r15 + off.gpr(rb)]
; and ecx, 0x1F
; rol eax, cl
; and eax, mask
; mov [r15 + off.gpr(ra)], rax
);
if instr.rc_bit() {
emit_cr0_from_reg(ops, off, false);
}
advance_and_count(ops, off);
Emit::Native
}
// rldicl: RA = ROTL64(RS, SH) & mask_left(mb) [64-bit]
PpcOpcode::rldiclx => {
let sh = instr.sh64();
let mask = rld_mask_left(instr.mb_md()) as i64;
dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)]);
if sh != 0 {
dynasm!(ops ; .arch x64 ; rol rax, sh as i8);
}
dynasm!(ops ; .arch x64
; mov rcx, QWORD mask
; and rax, rcx
; mov [r15 + off.gpr(ra)], rax
);
if instr.rc_bit() {
emit_cr0_from_reg(ops, off, true);
}
advance_and_count(ops, off);
Emit::Native
}
// rldicr: RA = ROTL64(RS, SH) & mask_right(me) [64-bit]
PpcOpcode::rldicrx => {
let sh = instr.sh64();
let mask = rld_mask_right(instr.mb_md()) as i64;
dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)]);
if sh != 0 {
dynasm!(ops ; .arch x64 ; rol rax, sh as i8);
}
dynasm!(ops ; .arch x64
; mov rcx, QWORD mask
; and rax, rcx
; mov [r15 + off.gpr(ra)], rax
);
if instr.rc_bit() {
emit_cr0_from_reg(ops, off, true);
}
advance_and_count(ops, off);
Emit::Native
}
// ===== Branches (block terminators) =====
// Unconditional: target = aa ? LI : pc+LI; lk -> lr = pc+4; pc = target.
PpcOpcode::bx => {
@@ -594,6 +688,36 @@ fn emit_store(ops: &mut Asm, off: &Offsets, helper: i64, ra: usize, rs: usize, d
advance_and_count(ops, off);
}
/// Emit `cr[0] = update_cr_signed(result)` where the result is in `rax`/`eax`:
/// a signed comparison of the value against 0. `test` sets ZF/SF (OF=0), so the
/// signed `setl/setg/sete` in [`emit_cr_from_flags`] give lt/gt/eq vs 0.
#[inline]
fn emit_cr0_from_reg(ops: &mut Asm, off: &Offsets, is64: bool) {
if is64 {
dynasm!(ops ; .arch x64 ; test rax, rax);
} else {
dynasm!(ops ; .arch x64 ; test eax, eax);
}
emit_cr_from_flags(ops, off, 0, /*signed=*/ true);
}
/// 32-bit `rlwinm`/`rlwimi`/`rlwnm` mask (mirrors `interpreter.rs::rlw_mask`).
fn rlw_mask(mb: u32, me: u32) -> u32 {
if mb <= me {
(u32::MAX >> mb) & (u32::MAX << (31 - me))
} else {
(u32::MAX >> mb) | (u32::MAX << (31 - me))
}
}
/// 64-bit left mask for `rldicl` (mirrors `interpreter.rs::rld_mask_left`).
fn rld_mask_left(mb: u32) -> u64 {
if mb == 0 { u64::MAX } else { u64::MAX >> mb }
}
/// 64-bit right mask for `rldicr` (mirrors `interpreter.rs::rld_mask_right`).
fn rld_mask_right(me: u32) -> u64 {
if me >= 63 { u64::MAX } else { u64::MAX << (63 - me) }
}
/// 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

View File

@@ -351,6 +351,90 @@ fn arith_reg_matches() {
}
}
/// Rotate/shift differential check: seeds `xer_so` (rc forms copy it into
/// `cr[0].so`) and pre-fills CR with distinct nibbles, then asserts full
/// GPR + CR + counters + StepResult.
fn check_rot(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) != emit::Emit::Fallback,
"rotate not natively emitted raw={raw:#010x} ({:?})",
instr.opcode
);
let seed = |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(&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(&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.gpr, b.gpr, "gpr mismatch raw={raw:#010x} ({:?})", instr.opcode);
assert_eq!(a.pc, b.pc, "pc mismatch raw={raw:#010x}");
assert_eq!(a.cycle_count, b.cycle_count, "cycle 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}");
}
#[test]
fn rotates_match() {
let mut s = 0x2107u64;
for _ in 0..ITERS {
let g = fuzz_gpr(&mut s);
let rs = (rng(&mut s) % 32) as u32;
let ra = (rng(&mut s) % 32) as u32;
let rb = (rng(&mut s) % 32) as u32;
let sh = (rng(&mut s) % 32) as u32;
let mb = (rng(&mut s) % 32) as u32;
let me = (rng(&mut s) % 32) as u32;
let so = (rng(&mut s) & 1) as u8;
for rc in [0u32, 1u32] {
// rlwinm (op 21), rlwimi (op 20), rlwnm (op 23): SH/MB/ME in the
// standard M-form fields (SH 16-20, MB 21-25, ME 26-30).
let m32 = (sh << 11) | (mb << 6) | (me << 1) | rc;
check_rot((21 << 26) | (rs << 21) | (ra << 16) | m32, g, so); // rlwinm
check_rot((20 << 26) | (rs << 21) | (ra << 16) | m32, g, so); // rlwimi
check_rot((23 << 26) | (rs << 21) | (ra << 16) | (rb << 11) | (mb << 6) | (me << 1) | rc, g, so); // rlwnm
// rldicl (op 30, XO 0), rldicr (op 30, XO 1): 6-bit SH split
// (bit5 at position 1), 6-bit MB/ME (bit5 at position 5).
let sh6 = (rng(&mut s) % 64) as u32;
let mb6 = (rng(&mut s) % 64) as u32;
let sh_lo = sh6 & 0x1F;
let sh_hi = (sh6 >> 5) & 1;
let m_lo = mb6 & 0x1F;
let m_hi = (mb6 >> 5) & 1;
let common = (rs << 21) | (ra << 16) | (sh_lo << 11) | (m_lo << 6) | (sh_hi << 1) | rc;
check_rot((30 << 26) | common | (m_hi << 5) | (0 << 2), g, so); // rldicl (XO field: MB6 + xo)
check_rot((30 << 26) | common | (m_hi << 5) | (1 << 2), g, so); // rldicr
}
}
}
#[test]
fn compares_match() {
let mut s = 0xc0deu64;