Commit Graph

6 Commits

Author SHA1 Message Date
MechaCat02
2c17a511f7 [iterate-4A] jit: native superblock chaining B1 (bx + fall-through), −6.4% wall
Increment B1 of block-linking: compile a chain of same-page, fully-covered
blocks into ONE Cranelift function whose internal block-to-block edges are
direct machine jumps, eliminating the per-block return to run_block and the
run_superblock dispatch tax for the chain. Env-gated XENIA_JIT_CHAIN (off by
default), and additionally disabled under the diff harness or with diagnostic
probes / mem-watch armed (chaining runs several blocks natively without the
per-block-entry observation those need, so they are mutually exclusive).

Mechanism (cranelift-jit can't patch finalized code, so no QEMU-style TB
chaining): SUPERBLOCK COMPILATION. compile_chain enumerates the forward-linear
chain from the entry — following static bx targets and fall-throughs, all
same-page (so the entry page_version is a sound cache key), outside the thunk
band, and covered — and emits one node per block plus a shared exit. At every
internal boundary it re-checks the two runtime stop-conditions exactly as the
Rust runner does: an MMIO touch (*mmio_count advanced vs the entry sample) and
the instruction budget (cumulative >= remaining, passed as a runtime param);
either stops the chain with pc/cycle/timebase already correct, and the runner
re-dispatches from the clean boundary. B1 scope is forward-linear: a bx or
fall-through continues; a conditional bcx, dynamic bclrx/bcctrx, off-page /
thunk / uncovered / already-visited successor ends the chain (B2 adds bcx
two-way + loop back-edges). Being *more* conservative (stopping earlier) is
always safe.

Correctness rests on composition: each node body is the same emit_node_body IR
the single-block path emits (already diff-validated bit-exact), so only the
chain glue is new — validated by two new unit tests (multi-block chain vs
interpreter over the same PCs; budget cut at the exact boundary) plus e2e.
Boundary MMIO check is skipped for load/store-free blocks (a pure-ALU block
can't advance mmio_count), dropping a memory load+compare per ALU boundary.

Plumbing: build_block exposed (xenia-cpu); KernelState::thunk_band getter;
MemEnv mmio_count null-safety (points at a static zero when no flat mapping);
CompiledFn gains a remaining-budget param (single-block ignores it); all
compiled entries share the (ctx, mem_env, remaining) ABI.

Validation: diff MISMATCHES=0 over 25.0M blocks (single-block op bodies
unbroken by the emit_node_body refactor); XENIA_JIT_CHAIN=1 movie plays,
tid25 resumes, source-read=12; fair perf (RUST_LOG=warn) interp 44.1s vs
chain 41.3s = −6.4% wall — run_superblock "other" 30.4%->26.2%, run_block
calls 145.7M->116.2M (1.25x block merge), step_block at parity (100.6->102.4
MIPS; the earlier "slower" reading was cranelift IR-logging inside the STEP
timer). Uncommitted probe knobs unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:55:12 +02:00
MechaCat02
5d5dc5dd89 [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>
2026-07-07 20:02:05 +02:00
MechaCat02
da0509b4ac [iterate-4A] jit: inline load fast-path (skip trampoline) — JIT reaches interpreter parity
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>
2026-07-07 19:44:56 +02:00
MechaCat02
56dbf52a5f [iterate-4A] jit: wire compiled-block cache + cover bx/bcx/bclrx (diff-clean, 6.57% native)
Wire the Cranelift block-JIT into execution and add branch coverage. Both
increments validated bit-exact against the interpreter via the in-process
differential harness on a full boot+movie run (movie plays, clean exit):

  wiring only (addi/addis): checked 146.8M blocks, 0.01% native, MISMATCHES=0
  + branches (bx/bcx/bclrx): checked 148.5M blocks, 6.57% native (9.76M), MISMATCHES=0

jit.rs
  * JitCache: direct-mapped 64K-slot compiled-block cache keyed (start_pc,
    page_version) identically to BlockCache, so self-modifying / DMA'd code
    invalidates native code the same way. Caches the None ("uncovered") verdict
    so an uncovered block is compile-attempted at most once per (pc,version).
    Owns the Jit/JITModule (keeps every CompiledFn valid for its lifetime).
  * covered(): addi/addis + bx/bcx/bclrx. bcctrx excluded (indirect target +
    dispatch_rec diagnostic hook the native path would skip).
  * pc-handling refactor: a branch terminator writes pc itself (writes_pc());
    the block epilogue stores end_pc only for straight-line (max-len / page-
    boundary) blocks. cycle/timebase still += N (covered ops never fault/yield;
    a branch is always the last instruction).
  * Branch lowering uses immediate targets — the interpreter's ctx.pc equals the
    instruction address at emit time, so bx/bcx relative targets are constants.
    emit_branch_taken mirrors the interpreter: optional CTR decrement, ctr_ok =
    (ctr as u32 vs 0) inverted by BO3, cond_ok = CR-bit BI byte == BO1 (both BO
    sub-cases const-fold), combined with select. bclrx reads lr&!3 before the LK
    link overwrites lr.

recompiler.rs
  * run_block / diff_step take &mut JitCache. run_block runs the native fn when
    get_or_compile returns one (else interpreter fallback); diff_step runs it on
    the speculative clone/OverlayMemory so every native block is diff-checked.
  * report_jit_summary(): native-vs-interpreted block counts (XENIA_JIT / _DIFF).

main.rs
  * WorkerCtx owns a JitCache; run_superblock routing passes it to
    diff_step/run_block; report_jit_summary() at clean exit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 07:28:26 +02:00
MechaCat02
07cd272412 [iterate-4A] jit: opcode-frequency histogram (XENIA_JIT_HIST) for coverage targeting
Gated 1-in-16 sampled opcode histogram in the recompiler (tallied in run_block,
printed sorted with cumulative % at clean exit). Drives which opcodes the JIT
lowers first. Boot+movie result: workload is FP-heavy — addi 15%, lwz 12%,
rlwinm 10%, lfs 9%, stfs 7.5%, lfsx 5%, fmaddsx 4.6%, bc 3.7%, fmulsx 3.6%,
stw 3.4% ... top-20 = 90%, ~33% floating-point. This is why we go straight to a
Cranelift machine-code JIT (which attacks the FP op bodies) rather than a
closure-threaded stage (which only removes dispatch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:36:53 +02:00
MechaCat02
ac289bca5e [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>
2026-07-06 20:25:18 +02:00