diff --git a/docs/ROLLBACK_MOTK_HOOKUP.md b/docs/ROLLBACK_MOTK_HOOKUP.md index aea42803..37aec3f3 100644 --- a/docs/ROLLBACK_MOTK_HOOKUP.md +++ b/docs/ROLLBACK_MOTK_HOOKUP.md @@ -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` diff --git a/runtime/include/netplay_state_digest.h b/runtime/include/netplay_state_digest.h index c2350e55..861c8c63 100644 --- a/runtime/include/netplay_state_digest.h +++ b/runtime/include/netplay_state_digest.h @@ -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 } diff --git a/runtime/include/psx_netplay.h b/runtime/include/psx_netplay.h index 8a04bae3..2234f3b5 100644 --- a/runtime/include/psx_netplay.h +++ b/runtime/include/psx_netplay.h @@ -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 diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index a780c4c0..7e8ef584 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -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(); diff --git a/runtime/src/gpu_vk_renderer.c b/runtime/src/gpu_vk_renderer.c index 6ccc4bc7..36a19b50 100644 --- a/runtime/src/gpu_vk_renderer.c +++ b/runtime/src/gpu_vk_renderer.c @@ -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 diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 53422a6b..7d34a759 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -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 diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index 7bfd4301..17e229e5 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -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); diff --git a/runtime/src/psx_netplay_rb.c b/runtime/src/psx_netplay_rb.c index 5da2600a..9830463b 100644 --- a/runtime/src/psx_netplay_rb.c +++ b/runtime/src/psx_netplay_rb.c @@ -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); }