[iterate-4C] JIT Phase 1: native integer ALU emitters + differential tests
crates/xenia-jit/src/emit.rs: native x64 for the hot non-recording integer ALU ops — addi, addis, ori, oris, xori, xoris, or(x), and(x), xor(x), add(x), subf(x), neg(x). Each mirrors its interpreter arm exactly, then does pc+=4 and the cycle/timebase bumps. Per-instance guards fall back to the interpreter for forms the emitter can't yet reproduce faithfully: recording (`.`/Rc) forms, OE overflow forms, and the db16cyc spin hint (or r31,r31,r31 -> Yield). RA=0 literal-zero rule handled at emit time. Differential harness (tests.rs): each opcode run through interpret_one vs a JIT-compiled 1-instr block over 2000 random register seeds (+ edge values 0/0xFFFFFFFF/0x80000000/INT64 boundaries); asserts full GPR/PC/XER/cycle/ timebase/StepResult equality. Plus a guard test that recording/OE/db16cyc forms are NOT natively emitted. Gate: golden n200m BYTE-IDENTICAL with XENIA_JIT=1; `cargo test -p xenia-jit` green. Throughput -n 200M --gpu-inline: 4.3s interp, 6.2s JIT (down from 7.1s all-fallback skeleton). Still fallback-bound on loads/stores/branches -> Phase 2 is the crossover. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
237
crates/xenia-jit/src/emit.rs
Normal file
237
crates/xenia-jit/src/emit.rs
Normal file
@@ -0,0 +1,237 @@
|
||||
//! Native x64 emitters for individual PPC opcodes (context-threading).
|
||||
//!
|
||||
//! Each emitter mirrors exactly one interpreter arm
|
||||
//! (`xenia-cpu/src/interpreter.rs`) and leaves guest state in `PpcContext`
|
||||
//! identical to what the interpreter would produce, then advances `pc` by 4 and
|
||||
//! bumps the cycle/timebase counters. Emitters cover only the cases they can
|
||||
//! reproduce faithfully; any variant they can't (recording `.` forms, `OE`
|
||||
//! overflow forms, the `db16cyc` spin hint) returns `false` from
|
||||
//! [`try_emit_native`] so the block falls back to the interpreter for that
|
||||
//! instruction — keeping every result byte-identical.
|
||||
//!
|
||||
//! Register convention (set up by the block prologue): `r15` = `*mut
|
||||
//! PpcContext`, `rbx` = `*mut JitEnv`. Emitters use `rax`/`rcx` as scratch
|
||||
//! (caller-saved; no cross-instruction state — all guest state lives in
|
||||
//! `PpcContext`). PPC operand roles follow the ISA: arithmetic D-forms write
|
||||
//! `rD` (`instr.rd()`); logical forms write `rA` (`instr.ra()`) from `rS`
|
||||
//! (`instr.rs()`).
|
||||
|
||||
use dynasmrt::{DynasmApi, dynasm};
|
||||
use xenia_cpu::PpcOpcode;
|
||||
use xenia_cpu::context::PpcContext;
|
||||
use xenia_cpu::decoder::DecodedInstr;
|
||||
|
||||
use crate::JitEnv;
|
||||
|
||||
/// Byte offsets into `JitEnv`/`PpcContext`, resolved once per block compile.
|
||||
pub struct Offsets {
|
||||
pub env_ctx: i32,
|
||||
gpr: i32,
|
||||
pub pc: i32,
|
||||
pub cycle: i32,
|
||||
pub timebase: i32,
|
||||
}
|
||||
|
||||
impl Offsets {
|
||||
pub fn resolve() -> Self {
|
||||
Offsets {
|
||||
env_ctx: core::mem::offset_of!(JitEnv, ctx) as i32,
|
||||
gpr: core::mem::offset_of!(PpcContext, gpr) as i32,
|
||||
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,
|
||||
}
|
||||
}
|
||||
/// Byte offset of guest register `i` (`gpr[i]`, 8 bytes each).
|
||||
#[inline]
|
||||
fn gpr(&self, i: usize) -> i32 {
|
||||
self.gpr + (i as i32) * 8
|
||||
}
|
||||
}
|
||||
|
||||
type Asm = dynasmrt::x64::Assembler;
|
||||
|
||||
/// Emit `pc += 4; cycle_count += 1; timebase += 1` — the postlude every native
|
||||
/// (non-branch) opcode shares, matching `interpreter.rs` (`execute` does
|
||||
/// `ctx.pc += 4`, `step_block` bumps the counters after each instruction).
|
||||
#[inline]
|
||||
fn advance_and_count(ops: &mut Asm, off: &Offsets) {
|
||||
dynasm!(ops
|
||||
; .arch x64
|
||||
; add DWORD [r15 + off.pc], 4
|
||||
; inc QWORD [r15 + off.cycle]
|
||||
; inc QWORD [r15 + off.timebase]
|
||||
);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn try_emit_native(ops: &mut Asm, off: &Offsets, instr: &DecodedInstr) -> bool {
|
||||
let ra = instr.ra();
|
||||
let rb = instr.rb();
|
||||
let rd = instr.rd(); // == rs()
|
||||
match instr.opcode {
|
||||
// rD = (rA==0 ? 0 : gpr[rA]) + EXTS(SIMM) [64-bit; never records]
|
||||
PpcOpcode::addi => {
|
||||
let simm = instr.simm16() as i32; // fits i32 (16-bit sign-extended)
|
||||
load_ra_or_zero(ops, off, ra);
|
||||
if simm != 0 {
|
||||
dynasm!(ops ; .arch x64 ; add rax, simm);
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// rD = (rA==0 ? 0 : gpr[rA]) + (EXTS(SIMM) << 16) [64-bit]
|
||||
PpcOpcode::addis => {
|
||||
// (i16 sign-extended) << 16 occupies bits 16..31 with the sign in
|
||||
// bit 31 — exactly an i32, so it fits an `add r64, imm32`.
|
||||
let simm = (instr.simm16() as i32) << 16;
|
||||
load_ra_or_zero(ops, off, ra);
|
||||
if simm != 0 {
|
||||
dynasm!(ops ; .arch x64 ; add rax, simm);
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(rd)], rax);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// gpr[rA] = gpr[rS] | ZEXT(UIMM) [never records]
|
||||
PpcOpcode::ori => {
|
||||
let uimm = instr.uimm16() as i32; // 0..65535 -> positive i32 (zext == sext)
|
||||
dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)]);
|
||||
if uimm != 0 {
|
||||
dynasm!(ops ; .arch x64 ; or rax, uimm);
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// gpr[rA] = gpr[rS] | (ZEXT(UIMM) << 16)
|
||||
PpcOpcode::oris => {
|
||||
let imm = (instr.uimm16() as u32) << 16;
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rd)]
|
||||
; mov ecx, imm as i32 // zero-extends into rcx
|
||||
; or rax, rcx
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// gpr[rA] = gpr[rS] ^ ZEXT(UIMM)
|
||||
PpcOpcode::xori => {
|
||||
let uimm = instr.uimm16() as i32;
|
||||
dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(rd)]);
|
||||
if uimm != 0 {
|
||||
dynasm!(ops ; .arch x64 ; xor rax, uimm);
|
||||
}
|
||||
dynasm!(ops ; .arch x64 ; mov [r15 + off.gpr(ra)], rax);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// gpr[rA] = gpr[rS] ^ (ZEXT(UIMM) << 16)
|
||||
PpcOpcode::xoris => {
|
||||
let imm = (instr.uimm16() as u32) << 16;
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rd)]
|
||||
; mov ecx, imm as i32
|
||||
; xor rax, rcx
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// 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;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rd)]
|
||||
; or rax, [r15 + off.gpr(rb)]
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// gpr[rA] = gpr[rS] & gpr[rB] [64-bit]
|
||||
PpcOpcode::andx => {
|
||||
if instr.rc_bit() {
|
||||
return false;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rd)]
|
||||
; and rax, [r15 + off.gpr(rb)]
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// gpr[rA] = gpr[rS] ^ gpr[rB] [64-bit]
|
||||
PpcOpcode::xorx => {
|
||||
if instr.rc_bit() {
|
||||
return false;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rd)]
|
||||
; xor rax, [r15 + off.gpr(rb)]
|
||||
; mov [r15 + off.gpr(ra)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// rD = gpr[rA] + gpr[rB] [64-bit]; skip OE/recording forms.
|
||||
PpcOpcode::addx => {
|
||||
if instr.oe() || instr.rc_bit() {
|
||||
return false;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(ra)]
|
||||
; add rax, [r15 + off.gpr(rb)]
|
||||
; mov [r15 + off.gpr(rd)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// rD = gpr[rB] - gpr[rA] [64-bit]; skip OE/recording forms.
|
||||
PpcOpcode::subfx => {
|
||||
if instr.oe() || instr.rc_bit() {
|
||||
return false;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(rb)]
|
||||
; sub rax, [r15 + off.gpr(ra)]
|
||||
; mov [r15 + off.gpr(rd)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
// rD = 0 - gpr[rA] [64-bit]; skip OE/recording forms.
|
||||
PpcOpcode::negx => {
|
||||
if instr.oe() || instr.rc_bit() {
|
||||
return false;
|
||||
}
|
||||
dynasm!(ops ; .arch x64
|
||||
; mov rax, [r15 + off.gpr(ra)]
|
||||
; neg rax
|
||||
; mov [r15 + off.gpr(rd)], rax
|
||||
);
|
||||
advance_and_count(ops, off);
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]
|
||||
fn load_ra_or_zero(ops: &mut Asm, off: &Offsets, ra: usize) {
|
||||
if ra == 0 {
|
||||
dynasm!(ops ; .arch x64 ; xor eax, eax);
|
||||
} else {
|
||||
dynasm!(ops ; .arch x64 ; mov rax, [r15 + off.gpr(ra)]);
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,10 @@ use xenia_cpu::decoder::DecodedInstr;
|
||||
use xenia_cpu::interpreter::{StepResult, interpret_one};
|
||||
use xenia_memory::MemoryAccess;
|
||||
|
||||
mod emit;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
/// Runtime environment handed to a compiled block. The emitted prologue reads
|
||||
/// only `ctx` (via `offset_of!`); `mem` and `last_result` are touched solely by
|
||||
/// the Rust helpers. `mem` is a real fat raw pointer, so no transmute is needed
|
||||
@@ -124,10 +128,7 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock {
|
||||
let instrs: Box<[DecodedInstr]> = block.instrs.clone().into_boxed_slice();
|
||||
|
||||
// Field offsets resolved at compile time — robust to struct layout.
|
||||
let off_ctx = core::mem::offset_of!(JitEnv, ctx) as i32;
|
||||
let off_cycle = core::mem::offset_of!(PpcContext, cycle_count) as i32;
|
||||
let off_timebase = core::mem::offset_of!(PpcContext, timebase) as i32;
|
||||
let off_pc = core::mem::offset_of!(PpcContext, pc) as i32;
|
||||
let off = emit::Offsets::resolve();
|
||||
let helper = jit_interpret_one as usize as i64;
|
||||
|
||||
let mut ops = dynasmrt::x64::Assembler::new().expect("dynasm assembler");
|
||||
@@ -144,10 +145,17 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock {
|
||||
; push r15
|
||||
; sub rsp, 8
|
||||
; mov rbx, rdi
|
||||
; mov r15, [rbx + off_ctx]
|
||||
; mov r15, [rbx + off.env_ctx]
|
||||
);
|
||||
|
||||
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, instr) {
|
||||
continue;
|
||||
}
|
||||
// Interpreter fallback for un-ported opcodes.
|
||||
let instr_ptr = instr as *const DecodedInstr as usize as i64;
|
||||
let expected_next = instr.addr.wrapping_add(4) as i32;
|
||||
dynasm!(ops
|
||||
@@ -158,13 +166,13 @@ fn compile_block(block: &DecodedBlock) -> CompiledBlock {
|
||||
; mov rax, QWORD helper
|
||||
; call rax
|
||||
// determinism postlude: cycle_count += 1; timebase += 1
|
||||
; inc QWORD [r15 + off_cycle]
|
||||
; inc QWORD [r15 + off_timebase]
|
||||
; inc QWORD [r15 + off.cycle]
|
||||
; inc QWORD [r15 + off.timebase]
|
||||
// non-Continue result -> exit returning the discriminant in eax
|
||||
; test eax, eax
|
||||
; jnz =>l_exit
|
||||
// taken-branch (pc discontinuity) -> stop the block, return Continue
|
||||
; cmp DWORD [r15 + off_pc], expected_next
|
||||
; cmp DWORD [r15 + off.pc], expected_next
|
||||
; jne =>l_cont
|
||||
);
|
||||
}
|
||||
|
||||
223
crates/xenia-jit/src/tests.rs
Normal file
223
crates/xenia-jit/src/tests.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
//! Differential opcode tests: for each natively-emitted opcode, run a random
|
||||
//! register state through the interpreter (`interpret_one`, then the same
|
||||
//! per-instruction counter bump `step_block` does) and through a JIT-compiled
|
||||
//! one-instruction block, and assert the resulting `PpcContext` (GPRs, PC,
|
||||
//! XER, CR, cycle/timebase) and `StepResult` are identical. This makes the
|
||||
//! hand-written x64 as trustworthy as the interpreter.
|
||||
|
||||
use super::{CompiledBlock, compile_block, run_jit_block};
|
||||
use crate::emit;
|
||||
use xenia_cpu::block_cache::DecodedBlock;
|
||||
use xenia_cpu::context::PpcContext;
|
||||
use xenia_cpu::decode;
|
||||
use xenia_cpu::decoder::DecodedInstr;
|
||||
use xenia_cpu::interpreter::interpret_one;
|
||||
use xenia_memory::MemoryAccess;
|
||||
|
||||
/// Memory that must never be touched — the ALU opcodes tested here don't
|
||||
/// access guest memory, so any call is a bug in the emitter.
|
||||
struct NoMem;
|
||||
impl MemoryAccess for NoMem {
|
||||
fn read_u8(&self, _: u32) -> u8 {
|
||||
unreachable!("ALU emitter touched memory (read_u8)")
|
||||
}
|
||||
fn read_u16(&self, _: u32) -> u16 {
|
||||
unreachable!("ALU emitter touched memory (read_u16)")
|
||||
}
|
||||
fn read_u32(&self, _: u32) -> u32 {
|
||||
unreachable!("ALU emitter touched memory (read_u32)")
|
||||
}
|
||||
fn read_u64(&self, _: u32) -> u64 {
|
||||
unreachable!("ALU emitter touched memory (read_u64)")
|
||||
}
|
||||
fn write_u8(&self, _: u32, _: u8) {
|
||||
unreachable!("ALU emitter touched memory (write_u8)")
|
||||
}
|
||||
fn write_u16(&self, _: u32, _: u16) {
|
||||
unreachable!("ALU emitter touched memory (write_u16)")
|
||||
}
|
||||
fn write_u32(&self, _: u32, _: u32) {
|
||||
unreachable!("ALU emitter touched memory (write_u32)")
|
||||
}
|
||||
fn write_u64(&self, _: u32, _: u64) {
|
||||
unreachable!("ALU emitter touched memory (write_u64)")
|
||||
}
|
||||
fn translate(&self, _: u32) -> Option<*const u8> {
|
||||
None
|
||||
}
|
||||
fn translate_mut(&self, _: u32) -> Option<*mut u8> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// ---- instruction encoders (PPC big-endian field layout; `raw` is the u32) ----
|
||||
|
||||
/// D-form: `op | b6_10<<21 | b11_15<<16 | imm16`.
|
||||
fn enc_d(op: u32, b6_10: u32, b11_15: u32, imm16: u16) -> u32 {
|
||||
(op << 26) | (b6_10 << 21) | (b11_15 << 16) | (imm16 as u32)
|
||||
}
|
||||
/// X-form (logical register): `op | b6_10<<21 | b11_15<<16 | b16_20<<11 | xo<<1 | rc`.
|
||||
fn enc_x(op: u32, b6_10: u32, b11_15: u32, b16_20: u32, xo: u32, rc: u32) -> u32 {
|
||||
(op << 26) | (b6_10 << 21) | (b11_15 << 16) | (b16_20 << 11) | (xo << 1) | rc
|
||||
}
|
||||
/// XO-form (arithmetic register): X-form plus the OE bit at position 10.
|
||||
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
|
||||
}
|
||||
|
||||
/// Deterministic PRNG (splitmix64-ish) so failures are reproducible.
|
||||
fn rng(state: &mut u64) -> u64 {
|
||||
*state = state.wrapping_add(0x9E37_79B9_7F4A_7C15);
|
||||
let mut z = *state;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
|
||||
fn ctx_from_gpr(gpr: [u64; 32], pc: u32) -> PpcContext {
|
||||
let mut c = PpcContext::new();
|
||||
c.gpr = gpr;
|
||||
c.pc = pc;
|
||||
c
|
||||
}
|
||||
|
||||
/// Assert the JIT emits `raw` natively and reproduces the interpreter exactly
|
||||
/// for the given register seed.
|
||||
fn check(raw: u32, gpr: [u64; 32]) {
|
||||
let pc = 0x8200_1000u32;
|
||||
let instr: DecodedInstr = decode(raw, pc);
|
||||
let off = emit::Offsets::resolve();
|
||||
|
||||
// Guard: the opcode must actually be natively emitted (else this test is
|
||||
// vacuous — a fallback would trivially match).
|
||||
let mut probe = dynasmrt::x64::Assembler::new().unwrap();
|
||||
assert!(
|
||||
emit::try_emit_native(&mut probe, &off, &instr),
|
||||
"opcode not natively emitted for raw={raw:#010x} ({:?})",
|
||||
instr.opcode
|
||||
);
|
||||
|
||||
// Interpreter reference: interpret_one, then the counter bump step_block does.
|
||||
let mem = NoMem;
|
||||
let mut a = ctx_from_gpr(gpr, pc);
|
||||
let ra = interpret_one(&mut a, &mem, &instr);
|
||||
a.cycle_count += 1;
|
||||
a.timebase += 1;
|
||||
|
||||
// JIT.
|
||||
let mut b = ctx_from_gpr(gpr, pc);
|
||||
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}");
|
||||
assert_eq!(a.timebase, b.timebase, "timebase mismatch raw={raw:#010x}");
|
||||
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}");
|
||||
assert_eq!(ra, rb, "StepResult mismatch raw={raw:#010x}");
|
||||
}
|
||||
|
||||
/// Random register seeds, including hard boundary values in a few slots.
|
||||
fn fuzz_gpr(seed: &mut u64) -> [u64; 32] {
|
||||
let mut g = [0u64; 32];
|
||||
for slot in g.iter_mut() {
|
||||
*slot = rng(seed);
|
||||
}
|
||||
// Sprinkle in edge values.
|
||||
g[0] = 0;
|
||||
g[3] = 0xFFFF_FFFF;
|
||||
g[4] = 0x8000_0000;
|
||||
g[5] = 0xFFFF_FFFF_FFFF_FFFF;
|
||||
g[6] = 0x0000_0000_8000_0000;
|
||||
g
|
||||
}
|
||||
|
||||
const ITERS: usize = 2000;
|
||||
|
||||
#[test]
|
||||
fn addi_matches() {
|
||||
let mut s = 0x1234u64;
|
||||
for _ in 0..ITERS {
|
||||
let g = fuzz_gpr(&mut s);
|
||||
let rd = (rng(&mut s) % 32) as u32;
|
||||
let ra = (rng(&mut s) % 32) as u32;
|
||||
let imm = rng(&mut s) as u16;
|
||||
check(enc_d(14, rd, ra, imm), g); // addi
|
||||
check(enc_d(15, rd, ra, imm), g); // addis
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_imm_matches() {
|
||||
let mut s = 0x5678u64;
|
||||
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 uimm = rng(&mut s) as u16;
|
||||
check(enc_d(24, rs, ra, uimm), g); // ori
|
||||
check(enc_d(25, rs, ra, uimm), g); // oris
|
||||
check(enc_d(26, rs, ra, uimm), g); // xori
|
||||
check(enc_d(27, rs, ra, uimm), g); // xoris
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logical_reg_matches() {
|
||||
let mut s = 0x9abcu64;
|
||||
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;
|
||||
check(enc_x(31, rs, ra, rb, 444, 0), g); // or
|
||||
check(enc_x(31, rs, ra, rb, 28, 0), g); // and
|
||||
check(enc_x(31, rs, ra, rb, 316, 0), g); // xor
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arith_reg_matches() {
|
||||
let mut s = 0xdef0u64;
|
||||
for _ in 0..ITERS {
|
||||
let g = fuzz_gpr(&mut s);
|
||||
let rd = (rng(&mut s) % 32) as u32;
|
||||
let ra = (rng(&mut s) % 32) as u32;
|
||||
let rb = (rng(&mut s) % 32) as u32;
|
||||
check(enc_xo(31, rd, ra, rb, 0, 266, 0), g); // add
|
||||
check(enc_xo(31, rd, ra, rb, 0, 40, 0), g); // subf
|
||||
check(enc_xo(31, rd, ra, rb, 0, 104, 0), g); // neg (rb ignored)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
fn recording_and_hint_forms_fall_back() {
|
||||
let off = emit::Offsets::resolve();
|
||||
let mut probe = dynasmrt::x64::Assembler::new().unwrap();
|
||||
let cases = [
|
||||
enc_xo(31, 3, 4, 5, 0, 266, 1), // add.
|
||||
enc_xo(31, 3, 4, 5, 1, 266, 0), // addo
|
||||
enc_x(31, 3, 4, 5, 444, 1), // or.
|
||||
0x7FFF_FB78u32, // db16cyc (or r31,r31,r31)
|
||||
];
|
||||
for raw in cases {
|
||||
let instr = decode(raw, 0x8200_1000);
|
||||
assert!(
|
||||
!emit::try_emit_native(&mut probe, &off, &instr),
|
||||
"raw={raw:#010x} ({:?}) should fall back, not native",
|
||||
instr.opcode
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user