Files
xenia-rs/crates/xenia-cpu/src/jit.rs
MechaCat02 cc7ff58cb0 [iterate-4A] jit: Cranelift block-JIT scaffolding + addi/addis PoC (pipeline proven)
Stage-2 foundation. Adds cranelift-jit/frontend/module/codegen 0.128.4 (the
1.90-compatible line; 0.133 needs Rust 1.94) and a new crates/xenia-cpu/src/jit.rs.

Jit owns a JITModule (and thus all compiled code memory). compile(&DecodedBlock)
lowers a block to native code ONLY if every opcode is covered() — otherwise
returns None and the caller interprets the whole block (coverage grows
opcode-by-opcode). ABI: extern "C" fn(*mut PpcContext, *const MemEnv) -> u32
(StepResult discriminant); guest registers are loaded/stored directly from the
#[repr(C)] PpcContext at offset_of! offsets. A covered block is straight-line and
always runs to completion, so pc advances to end_pc and cycle_count/timebase bump
by the instruction count once — matching the interpreter's per-instruction bump.

Covered set so far: addi, addis. Unit test jit_matches_interpreter_addi_block
compiles a 32x addi block and asserts the JIT's r3/pc/cycle_count/timebase match
the interpreter exactly — proves module setup, offset_of register access, IR
emission, the extern "C" calling convention, and cycle/pc accounting end-to-end.

Not yet wired into run_superblock (needs a compiled-block cache + routing); every
future opcode will be validated against the interpreter via the M0 XENIA_JIT_DIFF
harness before it counts as covered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:45:31 +02:00

256 lines
9.8 KiB
Rust

//! Cranelift machine-code block JIT (Stage-2).
//!
//! Compiles a fully-covered [`DecodedBlock`] to a native function and runs it in
//! place of the interpreter. A block is compiled **only if every opcode in it is
//! covered** (see [`covered`]); otherwise the caller interprets the whole block.
//! Coverage grows opcode-by-opcode, each one validated against the interpreter by
//! the recompiler's differential harness (`XENIA_JIT_DIFF`).
//!
//! ## ABI
//! Compiled block: `extern "C" fn(ctx: *mut PpcContext, mem: *const MemEnv) -> u32`.
//! The return is a [`StepResult`] discriminant ([`RET_CONTINUE`] etc.). Guest
//! registers are loaded/stored directly from the `#[repr(C)]` `PpcContext` at
//! `offset_of!` offsets; memory goes through trampolines that call the live
//! `MemoryAccess` (preserving MMIO / mem-watch / page-version) — added as
//! load/store opcodes are covered.
//!
//! ## Determinism
//! A covered block is straight-line and always runs to completion (covered ops
//! never fault or yield), so `cycle_count`/`timebase` advance by exactly the
//! block's instruction count — matching the interpreter's per-instruction bump.
use core::mem::offset_of;
use cranelift_codegen::ir::{types, AbiParam, InstBuilder, MemFlags, Value};
use cranelift_frontend::{FunctionBuilder, FunctionBuilderContext};
use cranelift_jit::{JITBuilder, JITModule};
use cranelift_module::{default_libcall_names, Module};
use crate::block_cache::DecodedBlock;
use crate::context::PpcContext;
use crate::decoder::DecodedInstr;
use crate::opcode::PpcOpcode;
use xenia_memory::MemoryAccess;
/// `StepResult` discriminants returned by compiled blocks. Kept in sync with
/// [`crate::interpreter::StepResult`] (only `Continue` is produced by covered
/// blocks today; the rest exist for when branch/sc/trap ops are covered).
pub const RET_CONTINUE: u32 = 0;
/// Thin environment handed to memory trampolines: wraps the live `MemoryAccess`.
#[repr(C)]
pub struct MemEnv<'a> {
pub mem: &'a dyn MemoryAccess,
}
/// A compiled block: a native entry point valid for as long as the owning
/// [`Jit`]'s module lives (the module owns the executable memory).
pub type CompiledFn = extern "C" fn(*mut PpcContext, *const MemEnv) -> u32;
/// Is this opcode currently lowerable to native code? Must mirror [`emit_op`].
/// A block is JIT-compiled only if `covered` is true for all its instructions.
pub fn covered(instr: &DecodedInstr) -> bool {
matches!(instr.opcode, PpcOpcode::addi | PpcOpcode::addis)
}
/// True if the whole block can be compiled (all opcodes covered).
pub fn block_covered(block: &DecodedBlock) -> bool {
block.instrs.iter().all(covered)
}
/// The Cranelift JIT. Owns the module (and thus all compiled code memory), so it
/// must outlive every [`CompiledFn`] it produces.
pub struct Jit {
module: JITModule,
ctx: cranelift_codegen::Context,
fbctx: FunctionBuilderContext,
}
impl Jit {
pub fn new() -> Result<Self, String> {
// Native host ISA + default flags. `JITBuilder::new` resolves the host
// target via cranelift-native (a transitive dep of cranelift-jit).
let builder = JITBuilder::new(default_libcall_names()).map_err(|e| e.to_string())?;
let module = JITModule::new(builder);
let ctx = module.make_context();
Ok(Self { module, ctx, fbctx: FunctionBuilderContext::new() })
}
/// Compile `block` to native code, or return `None` if any opcode is
/// uncovered (caller interprets the block).
pub fn compile(&mut self, block: &DecodedBlock) -> Option<CompiledFn> {
if !block_covered(block) {
return None;
}
let ptr = self.module.target_config().pointer_type();
self.module.clear_context(&mut self.ctx);
self.ctx.func.signature.params.push(AbiParam::new(ptr)); // ctx
self.ctx.func.signature.params.push(AbiParam::new(ptr)); // mem env
self.ctx.func.signature.returns.push(AbiParam::new(types::I32));
{
let mut b = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbctx);
let entry = b.create_block();
b.append_block_params_for_function_params(entry);
b.switch_to_block(entry);
b.seal_block(entry);
let ctxp = b.block_params(entry)[0];
let _memenv = b.block_params(entry)[1];
for instr in &block.instrs {
emit_op(&mut b, ctxp, instr);
}
// Straight-line covered block: PC advances linearly to end_pc.
let end_pc = b.ins().iconst(types::I32, block.end_pc as i64);
b.ins().store(MemFlags::trusted(), end_pc, ctxp, off(offset_of!(PpcContext, pc)));
// cycle_count += N ; timebase += N (one per executed instruction).
let n = block.instrs.len() as i64;
bump_u64(&mut b, ctxp, offset_of!(PpcContext, cycle_count), n);
bump_u64(&mut b, ctxp, offset_of!(PpcContext, timebase), n);
let ret = b.ins().iconst(types::I32, RET_CONTINUE as i64);
b.ins().return_(&[ret]);
b.finalize();
}
let id = self
.module
.declare_anonymous_function(&self.ctx.func.signature)
.ok()?;
self.module.define_function(id, &mut self.ctx).ok()?;
self.module.clear_context(&mut self.ctx);
self.module.finalize_definitions().ok()?;
let code = self.module.get_finalized_function(id);
// Safety: the module owns this code for its lifetime; the signature
// matches CompiledFn (SystemV/extern "C" on the host).
Some(unsafe { std::mem::transmute::<*const u8, CompiledFn>(code) })
}
}
#[inline]
fn off(o: usize) -> i32 {
o as i32
}
#[inline]
fn gpr_off(r: usize) -> i32 {
(offset_of!(PpcContext, gpr) + r * 8) as i32
}
#[inline]
fn load_gpr(b: &mut FunctionBuilder, ctxp: Value, r: usize) -> Value {
b.ins().load(types::I64, MemFlags::trusted(), ctxp, gpr_off(r))
}
#[inline]
fn store_gpr(b: &mut FunctionBuilder, ctxp: Value, r: usize, v: Value) {
b.ins().store(MemFlags::trusted(), v, ctxp, gpr_off(r));
}
#[inline]
fn bump_u64(b: &mut FunctionBuilder, ctxp: Value, offset: usize, by: i64) {
let cur = b.ins().load(types::I64, MemFlags::trusted(), ctxp, off(offset));
let next = b.ins().iadd_imm(cur, by);
b.ins().store(MemFlags::trusted(), next, ctxp, off(offset));
}
/// Emit IR for one covered instruction. Must mirror [`covered`] and the exact
/// semantics of the interpreter's `execute()` (validated by the diff harness).
fn emit_op(b: &mut FunctionBuilder, ctxp: Value, instr: &DecodedInstr) {
match instr.opcode {
// rd = (ra==0 ? 0 : gpr[ra]) + EXTS(simm) [64-bit]
PpcOpcode::addi => {
let ra = instr.ra();
let rav = if ra == 0 {
b.ins().iconst(types::I64, 0)
} else {
load_gpr(b, ctxp, ra)
};
let res = b.ins().iadd_imm(rav, instr.simm16() as i64);
store_gpr(b, ctxp, instr.rd(), res);
}
// rd = (ra==0 ? 0 : gpr[ra]) + (EXTS(simm) << 16) [64-bit]
PpcOpcode::addis => {
let ra = instr.ra();
let rav = if ra == 0 {
b.ins().iconst(types::I64, 0)
} else {
load_gpr(b, ctxp, ra)
};
let res = b.ins().iadd_imm(rav, (instr.simm16() as i64) << 16);
store_gpr(b, ctxp, instr.rd(), res);
}
// covered() gates this — unreachable for uncovered opcodes.
_ => unreachable!("emit_op called on uncovered opcode {:?}", instr.opcode),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block_cache::BlockCache;
/// Mock memory that returns the same instruction word at every address.
struct WordMem(u32);
impl MemoryAccess for WordMem {
fn read_u8(&self, _a: u32) -> u8 {
0
}
fn read_u16(&self, _a: u32) -> u16 {
0
}
fn read_u32(&self, _a: u32) -> u32 {
self.0
}
fn read_u64(&self, _a: u32) -> u64 {
((self.0 as u64) << 32) | self.0 as u64
}
fn write_u8(&self, _a: u32, _v: u8) {}
fn write_u16(&self, _a: u32, _v: u16) {}
fn write_u32(&self, _a: u32, _v: u32) {}
fn write_u64(&self, _a: u32, _v: u64) {}
fn translate(&self, _a: u32) -> Option<*const u8> {
None
}
fn translate_mut(&self, _a: u32) -> Option<*mut u8> {
None
}
}
// addi rD, rA, SIMM = (14<<26)|(rD<<21)|(rA<<16)|(SIMM & 0xFFFF)
fn addi(rd: u32, ra: u32, simm: u16) -> u32 {
(14 << 26) | (rd << 21) | (ra << 16) | simm as u32
}
#[test]
fn jit_matches_interpreter_addi_block() {
// A block of `addi r3, r3, 1` — 32 instrs (hits MAX_BLOCK_INSTRS, no
// terminator), fully covered.
let mem = WordMem(addi(3, 3, 1));
let mut bc = BlockCache::new();
let block = bc.lookup_or_build(0x1000, &mem).clone();
assert!(block_covered(&block), "addi block should be covered");
// Interpreter reference.
let mut ref_ctx = PpcContext::new();
ref_ctx.pc = 0x1000;
let _ = crate::interpreter::step_block(&mut ref_ctx, &mem, &block);
// JIT candidate.
let mut jit = Jit::new().expect("jit init");
let f = jit.compile(&block).expect("compile");
let mut jit_ctx = PpcContext::new();
jit_ctx.pc = 0x1000;
let env = MemEnv { mem: &mem };
let r = f(&mut jit_ctx as *mut _, &env as *const _);
assert_eq!(r, RET_CONTINUE);
assert_eq!(jit_ctx.gpr[3], ref_ctx.gpr[3], "r3 mismatch");
assert_eq!(jit_ctx.pc, ref_ctx.pc, "pc mismatch");
assert_eq!(jit_ctx.cycle_count, ref_ctx.cycle_count, "cycle mismatch");
assert_eq!(jit_ctx.timebase, ref_ctx.timebase, "timebase mismatch");
}
}