From cc7ff58cb0c04157715ac161d6b770d9a6a57cef Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Mon, 6 Jul 2026 20:45:31 +0200 Subject: [PATCH] [iterate-4A] jit: Cranelift block-JIT scaffolding + addi/addis PoC (pipeline proven) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Cargo.lock | 241 ++++++++++++++++++++++++++++++++++ crates/xenia-cpu/Cargo.toml | 4 + crates/xenia-cpu/src/jit.rs | 255 ++++++++++++++++++++++++++++++++++++ crates/xenia-cpu/src/lib.rs | 1 + 4 files changed, 501 insertions(+) create mode 100644 crates/xenia-cpu/src/jit.rs diff --git a/Cargo.lock b/Cargo.lock index a82b968..8f28cd9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/xenia-cpu/Cargo.toml b/crates/xenia-cpu/Cargo.toml index 63b9166..30fce2c 100644 --- a/crates/xenia-cpu/Cargo.toml +++ b/crates/xenia-cpu/Cargo.toml @@ -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 } diff --git a/crates/xenia-cpu/src/jit.rs b/crates/xenia-cpu/src/jit.rs new file mode 100644 index 0000000..81d78b2 --- /dev/null +++ b/crates/xenia-cpu/src/jit.rs @@ -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 { + // 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 { + 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"); + } +} diff --git a/crates/xenia-cpu/src/lib.rs b/crates/xenia-cpu/src/lib.rs index 03bf341..cd8214b 100644 --- a/crates/xenia-cpu/src/lib.rs +++ b/crates/xenia-cpu/src/lib.rs @@ -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;