diff --git a/runtime/main.c b/runtime/main.c index 6b351a68..b513a1db 100644 --- a/runtime/main.c +++ b/runtime/main.c @@ -331,6 +331,15 @@ static int foreign_hook(const char *module) { strstr(low, "reshade") || strstr(low, "obs-vulkan") || strstr(low, "nvspcap"); } +// What the crash report says of the faulting module. A fault on the thread that hands the frame limit to RivaTuner +// Statistics Server (rtss_limit.c, the "rtss" thread) is in the game's own calls of RTSS's library or around them, +// whatever module it is in (the one such crash faulted in ntdll): the note names that and the setting that keeps the +// game from calling it. On other threads RTSS's library is its overlay, which RTSS put there: foreign_hook's note. +enum { CRASH_NOTE_NONE, CRASH_NOTE_FOREIGN, CRASH_NOTE_RTSS }; +static int crash_module_note(const char *module, int on_rtss_thread) { + return on_rtss_thread ? CRASH_NOTE_RTSS : foreign_hook(module) ? CRASH_NOTE_FOREIGN : CRASH_NOTE_NONE; +} + // Set when main_run has returned: the mode's work is done and saved, and all that still runs is the C runtime's exit // and, inside ExitProcess, the detach call of every library in the process, the graphics driver's and those other // programs put there. A fault in one of them is not a failure of the run. On one PC an injector's Vulkan layer faulted @@ -363,9 +372,16 @@ static void crash_handler(const PlatCrash *x) { rt_logf(", %s address %#llx", x->access == 1 ? "writing" : x->access == 8 ? "executing" : "reading", (unsigned long long)x->addr); rt_log_raw("\n", 1); - if (foreign_hook(x->module)) + const char *thread = plat_thread_name(); + int on_rtss = thread && !strcmp(thread, "rtss"); + int module_note = crash_module_note(x->module, on_rtss); + if (module_note == CRASH_NOTE_FOREIGN) rt_logf(" %s is not part of the game: a recording, overlay or capture program put it into the game. Close that " "program or turn off its Vulkan capture, then start the game again\n", x->module); + else if (module_note == CRASH_NOTE_RTSS) + rt_logf(" the fault is on the thread that hands the frame limit to RivaTuner Statistics Server (rtss_limit.c), " + "in the game's calls of its library or around them; rtss = 0 in nx2navi.ini keeps the game from calling " + "it\n"); // what the thread was doing (a pipeline build names its shaders): the stderr note crash_veh writes misses the log const char *note = plat_crash_context_get(); if (note) rt_logf(" the thread was %s\n", note); @@ -399,7 +415,9 @@ static void crash_handler(const PlatCrash *x) { rt_fatal_notes(); // the card, its driver and the memory settings in effect (vk_device.c fatal_note) kern_free_cores_note(); fs_save_crash_wait(); // a save the game already reported as done finishes first (bounded; fs_host.c) - rtss_limit_restore(); // after the report: RivaTuner Statistics Server's profile as it was + // after the report: RivaTuner Statistics Server's profile as it was; not from its own thread, which holds the lock + // the restore would try for a second + if (!on_rtss) rtss_limit_restore(); rt_rumble_stop_bounded(); // a controller on HD rumble stops (dispatch.c: 300 ms at most, SDL may be the crash) fflush(stderr); prof_sampler_write(); // NX2NAVI_PROFILE: what was sampled until the crash (it gives up after 2 s if the sampler hangs) diff --git a/runtime/platform/linux.c b/runtime/platform/linux.c index ec6b9915..c0355e29 100644 --- a/runtime/platform/linux.c +++ b/runtime/platform/linux.c @@ -109,8 +109,12 @@ static void alt_stack(void) { static __thread cpu_set_t cpu_rest; static __thread int cpu_rest_set; +static __thread char thread_name[16]; // plat_thread_name: copied at the start, as a detached thread's struct may go +const char *plat_thread_name(void) { return thread_name[0] ? thread_name : NULL; } + static void *thread_main(void *p) { PlatThread *t = p; + memcpy(thread_name, t->name, sizeof thread_name); alt_stack(); __atomic_store_n(&t->tid, (int32_t)syscall(SYS_gettid), __ATOMIC_RELEASE); if (t->name[0]) pthread_setname_np(pthread_self(), t->name); diff --git a/runtime/platform/platform.h b/runtime/platform/platform.h index b2355c9d..4199b0ef 100644 --- a/runtime/platform/platform.h +++ b/runtime/platform/platform.h @@ -22,6 +22,9 @@ typedef void (*PlatThreadFn)(void *arg); #define PLAT_STACK_GUARANTEE 32768 void plat_stack_guarantee(void); PlatThread *plat_thread_new(PlatThreadFn fn, void *arg, const char *name); +// The name the calling thread was made with here (plat_thread_new, plat_thread_spawn), NULL for a thread made +// elsewhere; kept in the thread itself, so the crash report can read it with any lock held. +const char *plat_thread_name(void); // The same with the stack size given: stack bytes reserved for the thread (0: PLAT_STACK_DEFAULT). The default is the // same on every OS, so a thread that fits on Windows fits elsewhere. #define PLAT_STACK_DEFAULT (16u << 20) diff --git a/runtime/platform/win32.c b/runtime/platform/win32.c index e786d24a..92336626 100644 --- a/runtime/platform/win32.c +++ b/runtime/platform/win32.c @@ -34,8 +34,12 @@ struct PlatThread { PlatThreadFn fn; void *arg; volatile int32_t state; // 0 running and owned by its maker, 1 detached, 2 its function has returned + char name[32]; }; +static __thread char thread_name[32]; // plat_thread_name: copied at the start, as a detached thread's struct may go +const char *plat_thread_name(void) { return thread_name[0] ? thread_name : NULL; } + // What a thread made here keeps for itself is given back when its function returns: the handles below (the sleep // timer of plat_wait_ns, the events of plat_file_pread and plat_file_pwrite), what the other files keep per thread // (plat_thread_end_hook) and, for a detached thread, its PlatThread. The game makes and ends threads all through @@ -58,6 +62,7 @@ void plat_stack_guarantee(void) { static DWORD WINAPI thread_main(LPVOID p) { PlatThread *t = p; plat_stack_guarantee(); + memcpy(thread_name, t->name, sizeof thread_name); t->fn(t->arg); plat_thread_end_run(); thread_handles_close(); @@ -74,6 +79,7 @@ PlatThread *plat_thread_new_ex(PlatThreadFn fn, void *arg, const char *name, siz if (!t) return NULL; t->fn = fn; t->arg = arg; + if (name) snprintf(t->name, sizeof t->name, "%s", name); t->h = CreateThread(NULL, stack ? stack : PLAT_STACK_DEFAULT, thread_main, t, CREATE_SUSPENDED | STACK_SIZE_PARAM_IS_A_RESERVATION, NULL); if (!t->h) { free(t); return NULL; } diff --git a/tests/crash_stack_check.c b/tests/crash_stack_check.c index 814f0dd7..8b6b2243 100644 --- a/tests/crash_stack_check.c +++ b/tests/crash_stack_check.c @@ -34,7 +34,8 @@ static void crash(const PlatCrash *x) { // main.c crash_handler's first call while the boot builds pipelines: vk_pipeline_crash_note writes a file static const char text[] = "the process faulted while the boot built pipelines\n"; int wrote = plat_file_write(file, text, sizeof text - 1); - log2k("report line 1: %s (%#x), the file written: %d\n", x->what, x->code, wrote); + log2k("report line 1: %s (%#x) on thread %s, the file written: %d\n", x->what, x->code, + plat_thread_name() ? plat_thread_name() : "(unnamed)", wrote); log2k("report line 2: %d host frames\n", x->nframes); void *f[62]; USHORT n = RtlCaptureStackBackTrace(0, 62, f, NULL); diff --git a/tests/crash_stack_check.py b/tests/crash_stack_check.py index 27268192..02dbe7b0 100644 --- a/tests/crash_stack_check.py +++ b/tests/crash_stack_check.py @@ -54,7 +54,10 @@ def main(): timeout=60) lines = [f'report line {i}' for i in (1, 2, 3)] missing = [x for x in lines if x not in p.stdout] - ok = p.returncode == 71 and not missing and 'stack overflow' in p.stdout and 'the file written: 1' in p.stdout + # platform.h plat_thread_name, which the crash report reads: the spawned thread's name; the fiber runs on the + # main thread, which the platform layer did not make + named = ('on thread deep' if mode == 'thread' else 'on thread (unnamed)') in p.stdout + ok = p.returncode == 71 and not missing and 'stack overflow' in p.stdout and 'the file written: 1' in p.stdout and named print(f'a stack overflow on a {mode}: exit code {p.returncode & 0xffffffff:#x} (want 0x47), ' f'{3 - len(missing)} of 3 report lines: {"ok" if ok else "FAIL"}', flush=True) if not ok: diff --git a/tests/foreign_hook_check.py b/tests/foreign_hook_check.py index 097dcc99..d8b7abc4 100644 --- a/tests/foreign_hook_check.py +++ b/tests/foreign_hook_check.py @@ -1,7 +1,8 @@ """The crash report's test for a module another program put into the game (runtime/main.c foreign_hook): the Vulkan layers, hooks and overlays of recorders, overlays and capture tools are named as theirs; the game's own libraries and -the drivers are not, nor the validation layer the game loads itself (NX2NAVI_VK_VALIDATION). Builds foreign_hook, cut -out of main.c, into a test program. No game files. +the drivers are not, nor the validation layer the game loads itself (NX2NAVI_VK_VALIDATION). A fault on the thread that +sets RivaTuner Statistics Server's limit gets the RTSS note whatever the module (main.c crash_module_note). Builds those +functions, cut out of main.c, into a test program. No game files. Usage: python tests/foreign_hook_check.py [path to clang]""" import os @@ -43,8 +44,13 @@ def function(signature): def main(): names = FOREIGN + OWN - src = ('#include \n#include \n#include \n' + function('static int foreign_hook(') + enum = source[source.index('enum { CRASH_NOTE_NONE'):] + enum = enum[:enum.index('\n') + 1] + src = ('#include \n#include \n#include \n' + function('static int foreign_hook(') + '\n' + + enum + function('static int crash_module_note(') + '\nint main(int argc, char **argv) {\n for (int i = 1; i < argc; i++) printf("%d\\n", foreign_hook(argv[i]));\n' + ' printf("%d %d %d %d\\n", crash_module_note("RTSSHooks64.dll", 1), crash_module_note("ntdll.dll", 1),\n' + ' crash_module_note("RTSSHooks64.dll", 0), crash_module_note("amdvlk64.dll", 0));\n' ' return 0;\n}\n') work = ROOT / 'build' / 'test' / 'foreign_hook' work.mkdir(parents=True, exist_ok=True) @@ -62,7 +68,12 @@ def main(): bad += 1 print(f'{name or "(outside every module)"}: {"another program" if g == "1" else "not foreign"}' f'{"" if g == want else " FAIL"}') - if len(got) != len(names): + notes = got[len(names):] + ok = notes == ['2', '2', '1', '0'] + print(f'crash note: on the RTSS thread (RTSSHooks64.dll, ntdll.dll) the RTSS note, elsewhere RTSSHooks64.dll foreign, ' + f'a driver none: {"ok" if ok else "FAIL " + str(notes)}') + bad += not ok + if len(got) != len(names) + 4: bad += 1 print(f'foreign_hook_check: {"FAILED" if bad else "ok"}') return 1 if bad else 0