diff --git a/docs/ROLLBACK_MOTK_HOOKUP.md b/docs/ROLLBACK_MOTK_HOOKUP.md index 8fef71ea..670a8028 100644 --- a/docs/ROLLBACK_MOTK_HOOKUP.md +++ b/docs/ROLLBACK_MOTK_HOOKUP.md @@ -4978,3 +4978,41 @@ Cachy `GAP1_LEGACY` / `RUNWAY_EMPTY` invents. Soft fork @1480 sat **before** ahead-of-tip seat; fewer `GAP1_LEGACY` / `RUNWAY_EMPTY` before tip+1. Accuracy (§112 arm) still required for matched fin cyc at tip+1. + +## 114. Platform tip+1 apply-only heal + KF stream (2026-08-13) + +**Problem:** matched-baseline tip+1 after post-FMV loading is Win↔Linux +platform nondeterminism (cpu/clk/tim/ram diverge with matched dirty). Verify +span past load always aborted; empty tip KF rematched tip and stormed. + +**What landed (§114):** + +1. Apply-only MEDIA_KF heal (`target=load`) — no tip+1 resim. +2. Heal Live → invent off + `g_post_fmv_platform_nondet` + initiator KF stream + every 16 sim ticks (dense snaps). +3. Heal loop CAP → same keep-live stream instead of hard DESYNC abort. + +**Gap:** stream always rewound to HC's stuck tip+1 fork (`resolved+1`), so +`choose_load` re-pinned keep forever → loading-screen livelock (no DESYNC). + +## 115. Platform KF stream escape / host-tip ride (2026-08-13) + +**Soak (MotK Win↔CachyOS after §114):** `FIRST CORE DIVERGE sim=1112`, heal +pin `1111` / `pc=0x800768e8`, then hundreds of `§114 KF stream begin +fork=1112` + `heal KF CAP →1111` with `invent off until rematch`. No DESYNC; +loading never advanced. HC `peek_mismatch` stays at tip+1 while +`resolved_through=keep`. + +**What landed (§115):** + +1. **Host-tip ride** — when HC fork is the known platform tip+1 and + `sim > fork`, stream `begin_rewind(sim)` so MEDIA-KF pins near host tip. +2. **Accept on advance** — heal Live/tip-hold with `tip > keep` primes HC + past the soft fork, clears invent-hold/stream, sets + `g_platform_accepted_fork` (hc-fork / begin refuse re-heal). +3. **Stream CAP** (`RB_FMV_PLATFORM_KF_STREAM_LIMIT=6`) — same stale fork + without escape → force accept. + +**Re-soak watch:** after first post-FMV tip+1 diverge, logs +`§115 KF stream ride host tip` and/or `§115 platform fork accepted`; sim +advances past keep; no endless `snap applied tick=`; loading completes. diff --git a/runtime/include/psx_netplay_rb.h b/runtime/include/psx_netplay_rb.h index cb9789e9..dd4d6e00 100644 --- a/runtime/include/psx_netplay_rb.h +++ b/runtime/include/psx_netplay_rb.h @@ -114,6 +114,10 @@ int psx_netplay_rb_post_fmv_heal_eligible(void); /* §111: 1 while heal sticky window is live (invent hold may be tip-lead only). */ int psx_netplay_rb_post_fmv_heal_sticky(void); +/* §115: 1 if this tip+1 fork was accepted (HC primed past it) — hc-fork / + * begin must not re-open apply-only heal for it. */ +int psx_netplay_rb_platform_fork_accepted(uint32_t fork_tick); + /* §93: 1 if tick sits in the last FMV media bout / settle tail (or DESYNC * hold). Begin/follow/hc-fork must not open episodes that load there. * §97 MEDIA_KF (default on): returns 0 for media-range — episodes allowed. */ diff --git a/runtime/src/psx_netplay.c b/runtime/src/psx_netplay.c index c0d40f08..8335b7be 100644 --- a/runtime/src/psx_netplay.c +++ b/runtime/src/psx_netplay.c @@ -693,6 +693,9 @@ static void np_try_hc_fork_recovery(uint32_t fork_tick) return; if (g_np.local_slot != 0) return; /* initiator side only */ + /* §115: tip+1 soft fork already accepted — stay Live. */ + if (psx_netplay_rb_platform_fork_accepted(fork_tick)) + return; /* §64: do not bookkeep persist while an episode / tip-hold / load is * in flight. Previously fork_tick advances during tip-extend rereplay * started the persist clock, so tip-extend abandon → Live opened a diff --git a/runtime/src/psx_netplay_rb.c b/runtime/src/psx_netplay_rb.c index 09384f13..57dd73e1 100644 --- a/runtime/src/psx_netplay_rb.c +++ b/runtime/src/psx_netplay_rb.c @@ -216,6 +216,8 @@ static void rb_clear_post_fmv_heal_sticky(const char *why); static void rb_post_fmv_platform_nondet_enter(uint32_t keep_tick, uint32_t fork_tick, uint32_t live_sim, const char *why); static void rb_clear_post_fmv_platform_nondet(const char *why); +static void rb_post_fmv_platform_accept(uint32_t through, uint32_t fork, + const char *why); static int rb_post_fmv_heal_eligible(void); static int rb_fmv_media_active(void); /* §96 — used by ownership budget during FMV */ @@ -452,6 +454,10 @@ static uint32_t g_post_fmv_heal_loop_count; static int g_post_fmv_platform_nondet; static uint32_t g_post_fmv_platform_keep_tick; static uint32_t g_platform_kf_next_sim; +/* §115: KF stream opens while still pinned at keep_tick (livelock counter). */ +static uint32_t g_platform_kf_stream_count; +/* §115: tip+1 fork accepted (HC primed past it) — refuse re-heal/stream. */ +static uint32_t g_platform_accepted_fork; /* §100: recent FMV media snap CRCs — prefer choose_load ticks we already * sealed, and let probe_match confirm without a ring edge-case miss. */ @@ -579,6 +585,9 @@ static uint32_t g_last_begin_mismatch = 0xffffffffu; * MEDIA_KF this often (sim ticks) so both peers ride the host timeline * through loading instead of Live-walking a forked tip+1. */ #define RB_FMV_PLATFORM_KF_IV 16u +/* §115: same keep_tick re-stream limit — beyond this, prime HC past the + * tip+1 fork and clear invent-hold (Win↔Linux loading-screen livelock). */ +#define RB_FMV_PLATFORM_KF_STREAM_LIMIT 6u /* MotK TipHold: quiet window so press→release→D-pad coalesce in one episode. * §27: seal slack uses library default (2) so Live may invent tip+1..tip+2 * during tip-hold — FORCE0 parked presentation at the tip (mini delay-sync). @@ -936,18 +945,24 @@ static void ownership_exit_to_live(uint32_t tip, uint32_t frontier, "psxrecomp: rb ownership Live tip=%u frontier=%u (%s) — no TipHold\n", (unsigned)tip, (unsigned)frontier, why ? why : "?"); fflush(stderr); - /* §109/§114: apply-only heal converged — keep invent OFF (all leads) and - * stream KF through tip+1. §111 sticky alone only holds when lead>0, so - * the tip-starved peer invents into platform nondet (Win↔Linux soak). - * Do not lockstep RELEASE here. */ + /* §109/§114/§115: apply-only heal converged. First pin at the soft-fork + * keep → invent OFF + KF stream. Once a later host-tip KF advances past + * the keep/fork, accept the timeline (§115) instead of re-pinning forever. */ if (g_post_fmv_heal_kf) { uint32_t fork = g_last_begin_mismatch; if (fork == 0xffffffffu) fork = g_post_fmv_heal_fork_tick; g_post_fmv_heal_kf = 0; g_fmv_core_match_streak = 0; /* need fresh CONFIRM after Live walk */ - rb_post_fmv_platform_nondet_enter(tip, fork, tip, - "post-FMV heal KF Live"); + if (g_post_fmv_platform_nondet && + g_post_fmv_platform_keep_tick > 0u && + tip > g_post_fmv_platform_keep_tick) { + rb_post_fmv_platform_accept(tip, fork, + "heal Live past platform keep"); + } else { + rb_post_fmv_platform_nondet_enter(tip, fork, tip, + "post-FMV heal KF Live"); + } } if (g_stash_begin_valid && g_rb && g_stash_begin_epoch == rnet_rb_get_epoch_id(g_rb)) { @@ -1873,6 +1888,44 @@ static void rb_clear_post_fmv_platform_nondet(const char *why) g_post_fmv_platform_nondet = 0; g_post_fmv_platform_keep_tick = 0u; g_platform_kf_next_sim = 0u; + g_platform_kf_stream_count = 0u; +} + +/* §115: accept a tip+1 platform fork — prime HC past it, drop invent-hold / + * KF stream, and refuse re-heal for that fork tick. */ +static void rb_post_fmv_platform_accept(uint32_t through, uint32_t fork, + const char *why) +{ + uint32_t prime = through; + if (fork > prime) + prime = fork; + if (g_b.hc && prime > 0u) + netplay_hc_prime_after(g_b.hc, prime); + if (fork > 0u) + g_platform_accepted_fork = fork; + else if (through > 0u) + g_platform_accepted_fork = through; + g_request_post_fmv_heal = 0; + g_post_fmv_heal_kf = 0; + g_platform_kf_stream_count = 0u; + rb_clear_post_fmv_platform_nondet(why ? why : "§115 platform accept"); + rb_clear_post_fmv_heal_sticky(why ? why : "§115 platform accept"); + if (g_fmv_unmatched_desync) + rb_clear_fmv_desync_hold(why ? why : "§115 platform accept"); + psx_netplay_hc_fork_recovery_clear(); + fprintf(stderr, + "psxrecomp: rb §115 platform fork accepted through=%u fork=%u " + "(%s) — invent unlocked; no re-heal for this tip+1\n", + (unsigned)through, (unsigned)fork, why ? why : "?"); + fflush(stderr); +} + +int psx_netplay_rb_platform_fork_accepted(uint32_t fork_tick) +{ + return (g_platform_accepted_fork != 0u && + fork_tick == g_platform_accepted_fork) + ? 1 + : 0; } /* §109/§97: arm host keyframe for media-range OR requested post-FMV heal. */ @@ -5246,15 +5299,23 @@ static void ownership_on_post_match(uint32_t tip) fflush(stderr); ownership_clear_chain_budget(); /* §109/§114: heal KF reached tip with no confirmed ahead — still - * converged; keep invent off + KF stream (do not RELEASE). */ + * converged; keep invent off + KF stream (do not RELEASE). §115: if the + * pin already advanced past the original keep, accept instead. */ if (g_post_fmv_heal_kf) { uint32_t fork = g_last_begin_mismatch; if (fork == 0xffffffffu) fork = g_post_fmv_heal_fork_tick; g_post_fmv_heal_kf = 0; g_fmv_core_match_streak = 0; - rb_post_fmv_platform_nondet_enter(tip, fork, tip, - "post-FMV heal KF tip-hold"); + if (g_post_fmv_platform_nondet && + g_post_fmv_platform_keep_tick > 0u && + tip > g_post_fmv_platform_keep_tick) { + rb_post_fmv_platform_accept(tip, fork, + "heal tip-hold past platform keep"); + } else { + rb_post_fmv_platform_nondet_enter(tip, fork, tip, + "post-FMV heal KF tip-hold"); + } } /* §61: discard any stashed BEGIN for this episode (rexmit) so Live does * not reopen it. */ @@ -6248,6 +6309,8 @@ void psx_netplay_rb_cold_reset(void) g_post_fmv_platform_nondet = 0; g_post_fmv_platform_keep_tick = 0u; g_platform_kf_next_sim = 0u; + g_platform_kf_stream_count = 0u; + g_platform_accepted_fork = 0u; g_last_good_bb_pc = 0; } @@ -7456,6 +7519,21 @@ int psx_netplay_rb_begin_rewind(uint32_t mismatch_tick, int slot) if (!g_rb || !s) return 0; + /* §115: tip+1 platform fork already accepted — do not re-heal it. */ + if (g_platform_accepted_fork != 0u && + mismatch_tick == g_platform_accepted_fork) { + static uint32_t s_accept_refuse_log; + if (s_accept_refuse_log != mismatch_tick) { + fprintf(stderr, + "psxrecomp: rb begin REFUSED mismatch=%u — §115 platform " + "fork already accepted\n", + (unsigned)mismatch_tick); + fflush(stderr); + s_accept_refuse_log = mismatch_tick; + } + g_request_post_fmv_heal = 0; + return 0; + } /* Prefer tip-extend over refusing while an episode is already open. * Span-cap TipHold commit leaves the session inactive — fall through to * open a fresh episode for this mismatch (symmetric with peer). */ @@ -8563,16 +8641,19 @@ static void begin_follower(uint32_t epoch, uint32_t mismatch, uint32_t load, uin } } -/* §114: initiator re-opens apply-only MEDIA_KF while platform nondet is - * armed and cores still disagree — keeps peers on the host timeline. */ +/* §114/§115: initiator re-opens apply-only MEDIA_KF while platform nondet is + * armed and cores still disagree. Prefer host *current* tip when HC is stuck + * on the known tip+1 fork so the pin advances; cap same-keep streams. */ static void rb_post_fmv_platform_kf_stream_poll(void) { RNetSession *s = sess(); uint32_t sim; uint32_t fork = 0u; + uint32_t stream_mm; int local; int remote; int n; + int stale_platform_fork; if (!g_post_fmv_platform_nondet && !g_fmv_unmatched_desync) return; @@ -8595,6 +8676,48 @@ static void rb_post_fmv_platform_kf_stream_poll(void) } if (fork == 0u) fork = (sim > 0u) ? sim : 1u; + if (g_platform_accepted_fork != 0u && fork == g_platform_accepted_fork) { + g_platform_kf_next_sim = sim + RB_FMV_PLATFORM_KF_IV; + return; + } + + stale_platform_fork = + (g_post_fmv_heal_fork_tick != 0u && fork == g_post_fmv_heal_fork_tick) || + (g_post_fmv_platform_keep_tick != 0u && + fork == g_post_fmv_platform_keep_tick + 1u); + + /* §115: HC stays stuck at tip+1 forever (resolved=keep). Streaming that + * fork only re-applies keep. Ride host sim so choose_load pins near tip. */ + stream_mm = fork; + if (stale_platform_fork && sim > fork) { + stream_mm = sim; + fprintf(stderr, + "psxrecomp: rb §115 KF stream ride host tip fork=%u→%u " + "sim=%u (stale tip+1)\n", + (unsigned)fork, (unsigned)stream_mm, (unsigned)sim); + fflush(stderr); + } + + if (stale_platform_fork) { + g_platform_kf_stream_count++; + if (g_platform_kf_stream_count > RB_FMV_PLATFORM_KF_STREAM_LIMIT) { + uint32_t through = g_post_fmv_platform_keep_tick; + if (sim > through) + through = (sim > 0u) ? (sim - 1u) : fork; + fprintf(stderr, + "psxrecomp: rb §115 KF stream CAP fork=%u count=%u " + "sim=%u — accept platform tip+1\n", + (unsigned)fork, (unsigned)g_platform_kf_stream_count, + (unsigned)sim); + fflush(stderr); + rb_post_fmv_platform_accept(through, fork, + "KF stream limit at stale tip+1"); + return; + } + } else { + g_platform_kf_stream_count = 0u; + } + n = g_b.slot_count ? *g_b.slot_count : 0; remote = (local == 0) ? 1 : 0; if (n < 2) @@ -8603,11 +8726,12 @@ static void rb_post_fmv_platform_kf_stream_poll(void) g_post_fmv_heal_loop_count = 0u; g_platform_kf_next_sim = sim + RB_FMV_PLATFORM_KF_IV; fprintf(stderr, - "psxrecomp: rb §114 KF stream begin fork=%u sim=%u " - "(platform nondet)\n", - (unsigned)fork, (unsigned)sim); + "psxrecomp: rb §114 KF stream begin fork=%u stream_mm=%u sim=%u " + "(platform nondet; stream=%u)\n", + (unsigned)fork, (unsigned)stream_mm, (unsigned)sim, + (unsigned)g_platform_kf_stream_count); fflush(stderr); - (void)psx_netplay_rb_begin_rewind(fork, remote); + (void)psx_netplay_rb_begin_rewind(stream_mm, remote); } void psx_netplay_rb_pump(void)