mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
a069f1ca0c
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>
112 lines
4.2 KiB
C
112 lines
4.2 KiB
C
/* Per-seat peer RESOLVED frontiers for rollback episodes.
|
|
*
|
|
* A follower can only replay from a snapshot at or below the frontier it
|
|
* advertised. With two seats there is one follower and one number. With more,
|
|
* the initiator must open at or below the SLOWEST follower's frontier: taking
|
|
* the highest advert (the old single watermark) let the fastest peer stand in
|
|
* for the others, and on a 3-seat WAN race the host opened load=2400 against
|
|
* followers at 2344 and 2319 -- every follower refused, the episodes aborted
|
|
* and the session diverged before the race started.
|
|
*
|
|
* Header-only and free of runtime state so the bookkeeping is unit-tested
|
|
* (tests/test_netplay_peer_frontier.c). */
|
|
#ifndef NETPLAY_PEER_FRONTIER_H
|
|
#define NETPLAY_PEER_FRONTIER_H
|
|
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
|
|
#define NETPLAY_PEER_FRONTIER_SEATS 16u
|
|
|
|
typedef struct NetplayPeerFrontier {
|
|
uint32_t through[NETPLAY_PEER_FRONTIER_SEATS];
|
|
} NetplayPeerFrontier;
|
|
|
|
static inline void netplay_peer_frontier_reset(NetplayPeerFrontier *f)
|
|
{
|
|
if (f)
|
|
memset(f, 0, sizeof(*f));
|
|
}
|
|
|
|
/* A RESOLVED advert from `seat`. Watermark: never lowers. */
|
|
static inline void netplay_peer_frontier_note(NetplayPeerFrontier *f, int seat,
|
|
uint32_t through)
|
|
{
|
|
if (!f || seat < 0 || (uint32_t)seat >= NETPLAY_PEER_FRONTIER_SEATS)
|
|
return;
|
|
if (through > f->through[seat])
|
|
f->through[seat] = through;
|
|
}
|
|
|
|
/* Evidence above `tick` was invalidated (realign) or a follower proved it
|
|
* cannot follow above it (NACK): no seat may stay above it. */
|
|
static inline void netplay_peer_frontier_clamp(NetplayPeerFrontier *f, uint32_t tick)
|
|
{
|
|
uint32_t i;
|
|
if (!f)
|
|
return;
|
|
for (i = 0; i < NETPLAY_PEER_FRONTIER_SEATS; ++i)
|
|
if (f->through[i] > tick)
|
|
f->through[i] = tick;
|
|
}
|
|
|
|
/* Frontier every seat in `seats` has reached: the lowest, and 0 while any of
|
|
* them has not advertised yet. */
|
|
static inline uint32_t netplay_peer_frontier_min(const NetplayPeerFrontier *f,
|
|
uint32_t seats)
|
|
{
|
|
uint32_t i, lo = 0xffffffffu;
|
|
int any = 0;
|
|
if (!f)
|
|
return 0u;
|
|
for (i = 0; i < NETPLAY_PEER_FRONTIER_SEATS; ++i) {
|
|
if (!(seats & (1u << i)))
|
|
continue;
|
|
any = 1;
|
|
if (f->through[i] < lo)
|
|
lo = f->through[i];
|
|
}
|
|
return any ? lo : 0u;
|
|
}
|
|
|
|
/* §55 bisect cap: after a baseline mismatch at load L the next load must be
|
|
* below L. Once every peer has hash-confirmed a tick at or past L (agreed
|
|
* ADVANCE from a live confirm) the fork is healed and the cap is stale; left
|
|
* armed it refused every later episode, because the only loads it allowed had
|
|
* long left the snapshot ring (4-seat race: cap 1928, agreed 3136, ring
|
|
* 2656..3166 -> "no confirmed snap" and a permanent fork from 3168). */
|
|
static inline int netplay_fork_cap_retired(uint32_t fork_cap, uint32_t agreed_through)
|
|
{
|
|
return fork_cap > 0u && agreed_through >= fork_cap;
|
|
}
|
|
|
|
/* Concurrent BEGIN while an episode is active. Wire epoch ids carry their
|
|
* initiator in the low bits (`slot_mask`); the lower initiator slot wins.
|
|
* Returns +1 when the incoming episode wins (yield ours if still in the
|
|
* handshake, then follow it), -1 when ours wins (drop the incoming BEGIN),
|
|
* 0 when the rule does not apply (same epoch, or a follower seeing another
|
|
* epoch from the initiator it already follows -- an abort-and-reopen).
|
|
*
|
|
* A follower applies it too: with three or more seats two guests can both
|
|
* BEGIN on the same mispredict, and a follower that followed whichever
|
|
* arrived first split the seats across epochs (4-seat WAN: host following
|
|
* slot 3's epoch while slot 1's won; every seat then waited on baselines
|
|
* that never came). */
|
|
static inline int netplay_rb_concurrent_begin(uint32_t ours, uint32_t theirs,
|
|
int following, uint32_t slot_mask)
|
|
{
|
|
uint32_t our_slot = ours & slot_mask;
|
|
uint32_t their_slot = theirs & slot_mask;
|
|
if (ours == theirs)
|
|
return 0;
|
|
if (following && our_slot == their_slot)
|
|
return 0;
|
|
if (their_slot < our_slot)
|
|
return 1;
|
|
if (their_slot > our_slot)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
#endif /* NETPLAY_PEER_FRONTIER_H */
|