Write the frame-time summary and the stutter map at a fatal error too, the map only when its lock is free.

dispatch.c fatal_end wrote only the profiler's samples, so a run that ended in a FATAL had no "frametime" or
stutter-map lines for its last period: the numbers that would show what the game was doing before it failed.

perf.c perf_summary_flush_try (weak in dispatch.c for builds without perf.c) runs after fs_save_crash_wait: ft_stop,
then the running period's "frametime: at a fatal error" lines and the stutter map. The failing thread may hold any
lock, so stutter_map.c gains stutter_report_as(tag, try_only), which only tries sm_lock and writes nothing when it is
taken (stutter_report keeps the blocking take); the back end's exit lines (gfx_exit_log) and the trace's segments
(seg_close) are left out, as they read state the failure may have left half made. The crash report keeps no summary:
its state may be damaged.

Not reproduced before the change (a missing diagnostic). tests/fatal_exit_check.py now wants the summary at
rt_fatal and checks statically that perf_summary_flush_try only tries the map's lock and leaves the back end out;
tests/stutter_hitch_check.c gets the trylock stand-in it needs. fatal_exit_check, stutter_hitch_check,
stutter_map_check and the compile checks of every runtime file pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 17:41:19 -05:00
parent 153295b64c
commit c040ca6c5f
8 changed files with 45 additions and 5 deletions

View File

@@ -59,6 +59,10 @@ void rt_fatal_notes(void) {
__attribute__((weak)) void fs_save_crash_wait(void) {} // fs_host.c; builds without it (the unit checks) have no saves
// The frame-time summary and the stutter map at a fatal error (perf.c); builds without perf.c (the unit checks) have
// none.
__attribute__((weak)) void perf_summary_flush_try(void) {}
// HD rumble's neutral value before a failed run ends (input.c input_rumble_hd_stop); builds without input.c (the unit
// checks) have no controller.
__attribute__((weak)) void input_rumble_hd_stop(void) {}
@@ -111,6 +115,7 @@ 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)

View File

@@ -112,6 +112,7 @@ void perf_present(uint64_t frame);
void perf_cpu_start(void); // the process's logical processors (NX2NAVI_CACHE_DIE, NX2NAVI_ONE_PER_CORE), at start
int perf_cpu_on_cache_die(void); // 1 while the process runs on the die with the largest L3 cache alone (perf.c cpu_placement)
void perf_summary_flush(void); // prints the frame-time summary of the running period (exit); the 60 s lines come from perf_present
void perf_summary_flush_try(void); // the same at a fatal error, the stutter map only if its lock is free
void perf_focus_lost(void); // the window lost the focus: the session's reports at the next present, in play
void gfx_exit_log(void); // back end's lines for the exit summary (weak no-op in prof_win.c)
void gfx_window_closed(void); // gfx_present.c (gfx_wsys.h): the end of a closed window; does not return

View File

@@ -775,6 +775,19 @@ void perf_summary_flush(void) { // at exit (prof_write)
summary_t0 = 0;
}
// A fatal error's frame-time summary (dispatch.c fatal_end), on the failing thread, which may hold any lock: the
// running period's "frametime" lines and the stutter map so far, the map only when its lock is free. The back end's
// exit lines (gfx_exit_log) and the trace's segments (seg_close) are left out: they read state the failure may have
// left half made. The present thread's numbers may be caught mid-frame if it still runs past ft_stop's wait.
void perf_summary_flush_try(void) {
ft_stop();
if (summary_t0) {
ft_report(kern_now_ns(), "at a fatal error");
if (!stutter_report_as("at a fatal error", 1)) rt_log("stutter map: its lock is held; not written at the fatal error");
}
summary_t0 = 0;
}
// Returns the replay time since the previous present. add 0: the exit's summary is written (ft_stop), so the frame
// is not added to it.
static uint64_t ft_frame(uint64_t frame, uint64_t now, uint64_t d, int add) {

View File

@@ -427,14 +427,20 @@ typedef struct {
const Pipe *list[SM_PIPES];
} MapCopy;
void stutter_report(const char *tag) {
if (!stutter_map_on) return;
// try_only: sm_lock is only tried (a fatal error's summary, perf.c perf_summary_flush_try, on a thread that may
// hold it); 0 when it was held and nothing was written.
int stutter_report_as(const char *tag, int try_only) {
if (!stutter_map_on) return 1;
MapCopy *m = malloc(sizeof *m);
if (!m) {
rt_log("stutter map (%s): no memory for the report", tag);
return;
return 1;
}
if (!try_only) plat_mutex_lock(&sm_lock);
else if (!plat_mutex_trylock(&sm_lock)) {
free(m);
return 0;
}
plat_mutex_lock(&sm_lock);
int na = n_areas;
memcpy(m->areas, areas, (size_t)na * sizeof *areas);
uint32_t np = 0, lost = pipes_lost;
@@ -502,4 +508,7 @@ void stutter_report(const char *tag) {
p->fresh ? ", a shader translated in play" : "");
}
free(m);
return 1;
}
void stutter_report(const char *tag) { stutter_report_as(tag, 0); }

View File

@@ -28,3 +28,4 @@ void stutter_ready(int mesh, const uint8_t *vi, const uint8_t *fi, double ms, in
void stutter_frame(uint64_t frame, uint64_t now, uint64_t d, const uint64_t *part, const char *const *names, int n,
int cause);
void stutter_report(const char *tag); // the map so far; every 5 minutes of play, at a focus loss in play and at exit
int stutter_report_as(const char *tag, int try_only); // try_only 1: only when its lock is free (0: it was not, nothing written)

View File

@@ -60,6 +60,9 @@ void prof_sampler_write(void) {}
void rtss_limit_restore(void) {}
void kern_checkpoint(void) {}
// perf.c's summary at a fatal error, which dispatch.c calls through a weak symbol
void perf_summary_flush_try(void) { say("frametime: at a fatal error\n"); }
// input.c's stop, which dispatch.c calls through a weak symbol: says it ran, or hangs as when SDL's lock is held
static int rumble_hangs;
void input_rumble_hd_stop(void) {

View File

@@ -78,6 +78,13 @@ def static_checks():
print(f'static: main.c crash_handler starts with the shared guard: {"ok" if ok else "FAIL (" + first + ")"}',
flush=True)
bad += not ok
# the fatal error's summary leaves out what the failure may have left half made, and only tries the map's lock
flush = re.sub(r'//[^\n]*', '', function((RT / 'perf.c').read_text(encoding='utf-8'), 'void perf_summary_flush_try(void)'))
ok = ('stutter_report_as(' in flush and 'stutter_report(' not in flush and 'gfx_exit_log' not in flush
and 'seg_close' not in flush)
print(f'static: perf.c perf_summary_flush_try tries the stutter map\'s lock and leaves the back end out: '
f'{"ok" if ok else "FAIL"}', flush=True)
bad += not ok
# both failed ends stop HD rumble through the bounded stop, after RivaTuner's restore
fatal = re.sub(r'//[^\n]*', '', function((RT / 'dispatch.c').read_text(encoding='utf-8'),
'static void fatal_end(int code, const char *fmt, va_list ap) {'))
@@ -92,7 +99,7 @@ def static_checks():
CASES = [
# mode, exit code, lines that must be there, lines that must not
('fatal', 70, ['FATAL: the test\'s fatal error 1', 'rumble: stopped'], ['detach:']),
('fatal', 70, ['FATAL: the test\'s fatal error 1', 'frametime: at a fatal error', 'rumble: stopped'], ['detach:']),
# the rumble stop hangs (SDL's lock held by a thread that crashed): the end goes on after 300 ms
('fatal_rumble', 70, ['FATAL: the test\'s fatal error 1'], ['rumble: stopped', 'detach:']),
# two faults at once: the first report whole, the second thread waits (dispatch.c rt_ending_enter)

View File

@@ -11,6 +11,7 @@ int cfg_bool(const char *name) { return !strcmp(name, "NX2NAVI_STUTTER_MAP"); }
uint64_t kern_now_ns(void) { return clock_ns; }
void plat_mutex_lock(PlatMutex *m) { (void)m; }
void plat_mutex_unlock(PlatMutex *m) { (void)m; }
int plat_mutex_trylock(PlatMutex *m) { (void)m; return 1; }
static int logged;
static char head[1400]; // the last report's first line
void rt_log(const char *fmt, ...) {