mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
rollback patches
This commit is contained in:
@@ -7,11 +7,13 @@ Today MotK / psxrecomp lobby netplay defaults to **rollback**:
|
||||
`stage_local → poll_admit (invent within P) → guest frame → finish_frame`.
|
||||
Missing remotes are invented (**hold-last**, MotK digital) only while
|
||||
`wire_need ≤ highest_remote + P`; outside that window admit stalls (BattleShip
|
||||
phase_lock). Idle invent re-mismatched every held D-pad tick after commit
|
||||
(char-select episode storm). Seal gap-fill stays idle. Late wire goes through
|
||||
the input contract; rewinds open an `RNetRbSession` episode. Menu releases
|
||||
rewind (FMV soft-promote release-only only). Host **Disable Rollback** (or
|
||||
`PSX_NET_MODE=delay`) forces classic delay-sync `try_admit`.
|
||||
phase_lock). Gap=1 uses a **short** invent grace before hold-last (§23);
|
||||
gap≥2 uses the full §21 budget. Idle invent re-mismatched every held D-pad
|
||||
tick after commit (char-select episode storm). Seal gap-fill stays idle.
|
||||
Late wire goes through the input contract; rewinds open an `RNetRbSession`
|
||||
episode. Menu releases rewind (FMV soft-promote release-only only). Host
|
||||
**Disable Rollback** (or `PSX_NET_MODE=delay`) forces classic delay-sync
|
||||
`try_admit`.
|
||||
|
||||
---
|
||||
|
||||
@@ -28,7 +30,8 @@ rewind (FMV soft-promote release-only only). Host **Disable Rollback** (or
|
||||
|
||||
Env gate: `PSX_NET_MODE=delay|rollback`. Lobby default publishes
|
||||
`match_caps.rollback=true`; **Disable Rollback** publishes false. Auto D/P from
|
||||
RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction.
|
||||
RTT (`P = 4 + D`, see §22); optional Manual Input Delay / Prediction.
|
||||
Adaptive mid-match delay bumps are always on (no lobby disable).
|
||||
|
||||
---
|
||||
|
||||
@@ -74,6 +77,11 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction.
|
||||
- [x] `load_tick` tip slack (one `snap_interval` behind newest) so lagging
|
||||
peer still has the snap; follow refuse → SYNC `initiator=0` NACK;
|
||||
initiator aborts SealInputs (plus 4s seal timeout)
|
||||
- [x] SEAL_ROWS: stash if episode not open yet (same TURN/UDP reordering race
|
||||
as BASELINE below — the initiator's `export_local_seals()` burst can
|
||||
beat a delayed SYNC BEGIN through the relay); unstashed on
|
||||
`begin_follower` open, keyed by epoch (2026-08-01 scheduler asymmetry
|
||||
review gap-close).
|
||||
- [x] BASELINE: stash if episode not open yet; burst rexmit on TURN; initiator
|
||||
ready-timeout → Replay (Verify still waits for peer POST)
|
||||
- [x] Resume PC: reject low/vector junk (`0xB0` etc.); prefer function entries;
|
||||
@@ -1405,3 +1413,847 @@ ghost `pub=held wire=ffff` release episodes; no
|
||||
`agreed ADVANCE tip→tip+N` immediately after tip-hold enter/commit from
|
||||
stale HC; tip-hold→commit should often be non-adjacent when a release
|
||||
extends the tip.
|
||||
|
||||
---
|
||||
|
||||
## 18. POST-diverge tooling + post-realign cooldown (2026-08-01)
|
||||
|
||||
**Post-§17 soak:** coalesce worked (14 tip-extends, 15 coalesce-ahead,
|
||||
6/23 tip-holds extended by up to 24 ticks). One new failure mode: at tip
|
||||
9203 both peers POSTed different cores (`8a460717` vs `138700d6`),
|
||||
aborted, realigned to 9200 — then `RB_ABORT_COOLDOWN_TICKS=24` made every
|
||||
subsequent real dpad edge `promote-no-resim reason=cooldown`, so the
|
||||
fork never self-corrected (live digs still mismatched at 9216/9248 until
|
||||
disconnect). Also: POST only compared tip digests, so there was no
|
||||
visibility into *which earlier tick* in the sealed span actually forked.
|
||||
|
||||
**Fixes landed:**
|
||||
|
||||
- [x] **POST-DIVERGE diag dump** (before `abort_episode` clears seals):
|
||||
for `load..tip`, print sealed buttons + hist overlay per seat, then
|
||||
local/peer FRAME_COMMIT cores from the HC ring, highlighting
|
||||
`FIRST_MISMATCH` when the fork is earlier than the POST tip.
|
||||
Grep: `rb POST-DIVERGE`. Both peers print the same shape — diff the
|
||||
two logs at the abort to see seal/FC asymmetry.
|
||||
- [x] **`netplay_hc_peer_digest`** getter (symmetric with `local_digest`)
|
||||
for the FC scan; unit-tested in `test_netplay_hash_confirm`.
|
||||
- [x] **Skip rewind cooldown after a successful POST realign**
|
||||
(`RB_POST_REALIGN_COOLDOWN_TICKS=0` + `clear_rewind_cooldown`).
|
||||
Storm cooldown (`streak >= 2`) and "no realign tip" baseline
|
||||
cooldown are unchanged. Log: `rb rewind cooldown cleared (...post...)`.
|
||||
|
||||
**Re-soak checks:** on the next `post core/aux diverge`, expect a
|
||||
`POST-DIVERGE diag` / `seals` / `fc FIRST_MISMATCH` block immediately
|
||||
before `episode ABORT`; after realign, expect `rewind cooldown cleared`
|
||||
(not `cooldown until sim=…`); subsequent real edges should open episodes
|
||||
again instead of `promote-no-resim reason=cooldown`. If
|
||||
`FIRST_MISMATCH` is earlier than POST tip, that tick is the next
|
||||
investigation target (sealed-pad asymmetry vs. sim nondeterminism).
|
||||
|
||||
---
|
||||
|
||||
## 19. Tip-extend invent-idle seal + POST storm cooldown (2026-08-01)
|
||||
|
||||
**Post-§18 soak:** tip-hold coalesce/tip-extend worked through a press→
|
||||
release→press chain (epoch 21, tip 1416→1425), then POST diverged at
|
||||
1425 (`310e567b` vs `dd8432b2`). Diag:
|
||||
|
||||
- Host arm audit: `s1=ff7f` (LEFT — correct wire-promoted resign)
|
||||
- Host POST dump: `t=1425 s1=ffffP/h=ff7f!` (seal invent-idle vs hist LEFT)
|
||||
- Peer dump: `s1=ffffP/h=ffffP` (FOLLOW local seat never had the press in hist)
|
||||
- Prior `ready timeout` left `streak=1`; POST → `streak=2` → **storm
|
||||
cooldown** (`until sim=1474`) → promote-no-resim while Live stayed
|
||||
forked at pin 1392 (`baseline core mismatch` storm)
|
||||
|
||||
**Root cause:**
|
||||
|
||||
1. Tip-hold invent-cap stalls Live at the tip, so FOLLOW's local hist
|
||||
lacks `tip+1..edge` even though the pad was already sampled into the
|
||||
delay ring at `sim+delay`. Seal `get_input_row` fell through to
|
||||
`invent_idle` → exported `ffffP` for the correction seat.
|
||||
2. Initiator had correctly resigned from wire peek (`ff7f`), then
|
||||
`apply_peer_seal_rows` accepted the FOLLOW invent and overwrote it.
|
||||
3. §18's `POST_REALIGN_COOLDOWN=0` was skipped by the storm branch
|
||||
(`streak >= 2`); tip-hold commit did not clear the abort streak.
|
||||
|
||||
**Fixes landed:**
|
||||
|
||||
- [x] **`host_get_input_row` / tip-extend prefresh:** promote from
|
||||
`rnet_session_peek(_remote)_input` at `wire_tick_from_sim` before
|
||||
hist/invent; tip-extend (initiator + FOLLOW) prefreshes the
|
||||
correction + local seats over the new tip range.
|
||||
- [x] **`rnet_rb_apply_peer_seal_rows`:** reject predicted peer rows that
|
||||
would clobber a non-predicted sealed row (still credit the mask).
|
||||
- [x] **POST realign never storm-cools** when a realign tip exists
|
||||
(clears streak + `RB_POST_REALIGN_COOLDOWN_TICKS`); tip-hold
|
||||
finalize also clears `g_bl_mismatch_streak`.
|
||||
|
||||
**Re-soak checks:** tip-extend rereplay arm/fin should keep matching
|
||||
auth pads (`s1=ff7f` not `ffffP`); POST dump should not show
|
||||
`ffffP/h=ff7f!`; on POST diverge expect `rewind cooldown cleared
|
||||
(...post...)` even after a prior ready-timeout; no immediate baseline
|
||||
mismatch storm at the pin from promote-no-resim.
|
||||
|
||||
---
|
||||
|
||||
## 20. Skip presentation during resim ticks (2026-08-01)
|
||||
|
||||
**Motivation:** with §17-19 landed, soaks were spending a large fraction
|
||||
of wall-clock time in `Replay` (the `[FPS] ... replay=NN% (n=…)` line).
|
||||
Every resimulated vblank still ran the *full* GL/VK/SW present path
|
||||
(`gl_renderer_present_vram` / `gl_renderer_sync_cpu` FBO readback,
|
||||
`vk_renderer_present_*`, or the software `SDL_UpdateTexture` +
|
||||
`SDL_RenderPresent` path) even though none of that pixel work is visible
|
||||
— it gets fully overwritten by the next resimulated (or the final Live)
|
||||
frame. Host audio pump was already skipped during resim (see the
|
||||
existing `!psx_netplay_is_resimulating()` guard around `sdl_audio_update`
|
||||
in `sdl_vblank_present_body`); presentation had no equivalent guard.
|
||||
|
||||
**Fix:** added a `psx_netplay_is_resimulating()` early return in
|
||||
`sdl_vblank_present_body()` (`runtime/src/main.cpp`) immediately before
|
||||
the `---- Display from our VRAM ----` block, mirroring the existing
|
||||
audio-pump skip. This is presentation-only:
|
||||
|
||||
- `psx_netplay_finish_frame()` (the digest/hash-confirm capture) and the
|
||||
`ep.do_epilogue` / `ep.override` / `ep.skip_pace` epilogue flags used
|
||||
by `sdl_vblank_present()` for netplay admit/pace are all latched
|
||||
*before* this point in the function, so admit/pace timing is
|
||||
unaffected.
|
||||
- `gpu_depth24_present_hold_tick()` and the widescreen-engage check
|
||||
(which must tick every vblank per their own comments) run *before*
|
||||
the new guard, so their bookkeeping is preserved.
|
||||
- Host-only presentation state (frame-blend `prev_buf`, disabled-frame-
|
||||
presented latch, present ring) simply doesn't advance on resim ticks —
|
||||
the same category of skip already used by the blank-display,
|
||||
catchup-budget, and turbo-loads early returns elsewhere in this
|
||||
function. Live's next real present after the episode settles still
|
||||
draws the final, correct frame.
|
||||
- Digest content is unaffected: `host_state_digest()` never reads GPU/
|
||||
VRAM/framebuffer state (core+aux hashing is CPU/RAM/IRQ/timers/CD/SPU
|
||||
register state, not pixels), so skipping the pixel work cannot change
|
||||
what gets hashed or sent over the wire.
|
||||
|
||||
Debug and Release builds both compile clean after the change.
|
||||
|
||||
**Re-soak checks:** `[FPS]` line's `replay=NN%` should be unaffected
|
||||
(same resim ratio, since this doesn't change *when* resim happens, only
|
||||
whether it presents), but wall-clock game FPS during heavy-resim spans
|
||||
(chain-episoding, tip-hold storms) should improve since the GL readback/
|
||||
present cost is no longer paid on non-terminal resim vblanks. No new
|
||||
divergence should appear — this change touches host presentation only,
|
||||
never digested/guest state.
|
||||
|
||||
---
|
||||
|
||||
## 21. Invent-grace ceiling scales with `input_delay` (2026-08-01)
|
||||
|
||||
**Motivation:** a soak sweep across `PSX_NET_DELAY` values (`D=4` twice
|
||||
via the launcher's "default" and an explicit `d=4`, plus `D=6`) showed
|
||||
raising `D` did not reduce prediction — it made it *worse*. Normalized
|
||||
per 1000 sim ticks:
|
||||
|
||||
| | D=4 | D=6 |
|
||||
|---|---|---|
|
||||
| `rb invent grace` events | 127.7 | 232.0 |
|
||||
| `tip-hold coalesce-ahead` | 18.9 | 73.0 |
|
||||
| `rb episode commit` | 23.9 | 28.4 |
|
||||
| `rb wire rewind-request` | 24.4 | 23.4 |
|
||||
|
||||
Invent (predict) frequency roughly doubled and coalesce-ahead nearly
|
||||
quadrupled from D=4→D=6, while the actual correction/rewind rate barely
|
||||
moved — the opposite of what a bigger delay buffer should buy.
|
||||
|
||||
**Root cause:** `np_try_admit_rollback()` computes `wire = sim + D`
|
||||
(`rnet_wire_tick_from_sim`) but the decision to invent vs. stall never
|
||||
used `D` at all. Any gap `wire > highest_remote_wire + 1` immediately
|
||||
falls into `np_invent_grace_stall()`, whose patience budget was
|
||||
`1.5x measured RTT`, hard-capped at **60ms regardless of `D`** (40ms in
|
||||
the pre-RTT startup fallback). So choosing `D=6` (100ms of nominal
|
||||
buffer) vs `D=4` (66.8ms) never changed how long the sim was willing to
|
||||
wait for a late remote sample before predicting — the configured delay
|
||||
only changed *which* wire tick was being requested, not the patience
|
||||
for it. With this soak's RTTs already well under the 60ms ceiling
|
||||
(17-53ms), the ceiling — not the delay — was the binding constraint the
|
||||
whole time, so the extra `D` bought nothing but longer predicted runs to
|
||||
later reconcile (more invents, more coalescing to absorb them, same
|
||||
rewind rate).
|
||||
|
||||
**Fix landed:** `np_invent_grace_stall()`'s ceiling now scales with
|
||||
`g_np.input_delay`: `rtt_ceiling = clamp(delay_ms/2, 60, 150)` and
|
||||
`fallback_ceiling = clamp(delay_ms/3, 40, 100)`, where
|
||||
`delay_ms = input_delay_ticks * tick_ms` (tick_ms from the live
|
||||
tick-period EMA, nominal 17ms fallback). `D<=4` reproduces the old
|
||||
60/40ms ceilings unchanged (floors preserve historical behavior); larger
|
||||
`D` now actually grants more real wall-clock patience before falling
|
||||
back to prediction, in proportion to the extra buffer the user asked
|
||||
for. The RTT-based *scaling* of the budget itself (`1.5x RTT`) is
|
||||
unchanged — only the ceiling moves. The `rb invent grace budget` log
|
||||
line now also prints `delay_ms=` so a soak can confirm the ceiling in
|
||||
effect.
|
||||
|
||||
**Re-soak checks:** re-run the same `D=4` vs `D=6` comparison; expect
|
||||
`rb invent grace budget=...delay_ms=...` to show a materially higher
|
||||
ceiling at `D=6` than `D=4`, and expect invent-events/1k-ticks to no
|
||||
longer scale up with `D` (ideally trend down, since more genuinely-late
|
||||
packets should now clear within the wait instead of triggering a
|
||||
predict). `D<=4` behavior should be bit-for-bit unchanged from prior
|
||||
soaks (floors match the old hard caps).
|
||||
|
||||
---
|
||||
|
||||
## 22. Delay-buffer admit + adaptive delay (2026-08-01)
|
||||
|
||||
**Motivation:** Soaks with manual `D=4` / `P=2` still saw frequent small
|
||||
resims. Expectation mismatch: raising `D` alone does not widen the
|
||||
sim-vs-remote phase gap that gates invent — both peers shift `wire=sim+D`
|
||||
together, so the gap stays ~0–1. The product goal is delay-sync-like
|
||||
operation at the delay edge (buffer filled, invent rare), with the
|
||||
prediction runway as insurance for spikes, freeze+refill when `P` is
|
||||
exhausted, and adaptive `D` growth when freezes persist.
|
||||
|
||||
**Preserved (do not regress):**
|
||||
- Gap gate: invent freely at `gap ≤ 1` (stalling every miss serialized
|
||||
both sims to ~40–50 fps — §12).
|
||||
- Timesync micro-throttle remains the primary phase-lock tool.
|
||||
|
||||
**Lobby / auto D/P (recomp-ui):**
|
||||
| Measured RTT | Input Delay | Prediction (`P = 4 + D`) |
|
||||
| ------------ | ----------: | -----------------------: |
|
||||
| 0–20 ms | 2 | 6 |
|
||||
| 20–50 ms | 2 | 6 |
|
||||
| 50–80 ms | 3 | 7 |
|
||||
| 80–120 ms | 4 | 8 |
|
||||
| 120–160 ms | 5 | 9 |
|
||||
| then steps up | … | `4+D` |
|
||||
|
||||
Manual defaults: **D=2**, **P=6**. Prediction tooltip: keep
|
||||
`prediction = 4 + delay`. No UI to disable adaptive delay — with
|
||||
`P=4+D` freezes should be rare; when they happen, growing `D` is the
|
||||
intended response. Soak-only: `PSX_RB_ADAPT_DELAY=0`.
|
||||
|
||||
**Runtime changes:**
|
||||
1. **Timesync:** skip `note_late` during active episode / tip-hold (resim
|
||||
cost ≠ transit latency). Pegged-streak off-guard raised 12→18.
|
||||
`psx_netplay_timesync_on_episode_boundary()` clears pegged streak on
|
||||
begin / tip-hold / abort (debt kept for post-episode pacing).
|
||||
2. **Admit telemetry:** `rb admit stats invent_gap1/gap2/gap3+
|
||||
pcap_stalls/pcap_enters` every ~5s; invent path buckets by gap.
|
||||
3. **P-cap freeze:** `wire > highest_remote + P` logs
|
||||
`rb pcap FREEZE enter/exit` and counts freeze enters in a 5s window.
|
||||
4. **Adaptive delay (always on):** host (`local_slot==0`) after ≥3 freeze
|
||||
*enters* in 5s, with 10s cooldown, calls
|
||||
`rnet_session_request_delay_change(D+1)` (max 16). `P` stays at the
|
||||
lobby-committed value. Guests apply via DELAY_SYNC.
|
||||
5. **recomp-net DELAY_SYNC:** deferred apply queue
|
||||
`(pending_delay, effective_tick)`; commit in `rnet_session_advance` /
|
||||
`set_sim_tick` when `sim_tick >= effective_tick`. New
|
||||
`rnet_session_request_delay_change`. Retransmit pending packets every
|
||||
50ms while queued. MotK mirrors `g_np.input_delay` from
|
||||
`rnet_session_committed_delay` on admit / after live advance.
|
||||
|
||||
**Re-soak checklist:**
|
||||
- [ ] LAN with auto or manual **D=2 / P=6**: expect fewer invents than the
|
||||
old D=4/P=2 soak; gap1 invents dominate over gap2/gap3+.
|
||||
- [ ] Log lines: `rb admit stats …`, rare/absent `rb pcap FREEZE enter`.
|
||||
- [ ] Induced latency spike: freeze enter → after 3 enters,
|
||||
`rb adaptive delay bump` + both peers `rb delay committed`.
|
||||
- [ ] Timesync does not trip `OFF 10s` solely from tip-hold/resim load.
|
||||
- [ ] Fight scene: fps / episode density vs prior soak; no serialization
|
||||
regression (still inventing at gap≤1).
|
||||
|
||||
---
|
||||
|
||||
## 23. Short invent grace at gap=1 (2026-08-01)
|
||||
|
||||
**Soak evidence (same session pair, TURN):**
|
||||
| Setting | ep/1k ticks | host invent_gap1/1k | pcap freezes |
|
||||
| ------- | ----------: | ------------------: | -----------: |
|
||||
| D=2 P=6 | ~23 | ~260 | 0–1 |
|
||||
| D=4 P=8 | ~25 | ~297 | 0 |
|
||||
|
||||
Raising D/P did nothing — invents were ~99% `gap1` (free path). Adaptive
|
||||
delay never armed (P=8 runway unused). Hitch source remains
|
||||
gap1 invent → edge mispredict → tip-hold/coalesce → resim.
|
||||
|
||||
**Fix:** ahead-only **short** invent grace when
|
||||
`wire == highest_remote + 1`:
|
||||
- Cap = `max(8, min(16, RTT_ms))` (auto), or `PSX_RB_GAP1_GRACE_MS`
|
||||
(0 = old invent-free gap1).
|
||||
- Must stay << one peer guest frame so we do **not** revive the
|
||||
2026-07-31 serialization (full stall on every miss → 40–50 fps).
|
||||
- Gap1 grace expiries do **not** feed the invent-grace adaptive-off
|
||||
streak (would trip constantly under normal phase stagger).
|
||||
- Gap≥2 keeps the full §21 budget unchanged.
|
||||
|
||||
**Timesync:** mispredict debt add ~0.75 tick (was 0.5); admit shave
|
||||
≤4 ms/tick (was 3).
|
||||
|
||||
**Telemetry:** `rb admit stats` now includes `gap1_grace=` (times the
|
||||
short stall returned before invent).
|
||||
|
||||
**Re-soak checks:** compare to D=4/P=8 baseline (~25 ep/1k):
|
||||
- Expect `gap1_grace` rising and `invent_gap1` / ep/1k falling.
|
||||
- Steady FPS must not collapse to ~40–50 with zero episodes (that
|
||||
would mean the short cap is too large / both peers serializing).
|
||||
- `PSX_RB_GAP1_GRACE_MS=0` restores prior gap1 invent-free behavior.
|
||||
|
||||
---
|
||||
|
||||
## 24. Admit-hitch cleanup after gap1 grace (2026-08-01)
|
||||
|
||||
**§23 soak:** ep/1k ~25→14 and invent_gap1/1k ~297→188, but feel was still
|
||||
bad. FPS forensics: **17/53** host windows were admit-dominated hitch
|
||||
(`admit` 12–17 ms, `replay=0%`) — not resim. Causes:
|
||||
|
||||
1. Gap1 grace spun the admit loop (~10k stall returns / ~2k ticks) and
|
||||
often **expired into invent anyway** (stall tax without preventing
|
||||
predict). Cap was up to 16 ms on measured RTT.
|
||||
2. TipHold **coalesce-ahead** reset the quiet timer on every edge
|
||||
(1825 resets) so Live stayed parked in tip-hold with high admit cost.
|
||||
|
||||
**Fixes:**
|
||||
- Gap1 auto cap → `max(4, min(10, RTT/2))`; count `gap1_grace` once per
|
||||
wire tick; after 10 consecutive expire→invent, **OFF 1s**
|
||||
(`rb gap1 invent grace OFF 1s`).
|
||||
- TipHold: **350 ms absolute wall cap** from enter (`rb tip-hold WALL CAP`);
|
||||
pure TipHold coalesce extends `tip_hold_until` but **does not** reset
|
||||
quiet_t0 (rereplay still does); coalesce-ahead log rate-limited.
|
||||
|
||||
**Re-soak:** expect fewer admit-dominated `replay=0%` dips, lower
|
||||
`gap1_grace` counts (per-wire), occasional `gap1 invent grace OFF`,
|
||||
`tip-hold WALL CAP` when menus mash, ep/1k staying ≤§23 or better.
|
||||
|
||||
---
|
||||
|
||||
## 25. TipHold WALL vs quiet + gap1 shrink (2026-08-01)
|
||||
|
||||
**§24 soak:** admit-spin fixed (`gap1_grace/1k` 4550→278, coalesce 1825→32)
|
||||
but feel regressed: **27/27** tip-hold commits were `WALL CAP 350ms`
|
||||
(quiet never won — quiet need ~400ms > WALL 350ms), and gap1 invent-free
|
||||
OFF climbed invent_gap1/1k 188→360 and ep/1k 14→19. Hitching returned as
|
||||
replay-dominated.
|
||||
|
||||
**Fixes:**
|
||||
- TipHold: quiet max **180ms** (can finish); **THRASH CAP 150ms** only when
|
||||
`coalesce_n ≥ 3`; **SAFETY CAP 500ms** last resort. Logs:
|
||||
`tip-hold THRASH CAP` / `tip-hold SAFETY CAP`.
|
||||
- Gap1: on expire→invent streak, **SHRINK to 2ms for 1s** (not invent-free).
|
||||
Log: `rb gap1 invent grace SHRINK 2ms for 1000ms`.
|
||||
|
||||
**Re-soak:** expect mostly quiet tip-hold commits (not WALL), occasional
|
||||
THRASH on mash, invent_gap1 not jumping when SHRINK fires, ep/1k ≤§23.
|
||||
|
||||
---
|
||||
|
||||
## 26. FMV lockstep was re-serializing WAN (2026-08-01)
|
||||
|
||||
**Clue:** `PSX_RB_GAP1_INVENT=1` still stayed ~30 fps on CGNAT/TURN. Early
|
||||
menu with invent free-ran at **~60 fps / admit≈0**; the cliff lined up with
|
||||
`FMV rewind-defer ON` and invent count **0** until `invent_at` (~4s of
|
||||
post-FMV MIN+UNLOCK delay-sync). Tip-hold/replay windows later recovered
|
||||
toward ~50 fps — so prediction *can* drive presentation; admit was still
|
||||
gated on confirmed tip.
|
||||
|
||||
**Root causes:**
|
||||
1. `lockstep_no_invent` = media **and** `g_fmv_lockstep_until` (MIN 180 +
|
||||
UNLOCK_GRACE 64). Correct for depth24 Start/skip; fatal for WAN title
|
||||
menus (network paces every frame).
|
||||
2. Default admit refused invent while `remote_lead > 0` until TIP_STALE
|
||||
(~150ms+) — delay-sync unless ENV forced gap1 invent.
|
||||
3. RUNWAY_EMPTY invent grace floored at 60ms ceiling → ~25ms tax/tick.
|
||||
|
||||
**Fixes:**
|
||||
- No-invent gate = **media + settle (24 ticks) only**. Digest lockstep
|
||||
still drives dense snaps / RELEASE logs, not admit.
|
||||
- Gap1 short-grace→invent is **default ON** (`PSX_RB_GAP1_INVENT=0` for
|
||||
old cushion-wait).
|
||||
- Invent grace OFF after 15 expiries for **5s**; RUNWAY ceiling floor 20 /
|
||||
cap 80 (was 60/150).
|
||||
|
||||
**Re-soak (no ENV needed):** FMV itself may still track packet rate (no
|
||||
invent during media — intentional). Post-FMV menus should invent within
|
||||
~settle and climb toward 60 with rollbacks, not flat ~30. Look for
|
||||
`gap1 invent ON`, `invent grace OFF 5s`, settle log
|
||||
`no invent through settle; then invent+resim §26`.
|
||||
|
||||
---
|
||||
|
||||
## 27. Presentation continuity without resim storms (2026-08-01)
|
||||
|
||||
**Problem:** §26 unlocked invent/resim, but the eye still saw stalls —
|
||||
tip-hold parked Live at tip (`invent_slack=0`), gap1/RUNWAY grace taxed
|
||||
every miss, and deep `RUNWAY_EMPTY` invents (`pred_depth` 4–5) chained
|
||||
episodes. Telemetry showed high `replay%` while `present` starved.
|
||||
|
||||
**Policy pivot:** keep the display clock moving with **shallow** prediction;
|
||||
resim real edges; do not reintroduce delay-sync after invent commits.
|
||||
|
||||
**Fixes:**
|
||||
- TipHold seal slack → library default **2** (was FORCE0); quiet **80ms**,
|
||||
thrash **100ms @ coalesce≥2**, safety **250ms**.
|
||||
- Gap1 invent grace default **0** (`PSX_RB_GAP1_GRACE_MS` override kept).
|
||||
- Invent depth cap: `pred_depth ≥ 2` waits until tip stale (1× invent RTT,
|
||||
floor 40ms); RUNWAY invent grace hard-capped at **8ms**.
|
||||
- FPS line: `present_gap_p95=… ms max=… ms` (primary soak metric).
|
||||
|
||||
**Pass (WAN/TURN, post-settle):** `present_gap_p95 ≤ 33ms`, invent mostly
|
||||
`GAP1` with `pred_depth ≤ 2`, ep/1k moderate (not zero invents, not
|
||||
constant deep resim). FMV media may still be packet-paced (no invent).
|
||||
|
||||
---
|
||||
|
||||
## 28. Gap=1 phase-vs-starvation split — LAN resim storms (2026-08-01)
|
||||
|
||||
**Problem:** §27's `gap1_grace=0` default fixed WAN 30fps delay-sync but
|
||||
regressed LAN. A 2-peer soak at `D=2 P=6` (one peer via TURN relay, one
|
||||
direct host-host) showed **~310 gap1 invents / ~2400 frames**, a first
|
||||
core diverge at sim=962, then a tip-hold commit storm (`ep/1k≈15`,
|
||||
`present_gap_p95` spiking to 3.4s once). The invent geometry was
|
||||
perfectly stable the whole soak:
|
||||
|
||||
```
|
||||
wire = sim+2, remote_tip = sim+1, remote_lead = 1, pred_depth = 1
|
||||
```
|
||||
|
||||
That is **not starvation** — the confirmed remote tip is still one row
|
||||
ahead of sim, i.e. there is real work already in flight. It is a
|
||||
one-tick **phase offset**: this peer is consistently sampling wire a
|
||||
tick before the matching remote row lands. §27 treated every gap=1 miss
|
||||
identically (`invent now`), converting each phase tick into a mispredict
|
||||
→ rewind-request → tip-hold episode.
|
||||
|
||||
**Fix — Case A / Case B split at gap=1** (`np_gap1_grace_cap_ms`,
|
||||
`np_tip_track_advance`/`np_tip_age_ms`, gap1 branch of
|
||||
`np_try_admit_rollback` in `psx_netplay.c`):
|
||||
|
||||
- Track the remote tip's own arrival cadence every admit tick (not just
|
||||
on miss): `g_tip_arrival_ema_ms` = EMA of ms between
|
||||
`highest_remote_wire` advances, `np_tip_age_ms()` = ms since the last
|
||||
advance.
|
||||
- **Case A (healthy/advancing tip):** `remote_lead ≥ 1` and the tip
|
||||
advanced within the last `1.5×` its own cadence. The newest row is
|
||||
probably already in flight — wait `clamp(period − tip_age, 4, 10)` ms
|
||||
before inventing, instead of 0. Logged as `reason=GAP1_PHASE`.
|
||||
- **Case B (stale/starved tip):** `remote_lead ≤ 0`, or the tip hasn't
|
||||
advanced in over 1.5× its cadence (or cadence unknown). Invent
|
||||
immediately — same as §27's default — so genuine WAN gaps stay
|
||||
responsive and this never turns back into cushion-wait.
|
||||
- `PSX_RB_GAP1_GRACE_MS` still forces the old flat cap (bypasses the
|
||||
Case A/B split entirely) for A/B testing or a manual operator override.
|
||||
|
||||
**Telemetry:** `rb admit stats` gained `gap1_case_a=`, `gap1_case_b=`,
|
||||
`tip_ema=` (the learned arrival cadence in ms). Expect on LAN:
|
||||
`gap1_case_a` rising, `gap1_case_b`/`invent_gap1_legacy` mostly flat,
|
||||
`tip_ema` settling near the true tick period (~16–17ms at 60fps),
|
||||
`present_gap_p95` and ep/1k dropping back toward the pre-§27 LAN
|
||||
baseline (~60fps, near-zero episodes) without WAN regressing to 30fps.
|
||||
|
||||
**Re-soak:** LAN pass = few-to-no `GAP1_LEGACY`/`RUNWAY_EMPTY` invents
|
||||
once `tip_ema` converges, `gap1_case_a` dominant, ep/1k near 0, fps back
|
||||
near 60. WAN pass = unchanged from §27 (Case B fires immediately on a
|
||||
genuinely stale/negative-lead tip, so high-RTT links don't gain a hidden
|
||||
per-tick stall).
|
||||
|
||||
---
|
||||
|
||||
## 29. §28 Case A wait was the hitch — scheduler needs pacing, not waiting
|
||||
(2026-08-01)
|
||||
|
||||
**Problem:** the very next LAN soak after §28 showed *worse* hitching.
|
||||
`gap1_case_a`/`gap1_grace` climbed to **1000/1000** (every gap=1 miss took
|
||||
the Case A wait), but only ~12% of those waits actually found the row —
|
||||
88% expired into invent anyway (`invent_gap1_legacy` 883, `GAP1_PHASE`
|
||||
314 logged). `tip_ema` (the learned tip-advance cadence) drifted from
|
||||
15ms to **28-41ms** over the match — 2-3x the true ~16ms tick period —
|
||||
because the wait itself delays the next admit, which delays the next
|
||||
tip-advance observation, which makes the next miss look "fresher" than
|
||||
it is (self-reinforcing feedback loop). Net effect: `admit` cost rose
|
||||
from ~6ms/f to ~8ms/f baseline, first core diverge moved earlier
|
||||
(sim 279 vs 962), ep/1k rose (16→27), and `present_gap` spiked to
|
||||
**~4 seconds** during storm windows with fps dropping to single digits.
|
||||
|
||||
**Root cause, reframed as a scheduler question, not an invent-policy
|
||||
question:** both soaks showed the exact same fixed point regardless of
|
||||
gap1 policy:
|
||||
|
||||
```
|
||||
wire = sim+D, remote_tip = sim+(D-1), remote_lead = D-1, pred_depth = 1
|
||||
```
|
||||
|
||||
This is **rock-stable**, not jittery — the signature of a fixed ~1-tick
|
||||
network transit/scheduling cost consuming one of the two delay frames,
|
||||
combined with a self-reinforcing rate effect: because gap=1 has always
|
||||
resolved via immediate invent (never a wait), the peer that structurally
|
||||
gets there first never has to slow down, so it keeps racing exactly one
|
||||
tick ahead of the other's publish rate indefinitely. A **per-miss wait**
|
||||
cannot fix either half of this: it cannot change a fixed transit delay,
|
||||
and by delaying every admit uniformly it does not reintroduce any
|
||||
asymmetry between the peers — so both soaks converged to the same
|
||||
useless (or actively harmful) place.
|
||||
|
||||
**Fix:** the only mechanism in this codebase that is actually
|
||||
asymmetric-safe here is mispredict-driven `np_timesync_note_late`
|
||||
pacing (`psx_netplay.c`) — soak evidence already established that real
|
||||
mispredicts land almost entirely on one peer (the one racing ahead),
|
||||
so pacing *that* peer down a few ms/tick closes phase without both
|
||||
sides fighting each other or serializing. It was just too weak to
|
||||
matter: MotK's hold-last invent means most gap=1 misses never produce a
|
||||
mispredict (the invented value already matches), so genuine correction
|
||||
signals are rare, and the old debt sizing (add ~0.75 tick/mispredict,
|
||||
cap 2 ticks, shave <=4ms/tick) drained faster than it could accumulate
|
||||
enough to move a persistent D-1 phase.
|
||||
|
||||
- **Reverted** §28's per-miss Case A wait. Gap=1 invents immediately
|
||||
again (§27 behavior) — zero added admit latency. The Case A/B
|
||||
classification is kept as **pure diagnostics** (`GAP1_PHASE` /
|
||||
`GAP1_LEGACY` reason, `gap1_case_a`/`gap1_case_b` counters) computed
|
||||
from the same tip-health signal, but nothing waits on it.
|
||||
`PSX_RB_GAP1_GRACE_MS` still forces the old flat-cap wait for A/B
|
||||
testing.
|
||||
- **Strengthened timesync pacing** (`np_timesync_note_late`,
|
||||
`np_timesync_throttle`): per-mispredict debt add raised from ~0.75
|
||||
tick to ~1.25 ticks, cap raised from 2 ticks to 3 ticks, admit-side
|
||||
shave raised from <=4ms/tick to <=6ms/tick — so a small cluster of
|
||||
real mispredicts (e.g. one fight exchange) can actually walk a stuck
|
||||
D-1 phase back toward D within a handful of ticks instead of dozens.
|
||||
Adaptive-off guards (18 consecutive cap-hits → OFF 10s) are unchanged.
|
||||
|
||||
**What to watch in the next soak:** `gap1_case_a`/`gap1_case_b` should
|
||||
still climb (that's expected — LAN transit still eats ~1 tick most of
|
||||
the time) but `admit` ms/f and `present_gap_p95` should drop back near
|
||||
pre-§27/§28 baselines since no wait is added; `tip_ema` should hold near
|
||||
the true tick period instead of drifting; and `timesync pacing` log
|
||||
lines / `rb timesync OFF` should show whether the stronger debt actually
|
||||
narrows `remote_lead` back toward D over a match, or whether the
|
||||
transit-delay theory needs D raised instead (next lever if pacing alone
|
||||
doesn't move it).
|
||||
|
||||
---
|
||||
|
||||
## 30. Phase-control soak instrumentation — no algorithm change (2026-08-01)
|
||||
|
||||
**Context:** Prior soak showed host (`slot=0`) opening ~91 rewind-requests
|
||||
against remote slot 1 while guest opened only ~29 against slot 0, with
|
||||
timesync pacing 72 vs 17. Hypothesis: the peer that starts ahead spends
|
||||
more time in `rb_active`/`tip_holding`, so `np_timesync_note_late()`
|
||||
suppresses the very debt that would slow it — a control-loop leak —
|
||||
rather than pure gameplay/transport asymmetry.
|
||||
|
||||
**Change:** telemetry only (`psx_netplay.c`). No pacing constants, grace,
|
||||
or invent-policy edits.
|
||||
|
||||
**~1 Hz line** (each peer, stderr):
|
||||
|
||||
```
|
||||
psxrecomp: rb phase ctrl slot=N sim=… lead=… lead_avg=… lead_min=…
|
||||
lead_max=… debt_ms=… debt_added=… mispredict=… note_late=…
|
||||
suppressed_rb=… suppressed_off=… D=…
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `lead` / `lead_avg/min/max` | `remote_lead` now + window over the last ~1s of admits |
|
||||
| `debt_ms` | current timesync pacing debt (drains ≤6 ms/tick) |
|
||||
| `debt_added` | cumulative ms of debt ever added (never resets) |
|
||||
| `mispredict` | cumulative `pads_differ` resolves (true mispredicts) |
|
||||
| `note_late` | cumulative `note_late` calls that actually added debt |
|
||||
| `suppressed_rb` | cumulative `note_late` early-outs while `rb_active` or `tip_holding` |
|
||||
| `suppressed_off` | cumulative early-outs while timesync adaptive-OFF window |
|
||||
|
||||
Same counters also appear on the 5s `rb admit stats` line.
|
||||
|
||||
**How to read host vs guest:**
|
||||
|
||||
1. **Supports control-loop instability:** one peer consistently has higher
|
||||
`lead`/`lead_avg`, higher `mispredict`, *and* higher `suppressed_rb`
|
||||
(especially `suppressed_rb` rising with `mispredict` while
|
||||
`note_late` lags). That peer is losing correction signal during its
|
||||
own resim/tip-hold windows.
|
||||
2. **Points elsewhere (gameplay/transport):** `suppressed_rb` rare or
|
||||
balanced, but `mispredict` still skewed — edges/content or path RTT
|
||||
dominate; do not change the suppress gate yet.
|
||||
3. **Secondary:** `suppressed_off` climbing on one side means adaptive-OFF
|
||||
is also silencing pacing (different failure mode).
|
||||
|
||||
**Do not** tweak pacing constants from this soak until the hypothesis is
|
||||
confirmed or rejected from these four signals.
|
||||
|
||||
## 31. Protocol correctness track — distributed episode contract (2026-08-01)
|
||||
|
||||
Work split into two tracks (protocol correctness first, scheduler feel
|
||||
second) so remaining rollback storms can be attributed to scheduler policy
|
||||
rather than peers disagreeing about control flow. This section is the
|
||||
protocol track. All six items landed together; the RB_SYNC wire format
|
||||
gained an op code (the old `initiator` byte) and a flags byte (the old pad
|
||||
byte, so packet size is unchanged). Constants live in
|
||||
`recomp_net/session.h` (`RNET_RB_SYNC_OP_*`, `RNET_RB_SYNC_FLAG_*`,
|
||||
`RNET_RB_ABORT_CLASS_*`).
|
||||
|
||||
**1. Episode identity — slot-partitioned epoch ids.** Wire epoch =
|
||||
`(counter << 3) | initiator_slot` (`RB_EPOCH_SLOT_BITS`). Concurrent dual
|
||||
initiation can never collide on an id, and any epoch's initiator slot is
|
||||
derivable from the id alone (needed by the tie-break). `g_epoch` is now
|
||||
the counter, not the wire id; followers only raise their counter for log
|
||||
monotonicity.
|
||||
|
||||
**2. Dual-initiation tie-break.** Both peers opening episodes
|
||||
concurrently used to deadlock (each dropped the other's SYNC as "already
|
||||
active") or cross-wire baselines. Now, on receiving a competing BEGIN
|
||||
while initiating: the lower initiator slot wins. The loser yields quietly
|
||||
— `abort_episode` with class REALIGN, cooldown cleared (contention is not
|
||||
failure) — and falls through to follow the winner's episode. Yield only
|
||||
happens in SealInputs/AwaitingBaseline before the snap load mutated Live;
|
||||
deeper phases drop the SYNC and rely on abort propagation to clean up.
|
||||
|
||||
**3. Abort propagation.** Local aborts used to tear down silently; the
|
||||
peer kept sealing/verifying until its own skewed timeout. Every
|
||||
wire-visible abort now sends `RB_SYNC op=ABORT` (×3 burst, epoch-deduped
|
||||
on receive) with the cooldown class in the mismatch field and the sender's
|
||||
realign tick in the load field. The receiver mirrors teardown, realigns
|
||||
via the shared `pick_realign_tip()` policy (pin > episode load > agreed),
|
||||
and arms the same cooldown class.
|
||||
|
||||
**4. Shared cooldown class.** `abort_episode_realign` classifies BEFORE
|
||||
aborting (REALIGN / ABORT / STORM / NO_SNAP → 0/24/48/90 ticks via
|
||||
`abort_class_cooldown_ticks`) and stages the class into the wire notice,
|
||||
so both peers re-arm on the same schedule instead of deriving different
|
||||
cooldowns from different local evidence. The receiver also syncs its
|
||||
`g_bl_mismatch_streak` from the class (REALIGN clears; STORM forces ≥2).
|
||||
An ABORT for an episode we never joined still arms STORM/NO_SNAP classes
|
||||
so we don't immediately initiate into a peer that just declared a hard
|
||||
failure.
|
||||
|
||||
**5. Shared frontier.** Three parts: (a) follow-NACKs carry the
|
||||
follower's confirmed frontier (`follower_frontier_hint()`: agreed
|
||||
watermark, else mutually hash-confirmed tick) in the target field; the
|
||||
initiator demotes to `min(load-1, peer_frontier)` walked down to a real
|
||||
snap, so the reopened episode's load is provably followable instead of
|
||||
NACK-cycling. (b) `finalize_tip_hold` bursts RESOLVED (enter_tip_hold and
|
||||
commit already did) so a committed tip is never a local secret. (c) The
|
||||
follow REFUSE "past frontier" check treats the library's peer-advertised
|
||||
`rnet_rb_resolved_through` as a frontier floor — peers only advertise
|
||||
committed ticks, so a load at/below it is followable even when our own
|
||||
agreed/hc watermark lags from a lost packet.
|
||||
|
||||
**6. Shared rereplay + light-tip decision.** BEGIN/tip-extend SYNCs carry
|
||||
initiator-authoritative flags: `FLAG_LIGHT_TIP` (follower adopts verbatim;
|
||||
`rnet_rb_begin_episode` no longer auto-classifies when `from_peer_notify`,
|
||||
and `rnet_rb_recommend_light_tip` reports `corr.flags` only — this was the
|
||||
asymmetric baseline-burst seed) and `FLAG_REREPLAY` (tip-extend: follower
|
||||
mirrors the initiator's rereplay decision, OR'd with its own
|
||||
"Live invented past the prior tip" check, which can only add correction,
|
||||
never skip one).
|
||||
|
||||
Verified: MotK `psx-runtime` builds; all 11 recomp-net tests pass
|
||||
(rb_wire_test round-trips the op/flags byte and the ABORT shape;
|
||||
rollback_episode_test covers follower flag adoption). Standalone
|
||||
recomp-net repo synced with the same changes.
|
||||
|
||||
---
|
||||
|
||||
## 32. Scheduler-feel track kickoff — lead regulation (2026-08-01)
|
||||
|
||||
Protocol correctness (§31) landed clean: the rb-diag1/rb-diag2 re-soak had
|
||||
no NACK cycles, no REFUSED, no SPAN CAP, tie-breaks resolved deterministically,
|
||||
and ABORT propagated even when only one side locally detected the divergence.
|
||||
Remaining asymmetry is scheduler *feel*, not protocol disagreement. Fable's
|
||||
review flagged four items in priority order; this section covers the first
|
||||
(highest-leverage) one: **lead regulation**.
|
||||
|
||||
**Observed asymmetry (rb-diag soak):** host began ~3× as many rewind
|
||||
episodes as guest and owned almost all corrections against the guest's pad;
|
||||
pacing debt accumulated mostly on host (1428 ms vs 456 ms cumulative). The
|
||||
begin/mispredict ratio tracks almost 1:1 (host mispredict=124/begin=107,
|
||||
guest mispredict=50/begin=33) — `rnet_input_contract_stick_replace_decide`
|
||||
converts nearly every genuine mispredict into a rewind, so the real question
|
||||
is *why one peer mispredicts 2.5× more than the other*, not the contract's
|
||||
promote/rewind split.
|
||||
|
||||
**Hypothesis:** an over-prediction feedback loop. The peer that falls behind
|
||||
wall-clock (more Replay/TipHold work from owning more corrections) still has
|
||||
to keep pace with the wire tick rate, so it ends up predicting the remote pad
|
||||
further into the future before the real row lands; deeper predictions are
|
||||
mechanically more likely to be wrong; each wrong deep prediction becomes
|
||||
another correction it initiates, which costs more Replay/TipHold time — the
|
||||
same peer digs itself deeper. `np_timesync_note_late` already exists to
|
||||
brake exactly this (pacing debt shaved off at admit), but until now every
|
||||
mispredict added the *same* flat debt regardless of how deep the guess was,
|
||||
so a peer running way ahead and a peer running barely ahead got identical
|
||||
correction pressure.
|
||||
|
||||
**Change 1 — proportional debt (`psx_netplay.c`):** `np_timesync_note_late`
|
||||
now takes the mispredict's *age* (`sim - t`: how many ticks the wrong
|
||||
predicted row rode before the real one arrived and was caught). A row
|
||||
resolved right at the normal input-delay boundary (`age ≈ D`) is expected
|
||||
steady-state noise and gets the same flat debt as before — zero behavior
|
||||
change in the common case. A row that rode notably longer than `D` ticks
|
||||
means we were running unusually far ahead of the confirmed remote tip when
|
||||
we guessed it; extra debt scales `+25%` of the base add per tick past `D`,
|
||||
still capped by the existing 3-tick ceiling. This only activates additional
|
||||
braking specifically during the runaway-ahead pattern; it cannot make the
|
||||
aligned/steady-state case worse since `extra_age` is 0 there.
|
||||
|
||||
**Change 2 — telemetry (`psx_netplay.c` phase-ctrl line):** added
|
||||
`mispredict_age_avg=` / `mispredict_age_max=` (match-lifetime, like the
|
||||
other cumulative counters on that line) so the next soak can directly
|
||||
confirm or reject the causal hypothesis by comparing host vs guest average
|
||||
prediction age — instead of the begin/mispredict-count archaeology used to
|
||||
produce the numbers above. If the hypothesis holds, host's
|
||||
`mispredict_age_avg` should sit measurably above guest's, and both peers'
|
||||
ratio should compress with change 1 applied vs a soak with it reverted.
|
||||
|
||||
**Deliberately not done yet:** did not touch the tie-break weighting, the
|
||||
resim/tip-hold `note_late` suppression gate (already balanced — 17/17
|
||||
`suppressed_rb` in the §31 re-soak, so that specific §30 hypothesis is
|
||||
closed), or the admit-side throttle shave rate. Per this project's own
|
||||
established practice (§29: a guessed pacing fix regressed and had to be
|
||||
reverted after a soak), only one bounded, reversible, provably-no-worse-
|
||||
in-steady-state change plus matching telemetry landed per cut — the next
|
||||
soak's `mispredict_age_avg`/`lead_avg`/`debt_added` decide whether to push
|
||||
further (e.g. widen the +25%/tick coefficient) or whether the asymmetry is
|
||||
gameplay-driven (who's the aggressor) rather than scheduler-driven.
|
||||
|
||||
**Still open (scheduler-feel track, unchanged priority from fable's
|
||||
review):**
|
||||
- Presentation cadence during Replay/TipHold — **landed in §33** (hold-last
|
||||
present on wall-clock cadence during `is_resimulating`).
|
||||
- POST core/aux diverge (~0.8/1k ticks) — heal path (§31 ABORT propagation)
|
||||
already handles it gracefully; root cause is a separate emulation
|
||||
determinism gap (audit/cpu-split dumps already exist for that chase).
|
||||
- Gap1/grace — left as `invent-immediate` (§29's finding stands); revisit
|
||||
only after lead regulation is confirmed closing the gap, so grace changes
|
||||
aren't evaluated against a still-skewed baseline.
|
||||
|
||||
---
|
||||
|
||||
## 33. Hold-last present during resim (2026-08-01)
|
||||
|
||||
**Problem:** `sdl_vblank_present_body` early-returned on
|
||||
`psx_netplay_is_resimulating()` (AwaitingBaseline | Replay | Verify) and
|
||||
skipped every Swap. Admit still advanced uncapped, so `replay%` windows
|
||||
produced `present_gap_p95` of 80–150ms — the metric tracked replay load
|
||||
because presents only resumed on Live ticks. Visually: frozen front buffer
|
||||
for the whole resim span, then a jump.
|
||||
|
||||
**Fix (host presentation only — digests/wire unchanged):**
|
||||
|
||||
1. **GL sticky hold** (`gpu_gl_renderer.c`): before each Live `SwapWindow`,
|
||||
`hold_capture_drawable()` copies the drawn backbuffer into a hold
|
||||
texture. When interpolation owns Swap, `hold_capture_native_fbo()`
|
||||
blits the display band from the VRAM/wide FBO instead. Mid-resim must
|
||||
not re-read `s_hr_tex` (it mutates during Replay). First NATIVE
|
||||
hold-present letterboxes then re-captures as DRAWABLE (soak-proven path).
|
||||
2. **`gl_renderer_present_hold_last()`**: redraws the hold texture + Swap
|
||||
(swap interval already 0 under netplay — no vsync tax on resim throughput).
|
||||
3. **`main.cpp` resim branch**: on a wall-clock period clamped to
|
||||
`[8, 33] ms` (≈ `g_frame_period_ms`), call hold-last + `netplay_note_present()`.
|
||||
Suspend GL interpolation for the episode (`set_interpolation_suspended(1)`)
|
||||
so the interp thread cannot fight hold-last `SwapWindow` (A/B: GL+interp
|
||||
buggy, GL without interp clean). Live restores FMV/game suspend policy.
|
||||
SW path re-presents with the last Live `src`/`dst` rects (not `NULL,NULL`
|
||||
on the full 640×512 texture — that produced the upper-left postage stamp).
|
||||
VK left as no-op hold (status quo skip) for this cut.
|
||||
4. Epilogue still skips the frame pacer during resim — catch-up stays fast;
|
||||
only the present starvation is fixed.
|
||||
|
||||
**Pass:** post-settle high-`replay%` windows should show `present_gap_p95`
|
||||
near the frame period (~16–33ms) instead of tracking episode wall time.
|
||||
Quiet Live tail unchanged. Re-soak GL+interp and SW; TipHold admit-stall
|
||||
gaps (§32) are a separate follow-up.
|
||||
|
||||
---
|
||||
|
||||
## 34. Double menu inputs — tip-hold quiet + scrub + debounce (2026-08-01)
|
||||
|
||||
**Post-§33 soak (`rb-diag1/2`, LAN, software GPU, menu nav):** protocol
|
||||
healthy enough (tip-extend live, quiet present gaps ~18ms) but **dpad taps
|
||||
felt doubled** again. Audit arms showed fragmented digital pulses on the
|
||||
wire (e.g. `ffbf` @1044, idle, `ffbf` @1048–50, idle, `ffbf` @1055,
|
||||
tip-extend @1059) and tip-hold committing immediately between them.
|
||||
`scrub-ahead` logged **0** times; releases during tip-hold hit
|
||||
`begin_refused=1` then quiet-commit dropped the edge.
|
||||
|
||||
**Root causes:**
|
||||
|
||||
1. **TipHold quiet armed whenever `sim >= invent_cap`**, including through
|
||||
1-tick idle holes between controller bounce pulses (`QUIET_MAX=80ms`).
|
||||
2. **`np_scrub_ahead_predicted` no-op'd under TipHold** (`sim <= release_tick`
|
||||
while coalesce promoted release at `tip+N`).
|
||||
3. **`begin_refused` while tip-holding** (tip-extend fail / different seat)
|
||||
did not block quiet finalize — commit raced the unreabsorbed release.
|
||||
4. **1–2 tick idle holes** published as real press/release/press edges;
|
||||
MotK menus edge-trigger → multiple navigations.
|
||||
|
||||
**Fixes:**
|
||||
|
||||
- [x] Digital release debounce (2 sim ticks) in `psx_netplay_stage_local`
|
||||
while rollback is on — bridges micro-bounce without lagging real taps.
|
||||
- [x] Scrub-ahead end bound = `max(sim, tip+runway)` during TipHold.
|
||||
- [x] TipHold quiet only while pads idle **and** no pending wire delta
|
||||
`tip+1..runway` / `block_quiet`; held or pending resets the timer.
|
||||
Quiet window **100–150ms** (was min 80 / max 80).
|
||||
- [x] Thrash CAP suppressed while a wire edge is still pending (SAFETY
|
||||
still wins).
|
||||
- [x] `psx_netplay_rb_tip_hold_block_quiet` from coalesce/begin_refused;
|
||||
different-seat tip-extend fail → tip-hold commit then fresh begin.
|
||||
|
||||
**Re-soak checks:** one physical dpad tap → one cursor move; look for
|
||||
`scrub-ahead release` during tip-hold coalesce; far fewer
|
||||
`begin_refused=1` → immediate tip-hold commit pairs; tip-hold→commit
|
||||
dwell should often exceed ~100ms on calm edges.
|
||||
|
||||
---
|
||||
|
||||
## 35. TipHold SAFETY-while-held + hold-last on admit stall (2026-08-01)
|
||||
|
||||
**Post-§34 soak:** doubles improved (`begin_refused=0`, chatter pulses
|
||||
down) but **21/30 tip-holds hit SAFETY CAP 250ms**, driving
|
||||
`present_gap_p95≈240ms` during menu play. Quiet almost never won:
|
||||
`wire_pending_delta` treated future presses (tip already idle) as
|
||||
pending, and SAFETY committed **while digital still held** → Live
|
||||
key-repeat after the stall. TipHold invent-cap stalls admit with no
|
||||
guest vblank, so §33 hold-last never ran.
|
||||
|
||||
**Fixes:**
|
||||
|
||||
- [x] SAFETY deferred while any pad held (log `SAFETY deferred`); commit
|
||||
only on quiet/thrash when idle, or **ABSOLUTE 2000ms** stuck-pad
|
||||
escape. `sim > tip_hold_until` also refuses commit while held.
|
||||
- [x] Pending quiet-block = **unabsorbed release** only (tip held, wire
|
||||
delta ahead). Future press while tip idle no longer parks quiet.
|
||||
Stale `block_quiet` clears once idle + no pending release.
|
||||
- [x] `netplay_hold_last_present_tick()` shared by resim present path and
|
||||
TipHold admit-spin (`netplay_barrier_admit`) so SAFETY/held waits
|
||||
keep Swap alive. Skip frame pacer while tip-holding.
|
||||
|
||||
**Re-soak:** expect mostly quiet tip-hold commits (not SAFETY);
|
||||
`SAFETY deferred` while holding then quiet after release;
|
||||
`present_gap_p95` during menu not glued to 250ms; ABSOLUTE rare.
|
||||
|
||||
---
|
||||
|
||||
## 36. TipHold held = live pad, not frozen sim peek (2026-08-02)
|
||||
|
||||
**Post-§35 soak:** SAFETY deferred worked as coded — and every deferred
|
||||
episode then hit **ABSOLUTE CAP 2000ms** (`held=1 pending=0`, 6/6 both
|
||||
peers). Admit blew out (`admit≈988ms/f`, `1.0 fps`). Cause: invent-cap
|
||||
freezes `sim`, `stage_local` latch freezes `staged`, and
|
||||
`tip_hold_any_pad_held(sim)` peeked that one immutable delay-ring row —
|
||||
a real controller release could never clear `held`, so quiet never armed
|
||||
and ABSOLUTE was guaranteed.
|
||||
|
||||
**Fixes:**
|
||||
|
||||
- [x] `psx_netplay_stage_local` always refreshes a live physical snapshot
|
||||
(even when latched). Admit spin captures while
|
||||
`psx_netplay_rb_tip_holding()` so live keeps updating.
|
||||
- [x] `psx_netplay_live_pad_buttons()` + `tip_hold_any_pad_held(tip)`:
|
||||
local uses live buttons; remote uses latest arrived row in
|
||||
`tip..tip+runway` (not parked `sim`).
|
||||
- [x] Invent-cap stall in `try_admit` still keys off wire/staged (do not
|
||||
invent-walk while the published tip pad is held — that was the
|
||||
key-repeat class). Only wall-clock finalize uses live.
|
||||
|
||||
**Re-soak:** `SAFETY deferred` then quiet/commit soon after release
|
||||
(not ~2s ABSOLUTE); ABSOLUTE only for truly stuck pads; menu fps not
|
||||
collapsing to ~1 on held tip-holds.
|
||||
|
||||
+1
-1
Submodule lib/recomp-net updated: 8b31aad3a7...8fd500bc2d
@@ -49,6 +49,12 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea
|
||||
/* Clear to black + swap (display-disabled frame). */
|
||||
void gl_renderer_present_blank(void);
|
||||
|
||||
/* §33: re-present the last Live frame captured before Swap (or from a VRAM
|
||||
* snapshot when interpolation owned the last present). Used during rollback
|
||||
* resim so the window keeps a wall-clock present cadence without reading
|
||||
* mid-resim VRAM. Returns 1 if a Swap happened, 0 if no hold is available. */
|
||||
int gl_renderer_present_hold_last(void);
|
||||
|
||||
/* Sync the authoritative FBO down to CPU VRAM if the GPU side is ahead (else
|
||||
* a no-op). Screenshots and the debug server call this before reading CPU
|
||||
* VRAM. Do NOT use before 24-bit (FMV) scanout — a full readback can clobber
|
||||
|
||||
@@ -50,6 +50,10 @@ uint8_t netplay_hc_confirm_through(const NetplayHashConfirm* hc, uint32_t tick)
|
||||
uint8_t netplay_hc_local_digest(const NetplayHashConfirm* hc, uint32_t tick,
|
||||
uint32_t* digest_out);
|
||||
|
||||
/* Peek peer FRAME_COMMIT digest for tick; returns 0 if missing. */
|
||||
uint8_t netplay_hc_peer_digest(const NetplayHashConfirm* hc, uint32_t tick,
|
||||
uint32_t* digest_out);
|
||||
|
||||
/* 1 if the next tick after resolved_through has both digests but they differ.
|
||||
* Fills tick/local/peer when non-NULL. Used to log first live core fork. */
|
||||
uint8_t netplay_hc_peek_mismatch(const NetplayHashConfirm* hc, uint32_t* tick_out,
|
||||
|
||||
@@ -116,12 +116,17 @@ int psx_netplay_in_load_barrier(void);
|
||||
* Clears. Caller should soft-exit to lobby — do not keep waiting on the barrier. */
|
||||
int psx_netplay_consume_load_apply_failed(void);
|
||||
|
||||
/* Stage local pad for the current sim tick. Ignored once that tick is latched. */
|
||||
/* Stage local pad for the current sim tick. Ignored once that tick is latched.
|
||||
* Always refreshes the live physical snapshot (see live_pad_buttons). */
|
||||
void psx_netplay_stage_local(const PsxNetPad *pad);
|
||||
|
||||
/* 1 while linking or before this sim tick's local pad is latched. */
|
||||
int psx_netplay_needs_local_sample(void);
|
||||
|
||||
/* Latest physical local buttons (0xFFFF idle). TipHold SAFETY/quiet must use
|
||||
* this — staged/delay-ring peeks stay frozen while sim is invent-capped. */
|
||||
int psx_netplay_live_pad_buttons(uint16_t *out);
|
||||
|
||||
/* 1 if INPUT_CONFIRM hash disagreement stalled the session. */
|
||||
int psx_netplay_input_desync(uint32_t *tick, uint32_t *local_hash, uint32_t *remote_hash);
|
||||
|
||||
@@ -154,6 +159,10 @@ int psx_netplay_remote_lead(void);
|
||||
/* Session input delay frames (default 2 when inactive). */
|
||||
int psx_netplay_input_delay(void);
|
||||
|
||||
/* Clear timesync pegged-streak at episode/tip-hold boundaries so resim cost
|
||||
* cannot look like WAN transit and trip adaptive-off (see §22). */
|
||||
void psx_netplay_timesync_on_episode_boundary(void);
|
||||
|
||||
/*
|
||||
* Extra headroom for post-starvation / behind-peer catch-up
|
||||
* (min(16, max(0, remote_lead - D, recovery_burst))).
|
||||
|
||||
@@ -71,14 +71,15 @@ int psx_netplay_rb_tip_extend(uint32_t mismatch_tick, int slot);
|
||||
/* 1 while begin_rewind is suppressed (cooldown after abort / realign). */
|
||||
int psx_netplay_rb_rewind_suppressed(void);
|
||||
|
||||
/* 1 during depth24 / recent MDEC / post-FMV settle — MotK FMV. */
|
||||
/* 1 during depth24 / recent MDEC / short post-FMV settle — MotK FMV.
|
||||
* Post-FMV digest lockstep no longer blocks invent (§26). */
|
||||
int psx_netplay_rb_fmv_defer_rewind(void);
|
||||
|
||||
/* 1 while depth24 or recent MDEC (not settle). */
|
||||
int psx_netplay_rb_fmv_media_active(void);
|
||||
|
||||
/* 1 during FMV media + post-FMV lockstep (+ unlock grace) — admit waits for
|
||||
* remote wire (title Start / movie skip). Ticks the FMV→settle tracker. */
|
||||
/* 1 during FMV media + short settle (§26) — admit waits for remote wire.
|
||||
* Post-FMV digest lockstep no longer blocks invent. Ticks FMV→settle. */
|
||||
int psx_netplay_rb_lockstep_no_invent(void);
|
||||
|
||||
/* Mid-guest resim pump: abort if Replay has made no finish_frame progress.
|
||||
@@ -107,6 +108,10 @@ uint32_t psx_netplay_rb_tip_hold_invent_slack(void);
|
||||
* release edges when Live is stalled at invent-cap. */
|
||||
uint32_t psx_netplay_rb_tip_runway(void);
|
||||
|
||||
/* §34: while TipHold cannot tip-extend/begin an already-seen wire edge,
|
||||
* block wall-clock quiet finalize (release must not be dropped by commit). */
|
||||
void psx_netplay_rb_tip_hold_block_quiet(int block);
|
||||
|
||||
/* 1 if a peer FRAME_COMMIT should be dropped (TipHold invent still on the
|
||||
* wire after tip-extend hc_prime). Call from FRAME_COMMIT drain. */
|
||||
int psx_netplay_rb_ignore_peer_frame_commit(uint32_t tick, uint32_t hash);
|
||||
|
||||
@@ -619,6 +619,20 @@ static int s_last_dx, s_last_dy, s_last_dw, s_last_dh;
|
||||
* (and FPS) keep advancing — especially on a 2nd+ load of the same slot. */
|
||||
static int s_force_present_remaining = 0;
|
||||
|
||||
/* §33 rollback resim hold-last: sticky copy of the last Live present so
|
||||
* mid-resim ticks can Swap a frozen frame without reading mutating VRAM.
|
||||
* HOLD_DRAWABLE = full backbuffer copy taken just before SwapWindow.
|
||||
* HOLD_NATIVE = GPU blit of a display band (used when interp owns Swap). */
|
||||
#define HOLD_NONE 0
|
||||
#define HOLD_DRAWABLE 1
|
||||
#define HOLD_NATIVE 2
|
||||
static int s_hold_kind = HOLD_NONE;
|
||||
static GLuint s_hold_tex = 0;
|
||||
static GLuint s_hold_fbo = 0;
|
||||
static int s_hold_tw = 0, s_hold_th = 0; /* texture size in texels */
|
||||
static int s_hold_force_4_3 = 0;
|
||||
static int s_hold_linear = 0;
|
||||
|
||||
/* Post-load freeze probe (main.cpp): accumulate skip/swap/dirty marks. */
|
||||
static uint64_t s_probe_skip = 0;
|
||||
static uint64_t s_probe_swap = 0;
|
||||
@@ -652,6 +666,83 @@ static void present_force_consumed(void) {
|
||||
s_force_present_remaining--;
|
||||
}
|
||||
|
||||
static void hold_ensure_tex(int w, int h) {
|
||||
if (w < 1) w = 1;
|
||||
if (h < 1) h = 1;
|
||||
if (!s_hold_tex) {
|
||||
glGenTextures(1, &s_hold_tex);
|
||||
if (!s_hold_fbo)
|
||||
p_glGenFramebuffers(1, &s_hold_fbo);
|
||||
}
|
||||
if (s_hold_tw == w && s_hold_th == h)
|
||||
return;
|
||||
glBindTexture(GL_TEXTURE_2D, s_hold_tex);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE,
|
||||
NULL);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, s_hold_fbo);
|
||||
p_glFramebufferTexture2D(PSXGL_FRAMEBUFFER, PSXGL_COLOR_ATTACHMENT0,
|
||||
GL_TEXTURE_2D, s_hold_tex, 0);
|
||||
p_glBindFramebuffer(PSXGL_FRAMEBUFFER, 0);
|
||||
s_hold_tw = w;
|
||||
s_hold_th = h;
|
||||
}
|
||||
|
||||
/* Snapshot the just-drawn default backbuffer (letterbox + content) before Swap.
|
||||
* Hold-last can then redraw this exact image without touching guest VRAM. */
|
||||
static void hold_capture_drawable(void) {
|
||||
int ww = 0, wh = 0;
|
||||
if (!s_ctx || !s_win)
|
||||
return;
|
||||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||||
if (ww < 1 || wh < 1)
|
||||
return;
|
||||
hold_ensure_tex(ww, wh);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
glBindTexture(GL_TEXTURE_2D, s_hold_tex);
|
||||
glCopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, ww, wh);
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
s_hold_kind = HOLD_DRAWABLE;
|
||||
s_hold_force_4_3 = 0;
|
||||
s_hold_linear = 0;
|
||||
}
|
||||
|
||||
/* Snapshot a native display band from an FBO when the main thread will not
|
||||
* Swap (interpolation owns the cadence). Scale-aware blit into hold tex. */
|
||||
static void hold_capture_native_fbo(GLuint src_fbo, int dx, int dy, int dw, int dh,
|
||||
int force_4_3, int linear) {
|
||||
int S = s_scale > 0 ? s_scale : 1;
|
||||
if (!s_ctx || !src_fbo || dw < 1 || dh < 1)
|
||||
return;
|
||||
hold_ensure_tex(dw * S, dh * S);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, src_fbo);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, s_hold_fbo);
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
p_glBlitFramebuffer(dx * S, dy * S, (dx + dw) * S, (dy + dh) * S,
|
||||
0, 0, dw * S, dh * S,
|
||||
GL_COLOR_BUFFER_BIT, GL_NEAREST);
|
||||
p_glBindFramebuffer(PSXGL_READ_FRAMEBUFFER, 0);
|
||||
p_glBindFramebuffer(PSXGL_DRAW_FRAMEBUFFER, 0);
|
||||
s_hold_kind = HOLD_NATIVE;
|
||||
s_hold_force_4_3 = force_4_3 ? 1 : 0;
|
||||
s_hold_linear = linear ? 1 : 0;
|
||||
}
|
||||
|
||||
static void hold_invalidate(void) {
|
||||
s_hold_kind = HOLD_NONE;
|
||||
}
|
||||
|
||||
/* Defined after present_target_quad / letterbox helpers. */
|
||||
static void letterbox_rect_aspect(int ww, int wh, int num, int den,
|
||||
int *x, int *y, int *w, int *h);
|
||||
static void letterbox_rect(int ww, int wh, int *x, int *y, int *w, int *h);
|
||||
static void present_target_quad(GLuint tex, int tex_w, int tex_h,
|
||||
int x, int y, int w, int h, int linear,
|
||||
int lx, int ly, int lw, int lh);
|
||||
|
||||
static void coh_record(int kind, int x0, int y0, int x1, int y1) {
|
||||
GlCohEvent *e = &s_coh_ring[s_coh_seq % GL_COH_RING_CAP];
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
@@ -2592,6 +2683,11 @@ void gl_renderer_shutdown(void) {
|
||||
s_present_h = 0;
|
||||
s_depth24_skip_up = 0;
|
||||
rect_clear(&s_d24_skip_fb);
|
||||
hold_invalidate();
|
||||
s_hold_tex = 0;
|
||||
s_hold_fbo = 0;
|
||||
s_hold_tw = 0;
|
||||
s_hold_th = 0;
|
||||
}
|
||||
|
||||
/* CPU-readout present (24-bit FMV frames and the PSX_GL_FORCE_CPU_PRESENT
|
||||
@@ -2659,6 +2755,7 @@ void gl_renderer_present(const uint32_t *pixels, int src_w, int src_h, int linea
|
||||
p_glBindVertexArray(s_present_vao); glDrawArrays(GL_TRIANGLES, 0, 3);
|
||||
p_glBindVertexArray(0); p_glUseProgram(0);
|
||||
pres_record(GL_PRES_CPU, 0, 0, src_w, src_h, lx, ly, lw, lh);
|
||||
hold_capture_drawable();
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
SDL_GL_SwapWindow(s_win);
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
@@ -2674,6 +2771,7 @@ void gl_renderer_present_blank(void) {
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, ww, wh); glClearColor(0.f,0.f,0.f,1.f); glClear(GL_COLOR_BUFFER_BIT);
|
||||
pres_record(GL_PRES_BLANK, 0, 0, 0, 0, 0, 0, ww, wh);
|
||||
hold_capture_drawable();
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
SDL_GL_SwapWindow(s_win);
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
@@ -2692,6 +2790,7 @@ void gl_renderer_invalidate_present(void) {
|
||||
for (int i = 0; i < PRES_ROWS; i++) s_present_dirty[i] = ~0ull;
|
||||
s_last_present_path = -1;
|
||||
s_force_present_remaining = 8;
|
||||
hold_invalidate();
|
||||
interp_reset_history();
|
||||
}
|
||||
|
||||
@@ -3635,6 +3734,63 @@ static void present_target_quad(GLuint tex, int tex_w, int tex_h,
|
||||
p_glUseProgram(0);
|
||||
}
|
||||
|
||||
int gl_renderer_present_hold_last(void) {
|
||||
int ww = 0, wh = 0;
|
||||
int lx, ly, lw, lh;
|
||||
if (!s_ctx || !s_win || s_hold_kind == HOLD_NONE || !s_hold_tex)
|
||||
return 0;
|
||||
SDL_GL_GetDrawableSize(s_win, &ww, &wh);
|
||||
if (ww < 1 || wh < 1)
|
||||
return 0;
|
||||
glDisable(GL_SCISSOR_TEST);
|
||||
glViewport(0, 0, ww, wh);
|
||||
glClearColor(0.f, 0.f, 0.f, 1.f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
if (s_hold_kind == HOLD_DRAWABLE) {
|
||||
/* Captured image already includes letterbox bars. Stretch 1:1, or
|
||||
* letterbox to preserve aspect if the drawable resized mid-resim. */
|
||||
lx = 0;
|
||||
ly = 0;
|
||||
lw = ww;
|
||||
lh = wh;
|
||||
if (s_hold_tw > 0 && s_hold_th > 0 &&
|
||||
((int64_t)s_hold_tw * wh != (int64_t)s_hold_th * ww)) {
|
||||
if ((int64_t)ww * s_hold_th > (int64_t)wh * s_hold_tw) {
|
||||
lw = (int)(((int64_t)wh * s_hold_tw) / s_hold_th);
|
||||
if (lw < 1) lw = 1;
|
||||
lx = (ww - lw) / 2;
|
||||
lh = wh;
|
||||
ly = 0;
|
||||
} else {
|
||||
lh = (int)(((int64_t)ww * s_hold_th) / s_hold_tw);
|
||||
if (lh < 1) lh = 1;
|
||||
ly = (wh - lh) / 2;
|
||||
lw = ww;
|
||||
lx = 0;
|
||||
}
|
||||
}
|
||||
present_target_quad(s_hold_tex, s_hold_tw, s_hold_th,
|
||||
0, 0, s_hold_tw, s_hold_th, 0, lx, ly, lw, lh);
|
||||
} else {
|
||||
if (s_hold_force_4_3)
|
||||
letterbox_rect_aspect(ww, wh, 4, 3, &lx, &ly, &lw, &lh);
|
||||
else
|
||||
letterbox_rect(ww, wh, &lx, &ly, &lw, &lh);
|
||||
present_target_quad(s_hold_tex, s_hold_tw, s_hold_th,
|
||||
0, 0, s_hold_tw, s_hold_th, s_hold_linear,
|
||||
lx, ly, lw, lh);
|
||||
/* Upgrade to drawable after letterboxing once. Live+interp leaves
|
||||
* HOLD_NATIVE (no Swap on main); drawable path is the soak-proven
|
||||
* full-window image (GL without interp). */
|
||||
hold_capture_drawable();
|
||||
}
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
SDL_GL_SwapWindow(s_win);
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
s_probe_swap++;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
int force_4_3) {
|
||||
if (!s_ctx || !s_raster_ok) return;
|
||||
@@ -3671,6 +3827,9 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
int interp_pair = interp_capture(s_hr_fbo, disp_x, disp_y, w, h,
|
||||
linear, force_4_3, GL_PRES_VRAM);
|
||||
if (interp_pair) {
|
||||
/* Interp owns Swap — still snapshot the band so resim hold-last has
|
||||
* a frozen frame if an episode opens before the next main-thread Swap. */
|
||||
hold_capture_native_fbo(s_hr_fbo, disp_x, disp_y, w, h, force_4_3, linear);
|
||||
gl_perf_present_exit(0);
|
||||
present_dirty_rect(disp_x, disp_y, disp_x + w - 1, disp_y + h - 1, 0);
|
||||
present_force_consumed();
|
||||
@@ -3681,6 +3840,7 @@ void gl_renderer_present_vram(int disp_x, int disp_y, int w, int h, int linear,
|
||||
present_target_quad(s_hr_tex, VRAM_W, VRAM_H,
|
||||
disp_x, disp_y, w, h, linear, lx, ly, lw, lh);
|
||||
pres_record(GL_PRES_VRAM, disp_x, disp_y, w, h, lx, ly, lw, lh);
|
||||
hold_capture_drawable();
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
SDL_GL_SwapWindow(s_win);
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
@@ -3768,6 +3928,7 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
|
||||
int interp_pair = interp_capture(fbo, 0, disp_y, g_wide_w, disp_h,
|
||||
linear, 0, GL_PRES_WIDE);
|
||||
if (interp_pair) {
|
||||
hold_capture_native_fbo(fbo, 0, disp_y, g_wide_w, disp_h, 0, linear);
|
||||
gl_perf_present_exit(1);
|
||||
present_dirty_rect(0, disp_y, VRAM_W - 1, disp_y + disp_h - 1, 0);
|
||||
present_force_consumed();
|
||||
@@ -3779,6 +3940,7 @@ int gl_renderer_present_wide_fbo(int disp_x, int disp_y, int disp_h, int linear)
|
||||
present_target_quad(tex, g_wide_w, VRAM_H,
|
||||
0, disp_y, g_wide_w, disp_h, linear, lx, ly, lw, lh);
|
||||
pres_record(GL_PRES_WIDE, disp_x, disp_y, g_wide_w, disp_h, lx, ly, lw, lh);
|
||||
hold_capture_drawable();
|
||||
latency_ring_mark(LAT_SWAP_BEGIN);
|
||||
SDL_GL_SwapWindow(s_win);
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
|
||||
+513
-15
@@ -361,6 +361,12 @@ static Smooth60State g_smooth_60_state;
|
||||
* survive soft-return and poison FMV/FPS after session_reboot). */
|
||||
static bool s_disabled_frame_presented = false;
|
||||
static bool s_force_present_after_load = false;
|
||||
/* §33 SW hold-last: sdl_texture is 640x512; Live only uploads the active
|
||||
* display rect. Resim must reuse that src/dst — RenderCopy(NULL,NULL) sticks
|
||||
* the image in the upper-left corner (user-confirmed). */
|
||||
static int s_sw_hold_valid = 0;
|
||||
static SDL_Rect s_sw_hold_src;
|
||||
static SDL_Rect s_sw_hold_dst;
|
||||
/* After LOADED: optional freeze probe (PSX_POST_LOAD_PROBE=1). Off by default. */
|
||||
static int s_post_load_probe_enabled = -1; /* -1 = unread env */
|
||||
static int s_post_load_probe_left = 0;
|
||||
@@ -447,6 +453,7 @@ static void smooth_60_reset(void) {
|
||||
static void present_session_reset(void) {
|
||||
s_disabled_frame_presented = false;
|
||||
s_force_present_after_load = false;
|
||||
s_sw_hold_valid = 0;
|
||||
s_fps_last_time = 0;
|
||||
s_fps_last_frame = 0;
|
||||
s_fps_base_title.clear();
|
||||
@@ -3448,7 +3455,10 @@ static uint64_t s_np_admit_ticks = 0;
|
||||
static uint64_t s_np_guest_ticks = 0;
|
||||
static uint64_t s_np_last_admit_end = 0;
|
||||
static uint64_t s_np_timing_frames = 0;
|
||||
|
||||
/* §27: wall-clock gaps between successful presents (player-visible starvation). */
|
||||
static uint64_t s_present_last_ms = 0;
|
||||
static uint32_t s_present_gaps_ms[128];
|
||||
static unsigned s_present_gaps_n = 0;
|
||||
static int netplay_timing_on(void) {
|
||||
if (s_np_timing_enabled < 0) {
|
||||
const char *e = std::getenv("PSX_NETPLAY_TIMING");
|
||||
@@ -3457,6 +3467,37 @@ static int netplay_timing_on(void) {
|
||||
return s_np_timing_enabled;
|
||||
}
|
||||
|
||||
static void netplay_note_present(void);
|
||||
static void netplay_hold_last_present_tick(void);
|
||||
|
||||
static void netplay_note_present(void) {
|
||||
uint64_t now;
|
||||
uint32_t gap;
|
||||
if (!psx_netplay_active() || !netplay_timing_on())
|
||||
return;
|
||||
now = SDL_GetTicks64();
|
||||
if (s_present_last_ms != 0ull && now >= s_present_last_ms) {
|
||||
gap = (uint32_t)(now - s_present_last_ms);
|
||||
if (s_present_gaps_n < (unsigned)(sizeof(s_present_gaps_ms) / sizeof(s_present_gaps_ms[0])))
|
||||
s_present_gaps_ms[s_present_gaps_n++] = gap;
|
||||
}
|
||||
s_present_last_ms = now ? now : 1ull;
|
||||
}
|
||||
|
||||
static void netplay_present_gap_stats(uint32_t *p95_out, uint32_t *max_out) {
|
||||
unsigned n = s_present_gaps_n;
|
||||
unsigned idx;
|
||||
if (p95_out) *p95_out = 0;
|
||||
if (max_out) *max_out = 0;
|
||||
if (n == 0)
|
||||
return;
|
||||
std::sort(s_present_gaps_ms, s_present_gaps_ms + n);
|
||||
idx = (n > 1u) ? (unsigned)((95u * (n - 1u)) / 100u) : 0u;
|
||||
if (p95_out) *p95_out = s_present_gaps_ms[idx];
|
||||
if (max_out) *max_out = s_present_gaps_ms[n - 1u];
|
||||
s_present_gaps_n = 0;
|
||||
}
|
||||
|
||||
/* Stage local pad + poll admit until published. Parks the guest fiber when
|
||||
* called from the vblank callback (or before scheduler entry). Latches one
|
||||
* sample per sim tick; stalls on INPUT_CONFIRM desync. */
|
||||
@@ -3547,7 +3588,10 @@ static void netplay_barrier_admit(int override) {
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
if (psx_netplay_needs_local_sample()) {
|
||||
/* §36: TipHold parks sim, so needs_local_sample stays 0 after latch.
|
||||
* Still capture every spin so live_pad_buttons can see a real release
|
||||
* (SAFETY deferred must not ride ABSOLUTE 2000ms on a frozen peek). */
|
||||
if (psx_netplay_needs_local_sample() || psx_netplay_rb_tip_holding()) {
|
||||
PsxNetPad local{};
|
||||
if (override >= 0 && !g_headless) {
|
||||
capture_override_pad(override, &local);
|
||||
@@ -3603,6 +3647,11 @@ static void netplay_barrier_admit(int override) {
|
||||
SDL_GameControllerUpdate();
|
||||
}
|
||||
if (psx_return_to_lobby_requested()) return;
|
||||
/* §35: TipHold invent-cap stall freezes guest (no vblank present).
|
||||
* Keep Swap alive on the last Live frame so SAFETY/held waits do not
|
||||
* open a ~250ms present gap. */
|
||||
if (psx_netplay_rb_tip_holding())
|
||||
netplay_hold_last_present_tick();
|
||||
/* Wake on peer UDP (or 1ms timeout). SDL_Delay(1) under dual FMV load
|
||||
* often stretches multi-ms and cut MotK netplay intro ~59→~36; Delay(0)
|
||||
* busy-spins and can starve the peer (tick-0 hang). */
|
||||
@@ -3683,6 +3732,41 @@ static constexpr double PSX_FRAME_PERIOD_MS = 1000.0 / 59.94;
|
||||
* the sim at the wrong speed. */
|
||||
static double g_frame_period_ms = PSX_FRAME_PERIOD_MS;
|
||||
|
||||
/* §33/§35: re-present last Live frame on a wall-clock cadence while guest
|
||||
* sim is frozen (resim) or TipHold invent-cap stall (admit spin). */
|
||||
static void netplay_hold_last_present_tick(void) {
|
||||
static uint64_t s_hold_last_ms;
|
||||
uint64_t now = SDL_GetTicks64();
|
||||
uint32_t period = (uint32_t)(g_frame_period_ms + 0.5);
|
||||
int did = 0;
|
||||
if (period < 8u)
|
||||
period = 8u;
|
||||
if (period > 33u)
|
||||
period = 33u;
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
if (g_gl_active)
|
||||
gl_renderer_set_interpolation_suspended(1);
|
||||
#endif
|
||||
if (s_hold_last_ms != 0ull && now >= s_hold_last_ms &&
|
||||
(uint32_t)(now - s_hold_last_ms) < period)
|
||||
return;
|
||||
#ifndef PSX_SDL_NO_RENDER
|
||||
if (g_gl_active) {
|
||||
did = gl_renderer_present_hold_last();
|
||||
} else if (!g_vk_active && sdl_renderer && sdl_texture && s_sw_hold_valid) {
|
||||
SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255);
|
||||
SDL_RenderClear(sdl_renderer);
|
||||
SDL_RenderCopy(sdl_renderer, sdl_texture, &s_sw_hold_src, &s_sw_hold_dst);
|
||||
SDL_RenderPresent(sdl_renderer);
|
||||
did = 1;
|
||||
}
|
||||
#endif
|
||||
if (did) {
|
||||
netplay_note_present();
|
||||
s_hold_last_ms = now ? now : 1ull;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Host-stack-usage profile (RECURSION_BUG.md §17) ──────────────────────────
|
||||
* The decisive instrument for the long-run freeze. The guest call graph mirrors
|
||||
* onto the 1MB guest fiber stack (traps.c), and the native-stack guard trips at
|
||||
@@ -3974,11 +4058,17 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
const uint64_t replay_ticks = psx_netplay_rb_take_replay_ticks();
|
||||
const double replay_pct =
|
||||
100.0 * (double)replay_ticks / (double)s_np_timing_frames;
|
||||
uint32_t gap_p95 = 0, gap_max = 0;
|
||||
/* §27: presentation starvation — eye cares about gaps more
|
||||
* than replay%. Pass: post-settle present_gap_p95 ≤ 33ms. */
|
||||
netplay_present_gap_stats(&gap_p95, &gap_max);
|
||||
std::fprintf(stderr,
|
||||
"[FPS] game: %.1f fps (%.2fx) | frames: %llu | "
|
||||
"guest=%.2f ms/f admit=%.2f ms/f replay=%.0f%% (n=%llu)\n",
|
||||
"guest=%.2f ms/f admit=%.2f ms/f replay=%.0f%% "
|
||||
"present_gap_p95=%u ms max=%u ms (n=%llu)\n",
|
||||
fps, speed, (unsigned long long)s_frame_count,
|
||||
guest_ms, admit_ms, replay_pct,
|
||||
(unsigned)gap_p95, (unsigned)gap_max,
|
||||
(unsigned long long)s_np_timing_frames);
|
||||
s_np_admit_ticks = 0;
|
||||
s_np_guest_ticks = 0;
|
||||
@@ -4399,6 +4489,16 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Rollback resim / TipHold stall (§33/§35): presentation is host-only.
|
||||
* Resim skips Swap; TipHold invent-cap stalls admit (no guest vblank) —
|
||||
* both starved present_gap. Hold-last on wall-clock cadence. TipHold
|
||||
* also presents from the admit spin (no vblank while stalled).
|
||||
* Suspend GL interp so it cannot fight hold-last SwapWindow. */
|
||||
if (psx_netplay_is_resimulating()) {
|
||||
netplay_hold_last_present_tick();
|
||||
return ep;
|
||||
}
|
||||
|
||||
/* ---- Display from our VRAM ---- */
|
||||
probe_reached = 1;
|
||||
uint32_t w = 0, h = 0;
|
||||
@@ -4507,12 +4607,15 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
* SW stayed smooth). Falls through to the CPU readout path only if
|
||||
* the wide surface for this buffer doesn't exist yet. */
|
||||
if (gl_renderer_present_wide_fbo((int)di.display_x, (int)di.display_y,
|
||||
(int)h, g_video_aa ? 1 : 0))
|
||||
(int)h, g_video_aa ? 1 : 0)) {
|
||||
netplay_note_present();
|
||||
return ep;
|
||||
}
|
||||
} else {
|
||||
gl_renderer_present_vram((int)di.display_x, (int)di.display_y,
|
||||
(int)present_w, (int)h, g_video_aa ? 1 : 0,
|
||||
(fmv_frame || nw_pin) ? 1 : 0);
|
||||
netplay_note_present();
|
||||
return ep;
|
||||
}
|
||||
}
|
||||
@@ -4545,6 +4648,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
(int)present_w, (int)h, g_video_aa ? 1 : 0,
|
||||
(fmv_frame || nw_pin) ? 1 : 0);
|
||||
}
|
||||
netplay_note_present();
|
||||
return ep;
|
||||
}
|
||||
#endif
|
||||
@@ -4673,6 +4777,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
gl_renderer_present(sdl_pixel_buf, src_w, src_h,
|
||||
(g_video_aa && !depth24_frame) ? 1 : 0,
|
||||
pin_43 ? 1 : 0, 0 /* full width */);
|
||||
netplay_note_present();
|
||||
} else {
|
||||
if ((!sdl_renderer || !sdl_texture) && ensure_sw_sdl_present() != 0)
|
||||
return ep;
|
||||
@@ -4729,6 +4834,10 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
SDL_SetRenderDrawColor(sdl_renderer, 0, 0, 0, 255);
|
||||
SDL_RenderClear(sdl_renderer);
|
||||
SDL_RenderCopy(sdl_renderer, sdl_texture, &src, &dst);
|
||||
/* §33: remember active rect for resim hold-last (not full 640x512). */
|
||||
s_sw_hold_src = src;
|
||||
s_sw_hold_dst = dst;
|
||||
s_sw_hold_valid = 1;
|
||||
|
||||
/* Vsync self-heal. The renderer is created with PRESENTVSYNC for
|
||||
* tear-free output, but the wall-clock pacer above already holds
|
||||
@@ -4745,6 +4854,7 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) {
|
||||
SDL_RenderPresent(sdl_renderer);
|
||||
const Uint64 t1 = SDL_GetPerformanceCounter();
|
||||
latency_ring_mark(LAT_SWAP_END);
|
||||
netplay_note_present();
|
||||
const Uint64 freq = SDL_GetPerformanceFrequency();
|
||||
const Uint64 present_ms = (t1 >= t0 && freq) ? ((t1 - t0) * 1000u) / freq : 0;
|
||||
if (!g_present_vsync_disabled && present_ms > 250) {
|
||||
@@ -4775,7 +4885,7 @@ static void sdl_vblank_present(void) {
|
||||
psx_netplay_rb_flush_resume();
|
||||
if (ep.skip_pace || psx_return_to_lobby_requested())
|
||||
return;
|
||||
if (psx_netplay_is_resimulating())
|
||||
if (psx_netplay_is_resimulating() || psx_netplay_rb_tip_holding())
|
||||
return;
|
||||
if (!gpu_display_is_depth24() && psx_netplay_catchup_budget() > 0) {
|
||||
psx_netplay_catchup_consume_frame();
|
||||
@@ -4992,6 +5102,9 @@ namespace {
|
||||
int g_lnch_lan_my_slot = -1;
|
||||
std::string g_lnch_lan_player_id; /* local process identity for LAN seats */
|
||||
|
||||
static int ae_np_lan_occupied(const AeLanLobbyState& state);
|
||||
static int ae_np_lan_endpoint_port(const std::string& endpoint);
|
||||
|
||||
#ifdef _WIN32
|
||||
using AeLanSock = SOCKET;
|
||||
static constexpr AeLanSock kAeLanSockInvalid = INVALID_SOCKET;
|
||||
@@ -4999,6 +5112,8 @@ namespace {
|
||||
using AeLanSock = int;
|
||||
static constexpr AeLanSock kAeLanSockInvalid = -1;
|
||||
#endif
|
||||
static void ae_np_lan_sock_close(AeLanSock* s);
|
||||
static bool ae_np_lan_set_nonblock(AeLanSock s);
|
||||
AeLanSock g_lnch_lan_udp = kAeLanSockInvalid;
|
||||
sockaddr_in g_lnch_lan_peers[kAeLanMaxSlots]{};
|
||||
bool g_lnch_lan_peer_ok[kAeLanMaxSlots]{};
|
||||
@@ -5006,10 +5121,262 @@ namespace {
|
||||
/* LAN list latency from last Refresh probe; -1 unknown. */
|
||||
int g_lnch_lan_latency_ms = -1;
|
||||
|
||||
/* Cross-machine LAN lobby discovery (UDP broadcast browse/beacon).
|
||||
* The on-disk netplay_lan_lobby.txt registry only works for same-cwd /
|
||||
* same-machine; remote peers discover hosts via MOTK1 BROWSE→BEACON. */
|
||||
static constexpr int kAeLanDiscoverMax = 16;
|
||||
static constexpr int kAeLanBrowsePortLo = 7777;
|
||||
static constexpr int kAeLanBrowsePortHi = 7808; /* inclusive */
|
||||
static constexpr uint32_t kAeLanDiscoverStaleMs = 8000u;
|
||||
struct AeLanDiscovered {
|
||||
char endpoint[64];
|
||||
char name[64];
|
||||
char game[64];
|
||||
int player_count = 0;
|
||||
int max_slots = 2;
|
||||
int has_password = 0;
|
||||
int latency_ms = -1;
|
||||
int input_delay = 2;
|
||||
int input_prediction = 4;
|
||||
int rollback = 1;
|
||||
uint32_t session_id = 1;
|
||||
uint32_t last_seen_ms = 0;
|
||||
};
|
||||
static AeLanDiscovered g_lnch_lan_discovered[kAeLanDiscoverMax];
|
||||
static int g_lnch_lan_discovered_n = 0;
|
||||
static uint32_t g_lnch_lan_beacon_announce_ms = 0;
|
||||
|
||||
std::filesystem::path ae_np_lan_file() {
|
||||
return std::filesystem::current_path() / "netplay_lan_lobby.txt";
|
||||
}
|
||||
|
||||
static void ae_np_lan_sock_set_broadcast(AeLanSock s) {
|
||||
if (s == kAeLanSockInvalid) return;
|
||||
#ifdef _WIN32
|
||||
BOOL on = TRUE;
|
||||
setsockopt(s, SOL_SOCKET, SO_BROADCAST, (const char*)&on, sizeof(on));
|
||||
#else
|
||||
int on = 1;
|
||||
setsockopt(s, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on));
|
||||
#endif
|
||||
}
|
||||
|
||||
static void ae_np_lan_prune_discovered(void) {
|
||||
const uint32_t now = SDL_GetTicks();
|
||||
int w = 0;
|
||||
for (int i = 0; i < g_lnch_lan_discovered_n; ++i) {
|
||||
if ((uint32_t)(now - g_lnch_lan_discovered[i].last_seen_ms) >
|
||||
kAeLanDiscoverStaleMs)
|
||||
continue;
|
||||
if (w != i)
|
||||
g_lnch_lan_discovered[w] = g_lnch_lan_discovered[i];
|
||||
++w;
|
||||
}
|
||||
g_lnch_lan_discovered_n = w;
|
||||
}
|
||||
|
||||
static int ae_np_lan_discovered_find(const char* endpoint) {
|
||||
if (!endpoint || !endpoint[0]) return -1;
|
||||
for (int i = 0; i < g_lnch_lan_discovered_n; ++i) {
|
||||
if (std::strcmp(g_lnch_lan_discovered[i].endpoint, endpoint) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void ae_np_lan_discovered_upsert(const AeLanDiscovered& in) {
|
||||
if (!in.endpoint[0]) return;
|
||||
const int idx = ae_np_lan_discovered_find(in.endpoint);
|
||||
if (idx >= 0) {
|
||||
AeLanDiscovered& e = g_lnch_lan_discovered[idx];
|
||||
e = in;
|
||||
return;
|
||||
}
|
||||
if (g_lnch_lan_discovered_n >= kAeLanDiscoverMax) {
|
||||
/* Drop oldest. */
|
||||
int oldest = 0;
|
||||
for (int i = 1; i < g_lnch_lan_discovered_n; ++i) {
|
||||
if (g_lnch_lan_discovered[i].last_seen_ms <
|
||||
g_lnch_lan_discovered[oldest].last_seen_ms)
|
||||
oldest = i;
|
||||
}
|
||||
g_lnch_lan_discovered[oldest] = in;
|
||||
return;
|
||||
}
|
||||
g_lnch_lan_discovered[g_lnch_lan_discovered_n++] = in;
|
||||
}
|
||||
|
||||
static int ae_np_lan_format_beacon(char* buf, size_t cap,
|
||||
const AeLanLobbyState& st) {
|
||||
if (!buf || cap < 32) return -1;
|
||||
const int players = ae_np_lan_occupied(st);
|
||||
const int max_slots = st.max_slots >= 2 ? st.max_slots : 2;
|
||||
const int has_pw = st.password.empty() ? 0 : 1;
|
||||
const uint32_t sid = st.session_id ? st.session_id : 1u;
|
||||
int delay = g_lnch_lobby_input_delay;
|
||||
int pred = g_lnch_lobby_input_prediction;
|
||||
if (delay < 2) delay = 2;
|
||||
if (delay > 20) delay = 20;
|
||||
if (pred < 2) pred = 2;
|
||||
if (pred > 16) pred = 16;
|
||||
return std::snprintf(
|
||||
buf, cap,
|
||||
"MOTK1 BEACON\n%s\n%s\n%s\n%d\n%d\n%d\n%u\n%d\n%d\n%d\n",
|
||||
st.name.empty() ? "LAN Lobby" : st.name.c_str(),
|
||||
st.game.empty() ? "PSX" : st.game.c_str(),
|
||||
st.endpoint.c_str(),
|
||||
players, max_slots, has_pw, (unsigned)sid,
|
||||
delay, pred, g_lnch_rollback ? 1 : 0);
|
||||
}
|
||||
|
||||
/* Parse BEACON; prefer reply source IP + advertised port for joinability. */
|
||||
static int ae_np_lan_parse_beacon(char* buf, const sockaddr_in* from,
|
||||
int rtt_ms) {
|
||||
if (!buf || std::strncmp(buf, "MOTK1 BEACON\n", 13) != 0) return -1;
|
||||
char* p = buf + 13;
|
||||
char* lines[10] = {};
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
lines[i] = p;
|
||||
char* nl = std::strchr(p, '\n');
|
||||
if (!nl) {
|
||||
if (i < 6) return -1;
|
||||
break;
|
||||
}
|
||||
*nl = '\0';
|
||||
p = nl + 1;
|
||||
}
|
||||
if (!lines[0] || !lines[1] || !lines[2]) return -1;
|
||||
|
||||
AeLanDiscovered d{};
|
||||
std::snprintf(d.name, sizeof(d.name), "%s", lines[0]);
|
||||
std::snprintf(d.game, sizeof(d.game), "%s", lines[1]);
|
||||
const int adv_port = ae_np_lan_endpoint_port(lines[2]);
|
||||
int port = adv_port > 0 ? adv_port : 7777;
|
||||
if (from) {
|
||||
char ip[64];
|
||||
if (inet_ntop(AF_INET, &from->sin_addr, ip, sizeof(ip)))
|
||||
std::snprintf(d.endpoint, sizeof(d.endpoint), "%s:%d", ip, port);
|
||||
else
|
||||
std::snprintf(d.endpoint, sizeof(d.endpoint), "%s", lines[2]);
|
||||
} else {
|
||||
std::snprintf(d.endpoint, sizeof(d.endpoint), "%s", lines[2]);
|
||||
}
|
||||
d.player_count = lines[3] ? std::atoi(lines[3]) : 0;
|
||||
d.max_slots = lines[4] ? std::atoi(lines[4]) : 2;
|
||||
if (d.max_slots < 2) d.max_slots = 2;
|
||||
if (d.max_slots > kAeLanMaxSlots) d.max_slots = kAeLanMaxSlots;
|
||||
d.has_password = lines[5] ? (std::atoi(lines[5]) != 0) : 0;
|
||||
d.session_id = lines[6]
|
||||
? (uint32_t)std::strtoul(lines[6], nullptr, 10) : 1u;
|
||||
if (!d.session_id) d.session_id = 1u;
|
||||
d.input_delay = lines[7] ? std::atoi(lines[7]) : 2;
|
||||
d.input_prediction = lines[8] ? std::atoi(lines[8]) : 4;
|
||||
d.rollback = lines[9] ? (std::atoi(lines[9]) != 0) : 1;
|
||||
if (d.input_delay < 2) d.input_delay = 2;
|
||||
if (d.input_delay > 20) d.input_delay = 20;
|
||||
if (d.input_prediction < 2) d.input_prediction = 2;
|
||||
if (d.input_prediction > 16) d.input_prediction = 16;
|
||||
d.latency_ms = rtt_ms;
|
||||
d.last_seen_ms = SDL_GetTicks();
|
||||
ae_np_lan_discovered_upsert(d);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void ae_np_lan_broadcast_msg(AeLanSock s, int port, const char* msg) {
|
||||
if (s == kAeLanSockInvalid || !msg || port <= 0 || port > 65535) return;
|
||||
sockaddr_in to{};
|
||||
to.sin_family = AF_INET;
|
||||
to.sin_port = htons((uint16_t)port);
|
||||
to.sin_addr.s_addr = htonl(INADDR_BROADCAST);
|
||||
const int n = (int)std::strlen(msg);
|
||||
#ifdef _WIN32
|
||||
sendto(s, msg, n, 0, (const sockaddr*)&to, sizeof(to));
|
||||
#else
|
||||
sendto(s, msg, (size_t)n, 0, (const sockaddr*)&to, sizeof(to));
|
||||
#endif
|
||||
}
|
||||
|
||||
/* Sync browse: broadcast BROWSE across the common lobby port range and
|
||||
* collect BEACON replies for wait_ms. */
|
||||
static void ae_np_lan_browse(uint32_t wait_ms) {
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa;
|
||||
WSAStartup(MAKEWORD(2, 2), &wsa);
|
||||
#endif
|
||||
AeLanSock s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (s == kAeLanSockInvalid) return;
|
||||
if (!ae_np_lan_set_nonblock(s)) {
|
||||
ae_np_lan_sock_close(&s);
|
||||
return;
|
||||
}
|
||||
ae_np_lan_sock_set_broadcast(s);
|
||||
sockaddr_in bind_addr{};
|
||||
bind_addr.sin_family = AF_INET;
|
||||
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||
bind_addr.sin_port = htons(0);
|
||||
if (bind(s, (sockaddr*)&bind_addr, sizeof(bind_addr)) != 0) {
|
||||
ae_np_lan_sock_close(&s);
|
||||
return;
|
||||
}
|
||||
|
||||
const char browse[] = "MOTK1 BROWSE\n";
|
||||
const uint32_t t0 = SDL_GetTicks();
|
||||
for (int port = kAeLanBrowsePortLo; port <= kAeLanBrowsePortHi; ++port)
|
||||
ae_np_lan_broadcast_msg(s, port, browse);
|
||||
|
||||
const uint32_t deadline = t0 + (wait_ms ? wait_ms : 250u);
|
||||
while ((int32_t)(deadline - SDL_GetTicks()) > 0) {
|
||||
char buf[1024];
|
||||
sockaddr_in from{};
|
||||
#ifdef _WIN32
|
||||
int fromlen = (int)sizeof(from);
|
||||
const int n = recvfrom(s, buf, (int)sizeof(buf) - 1, 0,
|
||||
(sockaddr*)&from, &fromlen);
|
||||
#else
|
||||
socklen_t fromlen = sizeof(from);
|
||||
const int n = (int)recvfrom(s, buf, sizeof(buf) - 1, 0,
|
||||
(sockaddr*)&from, &fromlen);
|
||||
#endif
|
||||
if (n > 0) {
|
||||
buf[n] = '\0';
|
||||
const int rtt = (int)(SDL_GetTicks() - t0);
|
||||
(void)ae_np_lan_parse_beacon(buf, &from, rtt);
|
||||
} else {
|
||||
SDL_Delay(5);
|
||||
}
|
||||
}
|
||||
ae_np_lan_sock_close(&s);
|
||||
ae_np_lan_prune_discovered();
|
||||
}
|
||||
|
||||
static void ae_np_lan_apply_discovered_caps(const char* endpoint) {
|
||||
const int idx = ae_np_lan_discovered_find(endpoint);
|
||||
if (idx < 0) return;
|
||||
const AeLanDiscovered& d = g_lnch_lan_discovered[idx];
|
||||
g_lnch_lobby_input_delay = d.input_delay;
|
||||
g_lnch_lobby_input_prediction = d.input_prediction;
|
||||
g_lnch_rollback = d.rollback ? 1 : 0;
|
||||
}
|
||||
|
||||
static int ae_np_lan_fill_lobby_from_discovered(
|
||||
int discovered_index, RecompLauncherCNetplayLobby* out) {
|
||||
if (!out || discovered_index < 0 ||
|
||||
discovered_index >= g_lnch_lan_discovered_n)
|
||||
return 0;
|
||||
const AeLanDiscovered& d = g_lnch_lan_discovered[discovered_index];
|
||||
std::snprintf(out->lobby_id, sizeof(out->lobby_id), "lan:%s", d.endpoint);
|
||||
std::snprintf(out->name, sizeof(out->name), "LAN - %s",
|
||||
d.name[0] ? d.name : "Lobby");
|
||||
std::snprintf(out->game_name, sizeof(out->game_name), "%s",
|
||||
d.game[0] ? d.game : "PSX");
|
||||
out->game_version[0] = '\0';
|
||||
out->player_count = d.player_count;
|
||||
out->max_slots = d.max_slots;
|
||||
out->has_password = d.has_password;
|
||||
out->latency_ms = d.latency_ms;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void ae_np_lan_sock_close(AeLanSock* s) {
|
||||
if (!s || *s == kAeLanSockInvalid) return;
|
||||
#ifdef _WIN32
|
||||
@@ -5727,9 +6094,36 @@ namespace {
|
||||
static void ae_np_lan_rescan(void) {
|
||||
if (g_lnch_hosting_lan) {
|
||||
g_lnch_lan_latency_ms = 0;
|
||||
/* Still browse so the host list can show other LAN rooms. */
|
||||
ae_np_lan_browse(200u);
|
||||
AeLanLobbyState self{};
|
||||
if (ae_np_read_lan_file_state(&self) && !self.started) {
|
||||
AeLanDiscovered d{};
|
||||
std::snprintf(d.endpoint, sizeof(d.endpoint), "%s",
|
||||
self.endpoint.c_str());
|
||||
std::snprintf(d.name, sizeof(d.name), "%s",
|
||||
self.name.empty() ? "LAN Lobby" : self.name.c_str());
|
||||
std::snprintf(d.game, sizeof(d.game), "%s",
|
||||
self.game.empty() ? "PSX" : self.game.c_str());
|
||||
d.player_count = ae_np_lan_occupied(self);
|
||||
d.max_slots = self.max_slots >= 2 ? self.max_slots : 2;
|
||||
d.has_password = self.password.empty() ? 0 : 1;
|
||||
d.latency_ms = 0;
|
||||
d.input_delay = g_lnch_lobby_input_delay;
|
||||
d.input_prediction = g_lnch_lobby_input_prediction;
|
||||
d.rollback = g_lnch_rollback ? 1 : 0;
|
||||
d.session_id = self.session_id ? self.session_id : 1u;
|
||||
d.last_seen_ms = SDL_GetTicks();
|
||||
ae_np_lan_discovered_upsert(d);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (g_lnch_joined_lan) return;
|
||||
|
||||
/* Cross-machine discovery: broadcast BROWSE, collect BEACONs. */
|
||||
ae_np_lan_browse(300u);
|
||||
|
||||
/* Legacy same-machine file registry (shared cwd). */
|
||||
AeLanLobbyState st;
|
||||
if (!ae_np_read_lan_file_state(&st)) {
|
||||
g_lnch_lan_latency_ms = -1;
|
||||
@@ -5740,6 +6134,24 @@ namespace {
|
||||
if (g_lnch_lan_latency_ms < 0) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(ae_np_lan_file(), ec);
|
||||
} else if (ae_np_lan_discovered_find(st.endpoint.c_str()) < 0) {
|
||||
/* Promote file row into the discovered table so list_get is uniform. */
|
||||
AeLanDiscovered d{};
|
||||
std::snprintf(d.endpoint, sizeof(d.endpoint), "%s", st.endpoint.c_str());
|
||||
std::snprintf(d.name, sizeof(d.name), "%s",
|
||||
st.name.empty() ? "LAN Lobby" : st.name.c_str());
|
||||
std::snprintf(d.game, sizeof(d.game), "%s",
|
||||
st.game.empty() ? "PSX" : st.game.c_str());
|
||||
d.player_count = ae_np_lan_occupied(st);
|
||||
d.max_slots = st.max_slots >= 2 ? st.max_slots : 2;
|
||||
d.has_password = st.password.empty() ? 0 : 1;
|
||||
d.latency_ms = g_lnch_lan_latency_ms;
|
||||
d.input_delay = g_lnch_lobby_input_delay;
|
||||
d.input_prediction = g_lnch_lobby_input_prediction;
|
||||
d.rollback = g_lnch_rollback ? 1 : 0;
|
||||
d.session_id = st.session_id ? st.session_id : 1u;
|
||||
d.last_seen_ms = SDL_GetTicks();
|
||||
ae_np_lan_discovered_upsert(d);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5753,6 +6165,15 @@ namespace {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Extra LAN rows after the online server list. Prefer discovered table;
|
||||
* fall back to a single file-backed row when hosting / same-machine. */
|
||||
static int ae_np_lan_list_extra_count(void) {
|
||||
ae_np_lan_prune_discovered();
|
||||
if (g_lnch_lan_discovered_n > 0)
|
||||
return g_lnch_lan_discovered_n;
|
||||
return ae_np_lan_list_visible() ? 1 : 0;
|
||||
}
|
||||
|
||||
PsxLobbyMatchCaps ae_netplay_caps_from_settings(const RecompLauncherCSettings* s) {
|
||||
PsxLobbyMatchCaps caps{};
|
||||
caps.valid = 1;
|
||||
@@ -5933,8 +6354,22 @@ namespace {
|
||||
if (g_lnch_hosting_lan) {
|
||||
AeLanLobbyState st;
|
||||
if (!ae_np_read_lan_state(&st)) return;
|
||||
const int lobby_port = ae_np_lan_endpoint_port(st.endpoint);
|
||||
if (g_lnch_lan_udp == kAeLanSockInvalid)
|
||||
(void)ae_np_lan_udp_ensure(true, ae_np_lan_endpoint_port(st.endpoint));
|
||||
(void)ae_np_lan_udp_ensure(true, lobby_port);
|
||||
/* Periodic broadcast BEACON so Refresh can find us without a
|
||||
* unicast target (and for guests that listen between browses). */
|
||||
if (g_lnch_lan_udp != kAeLanSockInvalid && !st.started) {
|
||||
const uint32_t now = SDL_GetTicks();
|
||||
if (g_lnch_lan_beacon_announce_ms == 0u ||
|
||||
now - g_lnch_lan_beacon_announce_ms >= 2000u) {
|
||||
g_lnch_lan_beacon_announce_ms = now;
|
||||
ae_np_lan_sock_set_broadcast(g_lnch_lan_udp);
|
||||
char beacon[768];
|
||||
if (ae_np_lan_format_beacon(beacon, sizeof(beacon), st) > 0)
|
||||
ae_np_lan_broadcast_msg(g_lnch_lan_udp, lobby_port, beacon);
|
||||
}
|
||||
}
|
||||
} else if (g_lnch_remote_lan) {
|
||||
if (g_lnch_lan_udp == kAeLanSockInvalid)
|
||||
(void)ae_np_lan_udp_ensure(false, 0);
|
||||
@@ -5982,6 +6417,17 @@ namespace {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* LAN lobby discovery: answer browse with a BEACON (unicast). */
|
||||
if (std::strncmp(buf, "MOTK1 BROWSE", 12) == 0 && g_lnch_hosting_lan) {
|
||||
AeLanLobbyState st;
|
||||
if (ae_np_read_lan_state(&st) && !st.started) {
|
||||
char beacon[768];
|
||||
if (ae_np_lan_format_beacon(beacon, sizeof(beacon), st) > 0)
|
||||
ae_np_lan_udp_sendto(from, beacon);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (std::strncmp(buf, "MOTK3 JOIN\n", 11) == 0 && g_lnch_hosting_lan) {
|
||||
char* p = buf + 11;
|
||||
char* nl = std::strchr(p, '\n');
|
||||
@@ -6183,14 +6629,48 @@ namespace {
|
||||
}
|
||||
if (std::strncmp(buf, "MOTK1 START\n", 12) == 0) {
|
||||
g_lnch_remote_lan_state.started = true;
|
||||
const char* sid = buf + 12;
|
||||
if (*sid) {
|
||||
const unsigned v = (unsigned)std::strtoul(sid, nullptr, 10);
|
||||
/* MOTK1 START\n<session>\n[<delay>\n<prediction>\n<rollback>\n]
|
||||
* Trailing caps lines are host-authoritative (LAN had no
|
||||
* match_caps channel; guests used local UI defaults). */
|
||||
char* p = buf + 12;
|
||||
char* nl = std::strchr(p, '\n');
|
||||
if (nl) *nl = '\0';
|
||||
if (*p) {
|
||||
const unsigned v = (unsigned)std::strtoul(p, nullptr, 10);
|
||||
if (v) {
|
||||
g_lnch_lan_session_id = (uint32_t)v;
|
||||
g_lnch_remote_lan_state.session_id = (uint32_t)v;
|
||||
}
|
||||
}
|
||||
if (nl) {
|
||||
p = nl + 1;
|
||||
char* lines[3] = {};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (!p || !*p) break;
|
||||
lines[i] = p;
|
||||
char* n2 = std::strchr(p, '\n');
|
||||
if (n2) {
|
||||
*n2 = '\0';
|
||||
p = n2 + 1;
|
||||
} else {
|
||||
p = nullptr;
|
||||
}
|
||||
}
|
||||
if (lines[0] && lines[0][0]) {
|
||||
int d = std::atoi(lines[0]);
|
||||
if (d < 2) d = 2;
|
||||
if (d > 20) d = 20;
|
||||
g_lnch_lobby_input_delay = d;
|
||||
}
|
||||
if (lines[1] && lines[1][0]) {
|
||||
int pred = std::atoi(lines[1]);
|
||||
if (pred < 2) pred = 2;
|
||||
if (pred > 16) pred = 16;
|
||||
g_lnch_lobby_input_prediction = pred;
|
||||
}
|
||||
if (lines[2] && lines[2][0])
|
||||
g_lnch_rollback = (std::atoi(lines[2]) != 0) ? 1 : 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (std::strncmp(buf, "MOTK1 KICK\n", 11) == 0 ||
|
||||
@@ -6231,14 +6711,20 @@ namespace {
|
||||
}
|
||||
|
||||
int ae_np_list_count(void*) {
|
||||
return psx_lobby_list_count() + (ae_np_lan_list_visible() ? 1 : 0);
|
||||
return psx_lobby_list_count() + ae_np_lan_list_extra_count();
|
||||
}
|
||||
|
||||
int ae_np_list_get(void*, int index, RecompLauncherCNetplayLobby* out) {
|
||||
if (!out) return 0;
|
||||
const int remote_count = psx_lobby_list_count();
|
||||
if (index >= remote_count)
|
||||
return ae_np_lan_list_visible() ? ae_np_read_lan_lobby(out) : 0;
|
||||
if (index >= remote_count) {
|
||||
const int lan_i = index - remote_count;
|
||||
ae_np_lan_prune_discovered();
|
||||
if (g_lnch_lan_discovered_n > 0)
|
||||
return ae_np_lan_fill_lobby_from_discovered(lan_i, out);
|
||||
return (lan_i == 0 && ae_np_lan_list_visible())
|
||||
? ae_np_read_lan_lobby(out) : 0;
|
||||
}
|
||||
PsxLobbyRow row{};
|
||||
if (!psx_lobby_list_get(index, &row)) return 0;
|
||||
std::snprintf(out->lobby_id, sizeof(out->lobby_id), "%s", row.lobby_id);
|
||||
@@ -6564,6 +7050,10 @@ namespace {
|
||||
if (psx_lobby_in_lobby())
|
||||
(void)psx_lobby_leave();
|
||||
|
||||
/* Adopt host D/P/rollback from the last BEACON (if any) before
|
||||
* seating — START still re-asserts host-authoritative caps. */
|
||||
ae_np_lan_apply_discovered_caps(endpoint);
|
||||
|
||||
/* Must be a live LAN/Direct IP host (UDP PONG). Online-only hosts
|
||||
* never answer — refuse so we don't open a fake local room. */
|
||||
if (!ae_np_lan_probe_host_ms(endpoint, 750u))
|
||||
@@ -6847,9 +7337,17 @@ namespace {
|
||||
g_lnch_lan_session_id = state.session_id;
|
||||
if (!ae_np_write_lan_state(state)) return -1;
|
||||
ae_np_lan_send_update_to_peers(state);
|
||||
char start_msg[64];
|
||||
std::snprintf(start_msg, sizeof(start_msg), "MOTK1 START\n%u\n",
|
||||
(unsigned)state.session_id);
|
||||
char start_msg[96];
|
||||
int delay = g_lnch_lobby_input_delay;
|
||||
int pred = g_lnch_lobby_input_prediction;
|
||||
if (delay < 2) delay = 2;
|
||||
if (delay > 20) delay = 20;
|
||||
if (pred < 2) pred = 2;
|
||||
if (pred > 16) pred = 16;
|
||||
std::snprintf(start_msg, sizeof(start_msg),
|
||||
"MOTK1 START\n%u\n%d\n%d\n%d\n",
|
||||
(unsigned)state.session_id, delay, pred,
|
||||
g_lnch_rollback ? 1 : 0);
|
||||
for (int i = 0; i < kAeLanMaxSlots; ++i) {
|
||||
if (g_lnch_lan_peer_ok[i])
|
||||
ae_np_lan_udp_sendto(g_lnch_lan_peers[i], start_msg);
|
||||
|
||||
@@ -92,6 +92,14 @@ uint8_t netplay_hc_local_digest(const NetplayHashConfirm* hc, uint32_t tick,
|
||||
return 1u;
|
||||
}
|
||||
|
||||
uint8_t netplay_hc_peer_digest(const NetplayHashConfirm* hc, uint32_t tick,
|
||||
uint32_t* digest_out) {
|
||||
uint32_t d = 0;
|
||||
if (!hc || !peer_at(hc, tick, &d)) return 0u;
|
||||
if (digest_out) *digest_out = d;
|
||||
return 1u;
|
||||
}
|
||||
|
||||
uint8_t netplay_hc_peek_mismatch(const NetplayHashConfirm* hc, uint32_t* tick_out,
|
||||
uint32_t* local_out, uint32_t* peer_out) {
|
||||
uint32_t next;
|
||||
|
||||
+929
-82
File diff suppressed because it is too large
Load Diff
+964
-102
File diff suppressed because it is too large
Load Diff
@@ -31,6 +31,12 @@ int main(void) {
|
||||
CHECK(netplay_hc_confirm_through(&hc, 0), "tick 0 matched");
|
||||
CHECK(netplay_hc_resolved_through(&hc) == 0u, "resolved at 0");
|
||||
CHECK(!netplay_hc_confirm_through(&hc, 1), "tick 1 not yet");
|
||||
{
|
||||
uint32_t pd = 0;
|
||||
CHECK(netplay_hc_peer_digest(&hc, 0, &pd) && pd == 0x1111u,
|
||||
"peer_digest peeks FRAME_COMMIT hash");
|
||||
CHECK(!netplay_hc_peer_digest(&hc, 1, &pd), "peer_digest missing tick");
|
||||
}
|
||||
|
||||
netplay_hc_note_peer(&hc, 1, 0x2222u);
|
||||
CHECK(netplay_hc_confirm_through(&hc, 1), "tick 1 matched");
|
||||
|
||||
Reference in New Issue
Block a user