[iterate-4C] JIT Phase 0: dynasm skeleton (all-fallback, golden byte-identical)
New crate `xenia-jit` — the PPC->x64 block-JIT runtime substrate, behind
default-OFF XENIA_JIT. Phase 0 ports ZERO opcodes to native: every guest
instruction is emitted as `call jit_interpret_one` (the interpreter), so a
JIT-compiled block is byte-identical to step_block by construction. This
proves the ABI/counters/exit-semantics/mem-helpers/code-cache before any
opcode is hand-written.
- JitEnv{ctx, mem (fat raw ptr), last_result}; compiled block =
extern "C" fn(*mut JitEnv)->u32 (StepResult discriminant, 0=Continue).
Emitted code pins ctx in r15 + env in rbx (callee-saved across calls),
offsets via offset_of!.
- Determinism postlude: cycle_count/timebase +=1 after every retired
instruction; block stops at the same instruction as the interpreter
(non-Continue result, or taken-branch pc discontinuity).
- Per-slot JitCache mirrors BlockCache's (start_pc, page_version) gate;
each CompiledBlock OWNS a copy of its decoded instrs so baked instr
pointers can't dangle after a block-cache eviction.
- xenia-cpu: `pub fn interpret_one` (execute without the cycle bump — the
JIT owns counting).
- Seam: run_superblock main.rs:3154 dispatches to the JIT when enabled;
WorkerCtx gains an Option<JitCache> (Some only when XENIA_JIT set and
RET-CAPTURE debug env unset). observe_per_instruction gate unchanged, so
tooling runs never reach the JIT.
Gate: golden n200m BYTE-IDENTICAL both with and without XENIA_JIT=1.
Throughput -n 200M --gpu-inline: 4.5s interp vs 7.1s all-fallback skeleton
(the per-instruction call overhead Phase 1 removes for hot opcodes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
61
Cargo.lock
generated
61
Cargo.lock
generated
@@ -1056,6 +1056,33 @@ dependencies = [
|
||||
"strum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynasm"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f7d4c414c94bc830797115b8e5f434d58e7e80cb42ba88508c14bc6ea270625"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"byteorder",
|
||||
"lazy_static",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dynasmrt"
|
||||
version = "3.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "602f7458a3859195fb840e6e0cce5f4330dd9dfbfece0edaf31fe427af346f55"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"dynasm",
|
||||
"fnv",
|
||||
"memmap2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endian-type"
|
||||
version = "0.1.2"
|
||||
@@ -2888,6 +2915,28 @@ dependencies = [
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
@@ -5047,6 +5096,7 @@ dependencies = [
|
||||
"xenia-debugger",
|
||||
"xenia-gpu",
|
||||
"xenia-hid",
|
||||
"xenia-jit",
|
||||
"xenia-kernel",
|
||||
"xenia-memory",
|
||||
"xenia-types",
|
||||
@@ -5117,6 +5167,17 @@ dependencies = [
|
||||
"xenia-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-jit"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dynasm",
|
||||
"dynasmrt",
|
||||
"tracing",
|
||||
"xenia-cpu",
|
||||
"xenia-memory",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xenia-kernel"
|
||||
version = "0.1.0"
|
||||
|
||||
@@ -4,6 +4,7 @@ members = [
|
||||
"crates/xenia-types",
|
||||
"crates/xenia-memory",
|
||||
"crates/xenia-cpu",
|
||||
"crates/xenia-jit",
|
||||
"crates/xenia-xex",
|
||||
"crates/xenia-vfs",
|
||||
"crates/xenia-kernel",
|
||||
@@ -26,6 +27,7 @@ license = "BSD-3-Clause"
|
||||
xenia-types = { path = "crates/xenia-types" }
|
||||
xenia-memory = { path = "crates/xenia-memory" }
|
||||
xenia-cpu = { path = "crates/xenia-cpu" }
|
||||
xenia-jit = { path = "crates/xenia-jit" }
|
||||
xenia-xex = { path = "crates/xenia-xex" }
|
||||
xenia-vfs = { path = "crates/xenia-vfs" }
|
||||
xenia-kernel = { path = "crates/xenia-kernel" }
|
||||
@@ -37,6 +39,9 @@ xenia-analysis = { path = "crates/xenia-analysis" }
|
||||
xenia-ui = { path = "crates/xenia-ui" }
|
||||
|
||||
# External dependencies
|
||||
# JIT (PPC->x64 block recompiler; runtime-gated by XENIA_JIT)
|
||||
dynasm = "3"
|
||||
dynasmrt = "3"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "registry"] }
|
||||
tracing-appender = "0.2"
|
||||
|
||||
@@ -12,6 +12,7 @@ path = "src/main.rs"
|
||||
xenia-types = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
xenia-cpu = { workspace = true }
|
||||
xenia-jit = { workspace = true }
|
||||
xenia-xex = { workspace = true }
|
||||
xenia-vfs = { workspace = true }
|
||||
xenia-kernel = { workspace = true }
|
||||
|
||||
@@ -2499,15 +2499,29 @@ struct WorkerCtx {
|
||||
block_cache: xenia_cpu::block_cache::BlockCache,
|
||||
decode_cache: xenia_cpu::decoder::DecodeCache,
|
||||
force_per_instr: bool,
|
||||
/// PPC→x64 JIT code cache for this HW slot. `Some` only when `XENIA_JIT`
|
||||
/// is set (and the RET-CAPTURE debug env is not — the JIT's fallback path
|
||||
/// bypasses `step_block`'s head-of-block capture print). Substitutes for
|
||||
/// the `step_block` call in `run_superblock`; produces byte-identical
|
||||
/// state so goldens are unaffected.
|
||||
jit_cache: Option<xenia_jit::JitCache>,
|
||||
}
|
||||
|
||||
impl WorkerCtx {
|
||||
fn new(hw_id: u8, force_per_instr: bool) -> Self {
|
||||
let jit_cache = if xenia_jit::env_enabled()
|
||||
&& std::env::var("XENIA_RET_CAPTURE_PC").is_err()
|
||||
{
|
||||
Some(xenia_jit::JitCache::new())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self {
|
||||
hw_id,
|
||||
block_cache: xenia_cpu::block_cache::BlockCache::new(),
|
||||
decode_cache: xenia_cpu::decoder::DecodeCache::new(),
|
||||
force_per_instr,
|
||||
jit_cache,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3151,7 +3165,15 @@ fn run_superblock(
|
||||
let (result, executed) = {
|
||||
let ctx = kernel.scheduler.ctx_mut_ref(thread_ref);
|
||||
let cycle_before = ctx.cycle_count;
|
||||
let result = step_block(ctx, mem, block);
|
||||
// JIT seam (XENIA_JIT): run the JIT-compiled block if enabled, else
|
||||
// the interpreter. The JIT leaves ctx.cycle_count/pc and
|
||||
// mmio_access_count in exactly the interpreter's state, so all the
|
||||
// surrounding accounting (executed, sync/MMIO/budget chain checks)
|
||||
// is untouched and goldens stay byte-identical.
|
||||
let result = match wc.jit_cache.as_mut() {
|
||||
Some(jit) => jit.run_or_compile(block, ctx, mem),
|
||||
None => step_block(ctx, mem, block),
|
||||
};
|
||||
let executed = ctx.cycle_count.saturating_sub(cycle_before);
|
||||
(result, executed)
|
||||
};
|
||||
|
||||
@@ -213,6 +213,22 @@ pub fn step_block(
|
||||
result
|
||||
}
|
||||
|
||||
/// Execute exactly one already-decoded instruction — the JIT's interpreter
|
||||
/// fallback (`xenia-jit`). Identical to the body of [`step`]/`step_block`
|
||||
/// EXCEPT it does **not** bump `cycle_count`/`timebase`: the JIT owns the
|
||||
/// per-instruction counter increments so that a mix of native and
|
||||
/// fallback opcodes retires exactly one tick each, in order, byte-identical
|
||||
/// to the interpreter. `execute` itself advances `ctx.pc` (each arm does
|
||||
/// `ctx.pc += 4` or sets a branch target), same as the interpreter path.
|
||||
#[inline]
|
||||
pub fn interpret_one(
|
||||
ctx: &mut PpcContext,
|
||||
mem: &dyn MemoryAccess,
|
||||
instr: &DecodedInstr,
|
||||
) -> StepResult {
|
||||
execute(ctx, mem, instr)
|
||||
}
|
||||
|
||||
/// Execute a decoded instruction, updating context and memory.
|
||||
fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -> StepResult {
|
||||
match instr.opcode {
|
||||
|
||||
12
crates/xenia-jit/Cargo.toml
Normal file
12
crates/xenia-jit/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "xenia-jit"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
xenia-cpu = { workspace = true }
|
||||
xenia-memory = { workspace = true }
|
||||
dynasm = { workspace = true }
|
||||
dynasmrt = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
300
crates/xenia-jit/src/lib.rs
Normal file
300
crates/xenia-jit/src/lib.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
//! PPC→x64 block JIT for xenia-rs (`XENIA_JIT`, default OFF).
|
||||
//!
|
||||
//! **Phase 0 — skeleton.** This lands the whole runtime substrate (the
|
||||
//! compiled-block ABI, the interpreter-fallback helper, per-instruction counter
|
||||
//! bumps, block-exit semantics, and a per-slot code cache) while porting
|
||||
//! **zero** opcodes to native code: every guest instruction is emitted as a
|
||||
//! `call` into the interpreter (`xenia_cpu::interpreter::interpret_one`). This
|
||||
//! makes a JIT-compiled block **byte-identical** to `step_block` by
|
||||
//! construction, so the golden regression proves the ABI before any opcode is
|
||||
//! hand-written. Later phases replace individual `call interpret_one` sites
|
||||
//! with native x64 for the hot opcodes; un-ported opcodes keep falling back.
|
||||
//!
|
||||
//! ## Design (context-threading, dynasm-rs)
|
||||
//! Guest state lives in `PpcContext`; a compiled block is an
|
||||
//! `extern "C" fn(*mut JitEnv) -> u32` returning a [`StepResult`] discriminant
|
||||
//! (0 = `Continue`). Emitted code keeps the `PpcContext` pointer in `r15` and
|
||||
//! the `JitEnv` pointer in `rbx` (both callee-saved, so they survive the helper
|
||||
//! `call`s). Memory access + un-ported opcodes go through `extern "C"` helpers
|
||||
//! that receive `JitEnv` and reconstruct `&dyn MemoryAccess` from the fat raw
|
||||
//! pointer stored in it — no fat-pointer transmute.
|
||||
//!
|
||||
//! ## Determinism (the load-bearing invariant)
|
||||
//! After **every** retired instruction (native or fallback) the block bumps
|
||||
//! `ctx.cycle_count` and `ctx.timebase` by 1, matching
|
||||
//! `interpreter.rs::step_block` exactly. Blocks stop at the same instruction
|
||||
//! the interpreter would (non-`Continue` result, or a taken branch that makes
|
||||
//! `pc != expected_next`). The JIT code cache mirrors the interpreter block
|
||||
//! cache's `(start_pc, page_version)` invalidation, and each compiled block
|
||||
//! **owns a copy** of its decoded instructions so baked instruction pointers
|
||||
//! can never dangle after a block-cache eviction.
|
||||
|
||||
use dynasmrt::{DynasmApi, DynasmLabelApi, dynasm};
|
||||
|
||||
use xenia_cpu::block_cache::DecodedBlock;
|
||||
use xenia_cpu::context::PpcContext;
|
||||
use xenia_cpu::decoder::DecodedInstr;
|
||||
use xenia_cpu::interpreter::{StepResult, interpret_one};
|
||||
use xenia_memory::MemoryAccess;
|
||||
|
||||
/// 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
|
||||
/// to reconstruct the `&dyn` in the helpers.
|
||||
pub struct JitEnv {
|
||||
/// Guest CPU state — loaded into `r15` by the block prologue.
|
||||
ctx: *mut PpcContext,
|
||||
/// The guest memory the block runs against (fat raw pointer). Only the
|
||||
/// `extern "C"` helpers dereference this.
|
||||
mem: *const dyn MemoryAccess,
|
||||
/// The exact `StepResult` of the last instruction the block ran. The Rust
|
||||
/// wrapper reads this on a non-`Continue` exit so the full payload (e.g.
|
||||
/// `Unimplemented(op)`) is preserved without serializing it through the
|
||||
/// `u32` return channel.
|
||||
last_result: StepResult,
|
||||
}
|
||||
|
||||
/// A compiled block's callable form. First arg (`rdi`) is the `JitEnv`; the
|
||||
/// return value (`eax`) is a [`StepResult`] discriminant (0 = `Continue`).
|
||||
type JitBlockFn = unsafe extern "C" fn(*mut JitEnv) -> u32;
|
||||
|
||||
/// Map a `StepResult` to the block's `u32` return channel. Only the
|
||||
/// `Continue == 0` vs non-zero distinction is load-bearing (the wrapper reads
|
||||
/// `JitEnv::last_result` for the actual non-`Continue` value); the specific
|
||||
/// codes are for clarity/debugging.
|
||||
#[inline]
|
||||
fn sr_code(r: StepResult) -> u32 {
|
||||
match r {
|
||||
StepResult::Continue => 0,
|
||||
StepResult::SystemCall => 1,
|
||||
StepResult::Unimplemented(_) => 2,
|
||||
StepResult::Trap => 3,
|
||||
StepResult::Halted => 4,
|
||||
StepResult::Yield => 5,
|
||||
}
|
||||
}
|
||||
|
||||
/// Interpreter fallback for one instruction, called from emitted code.
|
||||
///
|
||||
/// SAFETY: invoked only from a compiled block created by [`compile_block`],
|
||||
/// which passes a `JitEnv` that is live on [`run_jit_block`]'s stack and an
|
||||
/// `instr` pointing into the owning `CompiledBlock`'s instruction copy (kept
|
||||
/// alive for the duration of the call). Reconstructs the `&mut PpcContext` and
|
||||
/// `&dyn MemoryAccess` from the env. Does not bump counters — the block does.
|
||||
unsafe extern "C" fn jit_interpret_one(env: *mut JitEnv, instr: *const DecodedInstr) -> u32 {
|
||||
// SAFETY: see function contract.
|
||||
let env = unsafe { &mut *env };
|
||||
let ctx = unsafe { &mut *env.ctx };
|
||||
let mem: &dyn MemoryAccess = unsafe { &*env.mem };
|
||||
let instr = unsafe { &*instr };
|
||||
let r = interpret_one(ctx, mem, instr);
|
||||
env.last_result = r;
|
||||
sr_code(r)
|
||||
}
|
||||
|
||||
/// One JIT-compiled block. Owns everything the emitted code references so the
|
||||
/// code, its instruction pointers, and its cache-key metadata share one
|
||||
/// lifetime.
|
||||
struct CompiledBlock {
|
||||
start_pc: u32,
|
||||
/// `DecodedBlock::page_version` at compile time; mismatch on lookup forces
|
||||
/// recompilation (mirrors the interpreter block cache invalidation).
|
||||
page_version: u64,
|
||||
/// Owned copy of the block's decoded instructions. The emitted `call`s bake
|
||||
/// raw pointers to these elements, so this boxed slice (stable address)
|
||||
/// MUST outlive `func`. Kept alive as a field; not read directly.
|
||||
_instrs: Box<[DecodedInstr]>,
|
||||
/// Backing executable mapping for `func`. Kept alive as a field.
|
||||
_buf: dynasmrt::ExecutableBuffer,
|
||||
/// Entry point into `_buf`.
|
||||
func: JitBlockFn,
|
||||
}
|
||||
|
||||
// SAFETY: `CompiledBlock` is only ever created, stored, and invoked on the
|
||||
// single owning HW-slot thread (each `WorkerCtx` has its own `JitCache`), the
|
||||
// same discipline as the interpreter's per-slot `BlockCache`. The raw pointers
|
||||
// it holds are self-owned. It is never shared across threads.
|
||||
unsafe impl Send for CompiledBlock {}
|
||||
|
||||
/// Compile `block` into a `CompiledBlock`. Phase 0: every instruction is a
|
||||
/// `call jit_interpret_one` + the mandatory counter/exit postlude.
|
||||
fn compile_block(block: &DecodedBlock) -> CompiledBlock {
|
||||
// Own the instruction stream first, then bake pointers into the *owned*
|
||||
// copy (its addresses are final once boxed).
|
||||
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 helper = jit_interpret_one as usize as i64;
|
||||
|
||||
let mut ops = dynasmrt::x64::Assembler::new().expect("dynasm assembler");
|
||||
let entry = ops.offset();
|
||||
let l_exit = ops.new_dynamic_label();
|
||||
let l_cont = ops.new_dynamic_label();
|
||||
|
||||
// Prologue: save callee-saved regs we use, keep the stack 16-aligned before
|
||||
// the helper calls (entry rsp%16==8; two pushes -> 8; `sub 8` -> 0), pin
|
||||
// env in rbx and ctx in r15.
|
||||
dynasm!(ops
|
||||
; .arch x64
|
||||
; push rbx
|
||||
; push r15
|
||||
; sub rsp, 8
|
||||
; mov rbx, rdi
|
||||
; mov r15, [rbx + off_ctx]
|
||||
);
|
||||
|
||||
for instr in instrs.iter() {
|
||||
let instr_ptr = instr as *const DecodedInstr as usize as i64;
|
||||
let expected_next = instr.addr.wrapping_add(4) as i32;
|
||||
dynasm!(ops
|
||||
; .arch x64
|
||||
// fallback: eax = jit_interpret_one(env, &instr); env.last_result set
|
||||
; mov rdi, rbx
|
||||
; mov rsi, QWORD instr_ptr
|
||||
; mov rax, QWORD helper
|
||||
; call rax
|
||||
// determinism postlude: cycle_count += 1; timebase += 1
|
||||
; 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
|
||||
; jne =>l_cont
|
||||
);
|
||||
}
|
||||
|
||||
// Natural end / discontinuity exit: Continue (eax=0). Shared epilogue.
|
||||
dynasm!(ops
|
||||
; .arch x64
|
||||
; =>l_cont
|
||||
; xor eax, eax
|
||||
; =>l_exit
|
||||
; add rsp, 8
|
||||
; pop r15
|
||||
; pop rbx
|
||||
; ret
|
||||
);
|
||||
|
||||
let buf = ops.finalize().expect("dynasm finalize");
|
||||
// SAFETY: `entry` is a valid offset into `buf`; the emitted code matches
|
||||
// the `JitBlockFn` ABI (System V, first arg rdi, return eax).
|
||||
let func: JitBlockFn = unsafe { std::mem::transmute::<*const u8, JitBlockFn>(buf.ptr(entry)) };
|
||||
|
||||
CompiledBlock {
|
||||
start_pc: block.start_pc,
|
||||
page_version: block.page_version,
|
||||
_instrs: instrs,
|
||||
_buf: buf,
|
||||
func,
|
||||
}
|
||||
}
|
||||
|
||||
/// Run a compiled block against `ctx`/`mem`, returning the same `StepResult`
|
||||
/// the interpreter's `step_block` would. The block bumps `cycle_count`/
|
||||
/// `timebase` and updates `ctx.pc` in place, exactly like the interpreter.
|
||||
fn run_jit_block(cb: &CompiledBlock, ctx: &mut PpcContext, mem: &dyn MemoryAccess) -> StepResult {
|
||||
// Erase the borrow lifetime so it fits `JitEnv::mem` (a raw
|
||||
// `*const dyn MemoryAccess`, i.e. `+ 'static`). This is a lifetime-only
|
||||
// transmute — the fat-pointer representation is unchanged — and is sound
|
||||
// because `env` does not escape: the block runs synchronously and returns
|
||||
// before `mem`'s borrow ends.
|
||||
let mem_static: &'static dyn MemoryAccess =
|
||||
unsafe { std::mem::transmute::<&dyn MemoryAccess, &'static dyn MemoryAccess>(mem) };
|
||||
let mut env = JitEnv {
|
||||
ctx: ctx as *mut PpcContext,
|
||||
mem: mem_static as *const dyn MemoryAccess,
|
||||
last_result: StepResult::Continue,
|
||||
};
|
||||
// SAFETY: `func` is code emitted by `compile_block` for the JitBlockFn ABI;
|
||||
// `env` outlives the call; `cb` (and its owned instrs the code references)
|
||||
// is borrowed for the whole call.
|
||||
let code = unsafe { (cb.func)(&mut env as *mut JitEnv) };
|
||||
if code == 0 {
|
||||
StepResult::Continue
|
||||
} else {
|
||||
env.last_result
|
||||
}
|
||||
}
|
||||
|
||||
// Matches the interpreter's `BlockCache` (64K direct-mapped, pc-indexed).
|
||||
const JIT_CACHE_SIZE: usize = 1 << 16;
|
||||
const JIT_CACHE_MASK: u32 = (JIT_CACHE_SIZE as u32) - 1;
|
||||
|
||||
/// Per-HW-slot JIT code cache. Direct-mapped by guest PC, gated on
|
||||
/// `(start_pc, page_version)` so it invalidates in lock-step with the
|
||||
/// interpreter block cache (self-modifying / reloaded code recompiles).
|
||||
pub struct JitCache {
|
||||
slots: Box<[Option<CompiledBlock>]>,
|
||||
compiles: u64,
|
||||
hits: u64,
|
||||
}
|
||||
|
||||
impl Default for JitCache {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl JitCache {
|
||||
pub fn new() -> Self {
|
||||
let mut v: Vec<Option<CompiledBlock>> = Vec::with_capacity(JIT_CACHE_SIZE);
|
||||
v.resize_with(JIT_CACHE_SIZE, || None);
|
||||
Self {
|
||||
slots: v.into_boxed_slice(),
|
||||
compiles: 0,
|
||||
hits: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compiles(&self) -> u64 {
|
||||
self.compiles
|
||||
}
|
||||
pub fn hits(&self) -> u64 {
|
||||
self.hits
|
||||
}
|
||||
|
||||
/// Look up (or compile) the JIT block for `block` and run it. `block` is the
|
||||
/// freshly-validated `DecodedBlock` from the interpreter cache, so its
|
||||
/// `page_version` is current; we key on it directly.
|
||||
pub fn run_or_compile(
|
||||
&mut self,
|
||||
block: &DecodedBlock,
|
||||
ctx: &mut PpcContext,
|
||||
mem: &dyn MemoryAccess,
|
||||
) -> StepResult {
|
||||
let idx = ((block.start_pc >> 2) & JIT_CACHE_MASK) as usize;
|
||||
let fresh = matches!(
|
||||
&self.slots[idx],
|
||||
Some(cb) if cb.start_pc == block.start_pc && cb.page_version == block.page_version
|
||||
);
|
||||
if fresh {
|
||||
self.hits += 1;
|
||||
} else {
|
||||
self.compiles += 1;
|
||||
self.slots[idx] = Some(compile_block(block));
|
||||
}
|
||||
let cb = self.slots[idx].as_ref().expect("just populated");
|
||||
run_jit_block(cb, ctx, mem)
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the JIT is enabled this run (`XENIA_JIT=1|true|yes`), cached once.
|
||||
pub fn env_enabled() -> bool {
|
||||
use std::sync::OnceLock;
|
||||
static ON: OnceLock<bool> = OnceLock::new();
|
||||
*ON.get_or_init(|| {
|
||||
std::env::var("XENIA_JIT")
|
||||
.ok()
|
||||
.map(|v| {
|
||||
let v = v.trim().to_ascii_lowercase();
|
||||
v == "1" || v == "true" || v == "yes"
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user