From 7684734b47cf0a42f965e2b84671f6367e7635a5 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 17:23:30 -0500 Subject: [PATCH] Log once per switch when rt_knob reads a value other than 0, 1 or ab as on. perf.c rt_knob reads "0" as off, "ab" as the A/B blocks and everything else as on, without a word: "cache_die = off", "crash_filter_guard = false" and "one_per_core = 0 ; x" all read as 1 (the audit's probe, Fv/knob.exe, cut rt_knob out with cfg.c: three 1s and no log line). A player who wrote such a value meant the opposite and had no way to learn it from the log. rt_knob now logs "cfg: NX2NAVI_ = '' is not 0, 1 or ab: read as 1" the first time it reads such a value for a switch. The value still reads as 1: reading words as 0 changes what a written setting does and waits for approval with F-8. Most of the 109 call sites keep the result in a static but some read it per call, so the warned names are kept in a table of 32 pointers (the names are string literals) filled by compare-and-swap; 0, 1 and ab pay only one more string compare. New tests/rt_knob_check.py cuts rt_knob and its table out of perf.c and builds them with stand-ins for the settings and the log: 0, 1, ab, empty and unset read as before without a line; off, false and "0 ; a note" read as 1 and log once each however often they are read. docs/building.md describes it. The fault was shown before the change by the audit's probe; rt_knob_check passes, and perf.c compiles with the dev flags and with -Wall -Wextra without new warnings. Co-Authored-By: Claude Opus 5.5 --- docs/building.md | 5 ++ runtime/perf.c | 22 ++++++++- tests/rt_knob_check.py | 108 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 tests/rt_knob_check.py diff --git a/docs/building.md b/docs/building.md index 945a7a55..9671760f 100644 --- a/docs/building.md +++ b/docs/building.md @@ -721,6 +721,11 @@ which builds the header alone: a walk before, after or around a frame, one still replay part first, capped by the overlap and the parts, and 100,000 random frames in which the census part never passes the frame and the parts keep their sum. No GPU, no game files. +A change to how the on/off switches read their value (`runtime/perf.c` `rt_knob`) passes `python tests\rt_knob_check.py`, +which cuts the function out of the file with its warning table and builds it with stand-ins for the settings and the +log: 0, 1 and ab read as before without a line, and any other value reads as 1 and is logged once per switch. No game +files. + The lifter (`recomp/`) and the CPU headers (`runtime/cpu.h`, `runtime/simd.h`) are checked against Unicorn. Every lifter change passes these, all run with `.venv`: diff --git a/runtime/perf.c b/runtime/perf.c index 022b771f..8e987186 100644 --- a/runtime/perf.c +++ b/runtime/perf.c @@ -939,12 +939,32 @@ int rt_ab(void) { return k == 1 || k == 2; } +// rt_knob reads "0" as off, "ab" as the A/B blocks and anything else as on, so "off", "false" or "0 ; a note" turn a +// switch on. A value other than 0, 1 and ab is logged once per switch; the names are string literals, so the table +// holds their pointers (a full table logs no more). The value is still read as on: taking such words as off would +// change what a written setting does. +#define KNOB_WARNED 32 +static const char *knob_warned[KNOB_WARNED]; + +static void knob_warn(const char *name, const char *key, const char *value) { + for (int i = 0; i < KNOB_WARNED; i++) { + const char *cur = __atomic_load_n(&knob_warned[i], __ATOMIC_ACQUIRE); + if (!cur && __atomic_compare_exchange_n(&knob_warned[i], &cur, name, 0, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) { + rt_log("cfg: %s = '%s' is not 0, 1 or ab: read as 1", key, value); + return; + } + if (cur == name) return; // logged already (another thread may have taken this slot for it just now) + } +} + int rt_knob(const char *name, const char *def) { char key[64]; snprintf(key, sizeof key, "NX2NAVI_%s", name); const char *e = cfg_get(key); if (!e || !*e) e = def; - return !strcmp(e, "ab") ? 2 : strcmp(e, "0") != 0; + if (!strcmp(e, "ab")) return 2; + if (strcmp(e, "0") && strcmp(e, "1")) knob_warn(name, key, e); + return strcmp(e, "0") != 0; } // NX2NAVI_ONE_PER_CORE=1: the process keeps the first logical processor of each physical core, so busy threads (the diff --git a/tests/rt_knob_check.py b/tests/rt_knob_check.py new file mode 100644 index 00000000..5547e31c --- /dev/null +++ b/tests/rt_knob_check.py @@ -0,0 +1,108 @@ +"""Build and run runtime/perf.c's rt_knob (cut out of the file with its warning table) with stand-ins for the settings and +the log: 0, 1 and ab read as before and log nothing; a value such as "off", "false" or "0 ; a note" still reads as 1 +and is logged once per switch, however often the switch is read. + +No GPU, no game files, any Python. + +Usage: rt_knob_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') + +HARNESS = r''' +#include +#include +#include + +static const char *vals[][2] = {{"NX2NAVI_A", "0"}, {"NX2NAVI_B", "1"}, {"NX2NAVI_C", "ab"}, {"NX2NAVI_D", "off"}, + {"NX2NAVI_E", "false"}, {"NX2NAVI_F", "0 ; a note"}, {"NX2NAVI_G", ""}}; +static const char *cfg_get(const char *key) { + for (size_t i = 0; i < sizeof vals / sizeof *vals; i++) + if (!strcmp(vals[i][0], key)) return vals[i][1]; + return NULL; +} +static int logs; +static char last[256]; +static void rt_log(const char *fmt, ...) { + va_list ap; + va_start(ap, fmt); + vsnprintf(last, sizeof last, fmt, ap); + va_end(ap); + logs++; +} + +%s + +static int fails; +static void expect(const char *what, int got, int want) { + if (got != want) printf("FAIL %%s: %%d, expected %%d\n", what, got, want), fails++; +} + +int main(void) { + for (int round = 0; round < 3; round++) { + expect("0", rt_knob("A", "1"), 0); + expect("1", rt_knob("B", "0"), 1); + expect("ab", rt_knob("C", "0"), 2); + expect("unset, default 1", rt_knob("H", "1"), 1); + expect("unset, default 0", rt_knob("H2", "0"), 0); + expect("empty, default 0", rt_knob("G", "0"), 0); + } + expect("lines for 0, 1, ab and defaults", logs, 0); + expect("off", rt_knob("D", "0"), 1); + expect("a line for off", logs, 1); + if (strcmp(last, "cfg: NX2NAVI_D = 'off' is not 0, 1 or ab: read as 1")) + printf("FAIL the line: %%s\n", last), fails++; + for (int i = 0; i < 5; i++) rt_knob("D", "0"); + expect("off read again", logs, 1); + expect("false", rt_knob("E", "1"), 1); + expect("0 with a note", rt_knob("F", "0"), 1); + expect("a line each", logs, 3); + if (!strstr(last, "'0 ; a note'")) printf("FAIL the line: %%s\n", last), fails++; + for (int i = 0; i < 3; i++) rt_knob("E", "1"), rt_knob("F", "0"); + expect("each once", logs, 3); + if (fails) return 1; + printf("rt_knob_check: all checks passed\n"); + return 0; +} +''' + + +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 cut(): + """perf.c from the warning table's #define to the end of rt_knob.""" + src = (ROOT / 'runtime' / 'perf.c').read_text(encoding='utf-8') + m = re.search(r'#define KNOB_WARNED .*?\nint rt_knob\(const char \*name, const char \*def\) \{\n.*?\n\}\n', src, re.S) + assert m, 'runtime/perf.c: the rt_knob block (KNOB_WARNED ... rt_knob) was not found' + return m.group(0) + + +def main(): + work = ROOT / 'build' / 'test' / 'rt_knob' + work.mkdir(parents=True, exist_ok=True) + c = work / 'rt_knob_check.c' + c.write_text('// Made by tests/rt_knob_check.py from runtime/perf.c.\n' + HARNESS % cut(), encoding='utf-8') + exe = work / ('rt_knob_check.exe' if os.name == 'nt' else 'rt_knob_check') + env = dict(os.environ) + if os.name == 'nt' and MSYS_BIN.exists(): + env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') + subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-Wall', '-Werror', str(c), '-o', str(exe)], env=env) + return subprocess.run([str(exe)], env=env).returncode + + +if __name__ == '__main__': + sys.exit(main())