mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
optimize scheduler WAN performance
This commit is contained in:
@@ -216,6 +216,18 @@ Adaptive mid-match delay bumps are always on (no lobby disable).
|
||||
Local tip production continues during Replay (protocol peer).
|
||||
POST match → chain next SPAN or Final Verify→Live (no TipHold invent-cap
|
||||
mid catch-up). Long catch-up periodically presents live Replay VRAM.
|
||||
- [x] **§49 WAN chain/GO/realign:** ownership chain only if `frontier > tip+8`
|
||||
(`PSX_RB_CHAIN_MIN_AHEAD`); initiator keeps GO until peer POST; ready
|
||||
timeout RTT-scaled; INPUT resends from peer ACK when behind tip-redundancy.
|
||||
(§49b: do **not** clear remote tips on Live realign — tip=0 pcap freeze.)
|
||||
- [x] **§50 FMV tip-extend gate:** do not tip-extend / FOLLOW-raise through
|
||||
FMV media or settle (same window as begin/follow refuse). TipHold + FMV
|
||||
→ commit sealed tip; mid-FMV TipHold entry also commits. Stops host
|
||||
rereplay past guest ownership-final Live → ABANDON `bfc03c04` core/CD fork.
|
||||
- [x] **§51 choose_load / Replay layers:** (1) newest provably-safe snap —
|
||||
peer RESOLVED must not clamp below HC floor; (2) inclusive commit-tip
|
||||
span over `%iv` fallback; (3) ownership SPAN continue skip-snap (no
|
||||
reload); (4) catchup present ~150ms cadence, not every resim frame.
|
||||
- [x] **Present only at MotK wait CDA0:** netplay `gpu_vblank_flush_present`
|
||||
skips while resume/check PC is `0x8006CD54` (leave pending until
|
||||
`0x8006CDA0`) so sealed idle resim cannot digest opposite ping-pong
|
||||
@@ -2787,3 +2799,116 @@ the Up stretch — menu ownership thrash / dual-init abort loop.
|
||||
**Re-soak watch:** after FMV into menu, `soft-promote … unlock-grace-release`
|
||||
or no `rewind-request pub=ffef`; no `tie-break YIELD` / `ownership chain
|
||||
tip=N frontier=N+1` storm; D-pad navigation stays single-step.
|
||||
|
||||
## 49. WAN ownership micro-chains + GO loss + realign tip cliff (2026-08-02)
|
||||
|
||||
**WAN soak (FORCE_TURN / hotspot):** FMV settle hang fixed (no invent-hold
|
||||
deadlock). Session played ~60 fps until ~sim 1231, then ownership chained
|
||||
SPAN every few ticks (epochs 16→64), guest `ready timeout (no initiator GO)`
|
||||
while host sat in Verify, dual realign to ~1264 with stale `remote_tip≈1544`
|
||||
→ `WIRE_HOLE` + `cushion KEEP (absurd lead=279)`.
|
||||
|
||||
**Fix:**
|
||||
1. **Ownership chain floor** `frontier > tip + 8` (env `PSX_RB_CHAIN_MIN_AHEAD`);
|
||||
micro catch-ups stay Live.
|
||||
2. **Initiator GO rexmit** until peer POST (TURN dropped the one-shot burst);
|
||||
ready timeout scales with RTT (4s…12s).
|
||||
3. **INPUT ACK resend** when peer ack lags the tip-redundancy window.
|
||||
|
||||
**§49b (same day):** Live realign must **not** `clear_remote_inputs`. Full wipe
|
||||
→ `remote_tip=0` → mutual `pcap FREEZE` (wire≫P; neither advances). Selective
|
||||
invalidate also fails after ring wrap (only far tips remain). Pre-clear soak
|
||||
already recovered: peer post-realign production fills `need` while
|
||||
`prepare_local_tip` keeps emitting; §45 cushion refuses invent across holes
|
||||
until real rows land. API kept for hard_resync-style paths only.
|
||||
|
||||
**Re-soak watch:** no `ready timeout (no initiator GO)` under TURN; no
|
||||
`ownership chain tip=N frontier=N+2..N+7` storms; after abort realign no
|
||||
`pcap FREEZE … remote_tip=0` / `remote tips cleared`.
|
||||
|
||||
## 50. FMV tip-extend through lockstep → BIOS realign / CD fork (2026-08-02)
|
||||
|
||||
**WAN soak (§49b binary):** handshake + GO healthy; ~55–60 fps; death was a
|
||||
permanent core+CD fork after the second FMV (locked at tick 1136
|
||||
`ea3971ce` vs `fbb8c8aa`, CD `7ac33b56` vs `ac7c89e4`). First fork was earlier:
|
||||
|
||||
1. Episode opens pre-FMV (`begin epoch=8 mismatch=112 load=96`).
|
||||
2. FMV media/settle arms mid-Replay — begin/follow refuse *new* episodes, but
|
||||
**tip-extend still raised** the tip (`116→122`) and TipHold-rereplayed.
|
||||
3. Guest `ownership final tip=119 → Live`; host tip-extends `119→122` with
|
||||
rereplay; guest `follow REFUSED — FMV lockstep` on the post-commit BEGIN.
|
||||
4. Host `tip-extend ABANDON` realigns to snap `pc=0xbfc03c04` (BIOS sticky);
|
||||
`FIRST CORE DIVERGE sim=121`; CD digests split and never fully heal.
|
||||
|
||||
**Fix:** Gate `psx_netplay_rb_tip_extend` and tip-extend FOLLOW on the same
|
||||
`rb_in_fmv_lockstep_window()` as begin/follow. TipHold + FMV →
|
||||
`finalize_tip_hold` (commit sealed tip). Replay + FMV → keep current target
|
||||
(no raise). Entering FMV while TipHolding also commits immediately.
|
||||
|
||||
**Re-soak watch:** no `tip-extend …` / `tip-extend FOLLOW` during
|
||||
`FMV rewind-defer` / settle; no `tip-extend ABANDON` → `pc=0xbfc03c04` after
|
||||
first movie; no early `FIRST CORE DIVERGE` ~121; cores/CD stay matched through
|
||||
menu + second FMV.
|
||||
## 51. choose_load vs Replay ownership vs present (2026-08-02)
|
||||
|
||||
Independent layers (do not fix load bugs by changing Replay behavior):
|
||||
|
||||
```text
|
||||
Layer 1 choose_load Where do I start?
|
||||
Layer 2 commit-tip snap Prefer newest safe snap that exists
|
||||
Layer 3 Replay ownership How do I reach the frontier? (no restart)
|
||||
Layer 4 Presentation How does the player see it?
|
||||
Layer 5 SPAN size Last — only if depth still justifies it
|
||||
```
|
||||
|
||||
**Invariant (Layer 1):** `choose_load` returns the **newest snapshot both peers
|
||||
can prove is safe** to replay from. Proof sources: commit/ADVANCE watermark,
|
||||
hash_confirm, peer RESOLVED. Peer RESOLVED must **not** lower the load below
|
||||
an HC-proven floor (soak: `ADVANCE 1189→1248` then `clamp→1189` → interval
|
||||
`load=1184` → 65-tick SPAN storm / visual twitch).
|
||||
|
||||
**Layer 2:** Inclusive dense span `[span_lo, through]` so the commit tip itself
|
||||
is preferred over `%iv` fallback when `span_lo == tip`.
|
||||
|
||||
**Layer 3:** Ownership SPAN chain continues Replay at the verified tip with
|
||||
`skip-snap` (baseline from current state) — SPAN is bookkeeping, not a
|
||||
semantic restart (`snap applied` every chunk was the twitch).
|
||||
|
||||
**Layer 4:** Catchup live present gated at ~150 ms (not ~frame period).
|
||||
|
||||
**Re-soak watch:** after menu mispredict, `choose_load` near mismatch (depth
|
||||
≲ iv); no `clamp … peer RESOLVED` that drops below `hc_floor`; chain logs
|
||||
`ownership continue skip-snap`; `replay%` / present flashes much lower;
|
||||
`RB_MAX_RESIM_SPAN` unchanged.
|
||||
|
||||
## 52. Seal rows had no rexmit → silent AwaitingBaseline deadlock (2026-08-02)
|
||||
|
||||
**WAN soak (§51 binary):** late-session 4s freeze. Host tip-extended
|
||||
`2598→2599` right after entering Replay (light tip), reached POST, then
|
||||
`verify timeout` → `episode ABORT` → both realign to 2576. Guest never
|
||||
entered Replay: baselines matched (`9bf062ce` both), ready=1, phase stuck at
|
||||
AwaitingBaseline with **zero log output** — the only silent early return in
|
||||
`maybe_enter_replay`'s gate chain is `!rnet_rb_all_peer_seal_rows_complete`.
|
||||
|
||||
**Root cause:** `export_local_seals()` fired only on discrete events
|
||||
(begin / tip-extend / FOLLOW) with no retransmit. The guest's FOLLOW
|
||||
extension grew its `sealed_span` to include 2599, but the host's post-extend
|
||||
re-export carrying the slot-0 row for 2599 was a single unacknowledged UDP
|
||||
burst — one WAN drop (hotspot, RTT 72ms) parked the guest forever. Baseline
|
||||
and GO already had keepalive rexmit (§49); seals did not. The receive-side
|
||||
stash comment even assumed "the sender's normal chunk retransmit cadence
|
||||
re-delivers it" — that cadence never existed.
|
||||
|
||||
**Fix:**
|
||||
1. **Seal keepalive** `maybe_rexmit_seals()` — rides `maybe_send_baseline`'s
|
||||
call path, re-runs `export_local_seals()` on the `RB_BASELINE_BURST_MS`
|
||||
cadence until the peer's POST arrives (POST proves a complete seal table).
|
||||
Idempotent receive (rows OR into `peer_seal_mask`), ≤3 chunks per send.
|
||||
2. **Named stall diag** — `!all_peer_seal_rows_complete` in
|
||||
`maybe_enter_replay` now logs once per episode after 500ms:
|
||||
`rb waiting peer seal rows base=… span=… slotN=0`.
|
||||
|
||||
**Re-soak watch:** no verify-timeout abort where the peer log shows matched
|
||||
baselines + ready but no `rb replay`; if `rb waiting peer seal rows` appears
|
||||
it should be followed by `rb seal rexmit` and a normal Replay entry, not an
|
||||
abort.
|
||||
|
||||
+1
-1
Submodule lib/recomp-net updated: 4cf57b5ad8...1c41a13705
@@ -3767,16 +3767,15 @@ static void netplay_hold_last_present_tick(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* §47: long Replay catch-up — wall-clock gate for a real VRAM present.
|
||||
* Returns 1 if this guest vblank should fall through to a live present. */
|
||||
/* §47/§51 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. */
|
||||
static int netplay_replay_catchup_should_live_present(uint32_t remaining) {
|
||||
static uint64_t s_catchup_present_ms;
|
||||
uint64_t now = SDL_GetTicks64();
|
||||
uint32_t period = (uint32_t)(g_frame_period_ms + 0.5);
|
||||
if (period < 8u)
|
||||
period = 8u;
|
||||
if (period > 33u)
|
||||
period = 33u;
|
||||
/* ~6–7 Hz: enough to show progress without flashing every resim frame
|
||||
* (old period≈frame_ms presented nearly every tick when rem>8). */
|
||||
uint32_t period = 150u;
|
||||
if (remaining <= 8u)
|
||||
return 0;
|
||||
if (s_catchup_present_ms != 0ull && now >= s_catchup_present_ms &&
|
||||
|
||||
+327
-59
@@ -180,6 +180,13 @@ static int g_needs_advance;
|
||||
static int g_seal_export_logged;
|
||||
static int g_baseline_rexmit_logged;
|
||||
static int g_post_rexmit_logged;
|
||||
/* §52: seal-row keepalive — export_local_seals() fired only on discrete
|
||||
* events (begin / tip-extend / FOLLOW); one lost UDP chunk left the peer
|
||||
* silently stuck in AwaitingBaseline (all_peer_seal_rows_complete never
|
||||
* true) until the initiator's verify timeout aborted the episode. */
|
||||
static uint64_t g_last_seal_rexmit_ms;
|
||||
static int g_seal_rexmit_logged;
|
||||
static int g_seal_wait_logged;
|
||||
static uint64_t g_seal_wait_ms; /* CLOCK_MONOTONIC ms when SealInputs began */
|
||||
static uint64_t g_verify_wait_ms;
|
||||
/* Live network-latency estimate, EMA'd from the POST handshake's own
|
||||
@@ -291,6 +298,12 @@ static uint32_t g_last_begin_mismatch = 0xffffffffu;
|
||||
#define RB_POST_BURST 8
|
||||
/* Initiator waits for follower ready-ACK; do NOT solo-enter Replay. */
|
||||
#define RB_READY_TIMEOUT_MS 4000u
|
||||
/* §49: WAN/TURN baseline GO can take multiple RTT; floor stays 4s. */
|
||||
#define RB_READY_TIMEOUT_MAX_MS 12000u
|
||||
/* §49: ownership chain min confirmed ticks ahead of tip. tip+1 (§48) still
|
||||
* opened SPAN every Verify on WAN (epochs 16→64 in ~40 ticks). tip+8 lets
|
||||
* Live absorb micro catch-ups; larger frontiers still Replay-own. */
|
||||
#define RB_OWNERSHIP_CHAIN_MIN_AHEAD 8u
|
||||
/* Failed episode (resim/baseline/post): calm from *live* sim before realign.
|
||||
* Was 60/120 — after abort, choose_load stuck at agreed tip so the next begin
|
||||
* resimmed the whole cooldown (depth 63→128) = "pushing further back". */
|
||||
@@ -432,6 +445,9 @@ static int seat_tick_confirmed(int slot, uint32_t tick);
|
||||
/* §47: begin_rewind is an ownership SPAN chain (skip cooldown; force target). */
|
||||
static int g_ownership_chain;
|
||||
static uint32_t g_ownership_chain_frontier;
|
||||
/* §51 Layer 3: SPAN chunk continue — state already at tip after Verify; do
|
||||
* not snap-reload (Replay ownership is one continuous recovery). */
|
||||
static int g_ownership_skip_snap;
|
||||
|
||||
static int rb_fmv_media_active(void)
|
||||
{
|
||||
@@ -529,6 +545,18 @@ static void rb_fmv_tick_settle(void)
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb FMV rewind-defer ON (depth24/mdec; no invent)\n");
|
||||
fflush(stderr);
|
||||
/* §50: TipHold through FMV is doomed — initiator tip-extends /
|
||||
* rereplays while the peer ownership-finals → Live, then follow
|
||||
* REFUSED (FMV lockstep) and tip-extend ABANDON realigns to a
|
||||
* BIOS sticky PC snap. Commit the sealed tip before media runs. */
|
||||
if (g_rb && rnet_rb_is_tip_holding(g_rb)) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb FMV mid TipHold — tip-hold commit "
|
||||
"through=%u\n",
|
||||
(unsigned)rnet_rb_get_target_tick(g_rb));
|
||||
fflush(stderr);
|
||||
finalize_tip_hold();
|
||||
}
|
||||
}
|
||||
/* Heartbeat through the movie — digs are sparse while MDEC is hot. */
|
||||
{
|
||||
@@ -895,6 +923,9 @@ static void clear_episode_wire_state(void)
|
||||
g_last_baseline_burst_ms = 0;
|
||||
g_ready_timeout_logged = 0;
|
||||
g_wait_peer_bl_logged = 0;
|
||||
g_last_seal_rexmit_ms = 0;
|
||||
g_seal_rexmit_logged = 0;
|
||||
g_seal_wait_logged = 0;
|
||||
}
|
||||
|
||||
static void clear_baseline_pin(void)
|
||||
@@ -1032,6 +1063,7 @@ static void abort_episode(const char *why)
|
||||
clear_tip_extend_prime();
|
||||
psx_scheduler_top_level_resume_clear();
|
||||
clear_episode_wire_state();
|
||||
g_ownership_skip_snap = 0;
|
||||
if (g_rb && rnet_rb_is_active(g_rb)) {
|
||||
rnet_rb_set_phase(g_rb, nRNetRbPhaseAbort);
|
||||
rnet_rb_session_reset(g_rb);
|
||||
@@ -1864,20 +1896,34 @@ static void advance_agreed_watermark_from_hc(void)
|
||||
/* Pick a load snap ≤ mismatch that the lagging peer is likely to have.
|
||||
* Live snaps only land on snap_interval ticks; never invent a phantom tick.
|
||||
*
|
||||
* Shared-frontier fast path: ticks the peer PROVABLY holds need no tip slack.
|
||||
* - hc resolved_through: both peers simmed those ticks and core digests
|
||||
* matched, so both saved the same interval snaps.
|
||||
* - last committed span (g_agreed_span_lo, g_agreed_through]: both peers
|
||||
* replayed it and resim saves a snap at every replayed tick (dense).
|
||||
* Without this, mismatch one tick after a commit re-loaded a full interval +
|
||||
* slack deep (798 → 768) and each menu tap replayed ~30 ticks twice — the
|
||||
* main-menu "resim back to title" hang. Peer eviction is still covered by the
|
||||
* follower NACK path (abort, not hang).
|
||||
* Layer 1 invariant (§51):
|
||||
* choose_load returns the newest snapshot both peers can prove is safe
|
||||
* to replay from — independent of how Replay later reaches the frontier.
|
||||
*
|
||||
* Provably-safe sources (any establishes the floor):
|
||||
* - tip-hold / episode commit watermark (g_agreed_through + dense span)
|
||||
* - hash_confirm resolved_through (both simmed, cores matched → same snaps)
|
||||
* - peer RESOLVED advertise (soft; must NOT lower below HC-proven ticks)
|
||||
*
|
||||
* Without the shared-frontier path, mismatch one tick after a commit re-loaded
|
||||
* a full interval + slack deep (798 → 768) — main-menu "resim back to title".
|
||||
* Peer eviction is still covered by follower NACK (abort, not hang).
|
||||
*
|
||||
* Fallback (no proven frontier below mismatch): interval ticks with one-
|
||||
* interval tip slack — ONLY when there is no shared frontier yet. Once
|
||||
* hash_confirm / a commit exists, never load an unconfirmed tip (those snaps
|
||||
* can fork dig_cpu while PC-cleared FRAME_COMMITs still matched). */
|
||||
static uint32_t hc_provable_through(void)
|
||||
{
|
||||
uint32_t rt;
|
||||
if (!g_b.hc)
|
||||
return 0u;
|
||||
rt = netplay_hc_resolved_through(g_b.hc);
|
||||
if (rt == 0u || !netplay_hc_confirm_through(g_b.hc, rt))
|
||||
return 0u;
|
||||
return rt;
|
||||
}
|
||||
|
||||
static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
{
|
||||
uint32_t t;
|
||||
@@ -1885,6 +1931,7 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
uint32_t newest;
|
||||
uint32_t cap;
|
||||
uint32_t shared = 0;
|
||||
uint32_t hc_floor = 0;
|
||||
int have_shared = 0;
|
||||
if (!out_load || !g_snaps || netplay_snap_ring_count(g_snaps) == 0)
|
||||
return 0;
|
||||
@@ -1897,20 +1944,17 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
(void)netplay_hc_heal_stale_gap(g_b.hc);
|
||||
heal_agreed_watermark_if_aged_out();
|
||||
advance_agreed_watermark_from_hc();
|
||||
hc_floor = hc_provable_through();
|
||||
|
||||
if (g_agreed_valid) {
|
||||
/* Hard watermark: tip-hold / episode commit tip. TipHold Live invent
|
||||
* + PC-cleared FRAME_COMMIT can false-confirm tip snaps (848) that
|
||||
* still fork baseline cores/av — never load above this via hc while
|
||||
* the agreed snap is still in the ring (heal raises it when aged out). */
|
||||
/* Commit / ADVANCE watermark. Do not raise above agreed via HC here —
|
||||
* ADVANCE already promotes interval HC ticks; TipHold invent historically
|
||||
* false-confirmed tip snaps above the commit frontier. */
|
||||
shared = g_agreed_through;
|
||||
have_shared = 1;
|
||||
} else if (g_b.hc) {
|
||||
uint32_t rt = netplay_hc_resolved_through(g_b.hc);
|
||||
if (netplay_hc_confirm_through(g_b.hc, rt)) {
|
||||
shared = rt;
|
||||
have_shared = 1;
|
||||
}
|
||||
} else if (hc_floor > 0u) {
|
||||
shared = hc_floor;
|
||||
have_shared = 1;
|
||||
}
|
||||
/* §38/§39: clamp ADVANCE→commit-frontier ONLY during post–tip-hold
|
||||
* holdoff. The always-on ≤runway+4 clamp (holdoff=0) undid healthy
|
||||
@@ -1931,21 +1975,26 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
shared = tip_rt;
|
||||
}
|
||||
}
|
||||
/* §40/§41: clamp to peer RESOLVED while the bounded gate is active.
|
||||
* After GATE_MS, peer_resolved_gate_blocks sticky-expires so HEAL-FORCE
|
||||
* cannot forever-DEFER begin (2026-08-02 post-§40 soak). */
|
||||
if (have_shared &&
|
||||
peer_resolved_gate_blocks(shared)) {
|
||||
static uint32_t s_peer_clamp_log_to;
|
||||
if (s_peer_clamp_log_to != g_peer_resolved_through) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb choose_load clamp %u→%u "
|
||||
"(peer RESOLVED frontier)\n",
|
||||
(unsigned)shared, (unsigned)g_peer_resolved_through);
|
||||
fflush(stderr);
|
||||
s_peer_clamp_log_to = g_peer_resolved_through;
|
||||
/* §40/§41/§51: peer RESOLVED may limit unconfirmed tips above HC, but must
|
||||
* never lower the load below HC-proven ticks (soak: ADVANCE 1189→1248 then
|
||||
* clamp→1189 → interval load=1184 → 65-tick SPAN storm). */
|
||||
if (have_shared && peer_resolved_gate_blocks(shared)) {
|
||||
uint32_t clamped = g_peer_resolved_through;
|
||||
if (hc_floor > clamped)
|
||||
clamped = hc_floor;
|
||||
if (clamped < shared) {
|
||||
static uint32_t s_peer_clamp_log_to;
|
||||
if (s_peer_clamp_log_to != clamped) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb choose_load clamp %u→%u "
|
||||
"(peer RESOLVED; hc_floor=%u)\n",
|
||||
(unsigned)shared, (unsigned)clamped,
|
||||
(unsigned)hc_floor);
|
||||
fflush(stderr);
|
||||
s_peer_clamp_log_to = clamped;
|
||||
}
|
||||
shared = clamped;
|
||||
}
|
||||
shared = g_peer_resolved_through;
|
||||
}
|
||||
/* Snap at T = state after tick T; replaying the mismatch needs load < mismatch. */
|
||||
if (have_shared && mismatch > 0u) {
|
||||
@@ -1967,9 +2016,12 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
for (t = hi;; --t) {
|
||||
int mutual = 0;
|
||||
if (g_agreed_valid) {
|
||||
/* Only committed / tip-hold POST tip and its dense resim span. */
|
||||
if (t <= g_agreed_through &&
|
||||
(t > g_agreed_span_lo || (iv <= 1u) || ((t % iv) == 0u)))
|
||||
/* §51 Layer 2: inclusive dense span [span_lo, through] so
|
||||
* the commit tip itself is preferred over %iv fallback
|
||||
* (1189 tip with span_lo=tip used to miss → load 1184). */
|
||||
if (t <= g_agreed_through && t >= g_agreed_span_lo)
|
||||
mutual = 1;
|
||||
else if ((iv <= 1u) || ((t % iv) == 0u))
|
||||
mutual = 1;
|
||||
} else if (g_b.hc && netplay_hc_confirm_through(g_b.hc, t)) {
|
||||
mutual = 1;
|
||||
@@ -1979,6 +2031,10 @@ static int choose_load_tick(uint32_t mismatch, uint32_t *out_load)
|
||||
t >= g_fmv_media_end_sim) {
|
||||
mutual = 1;
|
||||
}
|
||||
/* HC-proven ticks are mutual even outside the commit span. */
|
||||
if (!mutual && hc_floor > 0u && t <= hc_floor &&
|
||||
netplay_hc_confirm_through(g_b.hc, t))
|
||||
mutual = 1;
|
||||
if (mutual && netplay_snap_ring_has(g_snaps, t)) {
|
||||
*out_load = t;
|
||||
return 1;
|
||||
@@ -2110,6 +2166,38 @@ static void export_local_seals(void)
|
||||
}
|
||||
}
|
||||
|
||||
/* §52: keep re-exporting our seal rows on the baseline-rexmit cadence until
|
||||
* the peer's POST proves it entered Replay (it can only do that once its
|
||||
* seal table is complete). Baseline/GO already had keepalive rexmit; seal
|
||||
* rows were a single unacknowledged burst per discrete event, so one WAN
|
||||
* drop of e.g. the post-tip-extend chunk deadlocked the follower in
|
||||
* AwaitingBaseline until verify timeout. Receiver side is idempotent
|
||||
* (rows OR into peer_seal_mask), and the export is at most a few 24-row
|
||||
* chunks, so the cost is negligible. */
|
||||
static void maybe_rexmit_seals(void)
|
||||
{
|
||||
uint64_t now;
|
||||
if (!g_rb || !rnet_rb_is_active(g_rb) || !rnet_rb_inputs_sealed(g_rb))
|
||||
return;
|
||||
if (g_peer_post_ok)
|
||||
return; /* peer verified with complete seals — nothing left to heal */
|
||||
now = rb_mono_ms();
|
||||
if (g_last_seal_rexmit_ms != 0ull && now >= g_last_seal_rexmit_ms &&
|
||||
(now - g_last_seal_rexmit_ms) < (uint64_t)RB_BASELINE_BURST_MS)
|
||||
return;
|
||||
/* First call per episode is the begin-time export itself; only log once
|
||||
* an actual re-send happens. */
|
||||
if (g_last_seal_rexmit_ms != 0ull && !g_seal_rexmit_logged) {
|
||||
fprintf(stderr, "psxrecomp: rb seal rexmit base=%u span=%u\n",
|
||||
(unsigned)rnet_rb_get_seal_base_tick(g_rb),
|
||||
(unsigned)rnet_rb_get_seal_span(g_rb));
|
||||
fflush(stderr);
|
||||
g_seal_rexmit_logged = 1;
|
||||
}
|
||||
g_last_seal_rexmit_ms = now;
|
||||
export_local_seals();
|
||||
}
|
||||
|
||||
static void publish_sealed_sio(uint32_t tick)
|
||||
{
|
||||
int slot;
|
||||
@@ -2325,6 +2413,21 @@ static void stash_seal_rows_chunk(uint32_t epoch, uint32_t mismatch, uint32_t ta
|
||||
}
|
||||
}
|
||||
|
||||
/* §49: ready budget scales with measured RTT (TURN/hotspot). */
|
||||
static uint32_t rb_ready_timeout_ms(void)
|
||||
{
|
||||
uint32_t rtt = psx_netplay_rb_rtt_estimate_ms();
|
||||
uint32_t ms = RB_READY_TIMEOUT_MS;
|
||||
if (rtt >= 4u) {
|
||||
uint32_t scaled = rtt * 10u; /* ~5 RTT round-trips of headroom */
|
||||
if (scaled > ms)
|
||||
ms = scaled;
|
||||
}
|
||||
if (ms > RB_READY_TIMEOUT_MAX_MS)
|
||||
ms = RB_READY_TIMEOUT_MAX_MS;
|
||||
return ms;
|
||||
}
|
||||
|
||||
static void send_baseline_packet(int ready, int rexmit_log)
|
||||
{
|
||||
RNetSession *s = sess();
|
||||
@@ -2379,6 +2482,10 @@ static void maybe_send_baseline(void)
|
||||
return;
|
||||
if (!g_episode_snap_applied || g_pending_load_valid)
|
||||
return; /* must restore before hashing / advertising baseline */
|
||||
/* §52: seal-row keepalive rides the same call cadence as baseline/GO
|
||||
* rexmit — runs through SealInputs/AwaitingBaseline/Replay/Verify until
|
||||
* the peer POSTs. */
|
||||
maybe_rexmit_seals();
|
||||
follower = rnet_rb_is_from_peer_notify(g_rb) ? 1 : 0;
|
||||
if (!g_local_baseline_sent) {
|
||||
NetplayCoreParts parts;
|
||||
@@ -2438,6 +2545,16 @@ static void maybe_send_baseline(void)
|
||||
send_baseline_burst(1, RB_BASELINE_BURST, 1);
|
||||
} else if (!g_peer_baseline_ready) {
|
||||
send_baseline_burst(0, RB_BASELINE_BURST, 1);
|
||||
} else {
|
||||
/* §49: initiator already saw follower ACK and emitted GO once in
|
||||
* maybe_enter_replay, then stopped advertising. TURN drops that
|
||||
* burst → follower ready-timeout while initiator is in Replay/Verify
|
||||
* waiting for peer POST. Keep GO alive until peer POST arrives. */
|
||||
RNetRbPhase ph = rnet_rb_get_phase(g_rb);
|
||||
if (!g_peer_post_ok &&
|
||||
(ph == nRNetRbPhaseAwaitingBaseline || ph == nRNetRbPhaseReplay ||
|
||||
ph == nRNetRbPhaseVerify))
|
||||
send_baseline_burst(1, RB_BASELINE_BURST, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2619,8 +2736,39 @@ static void maybe_enter_replay(void)
|
||||
if (rnet_rb_get_phase(g_rb) == nRNetRbPhaseReplay)
|
||||
return; /* already armed */
|
||||
/* Never leave SealInputs with incomplete peer pads — hist invent would fork. */
|
||||
if (!rnet_rb_all_peer_seal_rows_complete(g_rb))
|
||||
if (!rnet_rb_all_peer_seal_rows_complete(g_rb)) {
|
||||
/* §52: this was the only *silent* early return in the gate chain —
|
||||
* a lost seal chunk parked us here until the peer's verify timeout
|
||||
* with zero log evidence. Name the incomplete seats once we've been
|
||||
* waiting long enough that keepalive rexmit should have healed it. */
|
||||
if (!g_seal_wait_logged && g_baseline_handshake_ms != 0ull) {
|
||||
uint64_t now = rb_mono_ms();
|
||||
if (now >= g_baseline_handshake_ms &&
|
||||
(now - g_baseline_handshake_ms) >= 500ull) {
|
||||
char seats[64];
|
||||
int pos = 0;
|
||||
int slot;
|
||||
int n = g_b.slot_count ? *g_b.slot_count : 0;
|
||||
int local = g_b.local_slot ? *g_b.local_slot : 0;
|
||||
seats[0] = '\0';
|
||||
for (slot = 0; slot < n && pos < (int)sizeof(seats) - 12; ++slot) {
|
||||
if (slot == local)
|
||||
continue;
|
||||
pos += snprintf(seats + pos, sizeof(seats) - (size_t)pos,
|
||||
" slot%d=%u", slot,
|
||||
(unsigned)rnet_rb_peer_seal_rows_complete(
|
||||
g_rb, (int32_t)slot));
|
||||
}
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb waiting peer seal rows base=%u span=%u%s\n",
|
||||
(unsigned)rnet_rb_get_seal_base_tick(g_rb),
|
||||
(unsigned)rnet_rb_get_seal_span(g_rb), seats);
|
||||
fflush(stderr);
|
||||
g_seal_wait_logged = 1;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (rnet_rb_get_phase(g_rb) != nRNetRbPhaseAwaitingBaseline)
|
||||
return;
|
||||
if (g_peer_baseline_digest != g_local_baseline_digest) {
|
||||
@@ -2656,15 +2804,18 @@ static void maybe_enter_replay(void)
|
||||
* ready-ACK RTT (both peers enter Replay on digest match). Full episodes
|
||||
* keep symmetric ready (follower ACK → initiator GO). */
|
||||
if (!rnet_rb_recommend_light_tip(g_rb)) {
|
||||
uint32_t ready_ms = rb_ready_timeout_ms();
|
||||
if (follower) {
|
||||
send_baseline_burst(1, RB_BASELINE_BURST, 0);
|
||||
if (!g_peer_baseline_ready) {
|
||||
uint64_t now = rb_mono_ms();
|
||||
uint64_t t0 = g_baseline_handshake_ms ? g_baseline_handshake_ms : now;
|
||||
if (now >= t0 && (now - t0) >= (uint64_t)RB_READY_TIMEOUT_MS) {
|
||||
if (now >= t0 && (now - t0) >= (uint64_t)ready_ms) {
|
||||
if (!g_ready_timeout_logged) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb ready timeout — abort (no initiator GO)\n");
|
||||
"psxrecomp: rb ready timeout — abort (no initiator GO) "
|
||||
"waited=%ums budget=%u\n",
|
||||
(unsigned)(now - t0), (unsigned)ready_ms);
|
||||
fflush(stderr);
|
||||
g_ready_timeout_logged = 1;
|
||||
}
|
||||
@@ -2676,10 +2827,12 @@ static void maybe_enter_replay(void)
|
||||
uint64_t now = rb_mono_ms();
|
||||
uint64_t t0 = g_baseline_handshake_ms ? g_baseline_handshake_ms : now;
|
||||
send_baseline_burst(0, RB_BASELINE_BURST, 1);
|
||||
if (now >= t0 && (now - t0) >= (uint64_t)RB_READY_TIMEOUT_MS) {
|
||||
if (now >= t0 && (now - t0) >= (uint64_t)ready_ms) {
|
||||
if (!g_ready_timeout_logged) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb ready timeout — abort (no peer ready-ACK)\n");
|
||||
"psxrecomp: rb ready timeout — abort (no peer ready-ACK) "
|
||||
"waited=%ums budget=%u\n",
|
||||
(unsigned)(now - t0), (unsigned)ready_ms);
|
||||
fflush(stderr);
|
||||
g_ready_timeout_logged = 1;
|
||||
}
|
||||
@@ -2921,6 +3074,8 @@ static void ownership_chain_next_span(uint32_t tip, uint32_t frontier)
|
||||
g_tip_extend_from_tick = 0;
|
||||
clear_tip_extend_prime();
|
||||
g_needs_advance = 0;
|
||||
/* §51 Layer 3: keep sim at tip — next SPAN is extend/continue, not restart. */
|
||||
g_ownership_skip_snap = 1;
|
||||
g_episode_snap_applied = 0;
|
||||
g_pending_resume_valid = 0;
|
||||
g_episode_baseline_matched = 0;
|
||||
@@ -2938,7 +3093,7 @@ static void ownership_chain_next_span(uint32_t tip, uint32_t frontier)
|
||||
if (local != 0) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb ownership chain tip=%u frontier=%u — wait FOLLOW "
|
||||
"(slot %d; peer initiates)\n",
|
||||
"(slot %d; peer initiates; skip-snap)\n",
|
||||
(unsigned)tip, (unsigned)frontier, local);
|
||||
fflush(stderr);
|
||||
return;
|
||||
@@ -2951,7 +3106,7 @@ static void ownership_chain_next_span(uint32_t tip, uint32_t frontier)
|
||||
g_ownership_chain = 1;
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb ownership chain tip=%u frontier=%u mismatch=%u "
|
||||
"(Replay owns catch-up — no TipHold)\n",
|
||||
"(Replay owns catch-up — no TipHold; skip-snap)\n",
|
||||
(unsigned)tip, (unsigned)frontier, (unsigned)mismatch);
|
||||
fflush(stderr);
|
||||
if (!psx_netplay_rb_begin_rewind(mismatch, slot)) {
|
||||
@@ -2959,6 +3114,7 @@ static void ownership_chain_next_span(uint32_t tip, uint32_t frontier)
|
||||
"psxrecomp: rb ownership chain FAILED tip=%u frontier=%u — Live\n",
|
||||
(unsigned)tip, (unsigned)frontier);
|
||||
fflush(stderr);
|
||||
g_ownership_skip_snap = 0;
|
||||
enter_tip_hold(tip);
|
||||
finalize_tip_hold();
|
||||
}
|
||||
@@ -2972,11 +3128,20 @@ static void ownership_on_post_match(uint32_t tip)
|
||||
|
||||
/* Contiguous frontier from the tick after the verified tip. */
|
||||
frontier = psx_netplay_rb_confirmed_frontier(tip + 1u);
|
||||
/* §48: a single confirmed tick ahead is Live work — chaining SPAN for
|
||||
* frontier=tip+1 dual-initiated every Verify and thrashed menus. */
|
||||
if (frontier > tip + 1u) {
|
||||
ownership_chain_next_span(tip, frontier);
|
||||
return;
|
||||
/* §48/§49: micro frontiers are Live work. §48 blocked tip+1; §49 raises
|
||||
* the floor so WAN SPAN handshakes are not opened for 2–7 tick gaps. */
|
||||
{
|
||||
uint32_t min_ahead = RB_OWNERSHIP_CHAIN_MIN_AHEAD;
|
||||
const char *e = getenv("PSX_RB_CHAIN_MIN_AHEAD");
|
||||
if (e && e[0]) {
|
||||
int v = atoi(e);
|
||||
if (v >= 1 && v <= 64)
|
||||
min_ahead = (uint32_t)v;
|
||||
}
|
||||
if (frontier > tip + min_ahead) {
|
||||
ownership_chain_next_span(tip, frontier);
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Exhausted contiguous confirmed work → Live (immediate tip-hold commit). */
|
||||
fprintf(stderr,
|
||||
@@ -3491,6 +3656,7 @@ void psx_netplay_rb_start(void)
|
||||
g_tip_extend_rereplay = 0;
|
||||
g_tip_extend_prime_tick = 0;
|
||||
g_tip_extend_from_tick = 0;
|
||||
g_ownership_skip_snap = 0;
|
||||
g_last_commit_epoch = 0xffffffffu;
|
||||
g_peer_commit_epoch = 0xffffffffu;
|
||||
g_peer_commit_tick = 0;
|
||||
@@ -3539,6 +3705,7 @@ void psx_netplay_rb_shutdown(void)
|
||||
g_tip_extend_rereplay = 0;
|
||||
g_tip_extend_prime_tick = 0;
|
||||
g_tip_extend_from_tick = 0;
|
||||
g_ownership_skip_snap = 0;
|
||||
g_last_commit_epoch = 0xffffffffu;
|
||||
g_peer_commit_epoch = 0xffffffffu;
|
||||
g_peer_commit_tick = 0;
|
||||
@@ -3627,6 +3794,31 @@ static int try_apply_pending_load(CPUState *cpu_in)
|
||||
if (!g_b.bios_checksum || !g_b.entry_pc)
|
||||
return 0;
|
||||
live_realign = g_live_realign_pending;
|
||||
/* §51 Layer 3: ownership SPAN continue — already at tip after Verify POST.
|
||||
* Snap reload is a semantic restart; skip it and baseline from current state.
|
||||
* Only when load equals the tip we just watermarked (not a fresh deep begin). */
|
||||
if (!live_realign && g_ownership_skip_snap &&
|
||||
g_agreed_valid && g_pending_load_tick == g_agreed_through) {
|
||||
uint32_t loaded_tick = g_pending_load_tick;
|
||||
g_ownership_skip_snap = 0;
|
||||
g_pending_load_valid = 0;
|
||||
g_pending_resume_valid = 0;
|
||||
g_episode_snap_applied = 1;
|
||||
g_episode_load_tick = loaded_tick;
|
||||
pin_baseline_from_ring(loaded_tick);
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb ownership continue skip-snap load=%u "
|
||||
"(no reload; Replay ownership)\n",
|
||||
(unsigned)loaded_tick);
|
||||
fflush(stderr);
|
||||
maybe_send_baseline();
|
||||
return 1;
|
||||
}
|
||||
if (g_ownership_skip_snap &&
|
||||
!(g_agreed_valid && g_pending_load_tick == g_agreed_through)) {
|
||||
/* Stale continue flag vs a fresh deep begin — fall through to load. */
|
||||
g_ownership_skip_snap = 0;
|
||||
}
|
||||
{
|
||||
int loaded = 0;
|
||||
uint32_t loaded_tick = g_pending_load_tick;
|
||||
@@ -3702,6 +3894,13 @@ static int try_apply_pending_load(CPUState *cpu_in)
|
||||
g_pending_resume_pc = pc;
|
||||
g_pending_resume_valid = 1;
|
||||
if (live_realign) {
|
||||
/* Do NOT clear remote tip rings here (§49b). Full clear left
|
||||
* tip=0 → mutual pcap FREEZE (wire≫P, neither advances).
|
||||
* Selective clear also fails after ring wrap (only far tips
|
||||
* remain). Prior soak recovered without clear: peer
|
||||
* post-realign production fills need; prepare_local_tip still
|
||||
* emits. Keep absurd tip in the ring — cushion refuses invent
|
||||
* on miss (§45) until real rows land. */
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb realign applied tick=%u pc=0x%08x core=%08x "
|
||||
"cd=%08x\n",
|
||||
@@ -3947,6 +4146,37 @@ int psx_netplay_rb_tip_extend(uint32_t mismatch_tick, int slot)
|
||||
phase != nRNetRbPhaseReplay && phase != nRNetRbPhaseVerify &&
|
||||
phase != nRNetRbPhaseTipHold)
|
||||
return 0;
|
||||
/* §50: begin/follow already refuse NEW episodes in FMV lockstep, but
|
||||
* tip-extend on an open episode still raised the tip through media /
|
||||
* settle. Host rereplayed past guest ownership-final → Live, guest
|
||||
* FMV-refused the follow BEGIN, host ABANDON-realigned to bfc03c04 —
|
||||
* FIRST CORE + lasting CD fork. Same gate as begin: no tip raise. */
|
||||
(void)rb_in_fmv_defer_rewind_window();
|
||||
if (rb_in_fmv_lockstep_window()) {
|
||||
if (phase_at_entry == nRNetRbPhaseTipHold) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb tip-extend REFUSED mismatch=%u — FMV "
|
||||
"lockstep (tip-hold commit)\n",
|
||||
(unsigned)mismatch_tick);
|
||||
fflush(stderr);
|
||||
finalize_tip_hold();
|
||||
return 1; /* absorbed: episode closed at sealed tip */
|
||||
}
|
||||
{
|
||||
static uint32_t s_fmv_ext_refuse_sim;
|
||||
uint32_t sim_now = rnet_session_sim_tick(s);
|
||||
if (s_fmv_ext_refuse_sim != sim_now) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb tip-extend REFUSED mismatch=%u — FMV "
|
||||
"lockstep (keep target=%u; no raise through media)\n",
|
||||
(unsigned)mismatch_tick,
|
||||
(unsigned)rnet_rb_get_target_tick(g_rb));
|
||||
fflush(stderr);
|
||||
s_fmv_ext_refuse_sim = sim_now;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
load = rnet_rb_get_load_tick(g_rb);
|
||||
if (mismatch_tick < load)
|
||||
return 0;
|
||||
@@ -4192,6 +4422,21 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* §51 Layer 3: chain continue must load exactly at the verified tip. */
|
||||
if (g_ownership_chain && mismatch_tick > 0u) {
|
||||
uint32_t tip = mismatch_tick - 1u;
|
||||
if (g_ownership_skip_snap ||
|
||||
(g_snaps && netplay_snap_ring_has(g_snaps, tip))) {
|
||||
if (load != tip) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb ownership force load %u→%u "
|
||||
"(chain tip; skip-snap=%d)\n",
|
||||
(unsigned)load, (unsigned)tip, g_ownership_skip_snap);
|
||||
fflush(stderr);
|
||||
load = tip;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* §38/§39: during tip-hold agreed holdoff, do not open load above the
|
||||
* commit frontier (peer may still tip-hold). Clamp in choose_load should
|
||||
* already pin load; this is belt-and-suspenders. */
|
||||
@@ -4211,19 +4456,23 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot)
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
/* §40/§41: defer above peer RESOLVED only while the bounded gate holds. */
|
||||
/* §40/§41/§51: defer above peer RESOLVED only while the gate holds — but
|
||||
* HC-proven loads are mutually safe (do not DEFER below hc_floor). */
|
||||
if (peer_resolved_gate_blocks(load)) {
|
||||
static uint32_t s_peer_defer_sim;
|
||||
if (s_peer_defer_sim != sim) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb begin DEFER mismatch=%u load=%u "
|
||||
"frontier=%u — peer RESOLVED\n",
|
||||
(unsigned)mismatch_tick, (unsigned)load,
|
||||
(unsigned)g_peer_resolved_through);
|
||||
fflush(stderr);
|
||||
s_peer_defer_sim = sim;
|
||||
uint32_t floor = hc_provable_through();
|
||||
if (load > floor) {
|
||||
static uint32_t s_peer_defer_sim;
|
||||
if (s_peer_defer_sim != sim) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb begin DEFER mismatch=%u load=%u "
|
||||
"frontier=%u hc_floor=%u — peer RESOLVED\n",
|
||||
(unsigned)mismatch_tick, (unsigned)load,
|
||||
(unsigned)g_peer_resolved_through, (unsigned)floor);
|
||||
fflush(stderr);
|
||||
s_peer_defer_sim = sim;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (s_refuse_suppressed > 0u) {
|
||||
fprintf(stderr,
|
||||
@@ -4334,6 +4583,25 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin
|
||||
* forks history regardless of what the initiator saw). Verify always
|
||||
* rereplays. */
|
||||
int need_rereplay = (wire_flags & RNET_RB_SYNC_FLAG_REREPLAY) ? 1 : 0;
|
||||
/* §50: mirror initiator — do not absorb tip raises through FMV. */
|
||||
(void)rb_in_fmv_defer_rewind_window();
|
||||
if (rb_in_fmv_lockstep_window()) {
|
||||
if (phase_at_entry == nRNetRbPhaseTipHold) {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb tip-extend FOLLOW REFUSED epoch=%u "
|
||||
"%u→%u — FMV lockstep (tip-hold commit)\n",
|
||||
(unsigned)epoch, (unsigned)old_t, (unsigned)target);
|
||||
fflush(stderr);
|
||||
finalize_tip_hold();
|
||||
} else {
|
||||
fprintf(stderr,
|
||||
"psxrecomp: rb tip-extend FOLLOW REFUSED epoch=%u "
|
||||
"%u→%u — FMV lockstep (keep target)\n",
|
||||
(unsigned)epoch, (unsigned)old_t, (unsigned)target);
|
||||
fflush(stderr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (phase_at_entry == nRNetRbPhaseTipHold)
|
||||
need_rereplay |= (sim_now > old_t) ? 1 : 0;
|
||||
else if (phase_at_entry == nRNetRbPhaseVerify)
|
||||
|
||||
Reference in New Issue
Block a user