mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
resim optimizations
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user