Files
psxrecomp/runtime/include/netplay_local_view.h
Shokunin 465f926ecf netplay: confirm against every peer, shed the own view when behind, progress-based stall timeout
Four-peer rollback desynced under load (R4, host behind): the guests
predicted the slow host's pad, hash-confirmed the tick among themselves
through the one shared peer ring, then promoted the host's real pad into
history only (hc-silent promote) while the host simulated it. The host
stayed forked; its recovery episode was then refused as a "zombie load"
behind the guests' (wrongly) agreed tip.

- Hash confirm is a quorum over every occupied peer seat
  (rnet_hc_set_peer_mask / note_peer_from, recomp-net) for three or more
  seats; two seats keep the one-peer chain.
- A peer more than 4 ticks behind the others' inputs sheds its own view
  (psx_mod_render_local_view status FAST_FORWARD, held 60 ticks), so the
  presentation-only pass goes before the simulation slips.
  PSX_NET_LOCAL_VIEW_SHED=0 keeps it.
- The admit-stall watchdog counts time without progress: a remote input tip
  that advances restarts it, so a slow peer is waited for.
  PSX_NET_ADMIT_STALL_MS (20000) and PSX_NET_LIVENESS_MS (1500) configure it.
- Pins recomp-net fix/rollback-span-stall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 06:59:03 -07:00

113 lines
4.4 KiB
C

#ifndef NETPLAY_LOCAL_VIEW_H
#define NETPLAY_LOCAL_VIEW_H
#include <stdint.h>
#include <string.h>
/* Host-only record of the display sub-rectangle a title asked the present
* path to show for the local netplay seat (psx_netplay_present_local_view).
* Never serialized and never visible to the guest: every peer still renders
* and digests the identical full frame. A request covers the simulation tick
* it was made on plus PSX_NETPLAY_LOCAL_VIEW_HOLD later ticks, so a title only
* has to renew it while its multi-view screen is up; menus fall back to the
* full frame by themselves. Rollback resimulation repeats ticks up to
* PSX_NETPLAY_LOCAL_VIEW_REPLAY behind the newest request; those never move
* the stamp backwards. A larger step back (a load) restarts it. */
#define PSX_NETPLAY_LOCAL_VIEW_HOLD 8u
#define PSX_NETPLAY_LOCAL_VIEW_REPLAY 64u
typedef struct PsxNetplayLocalView {
uint32_t x, y, w, h;
uint32_t tick;
uint8_t valid;
} PsxNetplayLocalView;
static inline void psx_netplay_local_view_reset(PsxNetplayLocalView *view)
{
if (view) memset(view, 0, sizeof(*view));
}
/* Rejects empty rectangles and anything outside 1024x512 VRAM space. */
static inline int psx_netplay_local_view_set(PsxNetplayLocalView *view,
uint32_t tick, uint32_t x,
uint32_t y, uint32_t w,
uint32_t h)
{
if (!view || w == 0u || h == 0u || x >= 1024u || y >= 512u ||
w > 1024u - x || h > 512u - y)
return 0;
if (view->valid && (int32_t)(tick - view->tick) < 0 &&
view->tick - tick <= PSX_NETPLAY_LOCAL_VIEW_REPLAY)
tick = view->tick;
view->x = x;
view->y = y;
view->w = w;
view->h = h;
view->tick = tick;
view->valid = 1u;
return 1;
}
/* 1 and the rectangle when a request is still current at `now` and fits a
* display of display_w x display_h; 0 otherwise. */
static inline int psx_netplay_local_view_get(const PsxNetplayLocalView *view,
uint32_t now, uint32_t display_w,
uint32_t display_h, uint32_t *x,
uint32_t *y, uint32_t *w,
uint32_t *h)
{
if (!view || !view->valid)
return 0;
const int32_t age = (int32_t)(now - view->tick);
if (age < 0 ? view->tick - now > PSX_NETPLAY_LOCAL_VIEW_REPLAY
: (uint32_t)age > PSX_NETPLAY_LOCAL_VIEW_HOLD)
return 0;
if (view->x + view->w > display_w || view->y + view->h > display_h)
return 0;
if (x) *x = view->x;
if (y) *y = view->y;
if (w) *w = view->w;
if (h) *h = view->h;
return 1;
}
/* Load shedding for psx_mod_render_local_view. A peer's own view is
* presentation only, so when this peer falls behind the others' inputs it
* goes first, before the simulation itself slips further (a slow peer forces
* deeper rollbacks on everyone). "Behind" is remote_lead - input delay: the
* newest remote input is normally about D ticks ahead of the simulation.
* Shedding starts at PSX_NETPLAY_LOCAL_VIEW_SHED_BEHIND ticks behind and
* lasts at least PSX_NETPLAY_LOCAL_VIEW_SHED_HOLD ticks and until the peer is
* within PSX_NETPLAY_LOCAL_VIEW_SHED_CAUGHT_UP ticks again, so the view does
* not flicker between the own view and the fallback every other frame. */
#define PSX_NETPLAY_LOCAL_VIEW_SHED_BEHIND 4
#define PSX_NETPLAY_LOCAL_VIEW_SHED_CAUGHT_UP 1
#define PSX_NETPLAY_LOCAL_VIEW_SHED_HOLD 60u
typedef struct PsxNetplayLocalViewShed {
uint32_t until;
uint8_t on;
} PsxNetplayLocalViewShed;
static inline int psx_netplay_local_view_shed_step(PsxNetplayLocalViewShed *s,
uint32_t sim,
int remote_lead, int delay)
{
const int behind = remote_lead - (delay > 0 ? delay : 0);
if (!s)
return 0;
if (behind >= PSX_NETPLAY_LOCAL_VIEW_SHED_BEHIND) {
s->on = 1u;
s->until = sim + PSX_NETPLAY_LOCAL_VIEW_SHED_HOLD;
} else if (s->on && (int32_t)(sim - s->until) >= 0 &&
behind <= PSX_NETPLAY_LOCAL_VIEW_SHED_CAUGHT_UP) {
s->on = 0u;
} else if (s->on &&
(uint32_t)(s->until - sim) > 2u * PSX_NETPLAY_LOCAL_VIEW_SHED_HOLD) {
s->until = sim + PSX_NETPLAY_LOCAL_VIEW_SHED_HOLD; /* stepped back (load) */
}
return s->on;
}
#endif