When `PSX_NETPLAY` is turned off, the build fails because `ae_np_account_sync` is referenced without being declared in that configuration. This issue caused automated continuous integration pipelines and local builds to fail unless netplay was explicitly forced on.
Wrapping this function call ensures that the runtime compiles cleanly for projects that do not require netplay support, restoring the ability to build with netplay disabled.
Some titles and enhancement mods/plugins might require display level gamma correction to improve shadow detail and overall visibility without altering guest rendering state. This introduces a public gamma adjustment interface for the OpenGL renderer via `gl_renderer_set_post_gamma`, that enables external plugins to adjust the display curve cleanly at runtime.
This adjustment applies exclusively to game content and temporal interpolation passes while leaving host interface elements, bezels, and previously composed frames untouched. Modifying the gamma setting immediately invalidates the presentation buffer so that changes become visible on screen without requiring new video memory writes from the guest system.
The toggle lives in the Create Lobby modal, before any room exists, and
its value goes out with `create`. The setter refused unless the client
was already seated in a server lobby, so the modal read the unchanged
preference back every frame and the checkbox snapped off. Refuse only in
a LAN / Direct-IP room, which has no gallery to allow -- the SNES host's
rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPXfGGhBBBxQPcEzaMsauF
Brings the PSX runtime level with what recomp-ui's netplay pages now ask a
host for, and with the SNES runner that already answers them
(snes_lobby_client.c / snes_host_lobby.c):
- Chat lines, seat rows and players-online rows carry the server's opaque
account key, so the launcher's ignore/block list survives a rename.
- psx_lobby_set_blocks pushes that list to the server as `set_blocks`, since
refusing to pair or seat the two together is the server's job; recomp-ui
re-sends it on every reconnect.
- list_scope_set: a LAN player no longer sees online rooms they have no
connection for, and an online player no longer sees their own LAN rows.
- Automatch: rulesets discovery, the relay latency probe (14-byte LE frame,
type 200 -> 201, nonce-matched), queue with the disc fingerprint, the
accept gate with a locally counted deadline, requeue/cancel/cooldown, and
claiming automatch's `error` codes only while a ticket is in flight.
Leaving an automatch room after the match: it is the server's room, has
no host to rematch with, and holding the seat blocks the next ticket.
- Account: init the auth client once per URL rather than every pump (the
login worker reads the host), anchor the device key to the executable
directory, and publish the signed-in handle as the lobby display name.
mods_enabled is asserted false on PSX: every netplay launch clears the mod
plan or refuses to start, and the caps a match runs are the ruleset's.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPXfGGhBBBxQPcEzaMsauF
PsxLobbyChatMsg gains the server's `mid` for a line, taken off `chat` and
`server_chat`, and psx_lobby_report_chat hands a list of them to
rnet_chat_report_build. The frame carries ids and never the text, so a
report cannot fabricate what somebody said. Same eight lines as the SNES
copy; what a report contains lives in recomp-net.
lib/recomp-net -> 859ba28 (support chat reporting), which is also where
rnet_account_set_secret_path comes from.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPXfGGhBBBxQPcEzaMsauF
The BIOS exception handler ran interpreted for the life of every process,
1.22 billion instructions and 44.6% of all interpreted work in Breath of
Fire III, even though it is compiled and even though the stub that
unblessed it sat at exactly the address bios/SCPH1001.toml already
declared as an install slot. Two mechanisms existed and did not compose:
the emitter hooked the declared slot PC, while the runtime's bless
verifier memcmp'd the whole body including that slot, so the body was
MISMATCH forever and native code never reached the hook.
Three parts, none of which pays off alone:
1. Publish the declared ranges to the runtime. The emitter emits a
PsxKernelPatchRange table next to the kernel body table and routes it
through the backend descriptor; the bless verifier compares a body in
segments that skip them. CLEAN now means "every byte outside every
declared range still matches ROM", which is the claim the native code
actually depends on.
2. Generalise the slot model to what the patches are. The old model
assumed a 4-word lui/addiu/jalr/nop stub written over ROM zeros and
fired on `word != 0`. Measured against both images, only one of five
observed shapes fits: patchers also plant a `jr` over real ROM
instructions, rewrite an 11-word prologue and insert an `mfc0 Cause`,
and NOP out a driver routine. A slot is now a range with a length and
an explicit `resume` (jalr / fallthrough / none), detected by
comparing every word against its ROM-baked value.
3. Hand back from the interpreter at the range end. Kernel page 0 is
permanently dirty, because the handler saves registers there on every
exception, so straight-line interpretation would run the whole
function after the patch. The emitter registers the resume PC as a
continuation key and dirty_ram_interp surfaces there.
A PC inside a declared range must never be a native dispatch key: the
compiled bytes there are not what executes. Seven pre-existing
jal-return continuations violated that and wedged the boot at frame 0 in
an infinite dispatch loop (OpenBIOS RAM 0x357C). The emitter now drops
continuation keys inside a range so dispatch misses through to the
interpreter, and refuses to emit if a range covers a function entry,
which would silently remove a callable function instead.
Both profiles declare their measured ranges, and SCPH1001.toml finally
pins [program.image] sha256 to v2.2 (CRC32 37157331) so a v2.0 dump
refuses to generate rather than producing phantom discovery gaps.
Measured on Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70), headless,
matched frames, PSX_KERNEL_PATCH_RANGES=0 for the OFF side:
OpenBIOS SCPH-1001 v2.2
bless mismatch 21 -> 0 74 -> 0
kernel bodies -87.7%/frame -97.8%/frame
all interp insns -42.6%/frame -94.2%/frame
The residual kernel-body work is exactly declared-words x entries on
every range, so nothing leaks past the hook. The A0/B0/C0 vector
trampolines are unchanged (+0.2%, noise): the profile excludes them by
design and they are a separate job. Card read traces are byte-identical
across the A/B on both images, all 32 entries, which exercises the two
ranges that replace the kernel's card handler.
Rule 18 holds throughout: the guest's patched instructions still execute
as written on the dirty-RAM interpreter. Only the code around them
changes backend. Nothing is synthesised.
Tests: runtime/tests/test_kernel_patch_ranges.c (new, the verifier's
accept/reject behaviour and the hand-back predicate) and the install-slot
range invariants in recompiler/tests/bios_address_model_test.cpp.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Focused port of the fork's fix/setup-host-bios-deadlock feature (10 commits) onto current upstream master, per the 2026-09-10 decision to port rather than rebase (the fork line and upstream share no merge base).
Content: the codegen host accepts any recompiled BIOS stem instead of only OpenBIOS/SCPH1001 (psx_bios_known_images.h); the runtime checks the setup host before validating a chosen BIOS and the first-run loop breaker reports a cause-specific reason; the CLI decodes subprocess output as UTF-8; --setup-selfcheck verifies a headless setup; the rbengine preflight names the recursive init. Two port conflicts resolved: the new CTest registrations in recompiler/CMakeLists.txt are kept (the names exist only in this change), and runtime.cmake takes the new recomp-ui guidance text. Cubic P3 finding fixed: the sibling PSX_NETPLAY FATAL_ERROR now also names --recursive.
Evidence: python syntax checks pass for psxrecomp_cli.py and the new tests; runtime/tests/test_codegen_host_bios_stems.py runs or skips only on a missing RECOMP_UI_ROOT. No full runtime build or retail route is claimed here. Developed with AI assistance (Cline, 2026-09-10).
libchdr (BSD-3-Clause) and toml11 (MIT) are statically linked into the
runtime and both require the notice in binary redistributions;
runtime/licenses/ existed but nothing copied it into a package.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The launcher and studio ship as Retro Launcher and Retro Studio now,
from the RetroPortingToolKit org, so the old product name is gone from
display text, documentation and release asset filenames.
The split is by case, and it is not cosmetic. CamelCase RetComM was
always the product; lowercase retcomm and RETCOMM_ are identifiers --
the C++ namespace, the binaries, the data directories, the CMake and
environment variables, the Cloudflare service names, the readme markers.
Renaming those would orphan existing installs for no benefit, so they
are untouched: 1101 lowercase retcomm and 205 RETCOMM_ still stand in
the launcher alone.
Four things are deliberately still spelled RetComM, and three of them
would have been real breakage:
- RetComM-Data is CamelCase but it is a directory on disk --
<exe_dir>\RetComM-Data\{runtime,config,data} for a portable install.
Renaming it strands every portable user's config, toolchains and
catalog cache.
- TechnicallyComputers/RetComM-Launcher in readme_has_launcher() is the
historical half of a both-slugs test. Renaming it makes it a duplicate
of the line above and sends every port built before the transfer back
to reporting its launcher section missing.
- _LEGACY_LAUNCHER_RE matched "^## RetComM Launcher". A sweep renamed it,
which would have silently stopped it matching every README already out
there; it now accepts both headings.
- The portable-stub lookup gained the pre-rename filenames back as
fallbacks, because old zips stay on the releases page and a rollback
downloads one.
Release assets become Retro-Launcher-* / Retro-Studio-*. That is safe
for installed clients: pick_launcher_asset() gates on *linux*/*windows*/
*macos* plus extension and arch, not on the product name, and the
Windows portable stub is identified by its RCM1 trailer rather than its
filename.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DpaQqVguHFpBuCmdoFYWY
The launcher and studio ship as Retro Launcher and Retro Studio now,
from the RetroPortingToolKit org, so the old product name is gone from
display text, documentation and release asset filenames.
The split is by case, and it is not cosmetic. CamelCase RetComM was
always the product; lowercase retcomm and RETCOMM_ are identifiers --
the C++ namespace, the binaries, the data directories, the CMake and
environment variables, the Cloudflare service names, the readme markers.
Renaming those would orphan existing installs for no benefit, so they
are untouched: 1101 lowercase retcomm and 205 RETCOMM_ still stand in
the launcher alone.
Four things are deliberately still spelled RetComM, and three of them
would have been real breakage:
- RetComM-Data is CamelCase but it is a directory on disk --
<exe_dir>\RetComM-Data\{runtime,config,data} for a portable install.
Renaming it strands every portable user's config, toolchains and
catalog cache.
- TechnicallyComputers/RetComM-Launcher in readme_has_launcher() is the
historical half of a both-slugs test. Renaming it makes it a duplicate
of the line above and sends every port built before the transfer back
to reporting its launcher section missing.
- _LEGACY_LAUNCHER_RE matched "^## RetComM Launcher". A sweep renamed it,
which would have silently stopped it matching every README already out
there; it now accepts both headings.
- The portable-stub lookup gained the pre-rename filenames back as
fallbacks, because old zips stay on the releases page and a rollback
downloads one.
Release assets become Retro-Launcher-* / Retro-Studio-*. That is safe
for installed clients: pick_launcher_asset() gates on *linux*/*windows*/
*macos* plus extension and arch, not on the product name, and the
Windows portable stub is identified by its RCM1 trailer rather than its
filename.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013DpaQqVguHFpBuCmdoFYWY
psx_netplay_auth.c/h are gone; recomp-net's rnet_auth is the one copy, and
the submodule is bumped to pick it up. Callers change name only
(psx_account_* -> rnet_account_*).
The duplication was a day old and had already cost a bug: the URL screen
that refused every OAuth authorize URL had to be fixed in this file and in
snesrecomp's copy separately, and the second was caught by remembering
rather than by anything structural.
Lobby client compiles with the same 12 pre-existing warnings as before and
no errors. main.cpp still cannot be compiled here -- it needs a generated
tree -- so its rename is verified by symbol cross-check only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015C38nLRmmyXHyJ7P1DiTxg
Sign-in did nothing when clicked: the browser never opened.
My own hardening broke it. account_open_url built a `xdg-open '<url>'`
command string and screened the URL for shell metacharacters, and an OAuth
authorize URL is full of `&` -- client_id&response_type&redirect_uri&scope
&state -- so every real URL hit the metacharacter check and was refused.
The feature could never have worked on Linux or macOS.
Fixed by removing the shell rather than by loosening the screen. The URL
goes to execlp as a single argv element, so there is no command line for a
metacharacter to escape into and nothing to quote. A double fork leaves
the opener reparented to init, so a game loop never has a zombie to reap.
Windows was always fine -- ShellExecuteA takes the URL directly.
The scheme check stays, and is now the whole of the screening: xdg-open
will act on a file:// URL or a local path, and this only ever legitimately
receives http(s).
Verified rather than reasoned about this time: with a stub opener on PATH,
a real Discord authorize URL arrives byte-identical, every `&` intact, and
a file:// URL is still refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015C38nLRmmyXHyJ7P1DiTxg
The runner half. psx_netplay_auth owns the HTTP, a worker thread and the
key; main.cpp is eight one-line adapters over it; queue_hello gains an
optional session field.
The key never crosses the wire. This runtime has no TLS -- wss:// is still
an explicit "not in this phase" -- so a permanent bearer credential sent
in cleartext would be captured once on a hostile network and reused
forever. Instead the server issues a nonce and this proves possession with
HMAC-SHA256 keyed by SHA-256 of the key. psx_sha256.c was already here, so
the HMAC is twenty lines and there is no new dependency.
netplay_secret is written 0600 beside the other runtime config, in the
form "<player_id> <secret>", and redeemed lazily on the first pump so a
machine that signed in once comes up signed in with no player action. A
stored key that no longer works (revoked, or a different server) is
dropped and the client falls back to guest rather than nagging every
launch. That file is the point of the exercise: sign in once on a PC and
the build you install on a handheld authenticates with no browser -- and
it is a password in a file, so it must not go into anything distributed.
Guest stays byte-identical. `session` is omitted entirely rather than sent
empty, so an unauthenticated hello is the exact frame this client has
always sent. The launcher wiring is guarded on RECOMP_LAUNCHER_HAS_ACCOUNT
so this builds against a recomp-ui that predates the callbacks too, and
the runner and the UI can land in either order.
Opening the browser is per platform (ShellExecute / xdg-open / open) since
there is no SDL_OpenURL here. The URL comes from our own lobby server but
still reaches a shell, so anything that is not a plain http(s) URL free of
shell metacharacters is refused rather than trusted.
Verified: psx_netplay_auth.c and psx_lobby_client.c both compile clean
under -Wall -Wextra, and every psx_account_* symbol main.cpp uses is
declared and defined. main.cpp itself could NOT be compiled here -- it
needs a generated tree (bios_rom_alias.h) -- so its changes are verified
by symbol cross-check and inspection only, not by a build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015C38nLRmmyXHyJ7P1DiTxg
After a match the soft-return launcher was built from a fresh settings struct
that never carried the memcard paths or enabled flags. With nothing to inspect
the Save panel painted a placeholder block pattern on both cards, which read as
foreign save data even though the personal card files were untouched (the
netplay sandbox is unbound and personal cards reloaded before the launcher
opens).
- soft_return_lobby: seed memcard_path[] from the personal cards (settings.toml
path, else <memcard_dir>/cardN.mcd) and memcard_enabled[] like first boot.
- rematch writeback: fold ls.memcard_enabled/memcard_path back into the runtime
slot config and settings.toml, mirroring the first-boot exit path, so a card
toggled or browsed in from the rematch launcher reaches memcard_init_slots.
--memcard-dir keeps winning over paths.
- enabled flag is passed as -1 for disabled (0 means "unset -> on" to the
launcher) and read back as > 0, at both seed sites. Needs recomp-ui 028fa5c.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016HE3VRJ7nBVP1x7x2Yw2Ho
Matched Ape Escape retail SCPH1001 LLE builds identify 0661ce77 as the visible boot regression: parent 155e269b reaches game code, while 0661ce77 stalls near BIOS frame 375. Revert its four dependent follow-ups too; retain PR #326 readback bounds and PR #328 Vulkan aspect changes.
The row-by-row raster dispatcher amplifies an existing malformed DMA list into massive GL work. Keep the broader CPU/DMA arbitration experiment separate for compatibility testing. Document attribution, validation, and the temporarily restored interlace limitation.
Validation: full Ape target reaches title/menu and frame 11669 on retail LLE; OpenBIOS control reaches frame 3701. Eight focused tests pass; retained GL readback fixture passes 71 checks at both 1x and 4x after linking local SDL with iconv. Beads: beads-iisy; DMA follow-up: beads-eio.3.122.
queue_hello was fixed this way once, with a comment explaining why: a
name with a quote in it builds malformed JSON, the server drops the frame
whole, and the rename simply never happens. Its siblings were missed.
psx_lobby_create and psx_lobby_join interpolated the lobby name, the
PASSWORD, the display name, the game name and version, the bind address
and disc_fp straight into the frame. So a quote in a lobby name or a
password silently broke create/join, and a crafted value injected fields
into the client's own message -- which is the shape an edited client
would use deliberately.
Fix the class: every free-text value in an outbound frame now goes
through json_escape, at all seven remaining sites (list, create, join,
server_chat's game_name, seat_swap_answer, signal's lobby_id,
set_host_endpoint). JSON_ESC_CAP and a prototype are hoisted above the
first frame that needs them.
Two supporting changes:
- the create and join frame buffers grew (1536 -> 2304, 1024 -> 1536),
because escaping can double every value and a frame that no longer
fits is a create that silently fails.
- join now checks snprintf for truncation. create already did; join did
not, so an over-long frame was sent truncated and dropped whole.
match_caps needed nothing: language is already filtered to alnum/_/- and
session_bios to a two-value whitelist.
Compiles clean with -Wall -Wextra -Wformat=2, no new warnings against the
parent commit. Verified on the translation unit only -- no full runtime
build was run here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1dT6h1ZshMVUUVKTPxsCD