|
|
|
|
@@ -86,15 +86,30 @@ 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(),
|
|
|
|
|
// A superblock reads `*mmio_count` at every block boundary; point
|
|
|
|
|
// it at a stable zero so an accidental call under the overlay
|
|
|
|
|
// (chaining is disabled there, but be defensive) reads 0 and
|
|
|
|
|
// never trips the MMIO stop instead of dereferencing null.
|
|
|
|
|
mmio_count: &MMIO_ZERO as *const u64,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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;
|
|
|
|
|
/// Stable zero used as the `mmio_count` target when no flat mapping is exposed
|
|
|
|
|
/// (see [`MemEnv::from_fast`]).
|
|
|
|
|
static MMIO_ZERO: u64 = 0;
|
|
|
|
|
|
|
|
|
|
/// A compiled block (or superblock): a native entry point valid for as long as
|
|
|
|
|
/// the owning [`Jit`]'s module lives (the module owns the executable memory).
|
|
|
|
|
///
|
|
|
|
|
/// The third argument is the **remaining superblock instruction budget** — how
|
|
|
|
|
/// many more guest instructions this slot-visit may retire. A single-block
|
|
|
|
|
/// compilation ignores it; a chained superblock ([`Jit::compile_chain`]) checks
|
|
|
|
|
/// it at every internal block boundary and stops the chain (returns to the Rust
|
|
|
|
|
/// runner) once the cumulative count reaches it, reproducing the runner's
|
|
|
|
|
/// `total_executed >= budget` stop exactly.
|
|
|
|
|
pub type CompiledFn = extern "C" fn(*mut PpcContext, *const MemEnv, u64) -> u32;
|
|
|
|
|
|
|
|
|
|
// ---- memory trampolines ---------------------------------------------------
|
|
|
|
|
//
|
|
|
|
|
@@ -219,6 +234,13 @@ pub struct JitCache {
|
|
|
|
|
/// their `MemEnv`. `fast_mem_init` guards first use.
|
|
|
|
|
fast_mem: Option<FastMem>,
|
|
|
|
|
fast_mem_init: bool,
|
|
|
|
|
/// Superblock-chaining configuration, set once per run via
|
|
|
|
|
/// [`Self::set_chain_config`]. `configured` guards first use so the
|
|
|
|
|
/// scheduler can install it lazily on the first block. When
|
|
|
|
|
/// `chain.enabled`, [`Self::get_or_compile`] builds superblocks (native
|
|
|
|
|
/// block-to-block chaining); otherwise it compiles one block at a time.
|
|
|
|
|
chain: ChainConfig,
|
|
|
|
|
configured: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for JitCache {
|
|
|
|
|
@@ -236,9 +258,26 @@ impl JitCache {
|
|
|
|
|
slots: slots.into_boxed_slice(),
|
|
|
|
|
fast_mem: None,
|
|
|
|
|
fast_mem_init: false,
|
|
|
|
|
chain: ChainConfig::default(),
|
|
|
|
|
configured: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// True once [`Self::set_chain_config`] has installed the run's chain config
|
|
|
|
|
/// (the scheduler calls it lazily on the first superblock).
|
|
|
|
|
pub fn is_configured(&self) -> bool {
|
|
|
|
|
self.configured
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Install the superblock-chaining configuration for this run. Called once
|
|
|
|
|
/// (idempotently guarded by [`Self::is_configured`]); the gate is stable for
|
|
|
|
|
/// a run (env + startup-armed probes), so a compiled block's chain/no-chain
|
|
|
|
|
/// shape never has to change after it is cached.
|
|
|
|
|
pub fn set_chain_config(&mut self, cfg: ChainConfig) {
|
|
|
|
|
self.chain = cfg;
|
|
|
|
|
self.configured = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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`).
|
|
|
|
|
@@ -254,14 +293,32 @@ impl JitCache {
|
|
|
|
|
/// visit. `None` means the block is uncovered (or compilation failed) and
|
|
|
|
|
/// the caller must interpret it. The `None` verdict is cached per
|
|
|
|
|
/// `(start_pc, page_version)` so uncovered blocks aren't re-attempted.
|
|
|
|
|
pub fn get_or_compile(&mut self, block: &DecodedBlock) -> Option<CompiledFn> {
|
|
|
|
|
///
|
|
|
|
|
/// When chaining is enabled ([`Self::set_chain_config`]) the compiled entry
|
|
|
|
|
/// is a **superblock** rooted at `block` — it may run several same-page
|
|
|
|
|
/// blocks natively before returning. `mem` is the live memory, used to build
|
|
|
|
|
/// the transient successor blocks of the chain (unused on the single-block
|
|
|
|
|
/// path). The superblock is keyed on the entry `(start_pc, page_version)`,
|
|
|
|
|
/// which is sound because every chained block is on the entry's page.
|
|
|
|
|
pub fn get_or_compile(
|
|
|
|
|
&mut self,
|
|
|
|
|
block: &DecodedBlock,
|
|
|
|
|
mem: &dyn MemoryAccess,
|
|
|
|
|
) -> Option<CompiledFn> {
|
|
|
|
|
let idx = ((block.start_pc >> 2) & JIT_CACHE_MASK) as usize;
|
|
|
|
|
if let Some(slot) = &self.slots[idx] {
|
|
|
|
|
if slot.start_pc == block.start_pc && slot.page_version == block.page_version {
|
|
|
|
|
return slot.func;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let func = self.jit.as_mut().and_then(|j| j.compile(block));
|
|
|
|
|
let cfg = self.chain;
|
|
|
|
|
let func = self.jit.as_mut().and_then(|j| {
|
|
|
|
|
if cfg.enabled {
|
|
|
|
|
j.compile_chain(block, mem, cfg)
|
|
|
|
|
} else {
|
|
|
|
|
j.compile(block)
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
self.slots[idx] = Some(CompiledSlot {
|
|
|
|
|
start_pc: block.start_pc,
|
|
|
|
|
page_version: block.page_version,
|
|
|
|
|
@@ -427,32 +484,19 @@ impl Jit {
|
|
|
|
|
Ok(Self { module, ctx, fbctx: FunctionBuilderContext::new(), tramp_ids })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Compile `block` to native code, or return `None` if any opcode is
|
|
|
|
|
/// uncovered (caller interprets the block).
|
|
|
|
|
/// Compile a single `block` to native code, or return `None` if any opcode
|
|
|
|
|
/// is uncovered (caller interprets the block). The compiled function ignores
|
|
|
|
|
/// its `remaining_budget` argument (a single block never chains).
|
|
|
|
|
pub fn compile(&mut self, block: &DecodedBlock) -> Option<CompiledFn> {
|
|
|
|
|
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));
|
|
|
|
|
|
|
|
|
|
// Import the trampolines into this function (before the builder borrows
|
|
|
|
|
// `ctx.func`). `declare_func_in_func` borrows the module immutably and
|
|
|
|
|
// `ctx.func` mutably — disjoint fields, so no borrow conflict.
|
|
|
|
|
let tr = Trampolines {
|
|
|
|
|
read8: self.module.declare_func_in_func(self.tramp_ids.read8, &mut self.ctx.func),
|
|
|
|
|
read16: self.module.declare_func_in_func(self.tramp_ids.read16, &mut self.ctx.func),
|
|
|
|
|
read32: self.module.declare_func_in_func(self.tramp_ids.read32, &mut self.ctx.func),
|
|
|
|
|
read64: self.module.declare_func_in_func(self.tramp_ids.read64, &mut self.ctx.func),
|
|
|
|
|
write8: self.module.declare_func_in_func(self.tramp_ids.write8, &mut self.ctx.func),
|
|
|
|
|
write16: self.module.declare_func_in_func(self.tramp_ids.write16, &mut self.ctx.func),
|
|
|
|
|
write32: self.module.declare_func_in_func(self.tramp_ids.write32, &mut self.ctx.func),
|
|
|
|
|
write64: self.module.declare_func_in_func(self.tramp_ids.write64, &mut self.ctx.func),
|
|
|
|
|
interp: self.module.declare_func_in_func(self.tramp_ids.interp, &mut self.ctx.func),
|
|
|
|
|
};
|
|
|
|
|
// Every compiled entry shares the superblock ABI `(ctx, mem_env,
|
|
|
|
|
// remaining) -> i32` so `CompiledFn` is a single type; a single block
|
|
|
|
|
// simply ignores the `remaining` param.
|
|
|
|
|
self.begin_func_chain(ptr);
|
|
|
|
|
let tr = self.import_trampolines();
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let mut b = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbctx);
|
|
|
|
|
@@ -465,31 +509,177 @@ impl Jit {
|
|
|
|
|
memenv: b.block_params(entry)[1],
|
|
|
|
|
tr,
|
|
|
|
|
};
|
|
|
|
|
let ctxp = ec.ctxp;
|
|
|
|
|
|
|
|
|
|
for instr in &block.instrs {
|
|
|
|
|
emit_op(&mut b, &ec, instr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A branch terminator writes `pc` itself (to its computed target);
|
|
|
|
|
// otherwise the block is straight-line and `pc` advances to
|
|
|
|
|
// `end_pc`. `writes_pc` is only ever true for the last instruction
|
|
|
|
|
// (branches terminate the block), so testing it is sufficient.
|
|
|
|
|
if !block.instrs.last().is_some_and(writes_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);
|
|
|
|
|
|
|
|
|
|
emit_node_body(&mut b, &ec, block);
|
|
|
|
|
let ret = b.ins().iconst(types::I32, RET_CONTINUE as i64);
|
|
|
|
|
b.ins().return_(&[ret]);
|
|
|
|
|
b.finalize();
|
|
|
|
|
}
|
|
|
|
|
self.finish_func()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Compile a **superblock** rooted at `entry`: the chain of same-page,
|
|
|
|
|
/// fully-covered blocks reachable by following static (unconditional) branch
|
|
|
|
|
/// targets and fall-throughs, lowered into ONE native function whose
|
|
|
|
|
/// internal block-to-block edges are direct machine jumps — eliminating the
|
|
|
|
|
/// per-block return to [`crate::recompiler::run_block`] and the Rust runner's
|
|
|
|
|
/// dispatch tax for the whole chain.
|
|
|
|
|
///
|
|
|
|
|
/// Correctness contract (must exactly mirror `run_superblock`'s chain loop):
|
|
|
|
|
/// * each block body is the *same* IR the single-block path emits (already
|
|
|
|
|
/// validated bit-exact by the diff harness), so only the chain glue is
|
|
|
|
|
/// new;
|
|
|
|
|
/// * at every internal boundary the function re-checks the two runtime
|
|
|
|
|
/// stop-conditions — an MMIO touch (`*mmio_count` advanced) and the
|
|
|
|
|
/// instruction budget (`cumulative >= remaining_budget`) — and stops the
|
|
|
|
|
/// chain (returns `Continue` with `pc` already at the next block) exactly
|
|
|
|
|
/// where the Rust loop would;
|
|
|
|
|
/// * the chain only ever follows edges resolved to be same-page, outside
|
|
|
|
|
/// the thunk band, and fully covered — every other successor stops the
|
|
|
|
|
/// chain, so the runner takes over from a clean boundary.
|
|
|
|
|
///
|
|
|
|
|
/// Being *more* conservative than the runner (stopping the chain earlier) is
|
|
|
|
|
/// always safe: `pc`/`cycle_count`/`timebase` are correct at the stop, the
|
|
|
|
|
/// return is `Continue`, and the runner re-dispatches from `pc` — identical
|
|
|
|
|
/// to never having chained past that point. Returns `None` (caller falls back
|
|
|
|
|
/// to single-block) if `entry` is uncovered.
|
|
|
|
|
///
|
|
|
|
|
/// **B1 scope:** forward-linear only — chains through unconditional `bx` and
|
|
|
|
|
/// fall-throughs. A conditional `bcx` (two-way) or a dynamic `bclrx`/`bcctrx`
|
|
|
|
|
/// ends the chain, as does a successor already in the chain (a back-edge /
|
|
|
|
|
/// loop). B2 adds two-way + loop CFGs.
|
|
|
|
|
pub fn compile_chain(
|
|
|
|
|
&mut self,
|
|
|
|
|
entry: &DecodedBlock,
|
|
|
|
|
mem: &dyn MemoryAccess,
|
|
|
|
|
cfg: ChainConfig,
|
|
|
|
|
) -> Option<CompiledFn> {
|
|
|
|
|
if !block_covered(entry) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let nodes = enumerate_chain(entry, mem, cfg);
|
|
|
|
|
// A degenerate one-node chain is just a single block — emit that (cheaper
|
|
|
|
|
// IR, no boundary machinery) so we don't pay for chaining we can't use.
|
|
|
|
|
if nodes.len() < 2 {
|
|
|
|
|
return self.compile(entry);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let ptr = self.module.target_config().pointer_type();
|
|
|
|
|
self.begin_func_chain(ptr);
|
|
|
|
|
let tr = self.import_trampolines();
|
|
|
|
|
|
|
|
|
|
{
|
|
|
|
|
let mut b = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbctx);
|
|
|
|
|
let ib = b.create_block();
|
|
|
|
|
b.append_block_params_for_function_params(ib);
|
|
|
|
|
b.switch_to_block(ib);
|
|
|
|
|
let ctxp = b.block_params(ib)[0];
|
|
|
|
|
let memenv = b.block_params(ib)[1];
|
|
|
|
|
let remaining = b.block_params(ib)[2]; // I64 remaining budget
|
|
|
|
|
let ec = EmitCtx { ctxp, memenv, tr };
|
|
|
|
|
|
|
|
|
|
// Sample the MMIO counter once at entry. The entry block dominates
|
|
|
|
|
// every node, so this value is usable at all boundaries. Because the
|
|
|
|
|
// chain stops at the first block that touches MMIO, "count now != the
|
|
|
|
|
// entry count" at a boundary is exactly "some block up to here touched
|
|
|
|
|
// MMIO" — equivalent to the runner's per-block `!= mmio_before` check.
|
|
|
|
|
let mmio_ptr = load_env_i64(&mut b, &ec, offset_of!(MemEnv, mmio_count));
|
|
|
|
|
let mmio_entry =
|
|
|
|
|
b.ins().load(types::I64, MemFlags::trusted(), mmio_ptr, 0);
|
|
|
|
|
|
|
|
|
|
// One IR block per node plus a shared exit.
|
|
|
|
|
let node_blocks: Vec<_> = (0..nodes.len()).map(|_| b.create_block()).collect();
|
|
|
|
|
let exit = b.create_block();
|
|
|
|
|
|
|
|
|
|
b.ins().jump(node_blocks[0], &[]);
|
|
|
|
|
|
|
|
|
|
for (i, node) in nodes.iter().enumerate() {
|
|
|
|
|
b.switch_to_block(node_blocks[i]);
|
|
|
|
|
emit_node_body(&mut b, &ec, &node.block);
|
|
|
|
|
|
|
|
|
|
if i + 1 == nodes.len() {
|
|
|
|
|
// Last node: no chainable successor — `pc` is set by the
|
|
|
|
|
// body; hand back to the runner.
|
|
|
|
|
b.ins().jump(exit, &[]);
|
|
|
|
|
} else {
|
|
|
|
|
// Boundary: stop the chain on a budget spend, and — only when
|
|
|
|
|
// this block could have touched MMIO — on an MMIO touch;
|
|
|
|
|
// otherwise jump straight into the next block's code.
|
|
|
|
|
let cum = b.ins().iconst(types::I64, node.cumulative_end as i64);
|
|
|
|
|
let over =
|
|
|
|
|
b.ins().icmp(IntCC::UnsignedGreaterThanOrEqual, cum, remaining);
|
|
|
|
|
// A block with no load/store cannot advance `mmio_count`, so
|
|
|
|
|
// the MMIO boundary check is provably redundant there. Since
|
|
|
|
|
// the chain has touched no MMIO up to this point (else an
|
|
|
|
|
// earlier boundary would have exited), skipping it here keeps
|
|
|
|
|
// the "no MMIO so far" invariant intact — and drops a memory
|
|
|
|
|
// load + compare from every pure-ALU boundary.
|
|
|
|
|
let touches_mem = node
|
|
|
|
|
.block
|
|
|
|
|
.instrs
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|i| i.opcode.is_load() || i.opcode.is_store());
|
|
|
|
|
let stop = if touches_mem {
|
|
|
|
|
let mmio_now =
|
|
|
|
|
b.ins().load(types::I64, MemFlags::trusted(), mmio_ptr, 0);
|
|
|
|
|
let mmio_changed =
|
|
|
|
|
b.ins().icmp(IntCC::NotEqual, mmio_now, mmio_entry);
|
|
|
|
|
b.ins().bor(mmio_changed, over)
|
|
|
|
|
} else {
|
|
|
|
|
over
|
|
|
|
|
};
|
|
|
|
|
b.ins().brif(stop, exit, &[], node_blocks[i + 1], &[]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b.switch_to_block(exit);
|
|
|
|
|
let ret = b.ins().iconst(types::I32, RET_CONTINUE as i64);
|
|
|
|
|
b.ins().return_(&[ret]);
|
|
|
|
|
|
|
|
|
|
// All predecessor edges have been emitted (forward-linear: node i is
|
|
|
|
|
// reached only from node i-1 or the entry; exit from every boundary).
|
|
|
|
|
b.seal_all_blocks();
|
|
|
|
|
b.finalize();
|
|
|
|
|
}
|
|
|
|
|
self.finish_func()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Begin a fresh single-block function signature: `(ctx, mem_env) -> i32`.
|
|
|
|
|
fn begin_func(&mut self, ptr: types::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));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Begin a superblock function signature: `(ctx, mem_env, remaining) -> i32`.
|
|
|
|
|
fn begin_func_chain(&mut self, ptr: types::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.params.push(AbiParam::new(types::I64)); // remaining budget
|
|
|
|
|
self.ctx.func.signature.returns.push(AbiParam::new(types::I32));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Import the trampolines into the function being built. `declare_func_in_func`
|
|
|
|
|
/// borrows the module immutably and `ctx.func` mutably — disjoint fields, so
|
|
|
|
|
/// no borrow conflict.
|
|
|
|
|
fn import_trampolines(&mut self) -> Trampolines {
|
|
|
|
|
Trampolines {
|
|
|
|
|
read8: self.module.declare_func_in_func(self.tramp_ids.read8, &mut self.ctx.func),
|
|
|
|
|
read16: self.module.declare_func_in_func(self.tramp_ids.read16, &mut self.ctx.func),
|
|
|
|
|
read32: self.module.declare_func_in_func(self.tramp_ids.read32, &mut self.ctx.func),
|
|
|
|
|
read64: self.module.declare_func_in_func(self.tramp_ids.read64, &mut self.ctx.func),
|
|
|
|
|
write8: self.module.declare_func_in_func(self.tramp_ids.write8, &mut self.ctx.func),
|
|
|
|
|
write16: self.module.declare_func_in_func(self.tramp_ids.write16, &mut self.ctx.func),
|
|
|
|
|
write32: self.module.declare_func_in_func(self.tramp_ids.write32, &mut self.ctx.func),
|
|
|
|
|
write64: self.module.declare_func_in_func(self.tramp_ids.write64, &mut self.ctx.func),
|
|
|
|
|
interp: self.module.declare_func_in_func(self.tramp_ids.interp, &mut self.ctx.func),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Define + finalize the function currently in `self.ctx` and return its
|
|
|
|
|
/// native entry point.
|
|
|
|
|
fn finish_func(&mut self) -> Option<CompiledFn> {
|
|
|
|
|
let id = self
|
|
|
|
|
.module
|
|
|
|
|
.declare_anonymous_function(&self.ctx.func.signature)
|
|
|
|
|
@@ -504,6 +694,131 @@ impl Jit {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Emit the IR for one block's body: every covered instruction, then the `pc`
|
|
|
|
|
/// epilogue (`end_pc` for a straight-line block; a branch writes its own `pc`)
|
|
|
|
|
/// and the per-instruction `cycle_count`/`timebase` bump. Shared by the single-
|
|
|
|
|
/// block ([`Jit::compile`]) and superblock ([`Jit::compile_chain`]) paths so a
|
|
|
|
|
/// chained block is byte-identical to a standalone one.
|
|
|
|
|
fn emit_node_body(b: &mut FunctionBuilder, ec: &EmitCtx, block: &DecodedBlock) {
|
|
|
|
|
let ctxp = ec.ctxp;
|
|
|
|
|
for instr in &block.instrs {
|
|
|
|
|
emit_op(b, ec, instr);
|
|
|
|
|
}
|
|
|
|
|
// A branch terminator writes `pc` itself (to its computed target); otherwise
|
|
|
|
|
// the block is straight-line and `pc` advances to `end_pc`. `writes_pc` is
|
|
|
|
|
// only ever true for the last instruction, so testing it is sufficient.
|
|
|
|
|
if !block.instrs.last().is_some_and(writes_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(b, ctxp, offset_of!(PpcContext, cycle_count), n);
|
|
|
|
|
bump_u64(b, ctxp, offset_of!(PpcContext, timebase), n);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---- superblock chain enumeration ----------------------------------------
|
|
|
|
|
|
|
|
|
|
/// Guest 4 KiB page mask. A superblock stays within the entry block's page so
|
|
|
|
|
/// the entry's `page_version` is a sound cache key for the whole chain (a
|
|
|
|
|
/// cross-page rewrite that should invalidate it would land on a *different*
|
|
|
|
|
/// page's version, which we would not be keyed on — hence same-page only).
|
|
|
|
|
const CHAIN_PAGE_MASK: u32 = !0xFFF;
|
|
|
|
|
|
|
|
|
|
/// Cap on blocks per superblock. The runtime budget (≤192 instrs) bounds how
|
|
|
|
|
/// many blocks actually *run*, but the static chain could be longer; stop
|
|
|
|
|
/// enumerating once the cumulative count reaches the max budget (later nodes can
|
|
|
|
|
/// never be reached) and hard-cap the node count so a pathological page can't
|
|
|
|
|
/// produce an enormous function.
|
|
|
|
|
const MAX_CHAIN_NODES: usize = 64;
|
|
|
|
|
|
|
|
|
|
/// Configuration for superblock chaining, set once per run from the scheduler
|
|
|
|
|
/// (see [`JitCache::set_chain_config`]).
|
|
|
|
|
#[derive(Clone, Copy, Default)]
|
|
|
|
|
pub struct ChainConfig {
|
|
|
|
|
/// Master gate: chain only when enabled (`XENIA_JIT_CHAIN`, and never under
|
|
|
|
|
/// the diff harness or with diagnostic probes / mem-watch armed).
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
/// The maximum superblock instruction budget (the runner's
|
|
|
|
|
/// `SUPERBLOCK_INSTR_BUDGET`) — bounds static enumeration.
|
|
|
|
|
pub max_budget: u64,
|
|
|
|
|
/// Import-thunk address band `(lo, hi)` inclusive; the chain never follows a
|
|
|
|
|
/// successor into it (those PCs need the full worker-prologue dispatch).
|
|
|
|
|
pub thunk_band: Option<(u32, u32)>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// One node of an enumerated superblock chain.
|
|
|
|
|
struct ChainNode {
|
|
|
|
|
block: DecodedBlock,
|
|
|
|
|
/// Total guest instructions from the entry through the end of this block
|
|
|
|
|
/// (forward-linear ⇒ a single path ⇒ a compile-time constant). Compared
|
|
|
|
|
/// against the runtime `remaining_budget` at this node's boundary.
|
|
|
|
|
cumulative_end: u64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The static (compile-time-known) successor PC of `block`, or `None` if the
|
|
|
|
|
/// successor is dynamic / this is a chain-terminating branch:
|
|
|
|
|
/// * not a terminator ⇒ fall-through to `end_pc`;
|
|
|
|
|
/// * `bx` (unconditional, incl. `bl`) ⇒ its immediate target;
|
|
|
|
|
/// * `bcx` (conditional) / `bclrx` / `bcctrx` / anything else ⇒ `None` (B1).
|
|
|
|
|
fn static_successor(block: &DecodedBlock) -> Option<u32> {
|
|
|
|
|
let last = block.instrs.last()?;
|
|
|
|
|
if !last.opcode.terminates_block() {
|
|
|
|
|
return Some(block.end_pc);
|
|
|
|
|
}
|
|
|
|
|
match last.opcode {
|
|
|
|
|
PpcOpcode::bx => Some(if last.aa() {
|
|
|
|
|
last.li() as u32
|
|
|
|
|
} else {
|
|
|
|
|
last.addr.wrapping_add(last.li() as u32)
|
|
|
|
|
}),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Walk the forward-linear chain from `entry`, returning the nodes to compile
|
|
|
|
|
/// (node 0 = `entry`). Stops at the first successor that is dynamic, off-page,
|
|
|
|
|
/// in the thunk band, uncovered, already-visited (a loop back-edge — deferred to
|
|
|
|
|
/// B2), or once the cumulative count / node cap is hit. `entry` is assumed
|
|
|
|
|
/// covered (the caller checks).
|
|
|
|
|
fn enumerate_chain(entry: &DecodedBlock, mem: &dyn MemoryAccess, cfg: ChainConfig) -> Vec<ChainNode> {
|
|
|
|
|
use crate::block_cache::build_block;
|
|
|
|
|
let page = entry.start_pc & CHAIN_PAGE_MASK;
|
|
|
|
|
let mut nodes: Vec<ChainNode> = Vec::new();
|
|
|
|
|
let mut seen: std::collections::HashSet<u32> = std::collections::HashSet::new();
|
|
|
|
|
let mut cum: u64 = 0;
|
|
|
|
|
let mut pc = entry.start_pc;
|
|
|
|
|
|
|
|
|
|
loop {
|
|
|
|
|
// Rebuild the block from `mem` (the caller's `entry` is only used for its
|
|
|
|
|
// start PC / covered check; every node is freshly, identically built).
|
|
|
|
|
let block = build_block(pc, mem, mem.page_version(pc));
|
|
|
|
|
cum += block.instrs.len() as u64;
|
|
|
|
|
seen.insert(pc);
|
|
|
|
|
|
|
|
|
|
// Resolve whether we may chain into a next node.
|
|
|
|
|
let chain_to = if nodes.len() + 1 >= MAX_CHAIN_NODES || cum >= cfg.max_budget {
|
|
|
|
|
// Later nodes can't be reached within budget / node cap.
|
|
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
static_successor(&block).filter(|&s| {
|
|
|
|
|
(s & CHAIN_PAGE_MASK) == page
|
|
|
|
|
&& !cfg.thunk_band.is_some_and(|(lo, hi)| s >= lo && s <= hi)
|
|
|
|
|
&& !seen.contains(&s)
|
|
|
|
|
&& block_covered(&build_block(s, mem, mem.page_version(s)))
|
|
|
|
|
})
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
nodes.push(ChainNode { block, cumulative_end: cum });
|
|
|
|
|
|
|
|
|
|
match chain_to {
|
|
|
|
|
Some(next) => pc = next,
|
|
|
|
|
None => break,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
nodes
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
|
fn off(o: usize) -> i32 {
|
|
|
|
|
o as i32
|
|
|
|
|
@@ -1347,7 +1662,7 @@ mod tests {
|
|
|
|
|
let mut jit_ctx = PpcContext::new();
|
|
|
|
|
jit_ctx.pc = 0x1000;
|
|
|
|
|
let env = MemEnv::new(&mem);
|
|
|
|
|
let r = f(&mut jit_ctx as *mut _, &env as *const _);
|
|
|
|
|
let r = f(&mut jit_ctx as *mut _, &env as *const _, u64::MAX);
|
|
|
|
|
|
|
|
|
|
assert_eq!(r, RET_CONTINUE);
|
|
|
|
|
assert_eq!(jit_ctx.gpr[3], ref_ctx.gpr[3], "r3 mismatch");
|
|
|
|
|
@@ -1401,7 +1716,7 @@ mod tests {
|
|
|
|
|
let mut jit_ctx = PpcContext::new();
|
|
|
|
|
jit_ctx.pc = 0x41000;
|
|
|
|
|
jit_ctx.gpr[4] = 0x40000;
|
|
|
|
|
f(&mut jit_ctx as *mut _, &env as *const _);
|
|
|
|
|
f(&mut jit_ctx as *mut _, &env as *const _, u64::MAX);
|
|
|
|
|
assert_eq!(jit_ctx.gpr[3], 0xDEAD_BEEF, "fast-path load value");
|
|
|
|
|
assert_eq!(jit_ctx.gpr[3], ref_ctx.gpr[3], "fast-path vs interpreter");
|
|
|
|
|
|
|
|
|
|
@@ -1414,7 +1729,101 @@ mod tests {
|
|
|
|
|
let mut jit2 = PpcContext::new();
|
|
|
|
|
jit2.pc = 0x41000;
|
|
|
|
|
jit2.gpr[4] = 0x9000_0000;
|
|
|
|
|
f(&mut jit2 as *mut _, &env as *const _);
|
|
|
|
|
f(&mut jit2 as *mut _, &env as *const _, u64::MAX);
|
|
|
|
|
assert_eq!(jit2.gpr[3], ref2.gpr[3], "slow-path (unmapped) vs interpreter");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// b <target> (relative, AA=0, LK=0) = (18<<26) | (LI & 0x03FF_FFFC)
|
|
|
|
|
fn b_rel(from: u32, to: u32) -> u32 {
|
|
|
|
|
let li = to.wrapping_sub(from) & 0x03FF_FFFC;
|
|
|
|
|
(18 << 26) | li
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A forward-linear superblock (three blocks joined by unconditional `b`
|
|
|
|
|
/// branches, all on one page) must leave the context bit-identical to the
|
|
|
|
|
/// interpreter stepping the same three blocks in sequence — proving the
|
|
|
|
|
/// chain glue (successor resolution, pc/cycle accounting, straight-line
|
|
|
|
|
/// dominance of the entry-sampled state) matches, not just the op bodies.
|
|
|
|
|
#[test]
|
|
|
|
|
fn jit_chain_matches_interpreter_over_blocks() {
|
|
|
|
|
use xenia_memory::page_table::MemoryProtect;
|
|
|
|
|
use xenia_memory::GuestMemory;
|
|
|
|
|
|
|
|
|
|
let mem = GuestMemory::new().expect("reserve 4GB");
|
|
|
|
|
mem.alloc(0x50000, 0x1000, MemoryProtect::READ | MemoryProtect::WRITE).unwrap();
|
|
|
|
|
|
|
|
|
|
// Block A @0x50000: addi r3,r3,1 ; b 0x50100
|
|
|
|
|
mem.write_u32(0x50000, addi(3, 3, 1));
|
|
|
|
|
mem.write_u32(0x50004, b_rel(0x50004, 0x50100));
|
|
|
|
|
// Block B @0x50100: addi r3,r3,10 ; b 0x50200
|
|
|
|
|
mem.write_u32(0x50100, addi(3, 3, 10));
|
|
|
|
|
mem.write_u32(0x50104, b_rel(0x50104, 0x50200));
|
|
|
|
|
// Block C @0x50200: addi r3,r3,100 ; b 0x50300. 0x50300 is left zeroed →
|
|
|
|
|
// decodes to `Invalid` (uncovered) → the chain cleanly stops after C
|
|
|
|
|
// (its unconditional target isn't a compilable block).
|
|
|
|
|
mem.write_u32(0x50200, addi(3, 3, 100));
|
|
|
|
|
mem.write_u32(0x50204, b_rel(0x50204, 0x50300));
|
|
|
|
|
|
|
|
|
|
let cfg = ChainConfig { enabled: true, max_budget: 192, thunk_band: None };
|
|
|
|
|
|
|
|
|
|
let entry = crate::block_cache::build_block(0x50000, &mem, mem.page_version(0x50000));
|
|
|
|
|
let mut jit = Jit::new().expect("jit init");
|
|
|
|
|
let f = jit.compile_chain(&entry, &mem, cfg).expect("chain compile");
|
|
|
|
|
let env = MemEnv::new(&mem);
|
|
|
|
|
|
|
|
|
|
// JIT superblock: one call runs A→B→C, then stops (C's target is
|
|
|
|
|
// uncovered). pc must land at C's target (0x50300), r3 = 111.
|
|
|
|
|
let mut jit_ctx = PpcContext::new();
|
|
|
|
|
jit_ctx.pc = 0x50000;
|
|
|
|
|
f(&mut jit_ctx as *mut _, &env as *const _, 192);
|
|
|
|
|
|
|
|
|
|
// Interpreter reference: step the same three blocks in sequence.
|
|
|
|
|
let mut ref_ctx = PpcContext::new();
|
|
|
|
|
ref_ctx.pc = 0x50000;
|
|
|
|
|
for pc in [0x50000u32, 0x50100, 0x50200] {
|
|
|
|
|
let blk = crate::block_cache::build_block(pc, &mem, mem.page_version(pc));
|
|
|
|
|
let _ = crate::interpreter::step_block(&mut ref_ctx, &mem, &blk);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assert_eq!(jit_ctx.gpr[3], 111, "r3 after A+B+C");
|
|
|
|
|
assert_eq!(jit_ctx.gpr[3], ref_ctx.gpr[3], "r3 chain vs interpreter");
|
|
|
|
|
assert_eq!(jit_ctx.pc, 0x50300, "pc left at C's (uncovered) target");
|
|
|
|
|
assert_eq!(jit_ctx.pc, ref_ctx.pc, "pc chain vs interpreter");
|
|
|
|
|
assert_eq!(jit_ctx.cycle_count, ref_ctx.cycle_count, "cycles (3 blocks)");
|
|
|
|
|
assert_eq!(jit_ctx.cycle_count, 6, "6 instrs retired across the chain");
|
|
|
|
|
assert_eq!(jit_ctx.timebase, ref_ctx.timebase, "timebase");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The budget must cut the chain at exactly the runner's boundary: with a
|
|
|
|
|
/// budget of 2 instructions, only block A (2 instrs) runs; the chain stops
|
|
|
|
|
/// before B, leaving pc at B's entry.
|
|
|
|
|
#[test]
|
|
|
|
|
fn jit_chain_stops_at_budget() {
|
|
|
|
|
use xenia_memory::page_table::MemoryProtect;
|
|
|
|
|
use xenia_memory::GuestMemory;
|
|
|
|
|
|
|
|
|
|
let mem = GuestMemory::new().expect("reserve 4GB");
|
|
|
|
|
mem.alloc(0x60000, 0x1000, MemoryProtect::READ | MemoryProtect::WRITE).unwrap();
|
|
|
|
|
// A @0x60000: addi r3,r3,1 ; b 0x60100 (2 instrs, cumulative=2)
|
|
|
|
|
mem.write_u32(0x60000, addi(3, 3, 1));
|
|
|
|
|
mem.write_u32(0x60004, b_rel(0x60004, 0x60100));
|
|
|
|
|
// B @0x60100: addi r3,r3,10 ; b .
|
|
|
|
|
mem.write_u32(0x60100, addi(3, 3, 10));
|
|
|
|
|
mem.write_u32(0x60104, b_self());
|
|
|
|
|
|
|
|
|
|
let cfg = ChainConfig { enabled: true, max_budget: 192, thunk_band: None };
|
|
|
|
|
let entry = crate::block_cache::build_block(0x60000, &mem, mem.page_version(0x60000));
|
|
|
|
|
let mut jit = Jit::new().expect("jit init");
|
|
|
|
|
let f = jit.compile_chain(&entry, &mem, cfg).expect("chain compile");
|
|
|
|
|
let env = MemEnv::new(&mem);
|
|
|
|
|
|
|
|
|
|
// remaining budget = 2 → cumulative_end[A]=2 >= 2 → stop after A.
|
|
|
|
|
let mut jit_ctx = PpcContext::new();
|
|
|
|
|
jit_ctx.pc = 0x60000;
|
|
|
|
|
f(&mut jit_ctx as *mut _, &env as *const _, 2);
|
|
|
|
|
|
|
|
|
|
assert_eq!(jit_ctx.gpr[3], 1, "only A ran (B did not add 10)");
|
|
|
|
|
assert_eq!(jit_ctx.pc, 0x60100, "pc left at B's entry (chain cut at budget)");
|
|
|
|
|
assert_eq!(jit_ctx.cycle_count, 2, "exactly A's 2 instrs retired");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|