Commit Graph

176 Commits

Author SHA1 Message Date
Matthew Stanley fb585541ec opengl: sustain full-rate gameplay and FMV presentation 2026-07-11 14:07:17 -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 e4145d7008 Merge feat/crashbash-fw-fixes: GTE lm/sf depth-cue/lighting fix + dispatch shell-window + VBlank pacing
GTE honors per-instruction lm (bit 10) and sf (bit 19) in the depth-cue and
lighting family (INTPL/DPCS/DPCT/DPCL/NCDS/NCDT/CDP/...) with the faithful
hardware formula and always-on GTE observability rings (gte_ring_dump,
gte_intpl_dump, gte_frame_stats, gte_latch_dump). Fixes Crash Bash
menu-character strobe/flicker and in-game flattening (signed INTPL vertex
tweens were clamped to zero by the hardcoded lm=1).

Also: dispatch no longer shadows runtime-relocated game code in the BIOS
shell window (native-ok byte compare routes matching text to the game path);
stale GPUSTAT-poll VBlank over-delivery fix + present pacing tooling.

Cross-title validation (GTE_LM_FIX.md): Crash Bash (fix target), Ape Escape,
Tomba 2, Mega Man X6 - all rebuilt branch-flavor (regen included) and verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 08:43:38 -07:00
Matthew Stanley 54df1ecf7b Merge feat/audio-quality-pin: measured audio-quality campaign (MMX5 complaint)
Always-on audio tap rings + SPU event ring (runtime + psx-beetle oracle),
audio_stats/audio_wav/audio_events debug commands, offline A/B toolchain.

Fixes, all measured against the Beetle oracle:
- clock-domain DRC bridge for SPU output (pull-mode, polyphase resampler)
- guest-cycle sample budget (768 cyc/sample exact; was 60.00Hz assumption)
- frame pacer bounded catch-up (was forgiving all >1-period debt)
- PI fill servo with anti-windup (was P-only with standing deficit)
- voice loop-address register is LIVE state (X5 stuck-voice +11dB fix)
- CDC decode-volume matrix (X5 music fades)

MMX5: underruns ~360 -> 1 per 150s; level -3.8 dBFS clipping -> -15.5
vs oracle -14.1. Cross-title gate: Tomba built+booted, bridge fill
stable at target, user ear-validated both titles.
2026-07-10 08:13:17 -07:00
Matthew Stanley 9cf627113a audio: clock-domain DRC bridge for SPU output (fixes sustained crackle)
Replace the push-model SDL_QueueAudio output with the shared band-limited
polyphase resampler + P-only fill servo (recomp_audio_drc.h), fed by an SPSC
ring under SDL_LockAudioDevice and drained by a pull callback. The SPU renders
44100/60 per wall-clock frame while the host consumes on its own crystal; the
unsynchronized clocks drifted the queue to underrun (silence gaps = sustained
crackle). The bridge nudges the resample ratio <=+/-0.5% to hold the ring near
target so the queue never runs dry.

Measured on MMX6: legacy push ~1.5-2.5 underruns/s (queue draining to 0ms) ->
bridge 0 underruns, fill held ~50ms. PSXRECOMP_AUDIO_LEGACY=1 keeps the old
push path for A/B; PSXRECOMP_AUDIO_PROBE=1 prints the fill/underrun trend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:50:55 -07:00
Matthew Stanley f36b458a78 launcher: revert stb launcher to RmlUi on master (user decision)
The stb launcher revamp (8b098bd, ae89a8f, 89e0e0f) uses modern GL 3.0+
without the SDL_GL_GetProcAddress loader ('Phase 2b'), so it fails to
link into game builds, and its rendered UI is not acceptable (user
review 2026-07-10). Revert master to the working RmlUi launcher — the
same revert every recent release (Ape v0.0.2, Tomba2 v0.0.2) already
shipped with. The stb work remains reachable via the original commits
if a Phase-2b GL-loader follow-up ever revives it.

(cherry picked from release/ape-v0.0.2-rmlui 929b78b)
2026-07-10 00:12:26 -07:00
Matthew Stanley 5a5922d98f audio: always-on PCM tap rings + oracle WAV capture (SNES-campaign port)
Measurement infrastructure for the audio-quality campaign (MMX5 complaint),
ported from snesrecomp's audio_trace/oracle methodology and the
_audio_round2 tap model. No behavior change to the audio path itself.

- audio_trace.{c,h}: three always-on 95 s PCM rings — T1 spu_out (canonical
  spu_render output), T2 cd_in (XA/CD-DA PCM entering the SPU CD bus),
  T3 host (exact post-fade bytes handed to SDL_QueueAudio) — plus a 512k
  event ring (every SPU reg write stamped with the SPU sample clock;
  RENDER/PUMP_SKIP/UNDERRUN/MUTE/UNMUTE/CD_PUSH/SPU-DMA), audible-vs-
  silent counters (-42 dBFS gate), queue watermarks, and a server-side
  44100 Hz WAV dumper (rate exactness matters: snesrecomp da33c06).
- psx-beetle: the libretro audio callbacks stop discarding Beetle's fully-
  mixed reference SPU PCM and record it into the same tap ring. Port 4380
  gains audio_stats/audio_wav/audio_events with the identical wire protocol
  as the runtime (rule 16 symmetric harness) — the oracle WAV capture.
- debug_server: audio_stats / audio_wav / audio_events on the runtime port.
- tools: audio_ab_diff.py + audio_spectral_ab.py (drift-tolerant offline
  A/B: drift ppm, onsets, cents, timbre, click rate) and audio_analyze.py
  (first-dirty-tap cross-tap decision tree) ported from snesrecomp/
  _audio_round2; audio_capture_ab.py dumps both processes symmetrically.

Probes query history; nothing is armed at probe time.
2026-07-10 00:00:09 -07:00
Matthew Stanley c4474b9340 fix(gte): honor lm/sf from the instruction in the depth-cue/lighting family; add always-on GTE observability rings
The depth-cue tail (INTPL/DPCS/DPCT/DPCL/NCDS/NCDT/CDP) and the lighting
helpers (light_transform / light_color / color_output) hardcoded lm=1 on
every IR write-back, clamping all negative components to zero. Crash Bash
tweens its vertex animation through INTPL (cmd 0x0980011, lm=0): pose A in
IR1-3, pose B in the far-color registers, blend in IR0. With lm forced on,
every negative vertex component collapsed to 0 on tween frames, squashing
characters onto their anchor point. At 10fps keyframes / 30fps renders that
made the menu character strobe (2 of 3 renders flattened, so exactly one
display buffer ever held a healthy pose) and in-game characters flatten.
Beetle/hardware render the same frames correctly.

depth_cue_from_ir is now the faithful hardware formula: base = IR<<12,
step = lim(((FC<<12) - base) >> sf*12) with the forced-unsigned-off
intermediate clamp, MAC = (base + IR0*step) >> sf*12, final IR clamp with
lm from the instruction word. sf (bit 19) is honored instead of assuming 12.
The lighting helpers take lm from the instruction too (games light with
lm=1, so their behavior is unchanged).

Observability (always-on, build-dbg commands):
- GTE projection ring (gte_ring_dump): every RTPS/RTPT's V/RT/TR/H/OFX/OFY
  inputs and SXY/SZ/FLAG outputs, frame-stamped, with caller ra.
- GTE INTPL ring (gte_intpl_dump): every INTPL's IR0/IR1-3/FC inputs
  (snapshotted pre-op) and MAC/IR/FLAG outputs, with frame filter AND
  offset paging so a whole frame is reachable.
- gte_frame_stats: per-frame nproj/nsat/nflat/nintpl/nintpl_tiny —
  nintpl_tiny (all-tiny output from non-tiny input) is the collapse
  signature and pinned this bug to the INTPL math in one query.
- gte_latch_dump: retained ring of saturated projections.

Verified on Crash Bash SELECT GAME TYPE: nintpl_tiny 335-352/473 -> 0-3,
vertex buffer at 0x80056AC8 no longer alternates healthy/collapsed, and a
20-shot jittered screenshot burst shows the character in every frame
(was bimodal present/absent).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:19:21 -07:00
Matthew Stanley 184a18a2a8 Merge fix/mmx6-sprites: mirrored-2D uv sampling fix + shared uv model + display ring
- gpu_uv.h: the ONE shared PS1 uv-sampling model for GL/VK/SW (limits +
  Beetle Calc_UVOffsets mirrored-2D compensation)
- fixes the MMX6 sprite-sliver class (detached 1px sprite-colored lines at
  quad seams of X/Y-flipped sprites; pink line under wheel mechaniloids):
  poly path AND the gpu.c axis-aligned-mirrored-quad -> rect_scaled path
- always-on display ring (display_ring_get/aux/stats): frame-exact display
  + full-VRAM capture for cross-renderer forensics
- user-validated clean on MMX6 (GL); cross-title gate: Tomba GL boot +
  attract verified clean against this branch

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 19:27:52 -07:00
Matthew Stanley e8ad8ade3d debug: always-on display ring + full-VRAM aux capture
New always-on ring in the debug server: every vblank captures the display
area's raw 15-bit pixels AND the full 1024x512 VRAM as the ACTIVE RENDERER
sees it (GL: FBO truth via fbo_peek; SW: CPU truth), 64 frames of lookback.
Commands: display_ring_get (frame -> PNG), display_ring_aux (frame -> raw
u16 VRAM blob), display_ring_stats.

Purpose: FRAME-EXACT visual forensics. screenshot_file returns whatever is
on screen when the command lands (1-3 frames late), which cannot support
cross-renderer same-frame comparison; the ring can. Proven use (MMX6 sprite
triage): pulled the SAME deterministic frame from a GL run and a software
run, micro-cosimed the suspect sprite prim from the GP0 ring against the
same-frame captured texels/CLUT, and established GL and SW render the scene
pixel-identically with byte-identical texture content - reclassifying the
reported artifact from renderer bug to (pending Beetle oracle) game content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-09 18:26:31 -07:00
Matthew Stanley 7b746a0a4b fix(gpu): stop stale GPUSTAT-poll VBlank over-delivery; add present pacing + strobe tooling
Framework-level improvements distilled from an external Crash Bash fork of
psxrecomp, rebased onto current master. Two of the fork's five touched files
(code_provider.c stddef include, overlay_loader.c non-Windows atomic shim) are
already fixed upstream and were dropped; the CrashBash runtime target is
game-side and lives in the game repo, not here.

gpu.c — the core bug. gpu_read_gpustat()'s poll fallback raised IRQ_VBLANK+LCF
every 1000 GPUSTAT reads with no cycle gate. A title that busy-polls GPUSTAT for
GPU/DMA readiness (not just vsync) got ~38 extra "fake" vblanks/s — ~96/s
delivered vs the correct 60 — over-advancing the guest VSync frame counter and
jittering animation (measured on Crash Bash as character strobe). The recompiled
BIOS+game now carry per-block interrupt checks + cycle charging, so the tight
poll loops that once justified this hack already advance guest cycles and let the
one cycle-paced VBlank authority (interrupts.c) fire on schedule. The hack is now
OFF by default; PSX_POLLHACK_VBLANK=1 restores legacy for A/B. Also adds per-frame
GP0(E5) draw-offset-Y range instrumentation for double-buffer analysis.

debug_server.c — two debug commands: `vblank_rate` (cycle-paced raise/deliver
counts, poll-fallback raises, per-frame draw-offset range) and `dump_buffer`
(dumps a raw 512x240 VRAM band at a given display-Y so both double-buffer halves
can be inspected in one call).

main.cpp — two presentation improvements, both generic:
 * sync-to-host-refresh: nudge the wall-clock pacer to the exact panel period
   when the display is within ~2% of 60 Hz, so the pacer and SDL PRESENTVSYNC
   stop fighting (a 59.94 pacer vs a 60.00 panel beats 2/3/1 = judder). ON by
   default but only engages on ~60 Hz panels; other refresh rates keep 59.94.
 * frame-blend masker: optional two-frame average for 30fps double-buffered
   content, guarded to the plain software path. OFF by default (PSX_FRAME_BLEND=1).

Not yet compile-verified in this environment (no toolchain/ISO here); anchors,
symbol linkage (g_vblank_*/g_doff_*/g_pollhack_*, gpu_display_pixel_rgb,
png_write_rgb) and brace balance verified against master by inspection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 17:05:37 -07:00
Matthew Stanley 0cb2a77bff fix(irq): gate deferred thread-switch off dirty-pump and low-kernel sites
Ape Escape memcard save/load. The deferred in-exception cooperative
thread-switch could be honored at a boundary where a candidate resume PC
was committed but the live CPUState (stack/GPRs) still described the
previous local frame, corrupting the resumed thread.

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:50:09 -07:00
NyperYuhgard ae89a8fdd6 compat with me 2026-07-08 00:01:29 -05:00
Matthew Stanley 4ca767f90a obs(debug): phase profiler, idle-skip toggle, shadow-diff dump counters
TCP surfaces for the new instrumentation:
- phase_profile / phase_hot: a ~1 kHz background sampler over g_exec_phase
  reporting wall-time share per backend, with a since-boot histogram and
  windowed diffing;
- idle_skip: idle-loop cycle-skip counters and a runtime enable/disable
  toggle;
- overlay_shadow_dump gains interior_gated / in_shadow / native_exec /
  escapes / escapes_native for the shadow-diff hardening.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:39:51 -07:00
Matthew Stanley f29b0b7e74 diag(wtrace): always-on capture of Ape LOAD-GAME libcard op-state
Add compiled-in always-on wtrace ranges (slots 16-21) for the Ape memcard
higher-layer investigation: libcard op struct count/state (0x800b4e30),
save-name ptr+buffer (0x800b4e50), op-handler ptr (0x800b4ed0), top-scene
SCENE/TRIG (0x800e3880), game card-menu substate (0x8013af50), and kernel
card-driver per-state byte (0x7264). Per the ring-buffer rule these extend
the always-on capture path (query via wtrace_dump addr_lo/addr_hi) rather
than probe-time arming.

Checkpoint of the memcard save-stall investigation (not yet fixed): both
failure modes (hard freeze at 0x801382CC register smear; soft stall = async
read 0x25AC aborts at MC_ID2) trace to one root: the interrupt-driven async
card read per-byte SIO ACK-IRQ clocking is unreliable under the HLE-scheduler
+ dirty-interp + IRQ-delivery interaction (Stage-5 SIO axis). Low-level
protocol + completion bridge confirmed working; masking, low-level
event-delivery, compiled-resume-latch, and deferred-switch all ruled out.
Full detail in .claude/GOAL.md. Fixes #2/#3 kept.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 18:13:47 -07:00
Matthew Stanley b7880141a6 feat(debug): screenshot tool emits PNG by default on both backends
Extract the runtime's dependency-free RGB->PNG writer into a shared header
(png_write.h, static linkage so each self-contained binary keeps its own copy,
no new link deps). The Beetle debug server's screenshot_file now writes a PNG
(top-down 24bpp RGB) instead of a BMP and defaults its path to .png, matching
the runtime backend — so screenshots are viewer/Read-friendly with no manual
BMP->PNG conversion. Runtime already emitted PNG; this unifies the encoder.

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:31:18 -07:00
Matthew Stanley d31be0e758 gte: dome-locate probe (ra-based) + ws_dome_probe TCP cmd
Tally which GUEST function (guest ra at gte_execute, not the libgte leaf
0x80000F40 that g_debug_current_func_addr reports) projects far/sky vertices,
to identify the sky-draw fn for the per-function dome-expand bracket. ws_dome_probe
on=1 thr=<SZ> arms; dump lists ra + count + max_sz. FINDING (Ape): the title sky
is a curved dome with a RANGE of depths, so the depth-gated dome-expand only
scales its farthest verts (warps it, can't cleanly fill) — confirms the per-
function bracket is required. Lead: far-sky verts came from game code in the
~0x80138xxx overlay region. Probe stays as a tool; Ape dome wiring is a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:30:11 -07:00
Matthew Stanley 402fe01b11 gte: native-wide sky-dome expand (opt-in, off by default) + ws_dome tuning cmd
Depth-gated dome-expand: in native-wide (no squash), scale far-depth
(SZ >= ws_far_threshold) GTE-projected X outward from the projection centre by
(3*num)/(4*den) so a FINITE sky-dome mesh grows to fill the wider FOV instead
of leaving black corners. Live-tunable via the ws_dome + ws_far_threshold TCP
commands; off by default (no config wires it on), so inert unless toggled.

NOTE: depth alone does not cleanly isolate Ape's title dome (its sky is not the
uniquely-farthest layer — distant world geometry shares the depth range), so the
Ape title needs the per-function bracket route (find the dome draw fn, bracket
with gte_ws_set_dome_expand like the far-backdrop unsquash_funcs). The mechanism
lands here; the Ape wiring is a follow-up. Correct as-is for titles whose sky IS
the farthest layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:30:10 -07:00
Matthew Stanley c6dc87fdfa native-wide GL: fix center-blit double-buffer artifact + add GL wide_full TCP tool
- wide_blit_center now copies the canonical column into the wide-surface CENTRE
  over the FULL VRAM height, not just [disp_y, disp_h]. Ape vertical-double-
  buffers (display_y 0<->256, both bands in one wide surface); a scrolling
  cutscene (the cityscape intro) exposed centre rows outside the display band,
  which the disp_h-only blit left black -> horizontal black-band tearing. Full-
  height blit makes the wide centre bit-identical to the full-mirror output for
  every band. Fixes the regression; keeps the perf win.
- sticky gte_game_mode threshold/hysteresis retained (cutscene flicker fix).
- New GL wide-surface inspection over TCP: wide_dump_full backend method
  (glb_wide_dump_full) so the `wide_full` command dumps the ENTIRE GL compositor
  surface (both double-buffer bands + margins), matching the SW path. wide_shot
  (displayed band) already worked on GL. Lets native-wide be debugged entirely
  over the TCP server, no window focus.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:30:10 -07:00
Matthew Stanley ee09ab9bbb widescreen: generic Ape-Escape-class support + native-wide GL perf + cutscene flicker fix
Adds full 16:9/21:9 native-wide support for fully-3D titles (Ape Escape,
SCUS-94423) as a generic, opt-in, 4:3-byte-identical framework capability, plus
a native-wide GL compositor perf fix and a cutscene present-mode flicker fix.

Widescreen (generic, per-game opt-in via [widescreen*]):
- Shared screen-extent cull detector (runtime/include/ws_cull_detect.h) used by
  all three backends (recompiler, dirty-RAM interp, sljit). Per-game signature
  immediates ([widescreen.cull] screen_w_imms/screen_h_imms) — Ape's funnel
  tests 0x181/0xF1 on its 368px display, not Tomba's 0x140/0xE0.
- Three funnel idioms widened: unsigned per-vertex sltiu (psx_ws_cull_sltiu),
  signed min/max slti right edge + bare-bltz left edge (psx_ws_cull_slti/bltz,
  new helpers + SLJIT_HLP appends), and billboard center+/-halfwidth. Explicit
  [widescreen.cull] slti_sites for X-only funnels the auto-detector can't
  qualify.
- gte_game_mode: GTE-activity gameplay detector for 3D titles with no sprite-tag
  helper (gte.cpp notes every RTPS/RTPT). STICKY (>=3 verts, 45-frame hysteresis)
  so sparse cutscene frames never flip to a 4:3 pillarbox = fixes intro flicker.
- nw_hud_corners: push outer-third screen-space HUD sprites to the true wide
  corners in native-wide (else inset by the reveal).
- nw_backdrop: stretch a full-frame axis-aligned 2D backdrop quad to fill the
  wide frame (for titles with a flat sky plane).
- Pattern-scanned interp/sljit widen hooks now gated per-game (auto_screen_x /
  auto_backdrop): fixes an ungated auto_backdrop false-positive that rewrote a
  live GPR = the ~24s native-wide crash on Ape.

Native-wide GL perf (fixes 16:9 frame drops):
- The wide surface centre equals the canonical 4:3 framebuffer, so wide_blit_center
  copies canonical->wide-centre at present (both present paths) and the per-prim
  mirror SKIPS prims fully inside the 4:3 frame (mirror_x/geo/batch_center_only).
  Only reveal-margin prims re-rasterize. Peak frame time 38.8ms -> 17.5ms, no
  vsync misses. Toggle gl_wide_fast; 4:3 untouched (all gated on g_wide_cur).

Runtime-only except the recompiler cull emit (regen needed for cull widening).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 19:30:09 -07:00
Matthew Stanley d78e7a784f text_xlate: [[vram_patch]] pre-rendered-strip layer + GP0-ring builder attribution
Two interlocking pieces from the Tsumu HUD/menu-label investigation:

- GP0 ring builder attribution (gpu.h/gpu.c/debug_server.c): op-0x80
  entries now carry a bounded guest-stack unwind (bld[6]) + copy-time
  $sp (csp); gpu_frame_dump emits func/ra per entry and csp+bld[] for
  copies. This cracked the HUD compose: whole label strips are
  MoveImaged from the glyph sheet (src 834/852,352 -> HUD cache y=32).

- [[vram_patch]] (text_xlate.cpp/.h + gpu.c hook): third translation
  layer for text that exists only as pre-rendered pixels in an
  uploaded texture asset (Tsumu HUD labels, menus, prompts live in
  the glyph-sheet image; the 144-glyph font has no katakana at all).
  On every GP0 0xA0 upload completion the configured rects are
  verified halfword-for-halfword against expected JP pixels and only
  then rewritten with the target-language pixels via the renderer
  facade (software/GL/supersample coherent). Re-applies on every
  re-upload; verify-fail leaves foreign assets untouched. Debug:
  xlate stats gains vram_patches/vram_applied; new "vpatch"
  force-apply subcommand for config iteration without rebuild.

Verified on-screen: HUD "STAGE: 1-1 / BATTERY: 100", pause menu
(CONTINUE/RESTART/GIVE UP/CHANGE CONTROLS, TARGET BATT), title-menu
strips. docs/STRING_TRANSLATION.md Appendix Z documents the model,
mechanism, authoring recipe, and remaining strip backlog.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 01:41:42 -07:00
Matthew Stanley 46d43555e6 runtime: on-the-fly string translation mechanism (capture + apply)
Implements the framework from docs/STRING_TRANSLATION.md: an always-on
capture inventory + a draw-time apply hook, both hung off the psx_dispatch
chokepoint via fntrace_record (no generated-code edits, no BIOS regen,
works in Release).

- text_xlate.{h,cpp}: EncodingProfile abstraction (shift_jis profile:
  SJIS record reader with NUL/0xFFFF terminators + halfwidth-kana + control
  framing; UTF-8 -> fullwidth-SJIS transcoder so English renders with the
  font's existing fullwidth Latin, zero glyph injection). Capture inventory
  (FNV-1a64 keyed) queryable over TCP; apply repoints the string-arg reg at
  guest-stack scratch (KV-gated: only known records are swapped, stray
  pointers untouched; own scratch is skipped to avoid recapture).
- fntrace.c: call text_xlate_on_dispatch() at the dispatch chokepoint.
- config_loader: [localization].language (+ [runtime].language alias);
  env PSX_LANG overrides. Loads translations/*.toml per project.
- main.cpp: text_xlate_init() at game-config load.
- debug_server: "xlate" command (stats|dump|todo|reload) — observability
  over TCP, no log files.

Verified: builds (Release), Tsumu boots headless, capture live
(distinct records accumulating), apply disabled until a table is present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 11:42:07 -07:00
Matthew Stanley db4016b685 cdrom: model read-start latency + far-seek completion delay (MMX5 FMV fix)
Give ReadN/ReadS an initial read delay before the steady-state sector
cadence, classify SetLoc as near vs far and drive SeekL/SeekP completion
timing off it (the far-seek case PCSX calls out for Rockman X5), and fix
GetlocP to report the drive sub-Q position during an active read rather
than the last data-ready sector.

Previously the CD data-ready cadence was too eager, so the MMX5 intro
FMV / CD script never accumulated a valid decode window and the movie
player bailed with zero decodes. With faithful read/seek latency the
intro cutscene now decodes and plays.

debug_server: add a cdrom_command_history TCP command exposing the CD
command/seq ring (kind, cmd, params, read MSF, pending/queued state, pc)
to back this investigation.

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:51:32 -07:00
Matthew Stanley dcb63cb570 gpu/a0_history: add per-upload frame stamp (load-vs-upload ordering proof)
a0_history had no timestamp, so an upload's timing couldn't be compared against
the CD-load timeline. Add frame_stamp (s_frame_count at the upload) to the entry,
return it from gpu_get_a0_src, and emit "frame" in the a0_history JSON.

This pinned the MMX5 font bug: the (896/832/960,256) font uploads run at frames
1374/1381/1388 reading zero/0x77 garbage, while the CD delivers that buffer run
(LBA 10473 -> 0x800D5208..0x800DD208, real data) at frame ~1508 — the upload
races ~130 frames ahead of the font's CD load.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:51:32 -07:00
Matthew Stanley ba33564dc5 wtrace/parity: attribute DMA-sourced RAM writes to channel + kick PC
DMA-to-RAM writes were captured (all dma.c writes route through psx_write_word
-> the wtrace/parity hooks), but with misleading provenance: `pc`/`ra` came from
g_debug_last_store_pc, i.e. whatever CPU store ran last — meaningless mid-DMA,
and worse for async channels whose RAM writes run frames after the kick. There
was no way to tell a DMA write from a CPU store, or which channel produced it.

Fix (framework-level, all channels):
- dma.c: capture the DMA kick PC (the CHCR store) into g_dma_initiator_pc at
  trigger, stash per-channel, and re-publish it for the async channels whose RAM
  writes run in a deferred advance step (CDROM ch3; MDEC-out ch1, which also
  lacked the g_dma_exec_depth marking entirely and is now wrapped like CDROM).
- debug_server.c: WriteTraceEntry gains dma_ch (0-6, or -1 = CPU store); when a
  DMA is in flight, the entry's pc is the kick PC, not the stale store PC.
  Emitted as "dma_ch" in wtrace_dump.
- memory.c: parity note_write uses effective_store_pc() (kick PC during DMA).

Verified on MMX5: CD-streamed font buffer fills (0x800D5208) now tag dma_ch=3
with pc=0x80067264 (the CD loader kick); BIOS scratch CPU stores tag dma_ch=-1.
This is what let write-provenance see the font buffer's clear->CD-load->clear
cycle and localize the upload-races-the-load bug.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:51:31 -07:00
Matthew Stanley c7a7a67d0e gpu/a0_history: capture true LoadImage source pointer (s2), not just the RECT
a0_history recorded $s1 and labeled it "source data pointer", but for the MMX
LoadImage (FUN_80069dfc) $s1 is the scratch RECT descriptor (x/y/w/h, reused
every call) — the true pixel source is $a1, preserved in $s2 (`move s2,a1` at
entry). The mislabel sent font-load investigation to the wrong buffer.

Capture s2 (gpr[18]) = true source, plus a0/a1, expose via gpu_get_a0_src, and
emit s2/a0/a1 in the a0_history JSON. This is what let us find the real MMX5
font source buffers (0x800D5208 / 0x800DD208) and prove the upload reads them
during a post-clear/pre-CD-load zero window.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 23:51:31 -07:00
Matthew Stanley 4c7451d829 debug: suppress lockstep recording across ALL debug command handlers
The #2 lockstep comparator's record trace was polluted by debug-server
observer reads: handle_read_ram's psx_read_byte loop (and any other
handler touching guest RAM) executes at debug_server_poll safe points,
which can land inside an armed leader-to-leader record window. The
observer ops entered the trace as phantom guest ops and produced false
read-addr divergences whose 'compiled_actual' address was exactly the
TCP client's polled address (seen live on MMX5: probe reads of
0x8006FC24 and 0x800D1C00 flagged block 0x62FE8).

Fix at the process_command chokepoint: wrap every handler in
ls_suppress_begin/end so no individual handler can forget. Verified on
MMX5: with probes polling every 15s, 189M+ blocks check clean past the
point where the leak previously false-flagged at ~58M.

NOTE (negative result, do not re-attempt): also making the memory.c
g_ls_mode==2 replay fast-paths honor g_ls_suppress_record ("observer
reads must not consume trace ops") corrupts guest RAM — replayed guest
ops can run under a suppress held by in-guest instrumentation, and the
fall-through turns their side-effect-free replay writes into real
writes (deterministic FATAL ADEL at the MMX5 title press, IntRP walk
from addr 7). Replay integrity needs no suppress check: handlers only
run at safe points, never inside ls_at_leader's replay.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 23:51:31 -07:00
Matthew Stanley 9179c1ad5d debug: stop observability from perturbing SIO; add fmv_state command
debug_server_record_frame (every vblank) and the sio_state handler read
SIO_STAT/CTRL/RX via the GUEST-BUS sio_read(), which has hardware
side-effects: sio_tick(0) advances the delayed-IRQ state machine, reading
SIO_STAT decrements sio_ack_visible_reads and clears the ACK bit, reading
SIO_RX_DATA pops the RX FIFO. So the observer silently mutated the SIO/pad
handshake every frame in dev builds (the cardinal observability-alters-state
violation the event_ring header itself warns against).

Add side-effect-free sio_peek_stat/ctrl/rx_data() and use them on the debug
path; the guest bus keeps sio_read() with its real side effects. Also add an
fmv_state TCP command reporting the resolved config path + auto_skip_fmv +
frame-count-table params + live MDEC/XA/pad state, so the FMV-skip + config
state can be observed rather than inferred.

Note: this removes a real per-vblank SIO perturbation but is NOT the full fix
for the dev-build FMV-skip divergence (residual is the cycle-timing-core
class; see precise_irq_slice_state). It is correct observability hygiene.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:51:31 -07:00
Matthew Stanley e9b4ed5bbf gl: frame_perf CPU-side attribution - flush/wide wall time + plumbing counters
Tomba2 16:9 GL reproduced the reported collapse (12-14fps) and the GPU-side
split EXONERATED the GPU: scene_gpu ~10ms, mirror ~0.2ms, while emu_cpu is
~82ms (vs ~15ms in squash). gl_ws_ablate=1 (mirror passes fully off) leaves
it at ~75ms, so the wedge is CPU work outside the mirror draws. The GL
submission paths run INSIDE guest emulation (GP0/DMA processing), so driver
CPU cost lands in emu_cpu where no GPU timer sees it.

frame_perf now reports, per wide/4:3 frame class: CPU wall inside
flush_tex_batch (cpu_flush_ms_avg), CPU wall inside the glb_wide_* entry
points (cpu_wide_ms_avg), batches/frame, wide target sets/frame, and wide
FBO creations/frame (a nonzero steady value = surface recreation churn).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:23:06 -07:00
Matthew Stanley f5362f4ce9 gl: frame_perf mirror-pass GPU split + gl_ws_ablate ablation toggles
Native-wide perf attribution instrumentation (permanent):
- Each wide-mirror pass (tex-batch mirror, flat-geometry mirror, overlay
  full-width rect, wide_clear) is bracketed with a GL_TIMESTAMP pair
  (glQueryCounter; TIME_ELAPSED cannot nest inside the frame scene query).
  Pairs pool per buffered frame (1024/frame), summed at readback into
  mirror_gpu_ms + mirror_passes per sample; frame_perf now reports
  mirror_gpu_ms_avg/max, canon_gpu_ms_avg (scene minus mirror),
  mirror_passes_avg and mirror_pass_us for wide frames.
- New debug cmd gl_ws_ablate mode=<0..3>: 0 normal, 1 skip mirror passes
  entirely, 2 mirror state churn without draws, 3 mirror draws stay on the
  hr FBO (no per-pass FBO rebind; content-corrupting perf probe).

First MMX6 16:9 measurements with the split: mirror passes cost 20-125us
EACH (FBO ping-pong penalty per pass) but MMX6 2D batches to ~3-15
passes/frame so 16:9 attract holds ~60fps on this branch; the per-pass
cost x ~900 immediate flat prims is exactly the Tomba2 3D 60ms scene.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:59:53 -07:00
Matthew Stanley 2d2f4aebb4 gl: fix black-frame flicker (ISSUES #7) — exact pending-upload rects, not a union
Root cause (proven via the new present ring + gl_coh_ring correlation):
flush_cpu_upload() merged all pending CPU->VRAM writes into ONE union
bounding box. A frame with two disjoint uploads (sprite column + tile row —
routine MMX6 texture streaming) produced a union spanning the display
framebuffers, which the flush painted from the STALE CPU VRAM mirror (the
FBO is authoritative under GL) — stomping live frames with black for
exactly two presents (one per double-buffer parity). Scene-dependence of
the uploads made it look config/timing dependent; the software renderer is
immune because its CPU array is authoritative.

Fix: exact pending-rect list (16 rects; merges only when zero uncovered
pixels are added — containment or same-band extension; wrap-aware GP0(A0)
transfers split into up to 4 exact rects instead of the old "wrapped: take
all" full-VRAM union; overflow -> order-preserving flush-all; pre-context
union retained where the CPU array is authoritative). Merge rule verified
by a 20k-randomized-rect host test (0 stale / 0 missing painted pixels).

Observability (always-on): gl_present_ring records every SwapWindow (path,
src/letterbox rects, drained glGetError, wall-ms, backbuffer + blit-source
pixel samples) — the instrument that gave the 1:1 black-capture
correlation. debug_server_send_fmt now heap-formats responses >64KB (they
previously truncated into unparseable JSON, breaking large ring dumps).
(debug_server.c also carries the Vulkan screenshot_file sync hunk from the
concurrent VK bring-up; see the next commit.)

Validated: ~18-min MMX6 GL attract soak, ~1600 window captures across 3+
attract cycles — zero isolated black frames, zero GL errors (pre-fix:
19/40 captures black at the intro). Tomba1 build-gl boots clean; its full
attract soak follows. ENHANCEMENTS.md added (framework enhancement ledger,
R1 OpenGL / R2 Vulkan state).

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:04:17 -07:00
Matthew Stanley 2dc938816b debug-server: parse bare json numbers as decimal, not saturated hex
json_get_str fed every consumer the raw value text, and address fields
went through hex_to_u32: a client passing a bare int addr (python
json.dumps of 0x8013DDC4 emits '2148784580') got it parsed AS HEX;
strtoul saturates 10-digit decimals to 0xFFFFFFFF, so read_ram/write_ram
silently operated on junk. This fabricated a false 'stream overwrites
overlay code' lead during the Tomba2 attract hunt (2026-07-02).

json_get_str now normalizes bare pure-decimal values to 0x-hex text
(JSON bare numbers are decimal by definition; quoted values keep the
hex wire convention), and json_get_int scans the raw json itself so
integer fields keep decimal semantics.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:42:26 -07:00
Matthew Stanley 4cc757f0c9 bios: seed all 30 ROM-resident kernel-table targets; unknown dispatch is FATAL by default
Root cause of MMX6 "A game data could not be found" (weeks-old, cross-backend):
the game's save-scan calls memset through the kernel A0 jump table; entry
A0:2B points into BIOS ROM at 0xBFC02B8C (one of the entries the kernel never
patches into RAM). Transitive discovery can never find it (reachable only via
the data table, no jal), so the dispatch missed and psx_unknown_dispatch
silently no-op'd — killing the scan before firstfile ever ran. Every card
layer (SIO, IRQs, SwCARD events, sector data, kernel directory parse) was
verified correct along the way; the sole defect was the undiscovered ROM
function plus the silent miss.

- recompiler/seeds: enumerate the post-init A0/B0/C0 tables (kernel RAM
  0x200/0x874/0x674) and seed every ROM-pointing entry with no dispatch
  coverage — 30 functions (memset, memcmp, memmove, strncat, rand-family,
  card/dev services...). memset was merely the one MMX6 called; the rest were
  identical latent bugs. BIOS regen: 4406 -> 4439 dispatch entries.
  MMX6 CONTINUE now lists and loads saves (user-validated + scripted run:
  full FILE SELECT, 1093 card txns, unknown_dispatch_log total=0).

- traps.c: an unresolvable dispatch target now dumps a crash report and halts
  BY DEFAULT (PSX_FAIL_FAST_UNKNOWN_DISPATCH=0 opts out for diagnostic
  sessions; the miss is ring-recorded either way). The old silent-no-op
  default left stale registers and a severed call chain, and masked this bug
  for weeks. Deliberately NO interp fallback: the dirty-RAM interpreter is
  for runtime-written code (Rule 18); interpreting around a static-discovery
  hole would hide it exactly like the no-op did.

- runtime.cmake: EXE_NAME option — per-game process names (mmx6-runtime.exe)
  so one game's `taskkill /IM psx-runtime.exe` dev sweep no longer kills
  every other game's instance.

- debug rings (the tooling that found this): HLE call ring gains
  consecutive-duplicate collapse w/ repeat counts (a 4-way TestEvent poll at
  175K calls/s no longer flushes the ring in 0.1s), per-entry TCB + in_exc
  (thread attribution), a "since" paging param and 8MB dump buffer;
  card-trace EvCB watch widened to all 7 card event slots (slot 0 =
  SwCARD-IOE, the success event, was previously invisible).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:39:25 -07:00
Matthew Stanley 496d1dbbc8 runtime: HLE tier wiring + carried instrumentation from the card-load hunt
HLE tier wiring (completes the previous commit — HEAD builds again):
- main.cpp: [runtime] bios_hle/keep_intro consume + settings.toml layer +
  PSX_BIOS_HLE / PSX_BIOS_HLE_KEEP_INTRO env resolution + backend banner;
  boot-turbo window ORed into the turbo predicate; the fast_boot snapshot
  restore/capture path and s_fast_boot_active are removed (deprecated by
  the HLE boot shell-skip); psx_hle_scheduler_set_default call.
- traps.c: psx_hle_scheduler_enabled default now flows from config
  ([runtime] hle_scheduler) with the PSX_HLE_SCHEDULER env winning.
- debug_server.c: hle_dump command (always-on HLE ring query).

Carried instrumentation/state from the MMX6 card-load sessions (kept
deliberately; documented in memory + handoff):
- dirty_ram_interp.c: XOP_RES interp JAL call-resolution probe + xprobe
  watched dump section (emitted first), watch targets.
- overlay_loader.c: probed wrapper around dispatch_call + psx_sljit_call.
- debug_server.c: card_mgr_trace range widen, TestEvent spin-body
  collapse, 64MB response cap.
- interrupts.c: PSX_SAME_THREAD_RESTORE selector (0/1/2; TCB-equality
  hardening candidate).
- memory.c: PSX_POLL_PROOF temp gate (remove when card work resumes).
- savestate.c/.h (new): user save states (F1-F12), HLE-scheduler-gated
  load via psx_scheduler_resume_at; load-crash known, do not use for
  repros yet.
- psx_cycles/autocompile/cycle_testrom: timing-fastpath + exe-path
  session state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:27:57 -07:00
Matthew Stanley 13c5e0c5b5 stale-recompiler shard guard + exception-exit ring; Tomba menu wedge resolved
Root cause of the Tomba pause-menu wedge (loaded saves only): autocompiled
overlay shards were emitted by a recompiler binary OLDER than the last emitter
change, yet stamped with the CURRENT cg tag (compile_overlays derives the tag
from --runtime-include; nothing tied it to the emitting binary). The stale
shards corrupted the display-list task queue during load-game. NOT the
IRQ-resume class the prior handoff claimed - disproven with the new ring.

- recompiler: bake the emitter-source hash (shared canonical list
  runtime/codegen_hash_sources.cmake + hash_codegen.cmake) into psxrecomp-game;
  new --codegen-hash flag prints it (machine-parseable, pre-banner).
- compile_overlays.py: HARD-FAIL when the recompiler binary's baked hash
  differs from the cg tag hash it stamps (or the binary lacks the flag).
  Applies to both cache and --static modes. Closes the read==write-tag /
  stale-content drift class structurally.
- runtime.cmake: codegen-hash source list moved to the shared canonical file.
- interrupts.c: always-on exception-EXIT half added to irqctx_ring (take_pc,
  real_epc, exit_pc, escape reason, same-thread/restore flags, v1/ra
  saved-vs-exit, RestoreState redirect count) - proves per delivery which
  escape path fired and whether the interrupted GPRs were restored. Kept the
  same_thread_resume GPR-restore refinement (ring-verified equal-value in
  practice; faithful: hardware restores all GPRs on every exception return).
- debug_server.c: irqctx_ring dump extended with the exit-half fields.

Validated live: Tomba menu opens on loaded save (Stormy Mountain) with the
cache fully enabled after regenerating shards with a rebuilt recompiler
(build-cosim + build-prod caches). Guard verified positive and negative.
MMX6 gate passed: boot -> title -> opening cutscene -> gameplay handoff OK.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 19:50:26 -07:00
Matthew Stanley 553d9933d1 fix mmx6 timing divergences with cosim oracle 2026-07-01 09:28:50 -07:00
Matthew Stanley 0f34c29c45 diagnostics: add compiled block lockstep comparator 2026-06-30 16:59:58 -07:00
Matthew Stanley 18c94a1394 device_trace: general two-process device-event (IRQ) cycle ring
New always-on ring (device_trace.{c,h}, compiles into both binaries like
parity_trace) recording every hardware IRQ-raise edge with its guest cycle,
source (I_STAT bit) and detail. All native device sources funnel their raise
through one choke point psx_irq_raise(bit,detail) in interrupts.c (VBLANK/GPU/
CDROM/DMA/timers/SIO routed; DMA passes DICR channel flags, timers pass index),
so the device-timing audit sees every event from one place. devtrace_dump/ctl
debug commands (cycle-window + source filters) + tools/devtrace_diff.py align
the two device-event timelines by source ordinal and flag where cycles drift.
The Beetle oracle side (mednafen IRQ_Assert rising-edge hook) lives in the main
checkout.

This is the level below parity_trace: parity names the divergent control-flow /
gate input; device_trace names the divergent HARDWARE EVENT behind it.

First result (MMX6 cutscene asset-load over-fast wedge): the load's CD+DMA
activity is compressed into ~232M cycles on native vs ~384M on Beetle (~1.65x),
completing the func_8001CB3C scene setup ~100M cycles / ~176 frames early. The
producer is CD-read / DMA-completion timing (DMA channels 2/3/4 = GPU/CD/SPU
asset streaming) -- confirming the device-completion-latency hypothesis and
ruling in the CDROM/DMA cycle model as the over-fast subsystem. (VBlank verified
correct: native VBlank edges are exactly 564480 cyc apart.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 21:53:27 -07:00