From 78e9d23211f6723f29fd25140fae470fbba19d43 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 18:27:13 -0500 Subject: [PATCH] Write a fatal error's frame-time summary after RivaTuner's restore and the rumble stop, not before them. The review of the fatal summary change found that dispatch.c fatal_end called perf_summary_flush_try (ft_stop waits up to 50 ms, then the summary and the stutter map are formatted) before rtss_limit_restore and rt_rumble_stop_bounded. The lane's order for the failed ends, and A-6's for the orderly ones (gfx_present.c rt_exit_steps), puts what a kill of the process must not lose first: RTSS's profile and the controller's rumble. perf_summary_flush_try now runs after the restore and the bounded rumble stop, just before the stdio flushes and plat_terminate. The summary's lines still reach the log: rt_log_sync ran at the start of fatal_end, so every later line is written as it is made. Not reproduced before the change (an order of steps). tests/fatal_exit_check.py checks statically that fatal_end calls perf_summary_flush_try after rt_rumble_stop_bounded; the check fails on the code before this change and passes after it, and the program's cases (fatal, fatal_rumble, crash_crash, fatal_crash, crash_fatal) pass. The compile check of dispatch.c passes with no -Wall warnings. Co-Authored-By: Claude Opus 5.5 --- runtime/dispatch.c | 4 +++- tests/fatal_exit_check.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/runtime/dispatch.c b/runtime/dispatch.c index 0258de91..8b5d4112 100644 --- a/runtime/dispatch.c +++ b/runtime/dispatch.c @@ -116,12 +116,14 @@ static void fatal_end(int code, const char *fmt, va_list ap) { rt_fatal_notes(); kern_free_cores_note(); fs_save_crash_wait(); // a save the game already reported as done finishes first (bounded) - perf_summary_flush_try(); // "frametime: at a fatal error" and the stutter map: the session's last period prof_sampler_write(); // NX2NAVI_PROFILE: what was sampled until now, else a crash loses the whole profile // RivaTuner Statistics Server's profile as it was: left with the game's limit in it, the next run would take that // limit for the player's own (rtss_limit.c; it gives up after a second if the failing thread holds its lock) rtss_limit_restore(); rt_rumble_stop_bounded(); // a controller on HD rumble: nothing renews or stops its vibration after the end + // "frametime: at a fatal error" and the stutter map, the session's last period: after what a kill of the process + // must not lose, as in the orderly end (gfx_present.c rt_exit_steps); ft_stop waits up to 50 ms + perf_summary_flush_try(); fflush(stdout); fflush(stderr); // Not _Exit: in the C runtime it is ExitProcess, which stops the other threads wherever they are and then runs every diff --git a/tests/fatal_exit_check.py b/tests/fatal_exit_check.py index 19e62205..4622fdec 100644 --- a/tests/fatal_exit_check.py +++ b/tests/fatal_exit_check.py @@ -94,6 +94,12 @@ def static_checks(): print(f'static: {name} stops HD rumble through rt_rumble_stop_bounded after the RTSS restore: ' f'{"ok" if ok else "FAIL"}', flush=True) bad += not ok + # the fatal error's frame-time summary comes after the RTSS restore and the rumble stop, as in the orderly end + s, p = fatal.find('rt_rumble_stop_bounded('), fatal.find('perf_summary_flush_try(') + ok = 0 <= s < p + print(f'static: dispatch.c fatal_end writes the frame-time summary after the RTSS restore and the rumble stop: ' + f'{"ok" if ok else "FAIL"}', flush=True) + bad += not ok return bad