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.
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>
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>
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>
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.
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>
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
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.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>