diff --git a/crates/xenia-jit/src/emit.rs b/crates/xenia-jit/src/emit.rs index 7aea1a1..d5965f1 100644 --- a/crates/xenia-jit/src/emit.rs +++ b/crates/xenia-jit/src/emit.rs @@ -23,6 +23,20 @@ use xenia_cpu::decoder::DecodedInstr; use crate::JitEnv; +/// Outcome of trying to emit one instruction natively. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum Emit { + /// Not handled — the caller must emit the interpreter fallback + exit checks. + Fallback, + /// Fully handled; `pc += 4` and control always continues to the next + /// instruction (no exit check needed). + Native, + /// Handled, but the instruction may change `pc` discontinuously (a branch). + /// The caller must append the `pc == expected_next` check so a taken branch + /// ends the block and a fall-through continues — exactly like `step_block`. + Branch, +} + /// Which byte within a `CrField` — used to address the individual condition /// flags the emitters write with `setcc`. #[derive(Clone, Copy)] @@ -40,6 +54,10 @@ pub struct Offsets { pub pc: i32, pub cycle: i32, pub timebase: i32, + /// Link register (`lr: u64`). + lr: i32, + /// Count register (`ctr: u64`). + ctr: i32, /// Base of the `cr: [CrField; 8]` array. cr: i32, /// Stride between adjacent `CrField`s (`size_of::()`). @@ -62,6 +80,8 @@ 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, + lr: core::mem::offset_of!(PpcContext, lr) as i32, + ctr: core::mem::offset_of!(PpcContext, ctr) 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, @@ -87,6 +107,20 @@ impl Offsets { }; self.cr + (field as i32) * self.cr_stride + within } + /// Byte offset of absolute CR bit `bit` (0-31), matching + /// `PpcContext::get_cr_bit`: `field = bit/4`, `sub = bit%4` → + /// `lt/gt/eq/so`. The byte holds the flag as 0/1. + #[inline] + fn cr_bit_off(&self, bit: u32) -> i32 { + let field = (bit / 4) as usize; + let which = match bit % 4 { + 0 => CrByte::Lt, + 1 => CrByte::Gt, + 2 => CrByte::Eq, + _ => CrByte::So, + }; + self.cr_byte(field, which) + } } type Asm = dynasmrt::x64::Assembler; @@ -104,15 +138,27 @@ fn advance_and_count(ops: &mut Asm, off: &Offsets) { ); } -/// Try to emit native x64 for `instr`. Returns `true` if it fully handled the -/// instruction (computation + `advance_and_count`); `false` if the caller must -/// fall back to the interpreter for it. +/// Emit `cycle_count += 1; timebase += 1` WITHOUT touching `pc` — the postlude +/// for native branches, which set `pc` themselves. +#[inline] +fn count_only(ops: &mut Asm, off: &Offsets) { + dynasm!(ops + ; .arch x64 + ; inc QWORD [r15 + off.cycle] + ; inc QWORD [r15 + off.timebase] + ); +} + +/// Try to emit native x64 for `instr`. Returns [`Emit::Native`] if it fully +/// handled the instruction (computation + `advance_and_count`), [`Emit::Branch`] +/// if it handled a branch (caller must add the pc-discontinuity exit check), or +/// [`Emit::Fallback`] if the caller must fall back to the interpreter. pub fn try_emit_native( ops: &mut Asm, off: &Offsets, helpers: &crate::MemHelpers, instr: &DecodedInstr, -) -> bool { +) -> Emit { let ra = instr.ra(); let rb = instr.rb(); let rd = instr.rd(); // == rs() @@ -126,7 +172,7 @@ pub fn try_emit_native( } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax); advance_and_count(ops, off); - true + Emit::Native } // rD = (rA==0 ? 0 : gpr[rA]) + (EXTS(SIMM) << 16) [64-bit] PpcOpcode::addis => { @@ -139,7 +185,7 @@ pub fn try_emit_native( } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax); advance_and_count(ops, off); - true + Emit::Native } // gpr[rA] = gpr[rS] | ZEXT(UIMM) [never records] PpcOpcode::ori => { @@ -150,7 +196,7 @@ pub fn try_emit_native( } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax); advance_and_count(ops, off); - true + Emit::Native } // gpr[rA] = gpr[rS] | (ZEXT(UIMM) << 16) PpcOpcode::oris => { @@ -162,7 +208,7 @@ pub fn try_emit_native( ; mov [r15 + off.gpr(ra)], rax ); advance_and_count(ops, off); - true + Emit::Native } // gpr[rA] = gpr[rS] ^ ZEXT(UIMM) PpcOpcode::xori => { @@ -173,7 +219,7 @@ pub fn try_emit_native( } dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax); advance_and_count(ops, off); - true + Emit::Native } // gpr[rA] = gpr[rS] ^ (ZEXT(UIMM) << 16) PpcOpcode::xoris => { @@ -185,13 +231,13 @@ pub fn try_emit_native( ; mov [r15 + off.gpr(ra)], rax ); advance_and_count(ops, off); - true + Emit::Native } // gpr[rA] = gpr[rS] | gpr[rB] [64-bit]. Skip the db16cyc spin hint // (returns Yield) and the recording form. PpcOpcode::orx => { if instr.rc_bit() || instr.raw == 0x7FFF_FB78 { - return false; + return Emit::Fallback; } dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)] @@ -199,12 +245,12 @@ pub fn try_emit_native( ; mov [r15 + off.gpr(ra)], rax ); advance_and_count(ops, off); - true + Emit::Native } // gpr[rA] = gpr[rS] & gpr[rB] [64-bit] PpcOpcode::andx => { if instr.rc_bit() { - return false; + return Emit::Fallback; } dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)] @@ -212,12 +258,12 @@ pub fn try_emit_native( ; mov [r15 + off.gpr(ra)], rax ); advance_and_count(ops, off); - true + Emit::Native } // gpr[rA] = gpr[rS] ^ gpr[rB] [64-bit] PpcOpcode::xorx => { if instr.rc_bit() { - return false; + return Emit::Fallback; } dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)] @@ -225,12 +271,12 @@ pub fn try_emit_native( ; mov [r15 + off.gpr(ra)], rax ); advance_and_count(ops, off); - true + Emit::Native } // rD = gpr[rA] + gpr[rB] [64-bit]; skip OE/recording forms. PpcOpcode::addx => { if instr.oe() || instr.rc_bit() { - return false; + return Emit::Fallback; } dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(ra)] @@ -238,12 +284,12 @@ pub fn try_emit_native( ; mov [r15 + off.gpr(rd)], rax ); advance_and_count(ops, off); - true + Emit::Native } // rD = gpr[rB] - gpr[rA] [64-bit]; skip OE/recording forms. PpcOpcode::subfx => { if instr.oe() || instr.rc_bit() { - return false; + return Emit::Fallback; } dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rb)] @@ -251,12 +297,12 @@ pub fn try_emit_native( ; mov [r15 + off.gpr(rd)], rax ); advance_and_count(ops, off); - true + Emit::Native } // rD = 0 - gpr[rA] [64-bit]; skip OE/recording forms. PpcOpcode::negx => { if instr.oe() || instr.rc_bit() { - return false; + return Emit::Fallback; } dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(ra)] @@ -264,7 +310,7 @@ pub fn try_emit_native( ; mov [r15 + off.gpr(rd)], rax ); advance_and_count(ops, off); - true + Emit::Native } // ===== Compares: cr[bf] = { lt, gt, eq, so=xer_so!=0 } ===== // cr[bf] = signed(ra ? imm) [L: 64-bit, else 32-bit] @@ -278,7 +324,7 @@ pub fn try_emit_native( } emit_cr_from_flags(ops, off, bf, /*signed=*/ true); advance_and_count(ops, off); - true + Emit::Native } // cr[bf] = unsigned(ra ? imm) PpcOpcode::cmpli => { @@ -291,7 +337,7 @@ pub fn try_emit_native( } emit_cr_from_flags(ops, off, bf, /*signed=*/ false); advance_and_count(ops, off); - true + Emit::Native } // cr[bf] = signed(ra ? rb) PpcOpcode::cmp => { @@ -311,7 +357,7 @@ pub fn try_emit_native( } emit_cr_from_flags(ops, off, bf, /*signed=*/ true); advance_and_count(ops, off); - true + Emit::Native } // cr[bf] = unsigned(ra ? rb) PpcOpcode::cmpl => { @@ -331,50 +377,162 @@ pub fn try_emit_native( } emit_cr_from_flags(ops, off, bf, /*signed=*/ false); advance_and_count(ops, off); - true + Emit::Native } // ===== 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); - true + Emit::Native } PpcOpcode::lhz => { emit_load(ops, off, helpers.read_u16, ra, rd, instr.d(), Ext::Zx16); - true + Emit::Native } PpcOpcode::lha => { emit_load(ops, off, helpers.read_u16, ra, rd, instr.d(), Ext::Sx16); - true + Emit::Native } PpcOpcode::lwz => { emit_load(ops, off, helpers.read_u32, ra, rd, instr.d(), Ext::Zx32); - true + Emit::Native } // ===== Stores: mem[(rA==0?0:gpr[rA]) + EXTS(D/DS)] = gpr[rS] ===== PpcOpcode::stb => { emit_store(ops, off, helpers.store_u8, ra, rd, instr.d()); - true + Emit::Native } PpcOpcode::sth => { emit_store(ops, off, helpers.store_u16, ra, rd, instr.d()); - true + Emit::Native } PpcOpcode::stw => { emit_store(ops, off, helpers.store_u32, ra, rd, instr.d()); - true + Emit::Native } PpcOpcode::std => { // DS-form displacement (14-bit signed << 2). emit_store(ops, off, helpers.store_u64, ra, rd, instr.ds()); - true + Emit::Native } - _ => false, + // ===== Branches (block terminators) ===== + // Unconditional: target = aa ? LI : pc+LI; lk -> lr = pc+4; pc = target. + PpcOpcode::bx => { + if instr.lk() { + // lr = (pc+4) as u64, from the ORIGINAL pc (before it changes). + dynasm!(ops ; .arch x64 + ; mov edx, [r15 + off.pc] + ; lea ecx, [rdx + 4] // 64-bit base wraps like (pc+4) as u32 + ; mov [r15 + off.lr], rcx + ); + } + if instr.aa() { + let tgt = instr.li() as u32 as i32; + dynasm!(ops ; .arch x64 ; mov DWORD [r15 + off.pc], tgt); + } else { + let li = instr.li(); // signed offset (already <<2) + dynasm!(ops ; .arch x64 ; add DWORD [r15 + off.pc], li); + } + count_only(ops, off); + Emit::Branch + } + // Conditional: optional CTR decrement + CTR/CR test; taken -> pc=target, + // else pc+=4; lk -> lr = pc+4 in both cases (from the original pc). + PpcOpcode::bcx => { + let bo = instr.bo(); + let bi = instr.bi(); + emit_branch_cond(ops, off, bo, bi); // taken (0/1) -> al + dynasm!(ops ; .arch x64 ; mov edx, [r15 + off.pc]); // edx = original pc + if instr.lk() { + dynasm!(ops ; .arch x64 + ; lea ecx, [rdx + 4] + ; mov [r15 + off.lr], rcx + ); + } + // not-taken candidate (pc+4) in ecx; taken target in edx. + dynasm!(ops ; .arch x64 ; lea ecx, [rdx + 4]); + if instr.aa() { + let tgt = instr.bd() as u32 as i32; + dynasm!(ops ; .arch x64 ; mov edx, tgt); + } else { + let bd = instr.bd(); // signed offset (already <<2) + dynasm!(ops ; .arch x64 ; lea edx, [rdx + bd]); + } + dynasm!(ops ; .arch x64 + ; test al, al + ; cmovne ecx, edx // taken -> target, else stays pc+4 + ; mov [r15 + off.pc], ecx + ); + count_only(ops, off); + Emit::Branch + } + // Return via LR: taken -> pc = (lr as u32) & !3, else pc+=4; + // lk -> lr = pc+4 (set AFTER reading lr for the target). + PpcOpcode::bclrx => { + let bo = instr.bo(); + let bi = instr.bi(); + emit_branch_cond(ops, off, bo, bi); // taken (0/1) -> al + dynasm!(ops ; .arch x64 + ; mov edx, [r15 + off.pc] + ; lea r8d, [rdx + 4] // next_pc = pc+4, preserved in r8 + ; mov edx, [r15 + off.lr] // edx = lr low 32 + ; and edx, -4 // & !3 + ; mov ecx, r8d // not-taken default = next_pc + ; test al, al + ; cmovne ecx, edx // taken -> lr & !3 + ; mov [r15 + off.pc], ecx + ); + if instr.lk() { + dynasm!(ops ; .arch x64 ; mov [r15 + off.lr], r8); + } + count_only(ops, off); + Emit::Branch + } + + _ => Emit::Fallback, } } +/// Emit the `bcx`/`bclrx` condition evaluation, leaving `taken` (0/1) in `al`. +/// Mirrors the interpreter: optionally decrement CTR (when `BO2` is clear), then +/// `ctr_ok = BO2 || (((ctr as u32)!=0) ^ BO3)` and +/// `cond_ok = BO0 || (get_cr_bit(bi) == BO1)`; `taken = ctr_ok && cond_ok`. +/// (`BO` bits are numbered from the MSB: BO0=0x10, BO1=0x08, BO2=0x04, +/// BO3=0x02.) Everything folds to constants at compile time except the runtime +/// CTR/CR reads. Clobbers rax, rcx. +fn emit_branch_cond(ops: &mut Asm, off: &Offsets, bo: u32, bi: u32) { + // ctr_ok -> al + if bo & 0b00100 != 0 { + dynasm!(ops ; .arch x64 ; mov al, 1); // CTR ignored: always ctr_ok + } else { + // Decrement CTR (u64), then test its low 32 bits against 0. + dynasm!(ops ; .arch x64 + ; dec QWORD [r15 + off.ctr] + ; mov ecx, [r15 + off.ctr] + ; test ecx, ecx + ); + if bo & 0b00010 != 0 { + dynasm!(ops ; .arch x64 ; sete al); // branch if CTR == 0 + } else { + dynasm!(ops ; .arch x64 ; setne al); // branch if CTR != 0 + } + } + // cond_ok -> cl + if bo & 0b10000 != 0 { + dynasm!(ops ; .arch x64 ; mov cl, 1); // condition ignored: always cond_ok + } else { + let crbit = off.cr_bit_off(bi); + dynasm!(ops ; .arch x64 ; mov cl, [r15 + crbit]); // crbit is 0/1 + if bo & 0b01000 == 0 { + // expected bit == 0: cond_ok = !crbit + dynasm!(ops ; .arch x64 ; xor cl, 1); + } + } + dynasm!(ops ; .arch x64 ; and al, cl); // taken = ctr_ok & cond_ok +} + /// Result-extension mode for a native load. #[derive(Clone, Copy)] enum Ext { diff --git a/crates/xenia-jit/src/lib.rs b/crates/xenia-jit/src/lib.rs index ea1bcfb..d3ed310 100644 --- a/crates/xenia-jit/src/lib.rs +++ b/crates/xenia-jit/src/lib.rs @@ -242,11 +242,24 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock { ); for instr in instrs.iter() { - // Native fast path: emitters fully handle the instruction (compute + - // pc+=4 + counter bumps) and never diverge control flow, so no exit - // checks are needed after them. - if emit::try_emit_native(&mut ops, &off, &mem_helpers, instr) { - continue; + match emit::try_emit_native(&mut ops, &off, &mem_helpers, instr) { + // Native non-branch: fully handled (compute + pc+=4 + counter + // bumps), control always continues — no exit check needed. + emit::Emit::Native => continue, + // Native branch: it set pc/lr/ctr + bumped counters. Append the same + // pc-discontinuity check the fallback path uses so a taken branch + // ends the block and a fall-through continues (matches step_block). + emit::Emit::Branch => { + let expected_next = instr.addr.wrapping_add(4) as i32; + dynasm!(ops + ; .arch x64 + ; cmp DWORD [r15 + off.pc], expected_next + ; jne =>l_cont + ); + continue; + } + // Un-ported opcode: emit the interpreter fallback below. + emit::Emit::Fallback => {} } // Interpreter fallback for un-ported opcodes. let instr_ptr = instr as *const DecodedInstr as usize as i64; diff --git a/crates/xenia-jit/src/tests.rs b/crates/xenia-jit/src/tests.rs index a44be5e..0ded813 100644 --- a/crates/xenia-jit/src/tests.rs +++ b/crates/xenia-jit/src/tests.rs @@ -148,6 +148,20 @@ fn enc_x(op: u32, b6_10: u32, b11_15: u32, b16_20: u32, xo: u32, rc: u32) -> u32 fn enc_xo(op: u32, b6_10: u32, b11_15: u32, b16_20: u32, oe: u32, xo: u32, rc: u32) -> u32 { (op << 26) | (b6_10 << 21) | (b11_15 << 16) | (b16_20 << 11) | (oe << 10) | (xo << 1) | rc } +/// I-form `bx` (op 18): 24-bit LI (bits 6-29, target offset >> 2), AA, LK. +fn enc_bx(off_bytes: i32, aa: u32, lk: u32) -> u32 { + let li24 = ((off_bytes >> 2) as u32) & 0x00FF_FFFF; + (18 << 26) | (li24 << 2) | (aa << 1) | lk +} +/// B-form `bcx` (op 16): BO, BI, 14-bit BD (bits 16-29, offset >> 2), AA, LK. +fn enc_bcx(bo: u32, bi: u32, off_bytes: i32, aa: u32, lk: u32) -> u32 { + let bd14 = ((off_bytes >> 2) as u32) & 0x0000_3FFF; + (16 << 26) | (bo << 21) | (bi << 16) | (bd14 << 2) | (aa << 1) | lk +} +/// XL-form `bclrx` (op 19, XO 16): BO, BI, LK. +fn enc_bclrx(bo: u32, bi: u32, lk: u32) -> u32 { + (19 << 26) | (bo << 21) | (bi << 16) | (16 << 1) | lk +} /// Deterministic PRNG (splitmix64-ish) so failures are reproducible. fn rng(state: &mut u64) -> u64 { @@ -177,7 +191,7 @@ fn check(raw: u32, gpr: [u64; 32]) { let helpers = crate::MemHelpers::resolve(); let mut probe = dynasmrt::x64::Assembler::new().unwrap(); assert!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr), + emit::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback, "opcode not natively emitted for raw={raw:#010x} ({:?})", instr.opcode ); @@ -224,7 +238,7 @@ fn check_cmp(raw: u32, gpr: [u64; 32], xer_so: u8) { let helpers = crate::MemHelpers::resolve(); let mut probe = dynasmrt::x64::Assembler::new().unwrap(); assert!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr), + emit::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback, "compare not natively emitted raw={raw:#010x} ({:?})", instr.opcode ); @@ -358,6 +372,109 @@ fn compares_match() { } } +/// Branch differential check: seed pc/lr/ctr/cr identically, run the branch +/// through the interpreter and the JIT (a 1-instruction block), and assert +/// pc/lr/ctr/cr/counters/StepResult all match. Branches must be emitted as +/// [`emit::Emit::Branch`] specifically. +fn check_branch(raw: u32, lr: u64, ctr: u64, cr_seed: 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_eq!( + emit::try_emit_native(&mut probe, &off, &helpers, &instr), + emit::Emit::Branch, + "branch not emitted as Emit::Branch raw={raw:#010x} ({:?})", + instr.opcode + ); + + let seed = |c: &mut PpcContext| { + c.lr = lr; + c.ctr = ctr; + for (i, f) in c.cr.iter_mut().enumerate() { + *f = xenia_cpu::context::CrField::from_u8(cr_seed.wrapping_add(i as u8) & 0xF); + } + }; + + let mem = NoMem; + let mut a = ctx_from_gpr([0u64; 32], 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([0u64; 32], 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.pc, b.pc, "pc mismatch raw={raw:#010x} ({:?})", instr.opcode); + assert_eq!(a.lr, b.lr, "lr mismatch raw={raw:#010x} ({:?})", instr.opcode); + assert_eq!(a.ctr, b.ctr, "ctr mismatch raw={raw:#010x} ({:?})", instr.opcode); + 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}"); + assert_eq!(ra, rb, "StepResult mismatch raw={raw:#010x}"); +} + +#[test] +fn unconditional_branch_matches() { + let mut s = 0xb00u64; + for _ in 0..ITERS { + // Non-negative and negative offsets (multiples of 4), both lk states. + let off = ((rng(&mut s) & 0x3FFF) as i32) * 4 - 0x8000; + for lk in [0u32, 1u32] { + check_branch(enc_bx(off, 0, lk), 0xdead_beef_1234_0000, 0x55, 0); + } + // Absolute form: small positive absolute target. + let abs = ((rng(&mut s) & 0x3FFF) as i32) * 4; + check_branch(enc_bx(abs, 1, 0), 0, 0, 0); + } +} + +#[test] +fn conditional_branch_matches() { + let mut s = 0xbc0u64; + for _ in 0..ITERS { + let bo = (rng(&mut s) % 32) as u32; // exhaustive over the 5 BO bits + let bi = (rng(&mut s) % 32) as u32; // any CR bit + let off = ((rng(&mut s) & 0x1FFF) as i32) * 4 - 0x4000; + let ctr = rng(&mut s); // hits ctr==1 boundary (dec ->0) sometimes + let cr_seed = rng(&mut s) as u8; + for lk in [0u32, 1u32] { + check_branch(enc_bcx(bo, bi, off, 0, lk), 0, ctr, cr_seed); + } + // Force the CTR==1 -> 0 boundary explicitly. + check_branch(enc_bcx(bo, bi, off, 0, 0), 0, 1, cr_seed); + } +} + +#[test] +fn return_branch_matches() { + let mut s = 0xb1cu64; + for _ in 0..ITERS { + let bo = (rng(&mut s) % 32) as u32; + let bi = (rng(&mut s) % 32) as u32; + let lr = rng(&mut s); // exercises the & !3 alignment mask + let ctr = rng(&mut s); + let cr_seed = rng(&mut s) as u8; + for lk in [0u32, 1u32] { + check_branch(enc_bclrx(bo, bi, lk), lr, ctr, cr_seed); + } + check_branch(enc_bclrx(bo, bi, 0), lr, 1, cr_seed); + } +} + /// 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. @@ -375,7 +492,7 @@ fn recording_and_hint_forms_fall_back() { for raw in cases { let instr = decode(raw, 0x8200_1000); assert!( - !emit::try_emit_native(&mut probe, &off, &helpers, &instr), + emit::try_emit_native(&mut probe, &off, &helpers, &instr) == emit::Emit::Fallback, "raw={raw:#010x} ({:?}) should fall back, not native", instr.opcode ); @@ -392,7 +509,7 @@ fn check_mem(raw: u32, gpr: [u64; 32]) { let helpers = crate::MemHelpers::resolve(); let mut probe = dynasmrt::x64::Assembler::new().unwrap(); assert!( - emit::try_emit_native(&mut probe, &off, &helpers, &instr), + emit::try_emit_native(&mut probe, &off, &helpers, &instr) != emit::Emit::Fallback, "mem opcode not natively emitted raw={raw:#010x} ({:?})", instr.opcode );