mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Tell a crash on the RTSS thread as the game's own call of RivaTuner's library, and skip the restore there.
main.c foreign_hook matches "hook", so a crash in RTSSHooks64.dll got the line that the library is not part of the game and that a recording, overlay or capture program put it there, also when the game's own thread that sets the frame limit (rtss_limit.c's "rtss" worker) had loaded and called it. The one RTSS crash on record (issue #231) faulted in ntdll on that thread, where no note applied. And the crash handler then called rtss_limit_restore from that thread, whose own lock it holds (SRW locks are not recursive), so the restore's trylocks spent a second. The platform layer keeps the name a thread was made with in the thread itself (plat_thread_name, copied at its start: a detached thread's struct may go before it ends). main.c crash_module_note picks the note: on the "rtss" thread, whatever the module, the line says the fault is in the game's calls of RTSS's library or around them and that rtss = 0 keeps the game from calling it; on other threads RTSSHooks64.dll stays foreign, as it is RTSS's overlay there. The restore is skipped on the rtss thread. rtss_limit.c is unchanged: the thread is known by the name it is spawned with. Crash text only, besides the skipped restore. Not reproduced before the change. tests/foreign_hook_check.py checks the note's choice (RTSSHooks64.dll and ntdll on the RTSS thread: the RTSS note; RTSSHooks64.dll elsewhere: foreign; a driver: none), and tests/crash_stack_check.py that the crash report reads the faulting thread's name. They, thread_end_check, fatal_exit_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:
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 <ctype.h>\n#include <stdio.h>\n#include <string.h>\n' + function('static int foreign_hook(')
|
||||
enum = source[source.index('enum { CRASH_NOTE_NONE'):]
|
||||
enum = enum[:enum.index('\n') + 1]
|
||||
src = ('#include <ctype.h>\n#include <stdio.h>\n#include <string.h>\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
|
||||
|
||||
Reference in New Issue
Block a user