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>
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>
recompiler/CMakeLists.txt defined both `psxrecomp` (the user-facing CLI,
src/main_cli.cpp) and `PSXRecomp` (a legacy duplicate of psxrecomp-game, built
from the identical PSXRECOMP_GAME_SOURCES list). CMake treats those as distinct
targets, but Windows and macOS filesystems are case-insensitive, so both wrote
the same file and the last one linked won.
The CLI lost. After a clean build, psxrecomp.exe was the game recompiler,
answering `--config <game.toml>` instead of the documented
`build --disc ... --bios ... --output ...`. tools/build_cli.py packages that
binary into psxrecomp-cli-windows-x86_64.zip, and README's first instruction to
a new user is to download and run it, so the primary onboarding path handed
people a program matching no documentation. Invisible on Linux, which is why it
survived.
Remove the legacy target (psxrecomp-game already covers those sources) and add a
configure-time guard so it cannot recur: the guard compares OUTPUT_NAME
case-insensitively across a directory's targets and fails with an actionable
message. A rename alone would not have prevented recurrence.
The PSXRecomp.exe named in README's Release Package section is a different
artifact, the BIOS-only runtime named via OUTPUT_NAME in runtime/runtime.cmake,
and is unaffected.
Verified: clean build now yields psxrecomp.exe (791KB) printing the documented
`build --disc` interface; no case-colliding pair remains; guard confirmed to
fire by temporarily reintroducing a collision.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Preserve wider guest staging and composition regions while extending framebuffer draw bounds for native-wide margins. Regression-tested in 16:9 on Tomba and Mega Man X6 with bundled OpenBIOS.
Stretch explicitly configured backdrop X-store sites across the native-wide surface while leaving 4:3, FMV, menu, and unconfigured games unchanged.\n\nValidated by the contributor on Xenogears Battle Mountain and regression-tested on Tomba's Watch Tower flower backdrop with bundled OpenBIOS.
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>
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>
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>
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>
Regression from the load-delay commit: Tomba! 2 wedged at boot on a black
screen (steady ~62fps, so running but rendering nothing).
`lwl rt,x` immediately followed by `lwr rt,y` is THE unaligned-load idiom
every MIPS compiler emits, and MIPS-I explicitly permits the pair
back-to-back -- the second half merges with the first's result, not with
the pre-load register value. Deferring the writeback past the slot made
the merge read a stale rt, silently corrupting every unaligned load that
went through the interpreter.
Retire the pending write early when the delay-slot instruction is
LWL/LWR targeting the same register. That is forwarding, matching the
hardware special case, not a shortcut around the delay slot.
Bisected against Tomba! 2's pinned framework commit (68667cd), which
boots to gameplay: same fresh worktree and cold overlay cache, so the
overlay-cache difference was ruled out as the cause. Reverting only the
load-delay commit on this branch also booted, which isolated it here.
Verified after the fix:
- Tomba! 2 (SCUS-94454) boots to gameplay again.
- Tomba! (SCUS-94236) on OpenBIOS still shows the memory-card pointer
advancing 1:1 (129 distinct addresses per frame, not 2), so the
original card fix is intact, and still reaches its title screen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
Two observability gaps hit while debugging the memory-card corruption
above; both are serializer-side, the data was already recorded.
crash_trace: the report carried cpu->pc but not the dirty-RAM
interpreter's unsupported-instruction block. A run that died there
showed only pc == 0, and ten separate sites set that, so the cause was
unattributable. Emit interp_unsupported{pc,insn,reason,block_entry,
entry_ra,entry_sp,insns_into_block,midblock_count}.
dirty_ram_interp log: the per-instruction ring captured v0/v1/a0-a3/
t0-t2/sp/ra but not $at or $k0/$k1 -- precisely the registers that
matter for BIOS exception-handler code, which is most of what this
interpreter runs, and the ones the cardfasttrack.s load-delay idiom
turns on.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
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>
On macOS/Linux there is no native GUI file picker, and in headless mode a
picker would block, so pick_runtime_file() bails when it can't resolve the
BIOS/disc interactively. The old message ("<title> requires a command-line
path on this platform") never said WHICH flag to pass, so a first-time
from-source user on Linux hit "no BIOS selected; exiting." with no idea what
to do.
Thread the responsible flag (--bios / --disc) into pick_runtime_file and print
an actionable message with the flag and a full example command, on both the
headless and the no-GUI-picker paths. Points at the game README's "Building
From Source" for the full run line.
Co-Authored-By: Claude Opus 4.8 (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>