Commit Graph

514 Commits

Author SHA1 Message Date
Matthew Stanley c2b9d467e5 runtime: discover decodable unmarked overlay code
Admit post-EXE RAM to the dirty interpreter only after a real control transfer and an existing MIPS decodability check. Keep the already-merged overlay initialization guard out of this focused change.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-14 18:12:08 -07:00
Codex 347b6ffb01 Experiment with CPU-copied overlay discovery
Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
2026-07-14 16:13:29 -07:00
Codex edc3fd4e30 Preserve one-frame debug input pulses
Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
2026-07-14 16:02:13 -07:00
Codex b156770bbd Configure the runtime OpenGL 3.3 core context
Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
2026-07-14 15:59:39 -07:00
Matthew Stanley e247155cd6 overlay: use platform-native shared-library suffixes
Adapted from NyperYuhgard's PR #13 POSIX overlay work.

Co-authored-by: NyperYuhgard <93950153+NyperYuhgard@users.noreply.github.com>
2026-07-14 15:58:34 -07:00
Matt Stanley 4e31c6b71d Merge pull request #29 from mstan/feat/debug-analog-input-douglas
debug: expose analog input and stick status
2026-07-14 15:29:47 -07:00
Matt Stanley b1759ee8e1 Merge pull request #28 from mstan/park/pr13-misaligned-dispatch-diagnostics
traps: enrich misaligned dispatch diagnostics
2026-07-14 15:29:44 -07:00
Matt Stanley 75691a4546 Merge pull request #27 from mstan/park/pr13-overlay-cache-inventory
overlay: add opt-in cache inventory telemetry
2026-07-14 15:29:40 -07:00
Matthew Stanley 7350f2413a traps: enrich misaligned dispatch diagnostics
Adapted from NyperYuhgard's PR #13.

Co-authored-by: NyperYuhgard <93950153+NyperYuhgard@users.noreply.github.com>
2026-07-14 10:20:08 -07:00
Matthew Stanley 43e5f286b3 overlay: add opt-in cache inventory telemetry
Adapted from NyperYuhgard's PR #13.

Co-authored-by: NyperYuhgard <93950153+NyperYuhgard@users.noreply.github.com>
2026-07-14 10:19:54 -07:00
Matthew Stanley 7d1fadb07c runtime: report simulated-vblank FPS telemetry
Adapted from NyperYuhgard's PR #13.

Co-authored-by: NyperYuhgard <93950153+NyperYuhgard@users.noreply.github.com>
2026-07-14 10:19:50 -07:00
Codex dbaebaf37a Expose analog input through debug controls
Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
2026-07-14 10:01:12 -07:00
Matthew Stanley faaacc555d fix: keep explicit BIOS precedence over stray arguments 2026-07-14 00:15:47 -07:00
Matt Stanley f7d6bf2149 Merge pull request #22 from mstan/feat/config-guarded-mips-patches-douglas
Add config-driven guarded MIPS patches
2026-07-14 00:05:13 -07:00
Matt Stanley f88828327e Merge pull request #18 from mstan/fix/posix-overlay-export-scan-douglas
Load cached overlay exports on POSIX
2026-07-14 00:03:48 -07:00
Matt Stanley e94574a3d9 Merge pull request #21 from mstan/fix/msvc-generated-init-shaneomac
Add MSVC portability for runtime and generated startup
2026-07-14 00:03:44 -07:00
Matt Stanley a03350fb68 Merge pull request #23 from mstan/fix/overlay-init-guard-shaneomac
Guard overlay entry points before initialization
2026-07-14 00:03:41 -07:00
Matt Stanley 23edb88b68 Merge pull request #20 from mstan/fix/cdrom-encoded-interrupts-kem0x
Fix encoded CD-ROM interrupt mask matching
2026-07-14 00:03:38 -07:00
Matthew Stanley e7b61c807c recompiler: add guarded config MIPS patches
Add alias-aware, opcode-verified game-owned instruction replacements before discovery and code generation. Validate duplicate IDs and physical sites, preserve guarded overlay variants, and cover parser and generated-code behavior with CTest.

Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
2026-07-13 21:59:18 -07:00
Matthew Stanley c0fa9b1e89 overlay: load cached exports on POSIX
Port Douglas Vanderveer's manifest-driven macOS/Linux overlay discovery onto the current cache index and exact-dispatch loader. Validate both filename key fields, preserve ABI cleanup and rescan idempotence, and cover the POSIX primitives with a real shared-library fixture.

Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
2026-07-13 21:52:25 -07:00
Matthew Stanley 5b8098a9d9 runtime: guard overlay loader before initialization
Adapt only the fail-closed overlay lifecycle checks from mstan/psxrecomp#16 commit e6809ccf7d. Add a focused structural regression test and record the excluded dirty-interpreter and game/runtime work.

Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:48:39 -07:00
Martin Penkava 05d4f35bd6 build: MSVC portability for runtime and generated code
- psx_cyc.h / psx_cycles.c: _BitScanForward/_BitScanReverse shims for
  __builtin_ctz/__builtin_clz under _MSC_VER
- full_function_emitter.cpp / main_psx.cpp: emit a portable .CRT$XCU
  init-pointer instead of GCC-only __attribute__((constructor)) in
  generated dispatch/game code
- main.cpp: declare cross-language globals extern "C" so MSVC links
  them against their C definitions (no-op for GCC/Clang)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:47:40 -07:00
Matthew Stanley 76ddcc008a test(spu): pin END-only voice mute semantics
Document the overlap with kem0x PR #14 and explicitly record the game-specific and unrelated changes excluded from this branch.

Co-authored-by: Kareem Olim <kareemolim@gmail.com>
2026-07-13 21:40:11 -07:00
Matthew Stanley 52eb8e8a24 Fix encoded CD-ROM interrupt mask matching
Adapt the CD interrupt mask/status comparison from kem0x PR #14 and centralize it in a tested helper. The HC05 exposes numeric INT1..INT5 reasons but gates the line with a direct mask/status AND, so the common 0x07 mask must also pass INT4 and INT5.

Source: https://github.com/mstan/psxrecomp/pull/14

Source-commit: https://github.com/mstan/psxrecomp/commit/dd9e5c65073dd41fbac2d965d2f945a866ec59e2

Co-authored-by: Kareem Olim <kareemolim@gmail.com>
2026-07-13 21:38:58 -07:00
Matthew Stanley dde268dc0f input: decouple analog stick from D-pad in analog pad mode
The default controller map folds the left stick onto the D-pad bits
(up=dpup,lefty- … right=dpright,leftx+) so the stick acts as a D-pad
for digital-only games. In ANALOG pad mode that fold is wrong: the
stick already drives the analog axes, and on a real DualShock the
stick and D-pad are independent. A dual-analog title that uses the
D-pad as its own control therefore saw phantom D-pad presses from
every stick movement — and constant presses from stick centre drift.

Ape Escape is the concrete victim: it is locked to analog, its D-pad
left/right rotates the camera, so walking with the left stick (or a
drifting Series-X stick at rest, past the 12%/4000 deadzone) spun the
camera with no D-pad input. Reported by VGEsoterica.

Fix in the framework (class fix, all analog titles inherit): resolve
the effective pad type before building the button word, and when the
pad presents as analog this frame, suppress the LEFTX/LEFTY/RIGHTX/
RIGHTY axis sources as button contributors. Trigger axes (L2/R2) and
button sources are unaffected; digital mode keeps the stick->D-pad
fold (its only outlet there). The physical D-pad still drives the
D-pad bits, so Ape's camera rotate works from the D-pad as intended.
Stick/axis output is byte-for-byte unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:54:28 -07:00
Matthew Stanley 2bebd29629 Harden audio reserve and transient frame catch-up 2026-07-13 05:23:33 -07:00
Matthew Stanley b39387eb00 Fix exact native and static overlay dispatch 2026-07-13 05:23:16 -07:00
Matthew Stanley d53742310d release hygiene: no baked build-machine paths, loud malformed-settings warning
Triage of Tomba2Recomp GH issue #1 exposed three ways a release exe
behaves differently on the build machine than on every user machine:

- DEFAULT_BIOS_PATH baked an ABSOLUTE build-tree path into the binary,
  so promoted/release exes silently load the builder's BIOS wherever
  that path exists and the clean-install picker flow is never
  exercised where releases are validated. runtime.cmake now refuses
  absolute values (loud configure warning, converts to the relative
  default); dev checkouts keep prompt-free boots via a new
  resolve_bios_path rung that walks exe-dir ancestors for
  psxrecomp-v4/<relative>, a layout only dev trees have.

- A settings.toml with a TOML syntax error was silently discarded
  wholesale (user's renderer choice, BIOS/disc paths - everything),
  and the next launcher save then overwrote the file with defaults.
  load_user_settings now reports parse_error; the runtime preserves
  the broken file as settings.toml.bad and warns via message box +
  stderr.

- package_release.ps1 gains guards: fail packaging if the staged exe
  contains a drive-letter absolute SCPH1001.BIN path, if the stage
  carries BIOS/disc/save/sidecar files (SCPH*.BIN, *.cue, *.iso,
  *.mcd, bios.cfg, disc.cfg, settings.toml, keybinds.ini,
  overlay_captures.json), or if saves/ is non-empty.

Verified: rebuilt Tomba2 against this tree - configure warning fires
on the absolute arg, strings scan of the exe finds no drive-letter
BIOS path, and a boot with no [bios] setting and no bios.cfg resolves
the BIOS through the psxrecomp-v4 dev rung.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:52:37 -07:00
Matthew Stanley 5a2c6fd3f0 iso_reader: support multi-FILE cue sheets (redump track-split dumps)
The cue parser kept a single bin_filename that every FILE line
overwrote, so a redump-style dump (one .bin per track) mounted only
the LAST track file - the CD-DA audio bin - as the entire disc: no
ISO9660 filesystem, no boot EXE, black screen at startup (reported
against Tomba! 2, whose USA disc is data + one CD-DA track).

Replace the single ifstream with a BinSegment table: one entry per
FILE line, opened for the reader's lifetime and laid out at cumulative
disc-relative sector offsets in cue order. ReadSector/ReadRawSector
resolve a disc LBA to its owning segment; INDEX times, which a cue
states relative to the owning file, are converted to disc-relative
track starts once the segment layout is known. Single-file images are
a one-segment table with unchanged addressing.

Also fixed in passing: Open() never cleared tracks_ from a previous
open (reopening a reader accumulated stale TOC entries), per-read
sector-geometry probing is now computed once per segment, a cue whose
FILE is missing or non-BINARY (WAVE/MP3) fails Open() instead of
mis-mapping the TOC, and GetSectorCount() reports the whole-disc
total.

Verified: Tomba! 2 single-bin dump split at the track-2 boundary
(concatenation SHA1-identical to the original); a host-side test
opens both cues and confirms all 186430 sectors byte-identical and
the TOC identical (track 2 at LBA 174674, audio); the game boots to
intro FMV, title, and attract gameplay from the multi-file dump with
CD audio flowing, and the single-file dump boots identically on the
same binary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 18:29:32 -07:00
Matthew Stanley 34ca37cc65 Merge feat/opengl-performance: full-rate OpenGL presentation 2026-07-11 14:08:08 -07:00
Matthew Stanley fb585541ec opengl: sustain full-rate gameplay and FMV presentation 2026-07-11 14:07:17 -07:00
Matthew Stanley 8f2389211c Merge feat/kernel-stub-bless: byte-verified static dispatch for the relocated kernel window
Closes the latent Rule-18 soundness hole: kernel-window static dispatch
hits (RAM [0x500,0x8500), the BIOS's boot-time ROM copy) now require the
live RAM bytes of the function's full reachable extent to byte-match the
ROM source (lazy verify, cached, precisely invalidated on writes); patched
bodies divert to the faithful interpreter. Emitter ships
psx_bios_kernel_bodies[] (499 entries); runtime guard in memory.c; TCP
kernel_bless observability; PSX_KERNEL_BLESS=0 A/B switch.

USER-VALIDATED on Tomba (SCUS-94236): multiple play runs, LLE + HLE boots,
pads/menus/events, title 1.00x / village 0.92x parity; 208-214/219 kernel
functions verified clean, the 5 mismatches (runtime-patched pad-driver
family) faithfully interpreted as before.

NOTE: rolls the codegen hash — regen BIOS+game generated C and recompile
overlay shards with a same-tree recompiler on each title's next fw bump
(the overlay loader fail-closes loudly on tag mismatch).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:59:20 -07:00
Matthew Stanley 71c2df0f54 kernel-image bless: byte-verified static dispatch for the relocated kernel window
The generated dispatcher ran kernel-window table hits (RAM [0x500,0x8500),
the BIOS's boot-time copy of ROM 0x1FC10000+) natively with NO check that
the live RAM bytes still match the ROM image the functions were compiled
from — unsound for runtime-patched kernel code (the BIOS installs pad/SIO
handlers INSIDE compiled bodies; live diff: 15 clusters, 348 bytes). It
never misfired only because entry points happened to avoid patched keys.

A static kernel hit now requires psx_kernel_bless_dispatchable(): the
emitter emits psx_bios_kernel_bodies[] (key -> full RAM extent of the code
reachable from it; continuations carry their parent's extent, 499 entries),
and the runtime lazily byte-compares each entry's body against the ROM
source, caches the verdict, and invalidates precisely on writes into the
body (store chokepoint, DMA/EXE range marks, savestate restore). Clean ->
guarded native; patched/unverifiable -> the faithful dirty-RAM interpreter,
exactly as Rule 18 prescribes. This implements the runtime half of the
relocation-manifest contract (docs/RELOCATION_MANIFEST_FORMAT.md).

Measured on Tomba (SCUS-94236): 208-214 of 219 verified functions clean
(the 5 mismatches are the genuinely patched pad-driver family, faithfully
interpreted); millions of guarded native hits per boot; title 1.00x,
village 0.92x (parity — kernel keys were already native, now they are
PROVEN native); LLE and HLE boots + gameplay input verified. Kernel-window
interp measures 1.3% of interp burden (dirty_ram_stats), so this is a
soundness fix, not a speedup; the dominant interp cost is overlay-region
coverage (cg-tag hygiene), tracked separately.

Observability: TCP kernel_bless (entries/clean/mismatch/native_hits/
verifies/invalidations), always-on. PSX_KERNEL_BLESS=0 reverts to
unguarded-table behavior for A/B.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 19:12:32 -07:00
Matthew Stanley eefea1295f Merge codex/ape-widescreen-framework: scope curved backdrop projection sites (Ape widescreen) 2026-07-10 16:33:00 -07:00
Matthew Stanley 244474a147 widescreen: scope curved backdrop projection sites 2026-07-10 15:51:07 -07:00
Matthew Stanley a0818de0b4 Merge feat/ws-2d-scene-pillarbox: 2D-only scenes pillarbox in native-wide; present ring; full-pad input injection
Tomba 16:9: rooms, sky falls, and task-card sequences presented stretched
because native-wide's 2D-backdrop stretch fires wholesale in scenes whose
background phase never ends. Such scenes now present native 4:3 from the
faithful canonical buffer, keyed on DEEP UNTAGGED POLYGON OVERHANG (>=24px
past the canonical window, >=4 polys, sustained 2 frames) — the only world
signal that survived adversarial scenes (shaded prims, GTE volume, any-poly
and shallow overhang all falsified by census evidence; lineage in gpu.c).
Scoped to sprite-tag titles; full_2d (MMX6) and gte_game_mode (Ape) paths
unchanged. Plus: native-wide never stretches a canonical-width game frame
(fallback pillarboxes); always-on present-classification ring (TCP
present_ring) across all backends; debug input injection drives the full
pad model (hybrid d-pad drop + stick axes); savestate TCP honesty; census
tagged column + ws_hud_mode A/B (HUD-corners deferred: ENHANCEMENTS.md W2).
USER-VALIDATED on Tomba (SCUS-94236).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:41:52 -07:00
Matthew Stanley d57ce7a121 widescreen: census tagged column + ws_hud_mode A/B; defer HUD-corners for tag titles (ENHANCEMENTS W2)
The Tomba nw_hud_corners experiment surfaced two blockers, both documented
in ENHANCEMENTS.md W2 with census evidence: composite dialog boxes spanning
zone boundaries tear (end caps pulled to opposite edges), and the AP counter
renders through the TAGGED sprite funnel so the untagged-only gate skips it.
Groundwork kept for the next attempt:
- WsCensusEntry gains a `tagged` column (ws_census CSV) so funnel membership
  is measurable per prim.
- TCP ws_hud_mode {"tag_rects":0|1} live-A/Bs the tagged-rect gate.
Tomba's game.toml reverts nw_hud_corners to false (its own repo).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:37:59 -07:00
Matthew Stanley b85f10959b widescreen: pillarbox 2D-only scenes in native-wide; HUD corner re-anchor for tag titles; present ring
2D-only-scene classifier (fixes room/fall/title-card stretch, Tomba):
- Native-wide's 2D-backdrop stretch fires wholesale in scenes whose
  background phase never ends (room interiors, sky-only falls), stretching
  the whole frame. Such scenes now present native 4:3 from the faithful
  canonical buffer (gpu_ws_present_native_43).
- The world signal is DEEP UNTAGGED POLYGON OVERHANG: polys whose raw SX
  extent crosses >=24px past the canonical window, >=4 per frame, sustained
  2+ consecutive frames. Lineage of rejected signals (each defeated by a
  real scene, census-evidenced): shaded-prim presence (title-card letter
  tiles are gouraud), GTE projection volume (task-jingle projects
  world-scale counts; title screen idles at 1680 verts), any-poly overhang
  (card letters slide in from off-screen), shallow overhang (jingle
  sparkles poke <=20px). Outdoor terrain measures 55 deep-overhang
  polys/frame vs 0-1 for every observed 2D scene/effect.
- Scoped to sprite-tag titles: full_2d (MMX6) and gte_game_mode (Ape)
  classification is unchanged.

Native-wide present invariant:
- A game frame that cannot present wide (compositor fallback) pillarboxes
  4:3 instead of stretching the canonical read across the wide window
  (all backends; squash mode still stretches by design).

HUD corner re-anchoring for sprite-tag titles ([widescreen] nw_hud_corners):
- Poly/line prims never shift (they are the world / tagged billboards);
  rect-family prims shift only when UNTAGGED — the proven hud_sprt_squash
  discriminator — so enabling nw_hud_corners is safe without a packet
  arena range.

Observability (always-on, institutional):
- present_ring: per-present classification ring (path, present width,
  game_mode, native_43, wide fallback, tag age, gte_verts, ovh_prims)
  for every backend; TCP "present_ring". Answers "why did this frame
  stretch" after the fact.
- gpu_state ws block gains last_3d_frame, gte_verts, last_world3d_frame,
  ovh_prims, last_ovh_frame; GTE vert counting is now always-on.

Input injection drives the full pad model:
- set_input/press now resolve pad type like real sampling: injected d-pad
  drops a hybrid pad to digital (movement works), optional lx/ly/rx/ry
  stick override (0..255), type via the coherent request channel.
  Previously buttons-only: menus reacted, walking never did.

Savestate honesty:
- savestate_request_save/load return staged/refused; the TCP handler
  reports ok:false instead of silently acking loads it cannot perform
  (LLE host-fiber runs). Torn-restore reliability audit remains open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:23:44 -07:00
Matthew Stanley 2ba81d5d7b Merge feat/audio-mmx4-attract: MMX4 attract-demo audio fixed (two defects)
1) cdrom: continuous disc clock — the read stream froze disc time while
   the guest had an unacked INT, starving the XA->SPU ring ~5-6x/s
   (6 Hz music dropout flutter). Replaced with the Beetle-mirrored
   SetAIP/CheckAIP model: disc never pauses, data-ready INT1 pends one
   deep, presented on ack. read_hold_cycles/int1_* tripwires in
   cdrom_state.

2) spu: END-without-REPEAT now jumps to the repeat address AND forces
   the envelope to zero (Beetle spu.cpp:333,341-352). Previously parked
   one-shot SFX voices decoded past the terminator at full envelope —
   X4's SFX bank parks them on +28672-DC filler with ~infinite release,
   railing the mix at +32767 (38-41% samples clipped = the 'static').

User ear-validated on the MMX4 attract ice-stage demo. Gate: MMX4 0.00%
clip post-fix; MMX5 boots healthy (pend path exercised 12-13x, 0 lost);
Tomba behavior equivalent to master. Runtime-only — no regen needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:19:14 -07:00
Matthew Stanley 462e2bf5fd spu: END-without-REPEAT jumps to repeat and forces the envelope to zero
An ADPCM block whose flags carry END (bit 0) but not REPEAT (bit 1)
must (a) jump the decode pointer to the latched repeat address and
(b) force the ADSR envelope to ZERO on entering Release — Beetle
spu.cpp:333 (CurAddr = LoopAddr, unconditional) and :341-352 ("Force
enveloping to 0 if not looping"). Our voice_next_sample did neither:
it entered Release with the envelope intact and kept decoding FORWARD
past the terminator, assuming the release ramp would mask whatever
followed in SPU RAM.

X4's SFX bank breaks that assumption three ways: its one-shot samples
terminate on an END-only block; the next block in RAM is non-silent
filler (0x77 nibbles at shift 0 = constant +28672) carrying a
self-referential LOOPSTART flag, so the walker parks there forever;
and the bank's ADSR release shift is ~infinite, so the envelope never
decays. Every finished SFX voice became a full-scale DC source. In the
X4 attract ice-stage demo (user report), a handful of ice-block-break
SFX stacked ~15 such voices and pinned the whole mix at +32767 for
~35 s — heard as static / the music "cutting out". Measured: 38-41%
of samples hard-clipped in the spu_out AND host taps during the demo.

With the fix (jump + env=0; the env-0 Release path then deactivates
the voice, matching Beetle's silent-voice behavior): 0.00% clipped
samples over the same demo window, peak 32291, zero railed polls in
150 across the full attract cycle; the ice-crack SFX bodies still
play normally.

Found via the new rail-triggered spu_voices capture + the spu_ram
peek: 15 voices sharing start=0x16AD0 all parked at cur_addr=0x17930
looping 0x17920 (flags 0x07) at env 0x7FBF in phase RELEASE, and the
block bytes 00 07 77 77 .. decoding to +28672 DC.

Debug surface added (always-on doctrine):
- spu_voices now reports live env level + ADSR phase per voice.
- new spu_ram command: {"addr","len"} -> hex peek of SPU sample RAM.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 15:02:31 -07:00
Matthew Stanley 9306db59b3 cdrom: continuous disc clock — never freeze the read stream on INT ack
The read stream froze read_delay whenever the guest had an unacked
controller INT (and while the sector buffer was undrained). Real drives
never pause: measured on MMX4's attract/title XA music, the freeze
accumulated ~1M guest cycles/second (~3% of disc time), every XA sector
arrived 32-92 samples late, and the XA->SPU cd_ring starved ~147 samples
5-6x per second — audible as a 6 Hz crackle/dropout flutter in all
XA-streamed music (user report: MMX4 Zero ice-stage attract demo).

Faithful model, mirrored from the in-tree Beetle oracle (cdc.cpp
HandleSectorRead / SetAIP / CheckAIP / ClearAIP):

- Disc time NEVER pauses while reading; sectors are read on exact
  cycle schedule. XA-ADPCM realtime audio flows to the SPU decoder
  unconditionally (this alone removes the crackle).
- A data sector that comes due while the guest still has an unacked
  INT overwrites the sector buffer on schedule (hardware clobbers its
  FIFO the same way) and its data-ready INT1 pends ONE deep, presented
  the moment the guest fully acks (Beetle CheckAIP). A newer sector
  replacing an unpresented notification is counted + traced 'P'
  (Beetle "Previous notification skipped").
- Drive-state changes (Read/Play/Pause/Stop/Seek paths through
  start/stop_read_stream) cancel the pended notification (ClearAIP).
- The active-DMA refill path (deliver without new INT while a
  multi-sector CD DMA drains) is unchanged.

Verification on MMX4 (SLUS-00561) title/attract XA:
- CD_PUSH guest-cycle spacing: exactly 1,806,336 cycles (8 sectors at
  2x), min == max over 148 pushes (was 1.83-1.88M, systematically late).
- read_hold_cycles == 0 (was ~30.8M per 30 s) — kept as an always-on
  regression tripwire in cdrom_state.
- int1_pended == 0 during music: X4 acks fast; the pending path only
  arms for genuinely slow acks.
- spu_out/host tap rings: 6 Hz zero-gap storm (86-144 gaps/95 s of
  147-265 samples) eliminated; only real content silences remain.
- cd_underflow_frames stays 0 during playback.

Always-on observability added (ring-buffer doctrine):
- XA zero-run scanner in the decode path -> AUDIO_EV_XA_ZERO events
  (stage 0 = post-ADPCM, stage 1 = post-resample+volume, with LBA).
- CD_PUSH audio events now stamp the global guest-cycle clock (the
  spu_out sample stamp is pump-quantized and cannot measure cadence).
- cdrom_state: read_hold_cycles/read_hold_events (must stay 0),
  int1_pended / int1_lost / int1_pending_now.
- Savestate: pending_dataready fields appended to CDROM_SNAP_FIELDS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:55:52 -07:00
Matthew Stanley e8943945e1 Merge codex/tomba2-whoopee-beach-fix: native-wide flat/phase backdrop gates + FMV silent-hold
Tomba 2 widescreen enablers: [widescreen] nw_flat_backdrop /
nw_phase_backdrop native-wide mirror gates (canonical 4:3 untouched),
[video] fmv_skip_no_xa_hold for silent preloaded-logo auto-skip,
ws_dbg_stretch modes 7/8, fmv_state JSON escaping.

Conflicts (config_loader.h/.cpp, main.cpp) were both-sides-adds vs the
MMX4/5 clear_reveal work - resolved keeping both feature sets.
Compile + boot sanity: Ape build-release rebuilt and boots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:48:56 -07:00
Matthew Stanley 264f00cee5 widescreen: native-wide flat/phase backdrop gates + FMV silent-hold (Tomba 2 Beach Town)
- [widescreen] nw_flat_backdrop: in the native-wide mirror only, stretch
  untextured flat primitives across the wide output (flat-colour sky/water
  backdrops). Canonical 4:3 framebuffer stays byte-for-byte untouched.
- [widescreen] nw_phase_backdrop: stretch textured prims emitted before the
  frame first shaded 3D primitive (authored 2D backdrop phase), isolating
  the sky from the textured 3D foreground. Both runtime-only, off by default,
  title-opted (draw ordering is title-specific).
- [video] fmv_skip_no_xa_hold (4..3600 vblanks): presentation-side
  fast-forward latch after the last silent MDEC decode, so auto-skip covers a
  preloaded logo authored post-decode wait (Whoopee Camp logo). Guest
  timeline unchanged; default stays the generic 4-vblank hysteresis.
- ws_dbg_stretch correlation modes 7 (all prims) / 8 (all flat prims).
- fmv_state: JSON-escape config_path + report fmv_skip_no_xa_hold.

Validated on Tomba 2: Release build, unattended first-attract captures at
4:3 / 16:9 / 21:9, zero unknown dispatches.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 13:45:23 -07:00
Matthew Stanley e5a958f86a Arm the text-image guard from the disc image; require it for the alternate game-start latch
The dirty-RAM text-image guard (685f38d) only armed when the loose local
boot-EXE file next to game.toml existed - found via find_project_root's
repo-marker walk. End-user release installs have neither a repo checkout
nor the EXE file (it is copyrighted game data), so the guard silently
never armed. Without it, fntrace's alternate game-start latch
(psx_game_address_in_text && dirty_ram_text_native_ok) cannot fire for
titles whose BIOS handoff enters text inside the shell-window shadow
(RAM 0x30000-0x5AFFF -> shell ROM until game-start): the shadow never
lifts, execution runs dead shell ROM bytes over real game text, and the
boot dies in a jump to PC=0 (Ape Escape v0.0.2/v0.0.3 release zips,
frame 14, both HLE and LLE boots; in-repo runs armed invisibly and
masked the bug during validation).

Fix, class-level for every title:
- main.cpp: defer guard arming until the disc path is resolved and add a
  second source - extract the boot EXE from the mounted disc image via
  ISOReader (basename of game.toml's exe field, SYSTEM.CNF BOOT token
  fallback). The disc is the same bytes the BIOS loads, i.e. the true
  reference; every install has one. Local EXE file still wins for dev
  checkouts. A loud WARNING prints when neither source arms (previously
  silent, which is how the broken releases shipped green).
- fntrace.c: the alternate game-start latch now requires a registered
  image (dirty_ram_text_image_registered). Its safety argument - "RAM
  still holds shell bytes that differ from the game image, so it cannot
  fire prematurely" - only holds when an image exists; with no image,
  native_ok degrades to !dirty and untouched text pages pass it,
  latching game-start before the EXE loads and clearing the dirty
  baseline mid-load. Without an image, only the exact entry_pc dispatch
  may latch (the pre-685f38d conservative behavior).

Verified: v0.0.3 zip contents + rebuilt exe in an isolated directory
(no repo above it) boots to the Ape Escape intro on both HLE and LLE
paths, guard armed from disc SCUS_944.23; in-repo runs still arm from
the local EXE.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:54:00 -07:00
Matthew Stanley f1afc7fa44 Merge feat/l3-r3-stick-binds: first-class L3/R3 stick-click binds
Controller: l3=leftstick / r3=rightstick defaults (map rows were missing
entirely). Keyboard: T/Y defaults. Beetle oracle keyboard: T/Y. Shipped
input.ini templates + README updated. Validated on Ape Escape (user-
confirmed L3/R3 on real pad).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:09:50 -07:00
Matthew Stanley 29b1fd6246 input: first-class L3/R3 (stick click) binds across the keybind ecosystem
The controller map had no l3/r3 rows at all, so pad bits 1/2 could never
be produced by a gamepad — DualShock-only games (Ape Escape) were
unplayable. Keyboard slots existed but shipped unbound.

- main.cpp: PAD_L3/PAD_R3 bits; controller_map 14->16 entries; defaults
  l3=leftstick, r3=rightstick; input.ini template + doc comment updated.
- psx_keybinds.c: keyboard defaults L3=T, R3=Y (continue the Q/W/E/R
  shoulder row). Launcher Controls page picks them up automatically.
- beetle_main.cpp: same T/Y keys on the oracle so psx-beetle stays
  drivable for L3/R3 games.
- tools/package_release.ps1: shipped input.ini template gains l3/r3.
- README: keyboard + controller map tables.

Wire is unchanged: sio.c transmits the button word verbatim, matching
Beetle's dualshock.cpp. Validated in-game on Ape Escape (user-confirmed
R3/L3 inputs on a real pad).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 11:09:33 -07:00
Matthew Stanley dbfc124856 Merge master into feat/mmx4-widescreen-fw
Master brought the MMX5 true-wide generalization, which evolved this
branch's earlier X4 work (unified psx_ws_bg2d_* helpers, copied-a1 cull,
sltiu count sites, nw_left_hud_packet arena filter). Resolution keeps
master's evolved implementations and grafts the X4-only pieces on top:

- bg2d count_site accepts signed slti alongside sltiu (master style
  ?1u:0u emission); startx_site accepts subu rd,zero,rt (X4's
  -(scrollX & 15)).
- cull a1 move sites accept both move encodings (addu rD,a1,zero and
  addu rD,zero,a1) with a distinct addu-but-not-move error.
- HUD arena filter: master's nw_left_hud_packet_lo/hi is the single
  mechanism (X4's parallel nw_hud_source_lo/hi dropped); X4's broader
  prim coverage kept — ws_nw_hud_shift_vertices re-anchors polygons,
  lines and dots as rigid composites alongside the rect/sprite sites.
- mono_rect gets both ws_expand_fullscreen_rect and the HUD re-anchor.

Also carried from this branch: binary-search dispatch table and
Release-gated GL frame_perf instrumentation (auto-merged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:52:04 -07:00
Matthew Stanley c6df17102d gl: compile frame_perf instrumentation out of Release builds
Release compiles out the debug server, so leave the GL timer-query
plumbing disabled there too: a native-wide frame can otherwise issue
hundreds of unused mirror timestamp queries per frame. Developer
builds keep the always-on ring unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:32:52 -07:00
Matthew Stanley 97ea975580 widescreen: X4-generation 2D hooks — inline-count bg2d variants, copied-a1 cull, HUD source-range filter
Generalizes the Capcom 2D widescreen machinery for the MMX4 engine revision:

- [widescreen.bg2d] count_site accepts the inline slti/sltiu loop compare
  (X4/X5 style) alongside MMX6's li; startx_site accepts subu rd,zero,rt
  (X4's -(scrollX & 15)); inline-count sites route through the generic
  psx_ws_bg2d_* helpers that keep the emitted ring mask and skip MMX6's
  title-specific host-side refill state.
- [widescreen.cull] a1_sites now also accept a move rd,a1 (addu encoding):
  classifiers that copy the caller margin with no delay-slot nop widen the
  copied value instead.
- [widescreen] nw_hud_source_lo/hi: optional GP0 command-source range filter
  for nw_hud_corners, so a pure-2D game re-anchors only its dedicated HUD
  packet arena and never world sprites. 0/0 preserves legacy behavior.
  ws_nw_hud_shift_vertices re-anchors polygon/line prims as one rigid
  composite; mono rect/dot/8x8 sites shift too.

All helpers identity at 4:3; unset config keys are byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:32:40 -07:00
Matthew Stanley 40b476c4e4 widescreen: finish MMX5 edge handling 2026-07-10 10:07:40 -07:00