Compare commits

..

27 Commits

Author SHA1 Message Date
MechaCat02
869790bab9 fix(tools): self-locate DB / cwd instead of the stale renamed path
zq.py and sylph-run.sh hard-coded '/home/fabi/RE - Project Sylpheed/...'
(the dashed dir was renamed 'RE Project Sylpheed'), so both were broken.
Resolve relative to the script directory now; zq.py honours $SYLPHEED_DB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:04:28 +02:00
MechaCat02
19700736b8 [iterate-4E] Stage 1'a: enable host-atomic ReservationTable in native mode
native_active is now folded into parallel_active, so XENIA_NATIVE_THREADS=1
(a) enables kernel.reservations (lwarx/stwcx route through the inter-thread
ReservationTable) and (b) sets kernel.parallel_active for the wall-clock
vsync/coordination paths.

Closes the PPCBUG-108 landmine flagged in the rework pressure-test: native
mode runs guest code on multiple host threads concurrently, so the legacy
per-PpcContext reservation fallback (which cannot observe cross-thread
stores) is incorrect. Previously pure-native (no --parallel) left the table
disabled — silently wrong in release, debug_assert in debug.

Gate GREEN 3/3 (golden byte-identical, native renders, stress no-deadlock).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:21:49 +02:00
MechaCat02
48e166579b [iterate-4E] Stage 0: native-threads mode flag + functional gate
Scaffolding for the canary-model rework (host-thread-per-guest-thread,
opt-in, non-deterministic). No behavior change with the flag off.

- XENIA_NATIVE_THREADS=1 selects the new executor; folded into the
  parallel spawn gate (reuses Arc<Mutex<KernelState>> + worker dispatch).
- native_threads_enabled() = single source of truth for guarded branches.
- run_execution_native(): Stage-0 body delegates to the iterate-4D
  free-run executor verbatim (native == freerun for now); Stage 1' will
  replace the worker set with one host thread per guest thread.
- native-gate.sh: functional oracle replacing byte-goldens for the MT
  path — (1) lockstep golden byte-identity (flag-off safety net),
  (2) native render milestone (draws>0 && swaps>0), (3) native deadlock
  stress. GREEN 3/3 on current code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:18:28 +02:00
MechaCat02
fe797556c2 [iterate-4D] multi-core: barrier-less free-run coordinator (in-flight-slot skip)
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>
2026-07-05 19:47:03 +02:00
MechaCat02
5a2d4947ea [iterate-4D] multi-core: free-run lock-contention profiler + tick=2000 default; REFUTES lock-splitting
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>
2026-07-05 19:33:55 +02:00
MechaCat02
0b00b72292 [iterate-4D] multi-core Phase C: free-running executor (opt-in) + diagnosis
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>
2026-07-05 18:30:40 +02:00
MechaCat02
cce887d69b [iterate-4D] multi-core: coarse-grained parallel-safe superblock driver (Phase A+B)
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>
2026-07-05 16:50:42 +02:00
MechaCat02
eb7c6f98cd [iterate-4C] JIT native FP-arith Phase 2: fused madd family (the 27.7% op)
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.
2026-07-05 15:17:12 +02:00
MechaCat02
0672f37c9f [iterate-4C] JIT native FP-arith Phase 1: fadds/fsubs/fmuls/fdivs (+doubles)
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%).
2026-07-05 15:03:33 +02:00
MechaCat02
3f509ebc5c [iterate-4C] --ui: make threaded GPU the default (was opt-in XENIA_UI_GPU_THREAD)
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.
2026-07-05 14:16:12 +02:00
MechaCat02
0e24e980fb [iterate-4C] M3: default connected controller for user 0 (reach the Press A title loop)
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.
2026-07-05 13:29:52 +02:00
MechaCat02
f562e3ec2b [iterate-4C] JIT Phase B incr.2: region-granularity compilation (the real dispatch win)
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.
2026-07-04 23:18:15 +02:00
MechaCat02
d132fb6d8d [iterate-4C] JIT Phase B incr.1: native tail-chaining (XENIA_JIT_CHAIN, default off)
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>
2026-07-04 22:38:53 +02:00
MechaCat02
fec9e8de28 [iterate-4C] JIT Phase A: skip redundant block_cache lookup on chained hits
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>
2026-07-04 22:00:38 +02:00
MechaCat02
cce35658a5 [iterate-4C] JIT: native update-form loads/stores (lwzu/stwu/ldu/stdu/...)
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>
2026-07-04 21:34:43 +02:00
MechaCat02
7ec6941fe8 [iterate-4C] perf: cache running-thread ctx ptr across superblock chain
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>
2026-07-04 21:18:18 +02:00
MechaCat02
1ffa3fb56d [iterate-4C] JIT Phase 4c: per-block register caching (XENIA_JIT_REGCACHE)
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>
2026-07-04 20:50:39 +02:00
MechaCat02
506b9554a5 [iterate-4C] JIT Phase 4b: indexed loads/stores + shifts + mflr/mtlr
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>
2026-07-04 20:27:18 +02:00
MechaCat02
4194ed77a3 [iterate-4C] JIT Phase 4a: native FP + 64-bit loads/stores
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>
2026-07-04 20:20:55 +02:00
MechaCat02
afc3692223 [iterate-4C] JIT: gated fallback-opcode histogram (XENIA_JIT_STATS)
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>
2026-07-04 20:14:46 +02:00
MechaCat02
e8d0dc4a2d [iterate-4C] JIT Phase 3b: defer pc/counter accounting per block
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>
2026-07-04 20:08:15 +02:00
MechaCat02
781a82cc0a [iterate-4C] JIT Phase 3a: native rotate/mask family
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>
2026-07-04 19:54:51 +02:00
MechaCat02
701e4c399a [iterate-4C] JIT Phase 2b-ii: native branches (bx/bcx/bclrx)
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>
2026-07-04 19:48:47 +02:00
MechaCat02
5e521f7f53 [iterate-4C] JIT Phase 2b-i: native compares + CR field emission
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>
2026-07-04 19:39:16 +02:00
MechaCat02
0f1130e2e6 [iterate-4C] JIT Phase 2a: native loads/stores + backed-memory diff tests
emit.rs: native x64 for the hot D/DS-form memory ops — lbz, lhz, lha, lwz,
stb, sth, stw, std. EA = (rA==0?0:gpr[rA]) + EXTS(disp) computed inline;
the access itself calls a focused extern "C" helper (jit_read_u*/
jit_store_u*) that reconstructs &dyn MemoryAccess (and &PpcContext for
stores) from JitEnv and calls the SAME big-endian trait methods the
interpreter uses -> MMIO routing / mem-watch / page-version bumps identical.
Stores replicate the interpreter arms' reservation-invalidation prologue
exactly (no-op without a reservation table). Load extension (zx8/zx16/
sx16/zx32) via movzx/movsx, matching each arm.

tests.rs: VecMem (backed big-endian mock) + loads_match/stores_match diff
tests (2000 seeds each) asserting GPR + full memory image + counters vs the
interpreter, incl. RA=0 and byte-swap.

Gate: golden n200m BYTE-IDENTICAL with XENIA_JIT=1; cargo test -p xenia-jit
green (9 tests). Throughput -n 200M --gpu-inline: 3.6s interp, 4.85s JIT
(down from 6.2s at Phase 1). Remaining fallback = branches (every block
terminator) + compares -> the crossover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 23:55:16 +02:00
MechaCat02
231c35a28f [iterate-4C] JIT Phase 1: native integer ALU emitters + differential tests
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>
2026-07-03 23:47:06 +02:00
MechaCat02
1d56218d83 [iterate-4C] JIT Phase 0: dynasm skeleton (all-fallback, golden byte-identical)
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
18 changed files with 5425 additions and 65 deletions

61
Cargo.lock generated
View File

@@ -1056,6 +1056,33 @@ dependencies = [
"strum",
]
[[package]]
name = "dynasm"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7d4c414c94bc830797115b8e5f434d58e7e80cb42ba88508c14bc6ea270625"
dependencies = [
"bitflags 2.11.0",
"byteorder",
"lazy_static",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "dynasmrt"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "602f7458a3859195fb840e6e0cce5f4330dd9dfbfece0edaf31fe427af346f55"
dependencies = [
"byteorder",
"dynasm",
"fnv",
"memmap2",
]
[[package]]
name = "endian-type"
version = "0.1.2"
@@ -2888,6 +2915,28 @@ dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -5047,6 +5096,7 @@ dependencies = [
"xenia-debugger",
"xenia-gpu",
"xenia-hid",
"xenia-jit",
"xenia-kernel",
"xenia-memory",
"xenia-types",
@@ -5117,6 +5167,17 @@ dependencies = [
"xenia-types",
]
[[package]]
name = "xenia-jit"
version = "0.1.0"
dependencies = [
"dynasm",
"dynasmrt",
"tracing",
"xenia-cpu",
"xenia-memory",
]
[[package]]
name = "xenia-kernel"
version = "0.1.0"

View File

@@ -4,6 +4,7 @@ members = [
"crates/xenia-types",
"crates/xenia-memory",
"crates/xenia-cpu",
"crates/xenia-jit",
"crates/xenia-xex",
"crates/xenia-vfs",
"crates/xenia-kernel",
@@ -26,6 +27,7 @@ license = "BSD-3-Clause"
xenia-types = { path = "crates/xenia-types" }
xenia-memory = { path = "crates/xenia-memory" }
xenia-cpu = { path = "crates/xenia-cpu" }
xenia-jit = { path = "crates/xenia-jit" }
xenia-xex = { path = "crates/xenia-xex" }
xenia-vfs = { path = "crates/xenia-vfs" }
xenia-kernel = { path = "crates/xenia-kernel" }
@@ -37,6 +39,9 @@ xenia-analysis = { path = "crates/xenia-analysis" }
xenia-ui = { path = "crates/xenia-ui" }
# External dependencies
# JIT (PPC->x64 block recompiler; runtime-gated by XENIA_JIT)
dynasm = "3"
dynasmrt = "3"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "registry"] }
tracing-appender = "0.2"

View File

@@ -12,6 +12,7 @@ path = "src/main.rs"
xenia-types = { workspace = true }
xenia-memory = { workspace = true }
xenia-cpu = { workspace = true }
xenia-jit = { workspace = true }
xenia-xex = { workspace = true }
xenia-vfs = { workspace = true }
xenia-kernel = { workspace = true }

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
{
"instructions": 200000239,
"imports": 575447,
"instructions": 200000203,
"imports": 575647,
"unimpl": 0,
"draws": 3165,
"swaps": 895,
"draws": 3208,
"swaps": 910,
"unique_render_targets": 2,
"shader_blobs_live": 6,
"texture_cache_entries": 1

View File

@@ -63,7 +63,7 @@ const GUEST_PAGE_MASK: u32 = !(GUEST_PAGE_SIZE - 1);
/// One cached basic block. Owned by [`BlockCache`]; a `&DecodedBlock`
/// is handed to the interpreter via [`BlockCache::lookup_or_build`] and
/// stays valid until the next `lookup_or_build` on the same slot.
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct DecodedBlock {
/// Guest PC at which this block starts. Used as the slot tag.
pub start_pc: u32,
@@ -185,6 +185,18 @@ impl BlockCache {
}
}
/// Decode a standalone `DecodedBlock` at `start_pc` against `mem`, computing
/// its `page_version` from memory (the same `(start_pc, page_version)` key the
/// `BlockCache` uses). Unlike [`BlockCache::lookup_or_build`] this does not
/// touch any cache — it is used by the JIT region compiler to decode the
/// straight-line/same-page successor blocks it stitches into one compiled
/// region. `build_block` stops at the 4 KiB page boundary, so the returned
/// block is fully contained in the page whose version this records.
pub fn decode_block(start_pc: u32, mem: &dyn MemoryAccess) -> DecodedBlock {
let page_version = mem.page_version(start_pc);
build_block(start_pc, mem, page_version)
}
/// Walk forward from `pc`, decoding instructions and collecting them
/// into a `DecodedBlock`. The walk stops on the first of:
/// - a [`PpcOpcode::terminates_block`] true (the terminator IS

View File

@@ -213,6 +213,22 @@ pub fn step_block(
result
}
/// Execute exactly one already-decoded instruction — the JIT's interpreter
/// fallback (`xenia-jit`). Identical to the body of [`step`]/`step_block`
/// EXCEPT it does **not** bump `cycle_count`/`timebase`: the JIT owns the
/// per-instruction counter increments so that a mix of native and
/// fallback opcodes retires exactly one tick each, in order, byte-identical
/// to the interpreter. `execute` itself advances `ctx.pc` (each arm does
/// `ctx.pc += 4` or sets a branch target), same as the interpreter path.
#[inline]
pub fn interpret_one(
ctx: &mut PpcContext,
mem: &dyn MemoryAccess,
instr: &DecodedInstr,
) -> StepResult {
execute(ctx, mem, instr)
}
/// Execute a decoded instruction, updating context and memory.
fn execute(ctx: &mut PpcContext, mem: &dyn MemoryAccess, instr: &DecodedInstr) -> StepResult {
match instr.opcode {

View File

@@ -586,6 +586,23 @@ impl Scheduler {
}
}
/// True if HW slot `hw_id` currently has at least one Ready/ServicingIrq
/// thread (from the cached `non_empty_runnable` bitmap — O(1), no scan).
/// Used by the free-running parallel executor so an idle-slot worker can
/// park instead of spinning through a full `worker_prologue`.
#[inline]
pub fn slot_runnable(&self, hw_id: u8) -> bool {
(self.non_empty_runnable & (1 << hw_id)) != 0
}
/// True if ANY HW slot has a runnable thread (O(1)). Lets the free-running
/// coordinator distinguish "all slots idle → advance time / check deadlock"
/// from "work in flight".
#[inline]
pub fn any_runnable(&self) -> bool {
self.non_empty_runnable != 0
}
// ----- Compat accessors (preserve the pre-Axis-1 hw_threads[i].ctx pattern) -----
/// Read-only context of the currently-running thread on `hw_id`.

View File

@@ -0,0 +1,12 @@
[package]
name = "xenia-jit"
version.workspace = true
edition.workspace = true
license.workspace = true
[dependencies]
xenia-cpu = { workspace = true }
xenia-memory = { workspace = true }
dynasm = { workspace = true }
dynasmrt = { workspace = true }
tracing = { workspace = true }

1514
crates/xenia-jit/src/emit.rs Normal file

File diff suppressed because it is too large Load Diff

1252
crates/xenia-jit/src/lib.rs Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -648,6 +648,15 @@ impl KernelState {
}
}
/// The registered import-thunk address band `(lo, hi)`, set once at load
/// and never mutated during execution. Exposed so the parallel-mode
/// unlocked superblock driver can cache it and run its chain-break check
/// without touching `KernelState` (which is behind the kernel mutex).
#[inline]
pub fn thunk_addr_band(&self) -> Option<(u32, u32)> {
self.thunk_addr_band
}
/// Resolve a `(module, ordinal)` to its registered thunk address.
pub fn resolve_thunk(&self, module: ModuleId, ordinal: u16) -> Option<u32> {
self.thunks_by_ordinal.get(&(module, ordinal)).copied()

View File

@@ -125,6 +125,29 @@ fn gamepad_key(state: &xenia_hid::GamepadState) -> u128 {
u128::from_be_bytes(bytes)
}
/// Whether user 0 has a default always-connected, idle (no-buttons) virtual
/// controller. **Default ON** — matches canary, whose default input drivers
/// (xinput / winkey) present a connected pad, so a title screen advances to its
/// "Press A" input-poll state instead of stalling forever on
/// DEVICE_NOT_CONNECTED (a console always exposes controller slots; a real
/// gamepad under `--ui` supplies actual button state, otherwise the pad is idle).
/// Set `XENIA_NO_PAD=1` to present NO controller (reproduces the raw headless
/// no-HID trajectory).
fn virtual_pad_enabled() -> bool {
use std::sync::OnceLock;
static V: OnceLock<bool> = OnceLock::new();
*V.get_or_init(|| {
!std::env::var("XENIA_NO_PAD")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
})
}
/// True iff user 0 has a controller: a real host gamepad, or the default pad.
fn user_connected(state: &KernelState, user: u32) -> bool {
state.ui.as_ref().is_some_and(|ui| ui.is_connected(user)) || (user == 0 && virtual_pad_enabled())
}
fn xam_input_get_capabilities(
ctx: &mut PpcContext,
mem: &GuestMemory,
@@ -133,8 +156,7 @@ fn xam_input_get_capabilities(
// r3 = user_index, r4 = flags, r5 = out X_INPUT_CAPABILITIES*
let user = ctx.gpr[3] as u32;
let out_ptr = ctx.gpr[5] as u32;
let connected = state.ui.as_ref().is_some_and(|ui| ui.is_connected(user));
if !connected {
if !user_connected(state, user) {
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
return;
}
@@ -146,15 +168,16 @@ fn xam_input_get_state(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut Kern
// r3 = user_index, r4 = flags, r5 = out X_INPUT_STATE*
let user = ctx.gpr[3] as u32;
let out_ptr = ctx.gpr[5] as u32;
let Some(ui) = state.ui.as_ref() else {
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
return;
};
if !ui.is_connected(user) {
if !user_connected(state, user) {
ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64;
return;
}
let gamepad = ui.snapshot_gamepad();
// Real host gamepad if present, else the idle virtual pad (XENIA_VIRTUAL_PAD).
let gamepad = state
.ui
.as_ref()
.map(|ui| ui.snapshot_gamepad())
.unwrap_or_default();
let key = gamepad_key(&gamepad);
if key != state.last_input_bytes {
state.input_packet_number = state.input_packet_number.wrapping_add(1);

View File

@@ -155,6 +155,15 @@ impl GuestMemory {
.load(std::sync::atomic::Ordering::Relaxed)
}
/// 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]
pub fn mmio_access_count_ptr(&self) -> *const u64 {
self.mmio_access_count.as_ptr() as *const u64
}
#[inline]
fn bump_mmio_access(&self) {
self.mmio_access_count

87
native-gate.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# native-gate.sh — functional correctness gate for the iterate-4E canary-model
# native-threads rework (host-thread-per-guest-thread, non-deterministic).
#
# Byte-identical goldens cannot gate a multi-threaded run (OS interleaving is
# nondeterministic), so this script is the REPLACEMENT oracle. It runs three
# checks and exits 0 iff all pass:
#
# 1. LOCKSTEP GOLDEN (flag-off): the deterministic default path stays
# byte-identical on sylpheed_n200m.json. This is the safety net that
# proves the native-mode work did not disturb the reference path.
# 2. NATIVE RENDER MILESTONE: native mode (XENIA_NATIVE_THREADS=1) boots far
# enough to render — draws>0 && swaps>0 in the run digest — proving the
# executor runs guest CPU + drives the GPU end-to-end without hanging.
# 3. NATIVE DEADLOCK STRESS: parallel_stress_short under XENIA_NATIVE_THREADS=1
# with --halt-on-deadlock; N back-to-back short runs with no panic/hang,
# surfacing lost-wakeups / lock-order inversions the single run misses.
#
# Usage: [SYLPHEED_ISO=...] native-gate.sh [milestone_n] [milestone_timeout_s]
# milestone_n default 200000000 (renders; matches the golden anchor)
# milestone_timeout_s default 120
#
# The binary must be built first (the caller owns the build so the OOM guardrail
# CARGO_BUILD_JOBS=4 / free-check stays explicit):
# CARGO_BUILD_JOBS=4 cargo build --release
set -u
cd "$(dirname "$0")" || exit 2
BIN=./target/release/xenia-rs
ISO_CHECK=sylpheed.iso
MN="${1:-200000000}"
MTO="${2:-120}"
DIGEST=/tmp/native-gate-digest.json
LOG=/tmp/native-gate
mkdir -p "$LOG"
fails=0
hr(){ printf '=%.0s' {1..64}; echo; }
[ -x "$BIN" ] || { echo "FAIL: build first: CARGO_BUILD_JOBS=4 cargo build --release"; exit 3; }
# ---------------------------------------------------------------- 1) golden
hr; echo "[1/3] LOCKSTEP GOLDEN — flag-off byte-identity (sylpheed_n200m)"; hr
cargo test --release -p xenia-app --test sylpheed_oracles -- \
--ignored --nocapture sylpheed_n200m >"$LOG/golden.log" 2>&1
rc=$?
if [ $rc -eq 0 ]; then echo " PASS (golden byte-identical)"; else
echo " FAIL rc=$rc — see $LOG/golden.log"; tail -20 "$LOG/golden.log"; fails=$((fails+1)); fi
# ------------------------------------------------------- 2) native milestone
hr; echo "[2/3] NATIVE RENDER MILESTONE — XENIA_NATIVE_THREADS=1, -n $MN"; hr
rm -f "$DIGEST"
XENIA_NATIVE_THREADS=1 timeout "$MTO" "$BIN" check "$ISO_CHECK" \
-n "$MN" --gpu-inline --out "$DIGEST" >"$LOG/milestone.log" 2>&1
rc=$?
pkill -x xenia-rs 2>/dev/null
if [ $rc -ne 0 ]; then
echo " FAIL emulator rc=$rc (timeout=$MTO s) — see $LOG/milestone.log"
tail -20 "$LOG/milestone.log"; fails=$((fails+1))
elif [ ! -f "$DIGEST" ]; then
echo " FAIL no digest written — see $LOG/milestone.log"; fails=$((fails+1))
else
read -r draws swaps instrs < <(python3 - "$DIGEST" <<'PY'
import json,sys
d=json.load(open(sys.argv[1]))
print(d.get("draws",0), d.get("swaps",0), d.get("instructions",0))
PY
)
echo " digest: instructions=$instrs draws=$draws swaps=$swaps"
if [ "${draws:-0}" -gt 0 ] && [ "${swaps:-0}" -gt 0 ]; then
echo " PASS (native mode renders)"
else
echo " FAIL (native mode did not render: draws=$draws swaps=$swaps)"; fails=$((fails+1)); fi
fi
# ---------------------------------------------------------- 3) native stress
hr; echo "[3/3] NATIVE DEADLOCK STRESS — parallel_stress_short (native)"; hr
XENIA_NATIVE_THREADS=1 cargo test --release -p xenia-app --test parallel_stress -- \
--nocapture parallel_stress_short >"$LOG/stress.log" 2>&1
rc=$?
if [ $rc -eq 0 ]; then
grep -o "runs=[0-9]* ok=[0-9]* failed=[0-9]*" "$LOG/stress.log" | tail -1
echo " PASS (no deadlock/panic)"
else
echo " FAIL rc=$rc — see $LOG/stress.log"; tail -20 "$LOG/stress.log"; fails=$((fails+1)); fi
hr
if [ "$fails" -eq 0 ]; then echo "NATIVE GATE: PASS (3/3)"; exit 0
else echo "NATIVE GATE: FAIL ($fails/3 checks failed)"; exit 1; fi

View File

@@ -19,7 +19,9 @@
# resumes (tid25 start_entry 0x82506588 / tid26 0x825065b8). Full per-run log kept under
# /tmp/sylph-run/.
set -u
cd "/home/fabi/RE - Project Sylpheed/xenia-rs" || exit 2
# cd to this script's own dir (was a hard-coded '/home/fabi/RE - Project Sylpheed/
# xenia-rs' that went stale when the tree was renamed 'RE Project Sylpheed').
cd "$(dirname "$(readlink -f "$0")")" || exit 2
RUNS="${1:-6}"; N="${2:-3000000000}"; TO="${3:-180}"; XGREP="${4:-}"
OUT=/tmp/sylph-run; mkdir -p "$OUT"
BIN=./target/release/xenia-rs

8
zq.py
View File

@@ -14,9 +14,13 @@ Usage:
zq.py grep <substr> # instructions whose operands LIKE %substr%
zq.py find <word_hex> # instructions whose raw word == value (e.g. a ptr)
"""
import duckdb, sys
import duckdb, sys, os
DB = '/home/fabi/RE - Project Sylpheed/xenia-rs/sylpheed.db'
# Resolve the DB next to this script so the tool survives the tree being renamed
# (the old hard-coded '/home/fabi/RE - Project Sylpheed/...' path went stale when
# the dir became 'RE Project Sylpheed'). Override with $SYLPHEED_DB.
DB = os.environ.get('SYLPHEED_DB',
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'sylpheed.db'))
c = duckdb.connect(DB, read_only=True)
H = lambda x: '0x%08x' % x