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