Loads no longer call a trampoline on the common path. When memory exposes a flat
mapping and the address is proven non-MMIO and committed, the compiled block
loads directly from `membase + ea` (byte-swapped) with zero calls; otherwise it
falls back to the read trampoline.
Mechanism:
* MemoryAccess::fast_mem() -> Option<FastMem { membase, page_table, mmio_mask,
mmio_value }>. Default None; GuestMemory returns Some (flat 4 GiB mapping,
page-table pointer, MMIO aperture pair). Wrappers that intercept accesses
(the recompiler's speculative OverlayMemory) inherit None, so the fast path
is disabled under the diff harness and their interception is preserved.
* MemEnv carries membase/page_table/mmio_mask/mmio_value; MemEnv::new(mem)
fills them (or nulls when fast_mem() is None).
* emit_inline_load emits a 5-block diamond: membase==0 -> slow (disabled);
(ea & mmio_mask) == mmio_value -> slow (maybe MMIO); page_table[ea>>12]
COMMIT bit (49) clear -> slow (unmapped; the mapped check is mandatory —
unmapped pages are PROT_NONE and a raw load would fault); else fast load
membase+ea, bswap, zero-extend. Wired all integer loads (lbz/lhz/lwz/ld +
x-forms) and FP loads (lfs/lfsx reuse the diamond then bitcast/fpromote;
lfd/lfdx bit-copy). Stores and FP-punt still trampoline.
Validation (three ways, since the diff harness disables the fast path):
* unit test jit_inline_load_matches_interpreter — real GuestMemory, both the
mapped (fast) and unmapped (slow) address bit-match the interpreter.
* e2e XENIA_JIT=1 boot+movie plays, clean exit.
* diff regression: checked 149.2M blocks, MISMATCHES=0 (validates the
unchanged slow/ALU/branch/FP logic).
Measured (2e9 instr, block-exec MIPS): interpreter 98.2 / JIT trampoline-loads
93.8 / JIT inline-loads 96.5 — inline loads recover the trampoline overhead,
bringing the JIT to parity (~1.7% slower, within noise). Beating the interpreter
needs the levers it can't do: block-linking (30% dispatch), host registers.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
144 lines
6.5 KiB
Rust
144 lines
6.5 KiB
Rust
/// Raw pointers/constants a JIT needs to inline the load fast-path (skip the
|
|
/// trait-object call). Only the real [`crate::heap::GuestMemory`] exposes this;
|
|
/// wrappers that intercept accesses (e.g. the recompiler's speculative overlay)
|
|
/// return `None` from [`MemoryAccess::fast_mem`] so the JIT falls back to the
|
|
/// trait-object slow path and their interception is preserved.
|
|
///
|
|
/// The flat mapping means a guest address `a` translates to `membase + a`. A
|
|
/// load is safe to inline only when `a` is **not** MMIO and its page is
|
|
/// committed: `(a & mmio_mask) != mmio_value` AND page-table entry
|
|
/// `page_table[a >> 12]` has the COMMIT bit (bit 49). Unmapped pages are
|
|
/// `PROT_NONE`, so the committed check must precede the raw load.
|
|
#[derive(Clone, Copy)]
|
|
pub struct FastMem {
|
|
/// Base of the flat 4 GiB guest mapping. `membase + guest_addr` is the host
|
|
/// address. Never null for a real mapping (used by the JIT as the
|
|
/// "inline enabled" sentinel).
|
|
pub membase: *mut u8,
|
|
/// Base of the per-page allocation table (`AtomicU64` entries, reinterpreted
|
|
/// as `u64`). Entry `page_table[addr >> 12]` bit 49 = COMMIT.
|
|
pub page_table: *const u64,
|
|
/// MMIO fast-reject mask/value: `(addr & mask) == value` is the *necessary*
|
|
/// condition for `addr` to be MMIO, so `!=` proves non-MMIO.
|
|
pub mmio_mask: u32,
|
|
pub mmio_value: u32,
|
|
}
|
|
|
|
/// Trait for all guest memory access. Every load/store goes through this,
|
|
/// enabling MMIO checking and debugger observation on every access.
|
|
/// This is the key abstraction that eliminates the need for MMIO exception handlers.
|
|
///
|
|
/// **All methods take `&self`.** Write methods rely on interior mutability
|
|
/// (atomics in [`crate::heap::GuestMemory`], `Cell` in test mocks). The
|
|
/// actual byte stores into the backing memory are unsynchronized; callers
|
|
/// must not concurrently read and write the same byte range from different
|
|
/// threads. The per-page write version exposed by [`Self::page_version`] is
|
|
/// a coarse cache-invalidation signal and is published with `Release`
|
|
/// ordering by the writer — readers using `Acquire` (e.g. the texture
|
|
/// cache and the interpreter decode cache) get a synchronizes-with edge to
|
|
/// the corresponding data store.
|
|
pub trait MemoryAccess {
|
|
fn read_u8(&self, addr: u32) -> u8;
|
|
fn read_u16(&self, addr: u32) -> u16;
|
|
fn read_u32(&self, addr: u32) -> u32;
|
|
fn read_u64(&self, addr: u32) -> u64;
|
|
fn read_f32(&self, addr: u32) -> f32 {
|
|
f32::from_bits(self.read_u32(addr))
|
|
}
|
|
fn read_f64(&self, addr: u32) -> f64 {
|
|
f64::from_bits(self.read_u64(addr))
|
|
}
|
|
|
|
fn write_u8(&self, addr: u32, val: u8);
|
|
fn write_u16(&self, addr: u32, val: u16);
|
|
fn write_u32(&self, addr: u32, val: u32);
|
|
fn write_u64(&self, addr: u32, val: u64);
|
|
fn write_f32(&self, addr: u32, val: f32) {
|
|
self.write_u32(addr, val.to_bits());
|
|
}
|
|
fn write_f64(&self, addr: u32, val: f64) {
|
|
self.write_u64(addr, val.to_bits());
|
|
}
|
|
|
|
/// Read a block of bytes from guest memory.
|
|
fn read_bytes(&self, addr: u32, buf: &mut [u8]) {
|
|
for (i, byte) in buf.iter_mut().enumerate() {
|
|
*byte = self.read_u8(addr.wrapping_add(i as u32));
|
|
}
|
|
}
|
|
|
|
/// Write a block of bytes to guest memory.
|
|
fn write_bytes(&self, addr: u32, buf: &[u8]) {
|
|
for (i, &byte) in buf.iter().enumerate() {
|
|
self.write_u8(addr.wrapping_add(i as u32), byte);
|
|
}
|
|
}
|
|
|
|
/// True if `addr` falls in an MMIO region (a load/store there invokes a
|
|
/// device callback with side effects rather than touching backing RAM).
|
|
///
|
|
/// Default `false` (mock memories have no MMIO). `GuestMemory` overrides it.
|
|
/// Used by the recompiler's differential harness to *skip* comparing blocks
|
|
/// that touch MMIO — a device callback cannot be safely executed twice.
|
|
fn is_mmio(&self, _addr: u32) -> bool {
|
|
false
|
|
}
|
|
|
|
/// Raw pointers/constants for a JIT to inline the load fast-path, or `None`
|
|
/// to force the trait-object slow path. Default `None` (mock memories and
|
|
/// interception wrappers have no flat mapping to expose). `GuestMemory`
|
|
/// overrides it. See [`FastMem`].
|
|
fn fast_mem(&self) -> Option<FastMem> {
|
|
None
|
|
}
|
|
|
|
/// Get a direct host pointer for the given guest address.
|
|
/// Returns None if the address is invalid or in an MMIO region.
|
|
fn translate(&self, addr: u32) -> Option<*const u8>;
|
|
|
|
/// Get a mutable direct host pointer for the given guest address.
|
|
fn translate_mut(&self, addr: u32) -> Option<*mut u8>;
|
|
|
|
/// Monotonic write-version of the 4 KiB page containing `addr`.
|
|
/// Used by the interpreter's decode cache (xenia-cpu `DecodeCache`)
|
|
/// to invalidate entries when the guest rewrites code pages.
|
|
///
|
|
/// Default impl returns `1` — a constant non-zero value that works
|
|
/// for mock memories in tests (the decode cache treats
|
|
/// constant-version runs as "never invalidated"). Real memory
|
|
/// (`xenia-memory::GuestMemory`) overrides this with its
|
|
/// per-page counter.
|
|
fn page_version(&self, _addr: u32) -> u64 {
|
|
1
|
|
}
|
|
|
|
/// M1.8 — fenced 32-bit write. Used by the GPU's
|
|
/// `PM4_EVENT_WRITE_SHD` to publish a fence value into guest memory
|
|
/// after one or more data writes the CPU thread will read once it
|
|
/// observes the fence. Emits a `Release` fence before the data
|
|
/// store: any earlier writes by the calling thread happen-before
|
|
/// any thread that performs a matching `Acquire` load via
|
|
/// [`Self::read_u32_fence`].
|
|
///
|
|
/// On x86_64 (TSO) the `Release` fence compiles to a no-op; on
|
|
/// weaker targets it emits the appropriate barrier. The store
|
|
/// itself is 32-bit aligned and naturally atomic on x86_64
|
|
/// (single-copy atomicity) — we rely on that and only fence the
|
|
/// surrounding stores, not the store itself.
|
|
fn write_u32_fence(&self, addr: u32, val: u32) {
|
|
std::sync::atomic::fence(std::sync::atomic::Ordering::Release);
|
|
self.write_u32(addr, val);
|
|
}
|
|
|
|
/// M1.8 — fenced 32-bit read. Used by guest fence-poll loops that
|
|
/// busy-spin on a memory location the GPU writes via
|
|
/// [`Self::write_u32_fence`]. Emits an `Acquire` fence after the
|
|
/// load: any reads the calling thread issues *after* this call see
|
|
/// every write the producer issued *before* its `write_u32_fence`.
|
|
fn read_u32_fence(&self, addr: u32) -> u32 {
|
|
let v = self.read_u32(addr);
|
|
std::sync::atomic::fence(std::sync::atomic::Ordering::Acquire);
|
|
v
|
|
}
|
|
}
|