The lobby's BIOS identity was a two-value token, openbios or scph1001, and scph1001 meant "some retail dump". An SCPH-5552 peer and an SCPH-1001 peer both offered it, the host settled it, each peer booted its own image, and the rollback boot digest never matched: about 20 s of black window, then both players back in the lobby with no reason (reported in #243). Each offer now carries the CRC-32 of the retail image that peer would boot (online bios_offer.retail_crc; LAN JOIN line 6). The host settles retail only when every seat offers the same CRC, else OpenBIOS when every seat links it, else refuses to start and puts the reason on the launcher's status line ("Players use different BIOS images (SCPH-5552 and SCPH-1001) ..."). The settled CRC ships in match_caps (session_bios_crc) and on MOTK1 START line 7, and each peer boots only a dump with that CRC. Rooms where every peer has the same image keep working, including SCPH-5552-only builds with no OpenBIOS. Old peers send no CRC, so a mixed old/new room falls back to OpenBIOS or refuses; a session from an old host still accepts any retail dump. The lobby server relays bios_offer and match_caps as opaque objects, so it needs no change. The rule lives in netplay_bios_settle.c, shared by the online and LAN paths and covered by netplay_bios_settle_test. last_error grows to fit one sentence, with psx_lobby_set_last_error to set it.
13 KiB
Netplay with recomp-net (psxrecomp)
This is the feature overview for rollback-capable multiplayer in PSX
title projects that opt into -DPSX_NETPLAY=ON and ship lib/recomp-net
(plus lobby client). It describes what we are introducing for players and
title developers — not the internal soak checklist.
Deeper / implementation docs:
| Doc | Role |
|---|---|
NETPLAY_TOPOLOGY.md |
Locked online vs LAN topology |
ROLLBACK_MOTK_HOOKUP.md |
MotK rollback integration notes |
config_schema.md [netplay] |
Disc gate fields (require_cue, tracks, fingerprint) |
lib/recomp-net/README.md |
Transport / session library |
Lobby server WS_LOBBY.md (recomp-net-server) |
WebSocket lobby + SFU policy |
Opt-in at configure time: see GAME_PROJECT_SETUP.md
(PSX_NETPLAY, ENABLE_NETPLAY_IF_PRESENT). The launcher NETPLAY button only
appears when the host advertises GameInfo.netplay_supported.
Rollback netcode
Default match mode for opted-in titles is rollback (GGPO-style tip
prediction + resimulation), with delay-sync still available as an opt-out
(“Disable Rollback” in lobby settings, or PSX_NET_MODE=delay).
- Peers exchange pad tips over the session; missing remote input is predicted for a short horizon, then corrected by resim when the real tip arrives.
- Input delay (D) and prediction depth (P) can be auto-derived from RTT or set manually in the lobby.
- Savestate / snapshot rings and AV digests keep peers aligned; FMV / media paths use lockstep-friendly rules so digests stay meaningful.
Rollback is the product default for titles that ship it (e.g. MotK). Delay-sync remains useful for debugging and for hosts that prefer fixed lag.
Seats vs session slots
Lobby chat is masked for profanity and slurs on every path: the server
filters relayed lines, and every client runs recomp-net's
rnet_chat_filter_apply on each line it puts in its ring -- so LAN rooms
(host-relayed MOTK5 CHAT) are filtered without a server. The list is
lib/recomp-net/data/chat_filter_words.txt; RNET_CHAT_FILTER=0 disables
the client pass.
The lobby browser also carries a per-game server chat (server op
server_chat, relayed to every client listed for the same title) and lists
only the players online for this title; both are online-only, since a LAN
room has no server. The same emoji and profanity handling applies.
Players can move themselves in the lobby and ask to swap seats (online via
the server's seat_move / seat_swap_request / seat_swap_answer; on LAN
via MOTK5 SEATMOVE / SWAPASK / SWAPANS / SWAPRES relayed by the host),
so the lobby host can hold any seat. The session is planned at launch
(ae_np_plan_session_slots): the host is always session slot 0 — the
seat every host-only path keys on — and the other players follow in lobby-seat
order; each session slot drives the controller port of its lobby seat
(PsxNetplayConfig.port_of_slot), so the game sees a player where the lobby
seated them. Session slots are therefore compact (no holes) even when the
lobby is sparse; the sparse ports are what the game sees.
Host in the spectator table
Online rooms let the host watch from the gallery and still run the match. The
host keeps session slot 0 — the seat every host-only path keys on (save
states, card sync, the start barrier, overlay host controls) — but its pad is
muted (psx_netplay_stage_local substitutes "no controller") and slot 0 maps
to no controller port; player seats sit at lobby seat + 1 and drive port
slot − 1. The server publishes host_spectates with the launch and sizes the
input relay for the extra forwarded slot; every peer derives its session slot
from that one flag. The session can therefore hold PSX_MAX_PLAYERS + 1
slots. LAN rooms have no gallery, so this is online-only.
Env for command-line sessions: PSX_NET_HOST_SPECTATES=1 on every peer.
Bring your own memory card (seat 2)
By default a match runs on the host's memory-card choices: at launch the
host hashes / sends both of its cards to every guest (the SRAM state op), and
guests play from a sandbox copy so their own cards are never written.
Some games need each player's own card in the machine — Yu-Gi-Oh! Forbidden Memories duels load each duelist's deck from a separate card. For that, seat 2 (P2) can bring its card:
- In the lobby a memory-card glyph sits beside P2's name. P2 clicks it to offer its local slot-1 card; the host can click it to disallow / allow guest cards. It lights up only when both agree, and every peer sees the same state. Default is off (host cards only).
- At launch, before the host's card broadcast, P2 uploads that card to the host
over the session (
MEMCARDstate op, the one guest→host transfer). The host installs it as the match's slot-2 card and the normal broadcast then carries it to everyone, P2 included. - The host's real slot-2 card is neither read by the match nor written: the
host rebinds slot 2 to
<memcard_dir>/netplay/guest_card2.mcdfor the session and restores its own file on shutdown. P2's real cards stay untouched too (guests already sandbox both slots); what the match writes to slot 2 lands in P2's sandbox copy, not in P2's personal card. - The value is settled once by the host at start and delivered with the
launch caps (
guest_memcard_active, or the trailingMOTK1 STARTline on LAN), so a toggle racing the start cannot leave peers disagreeing about whether a card is coming.
The room page also carries a lobby chat. Online it is the server's chat op,
echoed to everyone seated; on LAN the host relays it (MOTK5 CHATREQ from a
guest, MOTK5 CHAT to everyone). Join, leave and kick are announced as system
lines the same way (empty sender fields on the wire mark them as system).
Env override for command-line sessions: PSX_NET_GUEST_MEMCARD=1 on every
peer (a host that expects a card from a seat that never sends one waits at the
mc_guest_wait barrier — the stall phase names it).
Hybrid graphics (determinism + present quality)
Netplay must keep guest simulation identical across peers. Present quality is separate.
When OpenGL is selected and netplay is active, psxrecomp can run a dual-raster path:
| Layer | Scale | Role |
|---|---|---|
| Software rasterizer | 1× (headless authority) | Deterministic VRAM / digests / rollback snaps |
| OpenGL | Player internal resolution (Native … 8K, or legacy 2×–4×) | Window present quality only |
Cost: extra CPU for the 1× SW pass. Benefit: peers can use different GL settings without desyncing the sim. SW-only netplay forces scale 1 for the whole path. Offline play keeps full supersampling with no dual-raster tax.
Internal resolution is a per-peer presentation setting. Settings → Display → Internal resolution is not a mod, so it is not cleared for netplay: each peer keeps its own preset, clamped to its own GPU, and one peer at 4K next to another at Native is a supported match. Only the GL present surface changes; the 1× software authority, the digests and the rollback snapshots are the same on every peer. Widescreen and frame-rate mods, by contrast, are cleared for netplay.
Connectivity: ICE, TURN, SFU, LAN
Online lobbies (WebSocket + SFU)
Online matches go through the lobby at netplay.retcomm.net (WebSocket control plane). Match UDP pad traffic for online rooms uses an SFU star (selective forwarding unit on the lobby server): every peer sends to the SFU; the SFU fans out to other seats. Peers do not mesh each other for game data online.
Current lobby policy prefers always SFU for online starts so CGNAT / asymmetric NAT does not strand players on failed ICE attempts.
ICE + TURN
ICE (with TURN relay via the project’s coturn on
netplay.retcomm.net) remains part of the stack for discovery,
signaling, and fallbacks. With PSX_NETPLAY=ON, runtime.cmake defaults
RNET_ENABLE_ICE=ON (libjuice) before adding recomp-net. Pass
-DRNET_ENABLE_ICE=OFF only for LAN-only / no-FetchContent builds.
libjuice is pulled as a pinned URL tarball (not git clone) so Retro
AppImage / mismatched-libcurl hosts can still configure; offline builds can
vendor lib/recomp-net/third_party/libjuice or set -DRNET_LIBJUICE_ROOT.
“Force TURN” in the UI can raise delay floors for relay-heavy paths; it
does not replace the SFU online architecture above.
LAN / Direct IP (P2P star)
Without a lobby start (LAN or Direct IP):
- 2 players: peer-to-peer UDP.
- 3+ players: host-as-relay local star — the session host fans out tips to other seats (same star shape as online SFU, but the game host is the hub).
Sim authority: pad slot 0 is the session host (START, state transfer).
Guests rearrange among seats 1..N−1.
Multitap + seat ceiling
| Item | Value |
|---|---|
| Library / lobby / UI ceiling | 8 seats (RNET_MAX_SLOTS, dual SCPH-1070) |
| Per-title cap | game.toml players / PSX_MAX_PLAYERS (e.g. MotK=2, Bomberman Party=5) |
Offline: recomp-ui exposes Multitap under Settings → INPUT on PSX titles
with num_players >= 3. Off hides seats beyond the two native controller
ports and caps g_offline_pad_count at 2 (settings.toml [controller] multitap, default on). Multitap still arms at game-start when three or more
offline seats are live (multitap_port from game.toml).
Multitap analog (hack): tap seats are plain digital by default. Opt in
with game.toml / settings.toml [controller] multitap_analog = true
(or Settings → INPUT / Lobby Settings). When on, tap seats may report
DualShock (0x73 + sticks) in multitap bulk status. Hosts publish
match_caps.multitap_analog so every peer applies the same setting at
launch. Not reliable across titles — leave off unless the game needs it.
Netplay (psxrecomp only): lobbies with more than 2 seats always
force SCPH-1070 multitap on (force_session_pads_connected /
session start when slot_count >= 3). The offline Multitap toggle does
not opt out of that. Empty tap seats are fine — not every slot needs a
device.
Rollback and delay-sync both carry multitap pad bytes.
BIOS settle: each peer advertises a BIOS offer (can run OpenBIOS, has a
retail dump and that dump's CRC-32, and whether OpenBIOS is selected) — online
on ready, LAN on JOIN. At Start the host freezes one session BIOS (openbios,
or scph1001 = retail with the image's CRC) via match_caps.session_bios /
session_bios_crc (online) or the MOTK1 START lines (LAN). Retail is used
only when every seated peer has the same retail image; otherwise OpenBIOS if
everyone links it; otherwise the host refuses to start and says which images
differ. Peers that cannot apply the settled BIOS abort instead of falling
back. That choice boots the match only — it does not change each peer’s saved
BIOS preference. See docs/BIOS_SELECTION.md (Netplay lobby settle).
Disc identity for multi-track titles
Netplay is dump-strict for titles that declare it. Peers must run the same playable image geometry, not “any USA ISO.”
Typical [netplay] in game.toml (MotK example):
[netplay]
require_cue = true
required_tracks = 17
# optional: required_disc_fp = "…" # TOC fingerprint
| Rule | Why |
|---|---|
Prefer / require .cue + sibling .bin track files |
Multi-track Redump layout (data + XA/audio) |
Exact track count when required_tracks > 0 |
Track-01-only dumps desync CDDA/XA vs full cues |
Reject bare incomplete mounts when require_cue |
Cue→bin fallback cannot invent missing tracks |
Optional TOC fingerprint (required_disc_fp) |
Same track count still wrong dump |
Generate & rebuild / prepare flows should point at the .cue, not a lone
.bin. See config_schema.md and the release checklist in
GAME_PROJECT_SETUP.md.
Related product pieces
- Lobby UI (recomp-ui): host/join, room settings, rollback toggles, FORCE
TURN, player names — only when
PSX_NETPLAYis on and the title advertises netplay. - VERSION / lobby match pin: peers should run the same release pin so generated code and protocol stay compatible.
- Mods: disabled for all netplay sessions (lobby / LAN / direct / rematch).
Launcher
commit_netplayand the runtime clear the in-session plan without touching the user's offline mod selection. Synced mod plans are deferred.
Enabling for a new title
- Vendor or submodule
recomp-netunderpsxrecomp/lib/recomp-net(or setRECOMP_NET_ROOT). - Before
include(runtime.cmake):set(PSX_NETPLAY ON CACHE BOOL … FORCE). psxrecomp_add_game_runtime(… ENABLE_NETPLAY_IF_PRESENT …)withMAX_PLAYERS/game.tomlplayersset correctly. ICE (libjuice) defaults ON withPSX_NETPLAY; override with-DRNET_ENABLE_ICE=OFFif needed.- Fill
[netplay]disc gates for multi-track games. - Test LAN 2P, then online lobby; soak rollback + FMV if the title uses media.
This document will grow as N-way rollback confirmation, SFU soak on 5P titles, and further ICE/SFU policy land.