57 Commits

Author SHA1 Message Date
Alexandros Mandravillis e3705227b5 Document runtime provenance and hardware references (#731) 2026-10-22 14:23:14 -07:00
Matthew Stanley 49fe052a3e Add uncharged guest calls: run a guest function in zero guest time
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:53:08 -07:00
Matthew Stanley a734cd9034 Record GPU list CPU holds, per-source exception cycles and recent C0 readbacks in always-on rings
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 22:52:52 -07:00
Matthew Stanley 911426552a Add shared seamless-loading kit: resident disc packs and guest services
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:55:30 -07:00
Matthew Stanley fe42da2744 Merge release/native-rendering-20261003 into master
Keeps both lines: master's PGXP session, draw-distance clamps, masked rejects
and saturated-winding NCLIP; the release's render-pass kit additions, verified
media/disc extents, timeline/present rings, cull-scale sites. Overlay ABI v30.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 19:27:02 -07:00
Matthew Stanley 81b449d7c9 Merge V8:2 framework branch (b9f9ba23) into master for render passes
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 08:36:58 -07:00
Alexandros Mandravillis c1a8337f23 dma: hold the cpu while a kicked gpu linked-list walk runs
The GPU linked-list walk reads its headers and payload on the guest clock
while the CPU keeps running. PSX-SPX ("CPU Operation during DMA") says the
opposite: DMA outruns CPU memory access, and a CPU access to RAM or I/O
waits while DMA moves data.

The difference is visible in the retail SCPH1001 intro. Right after
DrawOTag it re-copies its shared text packets from a template, which
clears their link words. With the CPU running alongside the walk, the
copy reached a packet before the walk did, and the walk followed link 0
into low RAM (docs/internal/RETAIL_BIOS_INTERLACE_ROLLBACK.md describes
the same race at BFC188D8 and defers the CPU/DMA arbitration fix).

The store that kicks a linked-list transfer now returns only when the walk
has ended: the kick advances the guest clock event by event and services
the devices until the list is done. A cyclic list still ends at the
walker's existing node limit, and the hold also stops if an advance moves
no list word. Kicks made inside the device service, during lockstep replay
or during a render pass are not held.

dma_gpu_list_cpu_hold links the production dma.c and dma_gpu_ll.c into a
fixture. It kicks A -> B -> end, clears B's header right after the kicking
store (the intro's packet copy), and checks that the GPU still received A's
and B's payload. Before this change the GPU received only A's. A control
checks that a kick from inside the device service is not held.
2026-09-30 22:36:54 +03:00
Matthew Stanley b210446460 Support native-wide presentation diagnostics and verified external mod media
Checkpoint exact NCLIP and cull-scale code generation, overlay ABI 25, draw-area clipping, timeline and presented-image rings, mod counters, hotkey matching and savestate messages. Add canonical external ROM/disc verification with immutable plugin-scoped snapshots. Focused recompiler/runtime validation: 16 tests passed.
2026-09-29 20:57:11 -07:00
Shokunin d22ebac9a4 Host-timed render passes for true in-between frames
psx_mod_render_pass_plan() / psx_mod_render_pass() let a trusted plugin
redraw its game between two logic ticks with the game's own draw code. A
pass is a sandboxed nested guest call:

- time is frozen: cycles are counted (GTE and mult/div deadlines keep
  working) but no device advances, no VBlank or device event fires and no
  interrupt is delivered; GPU linked-list DMA and delayed completions finish
  synchronously (psx_cycles freeze, interrupts, dma);
- stores go straight to RAM/scratchpad, bypassing code-page tracking,
  overlay watch and write traces; MMIO is an allow-list (GP0, GP1 DMA mode /
  info, GPU and OTC DMA, I_STAT/I_MASK) and SPU, CD, timer and other device
  stores are dropped and counted (memory.c);
- afterwards CPU (with the GTE), RAM, scratchpad, I-cache tags, interrupt,
  timer, DMA and GPU registers are restored exactly (render_pass.c, a GPU
  register checkpoint without savestate side effects), with a guest-cycle
  watchdog that rolls a runaway pass back;
- on OpenGL the declared display rect (hr colour + mask stencil, raw mirror,
  native-wide band, CPU VRAM rows) is backed up, drawn, captured and rolled
  back; writes outside it are journaled and restored, or refused.

The presenter keeps two generations of images (the game's own frame plus
one image per pass phase), promotes one when the flip-aware source sees the
display flip to its rect, and shows the newest image at each output
deadline's phase, crossfading across gaps when passes were shed. Presents
that fall due while passes run are made between them. The plan predicts the
output deadlines of the coming frame and sheds passes to fit a host-time
budget learnt from the presenter's idle time (PSX_RENDER_PASS_BUDGET).

PSX_MOD_FRAME_INTERPOLATION_HOLD repeats the newest frame where no pass
image applies, so a pass-driven title never shows anything later than stock.

Default off: nothing runs unless a plugin calls the API, and the plan
refuses in netplay, rollback, rewind, turbo, self-check resimulation, while
suspended (FMV) or without GL flip-aware interpolation. Pass writes are kept
out of frame_fingerprint. Debug: render_pass_stats, render_pass_dump,
PSX_RENDER_PASS_VERIFY=1 (state + VRAM hashes around every pass).

The freeze API lives in the runtime-only psx_cycle_freeze.h. psx_cycles.h is
part of the overlay codegen hash and stays byte-identical, so titles that
never use passes keep their overlay caches and savestates.

Tests: render_pass_plan_test (deadline-exact phases per rate, shedding,
selection), render_pass_freeze_test (10^6 frozen cycles, exact restore,
watchdog), render_pass_guards (sandbox choke points; the codegen-hashed headers stay
free of the pass API).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:47 -07:00
Matthew Stanley c564ccd1f1 runtime: route every RAM consumer through one live 2/8 MiB geometry
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-26 06:31:33 -07:00
Matthew Stanley 471ee0d34b dma: expose GPU linked-list observability
Add passive DMA2 ordering-table counters to dma_state so long or aborted linked-list walks are visible without relying on screenshots. The counters track starts, duplicate starts dropped while a walk is active, completions, cancels, last/max walk size and cycle cost, CHCR poll counts, the initiator PC, and the last eight cancel records.

This intentionally does not port PR #302's superseded walker changes or the PSX_GPU_LL_SYNC diagnostic lever; PR #304 remains the timing implementation.

Co-authored-by: Kevin Kline <9866117+kerokline@users.noreply.github.com>
2026-09-03 16:47:19 -07:00
Alexandros Mandravillis 99f775c839 runtime: close GPU DMA review gaps 2026-09-03 01:20:43 +03:00
Alexandros Mandravillis f424a8144d runtime: preserve live GPU DMA payload timing 2026-09-03 01:20:41 +03:00
Alex Vanderveen 85126d8d1b Revert "dma: charge DMA2 linked-list walks what the validated model charged" 2026-09-02 18:05:17 -04:00
Alex Vanderveen 39bcdf0d56 dma: charge DMA2 linked-list walks what the validated model charged
Slicing the GPU linked-list DMA across guest cycles (8af48ae9, PR #277) also
changed what a walk costs: 8 clocks for every node header plus 5 setup clocks
per non-empty node, where the pre-slicing path charged one clock per word via
schedule_delayed_complete(2, actual_words, DMA_GPU_CYCLES_PER_WORD).

An ordering table is mostly EMPTY nodes -- Final Fantasy VII's runs to
ot_max 4094 -- and each empty slot still paid the full 8-cycle header for a
packet that does not exist. Measured on FF7 (SCUS-94163):

  4,807 nodes / 12,757 words   52,384 cycles   vs   17,564   (3.0x)
 44,010 nodes / 117,507 words  499,187 cycles   vs  161,517   (3.1x)

An NTSC frame is 564,480 cycles, so a single walk reached 88% of a frame.
FF7's libgpu polls CHCR bit 24, finds the channel still busy after 790-890
reads, and force-clears the start bit at 0x800484F0 -- the documented
stuck-channel path. cancel_async_transfer() then abandons the walk partway
through, and the rest of the frame's geometry is never sent to GP0. In a
battle 12.6% of ordering tables were aborted this way; models rendered with
missing or shattered limbs.

The guest is behaving correctly: it inspected the busy bit before clearing it.
The slicing is also correct and worth keeping -- a guest really can change a
packet after starting DMA and before DMA reaches it, and only a sliced walker
models that. Only the cost was wrong.

Header is one clock, setup is free, so a walk costs exactly (nodes + words).
When setup is free the payload boundary is the same service as the header, so
the header phase falls through instead of returning; cycles_to_event() never
reports zero, and returning would spend an unaccounted scheduler cycle per
non-empty node. The packet is still read at its own boundary, so #277's
mid-walk mutation behaviour is unchanged and its test still passes.

(nodes + words) is not a new assertion -- it is the cost this runtime shipped
and was validated against. A non-zero per-node overhead may well be real on
hardware, but it needs a measurement behind it, not a substituted constant.

Observability, because none of the above was visible:

  dma_state gains a gpu_ot block -- starts, completes, cancels, starts_dropped,
  nodes/words/cycles of the last walk and the max, plus guest reads of CHCR(2)
  and a ring of the last 8 aborts carrying the guest PC, CHCR value, how far
  the walk got, and how many times the guest polled before giving up. The
  silent early return in start_async_gpu_linked_list() is now counted as
  starts_dropped rather than losing an ordering table with nothing said.

  PSX_GPU_LL_SYNC=1 drains a walk at start. Diagnostic A/B lever only: it
  completes immediately as well, so it is not a faithful restoration of the
  pre-slicing timing and must not be used as a fix.

The timing test now asserts the cost model rather than describing it: a table
of empty nodes must cost exactly one cycle per node. That is the check that
would have caught this.

Verified on FF7 SCUS-94163 from a save state: cycles_max 58,933 -> 16,972,
a 144-node/490-word walk costs exactly 490 cycles, and battle geometry renders
correctly with the walk still sliced.

Not fixed here: the same commit also broke gpu_frame_dump's func attribution.
GP0 writes now issue from dma_advance() on the cycle scheduler, so every
primitive is stamped with whatever the scheduler interrupted -- all 1,803
primitives in a captured battle frame attributed to BIOS 0x1FC02B50 instead of
the issuing game function. That disables gpu_frame_diff's "a function stopped
drawing" verdict and gpu_frame_layers entirely. Stamping the DMA initiator was
tried and does not work: g_debug_current_func_addr reads 0 at kick time. The
ra column still carries real game addresses as a fallback. Left as a separate
issue rather than shipped as a fix that does not fix it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9v7e6AVZe3eRP8VXey9Pz
2026-09-02 18:01:15 -04:00
Alexandros Mandravillis 8af48ae986 runtime: slice GPU linked-list DMA 2026-08-30 23:17:24 +03:00
Alex Vanderveen 02b08dae54 fix CTR graphics 2026-08-08 12:28:05 -04:00
Matthew Stanley 6e6a0b3ed8 spu: implement reverb, noise, sweeps and SPU IRQ; make CAUSE.IP2 combinational
Closes the four gaps named in issue #103 and lands the separable, verified
parts of PR #102. See docs/internal/SPU_FIDELITY_103.md for the per-piece
disposition and the oracle-verification queue.

SPU DSP (issue #103), clean-room from hardware documentation:

- Reverb: the documented 22050 Hz engine — vLIN/vRIN input, same-side and
  crossed different-side IIR reflections, 4-tap comb early echo, both
  all-pass filters, vLOUT/vROUT output, and the work-area address advance.
  Gated on SPUCNT bit 7 for WRITES only; reads, IRQ checks, address advance
  and output always run, as on hardware. Addressing is confined to
  [mBASE, 0x80000). Per-voice sends from EON, CD send from SPUCNT bits 0+2.
- Noise: 16-bit LFSR with the documented parity feedback, clocked at the
  SPUCNT bits 8-13 rate; NON voices substitute it for their ADPCM sample.
- Volume sweeps: all 24 voice L/R plus main L/R. Bit 15 selects sweep vs
  direct; sweep is a live envelope on the same rate machinery as ADSR, and a
  guest read of a sweeping register returns the live level.
- Capture buffers: CD L/R and voice 1/3 output written to SPU RAM, so a game
  that parks the IRQ address there still gets its interrupt.
- SPU IRQ (I_STAT bit 9): checked at every SPU RAM access class — FIFO, DMA
  both directions, ADPCM block fetch, capture writes, reverb work-area
  accesses — gated on SPUCNT bit 6, mirrored in SPUSTAT bit 6, acked by
  clearing SPUCNT bit 6.

The CD-only fast path in spu_render was removed rather than extended: the
SPU now has per-frame work that must run in every path, and an FMV (XA audio
with zero active voices) is exactly the CD-with-reverb case.

This is a clean-room implementation from the psx-spx/nocash register map and
documented algorithm. Beetle's source was deliberately not consulted: it is
GPL-2.0-or-later and this project is PolyForm Noncommercial. PR #16 was
parked for exactly that reason and PR #13's reverb was separately wrong (it
gated on bit 15 of dAPF1, an address offset, instead of SPUCNT bit 7). The
one place this knowingly deviates from hardware is the 22.05 -> 44.1 kHz
reconstruction filter, which the documentation does not specify; it is
isolated in a single function and flagged for oracle comparison.

CAUSE.IP2 (from PR #102):

IP2 is combinational on R3000A — it mirrors the interrupt line and falls the
instant the guest acks I_STAT or masks the source. It was being OR'd in at
delivery and never cleared, in TWO places (the compiled path and
psx_interpreter.c, the latter missed by PR #102), leaving a phantom pending
interrupt that can spin a kernel dispatcher looping on CAUSE.IP & SR.IM.
psx_irq_refresh_cause_ip2() is now the sole writer of bit 10, refreshed at
raise, I_STAT ack, I_MASK write, HLE context restore and power-on. Verified
against the Beetle oracle's irq.cpp/cpu.cpp rather than asserted, and
covered by test_cause_ip2_combinational.

Mid-dispatch audio pump (from PR #102), with a fix: routed through a gate
that mirrors sdl_audio_update's mute/sink decision. Pumping unconditionally
from the VBlank edge, as submitted, would have pushed real audio during every
turbo-load hard mute and defeated the freeze-in-place mute model.

SPU RAM DMA readback (from PR #102): DMA4 in the SPU->RAM direction wrote
literal zeros; SPU RAM is readable memory and titles carry state through it
across an Exec boundary.

Also fixes an unrelated build race found on the way: tools/embed_spirv.py
wrote its intermediate SPIR-V to <source>.spv, so the two runtime targets
that embed the same shaders raced on one path and one deleted the file the
other was reading.

Co-Authored-By: Alexandros Mandravillis <Alexbeav@live.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 18:16:16 -07:00
Matthew Stanley e629d622fa dma: implement channel 5 (PIO / expansion port) transfers
Salvaged from PR #13 (feat/launcher-stb-revamp, commit 2ba52e97) by
NyperYuhgard. Master fatal-halts on any DMA start for channel 5; this
moves words to/from RAM directly (device side unemulated: RAM-to-device
reads-and-discards, device-to-RAM zero-fills) and completes the
transfer with the normal IRQ path.

Note: zero-filling from an unemulated expansion device is weaker than
the fail-closed psx_fatal_halt default the other channels follow; kept
as submitted because the contributor exercised it on a real title.
Trimmed from the original hunk: unused DMA_PIO_CYCLES_PER_WORD macro
and mdec_finish_async_{in,out}_transfer wrappers nothing references.

Co-Authored-By: NyperYuhgard <noreply@github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 10:55:00 -07:00
Matthew Stanley bc6b277c94 feat(mods): add GPU DMA enhancement memory 2026-08-03 15:55:47 -07:00
Matthew Stanley 63d8f88dc5 runtime: group front-layer UI for widescreen 2026-07-27 15:54:23 -07:00
Alex Vanderveen 0b890cdc13 lobby game version control 2026-07-21 21:38:04 -07:00
Matthew Stanley e4e27f02b5 runtime: optimize MDEC FMV hot paths
Reconstruct the shareable pieces from PR #58 onto current master: add contiguous MDEC DMA input bursts, reduce per-pixel/per-byte MDEC output overhead, add sparse IDCT fast paths, and keep the MDEC activity stamp sane across rematches.

Also quiet the heavy debug trace recorders while MDEC video is active so debug builds can query state without starving FMV playback.

Regression testing was done with non-debug OpenGL builds across multiple FMV-heavy titles. Debug-tool FMV playback was also exercised with the quiet gate enabled.

Co-authored-by: Alex Vanderveen <alex@technicallycomputers.ca>

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 14:29:38 -07:00
Matthew Stanley 5ded80e61e Merge master (AOT overlay generation) into perf/interpreter-hotpath
Integration branch: the interpreter-perf series (interpreter fallback +
additive shard autocompile + always-on production observability + the
fail-closed alias-body emitter fix) rebased-by-merge onto master's AOT
overlay generation (play-free extraction, bounded indirect discovery,
AOT recall, shadow-diff redesign).

Semantic-union highlights:
- gte.cpp: HEAD's direct-field marshaling fast paths kept; master's
  s_gte_replay_sandbox guards grafted onto every affected site.
- psx_icache.c: HEAD's global tag array + fetch_miss split kept;
  master's transactional shadow record/replay retargeted onto it.
- overlay_capture.c: HEAD's commit/retry/wait-pending pipeline kept;
  master's durable history + autocapture APIs wired into it
  (persist_history_snapshot fed from the immutable .d contribution).
- overlay_loader.c: master's interp-record/native-replay shadow-diff
  redesign kept as base with HEAD's scheduler-bail hardening grafted
  (new SHDIFF_SCHED divergence kind); strict manifest v2 parsing keeps
  master's P pair-id lines; always-on native-call ring preserved.
- main.cpp shutdown calls BOTH overlay_autocapture_shutdown() and
  overlay_capture_wait_pending() (the latter drains the preserve-write
  FIFO the former does not).
- recompiler: master's generate_alias_group refactor retains the
  fail-closed alias-body re-entry defaults from c85f1d1.

Validated on this tree: recompiler + runtime + tests build; all six
runtime unit tests PASS; interpreter perf guards PASS; recompiler
patch/structural tests PASS (l1 decoder oracle noise = 376 pre-existing
on both parents).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 00:43:41 -07:00
Matthew Stanley ffda40934b runtime: preserve causality at device deadlines 2026-07-19 13:40:25 -07:00
Matthew Stanley 3949854981 Optimize interpreter fallback and additive overlay autocompile 2026-07-18 08:32:42 -07:00
Matthew Stanley fb585541ec opengl: sustain full-rate gameplay and FMV presentation 2026-07-11 14:07:17 -07:00
Matthew Stanley c3f4aab028 Merge feat/mmx6-widescreen: MMX6 true-wide support
# Conflicts:
#	runtime/launcher/launcher.cpp
#	runtime/launcher/launcher.h
2026-07-10 08:41:36 -07:00
Matthew Stanley 79cdb12eed feat: support MMX6 finite-map widescreen 2026-07-10 01:47:29 -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 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 f47310bede memory/dma: faithful I/O decode — KSEG2 no-op, sub-word memctrl, open-bus unmapped I/O
Tomba2's attract loop-back runs a BIOS bzero + read sweep over a wild
0xDF80xxxx (KSEG2) pointer. Three unfaithful behaviors surfaced, each a
fatal-halt in sequence (f9705/f10002):
  1. Flat 'addr & 0x1FFFFFFF' decode routed KSEG2 onto LIVE registers —
     the sweep zeroed the SIO/memctrl I/O block byte-by-byte. Real
     hardware maps nothing in KSEG2 beyond cache control; Beetle
     (cpu.cpp addr_mask[6..7]=0xFFFFFFFF) leaves it unmapped. All six
     CPU accessors now no-op KSEG2 (counted: g_kseg2_ignored_*).
  2. Memory-control block (0x1F801000..0x103C) and RAM_SIZE (0x1F801060)
     had 32-bit-only handlers; byte/halfword lanes now RMW faithfully.
  3. Unmapped registers INSIDE the I/O window (0x1F8010F8, SIO1 bytes,
     etc.) fatal'd on access; real hardware open-buses them and Beetle
     returns 0/ignores. The MMIO window tails and dma_read/write_masked
     unknown offsets are now counted open-bus (g_io_openbus_*); the
     always-on MMIO rings keep every such access observable.
Validated: attract runs 60fps past both fatal points through multiple
demo cycles. Known next: the attract loop-back logo REPLAY soft-locks
(guest-side; boot-time logo plays fine) — first reachable with this fix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:42:27 -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
Matthew Stanley 4ddccccaa9 checkpoint: exact-store-PC tooling fix (verified) + freeze/scheduler diagnostics + faithful-timing WIP
recompiler/code_generator.cpp: stamp g_debug_last_store_pc before EVERY game
store (SB/SH/SWL/SW/SWR, SWC2, + the ws-backdrop / game-option special paths)
and declare the extern in the game-C preamble. Previously only the BIOS emitter
(strict_translator.cpp) stamped it, so game stores inherited the last BIOS store
pc -> wtrace/readtrace mis-attributed game writes to the BIOS. VERIFIED LIVE:
MMX6's producer trace for the 0x800CD3F8 write now reports the real game store
pc 0x8001A708 (matches Beetle), not the stale 0xBFC11BF0.

runtime: g_pc0_reason slice-exit provenance, fail-closed exception-sentinel
asserts, named thread-event kinds, freeze-heartbeat RVA emission (diagnostics,
default-on, additive).

Carries the in-progress faithful-timing / MDEC work (cpu_state.h, memory.c,
dma.c, interrupts.c, dirty_ram_interp.c, full_function_emitter.cpp) as an
uncommitted-WIP checkpoint. tools/: regen_bios.sh, bios_emitter_fingerprint.sh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:20:17 -07:00
Matthew Stanley 2d915c3fa9 wip(tomba2): precise-IRQ-slicing groundwork — cycles_to_next_event + design spec
Phase 1 of the cycle-budgeted precise interrupt-take-point fix (root cause:
compiled blocks take IRQs only at block edges; HW + the dirty-RAM interpreter
take them at the exact instruction — oracle-confirmed at the Tomba 2 logo
Timer1 debounce). Design validated with ChatGPT; full spec in PRECISE_IRQ_SLICE.md.

This commit lands the additive, no-behavior-change foundation (Task #2) plus the
prior session's diagnostic take-PC capture, and a build-hygiene dedup:

- cycles_to_next_event() (interrupts.c): single source of truth — min guest-cycle
  distance to the next DELIVERABLE interrupt (source raises its I_STAT bit AND
  that bit is unmasked in i_mask). Aggregates VBLANK + per-peripheral queries.
- timers/cdrom/dma/sio _cycles_to_irq(i_mask): each peripheral owns its own
  next-IRQ-distance query; all conservative under-estimates (smaller => slice
  more => never run past an event).
- debug_server: cycles_to_next_event command for live validation.
- interrupts.c: take-PC capture into the IRQ_DELIVER event-ring aux (prior
  session's diagnostic; records the architectural take-PC for cross-backend diff).
- overlay_loader.c: remove duplicate psx_native_bad_entry definition (HEAD had it
  twice; would not link cleanly).

Live-validated at the logo: returns the VBLANK distance, correctly excludes the
three free-running (unarmed/masked) timers and idle CD/DMA/SIO. Runtime-only,
no regen. The two-tier executor (Task #3) that consumes this is next.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 15:04:25 -07:00
Matthew Stanley 340d6e7074 boot snapshot: complete post-BIOS save-state for instant, rebuild-proof boot
Replaces the old incomplete RAM-only fast_boot snapshot with a COMPLETE
hardware save-state captured at the BIOS->game handoff. First launch (and the
first launch after any update) boots the real BIOS normally and captures the
full machine state at the game's PS-EXE entry_pc; every subsequent same-build
launch restores it and presents the game's first frame instantly.

This is not HLE: it persists the real hardware state a real BIOS run produced.

Format (boot_state.h v2): a sectioned file with an integrity key
(bios_checksum + entry_pc + PSX_OVERLAY_CODEGEN_HASH + ABI tag + codegen ver).
Any mismatch -> reject -> normal boot + recapture, so a user/app update can
never silently load a stale snapshot. Missing/short/unknown sections also reject
(no partial restore).

Complete capture (one section per subsystem), the fix for the old snapshot's
real bug (it left GPU/SPU/CD/DMA/SIO at reset while CPU/RAM assumed configured):
- CPU regs, 2 MB RAM, 1 KB scratchpad, IRQ (i_stat/i_mask), root timers, guest
  cycle counter, dirty-RAM page bitmap (guest-written code pages).
- GPU: display/draw-area/offset/mask/texpage + GPUSTAT bits + GP0 command
  machine + VRAM-transfer state; VRAM (1 MB) via the renderer-agnostic
  gr_vram_transfer_out/in hooks (so it is correct on the GPU-authoritative GL
  backend, where the FBO -- not gpu.c vram[] -- is truth, and the FBO re-upload
  on restore happens for free).
- SPU: register file + 24 voice decode/ADSR state + latches + 512 KB SPU RAM.
- CD-ROM: full controller FSM (regs/IRQ-latch/FIFOs/seek/read/pending/queued +
  XA decoder history + disc-speed model).
- DMA: channels[7] + dpcr/dicr + async-transfer state.
- SIO: registers + pad-config FSM + memcard FSM (incl. per-slot mc_slots[]) +
  cycle-paced shifter/ACK scheduler.

Host-side/recompiler-derived state (overlay/sljit tables, code-provider) is NOT
serialized -- it re-initializes fresh on the normal startup path.

Per-subsystem accessors use an X-macro field list so bytes/write/read can never
drift. Runtime-only; no recompiler/codegen change, no regen. Reuses the
[runtime] fast_boot key (semantics upgraded; type unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-24 23:10:37 -07:00
Matthew Stanley 01b4d91e8a WIP: forward-port Ape Escape framework (dev/ape-escape) onto master
Comprehensive 3-way merge of the dev/ape-escape branch into master for
the isolated wt/ape-escape worktree. Brought in every complementary ape
change automatically (dma.c, memory.c interrupt byte/half lanes,
function_analysis discovery, main_bios vector seeds, iso_reader
iso_sector_count, config_loader, crash-trace fields, headers). Resolved
the 13 conflicts by rule:

  - CPS dispatch / recompiler (code_generator, full_function_emitter,
    dirty_ram_interp, traps, main_psx) -> KEEP MASTER (evolved system).
  - CD model (cdrom.c) -> take ape (the model that booted Ape).
  - interrupts.c/.h -> reverted to MASTER (ape's interrupt-delivery is
    dispatch-coupled; the merged Frankenstein stormed).
  - sio/debug_server/freeze_heartbeat/main.cpp -> keep master (evolved).
  - crash_trace.c -> master (ape freeze-dump fns omitted to avoid the
    debug-ring cascade).

STATUS: does NOT boot yet. ape-cdrom + master-interrupts derails to
PC=0 at frame ~2092 (insta-close); ape-cdrom + ape-interrupts storms
(297M exception re-entries) at frame 2077. Root finding: ape's CD
anti-storm is coupled to ape's interrupt-delivery + call-depth + EPC
model, which master replaced with CPS continuation-passing, so the CD
subsystem does not forward-port as pure hardware simulation. Preserved
as the reference state for a design consult on bridging the two models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 15:55:23 -07:00
Matthew Stanley 9cd98b0c0c harden: refuse corrupt DMA lengths, fatal-halt all DMA/GPU death sites
A pre-fix degraded session died via host SEH (wild write, frame 282184)
with garbage DMA ch2 regs in flight, losing every ring. Convert the
corruption escape paths into held, fully-queryable states:

- dma.c: validate BCR-derived transfer length on every kick (cap 0x80000
  words = 2 MB; nothing legitimate exceeds RAM/VRAM size). Violation ->
  psx_fatal_halt with MADR/BCR/CHCR in the reason. Linked-list mode is
  exempt (BCR unused; node walker already has MAX_NODES).
- dma.c: clamp the CD overlay-capture span to host RAM end — the DMA
  word loop wraps inside the 2 MB mask but the capture memcpy/CRC took a
  flat ram+offset span and could read past the RAM array.
- dma.c, gpu.c: all six fprintf+exit(1) fatal sites (unimplemented DMA
  channel, unknown DMA reg, unimplemented/unknown GP0, unknown GP1) now
  route through psx_fatal_halt — rings stay live for post-mortem TCP.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 12:55:21 -07:00
Matthew Stanley 70c1eb8a51 Merge master (Bug B pacing fix + overlay cache campaign) into dev/ape-escape
Brings the ape-escape framework (real-EPC interrupt delivery,
g_psx_call_depth safe-point gating, continuation-entry codegen, DualShock
controller config, exception/entryint/irq-check rings, interp
dispatch_until + exit logging + static-text fallback) up to master
035a9fa (frame-pacing TOCTOU fix, vsync self-heal, watchdog classifier,
overlay JIT cache + autocapture/autocompile, Bug A interp call contract,
prod build hygiene).

Key reconciliations (ambiguity resolved toward master per plan):
- interrupts.c: real-EPC delivery (psx_check_interrupts_at) is the
  primary entry; the legacy psx_check_interrupts wrapper keeps the
  0x80000048 sentinel fallback (ape tip restored it), so old-flavor
  generated code keeps working interrupt delivery. Master's bottom
  compat shim removed (would have recursed). Both observability paths
  kept (event-ring gate outcomes + ape irq-check ring; new
  GATE_CALL_DEPTH/GATE_BAD_RESUME kinds).
- Claimed-IRQ cooldown: master's per-source CLAIMED_PROGRESS_QUANTUM=8
  (SIO exempt) over ape's unconditional 0; ape's 2026-06-05 starvation
  note kept as a MERGE NOTE — first knob to A/B if title-LOAD regresses.
  (User-validated: Tomba title-LOAD slot1/slot2 renders and plays on
  this merge — the historical ape-fw blackout is gone.)
- dirty_ram_interp.c: ape's dispatch_until_impl structure (stop_addr,
  static-text fallback, exit-log ring) with master's Bug A call contract
  (psx_dispatch_game_compiled + overlay_loader_call_native at interp
  jal/jalr), overlay loader/fingerprint entry checks, freeze-gated flow
  ring, chain-target attribution, and interp-active bracketing on the
  public wrappers. Ape tip's final local-flow rule kept: dirty game-text
  NEVER chains locally (the tip reversed its own earlier widening).
- freeze_heartbeat.c: classifiers composed — per-frame storm threshold
  (master) + progress-counter gating and 6s window (ape) + slow_frames
  kind (master), thresholds rescaled to the window.
- code_generator.cpp: ape's continuation-entry switch + master's
  refactored scan_jr_tables.
- debug_server.c: ape originals win over master's ported copies
  (bioscall ring, card trace with the richer address set); master-only
  tools kept (dirty_*_dump_file, card_trace filter handler).
- dma.c/cdrom.c: master's overlay-capture path + ape's DMA/command
  history diagnostics, both wired.

Builds: recompiler + Tomba runtime (legacy gen x merged runtime).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 22:05:13 -07:00
Matthew Stanley 5041712a05 runtime: phase-5 checkpoint — event-timeline ring, instant-disc rate fix, overlay diagnostics
Accumulated working-tree state for the overlay-JIT-cache phase, committed as a
checkpoint before the Bug A fix (next commit):

- event_ring.{c,h}: always-on unified event-timeline ring (IRQ deliver/gate/
  raise, I_STAT changes, DMA kick/sched/done, ENQ/DEQ per source, exec-mode
  tagged). Edge-recorded, eviction-bounded, survives a blue screen; dump via
  event_ring_dump/_tail/_clear.
- cdrom.c: disc_speed="instant" no longer collapses every sector read to the
  500cy floor (which flooded ~1100 DMA3 completions/frame and starved main
  code). CDROM_INSTANT_PERIOD bounds sector IRQs to CDROM_INSTANT_MAX_PER_FRAME
  (32)/frame. Known caveat: a transient ~1-2s reentry storm at boot frame ~210
  still trips the watchdog once, then recovers.
- interrupts.c: claimed-IRQ cooldown is now per-source — SIO stays 0 (card
  reads need back-to-back IRQs); DMA/VBLANK/timer get CLAIMED_PROGRESS_QUANTUM
  (8) blocks of guaranteed main-code progress between deliveries. Root cause of
  the instant-disc freeze: cooldown=0 let the block leader re-take the
  exception before its body ran (pinned at the 0x8005A1D8 field loop).
- gpu/dma/memory/sio/timers/psx_cycles: event-ring instrumentation at raise/
  kick/complete/schedule sites; g_event_step_conservative (single-cycle event
  stepping A/B used to refute the event-granularity hypothesis for Bug A).
- overlay_capture.c, overlay_loader.h, crc32: capture/manifest plumbing for
  per-entry validity (Inc3).
- recompiler: overlay-mode emission updates (code_generator, main_psx).
- tools: compile_overlays.py audit/patch updates; tomba_loadtest.py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 12:24:40 -07:00
Matthew Stanley b2929aa504 overlay: restore overlay DLL cache + capture (faithful port of codex/cache-discovery)
Reinstates the runtime-loaded overlay DLL cache (Layer A) and the offline
overlay compilation pipeline that were built on the pruned codex/cache-discovery
branch (tip 2bbafc2) and never merged. Ported onto current master; rebuilt
rather than history-restored.

Runtime (off by default, opt in via [runtime] overlay_cache = true):
- overlay_loader.c/.h, overlay_api.h: CRC32-keyed DLL cache at
  cache/<game_id>/<region>_<crc>.dll. On a dirty-RAM dispatch miss the loader
  LoadLibrary's the matching DLL, wires runtime callbacks via overlay_init(),
  enumerates func_XXXXXXXX exports, and dispatches them BEFORE the interpreter.
- overlay_capture.c/.h: write-once capture set; records overlay DMA bytes and
  writes overlay_captures.json (execution-verified seeds) at clean exit.
- dma.c: capture + cd_dma_log hook at CD DMA completion (async model).
- cdrom.c: cdrom_get_setloc_lba() for disc-position correlation.
- dirty_ram_interp.c: g_dirty_ram_exec_pc_table (exec-verified PCs), a
  decodability guard on JAL-into-dirty targets, and the overlay dispatch hooks.
- debug_server.c: cd_read_log + overlay_loader_status TCP commands.
- config_loader: [runtime] overlay_cache flag (default false).

Recompiler (compile_overlays.py / extract_overlays.py drive these):
- main_psx.cpp --overlay-exact: analyze only explicit entries + direct JAL
  targets and set split_mid_function_targets=false, so overlay basic-block
  targets are NOT split into bogus standalone functions (the mid-function-seed
  failure mode that produced black-on-load / blue-on-new-game).
- function_analysis.cpp: analyze_exact_entries(); code_generator.cpp: gate the
  mid-function split pre-pass behind split_mid_function_targets (default true,
  so normal main-EXE codegen is unchanged - Tomba still regens to 2854 funcs).

Deltas from the original: save states dropped (never worked); overlay enable is
runtime-config-gated (off by default) instead of a build-time define; loader
stderr logging converted to the overlay_loader_status TCP path (CLAUDE.md Rule 3).

Recovered from _backups/2026-06-06-consolidation/psxrecomp-all-PRE.bundle.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 12:32:19 -07:00
Matthew Stanley 834f440161 WIP: ape-fw title-LOAD debugging snapshot (uncommitted session work)
Captures in-progress Ape Escape framework + Tomba title-LOAD investigation:
- recompiler/src/function_analysis.cpp: hardened is_bios_dispatch_thunk pass
  (never gut a li/jr candidate already inside a discovered function).
- runtime: cdrom/dma/dirty_ram_interp/interrupts/sio/traps/debug_server +
  freeze_heartbeat + main.cpp debugging deltas; SIO card-protocol test updates.

Snapshot commit to consolidate loose WIP onto dev/ape-escape; not a finished
change. See handoff: title-LOAD black = card op-loop never delivering SwCARD
spec 0x8000 completion event.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 23:04:23 -07:00
Matthew Stanley d62d51ba2f Ape Escape bring-up: attract demo now loads
Framework changes that get Ape Escape (USA) booting through to its
attract/demo sequence. Captures the working state at this milestone.

Runtime:
- cdrom: read/seek + data-path fixes for disc streaming
- dma / interrupts: channel + IRQ handling needed by the boot chain
- dirty_ram_interp: expanded coverage for self-modifying / RAM handlers
- sio, traps, crash_trace: supporting fixes
- debug_server: probe/inspection commands used during bring-up

Recompiler:
- code_generator / full_function_emitter / function_analysis / main_psx:
  codegen fixes surfaced by the Ape Escape boot path

Committed as-is (no further edits) since the build is in a known-good,
demo-loading state. Branch: codex/ape-escape-support.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 19:32:36 -07:00
Matthew Stanley ec7e482691 Ape Escape support: framework WIP snapshot (safe haven)
Uncommitted framework work on codex/ape-escape-support reaching the
post-boot DualShock-required screen. Recompiler codegen (code_generator,
full_function_emitter, function_analysis, config_loader) + runtime
(cdrom, sio, dirty_ram_interp, dma, iso_reader, mdec, gpu, memory,
traps, debug_server, fntrace, freeze_heartbeat, psx_cycles) and headers.

Committed as a restore point before rebuilding Tomba against this
framework. Build dirs (recompiler/build-codex*) intentionally excluded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:48:38 -07:00
Matthew Stanley ec4306fbb3 mmx6 framework: CD/DMA/MDEC + codegen fixes (Tomba builds+plays, MMX6 boots to intro)
Framework work on codex/mmx6-support that makes Tomba build and play
(title screen, 0 dispatch misses) and MMX6 boot through BIOS to its
intro. Touches recompiler codegen (code_generator, function_analysis)
and runtime CD-ROM/DMA/MDEC/GPU/SIO timing (cdrom, dma, mdec, gpu,
psx_cycles, traps, fntrace, debug_server) + headers + runtime.cmake.

Milestone snapshot for collapsing onto master. Build dir excluded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:06:02 -07:00
Matthew Stanley 963a87a327 Replace DMA overlay hook with overlay_dump TCP command
The CD DMA hook (overlay_log.c) could not distinguish code overlays from
FMV streaming data — Tomba buffers FMV through the same physical address
range and DMA channel. Ripped it out entirely.

Replace with overlay_dump TCP command in debug_server.c: scans the
dirty_ram bitmap for contiguous dirty pages above a configurable physical
threshold (default 0x98000), writes <crc32>.bin files to a caller-supplied
directory, and returns a JSON manifest. dirty_ram IS the oracle for
"executed as code" — no false positives from data transfers.

tools/dump_overlays.py queries overlay_dump post-session and appends new
entries to logs/<game_id>/overlay_map.jsonl with (crc32, load_addr, size,
bytes_file). First run on Tomba captured 4 regions (8KB, 264KB, 356KB,
748KB) matching the known dirty_ram miss clusters.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 12:33:45 -07:00
Matthew Stanley 38ec2e7426 Add overlay logger (B+A Layer B-1)
Hooks CD DMA channel 3 completion to log code overlay loads to
logs/<game_id>/overlay_map.jsonl for build-time pre-compilation.

Hot path cost: two integer comparisons (address + size filters) and
return. CRC32 is NOT computed inline — too expensive for FMV-heavy
scenes that stream many small DMA transfers through channel 3.

Filters:
- phys_addr < 0x80000: skip (main EXE, kernel, data)
- size_bytes < 4096:   skip (streaming data, not code)

Deduplicates on (addr, size) in a 128-entry table; appends new entries
to an open file handle (no fopen/fclose per transfer). overlay_log_init
is called from main.cpp with the game_id and game.toml directory.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-30 11:28:00 -07:00
Matthew Stanley 0c5b892904 Fix Tomba visual rendering issues 2026-05-17 21:58:55 -07:00
Matthew Stanley 2f709ee542 Phase B: config-driven unified runtime + BIOS shell boot restored
- Config loader (recompiler/src/config_loader.{h,cpp}): single source of
  truth for both BIOS and game runtime settings (debug_port, window_title,
  memcard_dir, disc path). psxrecomp-bios and psxrecomp-game both accept
  --config <toml>; runtime takes --game <toml> (no more CLI overrides).
- runtime/src/main.cpp: replaced --disc/--memcard-dir/--game-root CLI
  overrides with --game <toml>; game.toml is the single source of truth.
  Discless launch (no args) goes to BIOS shell.
- runtime/src/dma.c: fixed update_master_irq() re-asserting I_STAT bit 3
  on every DICR read. BIOS shell DMA handler at 0x80058628 only clears
  DICR bit 27 (ch3 flag), so level-sensitive re-assert pinned the kernel
  in the exception loop. Bit 3 is now edge-triggered (one assert per DMA
  completion). Discless boot now reaches MAIN MENU / MEMORY CARD / CD
  PLAYER. With-disc Tomba boots through to Whoopee Camp splash.
- Audit tooling: codegen_audit.py (BIOS), codegen_audit_game.py (game),
  audit_config.py (config loader for tools), verify_audit_schema.py.
  Replaces older tools/_codegen_audit.py probe.
- Docs: CURRENT_STATE.md (project state), UNIFICATION_PLAN.md (Phase C
  translator merge plan), docs/config_schema.md (TOML schema reference),
  docs/audit_inventory.md (audit tier inventory).
- .gitignore: tools/_*.py convention for ad-hoc scratch probes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 01:31:27 -07:00
Matthew Stanley cf05a0ab1f runtime + recompiler: Tomba boot through BIOS into Whoopee Camp FMV
Recompiler — `recompiler/src/function_analysis.cpp` now stops a
function's end_addr at its mapped `jr $ra` + delay slot instead of
stretching through trailing data. Fixes Tomba's 0x80076DB0, a real
136-byte function that was being emitted as a psx_unknown_dispatch
stub because data after it poisoned the boundary heuristic.
`code_generator.cpp` emits `func_<next>(cpu); return;` as a tail
call when a mid-function seed split leaves the physical fall-through
in a different C function, plus a `debug_server_log_call_entry` at
every function entry. `full_function_emitter.cpp` splits dispatch
into `psx_dispatch_impl(cpu, addr, stop_addr)` with public
`psx_dispatch` and new `psx_dispatch_call(cpu, addr, return_addr)`
so JAL/JALR into RAM stubs can return cleanly without growing the
native stack.

Runtime — `runtime/src/dma.c`/`include/dma.h`/`src/memory.c` now
expose `dma_write_masked()`; the 16-bit and 8-bit mmio paths route
DICR/DPCR/CHCR partial writes through it so a partial DICR write no
longer accidentally clears unrelated channels' pending IRQ flags,
and CHCR only triggers DMA when the written mask actually includes
the start bit. Unified `complete_transfer(ch)` across all channels,
removed `prev_master_flag` edge detection.

MDEC (`runtime/src/mdec.c`, `include/mdec.h`) gains a full debug
surface: `MDECDebugState` snapshot, an always-on 4096-entry
`MDECDebugEvent` ring with 11 event kinds (reset, ctrl_write,
cmd_begin/done, decode_done, dma_in/out start/end, output_drained,
read_underflow), and `mdec_state` / `mdec_trace` / `mdec_trace_clear`
debug-server commands. DMA ch0/1 emit start/end events around MDEC
transfers.

CDROM (`runtime/src/cdrom.c`, `include/cdrom.h`) — read stream
separated from one-shot pending state (`read_cmd`, `read_delay`,
`start_read_stream`, `stop_read_stream`, `deliver_read_sector`);
`cdrom_state`/`cdrom_trace` report the new fields.

Thread/fiber tracing — `traps.c`'s Win32-fiber TCB save/restore now
emits 12 thread-event kinds; new `ThreadTraceEntry` ring (64 K) plus
`thread_trace` / `thread_trace_clear` debug-server commands report
both live CPU regs and the target TCB's full saved register block.

wtrace / mmio rings — `WriteTraceEntry` grew (cpu_pc, sp, v0/v1/a0-a3/
t0/t1); capacity 1 M -> 4 M. `MmioTraceEntry` grew (pc, cpu_pc, sp,
a0-a3, sr, epc, i_stat, i_mask). Both dump commands accept
`count`/`newest=1` for reverse iteration. `restore_trace` and
`dirty_block_log` emit extended register fields (a2/a3/sp).

Misc — `interrupts.c` polls debug-server every 16K checks so it
stays responsive without a separate thread. `psx_runtime.h`'s
`call_by_address` routes through `psx_dispatch_call`. Dirty-RAM
interp JAL/JALR consult `psx_dispatch_game_compiled` when the game
is wired in. iso_reader gains the entry points needed by the game
disc path. New `tools/_tomba_probe.py` for port-4470 health checks.

Generated — regenerated `SCPH1001_dispatch.c` against the new
`psx_dispatch_impl` shape.

ISSUES.md — opened Issue #3 (BIOS PS-logo glyph missing on the
disc-detected screen, cosmetic).

.gitignore — added `recompiler/build-asan*/`, `build-checked/`,
`__pycache__/`.

Validated end-to-end: BIOS boots, disc detected, Whoopee Camp FMV
plays with audio. Frame count advances through ~40K frames without
freezes. `unknown_dispatch_log.total = 0`. Remaining FMV cluster
issues (1/4-size render, FMV-2 audio missing, no skip) tracked
under TombaRecomp/ISSUES.md.

Co-Authored-By: Codex (OpenAI) <noreply@openai.com>
2026-05-11 12:34:49 -07:00