mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-25 18:05:12 -04:00
Fix lobby relay rewrite via WS peer IP and harden rollback netplay.
Resolve stash conflict in psx_lobby_client by keeping getpeername-based relay rewrite with the RFC1918-aware path, and land pending rollback hookup/runtime changes.
This commit is contained in:
@@ -4414,3 +4414,43 @@ GP0 → Software @ 1× → CPU VRAM → snaps / av= / GPUREAD (authority)
|
||||
**Re-soak watch:** matched `av=` across peers; visible 4× GL quality in
|
||||
match; no black first present after tip load; rematch keeps dual after
|
||||
lobby soft-return.
|
||||
|
||||
## 93. FMV stability gates — refuse media episodes / resim storm / MAX DESYNC (2026-08-05)
|
||||
|
||||
**Soak (rb-diag1/2 session 8):** post-FMV resim from matched baseline `39655d1b`
|
||||
diverged at sim 184 (~459 cycles) twice with identical abort digests; match-start
|
||||
episode load=736 died on baseline `mdec`/`cd` mismatch; FMV lockstep RELEASE
|
||||
with `streak=0` / MAX unmatched then soft-desynced the rest of the fight.
|
||||
Broken main-menu assets after were consistent with mid-FMV VRAM realign churn.
|
||||
|
||||
FMV savestate completeness is not the bug — cross-peer determinism / policy is.
|
||||
|
||||
**Fixes (§93 stability P0):**
|
||||
|
||||
1. **Media-range episode refuse:** track `g_fmv_media_lo`/`hi` through each FMV
|
||||
bout. Begin/follow refuse when mismatch or chosen load sits in
|
||||
`[lo, hi+SETTLE]` (or DESYNC hold), even if Live tip already left media.
|
||||
`psx_netplay_rb_fmv_episode_unsafe` + hc-fork skip. Snap apply that lands
|
||||
mid-media aborts before Replay (`episode load into FMV media`) and raises
|
||||
fork_cap. No `LIGHT_TIP` into media-range loads.
|
||||
2. **Resim-diverge storm:** second `resim core diverge` on the same sim →
|
||||
DESYNC keep-live + storm cooldown; no same-pin realign. First hit still
|
||||
raises fork_cap so the doomed load is not reused.
|
||||
3. **MAX unmatched RELEASE:** if lockstep hits MAX with `streak < CONFIRM`,
|
||||
arm `g_fmv_unmatched_desync` + storm cooldown — invent/begin stay held
|
||||
until cores rematch CONFIRM ticks (or session reset). Log
|
||||
`rb DESYNC — FMV lockstep MAX unmatched`.
|
||||
|
||||
**Re-soak watch:**
|
||||
|
||||
- No `begin epoch=…` / `follow` into FMV media or settle; expect
|
||||
`begin REFUSED … FMV media-range` / `episode load into FMV media`.
|
||||
- No second `ABORT — resim core diverge` on the same sim; expect
|
||||
`DESYNC — resim diverge storm` instead of same-pin realign loop.
|
||||
- Post-FMV: either `RELEASE … cores matched` or `DESYNC — MAX unmatched`
|
||||
with invent held — never `streak=0` + keep inventing through a fork.
|
||||
- Idle digs: `core`/`cd` match through title → vs → match start.
|
||||
- Menu after disconnect stays clean (no mid-FMV realign poison).
|
||||
|
||||
**Still open (P1/P2):** mid-session host state-transfer on storm; CD/MDEC
|
||||
cycle determinism for matched-baseline sim 184.
|
||||
|
||||
@@ -79,9 +79,14 @@ int psx_netplay_rb_fmv_defer_rewind(void);
|
||||
int psx_netplay_rb_fmv_media_active(void);
|
||||
|
||||
/* 1 during FMV media + short settle (§26) — admit waits for remote wire.
|
||||
* Post-FMV digest lockstep no longer blocks invent. Ticks FMV→settle. */
|
||||
* Post-FMV digest lockstep no longer blocks invent. Ticks FMV→settle.
|
||||
* Also 1 while §93 MAX-unmatched DESYNC hold is armed. */
|
||||
int psx_netplay_rb_lockstep_no_invent(void);
|
||||
|
||||
/* §93: 1 if tick sits in the last FMV media bout / settle tail (or DESYNC
|
||||
* hold). Begin/follow/hc-fork must not open episodes that load there. */
|
||||
int psx_netplay_rb_fmv_episode_unsafe(uint32_t tick);
|
||||
|
||||
/* Mid-guest resim pump: abort if Replay has made no finish_frame progress.
|
||||
* Full rb_pump stays admit/present-edge only (host-asymmetric). */
|
||||
void psx_netplay_rb_poll_replay_stall(void);
|
||||
|
||||
@@ -131,6 +131,8 @@ typedef struct {
|
||||
char display_name[PSX_LOBBY_NAME_LEN];
|
||||
char host[128];
|
||||
int port;
|
||||
/* Actual TCP peer after connect (split-horizon / hairpin-safe for SFU). */
|
||||
char connected_peer_ip[64];
|
||||
char path[128];
|
||||
char rx_http[4096];
|
||||
size_t rx_http_len;
|
||||
@@ -283,48 +285,30 @@ static int endpoint_host_port(const char *ep, char *host, size_t host_cap, int *
|
||||
return *port_out > 0 && *port_out <= 65535;
|
||||
}
|
||||
|
||||
static int host_name_is_loopback(const char *host)
|
||||
static void lobby_store_connected_peer_ip(int fd)
|
||||
{
|
||||
if (!host || !host[0])
|
||||
return 0;
|
||||
if (strncmp(host, "127.", 4) == 0)
|
||||
return 1;
|
||||
if (strcmp(host, "::1") == 0)
|
||||
return 1;
|
||||
if (strcmp(host, "localhost") == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Lobby WebSocket and UDP input relay are the same recomp-net-server process.
|
||||
* Launch may advertise INPUT_RELAY_ADVERTISE_HOST=127.0.0.1 (server default).
|
||||
* Remote peers rewrite that to the WS host they dialed (LAN IP / public DNS).
|
||||
* Same-machine peers keep loopback when UDP relay port is bound locally —
|
||||
* avoids hairpin NAT through the public DNS and start failures on hostname. */
|
||||
static int rewrite_relay_endpoint_to_lobby_host(char *ep, size_t cap)
|
||||
{
|
||||
char rh[128];
|
||||
int rport = 0;
|
||||
int n;
|
||||
if (!ep || !cap || !g_lc.host[0])
|
||||
return 0;
|
||||
if (!endpoint_host_port(ep, rh, sizeof(rh), &rport))
|
||||
return 0;
|
||||
if (strcmp(rh, g_lc.host) == 0)
|
||||
return 0;
|
||||
if (host_name_is_loopback(g_lc.host))
|
||||
return 0;
|
||||
if (host_endpoint_is_loopback(ep) && rnet_udp_port_available(rport) == 0) {
|
||||
fprintf(stderr,
|
||||
"psx_lobby: keep relay_endpoint %s "
|
||||
"(local UDP %d busy — same host as lobby server)\n",
|
||||
ep, rport);
|
||||
return 0;
|
||||
struct sockaddr_storage ss;
|
||||
socklen_t slen = (socklen_t)sizeof(ss);
|
||||
g_lc.connected_peer_ip[0] = '\0';
|
||||
if (fd < 0)
|
||||
return;
|
||||
memset(&ss, 0, sizeof(ss));
|
||||
if (getpeername(fd, (struct sockaddr *)&ss, &slen) != 0)
|
||||
return;
|
||||
if (ss.ss_family == AF_INET) {
|
||||
const struct sockaddr_in *in = (const struct sockaddr_in *)&ss;
|
||||
if (!inet_ntop(AF_INET, &in->sin_addr, g_lc.connected_peer_ip,
|
||||
sizeof(g_lc.connected_peer_ip)))
|
||||
g_lc.connected_peer_ip[0] = '\0';
|
||||
}
|
||||
n = snprintf(ep, cap, "%s:%d", g_lc.host, rport);
|
||||
if (n <= 0 || (size_t)n >= cap)
|
||||
return -1;
|
||||
return 1;
|
||||
#if defined(AF_INET6)
|
||||
else if (ss.ss_family == AF_INET6) {
|
||||
const struct sockaddr_in6 *in6 = (const struct sockaddr_in6 *)&ss;
|
||||
if (!inet_ntop(AF_INET6, &in6->sin6_addr, g_lc.connected_peer_ip,
|
||||
sizeof(g_lc.connected_peer_ip)))
|
||||
g_lc.connected_peer_ip[0] = '\0';
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static int parse_ipv4_dotted(const char *host, unsigned *o)
|
||||
@@ -357,6 +341,67 @@ static int ipv4_is_rfc1918(const unsigned o[4])
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int host_is_loopback_name(const char *host)
|
||||
{
|
||||
if (!host || !host[0])
|
||||
return 0;
|
||||
if (strncmp(host, "127.", 4) == 0)
|
||||
return 1;
|
||||
if (strcmp(host, "::1") == 0)
|
||||
return 1;
|
||||
if (strcmp(host, "localhost") == 0)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int host_is_rfc1918_name(const char *host)
|
||||
{
|
||||
unsigned o[4];
|
||||
if (!host || !host[0])
|
||||
return 0;
|
||||
if (!parse_ipv4_dotted(host, o))
|
||||
return 0;
|
||||
return ipv4_is_rfc1918(o);
|
||||
}
|
||||
|
||||
/* Lobby WebSocket and UDP input relay are the same recomp-net-server process.
|
||||
* Rewrite only when it improves reachability:
|
||||
* - keep an already-RFC1918 advertise (server LAN pick / split-horizon)
|
||||
* - rewrite to WS peer IP only when that peer is private/loopback and
|
||||
* advertise is public, loopback, or a hostname (never clobber LAN with WAN)
|
||||
* - else fall back to WS URL host string for loopback advertise only */
|
||||
static int rewrite_relay_endpoint_to_lobby_host(char *ep, size_t cap)
|
||||
{
|
||||
char rh[128];
|
||||
const char *use_host = NULL;
|
||||
int rport = 0;
|
||||
int n;
|
||||
int adv_lan, adv_loop, peer_local;
|
||||
if (!ep || !cap)
|
||||
return 0;
|
||||
if (!endpoint_host_port(ep, rh, sizeof(rh), &rport))
|
||||
return 0;
|
||||
adv_lan = host_is_rfc1918_name(rh);
|
||||
adv_loop = host_is_loopback_name(rh);
|
||||
if (adv_lan)
|
||||
return 0; /* server already gave a same-LAN dial target */
|
||||
peer_local = g_lc.connected_peer_ip[0] &&
|
||||
(host_is_rfc1918_name(g_lc.connected_peer_ip) ||
|
||||
host_is_loopback_name(g_lc.connected_peer_ip));
|
||||
if (peer_local)
|
||||
use_host = g_lc.connected_peer_ip;
|
||||
else if (adv_loop && g_lc.host[0] && !host_is_loopback_name(g_lc.host))
|
||||
use_host = g_lc.host;
|
||||
if (!use_host || !use_host[0])
|
||||
return 0;
|
||||
if (strcmp(rh, use_host) == 0)
|
||||
return 0;
|
||||
n = snprintf(ep, cap, "%s:%d", use_host, rport);
|
||||
if (n <= 0 || (size_t)n >= cap)
|
||||
return -1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int ipv4_is_link_local(const unsigned o[4])
|
||||
{
|
||||
return o && o[0] == 169 && o[1] == 254;
|
||||
@@ -1783,7 +1828,7 @@ static void handle_server_json(const char *json)
|
||||
if (rewrite_relay_endpoint_to_lobby_host(relay_endpoint,
|
||||
sizeof(relay_endpoint)) > 0) {
|
||||
fprintf(stderr,
|
||||
"psx_lobby: relay_endpoint %s → %s (lobby WS host)\n",
|
||||
"psx_lobby: relay_endpoint %s → %s (WS peer/host)\n",
|
||||
relay_raw, relay_endpoint);
|
||||
}
|
||||
strncpy(g_lc.join.host_endpoint, relay_endpoint,
|
||||
@@ -1967,6 +2012,12 @@ int psx_lobby_connect(const char *ws_url)
|
||||
return -3;
|
||||
}
|
||||
g_lc.fd = fd;
|
||||
lobby_store_connected_peer_ip(fd);
|
||||
if (g_lc.connected_peer_ip[0] &&
|
||||
strcmp(g_lc.connected_peer_ip, g_lc.host) != 0) {
|
||||
fprintf(stderr, "psx_lobby: WS connected peer %s (url host %s)\n",
|
||||
g_lc.connected_peer_ip, g_lc.host);
|
||||
}
|
||||
for (i = 0; i < 16; ++i) {
|
||||
key_raw[i] = (char)(rand() & 0xff);
|
||||
}
|
||||
|
||||
@@ -668,7 +668,8 @@ static void np_try_hc_fork_recovery(uint32_t fork_tick)
|
||||
* "persisted 39 ticks"). */
|
||||
if (psx_netplay_rb_active() || psx_netplay_rb_tip_holding() ||
|
||||
psx_netplay_rb_load_pending() || psx_netplay_rb_rewind_suppressed() ||
|
||||
psx_netplay_rb_fmv_defer_rewind() || psx_netplay_rb_lockstep_no_invent())
|
||||
psx_netplay_rb_fmv_defer_rewind() || psx_netplay_rb_lockstep_no_invent() ||
|
||||
psx_netplay_rb_fmv_episode_unsafe(fork_tick))
|
||||
return;
|
||||
sim = rnet_session_sim_tick(g_np.session);
|
||||
fork_cap = psx_netplay_rb_baseline_fork_cap();
|
||||
@@ -3950,6 +3951,13 @@ static void np_pump_session(void)
|
||||
#endif
|
||||
drain_lobby_signals();
|
||||
rnet_session_pump(g_np.session);
|
||||
/* STATE_CHUNK AIMD still needs many pumps per wall-second on TURN; frame-
|
||||
* paced call sites alone leave the cwnd idle between vblanks. */
|
||||
if (rnet_session_state_busy(g_np.session)) {
|
||||
int burst;
|
||||
for (burst = 0; burst < 12; burst++)
|
||||
rnet_session_pump(g_np.session);
|
||||
}
|
||||
np_drain_peer_frame_commits();
|
||||
if (g_np.rollback) {
|
||||
np_rollback_reconcile_wire();
|
||||
|
||||
+256
-16
@@ -34,6 +34,7 @@ int psx_netplay_rb_rewind_suppressed(void) { return 0; }
|
||||
int psx_netplay_rb_fmv_defer_rewind(void) { return 0; }
|
||||
int psx_netplay_rb_fmv_media_active(void) { return 0; }
|
||||
int psx_netplay_rb_lockstep_no_invent(void) { return 0; }
|
||||
int psx_netplay_rb_fmv_episode_unsafe(uint32_t tick) { (void)tick; return 0; }
|
||||
void psx_netplay_rb_poll_replay_stall(void) {}
|
||||
int psx_netplay_rb_take_promote_sweep(void) { return 0; }
|
||||
int psx_netplay_rb_fmv_unlock_grace_active(void) { return 0; }
|
||||
@@ -406,6 +407,9 @@ static uint32_t g_last_begin_mismatch = 0xffffffffu;
|
||||
* an unrecoverable fork (loud DESYNC log, keep-live, long cooldown). */
|
||||
#define RB_FORK_STORM_LIMIT 4u
|
||||
#define RB_FORK_STORM_COOLDOWN_TICKS 600u
|
||||
/* §93: matched-baseline resim diverge on the same sim twice is proven
|
||||
* nondeterminism (FMV/CD) — do not realign to the same pin again. */
|
||||
#define RB_RESIM_DIVERGE_STORM_LIMIT 2u
|
||||
/* After a genuine POST diverge where Live successfully realigned, do not
|
||||
* promote-only-block the next edges — cooldown was preventing the fork from
|
||||
* self-correcting (2026-08-01 §17 soak: tip=9203 abort → promote-no-resim
|
||||
@@ -533,6 +537,17 @@ static uint32_t g_fmv_core_match_streak;
|
||||
static int g_fmv_lockstep_released; /* sticky: never re-lock after invent on */
|
||||
/* Inclusive last tick both peers dense-snapped (media/lockstep/+tip). */
|
||||
static uint32_t g_fmv_dense_through;
|
||||
/* §93: media bout range (sim ticks). Begin/follow refuse loads/mismatches
|
||||
* inside [lo, unsafe_hi] even when Live tip has already left media — that
|
||||
* was the hole that opened ep8 load=176 / ep24 load=736 into FMV state. */
|
||||
static uint32_t g_fmv_media_lo;
|
||||
static uint32_t g_fmv_media_hi;
|
||||
/* §93: FMV lockstep hit MAX with streak < CONFIRM — cores never agreed.
|
||||
* Keep invent/begin off until cores rematch CONFIRM ticks or session reset. */
|
||||
static int g_fmv_unmatched_desync;
|
||||
/* §93: consecutive resim-core aborts on the same diverge sim. */
|
||||
static uint32_t g_resim_diverge_tick;
|
||||
static uint32_t g_resim_diverge_streak;
|
||||
static uint32_t g_bl_mismatch_streak;
|
||||
static uint32_t g_tip_hold_until;
|
||||
/* TipHold invent-cap quiet: wall-clock ms when we first stalled at tip+slack
|
||||
@@ -597,6 +612,8 @@ static void poll_tip_hold_finalize(void);
|
||||
static void clear_tip_extend_prime(void);
|
||||
static void clear_tip_hold_rereplay_pending(void);
|
||||
static int flush_tip_hold_deferred_rereplay(void);
|
||||
static void arm_rewind_cooldown_ticks(uint32_t sim, uint32_t ticks, const char *why);
|
||||
static void clear_rewind_cooldown(const char *why);
|
||||
static void tip_hold_try_finalize(void);
|
||||
static void schedule_episode_rereplay(uint32_t prefer_plus_one);
|
||||
static void ownership_on_post_match(uint32_t tip);
|
||||
@@ -795,7 +812,7 @@ static void rb_fmv_update_lockstep_gate(uint32_t sim)
|
||||
int matched = 0;
|
||||
uint32_t cap;
|
||||
uint32_t prev_until;
|
||||
if (!g_fmv_media_end_sim || rb_fmv_media_active() || g_fmv_lockstep_released)
|
||||
if (!g_fmv_media_end_sim || rb_fmv_media_active())
|
||||
return;
|
||||
|
||||
if (g_b.hc && !netplay_hc_peek_mismatch(g_b.hc, NULL, NULL, NULL)) {
|
||||
@@ -808,6 +825,22 @@ static void rb_fmv_update_lockstep_gate(uint32_t sim)
|
||||
else
|
||||
g_fmv_core_match_streak = 0;
|
||||
|
||||
/* §93: after MAX-unmatched DESYNC, keep counting so a later rematch can
|
||||
* clear invent/begin hold without requiring a new media bout. */
|
||||
if (g_fmv_lockstep_released) {
|
||||
if (g_fmv_unmatched_desync &&
|
||||
g_fmv_core_match_streak >= RB_FMV_LOCKSTEP_CONFIRM) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb FMV DESYNC cleared — cores rematched "
|
||||
"streak=%u sim=%u\n",
|
||||
(unsigned)g_fmv_core_match_streak, (unsigned)sim);
|
||||
fflush(stderr);
|
||||
g_fmv_unmatched_desync = 0;
|
||||
clear_rewind_cooldown("FMV cores rematched after MAX unmatched");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (sim < g_fmv_media_end_sim + RB_FMV_LOCKSTEP_MIN)
|
||||
return; /* accumulate streak; invent stays off through MIN */
|
||||
|
||||
@@ -827,23 +860,37 @@ static void rb_fmv_update_lockstep_gate(uint32_t sim)
|
||||
g_fmv_lockstep_until = grace_until;
|
||||
g_fmv_lockstep_released = 1;
|
||||
g_promote_sweep = 1;
|
||||
/* §71: MAX unlock with streak=0 means cores never confirmed after
|
||||
* FMV — keep promote_sweep sticky via a longer dense window so
|
||||
* reconcile prefers hist repair over tip episodes into a fork. */
|
||||
/* §71/§93: MAX unlock with streak=0 means cores never confirmed after
|
||||
* FMV — do NOT invent-unlock into a soft fork. Arm DESYNC hold +
|
||||
* storm cooldown; invent/begin stay gated until rematch or reset. */
|
||||
if (max_unmatched) {
|
||||
uint32_t dense = sim + RB_FMV_UNLOCK_GRACE * 2u;
|
||||
if (dense > g_fmv_lockstep_until)
|
||||
g_fmv_lockstep_until = dense;
|
||||
g_fmv_unmatched_desync = 1;
|
||||
arm_rewind_cooldown_ticks(sim, RB_FORK_STORM_COOLDOWN_TICKS,
|
||||
"FMV lockstep MAX unmatched "
|
||||
"(cores never confirmed)");
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb DESYNC — FMV lockstep MAX unmatched "
|
||||
"sim=%u streak=%u cap=%u — invent/begin held until "
|
||||
"cores rematch (confirm=%u)\n",
|
||||
(unsigned)sim, (unsigned)g_fmv_core_match_streak,
|
||||
(unsigned)cap, (unsigned)RB_FMV_LOCKSTEP_CONFIRM);
|
||||
fflush(stderr);
|
||||
}
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb FMV lockstep RELEASE sim=%u streak=%u "
|
||||
"cap=%u dense_until=%u (unlock_grace=%u; promote_sweep%s; "
|
||||
"invent already on after settle §26)\n",
|
||||
"%s)\n",
|
||||
(unsigned)sim, (unsigned)g_fmv_core_match_streak,
|
||||
(unsigned)cap, (unsigned)g_fmv_lockstep_until,
|
||||
(unsigned)RB_FMV_UNLOCK_GRACE,
|
||||
max_unmatched ? "; MAX unmatched"
|
||||
: ((sim >= cap) ? "; MAX" : "; cores matched"));
|
||||
: ((sim >= cap) ? "; MAX" : "; cores matched"),
|
||||
max_unmatched
|
||||
? "DESYNC hold — invent off until rematch"
|
||||
: "invent already on after settle §26");
|
||||
fflush(stderr);
|
||||
(void)prev_until;
|
||||
return;
|
||||
@@ -880,6 +927,7 @@ static void rb_fmv_tick_settle(void)
|
||||
if (media) {
|
||||
if (!g_was_in_fmv) {
|
||||
g_was_in_fmv = 1;
|
||||
g_fmv_media_lo = sim;
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb FMV rewind-defer ON (depth24/mdec; no invent)\n");
|
||||
fflush(stderr);
|
||||
@@ -896,6 +944,10 @@ static void rb_fmv_tick_settle(void)
|
||||
finalize_tip_hold();
|
||||
}
|
||||
}
|
||||
if (sim >= g_fmv_media_hi)
|
||||
g_fmv_media_hi = sim;
|
||||
if (g_fmv_media_lo == 0u || sim < g_fmv_media_lo)
|
||||
g_fmv_media_lo = sim;
|
||||
/* Heartbeat through the movie — digs are sparse while MDEC is hot. */
|
||||
{
|
||||
static uint32_t s_fmv_hb_sim;
|
||||
@@ -924,6 +976,8 @@ static void rb_fmv_tick_settle(void)
|
||||
if (g_was_in_fmv) {
|
||||
g_was_in_fmv = 0;
|
||||
g_fmv_media_end_sim = sim;
|
||||
if (sim >= g_fmv_media_hi)
|
||||
g_fmv_media_hi = sim;
|
||||
g_fmv_core_match_streak = 0;
|
||||
g_fmv_lockstep_released = 0;
|
||||
g_fmv_settle_until = sim + RB_FMV_SETTLE_TICKS;
|
||||
@@ -946,6 +1000,37 @@ static void rb_fmv_tick_settle(void)
|
||||
rb_fmv_update_lockstep_gate(sim);
|
||||
}
|
||||
|
||||
/* §93: true if an episode load/mismatch tick sits inside the last FMV media
|
||||
* bout or its settle tail — even when Live tip has already left media. */
|
||||
static int rb_fmv_tick_unsafe_for_episode(uint32_t tick)
|
||||
{
|
||||
uint32_t unsafe_hi;
|
||||
if (rb_fmv_media_active())
|
||||
return 1;
|
||||
if (g_fmv_unmatched_desync)
|
||||
return 1;
|
||||
if (g_fmv_media_hi == 0u && g_fmv_media_end_sim == 0u)
|
||||
return 0;
|
||||
unsafe_hi = g_fmv_settle_until;
|
||||
if (g_fmv_media_hi > 0u) {
|
||||
uint32_t media_settle = g_fmv_media_hi + RB_FMV_SETTLE_TICKS;
|
||||
if (media_settle > unsafe_hi)
|
||||
unsafe_hi = media_settle;
|
||||
}
|
||||
if (g_fmv_media_end_sim > 0u) {
|
||||
uint32_t end_settle = g_fmv_media_end_sim + RB_FMV_SETTLE_TICKS;
|
||||
if (end_settle > unsafe_hi)
|
||||
unsafe_hi = end_settle;
|
||||
}
|
||||
if (unsafe_hi == 0u)
|
||||
return 0;
|
||||
/* Media bout may start at 0 (boot). Treat lo==0 with hi set as from 0. */
|
||||
if (tick <= unsafe_hi &&
|
||||
(g_fmv_media_lo == 0u || tick >= g_fmv_media_lo))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Defer rewind during movie + short settle. */
|
||||
static int rb_in_fmv_defer_rewind_window(void)
|
||||
{
|
||||
@@ -954,6 +1039,8 @@ static int rb_in_fmv_defer_rewind_window(void)
|
||||
rb_fmv_tick_settle();
|
||||
if (rb_fmv_media_active())
|
||||
return 1;
|
||||
if (g_fmv_unmatched_desync)
|
||||
return 1;
|
||||
return sim < g_fmv_settle_until;
|
||||
}
|
||||
|
||||
@@ -963,7 +1050,8 @@ static int rb_in_fmv_defer_rewind_window(void)
|
||||
* re-serialized presentation to ~packet rate (~30 fps) even with
|
||||
* PSX_RB_GAP1_INVENT — invent count was 0 from FMV-on through invent_at.
|
||||
* Digest-gated lockstep (g_fmv_lockstep_until) still runs for dense snaps
|
||||
* / RELEASE logs; it no longer stalls admit. */
|
||||
* / RELEASE logs; it no longer stalls admit.
|
||||
* §93: also hold invent after MAX-unmatched DESYNC until cores rematch. */
|
||||
static int rb_in_fmv_lockstep_window(void)
|
||||
{
|
||||
RNetSession *s = sess();
|
||||
@@ -971,6 +1059,8 @@ static int rb_in_fmv_lockstep_window(void)
|
||||
rb_fmv_tick_settle();
|
||||
if (rb_fmv_media_active())
|
||||
return 1;
|
||||
if (g_fmv_unmatched_desync)
|
||||
return 1;
|
||||
return sim < g_fmv_settle_until;
|
||||
}
|
||||
|
||||
@@ -1654,23 +1744,79 @@ static void abort_episode_realign(const char *why)
|
||||
int hard_fail;
|
||||
int post_fail = why && strstr(why, "post ") != NULL;
|
||||
int baseline_fail = why && strstr(why, "baseline") != NULL;
|
||||
int resim_fail = why && strstr(why, "resim core") != NULL;
|
||||
int fmv_load_fail = why && strstr(why, "FMV media") != NULL;
|
||||
int arm_absurd_catchup = 0;
|
||||
|
||||
/* §55/§83: raise fork_cap BEFORE pick_realign_tip so we never select the
|
||||
* just-failed (or previously failed) baseline as the Live heal tip. */
|
||||
if (baseline_fail) {
|
||||
if (baseline_fail || fmv_load_fail) {
|
||||
uint32_t failed_load = g_rb ? rnet_rb_get_load_tick(g_rb) : 0u;
|
||||
if (fmv_load_fail && failed_load == 0u && why) {
|
||||
const char *lp = strstr(why, "load=");
|
||||
if (lp)
|
||||
(void)sscanf(lp + 5, "%u", &failed_load);
|
||||
}
|
||||
if (failed_load > 0u &&
|
||||
(g_bl_fork_cap == 0u || failed_load < g_bl_fork_cap)) {
|
||||
g_bl_fork_cap = failed_load;
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb fork cap — next load must be < %u "
|
||||
"(baseline mismatch there)\n",
|
||||
(unsigned)g_bl_fork_cap);
|
||||
"(%s)\n",
|
||||
(unsigned)g_bl_fork_cap,
|
||||
fmv_load_fail ? "FMV media load" : "baseline mismatch there");
|
||||
fflush(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
/* §93: matched-baseline resim diverge on the same sim is nondeterminism
|
||||
* (FMV/CD). Second hit → DESYNC keep-live; do not realign to the same pin. */
|
||||
if (resim_fail) {
|
||||
uint32_t diverge_sim = 0u;
|
||||
const char *p = strstr(why, "sim=");
|
||||
if (p && sscanf(p + 4, "%u", &diverge_sim) == 1 && diverge_sim > 0u) {
|
||||
if (diverge_sim == g_resim_diverge_tick)
|
||||
g_resim_diverge_streak++;
|
||||
else {
|
||||
g_resim_diverge_tick = diverge_sim;
|
||||
g_resim_diverge_streak = 1u;
|
||||
}
|
||||
} else {
|
||||
g_resim_diverge_streak++;
|
||||
}
|
||||
{
|
||||
uint32_t failed_load = g_rb ? rnet_rb_get_load_tick(g_rb) : 0u;
|
||||
if (failed_load > 0u &&
|
||||
(g_bl_fork_cap == 0u || failed_load < g_bl_fork_cap)) {
|
||||
g_bl_fork_cap = failed_load;
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb fork cap — next load must be < %u "
|
||||
"(resim diverge there)\n",
|
||||
(unsigned)g_bl_fork_cap);
|
||||
fflush(stderr);
|
||||
}
|
||||
}
|
||||
if (g_resim_diverge_streak >= RB_RESIM_DIVERGE_STORM_LIMIT) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb DESYNC — resim diverge storm sim=%u "
|
||||
"(%u× same tick; matched baseline nondeterminism) — "
|
||||
"keep-live, cooldown %u ticks (no same-pin realign)\n",
|
||||
(unsigned)g_resim_diverge_tick,
|
||||
(unsigned)g_resim_diverge_streak,
|
||||
(unsigned)RB_FORK_STORM_COOLDOWN_TICKS);
|
||||
fflush(stderr);
|
||||
g_abort_wire_class = RNET_RB_ABORT_CLASS_STORM;
|
||||
g_abort_wire_realign_tick = 0u;
|
||||
abort_episode(why);
|
||||
arm_rewind_cooldown_ticks(live_sim, RB_FORK_STORM_COOLDOWN_TICKS,
|
||||
"DESYNC resim diverge storm");
|
||||
return;
|
||||
}
|
||||
} else if (!baseline_fail) {
|
||||
g_resim_diverge_tick = 0u;
|
||||
g_resim_diverge_streak = 0u;
|
||||
}
|
||||
|
||||
have = pick_realign_tip(&tick);
|
||||
/* Sealed table dies in abort_episode — dump first on POST diverge. */
|
||||
if (post_fail)
|
||||
@@ -1726,6 +1872,7 @@ static void abort_episode_realign(const char *why)
|
||||
hard_fail = !have ||
|
||||
(why && (strstr(why, "baseline") != NULL ||
|
||||
strstr(why, "resim core") != NULL ||
|
||||
strstr(why, "FMV media") != NULL ||
|
||||
post_fail ||
|
||||
strstr(why, "desync") != NULL));
|
||||
if (hard_fail)
|
||||
@@ -4575,6 +4722,8 @@ static void finalize_tip_hold(void)
|
||||
g_bl_mismatch_streak = 0;
|
||||
g_fork_streak = 0u; /* §42 P4: clean commit ends any fork suspicion */
|
||||
g_fork_streak_tick = 0u;
|
||||
g_resim_diverge_tick = 0u;
|
||||
g_resim_diverge_streak = 0u;
|
||||
g_bl_fork_cap = 0u; /* §55 */
|
||||
g_peer_nack_floor = 0u; /* §62 */
|
||||
/* §38: hold agreed at tip until peer tip-hold (SAFETY ~250ms) can exit. */
|
||||
@@ -4954,6 +5103,8 @@ static void commit_episode(void)
|
||||
g_bl_mismatch_streak = 0;
|
||||
g_fork_streak = 0u; /* §42 P4 */
|
||||
g_fork_streak_tick = 0u;
|
||||
g_resim_diverge_tick = 0u;
|
||||
g_resim_diverge_streak = 0u;
|
||||
g_bl_fork_cap = 0u; /* §55 */
|
||||
g_peer_nack_floor = 0u; /* §62 */
|
||||
/* No post-commit cooldown: promote-only after commit made char-select
|
||||
@@ -5112,6 +5263,11 @@ void psx_netplay_rb_shutdown(void)
|
||||
g_fmv_core_match_streak = 0;
|
||||
g_fmv_lockstep_released = 0;
|
||||
g_fmv_dense_through = 0;
|
||||
g_fmv_media_lo = 0;
|
||||
g_fmv_media_hi = 0;
|
||||
g_fmv_unmatched_desync = 0;
|
||||
g_resim_diverge_tick = 0;
|
||||
g_resim_diverge_streak = 0;
|
||||
g_bl_mismatch_streak = 0;
|
||||
g_tip_hold_until = 0;
|
||||
g_tip_hold_quiet_t0_ms = 0ull;
|
||||
@@ -5507,6 +5663,30 @@ static int try_apply_pending_load(CPUState *cpu_in)
|
||||
"psxrecomp: rb snap applied tick=%u pc=0x%08x (resume deferred)\n",
|
||||
(unsigned)loaded_tick, (unsigned)pc);
|
||||
fflush(stderr);
|
||||
/* §93: Live media detector can be cold while the load snap is mid-
|
||||
* FMV (soak: begin@184 load=176 → flush media=1). Expand the media
|
||||
* range and abort before Replay poisons both peers. */
|
||||
if (rb_fmv_media_active()) {
|
||||
if (!g_was_in_fmv) {
|
||||
g_was_in_fmv = 1;
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb FMV rewind-defer ON "
|
||||
"(depth24/mdec; no invent)\n");
|
||||
fflush(stderr);
|
||||
}
|
||||
if (g_fmv_media_lo == 0u || loaded_tick < g_fmv_media_lo)
|
||||
g_fmv_media_lo = loaded_tick;
|
||||
if (loaded_tick >= g_fmv_media_hi)
|
||||
g_fmv_media_hi = loaded_tick;
|
||||
if (!g_ownership_skip_snap) {
|
||||
char why[96];
|
||||
snprintf(why, sizeof(why),
|
||||
"episode load into FMV media (load=%u)",
|
||||
(unsigned)loaded_tick);
|
||||
abort_episode_realign(why);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
maybe_send_baseline();
|
||||
return 1;
|
||||
}
|
||||
@@ -5670,10 +5850,17 @@ int psx_netplay_rb_fmv_media_active(void)
|
||||
|
||||
int psx_netplay_rb_lockstep_no_invent(void)
|
||||
{
|
||||
/* Media + settle only (§26). Post-FMV title menus invent+resim. */
|
||||
/* Media + settle only (§26). Post-FMV title menus invent+resim.
|
||||
* §93: also hold after MAX-unmatched DESYNC until cores rematch. */
|
||||
return rb_in_fmv_lockstep_window();
|
||||
}
|
||||
|
||||
int psx_netplay_rb_fmv_episode_unsafe(uint32_t tick)
|
||||
{
|
||||
(void)rb_in_fmv_defer_rewind_window();
|
||||
return rb_fmv_tick_unsafe_for_episode(tick);
|
||||
}
|
||||
|
||||
void psx_netplay_rb_poll_replay_stall(void)
|
||||
{
|
||||
uint64_t now;
|
||||
@@ -6069,13 +6256,29 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
|
||||
if (s_fmv_refuse_sim != sim) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb begin REFUSED mismatch=%u — FMV lockstep "
|
||||
"(no tip episode; admit waits for wire)\n",
|
||||
(unsigned)mismatch_tick);
|
||||
"(no tip episode; admit waits for wire)%s\n",
|
||||
(unsigned)mismatch_tick,
|
||||
g_fmv_unmatched_desync ? " [DESYNC hold]" : "");
|
||||
fflush(stderr);
|
||||
s_fmv_refuse_sim = sim;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* §93: refuse when the *mismatch* sits in the last media bout — Live tip
|
||||
* may already be past FMV while hc-fork still targets an in-media tick. */
|
||||
if (rb_fmv_tick_unsafe_for_episode(mismatch_tick)) {
|
||||
static uint32_t s_media_mm_refuse_sim;
|
||||
if (s_media_mm_refuse_sim != sim) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb begin REFUSED mismatch=%u — FMV media-range "
|
||||
"(lo=%u hi=%u settle_until=%u; no episode into media)\n",
|
||||
(unsigned)mismatch_tick, (unsigned)g_fmv_media_lo,
|
||||
(unsigned)g_fmv_media_hi, (unsigned)g_fmv_settle_until);
|
||||
fflush(stderr);
|
||||
s_media_mm_refuse_sim = sim;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Coalesce: refuse reopen after abort/storm only; reconcile promotes wire.
|
||||
* until is armed from live sim at failure (not rewound tip) so catch-up
|
||||
@@ -6183,6 +6386,19 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
|
||||
s_refuse_last_mismatch = 0xffffffffu;
|
||||
}
|
||||
|
||||
/* §93: chosen load may sit inside a prior FMV bout even when Live tip and
|
||||
* mismatch are past settle (soak: load=176 / 736 into media). */
|
||||
if (rb_fmv_tick_unsafe_for_episode(load)) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb begin REFUSED mismatch=%u load=%u — FMV "
|
||||
"media-range load (lo=%u hi=%u settle_until=%u)\n",
|
||||
(unsigned)mismatch_tick, (unsigned)load,
|
||||
(unsigned)g_fmv_media_lo, (unsigned)g_fmv_media_hi,
|
||||
(unsigned)g_fmv_settle_until);
|
||||
fflush(stderr);
|
||||
return 0;
|
||||
}
|
||||
|
||||
target = rnet_rb_suggest_target(g_rb, mismatch_tick, sim);
|
||||
if (target < load)
|
||||
target = load;
|
||||
@@ -6219,8 +6435,10 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
|
||||
/* Prefer MotK agreed tip when library watermark not yet set. Depth
|
||||
* ceiling comes from the session's configured light_tip_max_depth
|
||||
* (== RB_MOTK_TIP_RUNWAY), not the library default — see
|
||||
* psx_netplay_rb_start(). */
|
||||
* psx_netplay_rb_start(). §93: never light-tip into/near FMV media. */
|
||||
if (g_agreed_valid &&
|
||||
!rb_fmv_tick_unsafe_for_episode(load) &&
|
||||
!rb_fmv_tick_unsafe_for_episode(target) &&
|
||||
rnet_rb_is_light_tip_candidate_ex(load, target, g_agreed_through,
|
||||
rnet_rb_get_light_tip_max_depth(g_rb)))
|
||||
corr.flags = RNET_RB_CORR_LIGHT_TIP;
|
||||
@@ -6684,8 +6902,30 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
|
||||
/* Same policy as begin: do not follow into media/lockstep tip episodes. */
|
||||
if (rb_in_fmv_lockstep_window()) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb follow REFUSED epoch=%u load=%u — FMV lockstep\n",
|
||||
(unsigned)epoch, (unsigned)load);
|
||||
"psxrecomp: rb follow REFUSED epoch=%u load=%u — FMV lockstep%s\n",
|
||||
(unsigned)epoch, (unsigned)load,
|
||||
g_fmv_unmatched_desync ? " [DESYNC hold]" : "");
|
||||
fflush(stderr);
|
||||
g_follow_nack_pending = 1;
|
||||
g_follow_nack_epoch = epoch;
|
||||
g_follow_nack_mismatch = mismatch;
|
||||
g_follow_nack_load = load;
|
||||
g_follow_nack_target = target;
|
||||
g_follow_nack_slot = slot;
|
||||
g_follow_nack_sends = 0;
|
||||
send_follow_nack(epoch, mismatch, load, target, slot, 1);
|
||||
g_follow_nack_sends = 1;
|
||||
return;
|
||||
}
|
||||
/* §93: refuse follow when load/mismatch sits in the last FMV media bout. */
|
||||
if (rb_fmv_tick_unsafe_for_episode(load) ||
|
||||
rb_fmv_tick_unsafe_for_episode(mismatch)) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb follow REFUSED epoch=%u load=%u mismatch=%u — "
|
||||
"FMV media-range (lo=%u hi=%u settle_until=%u)\n",
|
||||
(unsigned)epoch, (unsigned)load, (unsigned)mismatch,
|
||||
(unsigned)g_fmv_media_lo, (unsigned)g_fmv_media_hi,
|
||||
(unsigned)g_fmv_settle_until);
|
||||
fflush(stderr);
|
||||
g_follow_nack_pending = 1;
|
||||
g_follow_nack_epoch = epoch;
|
||||
|
||||
Reference in New Issue
Block a user