Commit Graph

559 Commits

Author SHA1 Message Date
Matthew Stanley c8740ba119 tools: label CD known-divergence in devtrace_diff (+ACCURACY_BURNDOWN); keep Beetle oracle unpatched
PR #9 makes our CD controller more faithful than Beetle (SCPH-1001 version
94/09/19/C0 vs Beetle's hardcoded 97, ADPBUSY idle=0, disc-derived GetID region).
devtrace_diff.py now LABELS a cdrom-source native/beetle delta as KNOWN intentional
(more faithful) instead of red-flagging it; --strict shows raw. Beetle stays the
independent oracle. Documented in ACCURACY_BURNDOWN.md Axis 5.
2026-07-06 11:21:20 -07:00
Matthew Stanley 13d84d2f7b Merge branch 'pr9-kula' into merge-pr9
# Conflicts:
#	runtime/src/beetle_libretro.cpp
#	runtime/src/dirty_ram_interp.c
#	runtime/src/memory.c
2026-07-06 10:59:27 -07:00
Matt Stanley 58097452d8 Merge pull request #12 from chrisking1981/codex/text-divergence-guard
Guard static text dispatch with live RAM divergence
2026-07-06 10:58:14 -07:00
Matt Stanley e5006fec7a Merge pull request #11 from chrisking1981/codex/nested-boot-serial
Recognize boot executables in nested disc paths
2026-07-06 10:58:11 -07:00
Matthew Stanley 0b958796cb Merge wt/ape-escape: UNR GTE perspective divide + textured-poly SetTPage
Lands the Ape Escape bring-up's two faithful-core runtime fixes:
- gte: hardware Unsigned Newton-Raphson reciprocal for the perspective
  divide (Beetle-bit-exact over 400k inputs).
- gpu: textured polygons copy their texpage attribute word into GPU
  draw-mode state (Beetle SetTPage semantics) — fixes the black
  additive-glow class (Ape headlamps/sirens/fireworks/lava; D12 in
  accuracy/axis5_gpu.md).

Runtime-only (no codegen change from these two); titles pick them up on
exe rebuild.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:27:21 -07:00
Matthew Stanley 85987f2331 recompiler: control-flow-aware function extents fix data-as-code misclassification
The legacy extent estimate bounded every function at the next discovered
start and ran the undefined-opcode density heuristic over that whole span.
Psy-Q/Capcom titles (Mega Man X4) place inline data / jump tables directly
after real functions; the trailing data inflated the measured extent and the
density check then misclassified the real function as a data section —
emitting psx_unknown_dispatch stubs for directly-called code (X4: the whole
0x800EE3A4.. text tail, ~7,373 entries, became stubs and boot fail-fasted).

Walk each function's control flow from entry to its real terminus (mirroring
the proven analyze_exact_entries overlay walk, incl. Psy-Q jr-table
resolution). Provably-clean bodies (return/tail exit reached, zero invalid
primary opcodes, all indirect jumps resolved in-function) are bounded at
their terminus and marked code; everything else falls back to the legacy
boundary + density heuristic, which still rejects genuine data tables minted
as starts.

CODEGEN-AFFECTING: function extents change emitted C. Land games via the
coordinated regen cascade (regenerate every title against this emitter before
shipping; stale main images / overlay caches must be rebuilt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:27:09 -07:00
Matthew Stanley 53ae75debb beetle: fntrace ring captures a2/a3 call arguments
The Beetle-side fntrace ring recorded only a0/a1; four-argument kernel and
game calls could not be attributed from the oracle side. Capture a2/a3 via
PS_CPU::GetRegister at the trace hook and emit them in fntrace_dump, matching
the native runtime's fntrace field set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:26:47 -07:00
Matthew Stanley 902a07bbce launcher: settings pages + live controls rebind via shared psx_keybinds module
RmlUi launcher grows a Settings surface (renderer / supersampling / AA /
texture filter / screen model / skip-FMV / turbo / fullscreen / widescreen /
SPU-HQ / deadzone / BIOS / disc) and a live Controls rebind page. Bindings
persist to keybinds.ini through the new shared runtime module
runtime/include/psx_keybinds.h + runtime/src/psx_keybinds.c; main.cpp routes
SDL input through the rebindable table. Ape Escape is the first consumer;
the feature is game-agnostic framework code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 13:26:11 -07:00
Matthew Stanley 85876ec7cb gpu: textured polygons update draw-mode state from their texpage word
Real GPU copies the texpage attribute word carried inside every textured
polygon (GP0 0x24-0x3F, texture bit set) into the draw-mode state
(GPUSTAT bits 0-8), exactly like GP0(E1) bits 0-8 — Beetle runs
SetTPage(CB[4 + ((cc>>4)&1)] >> 16) on every such command
(beetle-psx mednafen/psx/gpu.cpp:1070). So the polygon's OWN word (not
the last E1) selects its semi-transparency mode, and later rect/sprite
prims (which carry no texpage word) consume the state the polygon left
behind. We fetched texels from the prim's page but blended with the
stale E1 global, and never updated the page state rects depend on.

Effect: additive glows blended with whatever mode the last E1 happened
to set, flipping bright light halos into dark/black blobs intermittently
(OT bucket order decides which prim precedes, hence camera/distance
dependence). Proven on Ape Escape attract frame 60052: nine op-0x2E
firework quads at the artifact coordinates carry prim mode 1 (B+F) while
the last E1 set mode 0 (B/2+F/2) — rendered as dark maroon blobs;
Beetle renders the same beat as bright fireworks. Post-fix sweep shows
siren-hat glow halos, MONKEY PARK marquee bloom, and lava glow restored
across two full attract cycles.

Poly words do not touch dither/draw-to-display (E1-only bits 9-10) nor
texture-disable (bit 11 latches only under GP1(09h) allow-change).
All backends inherit the fix via gr_set_semi_transparency /
current_texpage(). Faithful-core fix for all titles; documented as D12
in accuracy/axis5_gpu.md (the audit had marked semi-transparency
FAITHFUL — the modes were, their state source was not).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 12:44:29 -07:00
Matthew Stanley 9b7e806656 gte: hardware Unsigned Newton-Raphson reciprocal for perspective divide
gte_divide computed the perspective divide as exact
(H*0x20000/SZ3 + 1)/2; real PS1 GTE (and Beetle, mednafen psx/gte.cpp)
use the Unsigned Newton-Raphson reciprocal table. They diverge on
24.7% of inputs by +/-1 (max +/-3) in h_div_sz, which feeds screen
coords and the IR0/MAC0 depth-cue factor — a camera/distance-dependent
divergence. This implements the hardware UNR algorithm (incl. the
1<<17 overflow-flag semantics); matches Beetle bit-exactly over 400k
inputs. Faithful-core accuracy improvement for all titles.

Staged as a candidate for Ape Escape's camera-distance-dependent
black additive-glow artifact but NOT causally confirmed (the artifact
did not reproduce in the scene reachable for oracle A/B). Glow
investigation parked; this fidelity fix stands on its own.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 11:39:32 -07:00
Matthew Stanley 80f4ed66a5 Merge remote-tracking branch 'origin/master' 2026-07-05 00:34:12 -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 c1211551f4 runtime: game-specific exe name (OUTPUT_NAME from window title)
Every title instantiates psxrecomp_add_runtime_target with the same CMake
target name ("psx-runtime"), so all games produced an identical
psx-runtime.exe. Launching or killing one title's process by name then hit
another title's running instance (e.g. an X5 dev run stopping a concurrent
Tomba 2 run in a sibling worktree).

Derive a filename-safe OUTPUT_NAME from the per-game window title via
MAKE_C_IDENTIFIER (MegaManX5Recomp.exe, Tomba2Recomp.exe, ...). The CMake
target name stays "psx-runtime" so $<TARGET_FILE...> refs and the POST_BUILD
asset copies are unaffected. Oracle builds get an _oracle suffix.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-04 23:51:30 -07:00
Matt Stanley 48a297a254 Add R.A.I.D. Discord button to README 2026-07-04 13:13:00 -07:00
Matt Stanley a48fd04991 Add R.A.I.D. Discord banner asset 2026-07-04 13:12:58 -07:00
Matthew Stanley d3d766c5c3 parity_trace: implement read-ring readback (fixes psx-beetle build break)
The committed beetle debug server (h_parity_dump reads=1) and beetle_libretro
read hook already referenced parity_trace_reads_get / parity_trace_reads_total /
parity_trace_note_read, but only a dead per-watch scalar table existed — the two
readback functions were never implemented, so every psx-beetle rebuild failed to
link (blocking the oracle entirely).

Implement the dedicated READ ring as the symmetric counterpart of the write ring:
note_read pushes one ParityEntry per read of a watched word, carrying reader
provenance (pc = load PC, epc = read address, target/watch[slot] = loaded value)
PLUS that slot's current last-WRITER metadata (wpc/wcycle/wframe/wtcb). One read
row therefore answers "who read what value, and who last wrote it." reads_get /
reads_total mirror parity_trace_get / parity_trace_total (newest rows, oldest
first). Reset clears the ring. Removes the unused scalar last-reader table.

Validated: psx-beetle links + boots; PSX_PARITY_TRACE=1 accrues well-formed read
rows (reads_total>0, reader+writer provenance populated).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 12:55:10 -07:00
ai-tdd-labs 685f38dacb Guard static text dispatch with live RAM divergence
Packed games can rewrite code over their own loaded EXE text after static recompilation. A dirty-page bit alone is too coarse for dispatch decisions: it prevents stale native execution, but it also cannot distinguish data writes in a text page from instruction bytes that actually diverged from the compiled image.\n\nRegister the boot EXE image as a reference and let memory writes mark text pages whose bytes differ from that image. Static game dispatch now asks whether the target remains native-safe; diverged pages fall through to execute live RAM through the dirty interpreter, while matching code bytes can still use the compiled path.\n\nConstraint: Runtime-installed or rewritten RAM code must execute the program's own instructions, not HLE or stale static code\nRejected: Keep the existing dirty-page-only gate | too coarse for packed/self-modifying main text and data writes sharing code pages\nConfidence: medium\nScope-risk: moderate\nDirective: Do not bypass dirty_ram_text_native_ok() at game compiled dispatch sites without proving RAM still matches the compiled image\nTested: Local Tekken 3 branch using this mechanism reaches playable fights; clean upstream worktree compiles changed main/memory/dirty files before hitting an unrelated existing macOS overlay_loader LONG/InterlockedExchange build error\nNot-tested: Full upstream macOS build completion; non-macOS build matrix
2026-07-04 16:41:29 +02:00
ai-tdd-labs 32911f4fd3 Recognize boot executables in nested disc paths
Some PS1 games put the boot executable under a directory in SYSTEM.CNF, for example cdrom:\TEKKEN3\SLUS_004.02;1. Serial detection was normalizing the whole path token, so it could miss the executable serial and leave disc identity/region unresolved.\n\nNormalize only the basename after any slash or backslash before deriving the serial.\n\nConstraint: SYSTEM.CNF boot paths may include directory components\nRejected: Add game-specific serial overrides | path normalization is the generic ISO9660 behavior\nConfidence: high\nScope-risk: narrow\nDirective: Keep disc identity parsing game-neutral; do not add per-title serial tables for path-shape issues\nTested: Applied in local Tekken 3 run; runtime detected NTSC-U serial SLUS-00402 from TEKKEN3\SLUS_004.02\nNot-tested: Clean upstream worktree build blocked because pkg-config cannot locate sdl2 in this shell
2026-07-04 16:33:20 +02:00
Matthew Stanley 010a281b0c ENHANCEMENTS: R3 validation sweep results + merge record
3-title x 3-config user playthrough. OpenGL 4:3 = strong shippable path on all
titles; Vulkan 4:3 = YELLOW everywhere (ships hidden/experimental); OpenGL 16:9
= Tomba1 PASS, MMX6 YELLOW (Rainy Turtloid rain-area perf), Tomba2 FAIL (no ws
hooks on master). Both policy gates already in-tree (Vulkan hidden by default;
widescreen EXPERIMENTAL in launcher). Runtime-only; codegen hash unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:36:56 -07:00
Matthew Stanley d1746baab0 ENHANCEMENTS: MMX6 wedge incident resolved to poisoned self-healed shards
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 21:05:49 -07:00
Matthew Stanley a34fac4590 ENHANCEMENTS: R1b native-wide GL perf + band story; R2 item 5 (VK wide) done
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 21:00:35 -07:00
Matthew Stanley 0b23ea3318 vulkan: native-wide (16:9) compositor - R2 item 5
Mirrors the GL design with the Part-A lessons baked in:
- Per-base_x wide surfaces (RGBA8 color + OWN stencil image + framebuffer on
  the SHARED render pass, so every existing pipeline works unchanged and the
  mask-bit stencil mirror is real on the wide surface from day one - GL
  originally shipped a stencil-less wide FBO).
- The mirror is a second render pass appended to the SAME one-shot command
  buffer as the canonical batch (flush_geometry + flush_tex_batch): no extra
  submits, u_xoff/u_xhalf push constants carry the x-translation/wider clip
  (the VK shaders were pre-plumbed for this).
- Mirror scissor = full-width X but DRAW-AREA Y (the GL band-bleed lesson:
  full-height Y let draws bleed across vertical double-buffer bands and
  flicker the top/bottom edges).
- wide_clear via vkCmdClearAttachments (color + stencil to bit15) over the
  cleared band; full-screen overlay rects suppress the batch mirror and draw
  one full-wide-width rect instead (margins dim/fade correctly).
- GPU-direct present: vk_renderer_present_wide blits the displayed band from
  the wide image to the swapchain, letterboxed at (4*wide_w : 3*native_w);
  returns 0 -> caller falls back (unchanged) when no surface exists.
- vkb_render_wide_display CPU readback backs gr_wide_supported() + the ws
  debug dump; wide entry points no-op when nothing changed (they sit inside
  guest emulation - Part-A lesson).
- vk_perf gains wide/wclr per-frame counters.

First validation: Tomba2 build-vk 16:9 (PSX_WS_FORCE_2D=1) locks 60fps
(frame_period p50 16.68ms) with wide passes active; wide surface allocs are
one-time (vk_perf allocs ~0 steady state).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:56:06 -07:00
Matthew Stanley 8b819eb7e0 interp: word-tag the ws site caches - stale verdicts corrupted the guest
MMX6 16:9 attract wedged (misaligned wild dispatch 0x21010001 -> fatal halt,
stack-sampled at psx_unknown_dispatch under dirty_ram_dispatch). Mechanism:
the enlarged site caches persist across overlay reloads, and the generation
counter only sees page-marking hooks (kernel writes + DMA executable-range
marks) - code replaced by plain CPU stores (loader memcpy) keeps the page
dirty and never bumps the generation, so a cached backdrop verdict derived
from the PREVIOUS overlay bytes could fire a GPR rewrite at the wrong
instruction. The old 256-slot cache had the same hole but evicted within
seconds; the 8192-slot cache widened the exposure to a real wedge.

Each entry now also stores the site instruction word and revalidates it on
lookup (one fetch_word) - any code change at the site forces re-derivation.
Cost on the hot path: one load + compare per candidate instruction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:51:17 -07:00
Matthew Stanley dbe7812a6a interp: fix the REAL native-wide CPU collapse - per-instruction ws site scans
Stack-sampled (devkitPro gdb, Tomba2 16:9 attract in the 12fps wedge): the
main thread sat in ws_backdrop_site_kind <- exec_one. Under native-wide,
exec_one classified EVERY interpreted instruction as a potential backdrop
rewrite site; the classifier scans +/-512 bytes of RAM per miss, and its
256-slot direct-mapped cache (any two hot PCs 1KB apart collide) thrashed
on overlay-sized working sets, so hot interpreted loops re-scanned per
executed instruction. Squash mode gates the whole path off, which is why
ws_nw on=0 instantly restored 60fps while every GPU-side theory (mirror
passes, FBO ping-pong, extra prims, present path) failed - the earlier
GL GPU-time numbers were CPU-starvation-inflated timestamp gaps.

Fixes:
- exec_one pre-filters by opcode class first (addu/or/addi/addiu are the
  only rewrite-site forms) - detection no longer runs for ~85% of instrs.
- Both site caches (cull + backdrop) grow to 8192 full-PC-tagged slots.
- NEW g_dirty_ram_code_gen (memory.c): bumped on clean->dirty page
  transitions and every executable-range mark (overlay/EXE loads); cached
  classifications carry the generation, so an overlay reload re-derives
  them. This also fixes the latent staleness bug where both caches could
  serve a kind derived from a PREVIOUS overlay bytes at the same PC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:34:41 -07:00
Matthew Stanley cfa79bbc57 gl: native-wide perf collapse root cause - wide FBO lacked a stencil attachment
Proven by ablation on Tomba2 16:9 attract (12fps): a full mirror pass cost
~616us GPU per pass, but ablate=2 (FBO binds + state churn, no draws) = 3us
and ablate=3 (identical draws into the stencil-equipped hr FBO) = 1.5us.
The only difference between the fast and catastrophic configurations is
drawing with stencil test/write enabled (every batch's mask fixup pass)
into the one FBO with no stencil attachment - the driver handles that
combination with ~0.6ms of per-draw work. This also explains why the
deferred once-per-frame replay experiment (ws branch a8181dd) did not help:
it still drew into the stencil-less wide FBO, so the per-draw cost stayed.

Fix: each wide surface gets a DEPTH24_STENCIL8 renderbuffer like the hr FBO.
This is also a correctness fix: the wide surface now carries a real PSX
mask-bit stencil mirror (previously mask set/check silently no-opped on the
wide pass). wide_clear clears the stencil to bit15 alongside the color, and
surface creation clears both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:26:53 -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 a0b58433c2 gl: fix native-wide band bleed - mirror scissor Y clamps to the draw area
The wide-mirror pass scissored the FULL wide surface (both X and Y). The SW
reference (rt_wide) only widens X; its Y clip stays the canonical draw-area
clip. Under a vertical double-buffer (MMX6: draw area alternates y=0/y=240,
both bands in ONE full-height wide surface keyed by base_x) the full-height
Y scissor let draws that canonically clip at the band boundary bleed into
the OTHER band's rows, presented a frame later as user-visible top/bottom
edge flicker (16:9 GL only). Scissor is now full-width X, draw-area Y,
matching rt_wide exactly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:04:19 -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 d0cb91313f vulkan: FMV verified + CPU-present resource cache + explicit DS stencil barrier
- FMV on-screen verification (gap 2): Whoopee logo movie + intro CG movie
  render correctly on VK (window captures vs the earlier software runs).
  Finding: Tomba2 streams movies as 15-bit MDEC->VRAM uploads, not depth24 —
  the depth24 compose path is no-regression code pending a 24-bit title.
- cpres cache (gap 3): the depth24 CPU-present path now keeps a persistent
  VkImage + persistently-mapped staging keyed by (w,h) instead of
  creating/destroying both every movie frame; freed on resize + shutdown.
  The existing per-frame queue-idle wait is what makes reuse race-free.
- DS barrier (gap 4): begin_geo_pass now issues an explicit stencil-aspect
  self-barrier (LATE_FRAGMENT_TESTS write -> EARLY|LATE tests read/write) so
  cross-submit mask-bit ordering no longer rides on color-barrier side
  effects (spec-fragile). Layouts verified against the init chain and the
  render pass (DEPTH_STENCIL_ATTACHMENT_OPTIMAL throughout).

Noted for later (ENHANCEMENTS R2 4b): flush_cpu_upload's 2 staging allocs
per flush (~16/frame during MDEC FMV only, 0 in gameplay).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 19:20:35 -07:00
Matthew Stanley 5d8a0f19df vulkan: exact-rect pending uploads with a coalesced two-submit flush
Ports the GL stale-stomp fix (R1: a union of disjoint uploads painted stale
CPU-mirror pixels over live framebuffer content) to the Vulkan backend, with
a VK-specific flush design: the naive per-rect port re-created the boot-wedge
submit churn (2 staging allocs + 3 submits PER RECT; measured 0.7 fps at the
Whoopee logo under MDEC streaming). The coalesced flush packs all rect data
into ONE 1555 staging + ONE RGBA8 staging (VkBufferImageCopy bufferOffset per
rect), then exactly TWO queue submits per flush: one command buffer with both
N-region buffer->image copy sets, one render pass with N scissored 2-draw
blits into hr. UP_RECTS_MAX=64 on VK (row-band merging coalesces MDEC
macroblock streams to ~one rect per block row, so a streamed frame fits in a
single flush).

Verified: Tomba2 attract renders correctly on Vulkan at ~51 fps; vk_perf
shows mostly zero-submit frames with occasional small bursts (was a
sustained per-pixel churn wedge before the original batching fix).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:32:18 -07:00
Matthew Stanley 1ea29675e6 gl: R1 validation complete — Tomba1 gapless present-ring verdict (23,807 presents, 0 dark)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 17:19:20 -07:00
Matthew Stanley f5ea3b0d76 vulkan: bring-up to full-speed gameplay — batched uploads, VB cursors, STP order, FMV path
Three root-caused fixes (verified with guest-frame-aligned VRAM diffs vs the
software reference + window captures; Tomba2 title now pixel-identical to
software, attract within 0.62% of the GL oracle, 60.5 fps sustained):

1. Boot wedge: every GP0 pixel upload did 2 vkAllocateMemory + 2
   vkQueueSubmit (driver churn, minutes-long stall, watchdog abort).
   Deferred batched uploads (GL model): CPU writes land in the CPU array +
   dirty rect; one batched upload flushes at each GPU-op entry point.
2. Shredded 3D: draws raced CPU rewrites of the persistently-mapped vertex
   buffer from offset 0 (69.5% pixel divergence at guest frame 3000).
   Sub-allocation cursors + firstVertex bases, recycled at queue-idle.
3. Semi-transparency order violations (59.5% divergence): VK still had GL's
   retired whole-batch STP split; ported GL's current model (ordered color
   pass + color-masked stencil fixup via a new pipeline dimension; semi
   prims isolated one-per-batch).

main.cpp: Vulkan present block composes 24-bit FMV frames via
gpu_display_pixel_argb + vk_renderer_present_cpu (was NULL -> guaranteed
black video; on-screen verification still owed) + the no-VK-build
downgrade-to-GL guard salvaged from the retired _wt-vulkan worktree.
debug_server screenshot_file now syncs the VK backend (previous commit).

Known follow-ups (ENHANCEMENTS.md R2): port the exact-rect upload fix from
GL (VK carries the union model = same flicker class), verify FMV
on-screen, cache the FMV staging image, explicit DS barrier between
one-shot submits, wide compositor, SSAA validation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 16:27:06 -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 db159a1ea6 overlay loader: startup ABI batch-purge + cache-index cap 256->4096
Two defects found by the MMX6/Tomba2 pre-merge regression gate:

1. An ABI bump (v9->v10) invalidated every cached DLL, and the lazy path
   migrated them PER-DISPATCH (LoadLibrary -> reject -> DeleteFile -> retry)
   on the emulation thread that owns the window pump — a large cache
   presented as a "(Not Responding)" black window for minutes. Now a
   one-time preflight sweep at cache-scan enumerates the DIRECTORY, checks
   overlay_abi per DLL, purges mismatches (DLL + .ranges + index entry) in
   one pass, and writes a .abi_<tag>.ok marker so healthy caches skip the
   sweep on later boots. The per-load ABI gate stays as defense in depth.

2. CACHE_IDX_CAP was 256 and overflow was SILENT — Tomba2's 399-DLL cache
   truncated, leaving ~140 real DLLs invisible (their regions interpreted
   forever, no diagnostic). Cap raised to 4096 with a loud never-again
   loader_log on overflow.

Negative-tested: planted a v6 DLL under a valid name — swept on boot along
with 25 latent v9 orphans the lazy path had never touched; marker written;
game boots to title. Sweep enumerates the directory rather than the index
precisely because the index is capacity-bounded (bug 2 hid the plant on the
first test iteration).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 15:01:57 -07:00
Matthew Stanley 98a91418d5 coverage_vault: mirror DLLs recursively, preserving cg-generation layout
merge_cache used a flat listdir() against a nested cache
(<compiler>/<arch-abi>/cg<N>_<hash>/) so it matched nothing — the vault's
DLL mirror has been empty since the tool shipped. Walk the tree and keep
relative paths: the same content-keyed filename exists under different cg
dirs with different compiled bytes, so flattening would mix emitter
generations. stats() walks recursively to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:15:39 -07:00
Matthew Stanley bbe0d2ae8e dirty-interp: bound PC-table probe length; 4x table — Trolley-demo 0.3fps host burn
Tomba2's Trolley attract demo interprets a working set past 64K distinct
PCs. The per-PC tracking tables (g_dirty_ram_pc_table / exec_pc_table) used
an UNBOUNDED linear probe, so once a table filled, every lookup for an
absent PC scanned all 65536 slots before returning NULL — per executed
instruction. Host thread profile showed ~99% of emu-thread time inside
pc_table_get_or_insert_in; guest crawled at 0.3 fps (1M guest cycles per
3s wall) while telemetry showed the window "LIVE" on residual motion.

Fix: hard-bound the probe to 128 slots (miss => NULL => "stop tracking",
the documented degradation) and grow the tables 65536 -> 262144 so the
Trolley working set actually fits — the same table feeds overlay-capture
seed lists (MAX_CAPTURE_PCS aliases it), so the old size also silently
truncated capture manifests for large regions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 10:09:36 -07:00
Matthew Stanley 1444394242 overlay ABI v10: forward GTE special-register accessors — unblock new overlay coverage
The emitter routes mfc2/cfc2/mtc2/ctc2 on flag/IR/derived GTE registers
through direct gte_read_data/gte_read_ctrl/gte_write_data/gte_write_ctrl
calls (code_generator.cpp COP2 case), but the overlay DLL preamble had no
shims for them — any GTE-heavy overlay failed to LINK (undefined
gte_read_ctrl) and its region ran interpreted forever. That silently
blocked all new native coverage for the lava/Trolley attract-demo regions
after the caches rotated.

GTE state is shared runtime state, so the shims forward unconditionally
through OverlayCallbacks (a local copy would fork the flag/IR timeline).
ABI bumped 9 -> 10; the loader gate rejects+deletes pre-shim DLLs and
autocompile self-heals; plain gte_data[]/gte_ctrl[] accesses stay direct
CPUState reads in emitted code, matching the emitter.

Smoke-validated: previously-failing GTE-heavy captures now compile clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 09:14:40 -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 46d7dd4646 Reapply "interrupts: hardware-faithful nested IRQ delivery (SR-gated), not in_exception veto"
This reverts commit 06cab1da46.
2026-07-03 08:44:22 -07:00
ai-tdd-labs 6e711e0f2d macos: build & run Kula World natively on Apple Silicon
The Windows->Linux POSIX port left the recompiler and runtime almost
portable; two small platform gaps remained on macOS (AppleClang, arm64),
both guarded so Windows/Linux are unaffected:

- runtime/src/autocompile.c: add #include <stdlib.h> for getenv. glibc and
  windows.h leak that declaration transitively; the macOS SDK does not, so
  AppleClang errored on the implicit declaration.
- recompiler/src/main_bios.cpp: the C-compiler probe used ">NUL" (Windows
  null device). On POSIX that misdirects the redirect and drops a stray
  "NUL" file in the cwd; now #ifdef'd to /dev/null.

The ucontext fiber backend already carried the _XOPEN_SOURCE/_DARWIN_C_SOURCE
guards it needs; SDL2 (Homebrew) selects the Metal backend automatically.

Verified end to end on macOS 15.5 / Apple Silicon: BIOS -> KULA WORLD title
-> menu -> 1 Player -> a 3D level that rolls under control.

Adds docs/MACOS_BUILD_KULA.md and a macOS gameplay screenshot; README notes
the port. Ignores the local dist/ .app bundle (recompiled-BIOS derivative).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 09:41:37 +02:00
Claude b8673b0ad0 docs: Windows 11 build/run handoff for Kula World
Step-by-step instructions for an AI agent to reproduce the working Kula
World recomp on Windows: prerequisites (MSYS2 mingw64 + SDL2), clone the
branch, user-supplied BIOS/disc, extract the boot EXE, build the
recompiler, generate BIOS + game C, build the kula-runtime target, run,
controls, and an honest verified/unverified status list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
2026-07-03 07:03:11 +00:00
Claude a6ee29a4ea readme: Kula World bring-up on Linux — title screen + the 5 fixes
Adds this fork's Kula World (SCES-01000) end-to-end target to the README with
a title-screen screenshot and a summary of the five runtime bugs fixed during
bring-up (four of them game-agnostic framework improvements): the dirty-RAM
interpreter interrupt-latency fix, the main-RAM mirror, GetID region
derivation, CD status bits, and the SCPH-1001 CD controller version.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
2026-07-03 06:07:31 +00:00
Claude 590c236933 dirty_ram_interp: poll interrupts at interpreter entry — fixes Kula World wedge (title screen renders)
ROOT CAUSE of the Kula World black-screen wedge: a guest wait loop (libcd's
CdSync spin) that lives entirely in dirty RAM re-enters the dirty-RAM
interpreter once per short (~7-insn) block. The interpreter's only interrupt
poll was gated by the PER-INVOCATION (insns_executed & 0xFFF) counter, which
short blocks never reach — and when the loop exits to dispatch each iteration
that path isn't reached at all. So the interpreter ran millions of
instructions with psx_check_interrupts never called, while SR (IEc+IM2) and
Cause (IP2) fully satisfied the MIPS interrupt-take condition and i_stat had
CDROM+VBLANK pending. The CD IRQ that sets libcd's wait-flag was never
serviced; the loop spun ~14s per cycle until a rare timeout re-dispatched.

Fix: poll interrupts at the interpreter's per-invocation entry, gated by a
global invocation counter (every 64 entries), cpu->pc set for a coherent
resume — the interpreter analogue of a static block-leader poll.

RESULT: Kula World now boots to its title screen and menu. Diagnosed with the
self-stop trap + gdb sampling of SR/Cause/i_stat + the interrupt gate globals
(in_exception, total_checks) + g_dirty_ram_insns_run, which isolated 'CPU
spinning in the interpreter without polling interrupts'. General fix: any
dirty-RAM spin-loop now yields to interrupts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BdjxoRB1f9VQsNzZayCgGS
2026-07-03 06:02:35 +00:00