Attacks the ACTUAL bottleneck the profile found (the ~40%-of-worker-time quiesce
barrier — NOT lock contention, which was 4.8%). Removes the per-tick phaser
rendezvous entirely: workers free-run continuously; the coordinator just takes
the kernel lock each tick (like a 7th participant), runs the same housekeeping,
releases, and unparks idle workers.
The only reason the barrier existed was `dispatch_graphics_interrupts` borrowing
a guest thread's ctx as the ISR victim — which races with a worker that has that
ctx EXTRACTED for its unlocked region. Fixed with a per-slot in-flight flag: a
worker sets its bit under the kernel lock right after mem::replace-ing its ctx
out, clears it under the lock after writeback; the coordinator reads the flags
(under the lock → stable snapshot) and passes an in_flight_mask to
dispatch_graphics_interrupts, which SKIPS in-flight slots for victim selection.
Lockstep + the barrier executor pass mask 0 (skip nothing → byte-identical).
Measured (n=2B, JIT): typically ~17s = ~1.4× over lockstep-JIT (~24s), plays the
full video (2.0B instrs, 12041 draws / 7440 swaps — the responsive coordinator
delivers vsync faster so the guest advances more per instruction). Remaining
run-to-run variance (occasional ~65s) is EXTERNAL — the box shows loadavg ~3 and
a `powersave` governor with turbo off; it survived every code change (barrier,
mmio, sync, tick, barrier-removal) precisely because it isn't the code.
Validation: parallel_stress_short 20/20 ok under FREERUN=1; lockstep golden
byte-identical (interp + JIT). Opt-in XENIA_PARALLEL_FREERUN=1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds XENIA_PARALLEL_PROFILE=1 to the free-run executor: reports, summed across
workers, kernel-lock WAIT (contention), HELD, unlocked REGION, and idle-park
counts. This measured the actual bottleneck before committing to fine-grained
locking — and refuted it:
n=2B free-run profile (worker-thread-time basis):
kernel-lock WAIT = 4.8% <- contention is NOT the wall
kernel-lock HELD = 0.5%
unlocked REGION = 27.1% <- productive
idle_parks = 2.45M ; ~40% = workers blocked at the quiesce barrier
So fine-grained kernel locking would NOT help — the wall is COORDINATION
(the per-tick quiesce barrier + idle-park wake latency) and guest spin-waits.
Also raises the coordinator tick default 200µs -> 2000µs: the barrier is on the
critical path, so the small tick was the dominant cost. This roughly halves the
good-case time (n=2B ~17-18s vs lockstep-JIT ~24s = ~1.35×). BUT free-run is
still BIMODAL — a coordination pathology intermittently latches (~60s). (A/B
tests that looked like "XENIA_GPU_THREAD makes it fast" were run-ordering noise:
use_threaded=true either way — the flag is a no-op here; the split was the
pathology.) Next lever = attack the coordination bimodality (precise cross-slot
wake to kill the ≤50µs idle-park latency; or skip in-flight slots in the ISR
housekeeping to drop the barrier), NOT lock-splitting.
Opt-in (XENIA_PARALLEL_FREERUN=1); lockstep golden byte-identical (interp+JIT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes the per-round phaser barrier that capped the coarse parallel path at
~parity. Workers now FREE-RUN their HW slots (pick slot's thread, run a
parallel-safe region on the extracted ctx, writeback+epilogue under the lock,
repeat — no waiting on peers); a coordinator thread runs the same housekeeping
(coord_pre_round tickers/timers, dispatch_graphics_interrupts, inline-GPU drain,
coord_idle_advance) only on a wall-clock cadence, quiescing the workers at a
7-party phaser (via a global `quiesce` AtomicBool, robust vs the earlier
epoch-diff which desynced a late-starting worker) so ctx-borrowing housekeeping
stays race-free.
New: run_execution_parallel_freerun, parallel_region_budget() (default 2048,
XENIA_PARALLEL_BUDGET; decoupled from lockstep's 128 so the golden is
untouched), scheduler slot_runnable()/any_runnable(), a per-thread `retired=`
field in the XENIA_DUMP_SLOTS diagnostic, and dropping the global
mmio_access_count region break in the parallel driver (that shared counter,
bumped by ANY worker, collapsed every region to one block).
STATUS — correct but NOT yet a win. Measured (n=2B, --gpu-inline, JIT):
- runs the full 2B and plays the video (4939 draws / 1361 swaps).
- BIMODAL: good runs ~21s (edges out lockstep-JIT's ~24s) but a
kernel-mutex-contention / guest-spin-wait pathology intermittently latches
and makes a run 2-3x slower (~57-87s).
Root cause: the single Arc<Mutex<KernelState>> serializes the 6 workers, so the
~4.3x thread-parallelism the workload exposes (measured: work spread across
~5-8 balanced guest threads, top only ~7%) collapses to ~parity. Region-tuning
levers (barrier granularity, MMIO break, GPU cadence, sync break) were each
measured and none crack it — the real win requires FINE-GRAINED kernel locking.
Gated OPT-IN behind XENIA_PARALLEL_FREERUN=1; default --parallel stays the
per-round barrier executor. Lockstep untouched (6-config golden byte-identical);
parallel_stress_short 20/20 ok under FREERUN=1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The --parallel worker did a full 7-party phaser barrier + kernel-lock dance
around every single ~13-instruction interpreter block, making it 20x SLOWER
than lockstep (2.7 vs 55 MIPS at n=400M) and JIT-less. Replace the single-block
unlocked window with run_superblock_parallel_unlocked: a whole straight-line
region on the extracted ctx + per-worker caches (block + JIT), stopping at the
first import/halt/mmio/sync/budget boundary for the locked epilogue to handle.
Touches zero KernelState in the lock-free window (thunk band cached once under
the lock via new KernelState::thunk_addr_band). Same JIT seam as run_superblock,
so XENIA_JIT unset = interp, set = JIT — one driver covers Phase A and B.
Measured (--gpu-inline):
- recovered the parallel path 16-30x (2.7 -> 43-95 MIPS)
- video-phase n=2B parallel-JIT budget=8192: 21.0s vs lockstep-JIT 23.8s (+13%),
but violently budget-fragile (4096 = 79.9s) -> the hard per-round barrier +
load imbalance caps it well below the 4.3x runnable-width ceiling. Phase C
(free-running workers) needed for the real multiplier.
Determinism: lockstep path untouched; 6-config n=200M golden byte-identical
(incl. config6 JIT+chain+budget=1 == interp+budget=1). xenia-jit 24 tests green;
parallel_stress_short 20/20 ok.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native fmadds/fmsubs/fnmadds/fnmsubs + double siblings via hardware FMA
(vfmadd213sd/vfmsub213sd — single rounding, matching the interpreter's
correctly-rounded f64::mul_add; negated forms flip the result sign with xorpd,
and NaN — which the interpreter preserves unnegated — is caught by the finite
guard → deopt, so unconditional negate is exact on the fast path). Gated on a
runtime cpuid FMA3 check (emit::host_has_fma, cached OnceLock); on a non-FMA
host these arms fall back to the interpreter's software FMA — byte-identical, so
goldens hold on any host (NEVER emit mulsd+addsd, which double-rounds).
fp_arith_matches extended to the madd family (guarded on host FMA3). All 6-config
GOLDEN n200m BYTE-IDENTICAL; 24 xenia-jit tests green.
MEASURED (2B-instr run through the video, --gpu-inline, best-of-3) — CLEAN 3-way:
interp 72.6s (27.5 MIPS)
region JIT (pre-FP) 27.3s (73.2 MIPS) = 2.66x over interp
region JIT + native FP 23.3s (85.8 MIPS) = 3.12x over interp
So native FP's OWN contribution is ~1.17x (+17%) on top of the region JIT — it
removed 62% of all interpreter fallbacks (306.6M→117.3M, the FP-arith share) but
that translates to only ~17% wall-time here because the region JIT already made
non-FP code fast and the --gpu-inline drain + plumbing dominate the remainder.
This matches the conservative ~1.3-1.5x plan estimate (region JIT is the bigger
CPU lever on the video; FP is a solid, deterministic increment and a multiplier
for a future multi-core mode). Video still ~7x from real-time → multi-core next.
Emit native x64 for the hot FP-arith ops (currently 61.6% of interpreter
fallbacks through the intro video; the JIT emitted ZERO native FP arithmetic
before). Phase 1 = add/sub/mul/div, single (op59) + double (op63), Rc=0.
Correctness rests on a lemma (validated vs fpscr.rs): if the final result is
±NORMAL or ±ZERO, the interpreter set NO FPSCR exception bit and
FPRF=classify(result). So the fast path is: RN-nearest guard (FPSCR RN_MASK=0x3,
low 2 bits — NOT 0xC0000000) → native movsd/addsd/subsd/mulsd/divsd in xmm
(single narrows via cvtsd2ss;cvtss2sd = to_single at RN-nearest, bit-exact) →
result guard (reject inf/NaN; for doubles also reject subnormal → UX+DENORMAL) →
store + cheap inline FPRF (sign+iszero → one of ±NORMAL/±ZERO codes). Every
guarded (rare) case DEOPTS to jit_interpret_one for that one instruction —
counter-neutral (state.retire() defers the +1 on both paths; interp_one bumps no
counter and touches only fpr/fpscr/pc, never ctx.gpr, so no RegCache flush).
Rc=1 forms fall back (update_cr1 not replicated). FMA (madd) family deferred to
Phase 2 (needs hardware FMA3).
Infra: Offsets.fpscr; MemHelpers.interpret_one; emit_fp_arith + FpArith enum +
emit_fprf_normal_or_zero (emit.rs); DynasmLabelApi import (dynamic labels/relocs
in emit.rs). check_fp now SEEDS and ASSERTS fpscr (the real safety net) — added
check_fp_seeded. New tests: fp_arith_matches (fuzz + edge floats),
fp_arith_directed_rounding_matches (RN!=nearest → deopt), fp_arith_block_matches
(multi-op native↔deopt↔fallback boundary). 24 xenia-jit tests green.
GOLDEN n200m BYTE-IDENTICAL across all 6 configs (interp / JIT / +CHAIN(region)
/ +REGCACHE / +REGION_MAX=1 / +budget=1==interp+budget=1). Whole-video speedup
measured after Phase 2 (the fmadds family is the single biggest op at 27.7%).
Measured ~5× faster boot under --ui (~12 → ~63 MIPS) by moving the per-frame
PM4 drain (draws, YUV texture decode, resolves) + UI publish off the emulation
thread onto the GPU worker — canary's async-command-thread model. User-confirmed
visuals identical to the inline path (splash + intro video render correctly).
Flip the default: `--ui` now uses the threaded GPU backend. Opt back to inline
with `--gpu-inline` or `XENIA_UI_GPU_INLINE=1`. The deterministic golden path
already passes `--gpu-inline`, so goldens are unaffected (n2m re-checked MATCH).
`XENIA_UI_GPU_THREAD` retained as a no-op alias. Headless default (M1.9 threaded)
unchanged.
Note: threading the GPU only helps GPU-bound phases (boot). The intro VIDEO is
CPU-bound on multi-threaded 720p-YUV software decode (FP-arith-heavy, mostly JIT
fallback), so it stays ~real-video-length; that needs the multi-core / native-FP
levers, not GPU threading.
Sylpheed's boot reaches its front-end TITLE shell (steady frame loop — 2219
swaps over 5B instr; XamResetInactivity/XamEnableInactivityProcessing/
XamUserGetSigninState front-end mgmt; XamInputGetCapabilities polled 8880x) but
then STALLS: headless has no HID device, so GetCapabilities returns
DEVICE_NOT_CONNECTED and the game loops forever waiting for a controller. It
never advances to read input (XamInputGetState / XamInputGetKeystrokeEx = 0).
Canary reaches the "Press A" menu because its default input drivers (xinput /
winkey) present an always-connected controller. Match that: present a default
always-connected, IDLE (no-buttons) virtual controller for user 0 (a console
always exposes controller slots; a real gamepad under --ui still supplies actual
button state — see xam_input_get_state, which prefers ui.snapshot_gamepad()).
Set XENIA_NO_PAD=1 to present no controller (the raw headless no-HID trajectory).
MEASURED (headless, this is not inference): with the controller connected the
game advances into the interactive input-poll loop —
XamInputGetKeystrokeEx 0 -> 2164 (once per frame)
XamInputGetState 0 -> 2165
XamInputSetState (rumble) 0 -> 1
i.e. the "Press A" title-screen loop (attract video repeats while it polls for
A), exactly the M3 milestone behavior. Visual confirmation of the on-screen
"Press A" prompt is the remaining step (--ui + frame readback).
GOLDEN RE-BASELINE (deliberate): a connected controller changes the guest
trajectory WITHIN the 200M window (the title polls input before 200M), so both
sylpheed_n200m.json and sylpheed_n2m.json are re-baselined. Change is proven
ISOLATED: XENIA_NO_PAD=1 reproduces the OLD goldens byte-identical at both n.
New n200m: instructions 200000203, imports 575647, draws 3208, swaps 910
(was 200000239 / 575447 / 3165 / 895 — small front-end-input increments; RTs/
shaders/textures unchanged). All 6 JIT configs match the new golden byte-
identical (interp / JIT / JIT+CHAIN / +REGCACHE / REGION_MAX=1 / budget=1==
interp+budget=1); 131 xenia-kernel + 21 xenia-jit tests green.
Compile a straight-line/same-page REGION rooted at a block into one
CompiledBlock (XENIA_JIT_CHAIN): the set of same-page blocks reachable by
compile-time-known direct branches (BFS, capped by XENIA_JIT_REGION_MAX=32
+ 512-instr budget). Internal transitions between member blocks become plain
native jmps to dynasm labels — no per-block prologue/epilogue, no
jit_chain_next lookup, no stack churn — while the lockstep yield guards
(chain-enabled / budget / mmio) are evaluated inline at each block boundary
and a sync-sensitive block ends the region. Any successor not inlined
(indirect target, cross-page) falls to the increment-1 boundary chain:
jit_chain_next tail-jumps to the next region on a hit, or signals Rust to
build+compile it on a miss.
Byte-identical schedule to run_superblock: the SAME blocks run in the SAME
order with the SAME per-block budget/mmio/sync checks — only the dispatch
MECHANISM differs. Inlining is purely an optimization; the boundary chain is
the always-correct fallback, and inlined successors are same-page (⇒ mapped,
non-thunk) with the LR_HALT sentinel filtered, so next_pc_breaks_chain's
runtime arms are statically satisfied.
THROUGHPUT (best-of-5, n=200M --gpu-inline): interp 3.76s · plain JIT 3.98s ·
increment-1 chain 3.97s · REGION 3.49s. Region is ~1.08-1.14x FASTER than the
interpreter (first clear JIT win on the boot bench) and ~1.14x faster than
increment-1's throughput-neutral block chaining — confirming the thesis that
the real dispatch cost was the per-block prologue/epilogue increment 1 kept.
Region-size sweep: gains saturate by max=32 (max=64 identical).
GOLDEN n200m BYTE-IDENTICAL across 6 configs: interp / JIT / JIT+CHAIN(region)
/ JIT+CHAIN+REGCACHE / JIT+CHAIN+REGION_MAX=1 / JIT+CHAIN+budget=1 ==
interp+budget=1. 21 xenia-jit tests (added region_multiblock_matches_interp_
superblock + region_sync_block_ends_superblock).
Bug found+fixed during bring-up: a region chained PAST a sync-sensitive block
via the boundary chain instead of ending the superblock (compile_block gives
sync blocks the plain epilogue; the region must too) — this diverged the whole
200M schedule (draws 3201 vs 3165). Fix: sync block tail = unconditional
jmp l_region_ret; BFS never expands past a sync block. The sync regression
test asserts the region stops after A+B (cycle==4) instead of looping to budget.
New: xenia_cpu::block_cache::decode_block (pub standalone decoder for the
region builder) + DecodedBlock: Clone. Diagnostic knobs XENIA_JIT_REGION_MAX
(cap, default 32; =1 reproduces increment-1 chaining) and XENIA_JIT_NOREGION
(route chain compiles through compile_block — the A/B knob that isolated the
sync bug). Default XENIA_JIT/XENIA_JIT_CHAIN both still OFF; shipping path
unchanged.
Compiled blocks tail-jump straight to their successor's compiled code
(canary-style indirection dispatch) instead of returning to the Rust
superblock loop between blocks, checking the lockstep yield guards
INLINE. Gated behind XENIA_JIT_CHAIN (default off) — the shipping JIT
path (Phase A) is byte-for-byte unchanged.
Mechanism:
- JitEnv gains chain_{enabled,stop,deadline,mmio_ptr,mmio_before,cache}.
- Chaining epilogue (non-sync blocks only, when chain_active): flush cache
to ctx, then inline guards — budget (ctx.cycle_count >= start+budget,
== interp total_executed>=budget), mmio (per-block prologue snapshot vs
live counter), chain_enabled gate — then jit_chain_next(env, next_pc)
(pure JIT-cache freshness lookup, returns host entry or null). On a hit:
restore env->rdi, unwind THIS frame (pop to entry rsp), tail-jmp to the
successor (no stack growth; the return addr rides through to the final
block's ret). On null: set chain_stop=Miss, return to Rust.
- run_jit_chain drives it; run_superblock_jit_chained (used when chaining
compiled in + budget>1 + no probe/mem-watch armed) re-enters on a Miss to
build+compile the chainable next block. sync blocks keep the plain
epilogue (end the superblock like interp). GuestMemory::
mmio_access_count_ptr exposes the counter for the inline check.
BYTE-IDENTICAL golden n200m across: interp, JIT, JIT+REGCACHE, JIT+CHAIN,
JIT+CHAIN+REGCACHE, and JIT+CHAIN+budget=1 == interp+budget=1. 19 jit
tests green. (Fixed: prologue mmio snapshot must be chain_enabled-guarded
— a chaining-compiled block is also run via run_jit_block with a null
mmio_ptr on the Phase A / budget==1 / probes-armed paths.)
THROUGHPUT: NEUTRAL on the boot bench (JIT vs JIT+CHAIN statistically
equal, within noise). As the canary study predicted: block-granularity
chaining keeps the per-block prologue/epilogue (unwind+rewind per
boundary) and jit_chain_next ~= the run_fresh it replaces, so the
reclaimed Rust-loop overhead is Amdahl-swamped. The determinism-critical
machinery (inline yield guards, tail-jmp discipline, dispatch ABI) is now
PROVEN byte-identical — the foundation for region/function-granularity
compilation (increment 2), which is where the real dispatch win lives.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The JIT was ~1.077x SLOWER than the interpreter on the boot bench because
it paid a DOUBLE lookup per block: block_cache.lookup_or_build (to get the
DecodedBlock — the ~9.7% "block decode/cache" bucket) THEN JitCache's own
lookup. On a JIT-cache HIT the DecodedBlock is not needed: the freshness
key is (start_pc, mem.page_version(pc)), reconstructible from mem alone
(build_block stops at the 4 KiB page boundary, so both caches key on the
same single-page version).
Phase A adds a JIT-specialized superblock runner that runs chained
(2nd..Nth) blocks straight from the JIT cache, skipping block_cache on
hits:
- CompiledBlock now carries sync_sensitive (copied from DecodedBlock) —
the chain STOP guard needs it and a JIT hit has no DecodedBlock.
- JitCache::run_fresh(pc, ctx, mem): lookup-only fast path; computes pv
itself via mem.page_version (SMC coherence); Some((result, sync)) on a
fresh hit, None on a miss (never compiles — compilation stays on the
DecodedBlock path). New unit test run_fresh_hit_miss.
- run_superblock_jit (parallel to run_superblock, used when jit_cache is
Some): first block + JIT misses use block_cache.lookup_or_build +
run_or_compile (rebuild inline on miss so chain length — and the
schedule — is unchanged); chained hits use run_fresh. Non-Continue
break lazily rebuilds a block_ptr for worker_epilogue's SYSCALL/Trap
diagnostics. Same raw-ctx-ptr discipline; shared next_pc_breaks_chain
helper keeps both loops' chaining decisions in lockstep. Interp
run_superblock untouched except the extracted helper call.
Results (n=200M --gpu-inline): block_cache calls 11.4M -> 991k (-91% on
the JIT run — run_fresh handles 91% of block acquisitions). Throughput:
JIT 1.077x slower -> ~1.03x FASTER than interp (best-of-8 interleaved:
JIT 3.73s vs interp 3.84s) — first time the JIT beats the interpreter.
Golden n200m BYTE-IDENTICAL: interp==golden (path untouched), JIT==golden,
JIT+REGCACHE==golden, and JIT budget=1 == interp budget=1. 19 jit tests
green.
Phase B (native inline chaining, targets the 23.6% loop body) deferred —
see plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Port the D/DS-form update loads (lwzu, lhzu, lhau, lbzu, ldu) and stores
(stwu, sthu, stbu, stdu) — ~10% of the boot fallback executions (stwu 6.1%
+ lwzu 2.4% + ldu/stdu 1.4% + ...). EA = gpr[rA] + EXTS(disp) with rA read
directly (update forms are invalid for rA==0, and interp reads gpr[rA]
with no 0-substitution); rD/mem then gpr[rA]=ea truncated to u32 and
zero-extended (matches interp `... as u32; gpr[ra]=ea as u64`). Stores
capture rS into rdx BEFORE the rA writeback so `stXu rS,d(rA)` with rS==rA
stores the OLD rA value (interp order). New emit helpers
emit_update_ea_writeback / emit_load_update / emit_load64_update /
emit_store_update; all cache-aware (gld64/gst64 accessors).
New differential test update_loadstore_matches (regcache forced on),
incl. the store rS==rA case. 18 tests green.
Fallback executions 25.55M -> 22.97M (-10.1%, n=200M). Golden n200m
BYTE-IDENTICAL under interp, XENIA_JIT=1, and +XENIA_JIT_REGCACHE=1.
Whole-run ratio 1.079x -> 1.077x: as prior evidence predicted, opcode
coverage does not flip the JIT on this FP-video-decode-bound boot bench
(62% of remaining fallbacks are fmadds/fmuls/fadds/fsubs, expensive in
both paths). Correctness/coverage win; real speed lever is dispatch
elimination (block-linking), not more opcodes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
run_superblock re-resolved the running thread's PpcContext on every
chained block via two bounds-checked slot lookups (ctx_mut_ref for the
step + ctx(hw_id) for next_pc) — ~20M double-lookups per 100M-instr
window. The running thread is FIXED for the whole chain (an import
thunk or any sync-sensitive/MMIO op breaks the chain before any kernel
mutation could restructure the runqueue; step_block is pure guest
interpretation; the probe closure is read-only), so its context heap
slot is stable. Resolve the raw *mut PpcContext once before the loop
and reuse it — same raw-pointer discipline as block_ptr.
~3% faster clean wall (1.91s -> 1.85s, n=100M, best of 5). Golden
sylpheed_n200m BYTE-IDENTICAL under interp, XENIA_JIT=1, and
XENIA_JIT=1+XENIA_JIT_REGCACHE=1.
Found via exclusive-attribution profiling: the run is overhead-bound
with the non-step cost spread thin across the per-block chaining loop
(no single dominant lever); step_block itself is ~46% and only the JIT
attacks it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pin the 3 hottest guest GPRs (static ra/rb/rd frequency) into callee-saved
host regs r12/r13/r14 across a block. RegCache + gld64/gld32/gst64
accessors (dynasm Rq/Rd dynamic regs); ALL 75 GPR-access sites in the
emitters routed through the accessors. Prologue loads cached regs / epilogue
flushes (both l_cont+l_exit merge); fallback path flushes-before /
reloads-after (interpreter touches ctx.gpr; mem/FP helpers don't, and cached
regs are callee-saved, so no flush around those). RA=0 literal-0 rule
unaffected (emit_ea/load_ra_or_zero never read gpr[0]). Gated by
XENIA_JIT_REGCACHE (default off; forced on under cfg(test)); conditional
prologue keeps the cache-off codegen unchanged.
Golden n200m BYTE-IDENTICAL in all 3 configs (interp / JIT / JIT+RC); 17
differential tests pass with the cache FORCED ON. Measured (min of 6):
JIT+RC 4.16s vs JIT 4.14s vs interp 3.80s -> NEUTRAL on this benchmark:
per-block setup (5 push/pop + 3 load/flush) + fallback flush/reload (+6
mem ops each) cancels the intra-block savings (blocks ~13 instrs, fallbacks
frequent). Correct + default-off; may help more CPU-bound workloads
(gameplay, larger hot loops). Definitively: no JIT technique (coverage,
deferral, regcache) beats interp on this ~60%-overhead boot/render bench.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native lwzx/lhzx/lhax/lbzx/stwx/sthx/stbx (via refactored emit_load_tail/
emit_store_tail + emit_ea_x), slw/srw/sld/srd (variable shifts, non-rc,
cmovae zeroes for count>=width), and mfspr/mtspr for the pure LR(8)/CTR(9)
registers only (other SPRs have side effects -> fallback). Differential
tests indexed_loadstore_matches / shifts_match (sh spanning width
boundaries) / spr_lr_ctr_matches. Golden n200m BYTE-IDENTICAL with
XENIA_JIT (17 tests green).
Measured: fallback execs 13.5M->10.8M (64% below the original 29.7M).
BUT throughput ratio only 1.10->1.09 -> DEFINITIVE: on this boot/render
benchmark opcode coverage alone can't beat interp. Cutting fallbacks 64%
moved the ratio 7 points because the run is ~60% non-CPU-step overhead
(Amdahl) AND the remaining fallback is FP-arith (fmadds/fmuls/fadds ~57%),
expensive in BOTH paths (small wrapper delta) + FPSCR determinism risk.
Below-1.0 needs register caching (attacks native-op cost directly) or a
more CPU-bound workload.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native lfs/lfsx/lfd/lfdx/stfs/stfsx/stfd/stfdx and ld/ldx. FP goes through
dedicated helpers (jit_read_f32_as_f64/read_f64/store_f32/store_f64) that
call the EXACT interpreter mem methods + as-casts, so the f32<->f64
conversion is bit-identical by construction (no hand-written cvtss2sd /
NaN-payload risk); FP stores replicate the reservation-invalidation.
emit_ea_x (indexed EA), emit_fp_load/store, emit_int_load64 helpers; fpr
offset added. Differential test fp_loadstore_matches (2000 seeds, FPR
edge patterns 0/-0/inf/NaN/denormal/rounding, asserts fpr-by-bits + mem).
Golden n200m BYTE-IDENTICAL with XENIA_JIT (14 tests green).
Measured (XENIA_JIT_STATS): total fallback execs 29.7M->13.5M (-55%);
throughput ratio vs interp 1.16->1.10. Remaining fallback now FP-arith-
dominated: fmadds 20% + fmuls 16% + fadds 8% (~44%).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jit_interpret_one tallies fallback executions per opcode (behind
XENIA_JIT_STATS, off by default); dump_fallback_stats() prints the top 30
after a check run. Measured on Sylpheed n100M: the fallback tax is FP-
dominated — lfs/stfs/lfsx/stfsx ~50%, fmadds/fmuls/fadds/fmr ~21%,
ld 4.5%, shifts ~4%, mtspr/mfspr only ~3%. Refutes the earlier
"mflr/mtlr dominates" guess; FP is the crossover lever.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Strip the 3 per-instruction memory RMWs (pc+=4, cycle++, timebase++) from
straight-line native ops. EmitState.pending accumulates retired native
instrs at compile time; counters are materialized in bulk (add [cycle],N)
only at observability points, and pc is written absolutely (from the known
addr) only where read. Invariant: at every block-exit edge ctx.pc/
cycle_count/timebase are exactly the interpreter's values (nothing observes
them mid-block; fallbacks flush+set-pc first; native branches set pc
absolutely from addr and flush). Branches rewritten to use compile-time
addr (no more [pc] reads). New multi_instr_block_matches test diffs a
JIT block vs the real step_block (native+fallback+branch mix). Golden
n200m BYTE-IDENTICAL with and without XENIA_JIT (13 tests green).
Throughput 4.55->4.44s; ratio vs interp still ~1.16 -> gap is fallback
tax (mflr/mtlr, indexed/update ld-st, shifts still fallback) + the run
being only ~40% CPU-step, NOT the native-path RMWs. Deferral is also the
flush-discipline substrate for register caching.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native rlwinm/rlwimi/rlwnm (32-bit rol+and, mask folded to a compile-time
constant) and rldicl/rldicr (64-bit rol + 64-bit mask), including the Rc
recording forms via emit_cr0_from_reg (test + signed setcc into cr[0],
reusing the compare CR path). rlw_mask/rld_mask replicated as
compile-time helpers. Differential test rotates_match: 2000 seeds x
{rc,non-rc} x {32,64-bit} x xer_so, asserting full GPR+CR+counters.
Golden n200m BYTE-IDENTICAL with and without XENIA_JIT (12 tests green).
Throughput 4.7->4.55s (interp ~3.9s): opcode coverage now at diminishing
returns; the remaining gap is memory-traffic-per-instruction (no register
caching) + fallback tax, not missing opcodes -> Phase 4 (register cache)
is the crossover lever.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
try_emit_native now returns a 3-state Emit enum (Fallback/Native/Branch);
compile_block appends the pc==expected_next discontinuity check after a
native branch, mirroring step_block. bx (aa/rel, lk), bcx (CTR
decrement + CTR/CR condition, cmov-selected target), bclrx (return via
lr & !3) emitted natively; bcctrx stays on fallback (dispatch_rec side
effect). count_only postlude (counters, no pc+=4) for branches.
Differential tests: unconditional/conditional/return branches over
exhaustive BO bits, CTR==1->0 boundary, lr alignment mask, both lk
states, asserting pc/lr/ctr/cr/counters. Golden n200m BYTE-IDENTICAL
with and without XENIA_JIT (11 tests green). Throughput 4.85->4.7s
(interp ~3.9s); crossover still pending rotate/shift (Phase 3).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Native cmpi/cmpli/cmp/cmpl via x64 cmp + setcc into cr[bf].{lt,gt,eq},
so from xer_so. CrField flag offsets resolved with offset_of! (the type
is not repr(C), so byte positions are not assumed). Both 32/64-bit (L)
widths handled. Differential test compares_match (2000 seeds x both
widths x both xer_so values) asserts full CR nibble + counters; check()
now also asserts CR for all opcodes. Golden n200m BYTE-IDENTICAL with
and without XENIA_JIT.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
crates/xenia-jit/src/emit.rs: native x64 for the hot non-recording integer
ALU ops — addi, addis, ori, oris, xori, xoris, or(x), and(x), xor(x),
add(x), subf(x), neg(x). Each mirrors its interpreter arm exactly, then
does pc+=4 and the cycle/timebase bumps. Per-instance guards fall back to
the interpreter for forms the emitter can't yet reproduce faithfully:
recording (`.`/Rc) forms, OE overflow forms, and the db16cyc spin hint
(or r31,r31,r31 -> Yield). RA=0 literal-zero rule handled at emit time.
Differential harness (tests.rs): each opcode run through interpret_one vs a
JIT-compiled 1-instr block over 2000 random register seeds (+ edge values
0/0xFFFFFFFF/0x80000000/INT64 boundaries); asserts full GPR/PC/XER/cycle/
timebase/StepResult equality. Plus a guard test that recording/OE/db16cyc
forms are NOT natively emitted.
Gate: golden n200m BYTE-IDENTICAL with XENIA_JIT=1; `cargo test -p
xenia-jit` green. Throughput -n 200M --gpu-inline: 4.3s interp, 6.2s JIT
(down from 7.1s all-fallback skeleton). Still fallback-bound on
loads/stores/branches -> Phase 2 is the crossover.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New crate `xenia-jit` — the PPC->x64 block-JIT runtime substrate, behind
default-OFF XENIA_JIT. Phase 0 ports ZERO opcodes to native: every guest
instruction is emitted as `call jit_interpret_one` (the interpreter), so a
JIT-compiled block is byte-identical to step_block by construction. This
proves the ABI/counters/exit-semantics/mem-helpers/code-cache before any
opcode is hand-written.
- JitEnv{ctx, mem (fat raw ptr), last_result}; compiled block =
extern "C" fn(*mut JitEnv)->u32 (StepResult discriminant, 0=Continue).
Emitted code pins ctx in r15 + env in rbx (callee-saved across calls),
offsets via offset_of!.
- Determinism postlude: cycle_count/timebase +=1 after every retired
instruction; block stops at the same instruction as the interpreter
(non-Continue result, or taken-branch pc discontinuity).
- Per-slot JitCache mirrors BlockCache's (start_pc, page_version) gate;
each CompiledBlock OWNS a copy of its decoded instrs so baked instr
pointers can't dangle after a block-cache eviction.
- xenia-cpu: `pub fn interpret_one` (execute without the cycle bump — the
JIT owns counting).
- Seam: run_superblock main.rs:3154 dispatches to the JIT when enabled;
WorkerCtx gains an Option<JitCache> (Some only when XENIA_JIT set and
RET-CAPTURE debug env unset). observe_per_instruction gate unchanged, so
tooling runs never reach the JIT.
Gate: golden n200m BYTE-IDENTICAL both with and without XENIA_JIT=1.
Throughput -n 200M --gpu-inline: 4.5s interp vs 7.1s all-fallback skeleton
(the per-instruction call overhead Phase 1 removes for hot opcodes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 23:38:28 +02:00
15 changed files with 5230 additions and 60 deletions
/// Raw address of the MMIO access counter, as `*const u64`. The JIT's native
/// block-chaining epilogue reads it with a plain aligned load to detect a
/// mid-block MMIO touch inline (equivalent to a `Relaxed` load on x86-64 —
/// `AtomicU64` has the same layout as `u64`). Single-thread use only.
#[inline]
pubfnmmio_access_count_ptr(&self)-> *constu64{
self.mmio_access_count.as_ptr()as*constu64
}
#[inline]
fnbump_mmio_access(&self){
self.mmio_access_count
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.