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_<name> = '<value>' 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 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 17:23:30 -05:00
parent 74b9687a8f
commit 7684734b47
3 changed files with 134 additions and 1 deletions

View File

@@ -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`:

View File

@@ -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

108
tests/rt_knob_check.py Normal file
View File

@@ -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 <stdarg.h>
#include <stdio.h>
#include <string.h>
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())