Commit Graph

81 Commits

Author SHA1 Message Date
Matthew Stanley 49fe052a3e Add uncharged guest calls: run a guest function in zero guest time
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:53:08 -07:00
Matthew Stanley a734cd9034 Record GPU list CPU holds, per-source exception cycles and recent C0 readbacks in always-on rings
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:52:52 -07:00
Matthew Stanley 167babab9f Merge Tomba snapshot continuation fix into master
Keep upstream atomic v9 restore and handoff serialization. Add scoped snapshot deferral for host adapters, reset abandoned scopes at scheduler escapes, and rebind enabled mod hooks before restored execution. The backport parent preserves the tested Tomba v0.15 savestate/cache lineage. Focused host-snapshot, restored-handoff, and mod-runtime tests pass.
2026-10-04 17:59:57 -07:00
Matthew Stanley 6426b25f3d Preserve restored handoff and snapshot host-call continuations 2026-10-04 17:51:23 -07:00
Matthew Stanley 7c7133fbb4 Integrate MMX6 adaptive renderer with current framework master
Preserve current segmented dispatch, active-plan mod hooks and software scale limits. Extend function filters through the active plan, retain the last-store pointer with overlay ABI 25, and keep default config fingerprints stable. Refresh BIOS emitter stamps after byte-identical regeneration and reconcile regression fixtures.
2026-09-30 20:23:16 -07:00
Shokunin 825a89a9c5 runtime: keep host stores out of IsC, as DMA is
Only CPU stores are cache-isolated. Mods, the FMV skip, debug pokes, the
bg2d ring fill and the exception sentinel write guest memory from the host;
they now go through psx_host_write_*, which bypasses isc_store the way DMA
does, so a host store inside a FlushCache window reaches memory instead of
rewriting an I-cache tag. The render-pass checkpoint restores the new depth
counter like the DMA depth. isc_store_test covers it (a lane-0 host store in
tag-test mode would otherwise mark a cold line valid).

Docs: the SCPH-1001 residual starts at kernel init 0x598, and without #429
the fix removes 113 of the 126 OpenBIOS flush cycles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:55:26 -07:00
Shokunin d22ebac9a4 Host-timed render passes for true in-between frames
psx_mod_render_pass_plan() / psx_mod_render_pass() let a trusted plugin
redraw its game between two logic ticks with the game's own draw code. A
pass is a sandboxed nested guest call:

- time is frozen: cycles are counted (GTE and mult/div deadlines keep
  working) but no device advances, no VBlank or device event fires and no
  interrupt is delivered; GPU linked-list DMA and delayed completions finish
  synchronously (psx_cycles freeze, interrupts, dma);
- stores go straight to RAM/scratchpad, bypassing code-page tracking,
  overlay watch and write traces; MMIO is an allow-list (GP0, GP1 DMA mode /
  info, GPU and OTC DMA, I_STAT/I_MASK) and SPU, CD, timer and other device
  stores are dropped and counted (memory.c);
- afterwards CPU (with the GTE), RAM, scratchpad, I-cache tags, interrupt,
  timer, DMA and GPU registers are restored exactly (render_pass.c, a GPU
  register checkpoint without savestate side effects), with a guest-cycle
  watchdog that rolls a runaway pass back;
- on OpenGL the declared display rect (hr colour + mask stencil, raw mirror,
  native-wide band, CPU VRAM rows) is backed up, drawn, captured and rolled
  back; writes outside it are journaled and restored, or refused.

The presenter keeps two generations of images (the game's own frame plus
one image per pass phase), promotes one when the flip-aware source sees the
display flip to its rect, and shows the newest image at each output
deadline's phase, crossfading across gaps when passes were shed. Presents
that fall due while passes run are made between them. The plan predicts the
output deadlines of the coming frame and sheds passes to fit a host-time
budget learnt from the presenter's idle time (PSX_RENDER_PASS_BUDGET).

PSX_MOD_FRAME_INTERPOLATION_HOLD repeats the newest frame where no pass
image applies, so a pass-driven title never shows anything later than stock.

Default off: nothing runs unless a plugin calls the API, and the plan
refuses in netplay, rollback, rewind, turbo, self-check resimulation, while
suspended (FMV) or without GL flip-aware interpolation. Pass writes are kept
out of frame_fingerprint. Debug: render_pass_stats, render_pass_dump,
PSX_RENDER_PASS_VERIFY=1 (state + VRAM hashes around every pass).

The freeze API lives in the runtime-only psx_cycle_freeze.h. psx_cycles.h is
part of the overlay codegen hash and stays byte-identical, so titles that
never use passes keep their overlay caches and savestates.

Tests: render_pass_plan_test (deadline-exact phases per rate, shedding,
selection), render_pass_freeze_test (10^6 frozen cycles, exact restore,
watchdog), render_pass_guards (sandbox choke points; the codegen-hashed headers stay
free of the pass API).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:47 -07:00
Matthew Stanley f51617bb7d runtime: VBlank rate follows the GPU video mode (GP1(08h) bit 3), not the disc region
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 15:38:21 -07:00
Matthew Stanley c564ccd1f1 runtime: route every RAM consumer through one live 2/8 MiB geometry
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 06:31:33 -07:00
Matthew Stanley 69570b4760 Add opt-in function filters with safe snapshot boundaries 2026-09-19 22:42:49 -07:00
Matt Stanley d4c45adf62 fix(sio): remove title-specific card IRQ repair and opt-in (#362) 2026-09-13 16:33:46 -07:00
Kevin 8eb29d255d runtime: cheap SPUCNT gate for the sample-event scheduler
psx_spu_sample_event_service() and psx_spu_sample_event_cycles_to_next()
built a full SpuGlobalState snapshot (memset + ~40 register reads + three
24-voice loops) on every device-service call and every deadline recompute,
just to test one bit (SPUCNT & 0x40, SPU IRQ9 enable). Under an
MMIO-polling guest loop — every PS1 wait loop, loudest on the BoF3 Capcom
FMV whose LOGO overlay polls per tiny basic block — the snapshot was ~40%
of emu-thread wall time (gdb stack-sampled, clean boot, live FMV).

Add spu_ctrl_read() (one array load) and gate on it. Semantics identical:
same register, same bit.

Measured (BoF3 Capcom FMV, clean boot, headless vblank raises/s):
  Debug build:           28.5 -> ~52
  RelWithDebInfo build:  97.6 uncapped (~1.6x realtime; paced holds 60)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 15:54:47 -04:00
Matthew Stanley 47bda8172e runtime: fix savestates at dirty boundaries 2026-08-29 22:08:31 -07:00
Biggs McWedge 96c3a46ebd Make VBLANK_CYCLES editable at runtime
This change replaces the constant `VBLANK_CYCLES` with a global variable that can be edited at runtime. This allows the timing to be changed by other code, which is needed for PAL support.
2026-08-30 00:35:58 +01:00
Pegasus a74fd9f7f8 runtime: schedule SPU IRQ samples by guest clock
(cherry picked from commit 1a973806ca0af75355a62bbc59165ceff8ce294a)
2026-08-26 21:43:28 +03:00
Matthew Stanley cd68169cb8 savestate: reject dirty-pump snapshot contexts 2026-08-25 23:49:59 -07:00
Alex Vanderveen d4ea4ade1f fix crash path from bad splash screen bug, and optimize cross platform pacing + rollback 2026-08-14 02:25:59 -04:00
Alex Vanderveen a73a5c1093 rewind feature 2026-08-12 02:42:30 -04:00
Alex Vanderveen 12f4e3281e lobby rematch & bios hotswap stability 2026-08-11 18:59:19 -04:00
Alex Vanderveen 251456ee86 fix ape-escape 2026-08-09 00:36:06 -04:00
Alex Vanderveen 81e0e46b8c gating flush mem technique 2026-08-08 02:10:49 -04:00
Alex Vanderveen 593de584b7 Windows netplay + gating SIO/IRQ hacks to netplay 2026-08-07 17:40:02 -04:00
Alex Vanderveen 94cb3807a9 Merge origin/master into integrate/game-codegen-hooks-into-rollback
Keep rollback/netplay multiplayer routing, dual-raster GL present, and TOC
fingerprints while taking master's CHD support, mod load acceleration,
adaptive widescreen, and geometry/perspective APIs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 22:50:38 -04:00
Matthew Stanley 6e6a0b3ed8 spu: implement reverb, noise, sweeps and SPU IRQ; make CAUSE.IP2 combinational
Closes the four gaps named in issue #103 and lands the separable, verified
parts of PR #102. See docs/internal/SPU_FIDELITY_103.md for the per-piece
disposition and the oracle-verification queue.

SPU DSP (issue #103), clean-room from hardware documentation:

- Reverb: the documented 22050 Hz engine — vLIN/vRIN input, same-side and
  crossed different-side IIR reflections, 4-tap comb early echo, both
  all-pass filters, vLOUT/vROUT output, and the work-area address advance.
  Gated on SPUCNT bit 7 for WRITES only; reads, IRQ checks, address advance
  and output always run, as on hardware. Addressing is confined to
  [mBASE, 0x80000). Per-voice sends from EON, CD send from SPUCNT bits 0+2.
- Noise: 16-bit LFSR with the documented parity feedback, clocked at the
  SPUCNT bits 8-13 rate; NON voices substitute it for their ADPCM sample.
- Volume sweeps: all 24 voice L/R plus main L/R. Bit 15 selects sweep vs
  direct; sweep is a live envelope on the same rate machinery as ADSR, and a
  guest read of a sweeping register returns the live level.
- Capture buffers: CD L/R and voice 1/3 output written to SPU RAM, so a game
  that parks the IRQ address there still gets its interrupt.
- SPU IRQ (I_STAT bit 9): checked at every SPU RAM access class — FIFO, DMA
  both directions, ADPCM block fetch, capture writes, reverb work-area
  accesses — gated on SPUCNT bit 6, mirrored in SPUSTAT bit 6, acked by
  clearing SPUCNT bit 6.

The CD-only fast path in spu_render was removed rather than extended: the
SPU now has per-frame work that must run in every path, and an FMV (XA audio
with zero active voices) is exactly the CD-with-reverb case.

This is a clean-room implementation from the psx-spx/nocash register map and
documented algorithm. Beetle's source was deliberately not consulted: it is
GPL-2.0-or-later and this project is PolyForm Noncommercial. PR #16 was
parked for exactly that reason and PR #13's reverb was separately wrong (it
gated on bit 15 of dAPF1, an address offset, instead of SPUCNT bit 7). The
one place this knowingly deviates from hardware is the 22.05 -> 44.1 kHz
reconstruction filter, which the documentation does not specify; it is
isolated in a single function and flagged for oracle comparison.

CAUSE.IP2 (from PR #102):

IP2 is combinational on R3000A — it mirrors the interrupt line and falls the
instant the guest acks I_STAT or masks the source. It was being OR'd in at
delivery and never cleared, in TWO places (the compiled path and
psx_interpreter.c, the latter missed by PR #102), leaving a phantom pending
interrupt that can spin a kernel dispatcher looping on CAUSE.IP & SR.IM.
psx_irq_refresh_cause_ip2() is now the sole writer of bit 10, refreshed at
raise, I_STAT ack, I_MASK write, HLE context restore and power-on. Verified
against the Beetle oracle's irq.cpp/cpu.cpp rather than asserted, and
covered by test_cause_ip2_combinational.

Mid-dispatch audio pump (from PR #102), with a fix: routed through a gate
that mirrors sdl_audio_update's mute/sink decision. Pumping unconditionally
from the VBlank edge, as submitted, would have pushed real audio during every
turbo-load hard mute and defeated the freeze-in-place mute model.

SPU RAM DMA readback (from PR #102): DMA4 in the SPU->RAM direction wrote
literal zeros; SPU RAM is readable memory and titles carry state through it
across an Exec boundary.

Also fixes an unrelated build race found on the way: tools/embed_spirv.py
wrote its intermediate SPIR-V to <source>.spv, so the two runtime targets
that embed the same shaders raced on one path and one deleted the file the
other was reading.

Co-Authored-By: Alexandros Mandravillis <Alexbeav@live.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 18:16:16 -07:00
Alex Vanderveen 4ddd2d01b9 rollback patches 2026-08-01 14:13:58 -04:00
Matthew Stanley 987c6eeced Revert "Merge pull request #87 from mstan/feat/ui-5p"
This reverts commit e01347cbca, reversing
changes made to 60cb60eee6.
2026-07-31 15:37:57 -07:00
Alex Vanderveen fe403e6ce1 rollback patches 2026-07-31 17:14:35 -04:00
Alex Vanderveen b3d99b05ff rollback patching 2026-07-30 17:24:20 -04:00
Alex Vanderveen 3b88d9f9b5 rollback integrations 2 2026-07-30 01:58:30 -04:00
Alex Vanderveen 2b35b76a56 rollback engine patch 1 2026-07-29 21:44:44 -04:00
Alex Vanderveen 070fc805c4 Merge origin/master into feat/ui-5p.
Keep 5p/ICE netplay host + lobby client; take master's cycle batching,
SDL3 defaults, and dropped vendored lib/recomp-ui.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 20:17:14 -04:00
Alex Vanderveen fbad0be1b9 launcher performance, load state performance, game launch performance
it stops checking the entire PSX disc for full hash check every time the launcher runs, assuming the user's hash checked it when selecting the disc the first time and saving it to settings, so it just validates the file name and size instead, then it shares lifecycle with the launcher and the game to load the game faster, and several optimizations were made to loadstate paths which I think in the end relied on reanchoring host timings to the restored clock that came with the state
2026-07-27 11:03:10 -04:00
Henrique Guedes 6749b826d4 feat: OpenBIOS support — profile-driven BIOS recompilation
Adds PCSX-Redux's MIT-licensed OpenBIOS as a second BIOS the framework can
statically recompile, next to retail SCPH1001. Because OpenBIOS is
redistributable, a game verified against it ships its BIOS with the build —
players supply only a disc, and the launcher BIOS picker disappears.

The mechanism is general, not OpenBIOS-specific: every BIOS image gets a
bios/<STEM>.toml profile that is the single source of truth for its identity
and layout.

Recompiler
- bios/<STEM>.toml profiles: [program.image] (sha256 pin, redistributable
  flag), [recompiler.address_model] (boot-time ROM->RAM copy windows,
  kernel-bless window), install_slots, and [recompiler.runtime_exports]
  HLE anchors (0 = feature structurally unavailable on that BIOS).
- BiosAddressModel (new): validated model built from the profile, replacing
  ~9 hardcoded SCPH1001 window constants that had drifted apart across the
  emitter / discovery / game codegen. The emitted C normalize() derives
  from the same table. from_config rejects overlapping/misaligned windows
  and orderings where sequential folding != first-match. A CTest-registered
  equivalence test sweeps every RAM/ROM address against verbatim copies of
  the old helpers.
- Orphaned delay slots (the OpenBIOS-critical emitter fixes): jr/jalr
  slots are inlined before the jump resolves — previously their side
  effect was silently LOST (e.g. a case-return fragment's addiu $sp pop,
  which unwound a shifted frame and walked the exception handler's IntRP
  cursor into a wild dispatch). Orphaned branches resume at ds+4 on
  not-taken instead of re-executing the slot (a read-modify-write slot
  doubling twice broke OpenBIOS readPad). Jump targets latch pre-slot.
- MIPS-I load-delay modeling for dependent load pairs: the successor sees
  the pre-load value (OpenBIOS's cardfasttrack.s relies on it on purpose —
  "gotta break those bad emulators"). Unmodeled shapes warn at build time.
- Discovery hardening: branch-target dispatchability closure to fixpoint;
  jump tables placed at jr+8 no longer walked as code; cross-function
  continuations routed by an instruction->owner map.
- Signed MULT support (unreachable from the retail seed set; modern-GCC
  OpenBIOS emits it freely), latency-modeled like MULTU.
- Seeds from ELF symbol tables + data-section code-pointer harvest
  (tools/gen_bios_seeds_from_elf.py) — a source-built BIOS needs no
  disassembly pass. 646 OpenBIOS seeds, incl. 5 empirical dispatch-miss
  seeds.
- Stemmed outputs (<out_stem>_full.c / _dispatch.c) so two BIOSes coexist
  in one out_dir. tools/regen_bios.sh is profile-driven (--config).

Runtime
- psx_bios_image: the generated dispatch now self-describes the BIOS it
  was compiled from (identity: size/CRC32/SHA-256/wordsum; kernel-bless
  window; HLE anchors; bundled flag). Runtime SCPH1001 hardcodes removed.
- Launch check is a hard identity gate (a mismatched image under
  statically recompiled code is a guaranteed wild jump). Bundled builds
  hide the BIOS picker and treat a missing/tampered bundled image as an
  installation defect.
- Faithfulness fixes surfaced by OpenBIOS's tighter timing loops (all
  global, argued in code comments, regression-tested against SCPH1001):
  - SIO pad RX is cycle-paced like the memcard path (RX_RDY at
    shift-complete, ACK +170); total ack-IRQ latency unchanged.
    PSX_SIO_PAD_SYNC_RX=1 restores the legacy synchronous path for A/B.
  - SPUSTAT derived from SPUCNT instead of permanently-asserted busy.
  - GPUSTAT.13 (interlace field) alternates per vblank while interlaced,
    pinned 0 in progressive.
  - CD Init(0x0A) completes in ~3.9 ms instead of borrowing Beetle's
    34 ms Reset figure (OpenBIOS's short bounded retry loop livelocked).
  - Exception EPC guard accepts ROM-window block leaders, not just RAM.
- New TCP debug commands (documented in TCP_COMMANDS.md): bios_info,
  s3_smear_watch, callret_watch; write-trace ring gains $s0-$s5.

bios/openbios.bin is vendored (MIT — notice in bios/OpenBIOS.LICENSE,
attribution in THIRD_PARTY_ATTRIBUTION.md, source pin + build recipe in
bios/OpenBIOS.toml). The emitter fingerprint digest is now content-only:
existing generated/*.emitter.sha will read STALE once; re-run
tools/regen_bios.sh.
2026-07-23 17:10:23 -03:00
Alex Vanderveen 7cf9958135 FMV optimizations 2026-07-21 21:38:04 -07:00
Alex Vanderveen 31015ceab1 feat: launcher polish and recomp-net lobby netplay
Wire MotK-style lobby (recomp-net submodule, WS client, match_caps),
netplay delay-sync, and launcher UX so host sim settings stay aligned
across peers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:37:57 -07:00
Claudio Varandas 55120e102e runtime: implement COP0 software interrupts (CAUSE.IP0/IP1 vs SR.IM0/IM1)
Jackie Chan Stuntmaster (SLUS-00684) hard-blackscreens after the intro
logos: the game's exception dispatcher is re-entrant by design. Its
stage-1 handler saves SR/EPC to a single save slot, then executes
mtc0 CAUSE,0x100 + mtc0 SR,0x101 and relies on the resulting COP0
SOFTWARE interrupt (CAUSE.IP0 & SR.IM0 & IEc) to vector into its
stage-2, which acks the CD INT at the controller, delivers events, and
restores the saved SR. The runtime only ever considered the INTC line
(i_stat & i_mask) gated by SR.IM2, so the sw-int was never taken:
stage-2 never ran, CD INT3 stayed unacked (process_pending freezes the
completion countdown while irq_flag != 0), and the machine deadlocked
with the display disabled and SR parked at 0x101 (IM2 stripped -> all
hardware interrupts dead).

Three changes, all architectural (no per-game logic):

1. interrupts.c psx_check_interrupts(): a pending software interrupt
   (CAUSE & SR & 0x0300, plus IEc) is now a deliverable source. The
   fast path, the pending check, and the SR gate each consider it; the
   INTC path still requires IM2, the sw path only its own IM bit.
   Exception entry sets CAUSE.IP2 only when the hardware line is what
   is being delivered, so a pure sw-int presents the guest-written
   IP0/IP1 bits unmodified (the dispatcher discriminates its stages by
   exactly these bits).

2. interrupts.c: save/restore exception_jmpbuf across delivery. The
   sw-int nests inside the hardware-interrupt handler window; the
   nested setjmp used to overwrite the single global jmpbuf, so the
   OUTER handler's later RFE longjmp'd into the exited inner frame
   (host stack corruption, SIGSEGV).

3. dirty_ram_interp.c: deliver immediately after an interpreted MTC0
   to CAUSE or SR that makes a sw-int deliverable, with EPC = the
   committed next pc. Hardware vectors within the next instruction;
   waiting for the next block boundary opened a window in which a
   second hardware IRQ re-entered the (legitimately non-reentrant)
   guest dispatcher and destroyed its saved SR. precise_irq_deliverable
   gains the same sw-pending condition.

Verified on SLUS-00684: boots through BIOS/logos/FMV into rendered
3D gameplay at 60 fps, SR observed at the canonical 0x40000401, CD
irq_flag acked, MDEC decoding. BIOS-only boot unaffected.

Known scope limit: compiled (static) MTC0 sites do not yet trigger the
immediate check; the raising writes observed in the wild live in the
BIOS-installed, dirty-RAM-interpreted trampoline, which this covers.
2026-07-22 00:54:04 +01:00
Matthew Stanley b3bfdd9330 Fix in-handler RFE contract gaps that livelock the boot splash (Tomba 2 frame 1385)
Three-layer faithful-core fix for the exception-return contract, root-caused
live over TCP on a deterministic dbg-build repro (vblank authority vetoed
forever while the guest spun in the kernel idle loop, in_exception stuck 1):

1. Interpreter RFE backend parity: dirty-RAM RFE now calls
   psx_rfe_mark_escape() exactly like the recompiled opcode and every
   overlay shard (ABI v12), and dirty_ram_dispatch_inner takes a pending
   escape at the committed transfer before local dirty flow can keep
   interpreting the resumed code inside psx_check_interrupts' window.
2. Legacy-sentinel deliveries arm the escape too: the old blanket decline
   assumed a sentinel delivery always resumes AT the sentinel, but guest
   TCBs never hold the sentinel (Fix B anti-leak), so a kernel
   ReturnFromException restore resumes at an approximate real PC with no
   exit path armed. The take site preserves the legacy contract: a resume
   landing ON the sentinel still exits through the sentinel detector.
3. Same-thread completion of a sentinel delivery via explicit RFE /
   ReturnFromException converts to the legacy exit (restore interrupted
   GPRs, pc=0, continue the live host chain) keyed on the structural
   entry_tcb == exit_tcb test - otherwise the approximate TCB PC surfaces
   up the trampoline, severs the live chain, and the run dies one hop
   later on an un-re-enterable mid-function PC ("execution completed").

Validated: production build passes the previously-fatal splash transition
(frames 4717+ at 60fps, was deterministic death at 1385 x3 runs); dbg build
advances past the wedge (was frozen at exactly 1385); all runtime tests +
structural guards pass (guards extended to pin the interp RFE contract).
Known follow-up: the PSX_DEBUG_TOOLS build still loses one kernel CD event
through a third timing path at the same site - tracked as an open
foundation item; production config unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 19:00:29 -07:00
Matthew Stanley 05f6ef3e02 Optimize GTE fallback and isolate interpreter perf runs 2026-07-19 14:23:36 -07:00
Matthew Stanley b39387eb00 Fix exact native and static overlay dispatch 2026-07-13 05:23:16 -07:00
Matthew Stanley fb585541ec opengl: sustain full-rate gameplay and FMV presentation 2026-07-11 14:07:17 -07:00
Matthew Stanley 0cb2a77bff fix(irq): gate deferred thread-switch off dirty-pump and low-kernel sites
Ape Escape memcard save/load. The deferred in-exception cooperative
thread-switch could be honored at a boundary where a candidate resume PC
was committed but the live CPUState (stack/GPRs) still described the
previous local frame, corrupting the resumed thread.

- Keep the deferral pending when g_cosim_dirty_pump_site != 0: the
  dirty-RAM interpreter pump sites expose committed PCs for IRQ timing
  but the matching CPUState may not be materialized yet. Honor the
  switch only once execution reaches a normal interrupt poll (site 0).
- Do not defer when the interrupted EPC is in low BIOS/kernel space
  (< 0x10000): that code is already scheduler code, and deferring it
  re-enters the same VBlank EPC forever, starving the target thread.
- debug_server: name thread-event kinds 31/32/33 (defer / escape /
  stale) for observability.

Validated: Ape Escape save+load confirmed working by user; MMX6 and
Tomba 1 regression-checked with no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:50:09 -07:00
Matthew Stanley c167f74f89 runtime(irq): integrate idle-skip, exec-phase, shadow-escape fixup + setjmp epoch
The psx_check_interrupts integration point for the preceding three
changes:
- call psx_idle_note_check() before the deliverability evaluation so an
  event raised by an idle-skip is delivered in the same check, at the
  boundary real execution would have taken it;
- bracket the handler dispatch with g_exec_phase = 3 (compiled static
  text), save/restored across the setjmp loop like g_dirty_interp_active;
- bump g_exc_setjmp_epoch each time the exception setjmp frame is
  (re)armed, and call overlay_loader_shadow_escape_fixup() at the RFE
  escape-check and deferred-longjmp sites so an unwind through a live
  shadow frame restores its scoped globals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:39:50 -07:00
Matthew Stanley f612cc08e9 fix(irq): clear dirty resume-PC latch across exception handler (memcard fix #3)
The exception-handler dispatch in psx_check_interrupts already saves+clears+
restores g_dirty_interp_active and g_psx_dispatch_depth across the handler so an
RFE/escape longjmp can't leak the interrupted dirty block's state into the
handler. It MISSED g_dirty_safe_resume_pc.

The interrupted dirty block sets g_dirty_safe_resume_pc = its committed PC and
leaves it live across its own psx_check_interrupts call (the dirty pump only
restores it after that call returns). So a NESTED interrupt taken inside the
handler -- e.g. a VBLANK delivered while the kernel async-poll memory-card
callback runs (in_exception, IEc re-enabled) -- inherited the OUTER interrupted
block's stale resume PC as its EPC. same-thread-restore then matched
(cpu->pc == stale EPC) and resumed the OUTER block with the CALLBACK's registers:
the Ape Escape memcard consumer at 0x801382CC got v1 (jumptable index) = the
callback pointer 0x800B4E30, past its bounds check, and `jr jumptable[garbage]`
faulted (DISPATCH FATAL misaligned 0x00000003).

Fix: save+clear g_dirty_safe_resume_pc at handler entry, restore after the
dispatch loop, exactly mirroring the adjacent g_dirty_interp_active idiom. A
nested delivery now latches the actually-running code's PC (the callback's own
dirty pump re-sets it; compiled handler code uses s_compiled_interrupt_resume_pc).
Faithful: real hardware saves/restores all GPRs to the TCB across every exception.

irqctx_ring proof: stale-latch deliveries (exit_pc=0x801382CC, ra_saved=0x80010094
[callback], v1_saved=0x800B4E30) went many -> 0; no DISPATCH FATAL across a full
memcard-scene nav; the scene advances further ("Checking... Do not remove MEMORY
CARD"). A/B toggle: PSX_EXC_CLEAR_RESUME_LATCH=0.

Not the whole memcard fix: a separate soft stall remains (card completion event
0xF0000011 still not delivered). Tracked in .claude/GOAL.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 15:51:09 -07:00
Matthew Stanley 4f80d22096 fix(sched): defer in-exception thread switch to a clean block leader (memcard fix #2)
The Ape Escape "Checking MEMORY CARD" thread-switch smear: a genuine in-exception
ChangeThread detected deep in nested host dispatch commits a poisoned TCB context
(stale resume-PC latch ahead of the live GPRs) -> the resumed consumer faults on
a smeared jumptable index (DISPATCH FATAL).

Fix #2 (user-agreed architecture): don't honor the switch at the poisoned nested
point. Resume the outgoing thread transparently at its OWN EPC, record the switch
as pending, and honor it at the next block leader where that thread is running
with a materialized resume PC -- re-saving it cleanly there. Do NOT require the
outermost STATIC dispatch boundary: the memcard consumer lives permanently inside
the nested dirty-RAM interpreter and never reaches it; the scheduler landing
already resets the nested dispatch/call-unit counters on the longjmp, same as the
immediate-switch path.

- interrupts.c: the kind-30 escape splits into immediate (outermost / toggle-off
  / no clean resume PC) vs deferred; new honor hook at the top of
  psx_check_interrupts; always-on thread_trace kinds 31 (defer) / 32 (honor) /
  33 (stale-drop). The deferred branch restores the outgoing thread's clean
  GPRs/HI/LO/SR AND cpu->pc (the in-exception RFE had left cpu->pc at the target).
- traps.c: psx_sched_save_context / set_current_tcb / current_tcb exports over
  the scheduler's private TCB accessors.
- A/B toggle PSX_DEFER_SWITCH (default ON); =0 forces baseline immediate switch.

Validated (LLE + HLE, ring-verified): converts the smear DISPATCH-FATAL into
forward progress -- the card driver now reads 108 blocks. A/B (PSX_DEFER_SWITCH=0)
proves the fix is SOUND: the remaining stall (SIO IRQ bit 7 masked in I_MASK, card
read never completing) reproduces IDENTICALLY with the fix off, so it is a
SEPARATE, pre-existing SIO-mask bug -- the next blocker, not this fix's residual.
Goal (file-select showing File 1-4) NOT yet reached.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 12:29:53 -07:00
Matthew Stanley a6afa3ef8a diag(irqctx): record entry_sp + pump_site per IRQ delivery
Extend the always-on irqctx_ring with the live sp and dirty pump-site captured
at exception entry — the interrupted-thread TCB save records exactly this
(resume_pc=take_pc, sp=entry_sp). A pair where take_pc is a dirty return point
(0x801384AC) but entry_sp belongs to a native callee's frame is the corrupt
snapshot behind the Ape memcard native<->interp resume desync. Reader typedef +
JSON emit updated to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 09:11:03 -07:00
Matthew Stanley 88d7385c42 fix(sched): clear g_dirty_interp_active before the in-exception thread-switch escape longjmp
Fixes a hard-freeze REGRESSION in the Ape Escape "Checking MEMORY CARD"
scene introduced by rebasing the ape branch onto current master.

Root cause (confirmed by bisect toggle): master 590c236 added a
psx_check_interrupts() poll at the dirty-RAM interpreter's entry (every
64 invocations). The kind-30 in-exception thread-switch escape lives
inside psx_check_interrupts and, when the guest moves PCB[0] during the
handler, does longjmp(g_scheduler_jmpbuf) to unwind to psx_scheduler_run.
That escape path BYPASSES the dirty_ram_dispatch wrapper's restore of
g_dirty_interp_active (dirty_ram_interp.c ~1822). Before 590c236 the
escape only ever fired from COMPILED block leaders (flag already 0); the
new dirty-interp-entry poll site fires it with the flag == 1, so it LEAKS
as 1 across the unwind. The re-dispatched thread then mis-tags execution
as dirty-interp and hard-wedges (CPU halted: draw counter 0x8013AF70,
dirty_ram_insns, and thread events all static; black screen).

Fix: clear g_dirty_interp_active before the escape longjmp, honoring the
documented "cleared at the longjmp landing" contract (dirty_ram_interp.c
~205-210). psx_scheduler_run already resets g_psx_dispatch_depth on the
jmpbuf landing (traps.c ~708); g_dirty_interp_active was the one leaked
invariant this escape path missed. Call-site-agnostic: no-op from
compiled code (flag already 0).

Verified on LLE (PSX_BIOS_HLE=0): with poll ON + this fix the memcard
scene now SOFTLOCKS (draw counter climbs steadily, substate 0x8013AF56
advances 0->1) exactly like the pre-rebase build, instead of hard-
freezing. The remaining softlock ("Checking" never reaching file-select)
is the pre-existing secondary stall, tracked separately.

Also adds a diagnostic env toggle PSX_DIRTY_ENTRY_POLL (dirty_ram_interp.c)
that gates the 590c236 poll; defaults to ON (unchanged behavior), set =0
to bisect this interaction. Kept as an observability escape hatch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 20:56:54 -07:00
Matthew Stanley 938f89c8d8 wip(ape): in-exception thread-switch handling for the memcard scene
Ape Escape runs its own cooperative scheduler inside the kernel exception
handler (VBlank callback moves PCB[0]); handle the in-exception thread switch,
bump the IRQ-context ring to 4096 with a matching debug_server dump cap, and
carry the scheduler-escape trace state. WIP snapshot on the Ape branch (the
thread-smear investigation), preserved so the shared checkout can return to
master cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:31:18 -07:00
Matthew Stanley 4e6208a6e6 debug/fmv: always-on CD data-ready + CD-IRQ-delivery counters in fmv_state
Add two always-on counters (per the no-printf / ring-buffer rule) to diagnose the
MMX5 intro-FMV stall:
- cdrom.c: s_dataready_fires — CD INT1 (data-ready) raised per streamed sector,
  exposed via cdrom_get_dataready_fires().
- interrupts.c: g_cdrom_deliver_count — CD IRQ (IRQ_CDROM) exceptions actually
  taken by the guest.
Both surfaced in the fmv_state debug command (cd_dataready_fires / cd_irq_delivered).

These proved CD INT1 + CD-IRQ delivery are NOT the MMX5 FMV blocker (fires=627,
delivered=746 during the movie while the STR ring still never commits a frame) —
narrowing the bug to the CD-DMA-ch3-completion -> frame-commit path. Harmless
observability; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:51:32 -07:00
Matthew Stanley e8b6ddc343 irq: gate post-exception cooldown on guest cycles, not check-calls (MMX5 fork fix)
The post-exception IRQ cooldown counted psx_check_interrupts CALLS. A call is a
backend-dependent unit: compiled checks at block edges (plus extra sites like a
`jr ra` return transition), the dirty-RAM interp checks per-instruction. So the
per-call countdown reached zero at a DIFFERENT guest cycle in each backend, the
next IRQ was delivered at a different cycle/EPC, and the two backends forked
(cosim first-divergence cp 3259 @ cyc ~213.5M). Convert the cooldown to a
guest-cycle DEADLINE (post_exception_cooldown_until): guest cycles advance
identically in both backends (shared, ruler-validated cost model), so the
delivery decision is now backend-independent. Magnitudes preserve prior
breathing room (claimed ~= old 8 blocks, unclaimed ~= old 500 blocks).

Also:
- gpu: GP1(00h) reset no longer clears VRAM. VRAM survives a GPU reset on real
  hardware; only power-on gpu_init() clears our backing store (gpu_reset_state).
- codegen hash: include the emitter/translator/discovery .cpp TUs, not just
  their .h. A change confined to full_function_emitter.cpp altered generated
  dispatch+overlay code while leaving the hash unchanged -> stale overlay-cache
  hazard (same class as the ws-emission-not-in-hash gap).
- debug: ring-buffer WINDOW-QUERY primitives -- frame_lo/frame_hi filters on
  wtrace/mmio dumps + a capture-freeze frame so a causal window's tail is not
  evicted by a post-window event storm before a probe reads it (query the
  always-on ring for the window of interest, not arm-and-hope).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:51:32 -07:00
Matthew Stanley 4c39958226 dispatch: Class-A shell-window fix covers OVERLAY code — Tomba2 Whoopee-logo wedge
Root cause of the fresh-launch logo freeze (present at every commit in the
stack; window-capture-verified 13/13 repros, never a regression): the BIOS
dispatch normalize() maps RAM phys 0x30000-0x5AFFF to the shell ROM alias.
The Class-A fix routed post-game-start shell-window targets to the game/dirty
path first, but its gate also required psx_game_address_in_text() — which
exempts runtime-loaded OVERLAY code above the boot-EXE text end. Tomba2's
text ends at 0x38800; its post-logo main-thread resume PC 0x50B80 fell
through to normalize() and executed func_1FC38B80, a stale shell-image
data-as-code self-loop, starving the main thread forever (one VBlank
delivery per frame at the same EPC, zero main-thread instructions retired;
the game's ISR ran fine, so TCP frame counters kept ticking — the freeze
was invisible to every telemetry-only validation). Even PSX_FORCE_INTERP
missed it: normalize() shadowed the address before the interp path saw it.

Fix: post-game-start, the WHOLE shell-overlap window routes to
dirty_ram_dispatch before normalize() — real hardware executes the RAM
bytes regardless of who loaded them (boot EXE, overlay loader, DMA stream).
Pre-start shell addresses still normalize to the shell as before.

Also:
- interrupts/debug_server: nestgate_* per-guard veto-attribution counters on
  the nested-delivery gate (freeze_check), used to exonerate the IEc/SR
  guards and redirect the hunt from interrupts.c to dispatch.
- tools/window_capture.py: PrintWindow-based SDL-window capture with
  LIVE/FROZEN series verdict — window-liveness ground truth; TCP/VRAM
  screenshots provably lie about presentation (this bug's entire history).

Validated: fresh launch now runs logo -> intro FMV -> title -> attract with
sustained full-frame motion (window-level capture series).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:04:17 -07:00