mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
An exit that skipped rtss_limit_restore (Task Manager, a hang, lost_wait_exit, a kill during the pipeline save) left the game's limit in Profiles\<exe>.cfg, and the next run's first apply took it for the player's own: that run then put the stale limit back at its close, and a profile the game had made stayed for good (lane A's probe: kill1, then kill2 'held a limit of 60/1'). The crash handler and the fatal error's end also called into RTSSHooks64.dll from a process that had failed (P-13). rtss_limit.c now keeps one line per profile in rtss-profile.txt next to the executable (rtss_decide.h RtssSaved: whether the profile existed, its old limit and denominator, the value the run wrote), written atomically before the run's first change of the profile, updated with each value written, and removed once the profile is back (put_back; also when the first write did not stick). On the first poll with the hooks library a start that finds a line of its profile puts the profile back first (restore_left, rtss_restore_plan): the old limit, or no profile, unless the player changed or removed it since, which the written value tells. main.c crash_handler and dispatch.c fatal_end call the new rtss_limit_crash, which closes the run and leaves the profile to that start with no call into RTSS's library; only when the file could not be written does it put the profile back as before, and never from the worker's own thread. A file that cannot be written leaves the run as it was, with one line in the log. Profiles frozen by earlier versions cannot be told from the player's own. Not reproduced before the change. rtss_limit_check (T12: the line's format and parse, the file's rewrite, each plan; the static check that both crash paths go through rtss_limit_crash), rtss_worker_check (state_record, state_player, state_unwritable, crash_recorded, and kill_restart with its player and edited variants, each a killed run and the next start in processes of their own), fatal_exit_check and exit_order_check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
4.7 KiB
Python
103 lines
4.7 KiB
Python
"""Build and run the checks of runtime/rtss_limit.c, RivaTuner Statistics Server as the frame limiter.
|
|
|
|
tests/rtss_limit_check.c builds runtime/rtss_decide.h alone, the parts that make no system call. tests/rtss_worker_check.c
|
|
(Windows only, skipped elsewhere) compiles the real rtss_limit.c against a fake RTSS and runs each of its scenarios in a
|
|
process of its own. Checks statically that a crash and a fatal error (runtime/main.c crash_handler, runtime/dispatch.c
|
|
fatal_end) leave RTSS's profile to the next start through rtss_limit_crash, which calls into RTSS's library only
|
|
through the orderly restore and only when the state file holds no line of the run.
|
|
|
|
No GPU, no game files, no RTSS, any Python.
|
|
|
|
Usage: rtss_limit_check.py
|
|
"""
|
|
import os
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
|
|
|
|
|
def compiler():
|
|
"""$CC, else MSYS2 UCRT64's clang, else clang on PATH (as tests/diff/common.py picks it, without its Unicorn)."""
|
|
if os.environ.get('CC'):
|
|
return os.environ['CC']
|
|
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
|
|
return str(MSYS_BIN / 'clang.exe')
|
|
return shutil.which('clang') or 'clang'
|
|
|
|
|
|
def build(src, exe, env):
|
|
subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-Wall', '-Werror', '-I', str(ROOT / 'runtime'),
|
|
str(ROOT / 'tests' / src), '-o', str(exe)], env=env)
|
|
|
|
|
|
def function(source, signature):
|
|
start = source.index(signature)
|
|
brace = source.index('{', start)
|
|
depth, end = 1, brace + 1
|
|
while depth:
|
|
depth += (source[end] == '{') - (source[end] == '}')
|
|
end += 1
|
|
return re.sub(r'//[^\n]*', '', source[start:end])
|
|
|
|
|
|
def static_checks():
|
|
rt = ROOT / 'runtime'
|
|
bad = 0
|
|
for file, sig in (('main.c', 'static void crash_handler(const PlatCrash *x)'),
|
|
('dispatch.c', 'static void fatal_end(int code, const char *fmt, va_list ap) {')):
|
|
body = function((rt / file).read_text(encoding='utf-8'), sig)
|
|
ok = 'rtss_limit_crash(' in body and 'rtss_limit_restore(' not in body
|
|
print(f'static: {file} {sig.split("(")[0].split()[-1]} leaves RTSS\'s profile to the next start '
|
|
f'(rtss_limit_crash, no rtss_limit_restore): {"ok" if ok else "FAIL"}')
|
|
bad += not ok
|
|
crash = function((rt / 'rtss_limit.c').read_text(encoding='utf-8'), 'void rtss_limit_crash(void) {')
|
|
calls = ('load_profile', 'save_profile', 'delete_profile', 'set_property', 'get_property', 'update_profiles',
|
|
'put_back', 'write_limit', 'plat_mutex_lock', 'plat_mutex_trylock')
|
|
gate, restore = crash.find('recorded'), crash.find('rtss_limit_restore(')
|
|
ok = (crash.find('&closed, 1)') >= 0 and not any(c + '(' in crash for c in calls)
|
|
and 0 <= gate < restore and 'rtss_tid' in crash[:restore])
|
|
print(f'static: rtss_limit.c rtss_limit_crash closes the run and calls into RTSS only through the restore, when the '
|
|
f'state file holds no line and not on the worker\'s thread: {"ok" if ok else "FAIL"}')
|
|
return bad + (not ok)
|
|
|
|
|
|
def main():
|
|
failed = ['static checks'] if static_checks() else []
|
|
work = ROOT / 'build' / 'test' / 'rtss_limit'
|
|
work.mkdir(parents=True, exist_ok=True)
|
|
env = dict(os.environ)
|
|
if os.name == 'nt' and MSYS_BIN.exists():
|
|
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
|
pure = work / ('rtss_limit_check.exe' if os.name == 'nt' else 'rtss_limit_check')
|
|
build('rtss_limit_check.c', pure, env)
|
|
if subprocess.run([str(pure)], env=env).returncode != 0:
|
|
failed.append('rtss_limit_check')
|
|
if os.name != 'nt':
|
|
print('rtss_worker_check: skipped (rtss_limit.c limits only on Windows)')
|
|
return 1 if failed else 0
|
|
exe = work / 'rtss_worker_check.exe'
|
|
build('rtss_worker_check.c', exe, env)
|
|
names = subprocess.run([str(exe), '--list'], env=env, capture_output=True, text=True, check=True).stdout.split()
|
|
for name in names:
|
|
try:
|
|
p = subprocess.run([str(exe), name], env=env, capture_output=True, text=True, timeout=120)
|
|
ok, out = p.returncode == 0, p.stdout + p.stderr
|
|
except subprocess.TimeoutExpired as e:
|
|
out = e.stdout if isinstance(e.stdout, str) else (e.stdout or b'').decode(errors='replace')
|
|
ok, out = False, out + '\nTIMEOUT'
|
|
print(f'rtss_worker_check {name}: {"ok" if ok else "FAIL"}')
|
|
if not ok:
|
|
print(out)
|
|
failed.append(name)
|
|
print(f'{len(names) + 2 - len(failed)} of {len(names) + 2} checks ok' + (f'; failed: {", ".join(failed)}' if failed else ''))
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|