Netplay rollback: count post-movie agreement in confirmed ticks

The post-FMV lockstep gate counted a tick as agreed only when sim-1 was
already hash-confirmed. A peer digest arrives one RTT after its tick, so
the peer running ahead (the host, cross-machine) never saw sim-1 confirmed,
reset the streak every tick and logged 'rb DESYNC - FMV lockstep MAX
unmatched' after every movie while every digest matched. Each newly
confirmed tick past the movie's end now counts once; only a real mismatch
resets the streak.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-09 00:12:37 -07:00
parent 15a8096ed7
commit 4f76e2eec2
4 changed files with 80 additions and 9 deletions
+3
View File
@@ -53,6 +53,9 @@ if(BUILD_TESTING)
target_include_directories(netplay_sim_pad_cache_test PRIVATE include)
target_compile_definitions(netplay_sim_pad_cache_test PRIVATE PSX_MAX_PLAYERS=4)
add_test(NAME netplay_sim_pad_cache_test COMMAND netplay_sim_pad_cache_test)
add_executable(netplay_fmv_confirm_test tests/test_netplay_fmv_confirm.c)
target_include_directories(netplay_fmv_confirm_test PRIVATE include)
add_test(NAME netplay_fmv_confirm_test COMMAND netplay_fmv_confirm_test)
add_executable(netplay_local_view_test tests/test_netplay_local_view.c)
target_include_directories(netplay_local_view_test PRIVATE include)
add_test(NAME netplay_local_view_test COMMAND netplay_local_view_test)
+21
View File
@@ -0,0 +1,21 @@
/* netplay_fmv_confirm.h - post-movie agreement counting for rollback netplay.
*
* After a movie, both peers must agree for RB_FMV_LOCKSTEP_CONFIRM ticks
* before prediction is unlocked. Agreement is measured in hash-confirmed
* ticks: each newly confirmed tick past the movie's end counts once, however
* late its confirmation arrives. A confirmation is always about one RTT
* behind the tick it confirms, so testing "is sim-1 confirmed yet" every tick
* never succeeds on the peer that runs ahead. */
#ifndef PSX_NETPLAY_FMV_CONFIRM_H
#define PSX_NETPLAY_FMV_CONFIRM_H
#include <stdint.h>
/* Confirmed ticks gained when the confirmed frontier moves from `from` (the
* highest tick already counted, or the movie's end) to `resolved`. */
static inline uint32_t rb_fmv_confirm_gain(uint32_t from, uint32_t resolved)
{
return resolved > from ? resolved - from : 0u;
}
#endif
+25 -9
View File
@@ -116,6 +116,7 @@ uint32_t psx_netplay_rb_rtt_estimate_ms(void) { return 0; }
#else
#include "netplay_hash_confirm.h"
#include "netplay_fmv_confirm.h"
#include "netplay_input_hist.h"
#include "netplay_rb_post.h"
#include "netplay_snap_ring.h"
@@ -715,6 +716,8 @@ static uint32_t g_fmv_settle_until;
static uint32_t g_fmv_lockstep_until;
static uint32_t g_fmv_media_end_sim;
static uint32_t g_fmv_core_match_streak;
/* Highest hash-confirmed tick already counted into the streak. */
static uint32_t g_fmv_confirm_seen;
static int g_fmv_lockstep_released; /* sticky: never re-lock after invent on */
/* Inclusive last tick both peers dense-snapped (media/lockstep/+tip). */
static uint32_t g_fmv_dense_through;
@@ -1037,21 +1040,31 @@ static int rb_fmv_media_active(void)
/* After media: grow/shrink lockstep_until from hash_confirm agreement. */
static void rb_fmv_update_lockstep_gate(uint32_t sim)
{
int matched = 0;
uint32_t cap;
uint32_t prev_until;
if (!g_fmv_media_end_sim || rb_fmv_media_active())
return;
if (g_b.hc && !netplay_hc_peek_mismatch(g_b.hc, NULL, NULL, NULL)) {
uint32_t need = (sim > 0u) ? (sim - 1u) : 0u;
if (netplay_hc_confirm_through(g_b.hc, need))
matched = 1;
/* Agreement is counted in CONFIRMED ticks, not in "sim-1 is already
* confirmed" ticks. The peer's digest for a tick arrives one RTT after
* the tick; the peer that runs ahead (usually the host, cross-machine)
* never has sim-1 confirmed when it asks, so the old per-tick test reset
* the streak every tick and fired a false "MAX unmatched" DESYNC after
* every movie even though every digest matched. Only a real mismatch
* resets the streak now; a confirm that is merely late leaves it. */
{
uint32_t gained = 0u;
if (g_b.hc && netplay_hc_peek_mismatch(g_b.hc, NULL, NULL, NULL)) {
g_fmv_core_match_streak = 0;
} else if (g_b.hc && netplay_hc_confirm_through(g_b.hc, g_fmv_media_end_sim)) {
const uint32_t rt = netplay_hc_resolved_through(g_b.hc);
const uint32_t from = (g_fmv_confirm_seen > g_fmv_media_end_sim)
? g_fmv_confirm_seen : g_fmv_media_end_sim;
gained = rb_fmv_confirm_gain(from, rt);
if (rt > g_fmv_confirm_seen) g_fmv_confirm_seen = rt;
}
g_fmv_core_match_streak += gained;
}
if (matched)
g_fmv_core_match_streak++;
else
g_fmv_core_match_streak = 0;
/* §93: after MAX-unmatched DESYNC, keep counting so a later rematch can
* clear invent/begin hold without requiring a new media bout. */
@@ -1237,6 +1250,7 @@ static void rb_fmv_tick_settle(void)
if (sim >= g_fmv_media_hi)
g_fmv_media_hi = sim;
g_fmv_core_match_streak = 0;
g_fmv_confirm_seen = 0;
g_fmv_lockstep_released = 0;
g_fmv_settle_until = sim + RB_FMV_SETTLE_TICKS;
g_fmv_lockstep_until = sim + RB_FMV_LOCKSTEP_MIN;
@@ -6498,6 +6512,7 @@ void psx_netplay_rb_cold_reset(void)
g_fmv_settle_until = 0;
g_fmv_lockstep_until = 0;
g_fmv_media_end_sim = 0;
g_fmv_confirm_seen = 0;
g_fmv_core_match_streak = 0;
g_fmv_lockstep_released = 0;
g_fmv_dense_through = 0;
@@ -6551,6 +6566,7 @@ void psx_netplay_rb_shutdown(void)
g_fmv_settle_until = 0;
g_fmv_lockstep_until = 0;
g_fmv_media_end_sim = 0;
g_fmv_confirm_seen = 0;
g_fmv_core_match_streak = 0;
g_fmv_lockstep_released = 0;
g_fmv_dense_through = 0;
+31
View File
@@ -0,0 +1,31 @@
#include "netplay_fmv_confirm.h"
#undef NDEBUG
#include <assert.h>
/* Model the gate's counting: the peer's digests trail by `lag` ticks. */
static uint32_t streak_after(uint32_t media_end, uint32_t ticks, uint32_t lag)
{
uint32_t seen = 0, streak = 0, sim;
for (sim = media_end; sim < media_end + ticks; ++sim) {
const uint32_t resolved = sim > lag ? sim - lag : 0;
if (resolved < media_end) continue; /* nothing past the movie yet */
streak += rb_fmv_confirm_gain(seen > media_end ? seen : media_end, resolved);
if (resolved > seen) seen = resolved;
}
return streak;
}
int main(void)
{
assert(rb_fmv_confirm_gain(10, 10) == 0);
assert(rb_fmv_confirm_gain(10, 9) == 0);
assert(rb_fmv_confirm_gain(10, 13) == 3);
/* Same machine (no lag) and cross-machine (2-6 ticks of lag) both reach
* the 16-tick confirmation well inside the 300-tick cap. */
assert(streak_after(1000, 300, 0) >= 16);
assert(streak_after(1000, 300, 2) >= 16);
assert(streak_after(1000, 300, 6) >= 16);
/* Bursty confirmations (a frontier jump) still count every tick once. */
assert(streak_after(1000, 40, 30) == 9);
return 0;
}