The lobby server starts a 2-player room the host relay cannot carry on ICE
(recomp-net-server start_transport, launch transport "ice"). This client
knew only "host" and the legacy server relay. It inferred a relay from
equal host/guest endpoints -- which is exactly what two peers behind one NAT
advertise -- and each peer dialled its own public address: the match stayed
at "linking" forever.
- psx_lobby_launch_transport.h: one pure decision (host / ice / relay /
direct) with a unit test covering ICE, same-NAT endpoints, host relay and
the legacy relay cases.
- PsxLobbyJoinInfo.transport_ice; the launch handler records it and never
turns it into a relay; launch fill and resolve_use_ice start ICE p2p
(signalled over the lobby) for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep both netplay_peer_frontier.h and netplay_fmv_confirm.h includes; advance lib/recomp-net to a48d355 (main, includes #38).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
PSX_RB_DET_SELFTEST on R4 4P showed a replayed save of tick T differing
from the live save of T. Each cause, and the fix:
- Replay's finish_frame poll passed no PC hint (Live used the IRQ resume
chain); it now uses psx_netplay_snap_resume_hint.
- Devices are serviced lazily up to the next event, so root counters and
cycles_since_vblank lagged the clock by a host-path amount at save; a
load re-anchors them at the clock and drops the lag. The save now
publishes pending cycle charges and services every device to the clock
first, and renders the SPU up to it.
- GTE / mul-div completion stamps and the load-delay give-back were
re-anchored at the restored clock on every load, so a stall in flight at
the snap point cost the live run cycles but the replay none. New
BS_SEC_CPUTIM carries them relative to the clock.
- The SPU CD-input FIFO was outside the snap and wiped on rollback loads;
new BS_SEC_SPUCD carries the queued frames and the load keeps them.
- LZCR is serialized as derived from LZCS (the raw backing stays 0 until
the first LZCS write; guests only ever read the derived value).
Older snaps without the new sections load as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replay's finish_frame saved its tick snap with no PC hint while Live used
the IRQ resume / last check / cpu->pc chain; the same tick's snap could
carry a different resume PC (psx_netplay_snap_resume_hint now serves both).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Live realign restores the snap at T, sets the session to T+1 and
longjmps back into the guest from the admit spin. The resume ran before
T+1 was admitted, so that frame latched no inputs and did not advance;
the next frame was then counted as T+1 and the realigned seat ran one
frame ahead of its tick label from there on (ring saves after a NACK
realign carried the next tick's clock, 2x the cycles in the first
tick). Only the realigned seat shifted, so it forked from every other
seat; with four seats this produced the "resim core diverge" splits
and the later hc-fork / hang cascade.
flush_resume now admits the tick first on the Live realign path and
keeps spinning until it can.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Verifies the four PS1 semi-transparency modes, set-mask, mask check and
texel STP at 4x internal resolution with PGXP depth on and off, directly
and on the render thread, against the PS1 formulas and the software
rasterizer (new gl_blend_accuracy_test).
Finding: with the historical surface bases (fills and texels stored as
k*255/31, everything else as k*8) single-layer blends are off by one 5-bit
step in places (1312 of the test's checked pixels, e.g. B+F/4 over a fill).
[video] accurate_blending = true stores every VRAM write as k*8 so GL's
8-bit blend truncated by the readback is exactly the PS1 arithmetic; whites
of fills/texels then present as 248 like uploads already do. Default off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The lobby server starts a 2-player room the host relay cannot carry on ICE
(recomp-net-server start_transport, launch transport "ice"). This client
knew only "host" and the legacy server relay. It inferred a relay from
equal host/guest endpoints -- which is exactly what two peers behind one NAT
advertise -- and each peer dialled its own public address: the match stayed
at "linking" forever.
- psx_lobby_launch_transport.h: one pure decision (host / ice / relay /
direct) with a unit test covering ICE, same-NAT endpoints, host relay and
the legacy relay cases.
- PsxLobbyJoinInfo.transport_ice; the launch handler records it and never
turns it into a relay; launch fill and resolve_use_ice start ICE p2p
(signalled over the lobby) for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With three or more seats two guests can mispredict the same tick and both
BEGIN. The tie-break (lower initiator slot wins) ran only on initiators,
so a follower followed whichever BEGIN arrived first and stashed the rest.
On a 4-seat WAN race the host followed slot 3's epoch while slot 1's won;
the seats split across epochs, each waited for baselines that never came,
and the match ended after 20 s at rb_baseline.
The epoch id names its initiator, so a follower now applies the same rule
to the episode it follows (netplay_rb_concurrent_begin): yield during the
handshake and follow the winner, or drop the loser's BEGIN. A follower
seeing a new epoch from the initiator it already follows (abort and
reopen, the only case two seats can produce) is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two rollback bookkeeping bugs that only show with more than two seats
over a real link:
- g_peer_resolved_through took the highest RESOLVED from any peer. With
three seats the fastest follower stood in for the others, so the host
opened loads past the slower followers' frontiers (3P WAN: load 2400 vs
frontiers 2344 and 2319); every follower refused, episodes aborted and
the session diverged at 2496 before the race started. Adverts are now
kept per seat (netplay_peer_frontier.h) and the lowest across the
peer roster is the frontier.
- The §55 bisect cap (next load < L after a baseline mismatch at L) was
only cleared by an episode commit. An abort followed by a Live heal
left it armed for the rest of the session; once its loads left the
snapshot ring every episode was refused with "no confirmed snap"
(4P WAN: cap 1928, agreed 3136, ring 2656..3166) and a later fork at
3150 was never repaired. A live hash-confirmed agreed ADVANCE at or past
the cap now retires it.
Bumps lib/recomp-net for RB_RESOLVED sender attribution and rb_* queue
retransmit coalescing (RetroPortingToolKit/recomp-net fix/rb-ctrl-queue-dedupe).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Enhancement arenas (GPU DMA aperture, Expansion-1 mod memory) were untracked, so Crash widescreen drew every vertex native with no depth. Adds depth stats/clear causes to pgxp, depth to pgxp_tri_ring, input_route_start at_frame, integer number keys in game.toml.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Games that read ENVX right after KEYON saw 0 until the per-frame pump and keyed the voice off (Crash spin sfx at 21:9).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The post-FMV lockstep gate counted a tick as agreed only when sim-1 was
already hash-confirmed. A peer digest arrives one RTT after its tick, so
the peer running ahead (the host, cross-machine) never saw sim-1 confirmed,
reset the streak every tick and logged 'rb DESYNC - FMV lockstep MAX
unmatched' after every movie while every digest matched. Each newly
confirmed tick past the movie's end now counts once; only a real mismatch
resets the streak.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In a netplay match psx_mod_set_host_rumble now takes only this peer's own
guest port (psx_netplay_local_port) and drives this peer's input pad with
it; other ports and calls made while rolling back are ignored, so a
netplay = "host_output" plugin can report every port each tick.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Some titles compute vibration for every pad but only send it over SIO to a
DualShock (R4 with a presented NeGcon). A trusted mod can now hand the runtime
DualShock motor values per seat; the host pad gets the louder of that and the
guest's SIO motors. Values lapse 8 VBlanks after the last call and are cleared
at every session reset, so nothing can stick on. Opt-in: no title that does
not call it changes. PSX_RUMBLE_TRACE now also logs headless.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] fmv_chroma_smoothing = true makes the OpenGL present of a 24-bit
(MDEC) frame keep each sample's luma and take its chroma from a [1 2 1]
tent over the 3x3 source pixels, hiding the 2x2 colour blocks of 4:2:0
video without softening luma. Pairs with the existing fmv_filter upscale.
Guest VRAM/MDEC output untouched; 15-bit presents and the default unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
[video] texture_lod = "mipmap" emulates mip levels for PS1 texture pages,
which are decoded per fetch from VRAM and so have no GL mip chain: the
screen-space UV footprint of a minified, proven-3D primitive is box-averaged
(opaque texels of the fragment's STP class only), and anisotropic_filtering
(1..16) spreads that many taps along the footprint's major axis. Every tap
clamps to the primitive's UV bounds or texture window, so packed atlases
never bleed. The mode rides in the textured batch's filter key; off (the
default), sprites/HUD and magnified textures are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>