Commit Graph

331 Commits

Author SHA1 Message Date
Alex Vanderveen 4ddd2d01b9 rollback patches 2026-08-01 14:13:58 -04:00
Alex Vanderveen fe403e6ce1 rollback patches 2026-07-31 17:14:35 -04:00
Alex Vanderveen b3d99b05ff rollback patching 2026-07-30 17:24:20 -04:00
Alex Vanderveen 0819dee765 rollback patch 2026-07-30 13:27:36 -04:00
Alex Vanderveen 6c6bde94ae initial rollback patches for input handling and resim accuracy 2026-07-30 13:19:55 -04:00
Alex Vanderveen 88234d5e60 rollback integrations 2 2026-07-30 02:00:31 -04:00
Alex Vanderveen 3b88d9f9b5 rollback integrations 2 2026-07-30 01:58:30 -04:00
Alex Vanderveen 2b35b76a56 rollback engine patch 1 2026-07-29 21:44:44 -04:00
Alex Vanderveen 3da61df355 Revert "ok"
This reverts commit b836f2198c.
2026-07-29 03:18:10 -04:00
Alex Vanderveen b836f2198c ok 2026-07-29 03:15:42 -04:00
Alex Vanderveen 92efb5c4fd no mods in MP, minor patches 2026-07-29 02:40:50 -04:00
Alex Vanderveen 070fc805c4 Merge origin/master into feat/ui-5p.
Keep 5p/ICE netplay host + lobby client; take master's cycle batching,
SDL3 defaults, and dropped vendored lib/recomp-ui.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-28 20:17:14 -04:00
Matthew Stanley 3c67a52f95 opengl: add motion-adaptive interpolation blend 2026-07-28 14:27:40 -07:00
Matthew Stanley a292a7c136 Add activation-only widescreen cull margin 2026-07-28 11:08:03 -07:00
Matthew Stanley 106e31d6e9 Make SDL3 the default runtime backend 2026-07-28 09:22:28 -07:00
Matthew Stanley e4e2aa7a7d runtime: add per-game radial stick response 2026-07-28 00:20:45 -07:00
Matthew Stanley 64097606fb runtime: fix game-owned display and controller mods 2026-07-28 00:04:20 -07:00
Alex Vanderveen 227d395823 ICE optimizations, connectivity, latency polling, save/loadstate performance 2026-07-28 00:16:30 -04:00
Matthew Stanley eb262c4b23 feat(mods): let a trusted plugin read its own option values
The manifest schema already carries typed, validated, launcher-rendered,
persisted options ([[option]] boolean/choice/integer), and the launcher
provider already reads them. A trusted activation plugin could not: the
callback takes no arguments, so a plugin was only ever an on/off switch.

That forced a parameterised feature to be modelled as one feature per
value, and `constraint` only expresses ordered_integer WITHIN a feature, so
those pseudo-features could not even be made mutually exclusive. A game
wanting "Widescreen: 16:9 / 21:9" or "Interpolation: 90/120/144/165/240"
had no way to express it as one entry.

psx_mod_option_value(package, feature, option, out, out_size) closes the
gap against the value the player actually committed. It returns 0 with an
empty string when the plan is not committed or the ids do not resolve, so a
caller applies its own default rather than reading "" as a selection. Ids
are explicit because registration is by plugin id alone and the callback
carries no package/feature context.

Also in this commit, from the same investigation:

* Sustained overlay-autocompile failure is now LOUD. After 3 consecutive
  failed runs the runtime prints once to stdout: what failed, the exit
  code, that overlays are staying interpreted and frame times will be far
  worse than the build is capable of, what to check, and the compiler's own
  output tail. Previously the only trace was a `fails` counter reachable
  over TCP, so a wholly-interpreted title presented as "why is this slow"
  rather than as a message. Threshold is 3, not 1, because isolated
  failures (a shard racing a capture rewrite, a transient lock) are
  ordinary and self-heal. Measured cost of the silent case on Tomba!:
  p50 frame period 105ms, vs 3.1ms once shards existed.

* The autocompile child's working directory is printed at startup beside
  cache and captures. Every relative path in overlay_autocompile_cmd
  resolves against it and it comes from game.toml's detected project root,
  so a game.toml staged outside the real root silently repoints the entire
  compile command with no other symptom.

* A build tree whose cache still carries recomp-ui's OFF for
  RECOMP_UI_ENABLE_MODS now says so at configure time. 610234a's seed only
  fires on a fresh cache, and a stale OFF is indistinguishable from a
  deliberate one, so such trees kept producing Mods-less builds across
  reconfigures - the same "reads as a stale build" failure one level up,
  and invisible. The explicit-OFF contract is deliberate so this does not
  override it; it makes the state audible.

Runtime ctest 16/16. Warning verified to fire on a tree configured with
-DRECOMP_UI_ENABLE_MODS=OFF and to stay silent on a fresh configure.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 17:33:44 -07:00
Matthew Stanley 5ec343ab4f fix(bios): make the boot-skip flag mean the same thing on every BIOS
`bios_hle` (default on) is the player-facing "skip the BIOS and go
straight to the game" flag, and it did nothing under the bundled
OpenBIOS: choosing OpenBIOS sat the player in the boot animation while
the identical build on retail SCPH-1001 went straight to the game.

Root cause was ordering, not policy. The tier has two axes with
different per-image requirements:

  * boot-skip     needs only `shell_entry_phys`, works under pure LLE
                  (nothing is synthesized — the shell call just returns
                  immediately), so any image exporting it can skip
  * kernel-call    needs `deliver_event_ret`, the kernel's own
                  DeliverEvent `$ra`, or a delivered callback returns to
                  the wrong PC

main.cpp correctly refused the call axis on an image without
`deliver_event_ret` by assigning `bios_hle = false` — and then derived
`boot_skip` from that already-mutated flag. OpenBIOS omits
`deliver_event_ret` (B0 semantics unvalidated) but DOES export
`shell_entry_phys`, so an available skip was cancelled by an unrelated
refusal, and one flag meant two things depending on a BIOS detail no
player can see.

Both axes are now decided by one pure, dependency-free function,
`psx_bios_hle_plan()` (runtime/src/bios_hle_plan.c): the boot decision
reads the REQUESTED `bios_hle`, each axis is gated on only the anchor it
needs, and refusals are reported at startup instead of downgrading
silently. `psx_bios_hle_configure()` clamps `boot_skip` to
`shell_entry_phys != 0` as a backstop so the banner and `hle_dump`
cannot overstate what can fire. The main.cpp local is renamed
`bios_hle_requested` and is no longer written back from the plan — the
`session_reboot` path re-enters that block, so folding a refusal into
the request would have made a netplay rematch differ from first launch.

Also in this pass:

* A BIOS profile's `[runtime]` block was parsed into
  `BiosConfig::runtime` and read by NOTHING, and bios/OpenBIOS.toml
  carried an inert `bios_hle = false` there that read as the reason HLE
  was off on OpenBIOS. It was not. `load_bios_config` now REJECTS
  `[runtime]` in a BIOS profile: a profile states facts about an image,
  never preferences.
* `psx_icache_fastpath_test` had been unbuildable ("redefinition of
  `psx_advance_cycles`" — the test's counting stub vs the header's
  `static inline`), which blocked every runtime test registered after
  it. It now builds with `PSX_OVERLAY_DLL_BUILD=1`, the seam
  psx_cycles.h already provides for a TU supplying its own advance.

Tests: new `bios_hle_plan_test` pins the matrix, including a 64-case
sweep asserting the boot decision is independent of
`deliver_event_ret` and the call decision independent of
`shell_entry_phys`. Runtime ctest 15/15, recompiler ctest 30/30.

Validated on Ape Escape (SCUS-94423) built against this framework with
both backends linked, headless, all four cases:

  OpenBIOS default   bios_boot=skip to game; route=2 fire at vec 0x30000
                     ra=0xBFC06EA8 (the continuation OpenBIOS.toml
                     documents); in-game on screen
  retail default     bios_boot=skip to game; route=2 at vec 0x30000
                     ra=0xBFC0702C; in-game on screen; differs only in
                     bios_backend=HLE (LLE fallback)
  PSX_BIOS_HLE=0     bios_boot=real intro; ring empty (no hook)
  KEEP_INTRO=1       bios_boot=real intro; ring empty

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 16:36:48 -07:00
Matthew Stanley 63d8f88dc5 runtime: group front-layer UI for widescreen 2026-07-27 15:54:23 -07:00
Matthew Stanley 49b57f4178 docs(gpu): correct the MMX6 widescreen comments; record what is safe to delete
Comment-only. No compiled code changes -- the sole non-comment diff line moves
a trailing comment off the g_mmx6_hostside declaration into a block above it;
the declaration itself is byte-identical.

A survey of the mmx6-named widescreen symbols found the existing comments
misleading in a way that invites a dangerous cleanup, so the findings are
recorded in the source next to the code they describe.

1. "Compatibility entry points for already-generated MMX6 sources" (gpu.h) was
   wrong. These are not MMX6 compat shims and are not safe to delete. They are
   the widescreen-2D entry points the recompiler emits for ANY title with a
   [widescreen.bg2d] block, and live generated code in several non-MMX6 repos
   links them today:
     cols/startcol/startx/stream_left/stream_right
         -> CrashBash, MegaManX5, Tsumu (+ -release)
     undercap
         -> Ape, CrashBash, MMX4, MMX5, Tsumu, Vigilante8, THPS2
   Each forwards to its psx_ws_bg2d_* twin, which is the real implementation.
   Only the name is historical. Renaming is not runtime-only: the names come
   from code_generator.cpp, which is in codegen_hash_sources.cmake, so it rolls
   the overlay cache tag and forces regen + reshard of every title.

2. Host-side reveal (g_mmx6_hostside) is dead in every shipping build, which
   the previous "kept inert pending removal" note understated. Nothing sets it
   but the mmx6_reveal debug command, PSX_DEBUG_TOOLS is OFF for Release, and
   no generated source anywhere references a host-side symbol. Noted that
   removing it is the last step decoupling the generic psx_ws_bg2d_* API from
   MMX6 -- mmx6_hostside_active() is currently embedded in the ternaries of
   psx_ws_bg2d_cols/startcol/startx -- and listed the symbols that go with it.

3. BG buffer relocation is a parked FEATURE behind a load-bearing SYMBOL, a
   distinction the old comment did not draw. With g_mmx6_reloc == 0 both hooks
   return their argument unchanged, but MegaManX6Recomp/game.toml names the
   bufbase/cap instruction sites, so the emitter writes calls into generated
   code and psx_ws_mmx6_bg_bufbase is genuinely called by MMX6's generated C.
   The supportable cleanup is to gut the bodies to identity and keep the
   symbols, not to delete the functions.

Removal of (2) and (3) is deliberately deferred: it touches the renderer and
wants its own session with MMX6 revalidation.

Verified: gcc -fsyntax-only on gpu.c exits clean (an earlier draft embedded
"*/" inside a block comment and broke the parse -- caught before commit);
ctest 29/29 green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-27 11:49:31 -07:00
Matthew Stanley 2696c391b8 feat: add aspect-aware world participation 2026-07-27 10:48:41 -07:00
Matthew Stanley d116e0da3b feat(mods): add presentation interpolation rates 2026-07-27 10:40:49 -07:00
Matthew Stanley d34af54141 feat(mods): support native VBlank rate selection 2026-07-27 10:32:33 -07:00
Matthew Stanley 964cdb3870 feat(mods): expose adaptive display activation 2026-07-27 10:19:07 -07:00
Alex Vanderveen fbad0be1b9 launcher performance, load state performance, game launch performance
it stops checking the entire PSX disc for full hash check every time the launcher runs, assuming the user's hash checked it when selecting the disc the first time and saving it to settings, so it just validates the file name and size instead, then it shares lifecycle with the launcher and the game to load the game faster, and several optimizations were made to loadstate paths which I think in the end relied on reanchoring host timings to the restored clock that came with the state
2026-07-27 11:03:10 -04:00
Matthew Stanley fa408aae50 Merge trusted mod plugin support 2026-07-26 21:05:03 -07:00
Matthew Stanley 45dd4210cf Remove Tomba legacy hybrid controller path 2026-07-26 20:52:12 -07:00
Matthew Stanley 9683a1cf8c feat(mods): support mod-owned FMV skipping 2026-07-26 20:34:20 -07:00
Matthew Stanley 99abda239f feat(mods): add pre-runtime activation plugins 2026-07-26 20:18:00 -07:00
Matthew Stanley a4afb828ac feat(mods): add trusted static vblank plugins 2026-07-26 18:59:10 -07:00
Matthew Stanley 8b77d8a730 fix: make overlay autocompile cache-safe 2026-07-26 09:39:24 -07:00
Matthew Stanley 436fe14101 widescreen: support guarded object participation overrides 2026-07-25 23:24:41 -07:00
Matthew Stanley 05e569c121 runtime: add sparse guarded mod fields 2026-07-25 21:42:35 -07:00
Matthew Stanley 66f2c7be9e runtime: pass active mod context to resolvers 2026-07-25 21:14:28 -07:00
Matthew Stanley ddf3ca8962 feat: add ordered mod values and MIPS pair patches 2026-07-25 21:14:28 -07:00
Matthew Stanley bd460f3ce1 fix: guard complete dynamic patch records 2026-07-25 21:14:28 -07:00
Matthew Stanley eb3b4db599 feat: add bounded integer mod patches 2026-07-25 21:14:28 -07:00
Matthew Stanley 968c9c23ce feat: add feature-oriented mod resolution 2026-07-25 21:14:28 -07:00
Matthew Stanley 9ff287c1fa feat: support multi-option mod conditions 2026-07-25 21:14:28 -07:00
Matthew Stanley 6c1bd61a1f feat: derive modded discs from verified stock images 2026-07-25 21:14:27 -07:00
Matthew Stanley 316eaa44a7 feat: add versioned runtime mod packages 2026-07-25 21:14:27 -07:00
Matthew Stanley 986d8be94c Merge remote-tracking branch 'origin/master' into integrate/openbios-bundle
# Conflicts:
#	recompiler/src/full_function_emitter.cpp
#	recompiler/src/full_function_emitter.h
#	recompiler/src/main_bios.cpp
#	runtime/runtime.cmake
#	runtime/src/main.cpp
2026-07-25 12:31:05 -07:00
Matthew Stanley 33589d9c43 feat(bios): show the BIOS row on bundled builds and honour a chosen image
Two runtime-side changes the launcher's SYSTEM row needs.

The row was hidden whenever the linked BIOS was bundled, on the reasoning
that there was nothing to select. With OpenBIOS AND a retail image linked
that strands the player: choosing retail — and clearing back — is exactly
what the row is for. Visibility now asks the registry (is any linked
backend NOT the bundled one?) via psx_bios_has_selectable(), which is
also correct before selection, unlike psx_bios_image.

A BIOS named in settings.toml is likewise now honoured on every build.
It was previously ignored on bundled builds to stop a stale path pointing
at a foreign image, but that guard would now silently discard the only
in-app way to opt into retail. The identity gate is the real protection,
and it explains a mismatch rather than ignoring it. An EMPTY value stays
meaningful in the other direction: that is the launcher's Clear, "go back
to the bundled BIOS", so it is not adopted as a path.

Pairs with recomp-ui feat/bios-row-clear.

Not yet compile-verified against the launcher: building with
PSX_RECOMP_UI=ON on this branch fails in the recomp-ui netplay callback
ABI (main.cpp:4495), which traces to 87491cb and is inherited from this
branch's base predating master's matching integration — the same skew
that stops MMX4/MMX5 building on current master with their pinned
recomp-ui. Needs master merged in, which this branch requires before
merge regardless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 12:23:48 -07:00
Matthew Stanley 7e4adff41f feat(bios): backend ABI so two recompiled BIOSes link into one binary
Groundwork for "no BIOS chosen -> OpenBIOS, explicit choice -> that BIOS"
(docs/BIOS_SELECTION.md). Shipping both images in one build removes the
per-BIOS build flavour, and with it the three restatements of the same
choice that nothing cross-checked.

Two recompiled BIOSes could not previously coexist: each generated
dispatch exported 16 globals, 14 of which were identical between images
(16 func_ bodies at coincident normalized addresses, the two dispatch
entry points, the per-image tables, and the dispatch depth).

Each image now exports exactly one descriptor, PsxBiosBackend
<STEM>_psx_bios_backend, carrying its image info, dispatch entry points
and kernel-body table. Everything else it defines is static or
stem-prefixed:

  - func_ bodies and continuations take the stem prefix (fn_sym/cont_sym).
  - psx_dispatch / psx_dispatch_call are renamed in the generated
    preamble rather than at ~20 emit sites; the runtime keeps the
    unprefixed names as forwarders through the active backend, so the
    game's generated C and ~66 runtime call sites are untouched.
  - the image info, kernel-body and native-stub tables become static.
    psx_bios_image and psx_bios_kernel_bodies/_count are now published
    by the runtime from the selected backend, so memory.c and the other
    52 read sites are unchanged (the table is a pointer, indexing is
    identical).
  - g_psx_dispatch_depth moves to the runtime: it is shared dispatch
    state, not per-image, and its duplicate definition was one of the
    collisions.

The generated dispatch now includes psx_bios_backend.h instead of
re-declaring PsxKernelBody / PsxBiosImageInfo / PsxNativeStub locally —
those layouts had to be kept in sync with the runtime by hand, which the
old header comment admitted.

Verified by compiling both images and diffing their exported symbols:
dispatch objects export 3 stem-prefixed symbols each (was 16, 14
shared), full-body objects export 3911 and 13070, and the intersection
in both cases is empty.

Selection policy and the [runtime] openbios setting are not wired yet;
this is the linkability seam only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 11:52:22 -07:00
Matthew Stanley fdfad8f567 cdrom: fail loudly when a requested disc does not mount
Root cause of the "black screen on a .cue that works as a .bin" reports.

cdrom_init() calls iso_open() and, on failure, simply leaves the drive empty
(stat_reg = CDSTAT_SHELL). That is correct for a BIOS-only target, which runs
with no disc on purpose -- but when a game was GIVEN a disc path and the image
does not mount, the game boots against an empty drive and renders nothing. No
error, no log, just black.

The earlier validate_disc_for_launch() pass cannot catch this, because the two
sides read the disc differently: identify_disc() opens the data-track FILE
directly, so it reported a green "Disc verified" badge for cues that the
MOUNTING reader then rejected. Verified-but-unmountable was a silent state.

Demonstrated against the pre-fix reader (a8aa640) on one cue:

    lowercase/unquoted cue -> FAILS  (empty drive -> black screen)
    canonical cue          -> MOUNTS
    bare .bin              -> MOUNTS  <- why picking the .bin "fixed" it

So the symptom was never about cue-vs-bin semantics; it was cue PARSING
strictness. The old mount-side parser required the literal uppercase "BINARY"
and a quoted filename, while the verify side uppercased the line first. The
shared cue parser landed in 68667cd already fixes that class (case-insensitive
keywords, unquoted names, CRLF, case-insensitive payload lookup), which is why
all four titles now boot from either file.

This commit closes the remaining hole -- any OTHER reason a mount fails must
not present as a black screen:

- cdrom_has_disc() exposes the drive state (has_disc() was file-static, so no
  caller could tell a successful mount from an empty drive).
- main() checks it right after cdrom_init() and, when a disc path was supplied
  but nothing mounted, reports what was selected, what it was mounted as, the
  resolver's note, and that the .bin can be selected directly -- then exits
  instead of booting into black.

Also pins the regression: the lowercase/unquoted cue must MOUNT, not merely
parse, so the verify side and the mount side can never silently disagree again.
test_disc_path_resolve: 36 -> 38 checks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 00:52:55 -07:00
Matthew Stanley 5e73b1c18c fix(interp): implement R3000A load-delay value semantics
The dirty-RAM interpreter modelled the load-delay *interlock timing*
(psx_cyc_load_*) but wrote the destination register immediately, so the
instruction in the load-delay slot observed the loaded value. On MIPS-I
there is no interlock: the slot still sees the PREVIOUS value and the
writeback lands one instruction later.

That silently diverged from both real hardware and our own compiled
backend, which defers the writeback past the successor for dependent
pairs (full_function_emitter). Hand-written kernel asm depends on it.

Concrete casualty: OpenBIOS's cardfasttrack.s stashes the card pointer
variable's BASE with `or $at,$k0,$zero` in the delay slot of
`lw $k0,off($k0)`, then writes the advanced pointer back through $at.
With an immediate writeback $at became the pointer itself, the write-back
landed in unrelated memory, and the pointer never advanced -- every byte
of a 128-byte memory-card frame was stored to one address. Cards read as
permanently unformatted (reformatting could not fix it, because the READ
was what was broken) and the send side transmitted one byte 128x,
corrupting the card header to 'M' repeated.

Measured on Tomba! (SCUS-94236) against the card_data_writes ring:
before, one address repeated 124-127x per frame; after, 129 distinct
addresses advancing 1:1 with the protocol index -- matching a retail
SCPH1001 build byte for byte in behaviour. Save-and-load across two
memory-card slots verified in-game.

Implemented as a shim around the decoder so no individual load case
changes. Notes on the semantics:

  - A branch's delay slot is executed nested inside the branch, and is
    N+2 relative to the load, so it must observe the LOADED value. The
    retire therefore happens on entry to the next instruction rather
    than after the previous one returns.
  - Back-to-back loads retire the older writeback before reusing the
    pending slot, otherwise its register write is dropped.
  - Same-register conflict: the load wins (it retires later), matching
    what the compiled backend emits for a dependent pair.
  - Pending state is retired on interpreter exit and before exception
    delivery, where the pipeline would have drained.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 00:11:52 -07:00
Matthew Stanley 68667cd58b disc+bios: either file the user picks resolves, mounts, and verifies
Two path-resolution defects where the runtime silently accepted only one of
two equally-valid spellings of the same input.

1. Disc: picking the .cue threw away the disc's TOC.

normalize_disc_path_for_launch() swapped any .cue for its same-named .bin
unconditionally, and every disc entry point (game.toml, --disc, the cached
disc.cfg, the launcher's Change Disc, the first-run dialog) went through it.
That discarded the cue's table of contents -- every CD-DA track and pregap
with it -- even though iso_reader.cpp has full multi-FILE cue support that
the swap defeated before it was ever reached. In the other direction,
picking a redump audio-track bin mounted that track as the whole disc, which
cannot boot.

resolve_disc_path() (runtime/include/disc_path.h) now canonicalizes any pick:

  .cue that resolves            -> mount the cue (TOC intact)
  .cue with a missing bin       -> the payload it still names, else a
                                   stem-matched sibling image
  .bin/.iso/.img owned by a cue -> mount that cue
  bare image, no cue            -> mount it

identify_disc() resolves through the same code, so the header/serial/CRC32
verdict is read from the data track no matter which member was picked.

While here: the mount path and the verify path had two divergent private cue
parsers (quoted-only + case-sensitive vs quoted-or-bare + case-insensitive),
so they could disagree about the same file. Both now use one parser
(runtime/include/cue_sheet.h), which additionally handles CRLF, backslash
paths, and case-insensitive payload lookup for case-sensitive filesystems.

2. BIOS: the multi-BIOS rename made configs and dump folders incompatible.

SCPH*.toml name "US-PSX-SCPH1001.BIN" while every pre-existing dump folder,
bios.cfg and tools/regen_bios.sh default name "SCPH1001.BIN". Either
combination fataled with "cannot open BIOS file" despite the right ROM being
present. resolve_bios_rom() (recompiler/include/bios_rom_alias.h) matches on
the model token -- the last '-'-separated component of the stem -- so both
conventions resolve in both directions without a hardcoded alias table.
Applied at the config loader, the positional psxrecomp-bios CLI, and each
rung of the runtime's BIOS search.

The positional CLI also derived out_stem from the raw filename, so a
region-qualified ROM would have emitted US-PSX-SCPH1001_full.c -- a name
runtime.cmake cannot link. It now derives the model token.

Also fixes SCPH5552.toml, whose rom field omitted the bios/ directory that
its two siblings have.

Tests: test_disc_path_resolve (36 checks, five dump layouts) and
test_bios_rom_alias (15 checks, both directions + negative cases), both
registered in runtime/CMakeLists.txt. Registered the pre-existing
test_iso_reader_cdda, which was wired into no build.

Verified on real dumps: Ape Escape .cue and .bin both give
SCUS-94423 / NTSC-U / crc=C6F455BC; Tomba 2's multi-track dump gives
crc=1FB8C151 and a 2-track TOC from the .cue, the data bin, and the CD-DA
bin alike.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 22:17:52 -07:00
Alex Vanderveen ab99a6acac delay sync patches 2026-07-24 20:13:26 -04:00