An online room this build hosts asks the lobby server for transport "host"
(match_caps.relay): the host carries the match on its own UDP port and
guests dial it; the server's relay (SFU) is the fallback when a guest's
probe fails (recomp-net-server WS_LOBBY.md "Host relay"). The mechanics are
recomp-net's recomp_net/host_relay.h, pinned here (lib/recomp-net b0751b7);
this engine's own lobby-client copy gets the same glue as rnet_lobby_client:
the cap (emitted only when asked), the launch's transport ->
PsxLobbyJoinInfo.transport_host (force_input_relay 0; the host accepts
first / hubs instead of dialling the guest's LAN address), each seat's
published path / path_fresh, host_relay_step() from psx_lobby_pump, and
unmap on leave / disconnect.
psx_netplay: transport_host takes the LAN / hub path instead of the "MotK
online needs the SFU" rule. main.cpp: relay_host_get/set + relay_status
for recomp-ui (Network Settings / MATCH SETTINGS / ROOM "Match Relay"),
transport_host on the launch and into PsxNetplayConfig, and the
host==guest-advertise SFU inference skipped on a host launch; all under
RECOMP_LAUNCHER_HAS_HOST_RELAY so older recomp-ui pins still build.
Compile-checked against workspace recomp-ui 58604b0 (MOTK scratch
configure: full build and link).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The hub holds the lobby seat; the game settles everything itself
(retcomm-launcher docs/NETPLAY_HANDOFF.md):
- psx_lobby_ingest_record replays the hub's seat/room/launch messages
through handle_server_json, so fill_launch answers as in-game.
- --netplay-query answers what this build's lobby would send: disc_fp,
set_ready (BIOS offer), start (default caps; settled from a room).
- set_ready/start builders and the host start-caps settle are factored out
unchanged and shared by in-game PLAY and the query.
- A handed-off match ends the process (the hub holds the room).
Two CTR processes from records, local relay: all 80 live digests equal.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_window_fullscreen.cpp is a separate test program with its own main(). It includes SDL_main.h and is built with SDL_MAIN_HANDLED. The single-owner guard sdl3_main_single_include counts every file under runtime/tests, so it has failed since that test was added.
Name the file as a stand-alone program. Every other file under runtime/src, runtime/include and runtime/tests is still held to the one-owner rule.
The v3.0 Japanese (SCPH-5500, 09/09/96) and US (SCPH-5501, 11/18/96)
retail BIOS images join SCPH-1001, SCPH-101 and SCPH-5552, so every v3.0
region (J, A, E) has a profile.
Both images match SCPH-1001 v2.2 byte for byte in the kernel,
ROM [0x80, 0x18000). SCPH-5501's reset vector is identical too; SCPH-5500's
differs in 24 bytes. The shell differs in both. So each profile is
SCPH1001.toml with its own identity (name, id, rom, sha256 pin, stem): the
copy windows and install slots are kernel properties and carry over
unchanged. The shell runs through the dirty-RAM interpreter, as for
SCPH5552.
Each image gets its own seed list, derived by the same rule as
phase2_ghidra_seeds_SCPH5552.json: the SCPH-1001 corpus filtered to seeds
whose 64-byte ROM window is identical in both images (534 kept, 569
dropped), plus the two architectural vectors and 6 A0/B0/C0 kernel-table
targets. The files carry only addresses, labels and rationales, plus the
SHA-256 of both ROMs.
- psx_bios_known_images.h: CRC-32 FF3EEB8C (SCPH-5500) and 8D8CB7E4
(SCPH-5501), both 512 KiB. A build that does not link these backends
builds one from the player's own dump as a loadable module, like
SCPH-5552, and the netplay lobby can name the image.
- main_cli.cpp: map SCPH5500 / SCPH5501 dumps to their profiles.
- build_cli.py: package both profiles with the CLI framework.
- docs/BIOS_SELECTION.md: list them among the module-built images.
Preserve current segmented dispatch, active-plan mod hooks and software scale limits. Extend function filters through the active plan, retain the last-store pointer with overlay ABI 25, and keep default config fingerprints stable. Refresh BIOS emitter stamps after byte-identical regeneration and reconcile regression fixtures.
The windows-2022 release job accepted the committed backends in the step
shell (Git Bash) and refused them at configure: runtime.cmake ran the bash
fingerprint through whichever bash find_program() located, and that
environment hashed a byte-identical tree differently. The fingerprint is now
tools/bios_emitter_fingerprint.cmake, run with the configuring cmake itself
on every platform; the shell script is a thin wrapper around it. Same digest
shape as before, so existing stamps stay valid (verified on an LF tree and
an autocrlf clone).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Decision (Alex, 2026-09-30): the recompiled BIOS carries the same risk as
the recompiled game, which titles already commit, so the retail backend is
compiled in rather than built on the player's machine. The retail image is
still never shipped: the runtime needs its data at run time and accepts only
the exact image the backend was built from, so the player supplies their
own dump as before.
- generated/{OpenBIOS,SCPH1001}_{full,dispatch}.c and their .emitter.sha
stamps are tracked (.gitignore un-ignores exactly those); regenerated with
the current emitter.
- bios_emitter_fingerprint.sh no longer hashes the ROM image: the profile's
SHA-256 pin stands for it and the emitter refuses any other image, and CI
has no dump, so hashing it made every CI configure read "stale".
- runtime.cmake: the staleness check covers every LINKED stem and
-DPSXRECOMP_BIOS_STALE_FATAL=ON makes a stale stamp fail configure.
- game-release.yml: no CI-side OpenBIOS emission (generate_openbios.sh is
gone); configure with BIOS_STALE_FATAL and assert both backends linked.
check_generated.sh asserts the submodule carries both backends tracked
and no dump is tracked anywhere.
- The player-side module build stays for a known image a build does not
link (SCPH-5552).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A buildable dump used to verify as usable and then block at Play behind a
modal while the module compiled. Now:
- ae_bios_verify reports a buildable, not-yet-cached dump as needs_regen,
and the runtime wires recomp-ui's new bios_prepare_with_progress job
(ae_bios_prepare) whenever the toolchain is present and no Generate host
is: the launcher runs the build as a progress job from the wizard's
"Prepare BIOS" button or the Play prompt, re-verifies, and resumes the
wizard. Generate & rebuild is never offered for a BIOS in a bundled build.
- psx_bios_module.c gains a progress sink fed from the builder's output
lines (dump / emit / compile / self-check / published map to stage
percentages with readable messages); run_command streams lines on both
platforms.
- The launch-time build stays as the fallback for --bios / --no-launcher.
Requires recomp-ui 9f86c49 (GameInfo.bios_prepare_with_progress).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bundled releases link OpenBIOS only; retail-BIOS-derived C never ships.
"Bring your own BIOS" now works without it: when a dump matching a shipped
profile (psx_bios_known_images.h) is selected, the runtime builds that
backend on the player's machine with the overlay toolchain the release
already carries, caches it, and loads it as one more registered backend.
- runtime/include/psx_bios_module.h: the module contract. An overlay-style
self-contained shared library (no imports from the executable) exporting
psx_bios_module_{abi,codegen_hash,init,backend}; reaches the runtime only
through OverlayCallbacks plus a new PsxBiosModuleCallbacks table (traps,
dirty-RAM/kernel-bless probes, fntrace, log_probe) and pointer aliases of
the globals the generated dispatcher touches. Its #defines rewrite the
emitter's own extern lines, so the generated C compiles unchanged.
- runtime/include/psx_bios_module_glue.c.inc: module-side forwarders and the
four exports, compiled after overlay_dispatch_preamble.c.inc.
- runtime/src/psx_bios_module.c: host side. Cache path
<exe>/cache/bios/<os-arch>/bm<abi>_<codegen hash>_f<flavor>/<STEM>_<crc>,
ABI + codegen-hash gated load, callback fill, and a blocking build via
overlay_toolchain/bios_module_build.py (gcc via compiler.txt or PATH,
else tcc), then psx_bios_register().
- psx_bios_backend.c: the registry is now a mutable table seeded from the
CMake-generated psx_bios_builtin_registry by a constructor, with
psx_bios_register(); every existing walker keeps working.
- main.cpp: validate_bios_for_launch acquires a module (announcing a first
build) before declaring a mismatch; discovery accepts a buildable dump;
the launcher verify reports it usable ("ready" / "compiled on first
launch") with no Generate & rebuild; the BIOS row shows whenever the
toolchain is present; bundled_only respects module support.
- emitter: the CPS-marker constructor is not emitted in a module build.
- overlay_api.h: OverlayCallbacks gains a struct tag;
overlay_loader_callbacks() exposes the host table before loader init.
- tools/bios_module_build.py (ships in the toolchain): identity + profile
SHA-256 gate, staged emitter tree, emit, three-TU compile, ctypes
self-check, atomic publish. release_stage.py stages psxrecomp-bios, every
bios/*.toml, the seeds and the builder into overlay_toolchain/.
- Tests: runtime/tests/test_bios_module_host.c against three fake modules
(good / wrong ABI / wrong hash) on real dlopen; recompiler/tests/
test_bios_module_build.py builds the bundled OpenBIOS end to end and reads
the descriptor back; guards extended.
Verified on Twisted Metal 4 (bundled build + staged toolchain, headless):
first launch with --bios SCPH1001.BIN builds and loads the module and boots
on image=SCPH-1001; the second launch loads it from cache.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A shipped bundled build links OpenBIOS only and carries no CLI, sources
or toolchain, yet the codegen host forced the BIOS row on and a retail
pick answered with needs_regen ("Generate & rebuild to switch"), a step
that cannot run there.
- The BIOS row is shown only when a choice can mean something: a retail
backend is linked, nothing is linked yet (setup host), or the codegen
host actually wired Generate (prepare_with_progress). main.cpp decides
after host_apply instead of the host forcing has_bios.
- ae_bios_verify short-circuits a bundled-only build with a plain
"runs its bundled OpenBIOS only" message before the identity checks,
whose "this build expects SCPH-1001" wording names the pinned stem,
not a linked backend, and sets needs_regen only when Generate exists.
- test_bios_selection_guards pins both.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Retire the setup-host release model (CI wiped generated/ and shipped
emitters + sources + a wizard so every player regenerated and rebuilt the
title). Titles now commit generated/ and CI ships the compiled game.
- docs/ci/templates/game-release.yml replaces setup-release.yml: gate on
committed game C (tools/ci/check_generated.sh), emit the OpenBIOS backend
in CI (tools/ci/generate_openbios.sh), configure with
PSXRECOMP_REQUIRE_GAME_C=ON and assert the full path was taken, package
with tools/package_game_release.sh, verify the zip is a game not a kit.
- tools/package_game_release.sh: exe, assets, bundled OpenBIOS, configs, mod
catalog + overlay toolchain via release_stage.py, notices; refuses sources,
emitters, generated C, BIOS dumps. clear_generated.sh removed;
package_setup_host.sh marked retired.
- Scaffold: gitignore templates track generated/, package_release.sh.in
wrapper, setup_project.{sh,ps1} commit generated/ after Generate.
- Project Studio: audit flags ignored/untracked generated/, retail BIOS C
and setup-host workflows; merge_gitignore un-ignores generated/; ops emit
the bundled packager + workflow; generate_ci uses the new template.
- Docs: docs/ci/BUNDLED_RELEASES.md replaces HOST_ONLY_RELEASES.md.
- Tests: tools/tests/test_bundled_release_layout.py; fix pre-existing
false positives in the packager-surface and overlay-static tests.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With the HLE scheduler (the default), ChangeThread to the thread that is
already running returns 1 from psx_request_thread_switch. psx_syscall case 3
then returned 0 with cpu->pc still holding the SYSCALL's own address.
Compiled callers fall through on 0, but the dirty-RAM interpreter transfers
whenever cpu->pc is non-zero, so it re-executed the SYSCALL forever.
A game that yields from its main thread this way hung at the kernel's
SYSCALL (EPC 0x650) as soon as that kernel code ran interpreted, for
example when its body did not match the compiled kernel.
Clear cpu->pc on that continuation, as the SYS01/02 continuations do. The
guest-exception syscall path returns before case 3 and is unchanged.
syscall_change_thread_self links the production traps.c into a fixture
with a one-thread TCB table at -O0 and -O2. fixture_link.py stubs every
other runtime seam with abort(), so an unexpected call fails the test.
find_program() searched for both ccache and ccache.exe on every host.
Under WSL the Windows PATH is appended to the Linux one, so a Windows
ccache.exe (for example from WinLibs) was found first and set as the
compiler launcher. Every compile then failed to exec the Linux compiler.
Search for ccache.exe only when CMAKE_HOST_WIN32 is true. Native Windows,
Linux and macOS hosts find the same program as before.
0f1fd021a gave chat_push message-id and account parameters. The fixture
still called the four-argument form, so lobby_chat_ring_test stopped
compiling in any tree with the netplay submodules checked out. Pass NULL
for both; the ring behavior under test does not depend on them.
A netplay match that ends early returns both players to the lobby. The
runtime knew why (netplay_soft_exit's origin) but only printed it to
stdout, so players saw the lobby again with no reason. A boot-digest
mismatch (peers booted different BIOS images or boot settings) first
held a black window for the full 20 s admit-stall watchdog.
netplay_soft_exit now records a sentence for its origin, and the soft
return to the lobby puts it on the launcher's status line through
last_error (the room and browser views already show it). The dig0 gate
timestamps a mismatch; once the same pair of digests has held for 3 s,
the watchdog ends the match with its own reason. A peer that gets the
other side's BYE while it sees the same mismatch reports the mismatch,
not "the other player left". A CLI match prints the sentence to stderr.
The origin-to-text map and the grace decision live in
netplay_exit_reason.c, covered by netplay_exit_reason_test. Builds on
the previous commit's psx_lobby_set_last_error.