resim optimizations

This commit is contained in:
Alex Vanderveen
2026-08-02 19:11:04 -04:00
parent bc99f95fff
commit 90266d4eaa
6 changed files with 679 additions and 64 deletions
+207
View File
@@ -3233,3 +3233,210 @@ a burst of follower NACK/demotes right after them would mean the peer's
dense window is out of phase and the raise should be narrowed. For the Start
fix: pausing near resim activity should be single-menu; if a double recurs,
enable the pad edge log and capture the row sequence.
## 59. Lobby RTT→D table bump + TURN floor + faster first raise (2026-08-02)
**§58 soak:** dense tip snaps worked; after auto-delay reached D=6 the
scorecard hit `ep=+0` windows. Early minute still invent-stormed because
Play started at **D=3** with `force_turn=1` — lobby UDP RTT underestimates
the TURN game path.
**Changes:** rollback RTT→D table +1..+2 per band (floor 3); forced-TURN
floors D at 5; first auto-delay raise of a session confirms on 1 eval (~5 s).
## 60. Ownership-chain BEGIN stash + rexmit (FOLLOW deadlock) (2026-08-02)
**§59 soak hang:** early D=5 was healthy (`ep=+1`, depth_avg=10). Mid/late
cascade (core diverge → tip-extend abandon → baseline mismatch) ended in:
* host: `sim=3320 stall=rb_baseline` forever after `begin epoch=72`
* guest: `wait FOLLOW` then Live-ahead → permanent `pcap_freeze`
**Root cause:** pump drains SYNC before POST. Initiator finishes Verify+POST,
opens the next ownership-chain BEGIN, and sends it while the follower is
still in Verify for the prior epoch. `begin_follower` hit
`if (rnet_rb_is_active) return;` with **no log** — BEGIN dropped. BEGIN was
fire-and-forget (unlike seal/baseline keepalive), so the follower parked on
`wait FOLLOW` forever and the initiator sat in `rb_baseline`.
**Fixes:**
1. **Stash peer BEGIN** when a new-epoch BEGIN arrives while locally active
(tip-extend same-epoch still absorbed in place). Log:
`rb BEGIN stashed …`.
2. **Apply stash** when idle — at the follower's ownership-chain
`wait FOLLOW` return, and at end of pump when `!active`. Log:
`rb BEGIN unstash …`.
3. **BEGIN rexmit** from initiator while SealInputs/AwaitingBaseline and
peer baseline not yet seen (`rb BEGIN rexmit …`), same cadence as
seal/baseline keepalive.
4. **Skip-snap follow** at the ownership tip: follower may join a chain
BEGIN at `load == agreed_through` without a ring snap (mirrors
initiator skip-snap), instead of NACK → storm.
**Re-soak watch:** after ownership-chain lines, expect either immediate
`follow epoch=…` or `BEGIN stashed` → `BEGIN unstash` → `follow`; initiator
may log `BEGIN rexmit` on TURN. Must not see host `rb_baseline` + guest
`wait FOLLOW` with no follow for >1–2 s. Residual mid-session core forks
remain a separate correctness track.
## 61. Same-epoch BEGIN stash reopen + hc_dense peer clamp (2026-08-02)
**§60 soak regression:** FOLLOW deadlock was fixed, but sessions felt like
“finish resim → immediately redo.” Guest log pattern:
1. `follow epoch=16` (good)
2. while still active, host `BEGIN rexmit` for epoch 16 → **stashed**
3. `ownership final → Live`
4. `BEGIN unstash epoch=16` → re-open the episode just committed
5. host already past that work → guest `verify timeout` → both realign
and churn into the next epoch (FPS 3–11, long `present_gap`)
Also: host `choose_load` raised via `hc_dense` above peer RESOLVED
(e.g. 1180 vs 1168) → `follow REFUSED` / NACK / demote storm.
**Fixes:**
1. **Do not stash same-epoch BEGIN** while active (keepalive rexmit only).
2. **Refuse stash / unstash** for `g_last_commit_epoch`.
3. **Clear same-epoch stash** on ownership final and every episode commit
path (tip-hold, ownership tip commit, POST match).
4. **`hc_dense` raise only if** `peer_resolved_through >= hc_floor`
(otherwise stay on interval floor / agreed / peer_resolved).
**Re-soak watch:** after `ownership final` / `episode commit`, must **not**
see `BEGIN unstash` of that same epoch. `hc_dense` raises only when peer
RESOLVED already covers the tip. Legitimate *new*-epoch `BEGIN stashed` →
`unstash` still expected across ownership-chain handoffs.
## 62. NACK deep-demote WIRE_HOLE hang + peer NACK floor (2026-08-02)
**§61 soak:** stash fix held (zero `BEGIN stashed`/`unstash`), but the run
ended in an asymmetric permanent hang:
1. Ownership continue skip-snap at load=10232 → baseline core mismatch
(`f795b460` vs `a6e2e2a6`) → fork cap 10232, both sides realign to the
**same** snap (`core=07993348`).
2. Host bisects to load=10231, opens epoch 112 spanning back from live
≈10359.
3. Guest `follow REFUSED … snap missing` (ring newest ≈10364, oldest ≈10284
— 10231 evicted) → NACK with `frontier=10352` (**ahead** of the load, so
the existing "demote to peer frontier" clamp did nothing).
4. Host `NACK realign demote=10230 live_was=10359` (~129 ticks,
`snaps_dropped=70`). Remote wire rows 10231..10243 were already past the
tip-window retransmit → permanent `WIRE_HOLE need=10231` stall; guest ran
ahead to 10376 then froze on `pcap_freeze lead=-10`.
Also confirmed: the mid-session "jumped back far" feel (epoch 65, ~21 ticks
to the episode pin 7952 after `resim core diverge sim=7973`) is **correct**
behavior — the fork lies inside the episode span, so rewinding to a later
tip-extend snap would preserve the forked state. Not changed.
**Fixes:**
1. **Peer NACK floor** (`g_peer_nack_floor`): a NACK whose frontier is at or
ahead of the refused load means the peer's ring evicted that tick.
`choose_load` refuses any load ≤ floor (both fork-cap and plain paths);
log `choose_load refuse load=… ≤ peer NACK floor`. Cleared with the fork
cap on every verified commit / session reset.
2. **Wire-hole guard on NACK realign**: before rewinding Live to the demote
tick, scan remote wire rows (demote, live]. A missing tick followed by a
later present tick is a dead hole (retransmit window has moved on);
realign instead to the first snap past the hole
(`rb NACK hole-guard realign_to=…`). Watermarks still demote — only the
Live rewind depth is capped.
**Residual:** with fork cap forcing load < 10232 and NACK floor forcing
load > 10231, the bisect window can be empty — no episode opens and the fork
persists (live continues, no hang). True mid-session fork healing below both
snap rings needs full-state transfer — separate track. The skip-snap
baseline mismatch that *started* the cascade (same realign snap, different
skip-snap baselines) is also still open.
**Re-soak watch:** no minutes-long `WIRE_HOLE` / `admit waiting` stalls; on
follow NACK expect `peer NACK floor=…` then either a followable reopen or
`choose_load refuse` + cooldown; `NACK realign` lines now print
`demote=… realign=…` and realign must stay above any logged wire hole.
## 63. Double menu inputs — tip-hold after ownership final + resim present (2026-08-02)
**§62 soak:** protocol healthy (no WIRE_HOLE / stash reopen; scorecards
`ep≥0`, D≈5–6, clean lobby exit). UX regression: **dpad/Start felt
doubled** again. With `PSX_RB_PAD_LOG=1`, every menu episode re-applied
SIO edges Live had already applied (`DUP_SIO` at the same slot+sim):
* after `ownership final tip=3533 → Live`, Live walked 3536/3543/3548,
then epoch 16 loaded 3533 and resim'd those edges;
* Start↓ @5861 Live, invent hold-last Start↓ @5868 while auth was
release → episode load=5856 → Start↓ @5861 again on resim;
* `scrub-ahead` fired **0** times; catchup live-present (`rem>8`) could
flash those resim edges on screen.
**Root causes:**
1. Ownership final called `enter_tip_hold` then **immediate**
`finalize_tip_hold` → straight Live; next taps presented then got
rewound by the following episode.
2. Resim catchup live-presented when `remaining > 8` — tip episodes
(~15–24 ticks) re-showed edge-trigger outcomes.
3. Scrub-ahead end bound outside TipHold was only `sim`, so invent
poison past the tip was not cleared when a release promoted.
4. Host pad-edge trackers survived snap load (diag noise).
**Fixes:**
1. **Ownership final → tip-hold only** (quiet/coalesce/SAFETY own the
commit). Log: `ownership final … → tip-hold`. Same for chain-begin
failure fallback.
2. **Hold-last present through rem≤24** (full tip SPAN); only deeper
ownership-chain catch-up live-presents.
3. **Scrub-ahead** outside TipHold extends to `sim+9` (pred depth) so
release promote clears invent hold-last ahead of Live.
4. **`psx_netplay_on_rb_snap_loaded`** resets sio/invent/local pad-edge
trackers from `psx_frontend_on_rb_snap_loaded`.
**Re-soak watch:** `ownership final … → tip-hold` then
`tip-hold through=…` / quiet or coalesce commit (not immediate Live);
`scrub-ahead release` should appear on digital releases; tip episodes
should not log `catchup … present=live` unless rem>24; one physical
Start/dpad → one menu action. Pad-log may still show resim SIO edges
(correct for sim) — the feel test is presentation, not DUP_SIO count.
## 64. Catchup rem tip-cap + hc-fork persist not across episodes (2026-08-02)
**§63 short soak:** user saw **two resims back-to-back** and stopped.
1. `hc-fork recovery begin t=1023` (post-FMV unlock, core diverge, no pad
mispredict) → epoch 8 `1008..1032` with `catchup present=live rem=37`.
2. Tip-hold @1032, coalesce tip-extend →1046, peer COMMIT @1032 →
`tip-extend ABANDON` → realign 1032.
3. Immediate `hc-fork recovery begin t=1033 (persisted 39 ticks)` →
epoch 16. Then ownership-chain skip-snap @1056 → baseline mismatch.
**Confirmed in code:**
* `psx_netplay_rb_confirmed_remaining` returned `frontier−sim`, so tip
SPAN episodes reported rem=37/47 whenever confirmed work ran past the
tip — §63's `rem≤24` hold-last never engaged.
* `np_try_hc_fork_recovery` updated `s_fork_first_sim` / accumulated
persist **while the episode was still active**, then the active guard
only blocked the open. On tip-extend abandon → Live, persist was
already satisfied → second recovery with zero Live gap.
**Fixes:**
1. **Confirmed remaining** capped by `target−sim` while an episode is
active (catchup present keys off tip depth).
2. **hc-fork recovery** returns early on active/tip-hold/load **before**
persist bookkeeping — persist only counts Live ticks.
3. **`psx_netplay_hc_fork_recovery_restart`** on tip-extend abandon
(fresh first_sim + last_attempt) even though rewind cooldown clears.
**Residual:** post-FMV core fork itself and skip-snap baseline mismatch
remain separate correctness tracks — this only stops the immediate
double-resim feel and tip-episode live-present flash.
**Re-soak watch:** tip SPAN episodes should hold-last (`catchup
present=live` only when tip rem>24, e.g. deep ownership catch-up); after
`tip-extend ABANDON` expect `hc-fork recovery restart` then ≥16 Live
ticks before another `hc-fork recovery begin` (not same-tick reopen).
+7
View File
@@ -204,6 +204,13 @@ void psx_netplay_poll_snap(struct CPUState *cpu, uint32_t resume_pc);
/* 1 while a rollback episode is resimulating (mute audio / skip wall pacer). */
int psx_netplay_is_resimulating(void);
/* §63: reset host pad-edge trackers after snap load (guest SIO restored). */
void psx_netplay_on_rb_snap_loaded(void);
/* §64: restart hc-fork persist/retry clocks after tip-extend abandon so a
* second recovery episode cannot open on the same Live tick. */
void psx_netplay_hc_fork_recovery_restart(void);
/* Force software GPU when netplay starts — GL/VK VRAM readback is host-GPU
* nondeterministic and forks rollback snaps/resim. Implemented in main.cpp. */
void psx_frontend_netplay_force_sw_gpu(void);
+11 -5
View File
@@ -887,6 +887,8 @@ extern "C" void psx_frontend_on_rb_snap_loaded(void) {
}
gl_renderer_restage_vram_after_savestate();
vk_renderer_restage_vram_after_savestate();
/* §63: guest SIO came from the snap — drop Live pad-edge trackers. */
psx_netplay_on_rb_snap_loaded();
}
static uint64_t smooth_60_frame_hash(const uint32_t* pixels, size_t count) {
@@ -3767,16 +3769,20 @@ static void netplay_hold_last_present_tick(void) {
}
}
/* §47/§51 Layer 4: long Replay catch-up — wall-clock gate for a real VRAM
/* §47/§51/§63 Layer 4: long Replay catch-up — wall-clock gate for a real VRAM
* present. Independent of SPAN chunk boundaries (those are bookkeeping, not
* present events). Returns 1 if this guest vblank should fall through. */
* present events). Returns 1 if this guest vblank should fall through.
*
* §63: tip episodes are ≤ tip_runway (24). Live-presenting those frames
* re-showed edge-trigger menu inputs that Live had already displayed before
* the rewind (pad-log DUP_SIO Start/dpad). Hold-last through a full SPAN;
* only ownership-chain catch-up deeper than that shows live progress. */
static int netplay_replay_catchup_should_live_present(uint32_t remaining) {
static uint64_t s_catchup_present_ms;
uint64_t now = SDL_GetTicks64();
/* ~6–7 Hz: enough to show progress without flashing every resim frame
* (old period≈frame_ms presented nearly every tick when rem>8). */
/* ~6–7 Hz: enough to show progress without flashing every resim frame. */
uint32_t period = 150u;
if (remaining <= 8u)
if (remaining <= 24u)
return 0;
if (s_catchup_present_ms != 0ull && now >= s_catchup_present_ms &&
(uint32_t)(now - s_catchup_present_ms) < period)
+68 -20
View File
@@ -200,6 +200,8 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
}
void psx_netplay_shutdown(void) {}
void psx_netplay_stage_local(const PsxNetPad *pad) { (void)pad; }
void psx_netplay_on_rb_snap_loaded(void) {}
void psx_netplay_hc_fork_recovery_restart(void) {}
int psx_netplay_needs_local_sample(void) { return 0; }
int psx_netplay_live_pad_buttons(uint16_t *out)
{
@@ -441,6 +443,15 @@ static void np_try_hc_fork_recovery(uint32_t fork_tick)
return;
if (g_np.local_slot != 0)
return; /* initiator side only */
/* §64: do not bookkeep persist while an episode / tip-hold / load is
* in flight. Previously fork_tick advances during tip-extend rereplay
* started the persist clock, so tip-extend abandon → Live opened a
* second hc-fork recovery with zero Live gap (soak: epoch 8 → 16,
* "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())
return;
sim = rnet_session_sim_tick(g_np.session);
if (fork_tick != s_fork_tick) {
s_fork_tick = fork_tick;
@@ -452,10 +463,6 @@ static void np_try_hc_fork_recovery(uint32_t fork_tick)
if (s_fork_last_attempt_sim != 0u && sim >= s_fork_last_attempt_sim &&
(sim - s_fork_last_attempt_sim) < retry_ticks)
return;
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())
return;
s_fork_last_attempt_sim = sim;
remote = (g_np.local_slot == 0) ? 1 : 0;
if (g_np.slot_count < 2)
@@ -468,6 +475,21 @@ static void np_try_hc_fork_recovery(uint32_t fork_tick)
(void)psx_netplay_rb_begin_rewind(fork_tick, remote);
}
void psx_netplay_hc_fork_recovery_restart(void)
{
uint32_t sim;
if (!g_np.session)
return;
sim = rnet_session_sim_tick(g_np.session);
/* Force a fresh Live persist window + retry gap after abandon/realign. */
s_fork_first_sim = sim;
s_fork_last_attempt_sim = sim;
fprintf(stderr,
"psxrecomp: rb hc-fork recovery restart (sim=%u — fresh persist)\n",
(unsigned)sim);
fflush(stderr);
}
static void np_check_core_diverge(void)
{
uint32_t tick = 0, local_d = 0, peer_d = 0;
@@ -1346,18 +1368,39 @@ static void np_pad_log_edge(const char *tag, int slot, uint32_t sim, uint16_t pr
fflush(stderr);
}
/* §63: snap load restores guest SIO from the savestate; host-side edge
* trackers must not keep Live's post-tip buttons or resim logs (and invent
* edge state) false-edge against a stale prev. */
static uint16_t g_sio_pad_prev[PSX_MAX_PLAYERS];
static uint8_t g_sio_pad_have[PSX_MAX_PLAYERS];
static uint16_t g_inv_pad_prev[PSX_MAX_PLAYERS];
static uint8_t g_inv_pad_have[PSX_MAX_PLAYERS];
static uint16_t g_local_pad_prev = 0xFFFFu;
static int g_local_pad_have;
void psx_netplay_on_rb_snap_loaded(void)
{
int i;
for (i = 0; i < PSX_MAX_PLAYERS; ++i) {
g_sio_pad_prev[i] = 0xFFFFu;
g_sio_pad_have[i] = 0u;
g_inv_pad_prev[i] = 0xFFFFu;
g_inv_pad_have[i] = 0u;
}
g_local_pad_prev = 0xFFFFu;
g_local_pad_have = 0;
}
static void apply_pad_slot(int slot, const PsxNetPad *pad)
{
if (slot < 0 || slot >= g_np.slot_count || slot >= PSX_MAX_PLAYERS || !pad) return;
const int on_tap = sio_pad_on_multitap(slot);
if (np_pad_log_enabled()) {
static uint16_t s_sio_prev[PSX_MAX_PLAYERS];
static uint8_t s_sio_have[PSX_MAX_PLAYERS];
uint16_t prev = s_sio_have[slot] ? s_sio_prev[slot] : 0xFFFFu;
uint16_t prev = g_sio_pad_have[slot] ? g_sio_pad_prev[slot] : 0xFFFFu;
uint32_t sim = g_np.session ? rnet_session_sim_tick(g_np.session) : 0u;
np_pad_log_edge("sio", slot, sim, prev, pad->buttons);
s_sio_prev[slot] = pad->buttons;
s_sio_have[slot] = 1u;
g_sio_pad_prev[slot] = pad->buttons;
g_sio_pad_have[slot] = 1u;
}
sio_set_pad_connected(slot, 1);
sio_set_pad_config_capable(slot, on_tap ? 0 : 1);
@@ -1501,7 +1544,10 @@ static int np_digital_new_press(const RNetInputContractFrame *pub,
* RIGHT release@1607 then another `pub=ffdf wire=ffff` @1634).
*
* §34: TipHold stalls sim at tip while coalesce promotes release at tip+N —
* end must reach tip+runway, not only sim (sim <= release_tick was a no-op). */
* end must reach tip+runway, not only sim (sim <= release_tick was a no-op).
* §63: outside TipHold, also scrub sim..sim+P so gap1 invent poison past the
* current tip cannot re-hold Start/dpad before wire catches up (soak: sio
* Start↓@5861 then invent Start↓@5868 while auth was release). */
static void np_scrub_ahead_predicted(int slot, rnet_u32 release_tick,
const RNetRbFrame *released)
{
@@ -1519,6 +1565,12 @@ static void np_scrub_ahead_predicted(int slot, rnet_u32 release_tick,
uint32_t tip_end = tip + runway;
if (tip_end > end)
end = tip_end;
} else {
/* Pred depth default matches sched P=9; scrub invent rows Live will
* admit before the next remote burst. */
rnet_u32 ahead = sim + 9u;
if (ahead > end)
end = ahead;
}
if (end <= release_tick)
return;
@@ -2165,13 +2217,11 @@ static int np_try_admit_rollback(void)
any_invent = 1;
(void)netplay_ih_invent_hold_last(&g_np.ih, slot, sim, &row);
if (np_pad_log_enabled() && row.is_valid) {
static uint16_t s_inv_prev[PSX_MAX_PLAYERS];
static uint8_t s_inv_have[PSX_MAX_PLAYERS];
uint16_t prev = s_inv_have[slot] ? s_inv_prev[slot] : 0xFFFFu;
uint16_t prev = g_inv_pad_have[slot] ? g_inv_pad_prev[slot] : 0xFFFFu;
if (slot >= 0 && slot < PSX_MAX_PLAYERS) {
np_pad_log_edge("invent", slot, sim, prev, row.buttons);
s_inv_prev[slot] = row.buttons;
s_inv_have[slot] = 1u;
g_inv_pad_prev[slot] = row.buttons;
g_inv_pad_have[slot] = 1u;
}
}
}
@@ -2277,12 +2327,10 @@ void psx_netplay_stage_local(const PsxNetPad *pad)
psx_netplay_normalize_pad(&g_np.staged);
np_digital_debounce_staged();
if (np_pad_log_enabled()) {
static uint16_t s_local_prev = 0xFFFFu;
static int s_local_have;
uint16_t prev = s_local_have ? s_local_prev : 0xFFFFu;
uint16_t prev = g_local_pad_have ? g_local_pad_prev : 0xFFFFu;
np_pad_log_edge("local", g_np.local_slot, t, prev, g_np.staged.buttons);
s_local_prev = g_np.staged.buttons;
s_local_have = 1;
g_local_pad_prev = g_np.staged.buttons;
g_local_pad_have = 1;
}
g_np.staged_valid = 1;
g_np.latched_for_tick = 1;
+367 -31
View File
@@ -278,6 +278,20 @@ static struct {
uint16_t row_count;
} g_stash_seal_rows[RB_STASH_SEAL_ROWS_MAX];
static int g_stash_seal_rows_logged;
/* §60: SYNC BEGIN that arrived while we were still in an active episode
* (ownership-chain race — initiator finishes Verify+POST and emits the next
* BEGIN before our POST drain runs). Was silently dropped; follower then
* waited forever for FOLLOW. Latest-wins stash; applied once idle. */
static int g_stash_begin_valid;
static uint32_t g_stash_begin_epoch;
static uint32_t g_stash_begin_mismatch;
static uint32_t g_stash_begin_load;
static uint32_t g_stash_begin_target;
static int g_stash_begin_slot;
static uint8_t g_stash_begin_flags;
static int g_stash_begin_logged;
static uint64_t g_last_begin_rexmit_ms;
static int g_begin_rexmit_logged;
static uint64_t g_baseline_handshake_ms; /* when local baseline first sent */
static uint64_t g_last_baseline_burst_ms;
static int g_ready_timeout_logged;
@@ -499,6 +513,12 @@ static uint32_t g_fork_streak;
* L (2026-08-02 soak: 4× at 4176) can never heal — the next episode load must
* be strictly below it. 0 = no cap; cleared on verified commit / session reset. */
static uint32_t g_bl_fork_cap;
/* §62: a follow NACK with the peer's frontier AHEAD of the refused load means
* the peer's snap ring has evicted that tick — reopening at or below it can
* only NACK again (2026-08-02 soak: bisect 10231 vs peer ring oldest ≈10284
* → NACK → 129-tick demote → permanent WIRE_HOLE). Loads ≤ this floor are
* refused; cleared on verified commit / session reset. */
static uint32_t g_peer_nack_floor;
static RNetSession *sess(void); /* fwd — used by FMV lockstep window */
static void commit_episode(void); /* fwd — legacy immediate commit */
@@ -995,6 +1015,8 @@ static void clear_episode_wire_state(void)
g_last_seal_rexmit_ms = 0;
g_seal_rexmit_logged = 0;
g_seal_wait_logged = 0;
g_last_begin_rexmit_ms = 0;
g_begin_rexmit_logged = 0;
}
static void clear_baseline_pin(void)
@@ -1556,6 +1578,7 @@ uint32_t psx_netplay_rb_confirmed_remaining(void)
RNetSession *s = sess();
uint32_t sim;
uint32_t frontier;
uint32_t rem;
if (!s || !g_rb)
return 0;
if (!rnet_rb_is_resimulating(g_rb) && !rnet_rb_is_active(g_rb))
@@ -1564,7 +1587,18 @@ uint32_t psx_netplay_rb_confirmed_remaining(void)
if (sim == 0xffffffffu)
return 0;
frontier = psx_netplay_rb_confirmed_frontier(sim + 1u);
return (frontier > sim) ? (frontier - sim) : 0u;
rem = (frontier > sim) ? (frontier - sim) : 0u;
/* §64: catchup present keys off the *episode tip*, not confirmed work
* past tip. Tip SPAN episodes (≤24) were reporting rem=37+ whenever the
* contiguous frontier ran ahead, so §63's rem≤24 hold-last never engaged
* (soak: catchup present=live on 1008..1032). Cap by target−sim. */
if (rnet_rb_is_active(g_rb)) {
uint32_t tip = rnet_rb_get_target_tick(g_rb);
uint32_t tip_rem = (tip > sim) ? (tip - sim) : 0u;
if (tip_rem < rem)
rem = tip_rem;
}
return rem;
}
static void host_prefresh_seal_range(int slot, uint32_t lo, uint32_t hi)
@@ -1768,6 +1802,10 @@ static int maybe_abandon_tip_extend(void)
abort_episode("tip-extend abandon (peer committed below extension)");
schedule_live_realign(tick, "tip-extend abandon");
clear_rewind_cooldown("tip-extend abandon");
/* §64: tip-extend abandon clears rewind cooldown (extension edge may
* re-arrive as a pad mismatch). Also restart hc-fork persist so a
* state-resync episode cannot reopen on this same Live tick. */
psx_netplay_hc_fork_recovery_restart();
return 1;
}
@@ -2117,11 +2155,14 @@ static int choose_load_tick_inner(uint32_t mismatch, uint32_t *out_load)
uint32_t hc_dense = 0u;
if (hc_iv > raised)
raised = hc_iv;
/* §58: dense tip snaps — the raw HC-proven tick is raise-safe when a
* snap exists locally (both peers keep the same dense window; a
* missing peer snap is recoverable via follower NACK → demote,
* unlike the §53b interval-only rule which predates dense snaps). */
/* §58/§61: dense tip snaps — raise to raw HC-proven tick only when
* the peer has advertised RESOLVED at least that far. Otherwise both
* peers keep the dense window, but open can still land above the
* follower's frontier (soak: host 1180 vs peer_resolved 1168 →
* REFUSED / NACK / demote storm). Missing peer_resolved stays on
* interval floor (§53b); follower NACK remains the backstop. */
if (tip_dense_window() > 0u && hc_floor > raised &&
g_peer_resolved_through >= hc_floor &&
netplay_snap_ring_has(g_snaps, hc_floor)) {
raised = hc_floor;
hc_dense = hc_floor;
@@ -2286,6 +2327,23 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
m = g_bl_fork_cap; /* inner's shared walk returns load < m */
if (!choose_load_tick_inner(m, out_load))
return 0;
/* §62: peer proved it cannot follow ≤ floor (snap evicted). If the
* bisect window (must be < fork cap AND > peer floor) is empty, no
* mutually-loadable snap covers the fork — refuse to open rather
* than NACK-cycle into a deep demote. */
if (g_peer_nack_floor > 0u && *out_load <= g_peer_nack_floor) {
static uint32_t s_floor_log;
if (s_floor_log != *out_load) {
fprintf(stderr,
"psxrecomp: rb choose_load refuse load=%u ≤ peer NACK "
"floor %u (fork cap %u — no mutually-loadable snap)\n",
(unsigned)*out_load, (unsigned)g_peer_nack_floor,
(unsigned)g_bl_fork_cap);
fflush(stderr);
s_floor_log = *out_load;
}
return 0;
}
if (*out_load >= g_bl_fork_cap) {
static uint32_t s_refuse_log;
if (s_refuse_log != g_bl_fork_cap) {
@@ -2310,7 +2368,23 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
}
return 1;
}
return choose_load_tick_inner(mismatch, out_load);
if (!choose_load_tick_inner(mismatch, out_load))
return 0;
/* §62: same floor without a fork cap — do not reopen at a load the peer
* just proved unfollowable. */
if (g_peer_nack_floor > 0u && *out_load <= g_peer_nack_floor) {
static uint32_t s_floor_log2;
if (s_floor_log2 != *out_load) {
fprintf(stderr,
"psxrecomp: rb choose_load refuse load=%u ≤ peer NACK "
"floor %u\n",
(unsigned)*out_load, (unsigned)g_peer_nack_floor);
fflush(stderr);
s_floor_log2 = *out_load;
}
return 0;
}
return 1;
}
/* Follower's confirmed frontier for the NACK wire hint: the deepest tick this
@@ -2330,6 +2404,54 @@ static uint32_t follower_frontier_hint(void)
return 0u;
}
/* §62: realigning Live to `demote` requires remote wire inputs for every tick
* after it. A missing tick that is followed by a later PRESENT tick is a true
* hole — the peer's tip-window retransmit has already moved past it and it
* will never refill (2026-08-02 soak: demote 10359→10230, remote rows
* 10231..10243 evicted, 10244+ present → permanent WIRE_HOLE stall). A
* missing tail with nothing after it is just in flight and fine. */
static uint32_t remote_wire_hole_end_after(uint32_t demote, uint32_t live_sim)
{
RNetSession *s = sess();
int local, slot;
uint32_t t;
uint32_t first_missing = 0u;
uint32_t hole_start = 0u;
uint32_t hole_end = 0u;
int have_missing = 0;
if (!s || demote >= live_sim)
return 0u;
local = g_b.local_slot ? *g_b.local_slot : 0;
slot = (local == 0) ? 1 : 0;
if (g_b.slot_count && *g_b.slot_count < 2)
return 0u;
for (t = demote + 1u; t <= live_sim; ++t) {
RNetInputSample sample;
rnet_u32 wire = np_sched_wire_for_sim(t);
int present = rnet_session_peek_remote_input(s, slot, wire, &sample) &&
sample.valid;
if (!present) {
if (!have_missing) {
first_missing = t;
have_missing = 1;
}
} else if (have_missing) {
/* keep scanning — report the LAST closed hole */
hole_start = first_missing;
hole_end = t - 1u;
have_missing = 0;
}
}
if (hole_end > 0u) {
fprintf(stderr,
"psxrecomp: rb wire hole %u..%u after demote=%u — "
"realign there would stall\n",
(unsigned)hole_start, (unsigned)hole_end, (unsigned)demote);
fflush(stderr);
}
return hole_end;
}
/* MotK convention: SYNC op=NACK = follower cannot open episode (missing load
* snap / past frontier). Initiator aborts SealInputs instead of hanging.
* target field carries this follower's frontier (recomputed every send). */
@@ -2450,6 +2572,12 @@ static void maybe_enter_replay(void);
static void accept_peer_baseline(uint32_t epoch, uint32_t load, uint32_t dig_m,
uint32_t dig_a, uint32_t dig_b, uint32_t dig_c);
static void apply_stashed_baseline(void);
static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load,
uint32_t target, int slot, uint8_t wire_flags);
static void stash_peer_begin(uint32_t epoch, uint32_t mismatch, uint32_t load,
uint32_t target, int slot, uint8_t wire_flags);
static void apply_stashed_begin(void);
static void maybe_rexmit_begin(void);
static void enter_awaiting_baseline(void)
{
@@ -2714,6 +2842,8 @@ static void maybe_send_baseline(void)
* rexmit — runs through SealInputs/AwaitingBaseline/Replay/Verify until
* the peer POSTs. */
maybe_rexmit_seals();
/* §60: BEGIN keepalive until the follower joins (peer baseline seen). */
maybe_rexmit_begin();
follower = rnet_rb_is_from_peer_notify(g_rb) ? 1 : 0;
if (!g_local_baseline_sent) {
NetplayCoreParts parts;
@@ -3326,7 +3456,11 @@ static void ownership_chain_next_span(uint32_t tip, uint32_t frontier)
if (g_b.hc)
netplay_hc_prime_after(g_b.hc, tip);
advertise_agreed_resolved(tip);
g_last_commit_epoch = rnet_rb_get_epoch_id(g_rb);
g_last_commit_epoch = rnet_rb_get_epoch_id(g_rb); /* §42 P1 */
if (g_stash_begin_valid && g_stash_begin_epoch == g_last_commit_epoch) {
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
}
send_commit_notice(g_last_commit_epoch, tip);
rnet_rb_session_reset(g_rb);
@@ -3351,6 +3485,7 @@ static void ownership_chain_next_span(uint32_t tip, uint32_t frontier)
g_fork_streak = 0u;
g_fork_streak_tick = 0u;
g_bl_fork_cap = 0u; /* §55: commit/reset clears the bisect cap */
g_peer_nack_floor = 0u; /* §62 */
/* §48: only the lower seat initiates chain BEGINs. Both peers calling
* begin_rewind every chunk caused dual-init → tie-break abort → reload
@@ -3362,6 +3497,9 @@ static void ownership_chain_next_span(uint32_t tip, uint32_t frontier)
"(slot %d; peer initiates; skip-snap)\n",
(unsigned)tip, (unsigned)frontier, local);
fflush(stderr);
/* §60: initiator's next BEGIN often arrived (and was stashed) while we
* were still in Verify — apply it now that we are idle. */
apply_stashed_begin();
return;
}
@@ -3377,12 +3515,12 @@ static void ownership_chain_next_span(uint32_t tip, uint32_t frontier)
fflush(stderr);
if (!psx_netplay_rb_begin_rewind(mismatch, slot)) {
fprintf(stderr,
"psxrecomp: rb ownership chain FAILED tip=%u frontier=%u — Live\n",
"psxrecomp: rb ownership chain FAILED tip=%u frontier=%u — "
"tip-hold\n",
(unsigned)tip, (unsigned)frontier);
fflush(stderr);
g_ownership_skip_snap = 0;
enter_tip_hold(tip);
finalize_tip_hold();
}
g_ownership_chain = 0;
g_ownership_chain_frontier = 0;
@@ -3409,13 +3547,23 @@ static void ownership_on_post_match(uint32_t tip)
return;
}
}
/* Exhausted contiguous confirmed work → Live (immediate tip-hold commit). */
/* Exhausted contiguous confirmed work → tip-hold until pads quiet.
* §63: immediate finalize_tip_hold() Live-walked the next dpad/Start
* edges, then the following episode rewound and re-applied them
* (pad-log DUP_SIO / felt doubled). Tip-hold invent-cap + quiet/coalesce
* owns those edges instead. */
fprintf(stderr,
"psxrecomp: rb ownership final tip=%u frontier=%u → Live\n",
"psxrecomp: rb ownership final tip=%u frontier=%u → tip-hold\n",
(unsigned)tip, (unsigned)frontier);
fflush(stderr);
/* §61: discard any stashed BEGIN for this episode (rexmit) so Live does
* not reopen it. */
if (g_stash_begin_valid && g_rb &&
g_stash_begin_epoch == rnet_rb_get_epoch_id(g_rb)) {
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
}
enter_tip_hold(tip);
finalize_tip_hold();
}
void psx_netplay_rb_ownership_step(void)
@@ -3573,6 +3721,11 @@ static void finalize_tip_hold(void)
fprintf(stderr, "psxrecomp: rb episode commit through=%u (tip-hold)\n", (unsigned)target);
fflush(stderr);
g_last_commit_epoch = rnet_rb_get_epoch_id(g_rb); /* §42 P1 */
/* §61: drop same-epoch BEGIN stash so Live does not reopen it. */
if (g_stash_begin_valid && g_stash_begin_epoch == g_last_commit_epoch) {
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
}
send_commit_notice(g_last_commit_epoch, target); /* §42c */
rnet_rb_session_reset(g_rb);
g_tip_hold_until = 0;
@@ -3595,6 +3748,7 @@ static void finalize_tip_hold(void)
g_fork_streak = 0u; /* §42 P4: clean commit ends any fork suspicion */
g_fork_streak_tick = 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. */
g_post_tip_hold_holdoff_t0_ms = rb_mono_ms();
if (g_post_tip_hold_holdoff_t0_ms == 0ull)
@@ -3805,6 +3959,10 @@ static void commit_episode(void)
(unsigned)g_episode_count);
fflush(stderr);
g_last_commit_epoch = rnet_rb_get_epoch_id(g_rb); /* §42 P1 */
if (g_stash_begin_valid && g_stash_begin_epoch == g_last_commit_epoch) {
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
}
send_commit_notice(g_last_commit_epoch, target); /* §42c */
rnet_rb_session_reset(g_rb);
g_tip_extend_rereplay = 0;
@@ -3818,6 +3976,7 @@ static void commit_episode(void)
g_fork_streak = 0u; /* §42 P4 */
g_fork_streak_tick = 0u;
g_bl_fork_cap = 0u; /* §55 */
g_peer_nack_floor = 0u; /* §62 */
/* No post-commit cooldown: promote-only after commit made char-select
* D-pad feel rejected (hist updated, live sim not rolled). Abort/storm
* cooldown still arms from live sim on failed episodes. */
@@ -3926,12 +4085,15 @@ void psx_netplay_rb_start(void)
g_tip_extend_prime_tick = 0;
g_tip_extend_from_tick = 0;
g_ownership_skip_snap = 0;
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
g_last_commit_epoch = 0xffffffffu;
g_peer_commit_epoch = 0xffffffffu;
g_peer_commit_tick = 0;
g_fork_streak = 0u;
g_fork_streak_tick = 0u;
g_bl_fork_cap = 0u; /* §55: commit/reset clears the bisect cap */
g_peer_nack_floor = 0u; /* §62 */
clear_baseline_pin();
clear_episode_wire_state();
fprintf(stderr,
@@ -3979,12 +4141,17 @@ void psx_netplay_rb_shutdown(void)
g_tip_extend_prime_tick = 0;
g_tip_extend_from_tick = 0;
g_ownership_skip_snap = 0;
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
g_last_begin_rexmit_ms = 0;
g_begin_rexmit_logged = 0;
g_last_commit_epoch = 0xffffffffu;
g_peer_commit_epoch = 0xffffffffu;
g_peer_commit_tick = 0;
g_fork_streak = 0u;
g_fork_streak_tick = 0u;
g_bl_fork_cap = 0u; /* §55: commit/reset clears the bisect cap */
g_peer_nack_floor = 0u; /* §62 */
g_last_good_bb_pc = 0;
g_agreed_valid = 0;
g_agreed_through = 0;
@@ -4844,6 +5011,104 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
return 1;
}
static void stash_peer_begin(uint32_t epoch, uint32_t mismatch, uint32_t load,
uint32_t target, int slot, uint8_t wire_flags)
{
/* §61: never stash a BEGIN for the episode we are already in — that is a
* keepalive rexmit. Stashing it and unstashing after Live re-opened the
* same epoch (soak: follow 16 → final → unstash 16 → verify timeout). */
if (g_rb && rnet_rb_is_active(g_rb) &&
epoch == rnet_rb_get_epoch_id(g_rb))
return;
/* Also ignore BEGINs for an epoch we already committed. */
if (epoch == g_last_commit_epoch)
return;
g_stash_begin_valid = 1;
g_stash_begin_epoch = epoch;
g_stash_begin_mismatch = mismatch;
g_stash_begin_load = load;
g_stash_begin_target = target;
g_stash_begin_slot = slot;
g_stash_begin_flags = wire_flags;
if (!g_stash_begin_logged) {
fprintf(stderr,
"psxrecomp: rb BEGIN stashed epoch=%u load=%u target=%u "
"(local episode still active — apply when idle)\n",
(unsigned)epoch, (unsigned)load, (unsigned)target);
fflush(stderr);
g_stash_begin_logged = 1;
}
}
static void apply_stashed_begin(void)
{
uint32_t epoch, mismatch, load, target;
int slot;
uint8_t flags;
if (!g_stash_begin_valid || !g_rb || rnet_rb_is_active(g_rb))
return;
/* §61: drop stashes for epochs already committed / completed. */
if (g_stash_begin_epoch == g_last_commit_epoch) {
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
return;
}
epoch = g_stash_begin_epoch;
mismatch = g_stash_begin_mismatch;
load = g_stash_begin_load;
target = g_stash_begin_target;
slot = g_stash_begin_slot;
flags = g_stash_begin_flags;
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
fprintf(stderr,
"psxrecomp: rb BEGIN unstash epoch=%u load=%u target=%u\n",
(unsigned)epoch, (unsigned)load, (unsigned)target);
fflush(stderr);
begin_follower(epoch, mismatch, load, target, slot, flags);
}
/* §60: BEGIN was fire-and-forget; on TURN a single drop left the follower
* parked on "wait FOLLOW" while the initiator sat in rb_baseline forever.
* Retransmit on the same cadence as seal/baseline keepalive. */
static void maybe_rexmit_begin(void)
{
RNetSession *s = sess();
RNetRbPhase ph;
uint64_t now;
rnet_u8 wire_flags;
int32_t slot;
if (!g_rb || !s || !rnet_rb_is_active(g_rb) || rnet_rb_is_from_peer_notify(g_rb))
return;
ph = rnet_rb_get_phase(g_rb);
if (ph != nRNetRbPhaseSealInputs && ph != nRNetRbPhaseAwaitingBaseline)
return;
if (g_peer_baseline_ok)
return; /* follower has joined the episode */
now = rb_mono_ms();
if (g_last_begin_rexmit_ms != 0ull && now >= g_last_begin_rexmit_ms &&
(now - g_last_begin_rexmit_ms) < (uint64_t)RB_BASELINE_BURST_MS)
return;
if (g_last_begin_rexmit_ms != 0ull && !g_begin_rexmit_logged) {
fprintf(stderr,
"psxrecomp: rb BEGIN rexmit epoch=%u load=%u target=%u\n",
(unsigned)rnet_rb_get_epoch_id(g_rb),
(unsigned)rnet_rb_get_load_tick(g_rb),
(unsigned)rnet_rb_get_target_tick(g_rb));
fflush(stderr);
g_begin_rexmit_logged = 1;
}
g_last_begin_rexmit_ms = now;
wire_flags = (rnet_u8)(
(rnet_rb_get_corr_flags(g_rb) & RNET_RB_CORR_LIGHT_TIP)
? RNET_RB_SYNC_FLAG_LIGHT_TIP : 0u);
slot = rnet_rb_get_corrected_slot(g_rb);
(void)rnet_session_send_rb_sync(
s, rnet_rb_get_epoch_id(g_rb), rnet_rb_get_mismatch_tick(g_rb),
rnet_rb_get_load_tick(g_rb), rnet_rb_get_target_tick(g_rb),
(rnet_u8)(slot < 0 ? 0 : slot), RNET_RB_SYNC_OP_BEGIN, wire_flags);
}
static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uint32_t target,
int slot, uint8_t wire_flags)
{
@@ -4987,8 +5252,17 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
return;
}
}
if (!g_rb || rnet_rb_is_active(g_rb))
if (!g_rb)
return;
if (rnet_rb_is_active(g_rb)) {
/* §60/§61: tip-extend same-epoch is handled above. Same-epoch BEGIN
* rexmit must not be stashed (keepalive). Only stash a *new* epoch
* that arrived while we are still Verify/Replay on the prior one. */
if (epoch == rnet_rb_get_epoch_id(g_rb))
return;
stash_peer_begin(epoch, mismatch, load, target, slot, wire_flags);
return;
}
(void)rb_in_fmv_defer_rewind_window();
/* Same policy as begin: do not follow into media/lockstep tip episodes. */
if (rb_in_fmv_lockstep_window()) {
@@ -5008,24 +5282,35 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
return;
}
snap_n = g_snaps ? netplay_snap_ring_count(g_snaps) : 0u;
/* §60: ownership-chain continue uses skip-snap at the verified tip — the
* initiator does not require a ring snap there; neither may the follower,
* or we NACK a legitimate chain BEGIN and storm. */
if (!g_snaps || !netplay_snap_ring_has(g_snaps, load)) {
int skip_ok = (g_ownership_skip_snap && g_agreed_valid &&
load == g_agreed_through);
if (!skip_ok) {
fprintf(stderr,
"psxrecomp: rb follow REFUSED epoch=%u load=%u — snap missing "
"(ring count=%u newest=%u)\n",
(unsigned)epoch, (unsigned)load, (unsigned)snap_n,
(unsigned)(g_snaps ? netplay_snap_ring_newest_tick(g_snaps)
: 0u));
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;
}
fprintf(stderr,
"psxrecomp: rb follow REFUSED epoch=%u load=%u — snap missing "
"(ring count=%u newest=%u)\n",
(unsigned)epoch, (unsigned)load, (unsigned)snap_n,
(unsigned)(g_snaps ? netplay_snap_ring_newest_tick(g_snaps) : 0u));
"psxrecomp: rb follow skip-snap load=%u (ownership chain tip)\n",
(unsigned)load);
fflush(stderr);
/* Tell initiator to abort SealInputs; rexmit on TURN. */
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;
}
/* NACK loads past our shared frontier — initiator tip-slack beyond
* hash_confirm is a doomed baseline (cpu-only tip snap fork). */
@@ -5104,6 +5389,10 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
g_needs_advance = 0;
g_seal_export_logged = 0;
g_follow_nack_pending = 0;
/* Consumed — don't re-apply a stashed copy of this BEGIN next pump. */
if (g_stash_begin_valid && g_stash_begin_epoch == epoch)
g_stash_begin_valid = 0;
g_stash_begin_logged = 0;
rnet_rb_seal_inputs(g_rb, load, target, slot);
g_seal_wait_ms = rb_mono_ms();
export_local_seals();
@@ -5258,6 +5547,18 @@ void psx_netplay_rb_pump(void)
* load is followable instead of NACK-cycling. */
if (peer_frontier > 0u && peer_frontier < demote)
demote = peer_frontier;
/* §62: frontier AHEAD of the refused load = the peer's snap
* ring evicted that tick (not "peer behind"). Remember the
* floor so choose_load never reopens at/below it. */
if (peer_frontier >= load && load > g_peer_nack_floor) {
g_peer_nack_floor = load;
fprintf(stderr,
"psxrecomp: rb peer NACK floor=%u "
"(peer frontier=%u ahead — snap evicted)\n",
(unsigned)g_peer_nack_floor,
(unsigned)peer_frontier);
fflush(stderr);
}
/* Capture before abort_episode clears it — decides whether Live
* must rewind (snap already applied) or can stay put. */
int snap_was_applied = g_episode_snap_applied;
@@ -5302,12 +5603,41 @@ void psx_netplay_rb_pump(void)
abort_episode(why);
if (snap_was_applied && g_snaps &&
netplay_snap_ring_has(g_snaps, demote)) {
/* §62: never realign below a remote-input wire hole the
* tip-window retransmit no longer covers — Live would
* stall on WIRE_HOLE forever. Raise the realign target
* to the first snap past the hole (watermarks stay
* demoted; only the Live rewind depth is capped). */
uint32_t realign_to = demote;
uint32_t hole_end =
remote_wire_hole_end_after(demote, live_sim);
if (hole_end > 0u) {
uint32_t t;
uint32_t newest =
netplay_snap_ring_newest_tick(g_snaps);
realign_to = 0u;
for (t = hole_end; t <= newest; ++t) {
if (netplay_snap_ring_has(g_snaps, t)) {
realign_to = t;
break;
}
}
if (realign_to == 0u)
realign_to = newest;
fprintf(stderr,
"psxrecomp: rb NACK hole-guard realign_to=%u "
"(demote=%u hole_end=%u)\n",
(unsigned)realign_to, (unsigned)demote,
(unsigned)hole_end);
fflush(stderr);
}
fprintf(stderr,
"psxrecomp: rb NACK realign (snap was applied) "
"demote=%u live_was=%u\n",
(unsigned)demote, (unsigned)live_sim);
"demote=%u realign=%u live_was=%u\n",
(unsigned)demote, (unsigned)realign_to,
(unsigned)live_sim);
fflush(stderr);
schedule_live_realign(demote, why);
schedule_live_realign(realign_to, why);
} else {
fprintf(stderr,
"psxrecomp: rb NACK keep-live demote=%u "
@@ -5423,6 +5753,7 @@ void psx_netplay_rb_pump(void)
if (rnet_rb_is_active(g_rb) && rnet_rb_get_phase(g_rb) == nRNetRbPhaseSealInputs) {
uint64_t now = rb_mono_ms();
export_local_seals();
maybe_rexmit_begin();
if (g_seal_wait_ms != 0ull && now >= g_seal_wait_ms &&
(now - g_seal_wait_ms) >= (uint64_t)RB_SEAL_TIMEOUT_MS) {
abort_episode("seal timeout (peer missing snap / NACK lost)");
@@ -5438,6 +5769,11 @@ void psx_netplay_rb_pump(void)
maybe_enter_replay();
}
/* §60: idle follower with a stashed ownership-chain BEGIN (POST drain
* ordered after SYNC in this pump, or BEGIN arrived while Verify-active). */
if (!rnet_rb_is_active(g_rb))
apply_stashed_begin();
/* Keep BASELINE on the wire through Replay until the peer is clearly in. */
if (rnet_rb_is_active(g_rb) && rnet_rb_get_phase(g_rb) == nRNetRbPhaseReplay) {
if (g_local_baseline_sent && !g_local_post_sent) {
+19 -8
View File
@@ -1208,15 +1208,26 @@ static void np_auto_delay_tick(uint32_t now)
s_last_target = target;
return;
}
if (target != s_last_target) {
s_last_target = target;
s_agree_streak = 1u;
return;
/* §59: first raise of the session (never changed yet) confirms on 1 eval
* (~5 s) so a badly under-provisioned lobby D does not invent-storm for
* a full 10 s. Later raises keep 2-eval confirm; lowers stay at 3. */
{
uint32_t need_agree;
if (target < d)
need_agree = RB_AUTO_DELAY_AGREE;
else if (s_last_change_ms == 0u)
need_agree = 1u;
else
need_agree = 2u;
if (target != s_last_target) {
s_last_target = target;
s_agree_streak = 1u;
if (s_agree_streak < need_agree)
return;
} else if (++s_agree_streak < need_agree) {
return;
}
}
/* Asymmetric hysteresis: raises hurt only input latency, misses hurt
* determinism — confirm a raise on 2 evals (10 s), a lower on 3 (15 s). */
if (++s_agree_streak < ((target > d) ? 2u : RB_AUTO_DELAY_AGREE))
return;
if (s_last_change_ms != 0u &&
(uint32_t)(now - s_last_change_ms) < RB_AUTO_DELAY_COOLDOWN_MS)
return;