diff --git a/runtime/dispatch.c b/runtime/dispatch.c index dd925278..a9d6e0df 100644 --- a/runtime/dispatch.c +++ b/runtime/dispatch.c @@ -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 diff --git a/runtime/dispatch.h b/runtime/dispatch.h index 7a707505..5ecb436e 100644 --- a/runtime/dispatch.h +++ b/runtime/dispatch.h @@ -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); diff --git a/runtime/input.c b/runtime/input.c index 7839b8c8..71de1758 100644 --- a/runtime/input.c +++ b/runtime/input.c @@ -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); diff --git a/runtime/main.c b/runtime/main.c index 4874210b..f3567e16 100644 --- a/runtime/main.c +++ b/runtime/main.c @@ -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); diff --git a/tests/fatal_exit_check.c b/tests/fatal_exit_check.c index ea9c0718..b398d87c 100644 --- a/tests/fatal_exit_check.c +++ b/tests/fatal_exit_check.c @@ -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 #include +#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; } diff --git a/tests/fatal_exit_check.py b/tests/fatal_exit_check.py index 97eee1d0..12df584e 100644 --- a/tests/fatal_exit_check.py +++ b/tests/fatal_exit_check.py @@ -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: