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())