[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>
This commit is contained in:
MechaCat02
2026-07-06 20:45:31 +02:00
parent 07cd272412
commit cc7ff58cb0
4 changed files with 501 additions and 0 deletions

241
Cargo.lock generated
View File

@@ -87,6 +87,12 @@ dependencies = [
"equator",
]
[[package]]
name = "allocator-api2"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android-activity"
version = "0.6.1"
@@ -521,6 +527,9 @@ name = "bumpalo"
version = "3.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
dependencies = [
"allocator-api2",
]
[[package]]
name = "bytecheck"
@@ -874,6 +883,167 @@ dependencies = [
"libc",
]
[[package]]
name = "cranelift-assembler-x64"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50a04121a197fde2fe896f8e7cac9812fc41ed6ee9c63e1906090f9f497845f6"
dependencies = [
"cranelift-assembler-x64-meta",
]
[[package]]
name = "cranelift-assembler-x64-meta"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a09e699a94f477303820fb2167024f091543d6240783a2d3b01a3f21c42bc744"
dependencies = [
"cranelift-srcgen",
]
[[package]]
name = "cranelift-bforest"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f07732c662a9755529e332d86f8c5842171f6e98ba4d5976a178043dad838654"
dependencies = [
"cranelift-entity",
]
[[package]]
name = "cranelift-bitset"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18391da761cf362a06def7a7cf11474d79e55801dd34c2e9ba105b33dc0aef88"
[[package]]
name = "cranelift-codegen"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b3a09b3042c69810d255aef59ddc3b3e4c0644d1d90ecfd6e3837798cc88a3c"
dependencies = [
"bumpalo",
"cranelift-assembler-x64",
"cranelift-bforest",
"cranelift-bitset",
"cranelift-codegen-meta",
"cranelift-codegen-shared",
"cranelift-control",
"cranelift-entity",
"cranelift-isle",
"gimli",
"hashbrown 0.15.5",
"log",
"regalloc2",
"rustc-hash 2.1.2",
"serde",
"smallvec",
"target-lexicon",
"wasmtime-internal-math",
]
[[package]]
name = "cranelift-codegen-meta"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75817926ec812241889208d1b190cadb7fedded4592a4bb01b8524babb9e4849"
dependencies = [
"cranelift-assembler-x64-meta",
"cranelift-codegen-shared",
"cranelift-srcgen",
"heck",
]
[[package]]
name = "cranelift-codegen-shared"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "859158f87a59476476eda3884d883c32e08a143cf3d315095533b362a3250a63"
[[package]]
name = "cranelift-control"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b65a9aec442d715cbf54d14548b8f395476c09cef7abe03e104a378291ab88"
dependencies = [
"arbitrary",
]
[[package]]
name = "cranelift-entity"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8334c99a7e86060c24028732efd23bac84585770dcb752329c69f135d64f2fc1"
dependencies = [
"cranelift-bitset",
]
[[package]]
name = "cranelift-frontend"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43ac6c095aa5b3e845d7ca3461e67e2b65249eb5401477a5ff9100369b745111"
dependencies = [
"cranelift-codegen",
"log",
"smallvec",
"target-lexicon",
]
[[package]]
name = "cranelift-isle"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d3d992870ed4f0f2e82e2175275cb3a123a46e9660c6558c46417b822c91fa"
[[package]]
name = "cranelift-jit"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "807781e9097feba24c31fc8d85a217c2fbfc6c8b72a87da8717c056dc8b24a87"
dependencies = [
"anyhow",
"cranelift-codegen",
"cranelift-control",
"cranelift-entity",
"cranelift-module",
"cranelift-native",
"libc",
"log",
"region",
"target-lexicon",
"wasmtime-internal-jit-icache-coherence",
"windows-sys 0.61.2",
]
[[package]]
name = "cranelift-module"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6a588624367124596cb07c324fa865a4894200aede7fe933e817816188ace52"
dependencies = [
"anyhow",
"cranelift-codegen",
"cranelift-control",
]
[[package]]
name = "cranelift-native"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee32e36beaf80f309edb535274cfe0349e1c5cf5799ba2d9f42e828285c6b52e"
dependencies = [
"cranelift-codegen",
"libc",
"target-lexicon",
]
[[package]]
name = "cranelift-srcgen"
version = "0.128.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "903adeaf4938e60209a97b53a2e4326cd2d356aab9764a1934630204bae381c9"
[[package]]
name = "crc32fast"
version = "1.5.0"
@@ -1359,6 +1529,11 @@ name = "gimli"
version = "0.32.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7"
dependencies = [
"fallible-iterator",
"indexmap",
"stable_deref_trait",
]
[[package]]
name = "gl_generator"
@@ -2127,6 +2302,15 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "mach2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44"
dependencies = [
"libc",
]
[[package]]
name = "malloc_buf"
version = "0.0.6"
@@ -3183,6 +3367,20 @@ dependencies = [
"bitflags 2.11.0",
]
[[package]]
name = "regalloc2"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08effbc1fa53aaebff69521a5c05640523fab037b34a4a2c109506bc938246fa"
dependencies = [
"allocator-api2",
"bumpalo",
"hashbrown 0.15.5",
"log",
"rustc-hash 2.1.2",
"smallvec",
]
[[package]]
name = "regex"
version = "1.12.3"
@@ -3212,6 +3410,18 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]]
name = "region"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b6ebd13bc009aef9cd476c1310d49ac354d36e240cf1bd753290f3dc7199a7"
dependencies = [
"bitflags 1.3.2",
"libc",
"mach2",
"windows-sys 0.52.0",
]
[[package]]
name = "rend"
version = "0.4.2"
@@ -3808,6 +4018,12 @@ dependencies = [
"xattr",
]
[[package]]
name = "target-lexicon"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -4378,6 +4594,27 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "wasmtime-internal-jit-icache-coherence"
version = "41.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b96df23179ae16d54fb3a420f84ffe4383ec9dd06fad3e5bc782f85f66e8e08"
dependencies = [
"anyhow",
"cfg-if",
"libc",
"windows-sys 0.61.2",
]
[[package]]
name = "wasmtime-internal-math"
version = "41.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86d1380926682b44c383e9a67f47e7a95e60c6d3fa8c072294dab2c7de6168a0"
dependencies = [
"libm",
]
[[package]]
name = "wayland-backend"
version = "0.3.15"
@@ -5071,6 +5308,10 @@ name = "xenia-cpu"
version = "0.1.0"
dependencies = [
"bitflags 2.11.0",
"cranelift-codegen",
"cranelift-frontend",
"cranelift-jit",
"cranelift-module",
"serde",
"serde_json",
"thiserror 2.0.18",

View File

@@ -10,6 +10,10 @@ xenia-memory = { workspace = true }
tracing = { workspace = true }
bitflags = { workspace = true }
thiserror = { workspace = true }
cranelift-jit = "0.128.4"
cranelift-frontend = "0.128.4"
cranelift-module = "0.128.4"
cranelift-codegen = "0.128.4"
[dev-dependencies]
serde = { workspace = true }

255
crates/xenia-cpu/src/jit.rs Normal file
View File

@@ -0,0 +1,255 @@
//! 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");
}
}

View File

@@ -5,6 +5,7 @@ pub mod dispatch_rec;
pub mod disasm;
pub mod fpscr;
pub mod interpreter;
pub mod jit;
pub mod opcode;
pub mod overflow;
pub mod phaser;