diff --git a/runtime/include/freeze_heartbeat.h b/runtime/include/freeze_heartbeat.h index e8586982..1d2314c6 100644 --- a/runtime/include/freeze_heartbeat.h +++ b/runtime/include/freeze_heartbeat.h @@ -25,6 +25,12 @@ extern "C" { * binary produced the file. */ void freeze_heartbeat_start(const char *backend_label); +/* Suppress automatic wedge classification while the host intentionally parks + * guest execution, such as save-state/rewind overlays or netplay admit waits. + * Heartbeat JSON still updates; the detector window is reset while paused so + * the first post-resume window is built only from live guest samples. */ +void freeze_heartbeat_set_paused(int paused); + #ifdef __cplusplus } #endif diff --git a/runtime/src/freeze_heartbeat.c b/runtime/src/freeze_heartbeat.c index 3df0fa5f..46d82b9d 100644 --- a/runtime/src/freeze_heartbeat.c +++ b/runtime/src/freeze_heartbeat.c @@ -169,6 +169,11 @@ static uint32_t s_ring_count = 0; * The heartbeat JSON reports both written and suppressed event counts. */ static FreezeDumpPolicy s_dump_policy = {0}; static uint32_t s_last_wedge_kind = 0; /* informational, last detected kind */ +static volatile int s_wedge_classification_paused = 0; + +void freeze_heartbeat_set_paused(int paused) { + s_wedge_classification_paused = paused ? 1 : 0; +} #ifdef _WIN32 /* Capture the main thread's call stack at the moment of a hard freeze. @@ -774,7 +779,10 @@ static void heartbeat_write(void) { * a sustained healthy interval before it accepts a new event, and bounds * automatic full dumps for the process. Deliberate fatal dumps bypass it. */ uint32_t wedge_kind = 0; /* 0=healthy 1=hard 2=reentry storm 3=slow frames */ - if (s_ring_count >= WEDGE_WINDOW_TICKS) { + if (s_wedge_classification_paused) { + s_ring_count = 0; + s_last_wedge_kind = 0; + } else if (s_ring_count >= WEDGE_WINDOW_TICKS) { /* The just-pushed tick is at (s_ring_head - 1). The oldest in * our window is WEDGE_WINDOW_TICKS - 1 ticks before it. */ uint32_t newest_idx = (s_ring_head + RING_CAP - 1u) % RING_CAP; @@ -810,7 +818,8 @@ static void heartbeat_write(void) { wedge_kind = 5; /* spin freeze: game wedged while frames advance */ } - if (freeze_dump_policy_observe(&s_dump_policy, wedge_kind, + if (!s_wedge_classification_paused && + freeze_dump_policy_observe(&s_dump_policy, wedge_kind, g_psx_fatal_reason != NULL)) { s_last_wedge_kind = wedge_kind; int written = freeze_dump_write( diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index d7044423..cbf4c830 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -5375,6 +5375,7 @@ static void netplay_barrier_admit(int override) { s_np_timing_frames++; } int liveness_rearamed = 0; + freeze_heartbeat_set_paused(1); for (;;) { uint32_t dt = 0, lh = 0, rh = 0; const Uint64 now_ms = SDL_GetTicks64(); @@ -5400,13 +5401,13 @@ static void netplay_barrier_admit(int override) { ? 0u : psx_netplay_running_liveness_timeout_ms())) { netplay_soft_exit("netplay_peer_disconnect"); - if (psx_return_to_lobby_requested()) return; + if (psx_return_to_lobby_requested()) goto done; } /* Staged .pst rejected (stale codegen / BIOS / missing) — do not wait * out the 90s load barrier with stall=load_apply_done. */ if (psx_netplay_consume_load_apply_failed()) { netplay_soft_exit("netplay_load_failed"); - if (psx_return_to_lobby_requested()) return; + if (psx_return_to_lobby_requested()) goto done; } /* Mutual INPUT/CONFIRM stall still refreshes last_peer_rx — detect * "no sim progress" separately (common rematch + TURN loss mode). @@ -5424,7 +5425,7 @@ static void netplay_barrier_admit(int override) { "stall=%s lead=%d — returning to lobby\n", (unsigned)sim, stall[0] ? stall : "?", lead); netplay_soft_exit("netplay_load_stall"); - if (psx_return_to_lobby_requested()) return; + if (psx_return_to_lobby_requested()) goto done; } else if (!psx_netplay_in_load_barrier() && !running && now_ms - barrier_t0 >= 90000u) { char stall[64]; @@ -5436,7 +5437,7 @@ static void netplay_barrier_admit(int override) { "stall=%s lead=%d — returning to lobby\n", (unsigned)sim, stall[0] ? stall : "?", lead); netplay_soft_exit("netplay_link_stall"); - if (psx_return_to_lobby_requested()) return; + if (psx_return_to_lobby_requested()) goto done; } else if (!psx_netplay_in_load_barrier() && running && progress_t0 != 0 && now_ms - progress_t0 >= 20000u) { char stall[64]; @@ -5448,7 +5449,7 @@ static void netplay_barrier_admit(int override) { "stall=%s lead=%d — returning to lobby\n", (unsigned)sim, stall[0] ? stall : "?", lead); netplay_soft_exit("netplay_admit_stall"); - if (psx_return_to_lobby_requested()) return; + if (psx_return_to_lobby_requested()) goto done; } else if (now_ms - last_stall_log_ms >= 2000u) { char stall[96]; uint32_t sim = 0; @@ -5514,12 +5515,14 @@ static void netplay_barrier_admit(int override) { if (t1 >= admit_t0) s_np_admit_ticks += t1 - admit_t0; s_np_last_admit_end = t1; } - return; + goto done; } /* Episode snap may have been applied during pump/try_admit without * longjmp — flush here (no present-body C++ RAII) before spinning. * try_admit refuses to arm needs_advance while resume is pending. */ + freeze_heartbeat_set_paused(0); psx_netplay_rb_flush_resume(); + freeze_heartbeat_set_paused(1); #ifndef PSX_NO_DEBUG_TOOLS debug_server_poll(); #endif @@ -5528,7 +5531,7 @@ static void netplay_barrier_admit(int override) { while (SDL_PollEvent(&ev)) { if (ev.type == SDL_QUIT) { netplay_soft_exit("sdl_window_close"); - if (psx_return_to_lobby_requested()) return; + if (psx_return_to_lobby_requested()) goto done; } if (ev.type == SDL_KEYDOWN) { #if defined(PSX_SDL3) @@ -5538,7 +5541,7 @@ static void netplay_barrier_admit(int override) { #endif if (key == SDLK_ESCAPE) { netplay_soft_exit("netplay_barrier_escape"); - if (psx_return_to_lobby_requested()) return; + if (psx_return_to_lobby_requested()) goto done; } } if (ev.type == SDL_CONTROLLERDEVICEADDED || @@ -5551,7 +5554,7 @@ static void netplay_barrier_admit(int override) { if (!psx_start_bisect_no_gc_update_in_admit()) SDL_GameControllerUpdate(); } - if (psx_return_to_lobby_requested()) return; + if (psx_return_to_lobby_requested()) goto done; /* §35: TipHold invent-cap stall freezes guest (no vblank present). * Keep Swap alive on the last Live frame so SAFETY/held waits do not * open a ~250ms present gap. @@ -5569,6 +5572,8 @@ static void netplay_barrier_admit(int override) { /* Admit barriers (save/load sync) can last seconds without guest cycles. */ starvation_watchdog_heartbeat(); } +done: + freeze_heartbeat_set_paused(0); } static void sample_pad_into_sio(int override) { @@ -6326,6 +6331,7 @@ static void rewind_pause_present(void) { /* Freeze guest in vblank present while the rewind filmstrip is open. */ static void rewind_host_pause_loop(void) { + freeze_heartbeat_set_paused(1); while (psx_rewind_is_open()) { SDL_Event ev; while (SDL_PollEvent(&ev)) { @@ -6362,12 +6368,14 @@ static void rewind_host_pause_loop(void) { starvation_watchdog_heartbeat(); SDL_Delay(8); } + freeze_heartbeat_set_paused(0); /* Swallow the still-held close press so it doesn't bleed into the game. */ savestate_input_guard_arm(); } /* Freeze guest in vblank present while the save-state slot menu is open. */ static void savestate_menu_host_pause_loop(void) { + freeze_heartbeat_set_paused(1); while (savestate_menu_open) { SDL_Event ev; while (SDL_PollEvent(&ev)) { @@ -6408,6 +6416,7 @@ static void savestate_menu_host_pause_loop(void) { starvation_watchdog_heartbeat(); SDL_Delay(8); } + freeze_heartbeat_set_paused(0); /* Swallow the close press; a just-queued save must not snapshot it. */ savestate_input_guard_arm(); }