[iterate-4A] jit M0: recompiler seam + in-process differential harness
Foundation for a staged PPC block-recompiler (plan: closure-threaded -> Cranelift -> block-linking). No codegen yet — proves the integration seam and the correctness harness before any lowering exists. New crates/xenia-cpu/src/recompiler.rs: - run_block: executes a DecodedBlock; M0 falls back to the interpreter's execute() for every opcode, so it is bit-identical to step_block. Later stages dispatch lowered ops here and fall back only for uncompiled opcodes. - gates jit_enabled()/diff_enabled() (XENIA_JIT / XENIA_JIT_DIFF, cached). - In-process differential harness (diff_step): the interpreter is AUTHORITATIVE (drives real ctx+mem, so a JIT bug can never corrupt a run); the JIT runs SPECULATIVELY on a ctx clone against OverlayMemory (writes buffered in a byte HashMap, reads fall through to real pre-block memory), then registers are compared. Blocks touching MMIO or sync_sensitive (reservation/barrier) are skipped — a device callback can't be run twice and reservation state is shared cross-thread. report_diff_summary() prints checked/skipped/mismatch. Why in-process: the guest is only COARSELY deterministic — coord_idle_advance ticks vsync from wall-clock when idle, so two separate runs are not bit-exact and a cross-run signature compare would measure jitter, not JIT divergence. Supporting changes: PpcContext #[derive(Clone)] (harness clears the speculative clone's reservation_table Arc); is_mmio() on the MemoryAccess trait (default false) + GuestMemory impl via find_mmio; execute() made pub(crate); run_superblock routes the block body (DIFF->diff_step, JIT->run_block, else step_block). Validated: full boot+movie XENIA_JIT_DIFF=1 run = checked 218.4M blocks, skipped(mmio/sync) 511.6K (0.23%), MISMATCHES=0 CLEAN; movie plays (ADVreads=30, tid25 resumes); diff-mode ~2x slower (test-only path). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3139,7 +3139,18 @@ fn run_superblock(
|
|||||||
let _prof_t0 = xenia_gpu::prof::is_on().then(std::time::Instant::now);
|
let _prof_t0 = xenia_gpu::prof::is_on().then(std::time::Instant::now);
|
||||||
let result = {
|
let result = {
|
||||||
let ctx = kernel.scheduler.ctx_mut_ref(thread_ref);
|
let ctx = kernel.scheduler.ctx_mut_ref(thread_ref);
|
||||||
step_block(ctx, mem, block)
|
// JIT seam (M0). run_superblock's chain-loop + stop-conditions are
|
||||||
|
// unchanged; only the block body is routed:
|
||||||
|
// - XENIA_JIT_DIFF: interpreter authoritative + JIT checked in-process,
|
||||||
|
// - XENIA_JIT: JIT authoritative (M0 = interpreter fallback),
|
||||||
|
// - else: interpreter directly.
|
||||||
|
if xenia_cpu::recompiler::diff_enabled() {
|
||||||
|
xenia_cpu::recompiler::diff_step(ctx, mem, block)
|
||||||
|
} else if xenia_cpu::recompiler::jit_enabled() {
|
||||||
|
xenia_cpu::recompiler::run_block(ctx, mem, block)
|
||||||
|
} else {
|
||||||
|
step_block(ctx, mem, block)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let executed = kernel
|
let executed = kernel
|
||||||
.scheduler
|
.scheduler
|
||||||
@@ -4383,6 +4394,8 @@ fn dump_thread_diagnostic(
|
|||||||
if xenia_gpu::prof::enabled() {
|
if xenia_gpu::prof::enabled() {
|
||||||
xenia_gpu::prof::report(0);
|
xenia_gpu::prof::report(0);
|
||||||
}
|
}
|
||||||
|
// JIT differential harness: checked/skipped/mismatch tally at clean exit.
|
||||||
|
xenia_cpu::recompiler::report_diff_summary();
|
||||||
|
|
||||||
// STEP-10 diagnostic (observe-only, env-gated `XENIA_DUMP_SLOTS=1`).
|
// STEP-10 diagnostic (observe-only, env-gated `XENIA_DUMP_SLOTS=1`).
|
||||||
// Prints each scheduler slot's full runqueue with the fields needed to
|
// Prints each scheduler slot's full runqueue with the fields needed to
|
||||||
|
|||||||
@@ -62,6 +62,11 @@ pub const VSCR_SAT_MASK: u32 = 0x0000_0001;
|
|||||||
|
|
||||||
/// PowerPC processor context. Holds all register state for one guest thread.
|
/// PowerPC processor context. Holds all register state for one guest thread.
|
||||||
/// Mirrors PPCContext from ppc_context.h, minus JIT-specific fields.
|
/// Mirrors PPCContext from ppc_context.h, minus JIT-specific fields.
|
||||||
|
// `Clone` supports the recompiler's differential harness, which snapshots a
|
||||||
|
// context to run the JIT speculatively against the interpreter. Cloning shares
|
||||||
|
// the `reservation_table` Arc; the harness clears it on the speculative copy so
|
||||||
|
// a speculative lwarx/stwcx cannot perturb the authoritative reservation state.
|
||||||
|
#[derive(Clone)]
|
||||||
#[repr(C, align(64))]
|
#[repr(C, align(64))]
|
||||||
pub struct PpcContext {
|
pub struct PpcContext {
|
||||||
// General purpose registers (R0-R31)
|
// General purpose registers (R0-R31)
|
||||||
|
|||||||
@@ -214,7 +214,11 @@ pub fn step_block(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Execute a decoded instruction, updating context and memory.
|
/// Execute a decoded instruction, updating context and memory.
|
||||||
fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -> StepResult {
|
///
|
||||||
|
/// `pub(crate)` so the recompiler ([`crate::recompiler`]) can reuse the exact
|
||||||
|
/// per-opcode semantics as its interpreter-fallback path — the JIT lowers from
|
||||||
|
/// this same source of truth, and uncompiled opcodes route straight back here.
|
||||||
|
pub(crate) fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -> StepResult {
|
||||||
match instr.opcode {
|
match instr.opcode {
|
||||||
// ===== ALU: Immediate =====
|
// ===== ALU: Immediate =====
|
||||||
PpcOpcode::addi => {
|
PpcOpcode::addi => {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub mod interpreter;
|
|||||||
pub mod opcode;
|
pub mod opcode;
|
||||||
pub mod overflow;
|
pub mod overflow;
|
||||||
pub mod phaser;
|
pub mod phaser;
|
||||||
|
pub mod recompiler;
|
||||||
pub mod reservation;
|
pub mod reservation;
|
||||||
pub mod scheduler;
|
pub mod scheduler;
|
||||||
pub mod trap;
|
pub mod trap;
|
||||||
|
|||||||
328
crates/xenia-cpu/src/recompiler.rs
Normal file
328
crates/xenia-cpu/src/recompiler.rs
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
//! PPC block recompiler (JIT) — **M0 foundation**.
|
||||||
|
//!
|
||||||
|
//! This is the seam a staged block-recompiler plugs into. M0 ships **no real
|
||||||
|
//! codegen**: [`run_block`] executes a block by falling back to the
|
||||||
|
//! interpreter's [`crate::interpreter::execute`] for every instruction, so it
|
||||||
|
//! is bit-identical to [`crate::interpreter::step_block`] by construction. Its
|
||||||
|
//! purpose is to prove the integration seam and the **differential harness**
|
||||||
|
//! before any lowering exists. Later stages replace the per-instruction
|
||||||
|
//! fallback with lowered ops (closure-threaded, then Cranelift machine code)
|
||||||
|
//! opcode-by-opcode, each gated through the harness below.
|
||||||
|
//!
|
||||||
|
//! ## Determinism is the whole game
|
||||||
|
//! `cycle_count`/`timebase` are bumped once per guest instruction and drive the
|
||||||
|
//! IPM clock the movie/scheduler depend on. [`run_block`] reproduces
|
||||||
|
//! `step_block`'s counting and stop semantics **exactly**. The interpreter is
|
||||||
|
//! the permanent reference oracle; the recompiler must never diverge from it.
|
||||||
|
//!
|
||||||
|
//! ## Differential harness (`XENIA_JIT_DIFF`) — in-process
|
||||||
|
//! The guest is only *coarsely* deterministic: `coord_idle_advance` ticks vsync
|
||||||
|
//! from wall-clock when the scheduler idles, so vblank ISRs land at
|
||||||
|
//! wall-clock-dependent instruction boundaries and two separate runs are **not**
|
||||||
|
//! bit-identical. Comparing across runs would measure that jitter, not JIT
|
||||||
|
//! divergence. So the harness is **in-process** ([`diff_step`]):
|
||||||
|
//!
|
||||||
|
//! * the **interpreter is authoritative** — it drives the real context and
|
||||||
|
//! memory, so a JIT bug can never corrupt the run;
|
||||||
|
//! * the **JIT runs speculatively** on a *clone* of the pre-block context
|
||||||
|
//! against an [`OverlayMemory`] (writes buffered, reads fall through to real
|
||||||
|
//! *pre-block* memory), then its registers are compared to the
|
||||||
|
//! interpreter's;
|
||||||
|
//! * blocks that touch **MMIO** (a device callback can't be run twice) or are
|
||||||
|
//! **`sync_sensitive`** (reservation/barrier ops share cross-thread state)
|
||||||
|
//! are skipped, not compared.
|
||||||
|
//!
|
||||||
|
//! Run with `XENIA_JIT_DIFF=1`; [`report_diff_summary`] prints checked/skipped/
|
||||||
|
//! mismatch counts at exit. Zero mismatches ⇒ the recompiler matches the
|
||||||
|
//! interpreter on every comparable block.
|
||||||
|
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::atomic::{AtomicU64, AtomicU8, Ordering};
|
||||||
|
|
||||||
|
use crate::block_cache::DecodedBlock;
|
||||||
|
use crate::context::PpcContext;
|
||||||
|
use crate::interpreter::{execute, step_block, StepResult};
|
||||||
|
use xenia_memory::MemoryAccess;
|
||||||
|
|
||||||
|
/// Cached env gate. 0 = uninitialised, 1 = on, 2 = off.
|
||||||
|
#[inline]
|
||||||
|
fn cached_flag(cell: &AtomicU8, var: &str) -> bool {
|
||||||
|
match cell.load(Ordering::Relaxed) {
|
||||||
|
1 => true,
|
||||||
|
2 => false,
|
||||||
|
_ => {
|
||||||
|
let on = std::env::var_os(var).is_some();
|
||||||
|
cell.store(if on { 1 } else { 2 }, Ordering::Relaxed);
|
||||||
|
on
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `XENIA_JIT` — route block execution through the recompiler instead of the
|
||||||
|
/// interpreter's `step_block`. Opt-in; cheap cached check on the hot path.
|
||||||
|
#[inline]
|
||||||
|
pub fn jit_enabled() -> bool {
|
||||||
|
static F: AtomicU8 = AtomicU8::new(0);
|
||||||
|
cached_flag(&F, "XENIA_JIT")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `XENIA_JIT_DIFF` — enable the in-process differential harness ([`diff_step`]).
|
||||||
|
#[inline]
|
||||||
|
pub fn diff_enabled() -> bool {
|
||||||
|
static F: AtomicU8 = AtomicU8::new(0);
|
||||||
|
cached_flag(&F, "XENIA_JIT_DIFF")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Execute one decoded block. **M0: interpreter fallback for every opcode.**
|
||||||
|
///
|
||||||
|
/// Byte-for-byte the same loop as [`crate::interpreter::step_block`]: bump
|
||||||
|
/// `cycle_count`/`timebase` per instruction, bail on the first non-`Continue`
|
||||||
|
/// result, and stop on a PC discontinuity (only the terminator may branch).
|
||||||
|
pub fn run_block(
|
||||||
|
ctx: &mut PpcContext,
|
||||||
|
mem: &dyn MemoryAccess,
|
||||||
|
block: &DecodedBlock,
|
||||||
|
) -> StepResult {
|
||||||
|
let mut result = StepResult::Continue;
|
||||||
|
for instr in &block.instrs {
|
||||||
|
let expected_next = instr.addr.wrapping_add(4);
|
||||||
|
// M0 fallback: identical to the interpreter. Future stages dispatch
|
||||||
|
// lowered ops here and only fall back for uncompiled opcodes.
|
||||||
|
result = execute(ctx, mem, instr);
|
||||||
|
ctx.cycle_count += 1;
|
||||||
|
ctx.timebase += 1;
|
||||||
|
if !matches!(result, StepResult::Continue) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if ctx.pc != expected_next {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- in-process differential harness -------------------------------------
|
||||||
|
|
||||||
|
static CHECKED: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static SKIPPED: AtomicU64 = AtomicU64::new(0);
|
||||||
|
static MISMATCH: AtomicU64 = AtomicU64::new(0);
|
||||||
|
const MAX_MISMATCH_PRINTS: u64 = 20;
|
||||||
|
|
||||||
|
/// Run one block under the differential harness. The **interpreter is
|
||||||
|
/// authoritative** (drives `ctx`+`mem`); the JIT runs speculatively on a clone
|
||||||
|
/// against an overlay and its registers are compared. Returns the interpreter's
|
||||||
|
/// `StepResult`.
|
||||||
|
pub fn diff_step(
|
||||||
|
ctx: &mut PpcContext,
|
||||||
|
mem: &dyn MemoryAccess,
|
||||||
|
block: &DecodedBlock,
|
||||||
|
) -> StepResult {
|
||||||
|
// Reservation/barrier blocks share cross-thread state; don't speculate.
|
||||||
|
if block.sync_sensitive {
|
||||||
|
SKIPPED.fetch_add(1, Ordering::Relaxed);
|
||||||
|
return step_block(ctx, mem, block);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Speculative JIT run on a clone against buffered/overlay memory. Runs
|
||||||
|
// BEFORE the authoritative interpreter so its reads see pre-block memory.
|
||||||
|
let mut cand = ctx.clone();
|
||||||
|
cand.reservation_table = None; // never touch the shared reservation table
|
||||||
|
let overlay = OverlayMemory::new(mem);
|
||||||
|
let _ = run_block(&mut cand, &overlay, block);
|
||||||
|
let touched_mmio = overlay.touched_mmio.get();
|
||||||
|
|
||||||
|
// Authoritative interpreter run: commits real ctx + memory.
|
||||||
|
let auth = step_block(ctx, mem, block);
|
||||||
|
|
||||||
|
if touched_mmio {
|
||||||
|
SKIPPED.fetch_add(1, Ordering::Relaxed);
|
||||||
|
} else {
|
||||||
|
CHECKED.fetch_add(1, Ordering::Relaxed);
|
||||||
|
compare(&cand, ctx, block.start_pc);
|
||||||
|
}
|
||||||
|
auth
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compare speculative-JIT (`cand`) vs authoritative-interpreter (`auth`)
|
||||||
|
/// register state. Reports the first [`MAX_MISMATCH_PRINTS`] divergent blocks.
|
||||||
|
/// Covers the integer + FP + control state (M0/M1 scope); vector regs join when
|
||||||
|
/// VMX opcodes are lowered.
|
||||||
|
fn compare(cand: &PpcContext, auth: &PpcContext, start_pc: u32) {
|
||||||
|
let mut diffs: Vec<String> = Vec::new();
|
||||||
|
for i in 0..32 {
|
||||||
|
if cand.gpr[i] != auth.gpr[i] {
|
||||||
|
diffs.push(format!("r{i} jit={:#018x} int={:#018x}", cand.gpr[i], auth.gpr[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i in 0..32 {
|
||||||
|
if cand.fpr[i].to_bits() != auth.fpr[i].to_bits() {
|
||||||
|
diffs.push(format!(
|
||||||
|
"f{i} jit={:#018x} int={:#018x}",
|
||||||
|
cand.fpr[i].to_bits(),
|
||||||
|
auth.fpr[i].to_bits()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cand.lr != auth.lr {
|
||||||
|
diffs.push(format!("lr jit={:#x} int={:#x}", cand.lr, auth.lr));
|
||||||
|
}
|
||||||
|
if cand.ctr != auth.ctr {
|
||||||
|
diffs.push(format!("ctr jit={:#x} int={:#x}", cand.ctr, auth.ctr));
|
||||||
|
}
|
||||||
|
if cand.pc != auth.pc {
|
||||||
|
diffs.push(format!("pc jit={:#x} int={:#x}", cand.pc, auth.pc));
|
||||||
|
}
|
||||||
|
for i in 0..8 {
|
||||||
|
if cand.cr[i].as_u8() != auth.cr[i].as_u8() {
|
||||||
|
diffs.push(format!("cr{i} jit={:#x} int={:#x}", cand.cr[i].as_u8(), auth.cr[i].as_u8()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (cand.xer_ca, cand.xer_ov, cand.xer_so, cand.xer_tbc)
|
||||||
|
!= (auth.xer_ca, auth.xer_ov, auth.xer_so, auth.xer_tbc)
|
||||||
|
{
|
||||||
|
diffs.push("xer".into());
|
||||||
|
}
|
||||||
|
if cand.fpscr != auth.fpscr {
|
||||||
|
diffs.push(format!("fpscr jit={:#x} int={:#x}", cand.fpscr, auth.fpscr));
|
||||||
|
}
|
||||||
|
if cand.cycle_count != auth.cycle_count {
|
||||||
|
diffs.push(format!("cycles jit={} int={}", cand.cycle_count, auth.cycle_count));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !diffs.is_empty() {
|
||||||
|
let n = MISMATCH.fetch_add(1, Ordering::Relaxed);
|
||||||
|
if n < MAX_MISMATCH_PRINTS {
|
||||||
|
eprintln!("JIT-DIFF MISMATCH block={start_pc:#010x}: {}", diffs.join(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print checked/skipped/mismatch tallies (call at clean exit).
|
||||||
|
pub fn report_diff_summary() {
|
||||||
|
if !diff_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let (c, s, m) = (
|
||||||
|
CHECKED.load(Ordering::Relaxed),
|
||||||
|
SKIPPED.load(Ordering::Relaxed),
|
||||||
|
MISMATCH.load(Ordering::Relaxed),
|
||||||
|
);
|
||||||
|
eprintln!(
|
||||||
|
"=== JIT-DIFF SUMMARY: checked={c} skipped(mmio/sync)={s} MISMATCHES={m} — {} ===",
|
||||||
|
if m == 0 { "CLEAN ✓" } else { "DIVERGENCE ✗" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- overlay memory (speculative-write buffer) ---------------------------
|
||||||
|
|
||||||
|
/// A [`MemoryAccess`] wrapper for speculative JIT execution: reads fall through
|
||||||
|
/// to the real (pre-block) memory, writes are buffered in a byte-granular
|
||||||
|
/// overlay (never committed), and any MMIO touch sets `touched_mmio` so the
|
||||||
|
/// caller can skip comparing that block. Big-endian byte order throughout,
|
||||||
|
/// matching `GuestMemory`.
|
||||||
|
struct OverlayMemory<'a> {
|
||||||
|
real: &'a dyn MemoryAccess,
|
||||||
|
overlay: RefCell<HashMap<u32, u8>>,
|
||||||
|
touched_mmio: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> OverlayMemory<'a> {
|
||||||
|
fn new(real: &'a dyn MemoryAccess) -> Self {
|
||||||
|
Self { real, overlay: RefCell::new(HashMap::new()), touched_mmio: Cell::new(false) }
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn rb(&self, addr: u32) -> u8 {
|
||||||
|
match self.overlay.borrow().get(&addr) {
|
||||||
|
Some(&b) => b,
|
||||||
|
None => self.real.read_u8(addr),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn wb(&self, addr: u32, b: u8) {
|
||||||
|
self.overlay.borrow_mut().insert(addr, b);
|
||||||
|
}
|
||||||
|
/// Returns true (and flags the block) if `addr` is MMIO — callers must not
|
||||||
|
/// touch it speculatively.
|
||||||
|
#[inline]
|
||||||
|
fn is_mmio_stop(&self, addr: u32) -> bool {
|
||||||
|
if self.real.is_mmio(addr) {
|
||||||
|
self.touched_mmio.set(true);
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> MemoryAccess for OverlayMemory<'a> {
|
||||||
|
fn read_u8(&self, a: u32) -> u8 {
|
||||||
|
if self.is_mmio_stop(a) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
self.rb(a)
|
||||||
|
}
|
||||||
|
fn read_u16(&self, a: u32) -> u16 {
|
||||||
|
if self.is_mmio_stop(a) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
((self.rb(a) as u16) << 8) | self.rb(a.wrapping_add(1)) as u16
|
||||||
|
}
|
||||||
|
fn read_u32(&self, a: u32) -> u32 {
|
||||||
|
if self.is_mmio_stop(a) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
((self.rb(a) as u32) << 24)
|
||||||
|
| ((self.rb(a.wrapping_add(1)) as u32) << 16)
|
||||||
|
| ((self.rb(a.wrapping_add(2)) as u32) << 8)
|
||||||
|
| self.rb(a.wrapping_add(3)) as u32
|
||||||
|
}
|
||||||
|
fn read_u64(&self, a: u32) -> u64 {
|
||||||
|
if self.is_mmio_stop(a) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let hi = self.read_u32(a) as u64;
|
||||||
|
let lo = self.read_u32(a.wrapping_add(4)) as u64;
|
||||||
|
(hi << 32) | lo
|
||||||
|
}
|
||||||
|
fn write_u8(&self, a: u32, v: u8) {
|
||||||
|
if self.is_mmio_stop(a) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.wb(a, v);
|
||||||
|
}
|
||||||
|
fn write_u16(&self, a: u32, v: u16) {
|
||||||
|
if self.is_mmio_stop(a) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.wb(a, (v >> 8) as u8);
|
||||||
|
self.wb(a.wrapping_add(1), v as u8);
|
||||||
|
}
|
||||||
|
fn write_u32(&self, a: u32, v: u32) {
|
||||||
|
if self.is_mmio_stop(a) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.wb(a, (v >> 24) as u8);
|
||||||
|
self.wb(a.wrapping_add(1), (v >> 16) as u8);
|
||||||
|
self.wb(a.wrapping_add(2), (v >> 8) as u8);
|
||||||
|
self.wb(a.wrapping_add(3), v as u8);
|
||||||
|
}
|
||||||
|
fn write_u64(&self, a: u32, v: u64) {
|
||||||
|
self.write_u32(a, (v >> 32) as u32);
|
||||||
|
self.write_u32(a.wrapping_add(4), v as u32);
|
||||||
|
}
|
||||||
|
// execute() never calls translate/translate_mut for real memory (only the
|
||||||
|
// test mock does), so returning None is safe here.
|
||||||
|
fn translate(&self, _a: u32) -> Option<*const u8> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn translate_mut(&self, _a: u32) -> Option<*mut u8> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
fn page_version(&self, a: u32) -> u64 {
|
||||||
|
self.real.page_version(a)
|
||||||
|
}
|
||||||
|
fn is_mmio(&self, a: u32) -> bool {
|
||||||
|
self.real.is_mmio(a)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,16 @@ pub trait MemoryAccess {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
}
|
||||||
|
|
||||||
/// Get a direct host pointer for the given guest address.
|
/// Get a direct host pointer for the given guest address.
|
||||||
/// Returns None if the address is invalid or in an MMIO region.
|
/// Returns None if the address is invalid or in an MMIO region.
|
||||||
fn translate(&self, addr: u32) -> Option<*const u8>;
|
fn translate(&self, addr: u32) -> Option<*const u8>;
|
||||||
|
|||||||
@@ -504,6 +504,11 @@ impl GuestMemory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryAccess for GuestMemory {
|
impl MemoryAccess for GuestMemory {
|
||||||
|
#[inline]
|
||||||
|
fn is_mmio(&self, addr: u32) -> bool {
|
||||||
|
self.find_mmio(addr).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
// Tier-3 perf: `#[inline]` on the hot read/write paths lets LLVM
|
// Tier-3 perf: `#[inline]` on the hot read/write paths lets LLVM
|
||||||
// fold the MMIO + mapping checks into the interpreter's load/store
|
// fold the MMIO + mapping checks into the interpreter's load/store
|
||||||
// handlers, hoisting the "not-MMIO, mapped" branch out of the loop
|
// handlers, hoisting the "not-MMIO, mapped" branch out of the loop
|
||||||
|
|||||||
Reference in New Issue
Block a user