mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Stop HD rumble at a fatal error and a crash too, on a thread of its own the end waits for 300 ms at most.
input_rumble_hd_stop ran only in the orderly ends, so a FATAL or a crash while a controller vibrated on HD rumble left it buzzing: nothing renews or stops the value after the process is gone (input.c says so). It also took hd_lock with a blocking lock while it only tried the other two. dispatch.c rt_rumble_stop_bounded runs input_rumble_hd_stop (weak in dispatch.c, as fs_save_crash_wait is, for builds without input.c) on a new "rumble stop" thread and waits at most 300 ms for it, never joining it: the stop goes through SDL, which takes its own joystick lock, and a crash on SDL's thread while it held that lock would otherwise keep the ending thread there and the process would never end. fatal_end and main.c crash_handler call it right after rtss_limit_restore. input.c tries hd_lock 50 times 1 ms apart, as it does `lock` and sdl_lock, and skips the stop when it is not taken. Not reproduced before the change. tests/fatal_exit_check.py (now linking dispatch.c as its own unit: the stop runs at a fatal error; a stop that hangs leaves the end at code 70 after 0.3 s; statically both failed ends call the bounded stop after the RTSS restore), tests/rumble_hd_check.py and the compile checks pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,25 @@ 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
|
||||
|
||||
// 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) {}
|
||||
|
||||
// The stop goes through SDL, which takes its own joystick lock: a crash on SDL's thread while it held that lock would
|
||||
// hold the ending thread there for good, and the process would never end. So it runs on a thread of its own, and the
|
||||
// ending thread waits for it at most 300 ms and never joins it. A controller left buzzing is the lesser harm.
|
||||
static volatile int32_t rumble_stopped;
|
||||
static void rumble_stop_main(void *arg) {
|
||||
(void)arg;
|
||||
input_rumble_hd_stop();
|
||||
__atomic_store_n(&rumble_stopped, 1, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
void rt_rumble_stop_bounded(void) {
|
||||
if (!plat_thread_spawn(rumble_stop_main, NULL, "rumble stop", 0)) return;
|
||||
for (int i = 0; i < 30 && !__atomic_load_n(&rumble_stopped, __ATOMIC_ACQUIRE); i++) plat_sleep_ms(10);
|
||||
}
|
||||
|
||||
// One end for a failed run. The platform layer calls the crash handler on every thread that faults, without
|
||||
// waiting for the others (platform/win32.c crash_veh, crash_filter), and a fatal error can come beside a crash from
|
||||
// the same bad state (an allocation that failed here, a null pointer read there). The later end cut the first report
|
||||
@@ -96,6 +115,7 @@ static void fatal_end(int code, const char *fmt, va_list ap) {
|
||||
// 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
|
||||
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
|
||||
|
||||
@@ -66,3 +66,5 @@ void rt_fatal_notes(void);
|
||||
// for the first caller, which reports and ends the process; 0 for that thread again (a failure inside its own report),
|
||||
// which ends at once with its code; any other thread waits, logging nothing, until the process ends (30 s at most).
|
||||
int rt_ending_enter(void);
|
||||
// HD rumble stopped before a failed run ends, on a thread of its own that the caller waits for 300 ms at most.
|
||||
void rt_rumble_stop_bounded(void);
|
||||
|
||||
@@ -717,7 +717,12 @@ void input_rumble_hd_stop(void) {
|
||||
// The locks are tried for a while and not waited for: a thread of the game may hold one for good by now.
|
||||
for (int i = 0; i < 50 && !(got = plat_mutex_trylock(&lock)); i++) plat_wait_ns(1000000);
|
||||
if (!got) return;
|
||||
plat_mutex_lock(&hd_lock);
|
||||
got = 0;
|
||||
for (int i = 0; i < 50 && !(got = plat_mutex_trylock(&hd_lock)); i++) plat_wait_ns(1000000);
|
||||
if (!got) { // a fatal error or a crash on the thread that writes the reports (dispatch.c rt_rumble_stop_bounded)
|
||||
plat_mutex_unlock(&lock);
|
||||
return;
|
||||
}
|
||||
hd_ended = 1;
|
||||
int buzzing = rumble_hd_buzzing(&hd);
|
||||
rumble_hd_forget(&hd);
|
||||
|
||||
@@ -373,6 +373,7 @@ static void crash_handler(const PlatCrash *x) {
|
||||
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
|
||||
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)
|
||||
plat_exit(71);
|
||||
|
||||
@@ -7,14 +7,16 @@
|
||||
// fatal_exit_check crash_crash two threads fault at once: one whole crash report, exit code 71
|
||||
// fatal_exit_check fatal_crash a fault on another thread while rt_fatal reports: its FATAL line alone, exit code 70
|
||||
// fatal_exit_check crash_fatal rt_fatal on another thread while a crash is reported: the whole report, exit code 71
|
||||
// fatal_exit_check fatal_rumble rt_fatal while the HD rumble stop hangs (SDL's lock held): exit code 70 all the same
|
||||
//
|
||||
// The crash handler here is a stand-in for main.c crash_handler with its first line (the static check in
|
||||
// tests/fatal_exit_check.py holds main.c to it): it writes three report lines 100 ms apart and ends with plat_exit(71).
|
||||
#include "dispatch.c"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include "host.h"
|
||||
#include "platform/platform.h"
|
||||
|
||||
static void say(const char *s) {
|
||||
DWORD w;
|
||||
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE), s, (DWORD)strlen(s), &w, NULL);
|
||||
@@ -58,6 +60,13 @@ void prof_sampler_write(void) {}
|
||||
void rtss_limit_restore(void) {}
|
||||
void kern_checkpoint(void) {}
|
||||
|
||||
// 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) {
|
||||
if (rumble_hangs) Sleep(INFINITE);
|
||||
say("rumble: stopped\n");
|
||||
}
|
||||
|
||||
// ---- the crash report and the threads that fail beside it
|
||||
static void crash(const PlatCrash *x) {
|
||||
if (!rt_ending_enter()) plat_exit(71); // main.c crash_handler's first line
|
||||
@@ -93,6 +102,10 @@ int main(int argc, char **argv) {
|
||||
tls_var = 1;
|
||||
plat_crash_hook(crash);
|
||||
if (!strcmp(mode, "fatal")) rt_fatal("the test's fatal error %d", 1);
|
||||
if (!strcmp(mode, "fatal_rumble")) {
|
||||
rumble_hangs = 1;
|
||||
rt_fatal("the test's fatal error %d", 1);
|
||||
}
|
||||
if (!strcmp(mode, "crash_crash")) {
|
||||
CloseHandle(CreateThread(NULL, 0, fault, (void *)0, 0, NULL));
|
||||
CloseHandle(CreateThread(NULL, 0, fault, (void *)0, 0, NULL));
|
||||
@@ -113,6 +126,6 @@ int main(int argc, char **argv) {
|
||||
fault(0);
|
||||
return 3;
|
||||
}
|
||||
printf("usage: fatal_exit_check fatal|crash_crash|fatal_crash|crash_fatal\n");
|
||||
printf("usage: fatal_exit_check fatal|crash_crash|fatal_crash|crash_fatal|fatal_rumble\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,9 @@ ExitProcess makes (a driver's detach that faulted there turned the end into a cr
|
||||
checks statically that no runtime file outside the platform layer ends the process with _Exit, and that the fatal
|
||||
error's end and the lost device's (runtime/vk_device.c lost_wait_exit) end with plat_terminate. No game files, no GPU.
|
||||
|
||||
A fatal error stops HD rumble on a thread of its own and waits for it 300 ms at most (runtime/dispatch.c
|
||||
rt_rumble_stop_bounded), so a stop that hangs on SDL's lock does not hold the end.
|
||||
|
||||
Two failures at once end once: two threads that fault, a fault while a fatal error is reported and a fatal error
|
||||
while a crash is: the first report is written whole, the process ends with its code, and the second adds no line
|
||||
(runtime/dispatch.c rt_ending_enter, which runtime/main.c crash_handler calls first).
|
||||
@@ -15,6 +18,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
@@ -73,12 +77,24 @@ def static_checks():
|
||||
ok = first == 'if (!rt_ending_enter()) plat_exit(71);'
|
||||
print(f'static: main.c crash_handler starts with the shared guard: {"ok" if ok else "FAIL (" + first + ")"}',
|
||||
flush=True)
|
||||
return bad + (not ok)
|
||||
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) {'))
|
||||
for name, code in (('dispatch.c fatal_end', fatal), ('main.c crash_handler', body)):
|
||||
r, s = code.find('rtss_limit_restore('), code.find('rt_rumble_stop_bounded(')
|
||||
ok = 0 <= r < s and 'input_rumble_hd_stop(' not in code
|
||||
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
|
||||
return bad
|
||||
|
||||
|
||||
CASES = [
|
||||
# mode, exit code, lines that must be there, lines that must not
|
||||
('fatal', 70, ['FATAL: the test\'s fatal error 1'], ['detach:']),
|
||||
('fatal', 70, ['FATAL: the test\'s fatal error 1', '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)
|
||||
('crash_crash', 71, ['report line 1', 'report line 2', 'report line 3'], ['detach:', 'FATAL:']),
|
||||
# a fault while rt_fatal reports: its FATAL line and notes whole, no crash report, rt_fatal's code
|
||||
@@ -104,15 +120,17 @@ def main():
|
||||
libs = ['-lbcrypt', '-lws2_32', '-lpsapi', '-lwinmm', '-ldbghelp', '-lshell32', '-lole32', '-luser32', '-ladvapi32']
|
||||
subprocess.check_call([compiler(), '-O1', '-g', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing',
|
||||
'-march=x86-64-v3', '-I', str(RT), '-I', str(work), str(ROOT / 'tests' / 'fatal_exit_check.c'),
|
||||
*map(str, plat), '-o', str(exe), '-lm', *libs], env=env)
|
||||
str(RT / 'dispatch.c'), *map(str, plat), '-o', str(exe), '-lm', *libs], env=env)
|
||||
for mode, code, want, unwanted in CASES:
|
||||
t0 = time.monotonic()
|
||||
p = subprocess.run([str(exe), mode], env=env, capture_output=True, text=True, errors='replace', timeout=120)
|
||||
took = time.monotonic() - t0
|
||||
out = p.stdout + p.stderr
|
||||
missing = [w for w in want if w not in out]
|
||||
extra = [u for u in unwanted if u in out]
|
||||
fatal_lines = out.count('FATAL:')
|
||||
ok = p.returncode == code and not missing and not extra
|
||||
print(f'{mode}: exit code {p.returncode} (want {code}), {fatal_lines} FATAL line(s)'
|
||||
ok = p.returncode == code and not missing and not extra and took < 20
|
||||
print(f'{mode}: exit code {p.returncode} (want {code}) after {took:.1f} s, {fatal_lines} FATAL line(s)'
|
||||
+ (f', missing {missing}' if missing else '') + (f', unexpected {extra}' if extra else '')
|
||||
+ ('' if ok else ': FAIL'), flush=True)
|
||||
if not ok:
|
||||
|
||||
Reference in New Issue
Block a user