The tier mis-compiled and has been hard-gated off since 2026-06-25; overlay
gaps fall to gcc/tcc shards and the dirty-RAM interpreter, unchanged.
Re-do on master of the removal validated on the stale branch (6da8c02),
reconciled with everything master grew since the fork:
- Delete overlay_sljit.{c,h}, overlay_compile_worker.{c,h}, lib/sljit,
runtime/tests/sljit_*.c, and the runtime.cmake sources/include-dirs.
- Strip the sljit provider/backend from code_provider.{c,h},
overlay_backend.{c,h} ("sljit" in overlay_backend/PSX_OVERLAY_BACKEND now
degrades to auto so stale configs still boot), main.cpp, debug_server.c
(sljit_status/sljit_async/sljit_try TCP commands), overlay_loader.{c,h}
(live policy, JIT-on-miss, persisted-shard cache, async worker glue,
psx_sljit_call/cop2/memx host helpers — reachable only via the removed
cpu->sljit_helpers table), and cpu_state.h (SLJIT_HLP_* enum + the
sljit_helpers[] member; CPUState layout change reshards via the cg tag).
- Master-only entanglement resolved before deletion: 28165d6's Einhander WS
player-X bound emit existed in three paths (gcc code_generator, sljit,
dirty_ram_interp). The gcc shard link contract now forwards
psx_ws_player_x_bound (ABI v13, previous commit), so deleting the sljit
emit path loses no widescreen behavior; ffc6688's game-dispatch block
inside the dead psx_sljit_call helper goes with it.
- Reword comments that described sljit as a live backend (gpu.{c,h},
ws_*_detect.h, cosim_state.c, dirty_ram_interp.c, autocompile.h,
boot_state.h, config_loader.h, compile_overlays.py, and the CPS marker
comments the recompiler emits).
recompiler psxrecomp-game builds clean; game runtimes rebuilt + boot-verified
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the reusable Gaussian interpolation slice from PR #16 while keeping its reverb engine on a separate branch.
Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Apply the PS1 1023x511 size rule to every polygon triangle, line, and polyline continuation before widescreen transforms while preserving textured-command state latching.
Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Admit post-EXE RAM to the dirty interpreter only after a real control transfer and an existing MIPS decodability check. Keep the already-merged overlay initialization guard out of this focused change.
Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Port Douglas Vanderveer's manifest-driven macOS/Linux overlay discovery onto the current cache index and exact-dispatch loader. Validate both filename key fields, preserve ABI cleanup and rescan idempotence, and cover the POSIX primitives with a real shared-library fixture.
Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
Adapt only the fail-closed overlay lifecycle checks from mstan/psxrecomp#16 commit e6809ccf7d. Add a focused structural regression test and record the excluded dirty-interpreter and game/runtime work.
Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Document the overlap with kem0x PR #14 and explicitly record the game-specific and unrelated changes excluded from this branch.
Co-authored-by: Kareem Olim <kareemolim@gmail.com>
Comprehensive 3-way merge of the dev/ape-escape branch into master for
the isolated wt/ape-escape worktree. Brought in every complementary ape
change automatically (dma.c, memory.c interrupt byte/half lanes,
function_analysis discovery, main_bios vector seeds, iso_reader
iso_sector_count, config_loader, crash-trace fields, headers). Resolved
the 13 conflicts by rule:
- CPS dispatch / recompiler (code_generator, full_function_emitter,
dirty_ram_interp, traps, main_psx) -> KEEP MASTER (evolved system).
- CD model (cdrom.c) -> take ape (the model that booted Ape).
- interrupts.c/.h -> reverted to MASTER (ape's interrupt-delivery is
dispatch-coupled; the merged Frankenstein stormed).
- sio/debug_server/freeze_heartbeat/main.cpp -> keep master (evolved).
- crash_trace.c -> master (ape freeze-dump fns omitted to avoid the
debug-ring cascade).
STATUS: does NOT boot yet. ape-cdrom + master-interrupts derails to
PC=0 at frame ~2092 (insta-close); ape-cdrom + ape-interrupts storms
(297M exception re-entries) at frame 2077. Root finding: ape's CD
anti-storm is coupled to ape's interrupt-delivery + call-depth + EPC
model, which master replaced with CPS continuation-passing, so the CD
subsystem does not forward-port as pure hardware simulation. Preserved
as the reference state for a design consult on bridging the two models.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the memory-backed GPR model (every instruction loaded operands
from cpu->gpr[] and stored results back) with a block-local cache of MIPS
GPRs in host SAVED registers (S2..). Saved registers are callee-saved, so
they survive the read_/write_ icalls a plain load/store emits; the cache is
only flushed/reset at basic-block boundaries and around helper/call icalls
that touch cpu->gpr[] in memory. Prologue cost scales with register
pressure (saveds sized per-fragment from a distinct-GPR popcount).
Invariants: flush dirty->memory before every control transfer and before a
helper/call that reads gpr; reset (drop mappings) at branch-target labels
(after a preceding flush) and after any helper/call that writes gpr; gpr[0]
never cached.
Validation:
- Standalone emitter harness 66/66 (runtime/tests/sljit_emit_test.c).
- Offline differential (runtime/tests/sljit_offline_diff.c, --dump): the
new emitter is BYTE-IDENTICAL to the pre-regalloc (HEAD) emitter across
313 overlay functions, run from identical state + call stub. This is the
primary correctness proof — stronger than a single live diff.
- sljit_sp_probe.c: $sp-balance probes (prologue/epilogue, branch-delay,
register-sp, calls, loops).
A regalloc bug can only surface as a same-state differential divergence
(shards run live only after passing it), never a silently-wrong shard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
One batch widening to cover most remaining overlay instruction classes;
validated end-to-end via the live differential rather than per-op.
- jr rX (computed / jump tables): CTRL_TAILJUMP — capture the target before
the delay slot, then cpu->pc = target and return; the dispatch loop
re-dispatches (matches the interpreter's jr). jr $ra stays the clean-
return terminator. (A terminator-less tail-dispatch fragment still
declines — safe.)
- GTE/COP2: MFC2/CFC2/MTC2/CTC2/cmd (op 0x12), LWC2 (0x32), SWC2 (0x3A)
via psx_sljit_cop2(), a runtime helper mirroring the interpreter exactly.
- Unaligned mem: LWL/LWR/SWL/SWR via psx_sljit_memx(), likewise.
- BLTZAL/BGEZAL: link $ra = pc+8 before the delay slot, then branch.
Helper-emitted classes get parity by construction (the helper IS the
interpreter's code). Harness 66/66 (regression); runtime builds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The big coverage unlock — the emitter now accepts functions that call
others, not just leaves.
- New runtime helper psx_sljit_call(cpu,target,return_pc,check) in
overlay_loader.c, mirroring the dirty-RAM interpreter's call path
EXACTLY (psx_dispatch_game_compiled / overlay_loader_call_native /
psx_dispatch_call + g_psx_call_bail + psx_call_contract). Returns 1 if
the shard must return immediately (transfer/bail unwind), 0 to continue.
- Emitter: jal/jalr -> CTRL_CALL. Link reg + (jalr) dynamic target are
written BEFORE the delay slot; the helper is called AFTER it via S2
(helper address) with the result driving a conditional return. jal uses
an absolute target, jalr a dynamic one captured in S1.
- KSEG integration fix: the loader now passes the VIRTUAL entry to the
emitter (was passing phys with image_base_vram=0), so return_pc and
jal/J targets carry the KSEG bits the guest uses. The standalone harness
passed a virtual entry, which is why it stayed green; the live
differential caught the phys/virtual mismatch (saved $ra off by the
0x80000000 KSEG bit).
Verified: sljit_emit_test.c extended (jal, jalr, transfer/bail) -- 66/66
pass. Full psx-runtime build clean. Live differential re-validation of
the KSEG fix is the next step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- DIV/DIVU emit branch-guarded host DIVMOD_S32/U32: the divide-by-zero
and INT_MIN/-1 cases (which trap host idiv) are intercepted with local
sljit branches BEFORE the divide, reproducing the interpreter's defined
results exactly (lo=(a<0)?1:-1,hi=a for /0; lo=INT_MIN,hi=0 for the
signed overflow; lo=0xFFFFFFFF,hi=a for unsigned /0).
- J (absolute): fragment-relative target = (target26<<2 & 0x1FFFFFFF) -
entry_phys; reuses the branch machinery's unconditional arm. classify_
control now takes entry_phys; CTRL_JUMP added.
Verified: sljit_emit_test.c extended (signed/unsigned div incl. /0 both
signs + INT_MIN/-1 overflow; forward-J skip) — 52/52 pass. Full
psx-runtime build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Widen the leaf emitter from single-block to multi-block leaf functions,
and add the mult/hi-lo arithmetic family.
- Two-pass compile: pass 1 linearly scans to the jr $ra terminator,
collecting PC-relative branch targets + delay-slot offsets and
validating targets (in-range, 4-aligned, not landing on a delay slot);
pass 2 emits an sljit label per target, captures each branch predicate
into a saved reg (S1) BEFORE its delay slot, emits the delay slot, then
the conditional jump, and binds jumps to labels at the end.
- Branches: BEQ/BNE/BLEZ/BGTZ/BLTZ/BGEZ.
- Arithmetic: MULT/MULTU (64-bit sign/zero-extend -> MUL -> split into
hi/lo) + MFHI/MFLO/MTHI/MTLO.
- Still deferred to the interpreter (abort): J/JAL/JALR, BLTZAL/BGEZAL,
DIV/DIVU, LWL family, GTE/COP, jr-tables.
Verified: sljit_emit_test.c extended with loops (backward branch),
forward conditionals (max), and signed/unsigned mult — 40/40 pass. Full
psx-runtime build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MIPS->sljit emitter, paralleling dirty_ram_interp.c's exec_one but
emitting host code. First slice = single-block LEAF functions ending in
`jr $ra`; everything else aborts the whole fragment to the interpreter
(precision over recall).
- overlay_sljit.c: memory-backed GPRs, 32-bit ops (R3000A wraparound),
loads/stores via icall through the cpu read/write callbacks (same
MMIO/page-watch/dirty-tracking path as the interpreter). Opcodes:
SLL/SRL/SRA/SLLV/SRLV/SRAV, ADD(U)/SUB(U), AND/OR/XOR/NOR, SLT(U),
ADDI(U)/SLTI(U)/ANDI/ORI/XORI/LUI, LB/LH/LW/LBU/LHU/SB/SH/SW, SYNC.
- compile_fragment now returns CompiledFragment{fn,code_lo,code_len};
overlay_loader registers an sljit candidate (crc hashed from the live
bytes it JIT'd from, same live-byte validation as gcc candidates).
- Step 5 wiring is VALIDATION-GATED: JIT-on-miss fires only when
backend==sljit AND the same-state differential is armed (never in a
shadow run). Normal play stays byte-identical; live sljit execution
stays off until the differential gate passes.
- Debug: sljit_try <addr> probe; sljit_status reports shards_registered.
Verified: runtime/tests/sljit_emit_test.c (links overlay_sljit.c +
sljitLir.c only) JITs hand-built leaf functions and checks output —
26/26 pass incl. gpr0 invariant, sign/zero-ext, signed/unsigned compare,
and the decline cases. Full psx-runtime build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root cause (freeze dump 1781046069, addr2line-verified): the wall-clock
pacing loop in sdl_vblank_present read the performance counter twice per
iteration — once in the while-condition, once to compute remaining time.
If the frame deadline was crossed between the two reads (a preemption
window), the unsigned subtraction underflowed, *1000 overflowed, and
SDL_Delay received ~2^64/10^4 ms mod 2^32 ≈ 24.7 days at the standard
10 MHz QPC. The app parks in one SDL_Delay with every counter frozen,
wedged between 'i_stat |= VBLANK' (interrupts.c:286) and exception
delivery — which is why dumps showed i_stat=1 pinned. VBLANK was being
ACK'd every frame right up to the wedge in every dump examined; the
'VBLANK-ACK reentry storm' framing was a symptom misread.
- frame_pacing.c/h: shared race-free pacer (single counter read per
decision, compare-before-subtract, sleep clamped to one period).
Both call sites switched: main.cpp sdl_vblank_present and
beetle_main.cpp. Unit test pins the underflow case.
- Vsync self-heal: the 'recovered storm' dump (1781045865) was 8/8
main-thread samples inside wglSwapBuffers — NV driver blocking ~1.5s
per present. After 3 presents >250ms, drop driver vsync (we pace to
59.94 Hz ourselves). Counters surfaced in the freeze dump.
- Watchdog classifier: 'reentry_storm' threshold (5K/sec) was below the
healthy baseline (~2K reentry blocks/frame × 30-60 fps), mislabeling
normal play and host stalls — that label misdirected two prior
investigations. Now normalized per frame (>20K/frame), plus a new
slow_frames kind (<5 fps) with multi-sample stack capture.
Verified: test_frame_pacing ALL PASS; live Tomba paced at 59.962 fps
over 20s (single-process measurement).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>