Adds this fork's Kula World (SCES-01000) end-to-end target to the README with
a title-screen screenshot and a summary of the five runtime bugs fixed during
bring-up (four of them game-agnostic framework improvements): the dirty-RAM
interpreter interrupt-latency fix, the main-RAM mirror, GetID region
derivation, CD status bits, and the SCPH-1001 CD controller version.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
ROOT CAUSE of the Kula World black-screen wedge: a guest wait loop (libcd's
CdSync spin) that lives entirely in dirty RAM re-enters the dirty-RAM
interpreter once per short (~7-insn) block. The interpreter's only interrupt
poll was gated by the PER-INVOCATION (insns_executed & 0xFFF) counter, which
short blocks never reach — and when the loop exits to dispatch each iteration
that path isn't reached at all. So the interpreter ran millions of
instructions with psx_check_interrupts never called, while SR (IEc+IM2) and
Cause (IP2) fully satisfied the MIPS interrupt-take condition and i_stat had
CDROM+VBLANK pending. The CD IRQ that sets libcd's wait-flag was never
serviced; the loop spun ~14s per cycle until a rare timeout re-dispatched.
Fix: poll interrupts at the interpreter's per-invocation entry, gated by a
global invocation counter (every 64 entries), cpu->pc set for a coherent
resume — the interpreter analogue of a static block-leader poll.
RESULT: Kula World now boots to its title screen and menu. Diagnosed with the
self-stop trap + gdb sampling of SR/Cause/i_stat + the interrupt gate globals
(in_exception, total_checks) + g_dirty_ram_insns_run, which isolated 'CPU
spinning in the interpreter without polling interrupts'. General fix: any
dirty-RAM spin-loop now yields to interrupts.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
With the boot fixed, live state during the game's retry loop shows the CDROM
interrupt is raised (i_stat bit2) AND enabled (i_mask bit2) and the CPU takes
exceptions (VBLANK cleared each frame), yet the CDROM bit is never cleared and
the response FIFO never drained (irq_flag=0x03, response_read=0). The game's
libcd CD interrupt handler in the BIOS IntRP chain is not servicing the CD
IRQ, so CdSync never completes. Distinct from the CD-response-value bugs fixed
today; a BIOS-IRQ-chain / libcd-handler matter. Documents the next step and
the full session tally in docs/kula-world-findings.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
The shell CD-init (ROM 0x1DF50) sets kernel flag [0xA000DFFC]=1 when the CD
controller version high byte (Test sub-cmd 0x20) is >= 0x95; the boot CD-open
(0xBFC0D570) then issues a ReadTOC gated on that flag. We returned the
PSone-era 0x97 0x01 0x10 0xC2 (copied from beetle, which hardcodes it) — wrong
for the SCPH-1001 BIOS, whose 1994 sub-CPU reports 0x94 0x09 0x19 0xC0. With
the correct value the flag stays clear, the spurious frame-890 ReadTOC is
gone, and the boot command stream matches the oracle exactly. Found via
self-stop trap + fntrace-all + gdb watchpoint on [0xDFFC] + capstone tracing
the flag setter and its version check.
Also adds a symmetric self-stop trap to psx-beetle's CD-command hook so the
oracle can be frozen at the same boot point for state diffs.
Honest status: this closes the boot-flow divergence but the game still wedges
downstream in its OWN CD-init retry (GetStat/Init loop from game pc 0x63A14);
that is a separate issue, documented in docs/kula-world-findings.md round 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Adds PSX_FNTRACE_ALL=1 (records every dispatch from power-on) and documents
the full mechanistic chain of the wedge (docs/kula-world-findings.md round 5):
The frame-890 ReadTOC that breaks the game's CD reads is gated by kernel flag
[0xA000DFFC]: the CD-open function 0xBFC0D570 does 'beqz [0xDFFC], skip;
jal ReadTOC'. On ours [0xDFFC]=1 -> ReadTOC -> wedge; on the oracle it stays
0 -> reads continue. The flag is set to 1 by func_1FC1DF50 (a CD control
routine) called from shell code at ra=0x80036024. The shells run the same
verbatim bytes but diverge early (ours 0x35000-0x39000 issuing Test/ReadTOC,
oracle 0x30D00-0x33200 issuing Init/GetTN), so a branch input differs. Traced
via self-stop trap + fntrace-all + gdb watchpoint on [0xDFFC] + capstone.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Extends psxrecomp_cdcmd_cb_t (and the beetle_cdcmd_trace ring + cdrom_cmd_dump
JSON) with the guest PC at dispatch, read via PS_CPU::GetRegister(GSREG_PC).
Lets the boot-command-stream oracle-diff localize the first divergent
command's call site (kernel vs shell vs game) instead of only the command
byte — the next step in isolating the early-boot branch divergence behind
Kula World bug 3. Builds clean; behavior-inert when no listener is attached.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Bit 2 is ADPBUSY (XA-ADPCM playback in progress) and must idle at 0; we
raised it unconditionally under a mistaken 'ADPCM empty' reading. Bit 7
(BUSYSTS: command written, first response not yet ready) was never raised;
now mapped to the queued-command window, mirroring beetle's
PendingCommandCounter/phase busy window. Verified against beetle cdc.cpp
Read(A==0), which sets bit 2 never and bit 7 during command latency.
Boot-diff verification shows this does NOT change our boot command stream
(the early-boot branch divergence vs the oracle persists — documented in
docs/kula-world-findings.md round 4, with the next sharp step: add guest
PC to the oracle's cdcmd hook and diff the first divergent call site).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
New debug tool: PSX_CD_TRAP_CMD=<byte> [PSX_CD_TRAP_NTH=<n>] SIGSTOPs the
process the instant the Nth matching CD command dispatches, so a debugger
attaches to the exact moment with zero run-speed cost (a gdb conditional
breakpoint on exec_command slows the emu ~100x and never reaches the wedge).
Used it to pin Kula bug 3 one level deeper: the pre-wedge ReadTOC at frame
~890 is issued by the BIOS BOOT FLOW itself (guest stack shows only
Main/StartKernel-chain ROM addresses, zero game frames) — the boot's
post-EXE-load disc re-check. And the full command-history dump shows our
boot diverges from the oracle's from the VERY FIRST command: the oracle's
kernel opens with Init/Init/GetTN/GetID and never issues Test or ReadTOC;
ours opens with the shell's Test[20]/GetID/ReadTOC check and re-checks
after the EXE load. Same SCPH1001 bytes, different path — an early-boot
branch divergence (recompiled-BIOS or hardware-sim input), documented in
docs/kula-world-findings.md for the first-divergence session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Full oracle command trace confirms it NEVER issues ReadTOC; ours does, from
the BIOS (Pause->ReadTOC->GetID) at frame 890, with clean pre-read stat
(0x22, no error/shell bits). Same BIOS bytes on both, so our runtime reaching
the disc-identify path is a state-driven BRANCH divergence, not a CD-sim
error: likely a kernel 'disc already identified' flag left unset, or a
recompiler-level game-logic divergence. Next: instruction-level
first-divergence hunt around frame 890.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Adds a CD-command trace to psx-beetle (beetle_cdcmd_trace_hook.patch:
callback in cdc.cpp's command dispatch, wired through beetle_libretro.cpp
into cdrom_cmd_dump / cdrom_cmd_reset on the oracle debug server). This is
the two-process oracle-diff the setup is built for, now working on Linux.
Live-diff result (docs/kula-world-findings.md, bug 3): the oracle's game
reads level data with a clean Setloc->SeekL->Setmode->ReadN->Pause per
sector (SeekL x18, ReadN x22, GetlocL x89). Our runtime's game, in the
wedge, issues NEITHER SeekL NOR ReadN — only GetStat/Init/Setloc/Pause,
an error/retry branch the oracle never enters.
Crucial: our runtime DID deliver 307 sectors during early boot (ReadN +
data delivery work). Reads only stop at frame ~890 — the BIOS mid-game
region revalidation (ReadTOC+GetID). The region fix made that PASS, but
the post-ReadTOC CD state still stops the game resuming reads. Prime
suspect: our ReadTOC (case 0x1E) is a bare stat+COMPLETE stub that does
not model drive repositioning / the track table the game reads to
recompute its seek. Documented for the next session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
The live trace shows the game's CD-init state machine (GetStat/Init then
Setloc/Pause, ~2.5s cadence) receives CORRECT responses at every step —
stat=0x02 (healthy idle drive), INT3 ACK + INT2 COMPLETE both fire and the
game's ISR processes them — yet it retries forever. So bug 3 is NOT a
wrong-response-value bug like bug 2; the game is waiting on a timing or
event-completion condition its init check never sees satisfied. Documents
two next-session paths: (1) rebuild psx-beetle with a CD-command hook and
live-diff the boot->demo sequence, (2) find the game's init-complete branch
condition and check our INT2-COMPLETE delivery timing against it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Records the two fixed bugs (RAM mirror, GetID region) and characterizes
the open third wedge: the game's own CD driver loops Setloc(PVD)/Pause
without ever reading the directory, so the demo level never loads and
the screen stays black. The oracle loads it, so the divergence is
concrete and localized for the next oracle-diff session.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
GetID's last four response bytes were hardcoded 'SCEI' (NTSC-J). Real
hardware reports the region of the INSERTED disc. Kula World uses the
BIOS CD API, and the kernel CD driver revalidates the disc (ReadTOC +
GetID, polled with irq_enable=0x18) when it reinitializes after the
game's first file load. Seeing a 'Japanese' disc, it fell into the
endless GetStat/Init retry loop: CD delivery froze at sector 307 (LBA
318, just before the first level directory) and the game waited forever
on a black screen.
Diagnosed from the always-on cdrom trace ring (grabbed via gdb from the
live process): 307 sectors stream cleanly, Pause completes, then the
BIOS's own ROM CD firmware (0xBFC0Dxxx) masks INTC delivery, issues
ReadTOC + GetID, reads the response... and rejects it. Beetle PSX (the
oracle; boots the same dump + BIOS to demo gameplay) derives the SCEx
string from the disc's SYSTEM.CNF boot serial - so we now do the same:
cdrom_set_disc_scex() is fed at launch from the existing disc_identity
module (PAL->SCEE, NTSC-J->SCEI, NTSC-U->SCEA; default SCEA matches the
one supported BIOS). Verified so far: launch log reports 'disc region
PAL (serial SCES-01000)'; the sector-counter wall-break run is in
flight. Framework-wide note: Tomba/MMX6 (NTSC-U, own CD drivers) never
hit this path but now get 'SCEA' - matching real hardware - instead of
'SCEI'; regression-test before any master merge.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
beetle_libretro.cpp targets the C++ beetle-psx tree; upstream master has
been converted to plain C and no longer compiles against it. Documented
the working recipe: pin beetle-psx at 5759277b (the wtrace patch's base,
located via the patch's own blob hash), apply beetle_wtrace_hook.patch,
and apply the new beetle_sio_trace_hook.patch — FrontIO::SetSIOTraceCallback
(fires once per completed SIO byte exchange), a hook beetle_libretro.cpp
required that was missing from the committed patch set.
Verified on Linux/Xvfb: psx-beetle boots Kula World to demo gameplay and
serves ping/screenshot over the shared TCP debug protocol, alongside
kula-runtime — the two-process oracle setup from CLAUDE.md §16 now works
in this environment. Runtime-created per-game .mcr files are gitignored.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Linux port (2 fixes — the POSIX paths largely existed already):
- overlay_loader.c: portable atomic for the async-cache dirty flag
(InterlockedExchange/LONG behind _WIN32, __atomic_exchange_n on POSIX)
- code_provider.c: missing <stddef.h> for NULL
memory.c — main-RAM mirroring (the Kula World boot-wedge root cause):
the 2 MB RAM is mirrored 4x across the first 8 MB of each segment
(mem-ctrl RAM_SIZE default 0x0B88) and Kula World's crt0 parks $sp in
the 4th mirror (0x807FFFF8). Guest accesses to mirrors fell into the
open-bus no-op: stack writes vanished, $ra loaded back 0, and the game
jumped to address 0 at frame ~891 (fntrace-verified call chain). All
guest access paths now fold mirrors via psx_phys_addr().
fntrace.c — null-page dispatch guard: a dispatch whose phys target is
below 0x40 latches the capture freeze, so the always-on ring preserves
the window that led to a null jump instead of the wedge storm evicting
it. This is how the mirror bug was pinned: the frozen ring showed crt0
set sp=0x807FFFF8, then ra came back 0 from the first mirrored-stack
return.
runtime/CMakeLists.txt: kula-runtime target (SCES-01000), guarded on
the locally-generated game C existing so clones without the (untracked)
game dump still configure. Verified on Linux/Xvfb: BIOS-only psx-runtime
boots to the shell menu; kula-runtime boots the disc, game code runs
native (mirror stack live), VBLANK event callback ticks, CD streams the
HIRO directory. Software renderer + TCP debug server work unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
Kula World (SCES-01000) stores its header addresses in KUSEG form
(load 0x00011000, pc 0x00069F60) instead of the usual KSEG0 form. KUSEG
0x0-0x1FFFFFFF mirrors the same physical RAM, so parse_buffer now
normalizes header addresses into KSEG0 before validation instead of
rejecting the EXE. Only addresses inside the 2 MB RAM mirror are
remapped; zero-valued unused fields stay zero. L2 structural: 42/42 ok.
With that fix the recompiler runs end-to-end on the Kula World EXE:
5297 functions discovered, generated C compiles clean. games/kula/
game.toml records the header-derived config; the gitignore now tracks
per-game tomls while keeping discs/EXEs/generated output untracked.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
The freeze heartbeat (always-on, ships in production builds since it is the
only observability that survives a main-thread stall) gains two things that
make a *logical hang* diagnosable from the small, user-uploadable
psx_freeze_heartbeat.json alone:
1. CPU register file (pc + 32 GPRs, same shape as psx_last_run_report's "cpu")
and the full 1 KB scratchpad (0x1F800000..0x3FF) in every tick and every
auto-dump. PSX engines keep per-frame command/list state in scratchpad;
$t1 (gpr[9]) holds the BIOS A0/B0/C0 function index while a guest is parked
in a kernel-call poll, so this also answers "which B0 function".
2. A new wedge kind D (5, "spin_freeze"): frames advance at a healthy rate but
current_func + last_store_pc + dirty_ram_insns are all pinned across the
window = the game is spinning while the IRQ/render loop keeps running. The
existing hard-freeze / reentry-storm / slow-frames thresholds all miss this
class (Tomba dwarf-village dialogue hang, issue #1: ~51fps, exc_re ~1.6K/frame).
Runtime-only, no regen. Verified: release config (PSX_DEBUG_TOOLS=OFF) links;
emitted JSON parses clean (gpr[32], scratchpad 1024 bytes).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(1) OBJECT_DEPENDS on overlay_codegen_hash.h for the cg-tag CONSUMERS
(overlay_loader.c, boot_state.c). overlay_api.h pulls that header via
__has_include, which the compiler depfile does not record when the header is
absent at first compile -- so a later hash change left a STALE baked-in
PSX_OVERLAY_CODEGEN_HASH, making the loader READ cg<old> while autocompile WROTE
cg<new>: the runtime silently ignored freshly compiled shards and the area ran
~97% interpreted (the recurring read!=write overlay-lag class). The explicit
dependency makes the runtime cg tag unable to drift from the headers/autocompile.
(2) recompiler/tests/test_overlay_guard_codegen.py: deterministic codegen
regression test (no game) -- synthesizes a tiny PS-EXE, runs the recompiler in
overlay vs static mode, and asserts overlay functions emit the fail-closed
psx_native_bad_entry guard in their entry-switch default while static functions
keep the legacy fall-through. Guards the native-overlay blue-screen wedge fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completeness layer for the Tomba 2 FMV-freeze fix (the discovery/ownership side
of "containment is not ownership, ownership is not entry legality"). In overlay
discovery (main_psx.cpp --overlay), after walking the seed roots, every DIRECT
CALL (jal) target that lands strictly INSIDE an already-discovered function is
registered as an ALIAS ENTRY of that host via the existing alias machinery
(materialize_alias_groups). So when a host's discovered range absorbs a separate
routine (e.g. the crt0 `jal 0x50B08; break` falling through into the init-array
at 0x89788), the host's entry-switch now routes a dispatch of that PC to the
correct absorbed interior block and runs it NATIVE — instead of depending on the
PC having been captured, or falling closed to the interpreter.
Derived purely from static jal evidence (jal encodes a code target, so it never
mints code from data); does not depend on execution capture. Preferred over an
explicit range-clip because the absorbed code IS the right code — it just needed
a routable entry. The static two-phase FunctionDiscovery already enforces
hard_cap = next-entry, so only the overlay path needed this.
VERIFIED: regenerated cache (codegen cg4_42de29a8) emits `case 0x80089788u` in
func_800896E0; cps_probe for 0x89788 drops 1021 -> 0 (now main-chain native, not
the broken CPS find-by-range default); freeze stays gone, 60fps sustained. Pairs
with the fail-closed entry guard (bd60381), which remains the safety net for any
genuinely-unrouted interior PC.
Framework-wide: regression-test Tomba 1 / MMX6 / Ape before any master merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit eb99579a3702f5a87c902bdf20314203ce20c4a2)
Brings the fail-closed native entry guard from wt/tomba2 (b5a6940 / bd60381) to
master, where it had never been merged — which is why Tomba (and any master-built
title) still wedged. This is the long-standing "blue screen" / shared-RAM-
divergence-between-interp-and-native class.
Root cause: an overlay function dispatched at a FOREIGN INTERIOR PC (a range-
ownership mismatch routed a PC the function does not own into it) hit the CPS
entry-switch `default: break`, which fell through and ran the function FROM ITS
TOP — re-running its prologue and corrupting shared CPU/RAM state, so the guest's
main loop then waits forever on a condition that never gets set (the 0xCF0 SIO
spin / blue screen). Reproduced on Tomba's Village of All Beginnings: a native
0x000E7000-region shard function entered at a foreign PC wedged; the interpreter
ran the same bytes correctly.
Fix: the generated CPS entry-switch for OVERLAY functions (config_.overlay_mode)
now FAILS CLOSED on an unknown non-zero continuation PC — it restores the PC and
calls psx_native_bad_entry instead of running from the top. overlay_loader_dispatch
consumes g_native_bad_entry and routes the PC to the sanctioned dirty-RAM
interpreter (the live bytes run correctly — NOT a stub/HLE). True-prologue entry
still runs from the top. Static (BIOS/boot-EXE) functions keep the legacy fall-
through. ABI bumped 5->6 so stale ABI-5 caches are rejected and rebuilt with the
guard.
Minimal port: kept only the fail-closed mechanism + its ABI plumbing. The Tomba2
host_aliases interior-routing and the native-block / CPS-probe diagnostics (which
depend on the overlay-cache-v2 multi-range Candidate fields not on master) were
intentionally omitted — the fail-closed default alone is the safety net.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Ape Escape forward-port (e0c819c) added pre-prologue setup detection to
function_analysis (find_preprologue_setup_start / preprologue_window_is_valid +
is_bios_dispatch_thunk), extending function STARTS backward to absorb preceding
lui/lw global-load setup. On Tomba this mis-bounds a function in the Village-of-
All-Beginnings init path (function count 5562 -> 5492, ~70 functions absorbed),
producing a main-loop softlock: New Game / loading the Village save wedges with
the main loop spinning on the 0xCF0 SIO poll, never rendering the area (a "blue
screen"). Backend-independent (gcc + tcc), not overlay/interp related.
This worked in v0.5.x (pin 5007c46, pre-AE). Revert function_analysis.{cpp,h} to
that state. Confirmed: the 0xCF0 wedge signature disappears (repro cf0_frac
1.00 -> 0.02) and Tomba boots/plays again. Ape Escape is not a public release, so
rolling back its discovery changes is safe; MMX6's existing release is unaffected
(its binary was already built), but a future MMX6 rebuild should be re-verified
against this discovery. A follow-up can re-introduce the pre-prologue heuristic
correctly (game-gated or with a tighter boundary check) without the over-extension.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two framework bugs that made overlay variants run interpreted forever (the
"agonizing lag" in overlay-heavy areas like Tomba's dwarf village):
1. Cache read/write divergence. The loader reads <exe>/cache and the runtime
writes captures to <exe>/overlay_captures.json, but the gcc autocompile path
used the game.toml command verbatim, whose hard-coded --out-dir/--captures
pointed elsewhere (build-stable/build-modern). So compiled shards landed where
the loader never looked and captures were read stale -> the variant never went
native. Fix: the runtime now injects PSX_OVERLAY_CACHE_DIR / PSX_OVERLAY_CAPTURES
(the canonical loader paths) into every autocompile spawn; compile_overlays.py
honors them over any CLI flag (--captures now optional). One source of truth for
the cache location, uniform across all games, dev or prod. gcc/tcc stay separated
by the existing <compiler> namespace.
2. Overlay DLL preamble missing psx_ws_cull_sltiu. auto_screen_x emits this helper
at every width-cull site into EVERY build's C (identity at 4:3), and the main EXE
links it from gpu.c, but the overlay DLL preamble never defined it -> any overlay
variant with cull sites failed to link -> no shard -> interpreter. Fix: provide
the self-contained helper (depends only on the already-forwarded psx_ws_x_margin)
in the DLL preamble.
Proven on Tomba dwarf village: dispatch_native 29/s -> 11454/s (was ~97% interp,
now ~97% native).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The runtime forces SDL HIDAPI (SDL_HINT_JOYSTICK_HIDAPI=1) and disables RAWINPUT
(=0) so Steam's injected virtual Xbox controller enumerates as a GameController.
But HIDAPI's Xbox sub-driver is OFF by default on Windows (Xbox pads are normally
RAWINPUT/XInput there), so with RAWINPUT disabled a PHYSICAL Xbox One/Series
controller was claimed by no GameController driver at all -> SDL_IsGameController
false -> never opened -> zero button input. PS5 DualSense was unaffected (its
HIDAPI driver is on by default), which is why PS5 worked and Xbox did not.
Fix: enable SDL_HINT_JOYSTICK_HIDAPI_XBOX so HIDAPI also claims Xbox pads, keeping
the HIDAPI-preferred design (and avoiding the RAWINPUT+HIDAPI Xbox double-enumerate
the author was avoiding). Additive hint; no effect on the working PS5 path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Compiler-neutral tier-selection policy extracted from the mis-named
overlay_sljit.{c,h} into NEW overlay_backend.{c,h} (OverlayBackend enum +
resolve/name/active). overlay_sljit.{c,h} is now just the deprecated emitter.
New tcc tier. gcc and tcc run the IDENTICAL pipeline (recompiler -> C ->
compiler -> DLL -> loader); only the compiler binary differs, so both map to the
autocompile-spawn provider (code_provider.c) and the COMMAND is chosen by the
resolved backend (main.cpp). compile_overlays.py gains --compiler gcc|tcc: tcc
writes a separate tcc/ cache namespace, BOM-strips the C + a copy of the include
headers (tcc 0.9.27 can't skip a UTF-8 BOM), and uses tcc's built-in linker.
Backends (overlay_backend): auto (gcc if a gcc TOOLCHAIN is present, else tcc --
was sljit), gcc, tcc, auto-no-gcc (dev/test: force tcc even with gcc present, to
exercise the toolchain-less user path), sljit (DEPRECATED, gated off, lowest).
Priority: static > gcc shard > tcc shard > sljit(off) > interp. The loader scans
gcc/ then tcc/ and dedups by region+crc (filename) so a gcc shard always wins a
tie.
Self-contained tcc: with no explicit overlay_autocompile_cmd_tcc, the runtime
CONSTRUCTS the tcc command from a toolchain bundled beside the exe
(<exe>/overlay_toolchain/: embedded python + tcc + recompiler + compile_overlays.py
+ runtime headers) -- no system python or gcc needed. config_loader gains
overlay_autocompile_cmd_tcc (a dev override). autocompile cmd buffers enlarged for
the constructed path-heavy command.
Compilation stays OFF the dispatch/render thread (off-process autocompile
subprocess, same model as gcc); the main thread only spawns it and dlopens the
finished DLL.
Validated: MMX6 with overlay_backend=auto-no-gcc fills the tcc/ namespace live
from the bundled toolchain and plays through save-load + gameplay -- the exact
path the sljit tier softlocked on. The gcc path is untouched.
Runtime-only, no regen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Off-thread sljit compile worker (overlay_compile_worker.c/.h): on a dispatch
gap-fill miss the dispatch thread snapshots the region bytes and ENQUEUES; a
background worker JITs from the snapshot (never live RAM) and publishes the shard
to the on-disk cache via an atomic temp+rename; dispatch picks it up on a later
miss via an idempotent rescan, so the candidate table stays single-threaded. This
removes the synchronous JIT frame hitch. Validated on Tomba + MMX6: compiles
off-thread (one MMX6 compile was 26ms, paid entirely off the dispatch thread with
no frame-pacing spike), shards persist + reload, queue never backs up.
Observability (ring-buffer doctrine): always-on recent-compile ring + per-compile
timing in the worker, the resolved on-disk cache dir, and a tier-enabled flag, all
surfaced via a new sljit_async TCP debug command (documented in TCP_COMMANDS.md).
DISABLE the sljit tier by default (g_sljit_tier_enabled = 0): the sljit EMITTER
mistranslates some MMX6 overlay instruction(s) -- sljit-only save-load softlocks/
loopbacks while PURE INTERP works (discriminator), and it reproduces with the
worker OFF in the synchronous path (PSX_SLJIT_SYNC=1), so it is the emitter, not
the async worker. Until the emitter bug is found + fixed: no shard READ
(scan_sljit_cache_dir skipped), no GENERATE (worker not started + sljit_live
forced 0 + dispatch gap-fill gated), every overlay miss falls to the interpreter.
gcc (DLL cache + autocompile) and overlay capture are untouched, so priority is
gcc > interp and dispatch misses are still recorded for future gcc coverage. gcc
shards are bundled in release zips, so shipped builds are unaffected.
PSX_SLJIT_ENABLE=1 re-enables the tier for emitter-fix testing.
Runtime-only, no regen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces the boot-snapshot feature as a launcher setting instead of a
game.toml-only opt-in. Mirrors the turbo_loads toggle end-to-end: UserSettings
field (has_fast_boot/fast_boot, default false), config_loader read/write,
main.cpp apply/seed/seed-apply, launcher.cpp model field/bind/toggle/io-write.
Reuses the existing [runtime] fast_boot key as the persisted seed; nothing
renamed. A hint (reusing .ws-hint, shown only when On) notes the first boot
still loads to build the cache and is instant after. Launcher/config only; no regen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the old incomplete RAM-only fast_boot snapshot with a COMPLETE
hardware save-state captured at the BIOS->game handoff. First launch (and the
first launch after any update) boots the real BIOS normally and captures the
full machine state at the game's PS-EXE entry_pc; every subsequent same-build
launch restores it and presents the game's first frame instantly.
This is not HLE: it persists the real hardware state a real BIOS run produced.
Format (boot_state.h v2): a sectioned file with an integrity key
(bios_checksum + entry_pc + PSX_OVERLAY_CODEGEN_HASH + ABI tag + codegen ver).
Any mismatch -> reject -> normal boot + recapture, so a user/app update can
never silently load a stale snapshot. Missing/short/unknown sections also reject
(no partial restore).
Complete capture (one section per subsystem), the fix for the old snapshot's
real bug (it left GPU/SPU/CD/DMA/SIO at reset while CPU/RAM assumed configured):
- CPU regs, 2 MB RAM, 1 KB scratchpad, IRQ (i_stat/i_mask), root timers, guest
cycle counter, dirty-RAM page bitmap (guest-written code pages).
- GPU: display/draw-area/offset/mask/texpage + GPUSTAT bits + GP0 command
machine + VRAM-transfer state; VRAM (1 MB) via the renderer-agnostic
gr_vram_transfer_out/in hooks (so it is correct on the GPU-authoritative GL
backend, where the FBO -- not gpu.c vram[] -- is truth, and the FBO re-upload
on restore happens for free).
- SPU: register file + 24 voice decode/ADSR state + latches + 512 KB SPU RAM.
- CD-ROM: full controller FSM (regs/IRQ-latch/FIFOs/seek/read/pending/queued +
XA decoder history + disc-speed model).
- DMA: channels[7] + dpcr/dicr + async-transfer state.
- SIO: registers + pad-config FSM + memcard FSM (incl. per-slot mc_slots[]) +
cycle-paced shifter/ACK scheduler.
Host-side/recompiler-derived state (overlay/sljit tables, code-provider) is NOT
serialized -- it re-initializes fresh on the normal startup path.
Per-subsystem accessors use an X-macro field list so bytes/write/read can never
drift. Runtime-only; no recompiler/codegen change, no regen. Reuses the
[runtime] fast_boot key (semantics upgraded; type unchanged).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The overlay autocompile_cmd uses paths RELATIVE to the project root so the
shipped game.toml stays portable across machines. But on Windows,
subprocess/CreateProcess does not resolve a relative *executable* against the
child cwd, so the relative --recompiler path failed with WinError 2 and the
overlay cache NEVER warmed -- a silent failure that left games ~100%
interpreted (sluggish) with no native payoff. abspath --recompiler and
--runtime-include up front (matching what compile_dll already does for gcc -I)
so relative config resolves correctly everywhere.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add MegaManX6Recomp alongside TombaRecomp in the framework intro, and
correct the status note: neither game is fully validated end to end yet
(Tomba has more coverage; MMX6 is playable).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Validated CD-model rewrite (boot-EXE SeekL wedge fixed via deferred response
presentation), recompiler pre-prologue function discovery, dma/iso/memory
forward-port, and MMIO-READ (rtrace) instrumentation. Functional on Tomba
(in-game), Mega Man X6 (in-game), and Ape Escape (3D title/gameplay), 0
dispatch misses each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CD controller presented a command's first response IRQ synchronously,
inside the guest store that issued the command (exec_command -> set_irq +
fire_cdrom_irq). When the boot loader issues SeekL from *inside* the BIOS CD
interrupt handler -- right before the handler's trailing I_STAT ack -- that
synchronous response raised i_stat bit2, the handler's own INTC ack then
cleared it while irq_flag was still 0x03, and the single-outstanding latch
suppressed re-presentation forever. SeekL never completed -> livelock.
(Diagnosed with the new MMIO-read trace: the handler reads 0x1F801803=0xE3
and the controller ack 0x07 both land -- for the PRIOR interrupt -- so the
SeekL INT3 was simply lost at the INTC level, not a missing ack.)
Fix: real CD hardware takes thousands of cycles to produce a response, so
arm a presentation latency on every new response (set_irq) and gate
present_cdrom_irq() on it; cdrom_advance ages it. The present then lands at a
clean instruction boundary after the issuing ISR has returned, never inside
the store. The generation latch is unchanged (it still models edge-latched
delivery, one present per generation).
Validated: Ape Escape now boots past the SeekL wedge into 3D title/gameplay
(~60fps), matching the Beetle oracle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The write-trace ring proves whether a device-register store happened, but
to diagnose a CD interrupt that was never serviced you need to see what the
IRQ handler READ. Add an MMIO-read trace mirroring the existing MMIO-write
ring:
- debug_server.c: s_mmio_rtrace ring (reuses MmioTraceEntry, snapshots
func/pc/ra + i_stat/i_mask at read time), default-armed to the CD regs
(0x1F801800-1803) + I_STAT/I_MASK so it captures continuously from init;
rtrace_dump/clear/arm/ranges/stats commands.
- memory.c: wrap mmio_read32/16/8 (body -> _impl, thin wrapper records the
loaded value AFTER the single side-effecting read).
Range-filtered + always-on so the decisive device-register reads survive a
livelock that polls other MMIO. Additive diagnostics; no runtime behavior
change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.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>
Adds two launcher controls requested for Tomba (issue #6):
- Fullscreen on launch: new [video] fullscreen setting, cloned through the full
turbo_loads pipeline (UserSettings field + parse/save, launcher model/bind/
toggle/commit, main.cpp seed/read), applied as SDL_WINDOW_FULLSCREEN_DESKTOP at
game-window creation. The in-game F11 / Alt+Enter hotkey is unchanged.
- Skip Launcher on Boot: a footer switch (bottom-left) that boots straight into
the game on subsequent launches, with a (first-in-this-launcher) confirmation
modal explaining the --launcher escape hatch. The runtime skip_launcher round-
trip + --launcher / PSX_NO_LAUNCHER already existed; this wires the UI to it.
Also drops the redundant footer Settings/Back links (the top bar already has
them). Mirrors SuperMarioWorldRecomp's feature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The launcher Hybrid controller mode (built first, for Tomba, to mimic Tomba:
Special Edition) flips the emulated pad's reported type 0x41<->0x73 on
d-pad<->stick. After the real DualShock config state machine was added for MMX6
(98aa688), that flip regressed every game's libpad re-detect into a 1-frame
"pad unplugged": Tomba unpauses its menu / drops input, MMX6 phantom-dashes.
The modern config SM is correct and required by MMX6, so instead of weakening it
globally, restore the pre-98aa688 "config commands always answer 0xF3" behaviour
as a per-game opt-in: [controller] legacy_pad_config (default OFF). Only Tomba
sets it; with the flag off the modern pad path is byte-for-byte unchanged, so no
other title is affected. A/B verified live: legacy = clean Tomba flip / MMX6
input; modern = Tomba disconnect + MMX6 dash.
Explicitly LEGACY: a correct findpad/re-detect refactor would make the flip
benign for every game with no per-game branch (real hardware tolerates unlimited
analog-button presses); when that lands this whole feature set is to be removed.
Full rationale in sio.c (g_pad_legacy_cfg).
Also includes the deferred coherent pad-type apply (pad_type_req, applied only at
an idle/non-config bus boundary so a flip can't split a poll/handshake) and a
`pad_cfg` debug command + tools/padcfg.py + tools/sio_flip_trace.py used to A/B
the config SM live.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
allow_hybrid (default true) lets a game hide the "Hybrid" pad mode from the
launcher's segmented selector, leaving Analog | D-Pad. Parsed in config_loader,
threaded through GameInfo into the launcher data model, and gated in launcher.rml
with data-if="allow_hybrid" (mirroring ws_eligible). When hidden, a port that
would default to Hybrid is clamped to Analog so the selection stays valid. Games
that hard-require a DualShock (e.g. Mega Man X6) set [controller] allow_hybrid =
false; Tomba and others are unaffected.
Also remove the "much faster loading, audio plays through" hint under the Turbo
loads toggle — the On/Off control is self-explanatory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 16:9 BG widen draws LEFT extra tile columns each side, but the engine's
streamer (FUN_800273e4 -> FUN_800274a0) only refreshes one column per side per
frame at the leading edge. On scene entry / fast scroll the inner reveal columns
held stale tiles from a prior scrollY -> real-but-misplaced tiles spilling
up-and-left (the "staircase"). Census confirmed this is NOT the BG packet-budget
overflow (674-816 prims/frame, well under the 1000-tile cap).
Fix (render window == refresh window): clone FUN_800274a0's inner metatile->ring
fill into mmx6_fill_column() and re-stream every column of the widened window for
all 3 layers each frame (psx_ws_mmx6_bg_refill_all), triggered once/frame from the
existing count_site hook (mmx6_bg_refill_tick, frame-gated). Uses each layer's own
scroll, omits the engine's scrollX wrap-bookkeeping writes (those drive the engine's
own incremental streamer), skips map-loop seams; off-map columns fill tile 0 (the
renderer skips them). Validated byte-exact against the engine's own streaming
(0 mismatches / 1113 cells x 3 frames; debug cmd mmx6_freshfix {validate:1}). Gated
native-wide <=384w so 4:3 and the 512 hi-res title stay byte-identical.
Also removes the 2-col reveal cap (WS_FULL2D_MAX_REVEAL): it never addressed the
staircase and cost screen real-estate; the full 53px/4-col reveal is restored.
Parked, gated-off experiments ride along (inert, default off): host-side reveal
(Attempt A: gpu_ws_mmx6_emit_reveal + sw_wide_emit_tile) and BG packet-buffer
relocation (Attempt B: [widescreen.bg2d] bufbase/cap recompiler hooks). These target
the separate dense-stage budget-overflow concern, not this fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The BG renderer widen (a8f37c1) draws extra tile columns on both sides, but the
engine's 64-column tile ring is only refreshed at its leading edge: the streamer
(FUN_800273e4) re-streams ONE column per side per frame at world-X scrollX-16
(left) and scrollX+16+width (right). So the columns the widened renderer reaches
were never populated — they showed empty (black margin) or stale (old tiles as
the camera scrolled), the "stale margins" the user saw in motion.
Add two more [widescreen.bg2d] sites (stream_left_site / stream_right_site) that
push the streamer's left/right leading-edge world-X out by LEFT*16 via gpu.c
psx_ws_mmx6_bg_stream_{left,right}, so the incremental streamer keeps the ring
valid across the widened window as the camera scrolls. Same number of streams
(the single leading-edge target just moves out LEFT columns) — no extra cost.
Identity at 4:3 / 512 hi-res.
Verified: the previously-black left reveal margin now fills with coherent
adjacent stage; blackL/blackR = 0 across scenes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The native-wide mirror target clipped Y to the full surface (0..511), but a
vertically double-buffered game (MMX6: front y0..239 / back y240..479, both
sharing base_x=0) keeps both buffers in one shared wide surface. The background
tile loop starts a partial top row at y = buffer_top - (scrollY & 0xf), so
back-buffer draws bled across the band boundary into the front buffer's region
of the wide surface -> top/bottom edge flicker as the buffers flipped. Canonical
VRAM never showed it because its draw-area clip already confines each buffer.
Clip the wide target's Y to the canonical draw area (g_clip_y1..g_clip_y2),
exactly like rt_native, so each buffer's draws stay in its own band. X still
spans the full wide width (that is where the reveal margins live). User-confirmed
the top/bottom flicker is fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Pure-2D sprite games (Mega Man X6) render a true 4:3 field of view with no
overscan to reveal, so 16:9 requires making the engine draw more. Two parts:
- [widescreen] full_2d opts a pure-2D game into the native-wide present path
(it never emits the 3D sprite-tag hook the gameplay detector keys on, so
every in-game frame was misclassified as a 4:3 menu and pillarboxed).
- [widescreen.bg2d] is a gen-time hook on the per-layer background tile
renderer (MMX6 FUN_800270d0) at its column-count / start-tile-column /
start-screen-x instructions, routed through gpu.c psx_ws_mmx6_bg_{cols,
startcol,startx}. It draws LEFT=ceil(nw_offset/16) extra 16px columns on
BOTH sides so the loop fills the 16:9 reveal margins with real adjacent
stage, existing columns staying pixel-aligned with objects. Identity at
4:3 and in the engine's own 512 hi-res mode (so the title is untouched).
Tooling: wide_full debug command + sw_wide_dump_full dump the entire
native-wide compositor surface (both vertical-double-buffer y-bands and both
reveal margins) for diagnosis.
Known follow-ups: object screen-X cull widen (actors in the margins);
top/bottom flicker + stale-margin refresh on the vertical double-buffer.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Proof-of-concept for 2D "overscan reveal" widescreen. The native-wide
compositor is gated off for full-2D screens (ws_game_mode() is false unless the
recompiler sprite-tag hook fires, which only Tomba's 3D actors do). MMX6 is an
all-2D sprite engine that over-draws ~192px of world past the 320 viewport
(clipped by the draw area). Env PSX_WS_FORCE_2D=1 forces game_mode so
native-wide engages: draw_offset shift + draw-area widen + wide mirror +
widened present reveal the over-drawn columns.
VALIDATED: at 16:9 the present grows to 512px and shows real world content on
both sides (enemies/terrain previously clipped) with no texture corruption —
MMX6 widescreen via overscan-reveal is viable. KNOWN ISSUE: flicker/tearing
because the wide compositor was built for Tomba's HORIZONTAL double-buffering
(surfaces keyed by VRAM X-origin); MMX6 double-buffers VERTICALLY (front Y0..239
/ back Y240..479, same X) — needs the buffer-swap/present band handling adapted.
Experiment-only (env-gated, regen-free); not wired to a game flag yet.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turbo-through-loads no longer mutes audio (the mute/fade was removed), so it is
now the right default: fast loads with audio playing through and all guest
timing preserved. Flip the launcher/settings fallback default to ON
(LauncherModel + UserSettings) and refresh the now-stale "briefly mutes audio"
launcher hint to "much faster loading, audio plays through". The per-game
game.toml value still seeds the launcher toggle and a user choice in
settings.toml still overrides it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GL-only painter's-order bug: the player drew IN FRONT of sprites he should be
behind (an in-world AP item-block's letters, a save post) — correct on the
software renderer. GP0 capture + USER-confirmed isolation pinned it to the
textured-prim batch.
flush_tex_batch drew every batch in two passes split by the per-texel STP bit:
pass 1 = all prims' STP=0 texels, pass 2 = all prims' STP=1 texels. The split
ran for OPAQUE batches too (it also drives the PSX mask bit, which STP=1 texels
must set), so a BEHIND opaque prim's STP=1 texels (pass 2) overwrote a FRONT
opaque prim's STP=0 colour (pass 1) — the in-world objects are opaque prims with
STP=1 cutout texels, so they reordered against the character.
For an OPAQUE primitive the STP bit must not gate COLOUR (every texel is opaque).
So opaque batches now draw colour in ONE ordered pass over all texels (frag.a
still carries the per-texel mask bit → colour-attachment mask correct), followed
by a COLOUR-MASKED pass that only fixes the STENCIL for STP=1 texels. Same draw
count, submission order preserved, mask preserved. Semi-transparent batches keep
the genuine blended two-pass and are isolated to one prim per batch so their
cross-prim order is also correct. Opaque foliage/terrain batching (the perf win)
is untouched. USER-VALIDATED fixed on Tomba GL.
A future single-pass for semi modes 0/1/3 (GL_ONE,GL_SRC_ALPHA with a per-
fragment destination factor) could re-batch semi prims; mode 2 needs isolation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fast loads at authentic CD timing via turbo-during-loads (host fast-forward,
all guest timing preserved), plus the CD response arbiter + authentic command
latencies (dormant at the shipped 1x config, available for accelerated modes).
Validated live on MMX6 and Tomba: blazing loads, audio intact.
Three refinements that make turbo-during-loads the production fast-load path
at authentic CD timing, validated live on MMX6 and Tomba.
1. Arbiter bypass at authentic 1x (cdrom.c). The response arbiter (queue +
inter-IRQ gap + command latch) only matters when responses are delivered
FASTER than hardware (2x/4x/instant), where a new response can land before
the guest acks the prior one. At 1x that race cannot occur, and the queue/
gap only perturbs timing the game was validated against — which desynced
CD-streamed audio (boss/stage-select music looped/restarted early on MMX6).
set_irq/command-write/ack now take the exact pre-arbiter immediate path when
g_disc_speed_divisor == 1; the arbiter stays active for the accelerated
modes that need it.
2. Turbo engage-hysteresis (main.cpp). Only enter turbo after a load has been
continuously in progress for TURBO_LOADS_ENGAGE_FRAMES (20). Brief incidental
reads on music screens (a boss/stage-select streaming a few preview sectors
over its XA music) would otherwise flicker turbo on for a frame or two,
chopping audio and frame rate. Real loads hold for hundreds of frames, so
they lose only this short authentic-paced prefix.
3. Turbo no longer mutes audio (main.cpp). At 1x+turbo loads are sub-second,
and the mute/fade/hangover model produced more audible disruption (cuts and
"restarts") than just letting the stream play through; the audio queue is
capped, so a brief turbo burst can't build unbounded latency. FMV-skip still
mutes (fast-forwarding a whole movie's audio would be noise).
The shipped per-game config is disc_speed="1x" + turbo_loads=true: authentic
guest timing (nothing breaks, audio intact) with host-speed fast-forward during
loads (blazing real load times). Runtime-only, no regen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two CD-controller correctness fixes toward universal fast disc loads
(disc_speed=4x/instant) without per-game tuning.
1. Response arbiter (the core fix). The PS1 CD controller serializes
responses: only one occupies the IRQ slot (irq_flag + response FIFO)
at a time, and a later response is not exposed until the guest ACKs
the current one. set_irq() previously OVERWROTE irq_flag directly,
so when responses arrived faster than the guest acked (fast disc
timing) it silently DROPPED them. Proven: 24 dropped responses at
MMX6 boot under disc_speed=instant (INT2 completion clobbering an
unacked INT3 ACK), which wedged the loader.
set_irq() now snapshots the response bytes and either exposes (slot
free, gap elapsed) or QUEUES behind the visible response. The guest
ACK edge actively pumps the arbiter (cd_arbiter_pump): drain queued
responses in order, then start a command latched while the controller
was busy. cdrom_advance also pumps once the inter-IRQ gap elapses, so
a queued response is delivered even when no further ACK edge arrives.
Commands written while busy are latched and executed on ACK.
Result: 0 dropped responses at 1x/4x/instant. 1x boots to title
identically (no regression). The overwrite counter is retained as a
regression detector (now only counts true queue overflow).
2. Command-completion latency is now fixed, not speed-scaled. Pause/
Init/SeekL/GetID/ReadTOC second responses are controller/firmware
operations whose timing does NOT scale with read speed on real HW.
They used apply_speed() (divides by the disc-speed divisor), so at
4x the Pause completed 4x too fast and desynced the BIOS CD driver.
New cmd_latency() keeps them at authentic timing; only sector DATA
delivery (sector_delay_cycles) scales with disc_speed — which is
where essentially all load time lives, so loads still get fast.
Runtime-only (no regen). 1x verified clean on MMX6. A downstream
load-timing race still wedges 4x/instant at boot (guest spins with
interrupts disabled after a clean Pause) — tracked separately.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>