[iterate-4A] jit: cache MemEnv fast_mem + expose mmio_count (prep for block chaining)

Behavior-neutral prep on top of the inline-load parity milestone:

  * JitCache caches mem.fast_mem() (resolved once — the mapping is invariant for
    a run) so compiled blocks build their MemEnv without a virtual call per
    execution. MemEnv::from_fast(mem, Option<FastMem>) is the non-virtual
    constructor. Measured impact is within noise (96.5 -> 96.4 MIPS) but it
    removes redundant per-block work.
  * FastMem/MemEnv gain mmio_count: a pointer to GuestMemory's monotonic MMIO
    access counter. Unused for now; the upcoming superblock-chaining JIT will
    sample it across a block boundary to stop chaining on an MMIO touch
    (preserving the interpreter's fine-grained MMIO ordering).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-07 20:02:05 +02:00
parent da0509b4ac
commit 5d5dc5dd89
4 changed files with 41 additions and 4 deletions

View File

@@ -33,7 +33,7 @@ use crate::block_cache::DecodedBlock;
use crate::context::PpcContext;
use crate::decoder::DecodedInstr;
use crate::opcode::PpcOpcode;
use xenia_memory::MemoryAccess;
use xenia_memory::{FastMem, MemoryAccess};
/// `StepResult` discriminants returned by compiled blocks. Kept in sync with
/// [`crate::interpreter::StepResult`] (only `Continue` is produced by covered
@@ -56,6 +56,7 @@ pub struct MemEnv<'a> {
pub page_table: *const u64,
pub mmio_mask: u32,
pub mmio_value: u32,
pub mmio_count: *const u64,
}
impl<'a> MemEnv<'a> {
@@ -63,13 +64,21 @@ impl<'a> MemEnv<'a> {
/// exposes them ([`MemoryAccess::fast_mem`]). Memories that intercept
/// accesses return `None`, leaving `membase` null (fast path disabled).
pub fn new(mem: &'a dyn MemoryAccess) -> Self {
match mem.fast_mem() {
Self::from_fast(mem, mem.fast_mem())
}
/// Build the env from `mem` plus an already-resolved [`FastMem`] — lets the
/// caller cache `mem.fast_mem()` (a virtual call) instead of paying it per
/// block. `fm` must belong to `mem`.
pub fn from_fast(mem: &'a dyn MemoryAccess, fm: Option<FastMem>) -> Self {
match fm {
Some(f) => Self {
mem,
membase: f.membase,
page_table: f.page_table,
mmio_mask: f.mmio_mask,
mmio_value: f.mmio_value,
mmio_count: f.mmio_count,
},
None => Self {
mem,
@@ -77,6 +86,7 @@ impl<'a> MemEnv<'a> {
page_table: std::ptr::null(),
mmio_mask: 0,
mmio_value: 1, // (addr & 0) == 1 is never true; membase gate wins anyway
mmio_count: std::ptr::null(),
},
}
}
@@ -204,6 +214,11 @@ pub struct JitCache {
/// lookup misses and the caller interprets (no JIT available).
jit: Option<Jit>,
slots: Box<[Option<CompiledSlot>]>,
/// Cached `mem.fast_mem()` — resolved once (the mapping is invariant for a
/// run) so compiled blocks don't pay a virtual call per execution to build
/// their `MemEnv`. `fast_mem_init` guards first use.
fast_mem: Option<FastMem>,
fast_mem_init: bool,
}
impl Default for JitCache {
@@ -216,7 +231,23 @@ impl JitCache {
pub fn new() -> Self {
let mut slots: Vec<Option<CompiledSlot>> = Vec::with_capacity(JIT_CACHE_SIZE);
slots.resize_with(JIT_CACHE_SIZE, || None);
Self { jit: Jit::new().ok(), slots: slots.into_boxed_slice() }
Self {
jit: Jit::new().ok(),
slots: slots.into_boxed_slice(),
fast_mem: None,
fast_mem_init: false,
}
}
/// Build a `MemEnv` for `mem`, resolving (and caching) its `fast_mem()` on
/// first use. `mem` is invariant across a run, so the cached value stays
/// correct (the overlay used under the diff harness always yields `None`).
pub fn mem_env<'a>(&mut self, mem: &'a dyn MemoryAccess) -> MemEnv<'a> {
if !self.fast_mem_init {
self.fast_mem = mem.fast_mem();
self.fast_mem_init = true;
}
MemEnv::from_fast(mem, self.fast_mem)
}
/// Return the compiled entry point for `block`, compiling it on the first

View File

@@ -150,7 +150,7 @@ pub fn run_block(
) -> StepResult {
if let Some(f) = jit.get_or_compile(block) {
JIT_COMPILED_RUN.fetch_add(1, Ordering::Relaxed);
let env = MemEnv::new(mem);
let env = jit.mem_env(mem);
let raw = f(ctx as *mut PpcContext, &env as *const MemEnv);
// Covered blocks are straight-line (branch/sc/trap/db16cyc are all
// uncovered), so a compiled block always runs to completion and

View File

@@ -22,6 +22,11 @@ pub struct FastMem {
/// condition for `addr` to be MMIO, so `!=` proves non-MMIO.
pub mmio_mask: u32,
pub mmio_value: u32,
/// Pointer to the monotonic MMIO-access counter (`AtomicU64` as `*const
/// u64`). A superblock JIT samples this across a block boundary to detect an
/// MMIO touch and stop chaining there (preserving the interpreter's
/// fine-grained MMIO ordering).
pub mmio_count: *const u64,
}
/// Trait for all guest memory access. Every load/store goes through this,

View File

@@ -519,6 +519,7 @@ impl MemoryAccess for GuestMemory {
page_table: self.page_table.as_ptr() as *const u64,
mmio_mask: self.mmio_aperture_mask,
mmio_value: self.mmio_aperture_value,
mmio_count: &self.mmio_access_count as *const AtomicU64 as *const u64,
})
}