rollback integrations 2

This commit is contained in:
Alex Vanderveen
2026-07-30 02:00:31 -04:00
parent 3b88d9f9b5
commit 88234d5e60
8 changed files with 112 additions and 28 deletions
+3
View File
@@ -90,6 +90,9 @@ RTT (BattleShip tiers / phase_lock); optional Manual Input Delay / Prediction.
- [x] Storm calm: 12-tick rewind cooldown + promote-sweep after commit/realign;
MotK digital release-only → promote (no episode); freeze `cdrom_advance`
during Replay (CD IRQ timing was forking POST cores)
- [x] Netplay forces **software GPU** — GL/VK `glReadPixels` VRAM readback was
forking peer snaps (core matched, pin zlib ~220KB apart) and mid-resim
cores; baseline/POST also agree on `av=` (GPU+VRAM) via dig_b / POST input_digest
- [ ] Memory budget / thinner snap: optional strip CDROM/MDEC if MotK match
path allows (further FPS headroom)
- [x] Standalone ring bookkeeping test: `runtime/tests/test_netplay_snap_ring.c`
+1
View File
@@ -21,6 +21,7 @@ extern "C" {
uint32_t netplay_core_digest(const CPUState* cpu);
uint32_t netplay_master_digest(const CPUState* cpu);
uint32_t netplay_cdrom_digest(void);
uint32_t netplay_av_digest(void); /* GPU + VRAM */
#ifdef __cplusplus
}
+4
View File
@@ -195,6 +195,10 @@ void psx_netplay_poll_snap(struct CPUState *cpu, uint32_t resume_pc);
/* 1 while a rollback episode is resimulating (mute audio / skip wall pacer). */
int psx_netplay_is_resimulating(void);
/* Force software GPU when netplay starts — GL/VK VRAM readback is host-GPU
* nondeterministic and forks rollback snaps/resim. Implemented in main.cpp. */
void psx_frontend_netplay_force_sw_gpu(void);
#ifdef __cplusplus
}
#endif
+7
View File
@@ -1273,7 +1273,14 @@ static void flush_pack_if_sampling(int tpage_x, int tpage_y, int depth,
/* ---- coherency: GPU -> CPU readback -------------------------------------- */
static void ensure_cpu(void) {
extern int psx_netplay_active(void);
if (!s_raster_ok || !s_gpu_dirty) return;
/* Netplay forces SW backend; if GL is still selected, never read back —
* glReadPixels forks peer VRAM (and therefore rollback snaps/resim). */
if (psx_netplay_active()) {
s_gpu_dirty = 0;
return;
}
flush_flat_batch();
flush_tex_batch(); /* realise queued textured draws before reading the FBO back */
flush_cpu_upload();
+5
View File
@@ -2173,7 +2173,12 @@ static void flush_cpu_upload(void) {
/* GPU -> CPU mirror: drain batches, pack, copy the whole raw mirror down. */
static void ensure_cpu(void) {
extern int psx_netplay_active(void);
if (!s_ready || !s_gpu_dirty || !s_vram) return;
if (psx_netplay_active()) {
s_gpu_dirty = 0;
return;
}
flush_cpu_upload(); /* readback overwrites s_vram: pending writes land first */
flush_tex_batch(); flush_geometry();
/* Readback must reflect ALL current hr content, not just the incremental
+27
View File
@@ -1091,6 +1091,33 @@ static bool g_vk_active = false; /* Vulkan context live -> VK presen
* keeps CPU VRAM current every frame (so screenshots reflect the screen). */
static int g_gl_fbo_present = 1;
/* Rollback/delay netplay: GL/VK keep FBO-authoritative VRAM and sync the CPU
* mirror via readback — host-GPU nondeterministic; forks peer snaps (core
* matched, VRAM zlib ~220KB apart) then mid-resim cores. SW is guest authority. */
extern "C" void psx_frontend_netplay_force_sw_gpu(void) {
static int s_locked;
if (s_locked)
return;
s_locked = 1;
#ifndef PSX_SDL_NO_RENDER
if (g_gl_active)
gl_renderer_sync_cpu();
if (g_vk_active)
vk_renderer_sync_cpu();
#endif
gr_set_backend(GR_BACKEND_SOFTWARE);
#ifndef PSX_SDL_NO_RENDER
g_gl_active = false;
g_vk_active = false;
g_gl_fbo_present = false;
#endif
latency_ring_set_backend("software");
fprintf(stderr,
"psxrecomp: netplay forced software GPU "
"(GL/VK VRAM readback forks peers)\n");
fflush(stderr);
}
/* Vsync self-heal state (see SDL_RenderPresent wrapper in
* sdl_vblank_present). C linkage: freeze_heartbeat.c includes both in
* the freeze dump so a slow-frames wedge can be attributed to driver
+2
View File
@@ -1825,6 +1825,8 @@ int psx_netplay_start(const PsxNetplayConfig *cfg)
}
np_diag_capture(cfg, slots);
g_np.active = 1;
/* Before any snap/resim: SW GPU so VRAM is bit-identical across peers. */
psx_frontend_netplay_force_sw_gpu();
netplay_hc_reset(&g_np.hc);
g_np.rollback = cfg->rollback ? 1 : 0;
netplay_ih_reset(&g_np.ih, (int)rcfg.slot_count);
+63 -28
View File
@@ -85,8 +85,10 @@ static uint32_t snap_interval(void)
}
static int g_local_baseline_sent;
static uint32_t g_local_baseline_digest;
static uint32_t g_local_baseline_av;
static int g_peer_baseline_ok;
static uint32_t g_peer_baseline_digest;
static uint32_t g_peer_baseline_av;
static int g_peer_baseline_ready; /* dig_a flag: follower ACK after it has our baseline */
static int g_local_baseline_ready_sent;
static int g_local_post_sent;
@@ -94,6 +96,8 @@ static int g_peer_post_ok;
static uint32_t g_peer_post_digest;
static uint8_t g_peer_post_match;
static uint32_t g_post_digest;
static uint32_t g_post_av;
static uint32_t g_peer_post_av;
static int g_needs_advance;
static int g_seal_export_logged;
static int g_baseline_rexmit_logged;
@@ -115,6 +119,7 @@ static uint32_t g_stash_bl_epoch;
static uint32_t g_stash_bl_load;
static uint32_t g_stash_bl_dig_m;
static uint32_t g_stash_bl_dig_a;
static uint32_t g_stash_bl_dig_b;
static int g_stash_bl_logged;
static uint64_t g_baseline_handshake_ms; /* when local baseline first sent */
static uint64_t g_last_baseline_burst_ms;
@@ -242,12 +247,16 @@ static void clear_episode_wire_state(void)
{
g_local_baseline_sent = 0;
g_local_baseline_digest = 0;
g_local_baseline_av = 0;
g_peer_baseline_ok = 0;
g_peer_baseline_digest = 0;
g_peer_baseline_av = 0;
g_peer_baseline_ready = 0;
g_local_baseline_ready_sent = 0;
g_local_post_sent = 0;
g_peer_post_ok = 0;
g_post_av = 0;
g_peer_post_av = 0;
g_baseline_rexmit_logged = 0;
g_post_rexmit_logged = 0;
g_seal_wait_ms = 0;
@@ -574,7 +583,8 @@ static void publish_sealed_sio(uint32_t tick)
static void maybe_send_baseline(void);
static void maybe_enter_replay(void);
static void accept_peer_baseline(uint32_t epoch, uint32_t load, uint32_t dig_m, uint32_t dig_a);
static void accept_peer_baseline(uint32_t epoch, uint32_t load, uint32_t dig_m,
uint32_t dig_a, uint32_t dig_b);
static void apply_stashed_baseline(void);
static void enter_awaiting_baseline(void)
@@ -596,18 +606,20 @@ static void enter_awaiting_baseline(void)
apply_stashed_baseline();
}
static void accept_peer_baseline(uint32_t epoch, uint32_t load, uint32_t dig_m, uint32_t dig_a)
static void accept_peer_baseline(uint32_t epoch, uint32_t load, uint32_t dig_m,
uint32_t dig_a, uint32_t dig_b)
{
(void)epoch;
(void)load;
g_peer_baseline_ok = 1;
g_peer_baseline_digest = dig_m;
g_peer_baseline_av = dig_b;
if (dig_a & RB_BL_FLAG_READY)
g_peer_baseline_ready = 1;
fprintf(stderr,
"psxrecomp: rb peer baseline epoch=%u load=%u core=%08x ready=%u "
"psxrecomp: rb peer baseline epoch=%u load=%u core=%08x av=%08x ready=%u "
"(local_applied=%d phase=%d)\n",
(unsigned)epoch, (unsigned)load, (unsigned)dig_m,
(unsigned)epoch, (unsigned)load, (unsigned)dig_m, (unsigned)dig_b,
(unsigned)(dig_a & RB_BL_FLAG_READY), g_episode_snap_applied,
(int)rnet_rb_get_phase(g_rb));
fflush(stderr);
@@ -626,13 +638,15 @@ static void apply_stashed_baseline(void)
(unsigned)g_stash_bl_epoch, (unsigned)g_stash_bl_dig_m,
(unsigned)(g_stash_bl_dig_a & RB_BL_FLAG_READY));
fflush(stderr);
accept_peer_baseline(g_stash_bl_epoch, g_stash_bl_load, g_stash_bl_dig_m, g_stash_bl_dig_a);
accept_peer_baseline(g_stash_bl_epoch, g_stash_bl_load, g_stash_bl_dig_m,
g_stash_bl_dig_a, g_stash_bl_dig_b);
}
static void stash_or_accept_baseline(uint32_t epoch, uint32_t load, uint32_t dig_m, uint32_t dig_a)
static void stash_or_accept_baseline(uint32_t epoch, uint32_t load, uint32_t dig_m,
uint32_t dig_a, uint32_t dig_b)
{
if (g_rb && rnet_rb_is_active(g_rb) && epoch == rnet_rb_get_epoch_id(g_rb)) {
accept_peer_baseline(epoch, load, dig_m, dig_a);
accept_peer_baseline(epoch, load, dig_m, dig_a, dig_b);
return;
}
/* Host often applies+sends before the follower has opened the episode —
@@ -642,6 +656,7 @@ static void stash_or_accept_baseline(uint32_t epoch, uint32_t load, uint32_t dig
g_stash_bl_load = load;
g_stash_bl_dig_m = dig_m;
g_stash_bl_dig_a = dig_a;
g_stash_bl_dig_b = dig_b;
if (!g_stash_bl_logged) {
fprintf(stderr,
"psxrecomp: rb baseline stashed epoch=%u load=%u dig=%08x "
@@ -659,7 +674,7 @@ static void send_baseline_packet(int ready, int rexmit_log)
return;
(void)rnet_session_send_rb_baseline(s, rnet_rb_get_epoch_id(g_rb),
rnet_rb_get_load_tick(g_rb), g_local_baseline_digest,
ready ? RB_BL_FLAG_READY : 0u, 0, 0);
ready ? RB_BL_FLAG_READY : 0u, g_local_baseline_av, 0);
if (ready)
g_local_baseline_ready_sent = 1;
if (rexmit_log && !g_baseline_rexmit_logged) {
@@ -697,14 +712,16 @@ static void maybe_send_baseline(void)
follower = rnet_rb_is_from_peer_notify(g_rb) ? 1 : 0;
if (!g_local_baseline_sent) {
g_local_baseline_digest = netplay_core_digest(c);
g_local_baseline_av = netplay_av_digest();
g_local_baseline_sent = 1;
g_baseline_handshake_ms = rb_mono_ms();
g_last_baseline_burst_ms = 0; /* allow immediate burst */
send_baseline_burst(0, RB_BASELINE_BURST, 0);
fprintf(stderr,
"psxrecomp: rb baseline sent load=%u core=%08x cd=%08x (burst=%d)\n",
"psxrecomp: rb baseline sent load=%u core=%08x av=%08x cd=%08x (burst=%d)\n",
(unsigned)rnet_rb_get_load_tick(g_rb), (unsigned)g_local_baseline_digest,
(unsigned)netplay_cdrom_digest(), RB_BASELINE_BURST);
(unsigned)g_local_baseline_av, (unsigned)netplay_cdrom_digest(),
RB_BASELINE_BURST);
fflush(stderr);
return;
}
@@ -737,6 +754,7 @@ static void log_resim_tick_audit(uint32_t sim, const char *tag)
int n = g_b.slot_count ? *g_b.slot_count : 0;
uint32_t dig = 0u;
uint32_t core = 0u;
uint32_t av = netplay_av_digest();
uint32_t cd = netplay_cdrom_digest();
if (c) {
CPUState dig_cpu = *c;
@@ -749,9 +767,9 @@ static void log_resim_tick_audit(uint32_t sim, const char *tag)
dig = netplay_master_digest(&dig_cpu);
}
fprintf(stderr,
"psxrecomp: rb audit %s sim=%u dig=%08x core=%08x cd=%08x cyc=%llu",
"psxrecomp: rb audit %s sim=%u dig=%08x core=%08x av=%08x cd=%08x cyc=%llu",
tag ? tag : "?", (unsigned)sim, (unsigned)dig, (unsigned)core,
(unsigned)cd, (unsigned long long)psx_cycle_count);
(unsigned)av, (unsigned)cd, (unsigned long long)psx_cycle_count);
for (slot = 0; slot < n; ++slot) {
RNetRbFrame row;
if (rnet_rb_get_sealed_frame(g_rb, slot, sim, &row) && row.is_valid)
@@ -798,6 +816,13 @@ static void maybe_enter_replay(void)
abort_episode_realign(why);
return;
}
if (g_peer_baseline_av != g_local_baseline_av) {
char why[112];
snprintf(why, sizeof(why), "baseline av mismatch local=%08x peer=%08x",
(unsigned)g_local_baseline_av, (unsigned)g_peer_baseline_av);
abort_episode_realign(why);
return;
}
g_episode_baseline_matched = 1;
g_episode_load_tick = rnet_rb_get_load_tick(g_rb);
follower = rnet_rb_is_from_peer_notify(g_rb) ? 1 : 0;
@@ -1416,25 +1441,28 @@ void psx_netplay_rb_pump(void)
}
while (rnet_session_take_rb_baseline(s, &epoch, &load, &dig_m, &dig_a, &dig_b, &dig_c)) {
(void)dig_b;
(void)dig_c;
stash_or_accept_baseline(epoch, load, dig_m, dig_a);
stash_or_accept_baseline(epoch, load, dig_m, dig_a, dig_b);
}
while (rnet_session_take_rb_post(s, &epoch, &target, &dig_m, &in_dig, &match)) {
(void)in_dig;
if (!rnet_rb_is_active(g_rb) || epoch != rnet_rb_get_epoch_id(g_rb))
continue;
g_peer_post_ok = 1;
g_peer_post_digest = dig_m;
g_peer_post_av = in_dig;
g_peer_post_match = match;
if (rnet_rb_get_phase(g_rb) == nRNetRbPhaseVerify && g_local_post_sent) {
if (dig_m == g_post_digest)
if (dig_m == g_post_digest && in_dig == g_post_av)
commit_episode();
else {
char why[96];
snprintf(why, sizeof(why), "post core diverge local=%08x peer=%08x",
(unsigned)g_post_digest, (unsigned)dig_m);
char why[128];
if (dig_m != g_post_digest)
snprintf(why, sizeof(why), "post core diverge local=%08x peer=%08x",
(unsigned)g_post_digest, (unsigned)dig_m);
else
snprintf(why, sizeof(why), "post av diverge local=%08x peer=%08x",
(unsigned)g_post_av, (unsigned)in_dig);
(void)g_peer_post_match;
rnet_rb_on_post_diverge(g_rb);
abort_episode_realign(why); /* lobby stays; Live rewinds to agreed tip */
@@ -1503,7 +1531,8 @@ void psx_netplay_rb_pump(void)
g_post_rexmit_logged = 1;
}
(void)rnet_session_send_rb_post(s, rnet_rb_get_epoch_id(g_rb),
rnet_rb_get_target_tick(g_rb), g_post_digest, 0u, 1u);
rnet_rb_get_target_tick(g_rb), g_post_digest, g_post_av,
1u);
if (g_verify_wait_ms == 0ull)
g_verify_wait_ms = now;
else if (now >= g_verify_wait_ms &&
@@ -1639,23 +1668,29 @@ void psx_netplay_rb_finish_frame(void)
} else {
g_post_digest = 0u;
}
g_post_av = netplay_av_digest();
rnet_rb_set_phase(g_rb, nRNetRbPhaseVerify);
g_verify_wait_ms = rb_mono_ms();
(void)rnet_session_send_rb_post(s, rnet_rb_get_epoch_id(g_rb), done, g_post_digest, 0u,
1u);
(void)rnet_session_send_rb_post(s, rnet_rb_get_epoch_id(g_rb), done, g_post_digest,
g_post_av, 1u);
g_local_post_sent = 1;
g_post_rexmit_logged = 0;
fprintf(stderr,
"psxrecomp: rb post sent core=%08x cd=%08x (peer_ok=%d)\n",
(unsigned)g_post_digest, (unsigned)netplay_cdrom_digest(), g_peer_post_ok);
"psxrecomp: rb post sent core=%08x av=%08x cd=%08x (peer_ok=%d)\n",
(unsigned)g_post_digest, (unsigned)g_post_av,
(unsigned)netplay_cdrom_digest(), g_peer_post_ok);
fflush(stderr);
if (g_peer_post_ok) {
if (g_peer_post_digest == g_post_digest)
if (g_peer_post_digest == g_post_digest && g_peer_post_av == g_post_av)
commit_episode();
else {
char why[96];
snprintf(why, sizeof(why), "post core diverge local=%08x peer=%08x",
(unsigned)g_post_digest, (unsigned)g_peer_post_digest);
char why[128];
if (g_peer_post_digest != g_post_digest)
snprintf(why, sizeof(why), "post core diverge local=%08x peer=%08x",
(unsigned)g_post_digest, (unsigned)g_peer_post_digest);
else
snprintf(why, sizeof(why), "post av diverge local=%08x peer=%08x",
(unsigned)g_post_av, (unsigned)g_peer_post_av);
rnet_rb_on_post_diverge(g_rb);
abort_episode_realign(why);
}