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>
`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>
recomp-ui f6395d6 (upstream commit 39a6e4e, 'mods: make launcher integration
opt-in') gates the Mods view behind RECOMP_UI_ENABLE_MODS, which defaults OFF.
That is the right default for a cross-console launcher -- a console with no mod
system should not show an empty Mods tab -- but it is the wrong default here.
psxrecomp ships mod packages as a first-class documented feature
(docs/MOD_PACKAGES.md); Tomba! ships three catalogs and Ape Escape two. With
the option off, a title that already shipped a working Mods panel silently
loses it on its next rebuild: the panel is compiled in, the catalog is staged
beside the exe, and the view simply never appears. It reads as a stale build
rather than a missing flag, which is how it was found.
Opt in from the framework rather than in each game's CMakeLists, so every
title inherits it instead of six repos each needing the same line. Set before
including recomp_ui.cmake so its option() honours the value, and only when the
caller has not already decided, so -DRECOMP_UI_ENABLE_MODS=OFF still wins.
Verified on ApeEscapeRecomp: a clean configure with no extra flags now yields
RECOMP_UI_ENABLE_MODS:BOOL=ON and the compile definition on every launcher TU;
configuring with -DRECOMP_UI_ENABLE_MODS=OFF still produces none. runtime.cmake
is not in codegen_hash_sources.cmake, so the overlay cache tag is unaffected.
PSXRECOMP_BIOS_STEMS defaults to "OpenBIOS;SCPH1001", so every game link
required a SCPH1001 backend. That backend is compiled from generated C which is
a derivative of a copyrighted Sony BIOS, so it is gitignored and only exists
after a developer regenerates it from their own dump. A checkout that has only
the bundled MIT OpenBIOS -- the documented default, and what a fresh clone gets
-- configured fine and then failed at link with:
undefined reference to `SCPH1001_psx_bios_backend'
Hit while building Tomba 1, Tomba 2 and MMX6 to validate an unrelated branch;
all three needed -DPSXRECOMP_BIOS_STEMS=OpenBIOS as a workaround.
Filter the stem list to those that actually supply a backend, rather than
flipping the default. Flipping it would silently drop SCPH1001 for developers
who HAVE regenerated it; filtering keeps it fully supported when present and
simply absent when not.
Presence of the files is not a sufficient test. A generated/ tree produced
before the backend-descriptor mechanism existed has both <stem>_full.c and
<stem>_dispatch.c yet defines no descriptor -- exactly the local state that
produced the error above. The check therefore probes for the descriptor symbol
itself, in <stem>_dispatch.c (~1MB, where the emitter puts it) rather than the
multi-megabyte <stem>_full.c, so configure stays fast.
Also report what happened instead of leaving it implicit:
- STATUS line naming the linked backends
- STATUS line naming skipped stems with the regen command to fix them
- FATAL_ERROR, with the OpenBIOS regen command, if no backend resolves at all
(previously an empty registry would have surfaced as a confusing link error)
Verified: with default flags and a stale local SCPH1001 tree, Tomba 2 now
reports "BIOS backends skipped ... SCPH1001" / "BIOS backends linked: OpenBIOS",
links, and boots.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements docs/BIOS_SELECTION.md on top of the backend ABI. One build
now carries both images and decides at startup, so the per-BIOS build
flavour (and its three uncross-checked restatements) is gone.
- runtime.cmake takes PSXRECOMP_BIOS_STEMS (default "OpenBIOS;SCPH1001")
and generates psx_bios_registry.c from it, so the stem list is the
single source of truth for what a build carries.
- [runtime] openbios (default true) records whether a title may use the
bundled image. Not exposed to settings.toml: it is a developer's
compatibility finding, not a player preference.
- Selection: an explicit choice that matches a linked image wins;
otherwise the bundled image; otherwise prompt. A mismatched explicit
choice is explained and falls back to OpenBIOS when the title allows
it, instead of dead-ending.
- validate_bios_for_launch now ASKS WHICH backend an image is, rather
than checking against one hardcoded identity, and activates it.
Crucially, "no BIOS" means "the player chose none" — not "no BIOS file
was discoverable". The compile-time DEFAULT_BIOS_PATH is no longer
treated as a choice: it made a stray file near the executable produce a
BIOS Mismatch dialog before the bundled image was considered.
Also fixed while wiring this: the settings.toml BIOS-adoption check read
psx_bios_image.image_bundled, which now runs before any backend is
active (all-zero), so it asks the registry instead.
Verified on Tomba! with ONE binary, both paths:
no --bios -> OPENBIOS, no dialog, boots to in-game save
--bios SCPH1001.BIN-> SCPH-1001 (crc 37157331, bundled=0)
and the per-image state follows the selection, not just the label:
kernel-bless window 0x500-0x4BA0 vs 0x500-0x8500, deliver_event_ret
0x0 vs 0x80001720.
Still outstanding: savestates must record the active BIOS and refuse to
load under another (kernel RAM layout differs); a player-facing way to
clear a remembered choice; retiring PSXRECOMP_BIOS_STEM/_PROFILE, which
now only feed the staleness stamp.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PSX_NETPLAY defaulted ON and auto-discovered a recomp-net checkout from
four candidate paths, so every build linked the delay-sync library. The
lobby WebSocket client was worse: gated on nothing but its source files
existing, i.e. always compiled.
These are single-player titles. Neither belongs in their binaries.
Default PSX_NETPLAY to OFF and gate the lobby client on it too. A
multiplayer title opts in with -DPSX_NETPLAY=ON, which still links
recomp-net and the lobby client exactly as before (verified both
directions on Tomba).
ws2_32 stays unconditional: the TCP debug server needs sockets
regardless of netplay.
psx_netplay.c / psx_lobby_client.c still compile as their existing
no-op stubs rather than being #ifdef'd out — main.cpp has 74 call sites
and they are already written to stub cleanly, so guarding every one of
them would be a large, riskier change for a small object-size win.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
The runtime links generated/<serial>_{full,dispatch}.c, produced by a separate
recompiler step run before the build. Those paths are marked GENERATED so cmake
configure succeeds before that step has run — which meant skipping generation
surfaced only as a raw "cc1: fatal error: <file>: No such file or directory"
deep in the build, with no hint that a step was skipped or what produces the file
(exactly the trap a from-source builder on a fresh checkout hits).
Add runtime/check_generated_sources.cmake and wire it into
psxrecomp_add_runtime_target as a build-time dependency of the runtime target:
a missing game source now aborts at build start with the files named and the
exact fix (build recompiler -> extract EXE -> generate -> rebuild), and configure
emits an early non-fatal warning. Framework-wide, so every game inherits it.
Also fix the docs/BUILDING.md "build and run a game" recompiler invocation (the
framework is a submodule at psxrecomp/, not ../psxrecomp/) and add the
recompiler-build step.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds PCSX-Redux's MIT-licensed OpenBIOS as a second BIOS the framework can
statically recompile, next to retail SCPH1001. Because OpenBIOS is
redistributable, a game verified against it ships its BIOS with the build —
players supply only a disc, and the launcher BIOS picker disappears.
The mechanism is general, not OpenBIOS-specific: every BIOS image gets a
bios/<STEM>.toml profile that is the single source of truth for its identity
and layout.
Recompiler
- bios/<STEM>.toml profiles: [program.image] (sha256 pin, redistributable
flag), [recompiler.address_model] (boot-time ROM->RAM copy windows,
kernel-bless window), install_slots, and [recompiler.runtime_exports]
HLE anchors (0 = feature structurally unavailable on that BIOS).
- BiosAddressModel (new): validated model built from the profile, replacing
~9 hardcoded SCPH1001 window constants that had drifted apart across the
emitter / discovery / game codegen. The emitted C normalize() derives
from the same table. from_config rejects overlapping/misaligned windows
and orderings where sequential folding != first-match. A CTest-registered
equivalence test sweeps every RAM/ROM address against verbatim copies of
the old helpers.
- Orphaned delay slots (the OpenBIOS-critical emitter fixes): jr/jalr
slots are inlined before the jump resolves — previously their side
effect was silently LOST (e.g. a case-return fragment's addiu $sp pop,
which unwound a shifted frame and walked the exception handler's IntRP
cursor into a wild dispatch). Orphaned branches resume at ds+4 on
not-taken instead of re-executing the slot (a read-modify-write slot
doubling twice broke OpenBIOS readPad). Jump targets latch pre-slot.
- MIPS-I load-delay modeling for dependent load pairs: the successor sees
the pre-load value (OpenBIOS's cardfasttrack.s relies on it on purpose —
"gotta break those bad emulators"). Unmodeled shapes warn at build time.
- Discovery hardening: branch-target dispatchability closure to fixpoint;
jump tables placed at jr+8 no longer walked as code; cross-function
continuations routed by an instruction->owner map.
- Signed MULT support (unreachable from the retail seed set; modern-GCC
OpenBIOS emits it freely), latency-modeled like MULTU.
- Seeds from ELF symbol tables + data-section code-pointer harvest
(tools/gen_bios_seeds_from_elf.py) — a source-built BIOS needs no
disassembly pass. 646 OpenBIOS seeds, incl. 5 empirical dispatch-miss
seeds.
- Stemmed outputs (<out_stem>_full.c / _dispatch.c) so two BIOSes coexist
in one out_dir. tools/regen_bios.sh is profile-driven (--config).
Runtime
- psx_bios_image: the generated dispatch now self-describes the BIOS it
was compiled from (identity: size/CRC32/SHA-256/wordsum; kernel-bless
window; HLE anchors; bundled flag). Runtime SCPH1001 hardcodes removed.
- Launch check is a hard identity gate (a mismatched image under
statically recompiled code is a guaranteed wild jump). Bundled builds
hide the BIOS picker and treat a missing/tampered bundled image as an
installation defect.
- Faithfulness fixes surfaced by OpenBIOS's tighter timing loops (all
global, argued in code comments, regression-tested against SCPH1001):
- SIO pad RX is cycle-paced like the memcard path (RX_RDY at
shift-complete, ACK +170); total ack-IRQ latency unchanged.
PSX_SIO_PAD_SYNC_RX=1 restores the legacy synchronous path for A/B.
- SPUSTAT derived from SPUCNT instead of permanently-asserted busy.
- GPUSTAT.13 (interlace field) alternates per vblank while interlaced,
pinned 0 in progressive.
- CD Init(0x0A) completes in ~3.9 ms instead of borrowing Beetle's
34 ms Reset figure (OpenBIOS's short bounded retry loop livelocked).
- Exception EPC guard accepts ROM-window block leaders, not just RAM.
- New TCP debug commands (documented in TCP_COMMANDS.md): bios_info,
s3_smear_watch, callret_watch; write-trace ring gains $s0-$s5.
bios/openbios.bin is vendored (MIT — notice in bios/OpenBIOS.LICENSE,
attribution in THIRD_PARTY_ATTRIBUTION.md, source pin + build recipe in
bios/OpenBIOS.toml). The emitter fingerprint digest is now content-only:
existing generated/*.emitter.sha will read STALE once; re-run
tools/regen_bios.sh.
Discover recomp-ui from CMAKE_SOURCE_DIR/recomp-ui (or RECOMP_UI_ROOT).
Keep lib/recomp-net in the engine.
Co-authored-by: Cursor <cursoragent@cursor.com>
Remove the in-tree launcher implementation and its submodules now that game projects use recomp-ui for launcher support.
Keep Vulkan hidden from the launcher unless a game explicitly sets [video] offer_vulkan = true, and clamp stale Vulkan settings back to OpenGL when it is not offered.
Co-authored-by: Martin Penkava <mpenkava1337@gmail.com>
Audit fixes for the async shard-publication pipeline (45a2dda):
- autocompile_shutdown(): stops the pipeline, kills the compiler tree via a
kill-on-close job object (assigned while the child is SUSPENDED so cmd.exe
cannot spawn grandchildren outside it), and JOINS the watcher + preparer —
never abandoning a thread inside the Windows loader lock at ExitProcess.
Wired first into shutdown_runtime(). Crash exits reap the tree via the job.
- Failed off-thread prepares retry (3 attempts, 100 ms) on the worker before
the artifact silently reverts to a synchronous emu-thread first-map; retry
and giveup counters exposed in autocompile_status_json.
- Capacity caps fail closed and LOUD: cand_register shouts once at CAND_CAP;
load_one_dll_prepared refuses at MAX_LOADED_DLLS before registering, so
dll_already_loaded can never lose track and double-register on rescan.
- Publication confinement is filesystem-resolved: published_path_valid
canonicalizes the cache root and the pipe path (GetFinalPathNameByHandle /
realpath), prefix-checks canonical forms, and the worker maps the canonical
string while holding a no-delete-share guard handle (TOCTOU). `..`/junction
spellings that passed the lexical check are now rejected — proven by the
new overlay_path_canon_test boundary test.
- test_autocompile_publication gains a shutdown-mid-run scenario asserting
prepared == committed + discarded with nothing queued; perf guards updated
to require validate -> guard-handle -> canonical first-map ordering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Emit a shared <stem>_decls.h (runtime extern prologue, static-inline
unaligned helpers, all func + alias-body forward decls) and distribute
function bodies across N ~40k-line <stem>_full_NN.c shards instead of one
monolithic <stem>_full.c. Overlapping-alias host bodies become externally
linked so shard bucketing has no atomicity constraint. runtime.cmake's
GAME_GENERATED_FULL_C is now multi-value (single path still works, so
other games are unaffected until they regen + glob).
Tomba: SCUS_942.36_full.c 41MB single TU -> 36 shards; compile (-O3)
4m35s -> 51s (-j16, ~5.4x). Generated function bytes verified byte-identical
(6245/6245 functions).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tier mis-compiled and has been hard-gated off since 2026-06-25; overlay
gaps fall to gcc/tcc shards and the dirty-RAM interpreter, unchanged.
Re-do on master of the removal validated on the stale branch (6da8c02),
reconciled with everything master grew since the fork:
- Delete overlay_sljit.{c,h}, overlay_compile_worker.{c,h}, lib/sljit,
runtime/tests/sljit_*.c, and the runtime.cmake sources/include-dirs.
- Strip the sljit provider/backend from code_provider.{c,h},
overlay_backend.{c,h} ("sljit" in overlay_backend/PSX_OVERLAY_BACKEND now
degrades to auto so stale configs still boot), main.cpp, debug_server.c
(sljit_status/sljit_async/sljit_try TCP commands), overlay_loader.{c,h}
(live policy, JIT-on-miss, persisted-shard cache, async worker glue,
psx_sljit_call/cop2/memx host helpers — reachable only via the removed
cpu->sljit_helpers table), and cpu_state.h (SLJIT_HLP_* enum + the
sljit_helpers[] member; CPUState layout change reshards via the cg tag).
- Master-only entanglement resolved before deletion: 28165d6's Einhander WS
player-X bound emit existed in three paths (gcc code_generator, sljit,
dirty_ram_interp). The gcc shard link contract now forwards
psx_ws_player_x_bound (ABI v13, previous commit), so deleting the sljit
emit path loses no widescreen behavior; ffc6688's game-dispatch block
inside the dead psx_sljit_call helper goes with it.
- Reword comments that described sljit as a live backend (gpu.{c,h},
ws_*_detect.h, cosim_state.c, dirty_ram_interp.c, autocompile.h,
boot_state.h, config_loader.h, compile_overlays.py, and the CPS marker
comments the recompiler emits).
recompiler psxrecomp-game builds clean; game runtimes rebuilt + boot-verified
separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port Douglas Vanderveer's manifest-driven macOS/Linux overlay discovery onto the current cache index and exact-dispatch loader. Validate both filename key fields, preserve ABI cleanup and rescan idempotence, and cover the POSIX primitives with a real shared-library fixture.
Co-authored-by: douglasjv <vanderveerdouglas@gmail.com>
Triage of Tomba2Recomp GH issue #1 exposed three ways a release exe
behaves differently on the build machine than on every user machine:
- DEFAULT_BIOS_PATH baked an ABSOLUTE build-tree path into the binary,
so promoted/release exes silently load the builder's BIOS wherever
that path exists and the clean-install picker flow is never
exercised where releases are validated. runtime.cmake now refuses
absolute values (loud configure warning, converts to the relative
default); dev checkouts keep prompt-free boots via a new
resolve_bios_path rung that walks exe-dir ancestors for
psxrecomp-v4/<relative>, a layout only dev trees have.
- A settings.toml with a TOML syntax error was silently discarded
wholesale (user's renderer choice, BIOS/disc paths - everything),
and the next launcher save then overwrote the file with defaults.
load_user_settings now reports parse_error; the runtime preserves
the broken file as settings.toml.bad and warns via message box +
stderr.
- package_release.ps1 gains guards: fail packaging if the staged exe
contains a drive-letter absolute SCPH1001.BIN path, if the stage
carries BIOS/disc/save/sidecar files (SCPH*.BIN, *.cue, *.iso,
*.mcd, bios.cfg, disc.cfg, settings.toml, keybinds.ini,
overlay_captures.json), or if saves/ is non-empty.
Verified: rebuilt Tomba2 against this tree - configure warning fires
on the absolute arg, strings scan of the exe finds no drive-letter
BIOS path, and a boot with no [bios] setting and no bios.cfg resolves
the BIOS through the psxrecomp-v4 dev rung.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stb launcher revamp (8b098bd, ae89a8f, 89e0e0f) uses modern GL 3.0+
without the SDL_GL_GetProcAddress loader ('Phase 2b'), so it fails to
link into game builds, and its rendered UI is not acceptable (user
review 2026-07-10). Revert master to the working RmlUi launcher — the
same revert every recent release (Ape v0.0.2, Tomba2 v0.0.2) already
shipped with. The stb work remains reachable via the original commits
if a Phase-2b GL-loader follow-up ever revives it.
(cherry picked from release/ape-v0.0.2-rmlui 929b78b)
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.
The recompiled game is a huge (~270 MB) block of generated C. With no
CMAKE_BUILD_TYPE the compiler emits it at -O0 and the game runs at a fraction of
full speed (terrible framerate). A naive `cmake -B build` — including the command
in game READMEs — must not silently produce that, so default CMAKE_BUILD_TYPE to
Release for single-config generators when the user hasn't chosen one. Overridable
with -DCMAKE_BUILD_TYPE=...; no-op for multi-config generators.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Implements the framework from docs/STRING_TRANSLATION.md: an always-on
capture inventory + a draw-time apply hook, both hung off the psx_dispatch
chokepoint via fntrace_record (no generated-code edits, no BIOS regen,
works in Release).
- text_xlate.{h,cpp}: EncodingProfile abstraction (shift_jis profile:
SJIS record reader with NUL/0xFFFF terminators + halfwidth-kana + control
framing; UTF-8 -> fullwidth-SJIS transcoder so English renders with the
font's existing fullwidth Latin, zero glyph injection). Capture inventory
(FNV-1a64 keyed) queryable over TCP; apply repoints the string-arg reg at
guest-stack scratch (KV-gated: only known records are swapped, stray
pointers untouched; own scratch is skipped to avoid recapture).
- fntrace.c: call text_xlate_on_dispatch() at the dispatch chokepoint.
- config_loader: [localization].language (+ [runtime].language alias);
env PSX_LANG overrides. Loads translations/*.toml per project.
- main.cpp: text_xlate_init() at game-config load.
- debug_server: "xlate" command (stats|dump|todo|reload) — observability
over TCP, no log files.
Verified: builds (Release), Tsumu boots headless, capture live
(distinct records accumulating), apply disabled until a table is present.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Root cause of MMX6 "A game data could not be found" (weeks-old, cross-backend):
the game's save-scan calls memset through the kernel A0 jump table; entry
A0:2B points into BIOS ROM at 0xBFC02B8C (one of the entries the kernel never
patches into RAM). Transitive discovery can never find it (reachable only via
the data table, no jal), so the dispatch missed and psx_unknown_dispatch
silently no-op'd — killing the scan before firstfile ever ran. Every card
layer (SIO, IRQs, SwCARD events, sector data, kernel directory parse) was
verified correct along the way; the sole defect was the undiscovered ROM
function plus the silent miss.
- recompiler/seeds: enumerate the post-init A0/B0/C0 tables (kernel RAM
0x200/0x874/0x674) and seed every ROM-pointing entry with no dispatch
coverage — 30 functions (memset, memcmp, memmove, strncat, rand-family,
card/dev services...). memset was merely the one MMX6 called; the rest were
identical latent bugs. BIOS regen: 4406 -> 4439 dispatch entries.
MMX6 CONTINUE now lists and loads saves (user-validated + scripted run:
full FILE SELECT, 1093 card txns, unknown_dispatch_log total=0).
- traps.c: an unresolvable dispatch target now dumps a crash report and halts
BY DEFAULT (PSX_FAIL_FAST_UNKNOWN_DISPATCH=0 opts out for diagnostic
sessions; the miss is ring-recorded either way). The old silent-no-op
default left stale registers and a severed call chain, and masked this bug
for weeks. Deliberately NO interp fallback: the dirty-RAM interpreter is
for runtime-written code (Rule 18); interpreting around a static-discovery
hole would hide it exactly like the no-op did.
- runtime.cmake: EXE_NAME option — per-game process names (mmx6-runtime.exe)
so one game's `taskkill /IM psx-runtime.exe` dev sweep no longer kills
every other game's instance.
- debug rings (the tooling that found this): HLE call ring gains
consecutive-duplicate collapse w/ repeat counts (a 4-way TestEvent poll at
175K calls/s no longer flushes the ring in 0.1s), per-entry TCB + in_exc
(thread attribution), a "since" paging param and 8MB dump buffer;
card-trace EvCB watch widened to all 7 card event slots (slot 0 =
SwCARD-IOE, the success event, was previously invisible).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
USER PIVOT 2026-07-02 (CLAUDE.md §0 amendment included): psxrecomp now
carries an opt-in High-Level Emulation backend alongside LLE. LLE (the
recompiled BIOS) stays the default, the reference implementation, and the
correctness/timing oracle; every service HLE does not implement falls
through to LLE, so HLE is never load-bearing beyond what it covers.
Mechanism: full_function_emitter.cpp emits a null-by-default hook
g_psx_bios_hle_hook consulted at the top of every psx_dispatch_impl
iteration, on the pre-normalize physical address, BEFORE any backend
(game image / dirty-RAM / static table) claims the target. A handled
call resumes at $ra through the trampoline's existing return/tail
contract. NULL (default) = pure LLE, dispatch-identical. Regenerating
BIOS+game images is required (emitter change).
v1 call-HLE (runtime/src/bios_hle.c): the B0 event family — DeliverEvent
(incl. callback dispatch with the kernel-true $ra 0x1720), OpenEvent,
CloseEvent, TestEvent, EnableEvent, DisableEvent — each ground-truthed
instruction-by-instruction against the SCPH1001 kernel disassembly
(0xBFC11644..0xBFC11A84) and operating on the REAL guest EvCB table
([0x120]/[0x124], stride 0x1C). WaitEvent/threads/pads/card/A0/C0 fall
through to LLE. Cycle charges are documented approximations; LLE remains
the timing oracle. No-stubs stands.
Boot HLE: with the tier on (and no keep-intro opt-out) the hook one-shot
intercepts the first dispatch of the BIOS shell entry (RAM 0x30000 —
LoadRunShell's indirect call always routes through the dispatcher), so
BIOS Main() proceeds straight to SYSTEM.CNF + game EXE load with kernel
state built entirely by the real recompiled kernel init, unpaced (boot
turbo) until game handoff. This REPLACES the fast_boot snapshot
capture/restore; fast_boot=true now aliases the boot-skip alone.
Selection: [runtime] bios_hle / bios_hle_keep_intro (+ settings.toml
bios_hle mirror), PSX_BIOS_HLE / PSX_BIOS_HLE_KEEP_INTRO env overrides,
startup banner bios_backend=/bios_boot=. The PSX_HLE_SCHEDULER spike is
folded in as the tier's standing subsystem replacement: default from
[runtime] hle_scheduler via psx_hle_scheduler_set_default, env wins.
Observability: always-on 16K-entry HLE ring (route LLE-fallthrough /
HLE / boot-skip per record) + hle_dump TCP command (TCP_COMMANDS.md).
VALIDATED on Tomba (build-wt): LLE pass byte-identical behavior (real
intro, card check, save/load OK); HLE pass boots shell-less, TestEvent/
DeliverEvent route HLE, ChangeTh/ReturnFromException/OpenTh/CloseTh and
the card stack fall through to LLE, save/load OK (user-driven).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the Tomba pause-menu wedge (loaded saves only): autocompiled
overlay shards were emitted by a recompiler binary OLDER than the last emitter
change, yet stamped with the CURRENT cg tag (compile_overlays derives the tag
from --runtime-include; nothing tied it to the emitting binary). The stale
shards corrupted the display-list task queue during load-game. NOT the
IRQ-resume class the prior handoff claimed - disproven with the new ring.
- recompiler: bake the emitter-source hash (shared canonical list
runtime/codegen_hash_sources.cmake + hash_codegen.cmake) into psxrecomp-game;
new --codegen-hash flag prints it (machine-parseable, pre-banner).
- compile_overlays.py: HARD-FAIL when the recompiler binary's baked hash
differs from the cg tag hash it stamps (or the binary lacks the flag).
Applies to both cache and --static modes. Closes the read==write-tag /
stale-content drift class structurally.
- runtime.cmake: codegen-hash source list moved to the shared canonical file.
- interrupts.c: always-on exception-EXIT half added to irqctx_ring (take_pc,
real_epc, exit_pc, escape reason, same-thread/restore flags, v1/ra
saved-vs-exit, RestoreState redirect count) - proves per delivery which
escape path fired and whether the interrupted GPRs were restored. Kept the
same_thread_resume GPR-restore refinement (ring-verified equal-value in
practice; faithful: hardware restores all GPRs on every exception return).
- debug_server.c: irqctx_ring dump extended with the exit-half fields.
Validated live: Tomba menu opens on loaded save (Stormy Mountain) with the
cache fully enabled after regenerating shards with a rebuilt recompiler
(build-cosim + build-prod caches). Guard verified positive and negative.
MMX6 gate passed: boot -> title -> opening cutscene -> gameplay handoff OK.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>