diff --git a/crates/xenia-jit/src/emit.rs b/crates/xenia-jit/src/emit.rs index d5965f1..ceb3c57 100644 --- a/crates/xenia-jit/src/emit.rs +++ b/crates/xenia-jit/src/emit.rs @@ -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 diff --git a/crates/xenia-jit/src/tests.rs b/crates/xenia-jit/src/tests.rs index 0ded813..c56f618 100644 --- a/crates/xenia-jit/src/tests.rs +++ b/crates/xenia-jit/src/tests.rs @@ -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;