Files
NX2navi/tests/cfg_rows_check.py
chrg6 7a8bbad47e Remove the low-end GPU mode from the settings.
The user asked for it: the mode no longer gains frame rate, and the mesh shader fallback covers the cards it was written for. Its switch in the settings menu's Game tab is gone with the rows under it, and so are their 57 text ids and strings.

low_end_gpu itself does nothing any more. It is still a known key, so an older nx2navi.ini gives no unknown-key warning, and while it is on the log says once that the mode was removed in 0.10.2. The preset that gave nine keys other defaults is gone, and so is the rule that read the mode's keys only while it was on: each of them (mesh_emulation, tex_clobber, foliage, vram_heap_limit, vram_reserve_mb, upload_batch_mb, mirror_stage, mirror_hot, submit_finish_gpu and the others) is an ordinary setting with the default of its row or of its reader, so a line a player wrote by hand keeps counting and a file with only the switch runs the game as it runs for everyone else. guest_opt in nx2navi.ini counts for the next rebuild without the switch too (tools/make_game.py).

The recorder thread read the switch itself: on an AMD card without mesh shaders the mode turned it on. It is off on every AMD card now unless vk_recorder = 1 is set.

nx2navi.ini.example lists the former mode's settings as single settings, the 0.10.2 notes have a Removed entry and name the foliage fix of the commit before this one, and the comments that said what the mode sets say so in the past tense. tests/cfg_parse_check has the new reading of the switch and of the keys without it; the test matrix is at 90 ok with the same two not ok as before (installer_test, worktree_test), and a clean build compiles 169 runtime files and links. Not run in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 06:13:18 -05:00

382 lines
17 KiB
Python

"""Static check of runtime/cfg.c's settings tables against what the runtime reads. Standard library only, so the
content guard workflow runs it beside tools/check_content.py.
It fails on:
- a row or name after a // on a line of known[], metas[], sections[] or DECK[]: it is not compiled, and a
hidden metas[] row ends every start with "FATAL: cfg: NX2NAVI_<X> has no row in the settings table" (this shipped
in two test builds); a /* inside those tables, which this check does not read;
- a name twice in known[], or two metas[] rows for one key in one #if branch (the first branch stands for the table,
later ones repeat its keys);
- a metas[] row whose key is not in known[];
- a read with cfg_value, cfg_int, cfg_bool or cfg_double of "NX2NAVI_<KEY>" (directly, or through one level of a
function that passes its const char * parameter on to one of them) whose key has no metas[] row, unless the same
function asked cfg_get, cfg_is_set or cfg_meta about that key before it;
- a name the runtime reads that is not in known[] and does not start with CHEAT_ (cfg.c is_known): cfg_get, cfg_is_set
and cfg_meta of "NX2NAVI_<KEY>", rt_knob("<KEY>", ...), one level of functions whose const char * parameter reaches
cfg_get or a "NX2NAVI_%s" of it, and the `set <name> NX2NAVI_<KEY>` lines of recomp/patches-v111.txt. Such a key set
in nx2navi.ini logs "unknown key" and is listed as unknown in the menu.
Names read only with getenv, plat_setenv or cfg_setenv are environment-only and not checked. runtime/cfg_probe.c asks
for an unknown key on purpose and is left out, as are third_party/ and the generated *_spv.h headers.
Usage: python tests/cfg_rows_check.py [repo] one line per problem with file:line; exit 1 if any
python tests/cfg_rows_check.py --self-test inject each kind of fault into copies and check each is caught
"""
import re
import sys
from pathlib import Path
KEY = r'[A-Z][A-Z0-9_]*'
TABLES = { # declaration, what a row looks like on a line
'known[]': ('static const char *const known[]', r'"(' + KEY + r')"'),
'metas[]': ('static const CfgMeta metas[]', r'\{\s*"(' + KEY + r')"'),
'sections[]': ('sections[] = {', r'\{\s*"(' + KEY + r')"'),
'DECK[]': ('DECK[] = {', r'\{\s*"(' + KEY + r')"'),
}
VALUE_FNS = ('cfg_value', 'cfg_int', 'cfg_bool', 'cfg_double')
GUARD_FNS = ('cfg_get', 'cfg_is_set', 'cfg_meta')
LIT = r'"NX2NAVI_(' + KEY + r')"'
def matching(src, open_at):
"""The index of the bracket that closes the one at open_at (brackets in string literals and comments left out)."""
pair = {'{': '}', '(': ')'}[src[open_at]]
depth, k, n = 0, open_at, len(src)
while k < n:
c = src[k]
if src.startswith('//', k):
k = src.find('\n', k)
if k < 0:
return n
elif src.startswith('/*', k):
k = src.find('*/', k + 2)
if k < 0:
return n
k += 1
elif c == '"' or c == "'":
k += 1
while k < n and src[k] != c:
k += 2 if src[k] == '\\' else 1
elif c == src[open_at]:
depth += 1
elif c == pair:
depth -= 1
if depth == 0:
return k
k += 1
return n
def split_comment(line):
"""(code, comment) of one line: the comment starts at the first // outside a string literal."""
i, n, quote = 0, len(line), None
while i < n:
c = line[i]
if quote:
if c == '\\':
i += 2
continue
if c == quote:
quote = None
elif c in '"\'':
quote = c
elif line.startswith('//', i):
return line[:i], line[i:]
i += 1
return line, ''
def strip_comments(src):
"""C source with its comments blanked out, line numbers and string literals kept."""
out, i, n = [], 0, len(src)
while i < n:
c = src[i]
if c == '"' or c == "'":
j = i + 1
while j < n and src[j] != c and src[j] != '\n':
j += 2 if src[j] == '\\' else 1
out.append(src[i:j + 1])
i = j + 1
elif src.startswith('//', i):
j = src.find('\n', i)
i = n if j < 0 else j
elif src.startswith('/*', i):
j = src.find('*/', i + 2)
j = n if j < 0 else j + 2
out.append('\n' * src.count('\n', i, j))
i = j
else:
out.append(c)
i += 1
return ''.join(out)
def line_of(src, pos):
return src.count('\n', 0, pos) + 1
def tables(cfg, problems):
"""The names of each table, the first #if branch of each line only, with the problems of hidden rows."""
out = {}
for name, (decl, row) in TABLES.items():
i = cfg.find(decl)
if i < 0:
problems.append(f'runtime/cfg.c: no {name} table ("{decl}")')
out[name] = []
continue
j = cfg.index('{', i + len(decl) - 1)
body = cfg[j + 1:matching(cfg, j)]
line0 = line_of(cfg, j)
names, branch, seen_branch = [], [], {}
for n, line in enumerate(body.split('\n')):
st = line.strip()
if st.startswith('#if'):
branch.append(0)
continue
if st.startswith(('#else', '#elif')):
if branch:
branch[-1] += 1
continue
if st.startswith('#endif'):
if branch:
branch.pop()
continue
code, comment = split_comment(line)
if '/*' in code:
problems.append(f'runtime/cfg.c:{line0 + n}: a /* comment in {name}, which this check does not read')
hidden = re.findall(row, comment)
if hidden:
problems.append(f'runtime/cfg.c:{line0 + n}: {name} rows after // are not compiled: {", ".join(hidden)}')
if branch and branch[-1] > 0:
continue # a later branch repeats the first one's keys
for k in re.findall(row, code):
names.append((k, line0 + n))
out[name] = names
return out
def functions(src):
"""(name, parameters, body start, body end) of every function definition in comment-stripped source."""
out = []
for m in re.finditer(r'\b([A-Za-z_]\w*)\s*\(([^;{}()]*)\)\s*(?:const\s*)?\{', src):
name = m.group(1)
if name in ('if', 'for', 'while', 'switch', 'return', 'sizeof', 'catch'):
continue
b = m.end() - 1
out.append((name, m.group(2), b, matching(src, b)))
return out
def call_args(src, open_at):
"""The arguments of the call whose ( is at open_at, as text."""
end = matching(src, open_at)
args, depth, cur, k = [], 0, '', open_at + 1
while k < end:
c = src[k]
if c == '"':
j = k + 1
while j < end and src[j] != '"':
j += 2 if src[j] == '\\' else 1
cur += src[k:j + 1]
k = j + 1
continue
if c in '([{':
depth += 1
elif c in ')]}':
depth -= 1
elif c == ',' and depth == 0:
args.append(cur.strip())
cur = ''
k += 1
continue
cur += c
k += 1
args.append(cur.strip())
return args
def check(cfg_text, sources, patches):
"""The problems, one line each. sources: {path as reported: text}; cfg_text is also among them as runtime/cfg.c."""
problems = []
tabs = tables(cfg_text, problems)
known = [k for k, _ in tabs['known[]']]
known_s = set(known)
metas_s = {k for k, _ in tabs['metas[]']}
for k in sorted({k for k in known if known.count(k) > 1}):
problems.append(f'runtime/cfg.c: known[] lists {k} twice')
seen = {}
for k, n in tabs['metas[]']:
if k in seen:
problems.append(f'runtime/cfg.c:{n}: metas[] has a second row for {k} (the first at line {seen[k]})')
seen.setdefault(k, n)
for k, n in tabs['metas[]']:
if k not in known_s:
problems.append(f'runtime/cfg.c:{n}: metas[] row {k} is not in known[]')
stripped = {p: strip_comments(t) for p, t in sources.items()}
# one level of wrappers: a const char * parameter passed on to a value read, or to cfg_get / "NX2NAVI_%s"
wrappers = {} # name -> (argument index, 'value' | 'name', the literal carries the prefix)
for p, s in stripped.items():
for fname, params, b, e in functions(s):
if fname in VALUE_FNS or fname in GUARD_FNS or fname == 'rt_knob':
continue
body = s[b:e]
for idx, prm in enumerate(x.strip() for x in params.split(',')):
pm = re.fullmatch(r'const\s+char\s*\*\s*(\w+)', prm)
if not pm:
continue
v = pm.group(1)
if re.search(r'\b(?:%s)\s*\(\s*%s\s*\)' % ('|'.join(VALUE_FNS), v), body):
wrappers[fname] = (idx, 'value', True)
elif re.search(r'\bcfg_get\s*\(\s*%s\s*\)' % v, body):
wrappers.setdefault(fname, (idx, 'name', True))
elif re.search(r'"NX2NAVI_%%s"\s*,\s*%s\b' % v, body):
wrappers.setdefault(fname, (idx, 'name', False))
value_sites, name_sites = [], [] # (key, file, position, how)
for p, s in stripped.items():
start = s.index('static int is_known') if p.endswith('runtime/cfg.c') and 'static int is_known' in s else 0
for m in re.finditer(r'\b(%s)\s*\(\s*' % '|'.join(VALUE_FNS) + LIT, s):
if m.start() >= start:
value_sites.append((m.group(2), p, m.start(), m.group(1)))
for m in re.finditer(r'\b(%s)\s*\(\s*' % '|'.join(GUARD_FNS) + LIT, s):
if m.start() >= start:
name_sites.append((m.group(2), p, m.start(), m.group(1)))
for m in re.finditer(r'\brt_knob\s*\(\s*"(' + KEY + r')"', s):
name_sites.append((m.group(1), p, m.start(), 'rt_knob'))
for w, (idx, kind, prefixed) in wrappers.items():
for m in re.finditer(r'\b' + re.escape(w) + r'\s*\(', s):
args = call_args(s, m.end() - 1)
if idx >= len(args):
continue
lm = re.fullmatch(r'"(NX2NAVI_)?(' + KEY + r')"', args[idx])
if not lm or bool(lm.group(1)) != prefixed:
continue
(value_sites if kind == 'value' else name_sites).append((lm.group(2), p, m.start(), w))
for n, line in enumerate(patches.split('\n'), 1):
m = re.match(r'\s*set\s+\S+\s+NX2NAVI_(' + KEY + r')\b', line)
if m:
name_sites.append((m.group(1), 'recomp/patches-v111.txt', -n, 'a patch set'))
def where(p, pos):
return f'{p}:{-pos if pos < 0 else line_of(stripped[p], pos)}'
bodies = {p: functions(s) for p, s in stripped.items()}
for k, p, pos, how in value_sites:
if k in metas_s:
continue
body = next(((b, e) for _, _, b, e in bodies[p] if b < pos < e), None)
text = stripped[p][body[0]:pos] if body else ''
if re.search(r'\b(?:%s)\s*\(\s*"NX2NAVI_%s"' % ('|'.join(GUARD_FNS), k), text):
continue # the function asked whether the key is set first (kernel.c SPIN, vk_headroom.c)
problems.append(f'{where(p, pos)}: {how}("NX2NAVI_{k}") has no metas[] row and no cfg_get guard before it: '
f'the game would end with "has no row in the settings table"')
missing = {}
for k, p, pos, how in name_sites + value_sites:
if k not in known_s and not k.startswith('CHEAT_'):
missing.setdefault(k, []).append(f'{where(p, pos)} {how}')
for k, ws in sorted(missing.items()):
problems.append(f'{ws[0].split()[0]}: {k} is read ({"; ".join(ws[:3])}{"; ..." if len(ws) > 3 else ""}) but '
f'is not in cfg.c known[]: set in nx2navi.ini it logs "unknown key"')
return problems, len(known), len(tabs['metas[]']), len(value_sites), len(name_sites), sorted(wrappers)
def read_repo(repo):
rt = repo / 'runtime'
files = sorted(p for p in rt.rglob('*') if p.suffix in ('.c', '.cpp', '.h', '.inc') and 'third_party' not in p.parts
and not p.name.endswith('_spv.h') and p.name != 'cfg_probe.c')
sources = {p.relative_to(repo).as_posix(): p.read_text(encoding='utf-8-sig', errors='replace') for p in files}
patches = repo / 'recomp' / 'patches-v111.txt'
return sources['runtime/cfg.c'], sources, patches.read_text(encoding='utf-8') if patches.exists() else ''
def self_test(repo):
"""Each kind of fault, put into a copy of the tables or a made-up source file, must be reported."""
cfg, sources, patches = read_repo(repo)
base, *_ = check(cfg, sources, patches)
bad = 0
if base:
print(f'self-test: the repository itself has {len(base)} problems; the cases below are judged on new ones')
def case(what, want, cfg_text=None, extra=None, patch_text=None):
nonlocal bad
src = dict(sources)
src['runtime/cfg.c'] = cfg_text if cfg_text is not None else cfg
src.update(extra or {})
got, *_ = check(src['runtime/cfg.c'], src, patch_text if patch_text is not None else patches)
new = [p for p in got if p not in base]
ok = any(re.search(want, p) for p in new)
print(f'self-test: {what}: {"caught" if ok else "MISSED"}')
if not ok:
bad += 1
for p in new:
print(' ', p)
def add_line(table_decl, line):
"""cfg.c with line put in as the table's first line."""
i = cfg.index(table_decl)
j = cfg.index('\n', cfg.index('{', i + len(table_decl) - 1)) + 1
return cfg[:j] + ' ' + line + '\n' + cfg[j:]
case('a metas[] row after //', r'metas\[\] rows after // are not compiled: HIDDEN_ROW',
add_line(TABLES['metas[]'][0], '{"FULLSCREEN_X", "0", "0|1", LIVE}, // {"HIDDEN_ROW", "0", "0|1", LIVE},'))
case('a known[] name after //', r'known\[\] rows after // are not compiled: HIDDEN_NAME',
add_line(TABLES['known[]'][0], '"AB_X", // "HIDDEN_NAME",'))
case('a sections[] entry after //', r'sections\[\] rows after // are not compiled: HIDDEN_SECTION',
add_line(TABLES['sections[]'][0], '// {"HIDDEN_SECTION", "display"},'))
case('a /* in a table', r'a /\* comment in known\[\]', add_line(TABLES['known[]'][0], '/* "X", */'))
case('a name twice in known[]', r'known\[\] lists AA twice', add_line(TABLES['known[]'][0], '"AA",'))
case('two metas[] rows for one key', r'second row for FULLSCREEN',
add_line(TABLES['metas[]'][0], '{"FULLSCREEN", "0", "0|1", LIVE},'))
case('a metas[] row not in known[]', r'metas\[\] row ROW_NOT_KNOWN is not in known\[\]',
add_line(TABLES['metas[]'][0], '{"ROW_NOT_KNOWN", "0", "0|1", LIVE},'))
# a row the game reads, hidden behind a // put before it on its line: its readers are left without a row
at = cfg.index('{"FULLSCREEN"', cfg.index(TABLES['metas[]'][0]))
case('a value read on a row hidden by // (FULLSCREEN)', r'FULLSCREEN"\) has no metas\[\] row',
cfg[:at] + '// ' + cfg[at:])
fake = '''
static int my_opt(const char *key) { return cfg_int(key); }
static unsigned my_env(const char *name) { return cfg_get(name) ? 1 : 0; }
void fake(void) {
int a = cfg_int("NX2NAVI_NO_ROW_INT");
if (cfg_get("NX2NAVI_GUARDED_BOOL") && cfg_bool("NX2NAVI_GUARDED_BOOL")) a++;
a += my_opt("NX2NAVI_WRAPPED_NO_ROW");
a += rt_knob("KNOB_NOT_KNOWN", 0);
a += (int)my_env("NX2NAVI_ENV_NOT_KNOWN");
}
'''
extra = {'runtime/fake_check.c': fake}
case('an unguarded cfg_int without a row', r'cfg_int\("NX2NAVI_NO_ROW_INT"\) has no metas', extra=extra)
case('a value read through a wrapper without a row', r'my_opt\("NX2NAVI_WRAPPED_NO_ROW"\) has no metas', extra=extra)
case('a guarded read without a row is not reported as such, its name is',
r'GUARDED_BOOL is read .* not in cfg.c known', extra=extra)
got, *_ = check(cfg, {**sources, **extra}, patches)
guarded_flagged = any('GUARDED_BOOL") has no metas' in p for p in got)
print(f'self-test: a read after a cfg_get guard in the same function: {"REPORTED" if guarded_flagged else "passes"}')
bad += guarded_flagged
case('an rt_knob name not in known[]', r'KNOB_NOT_KNOWN is read', extra=extra)
case('a name read through a cfg_get wrapper', r'ENV_NOT_KNOWN is read', extra=extra)
case('a patch set\'s name not in known[]', r'PATCH_SET_NOT_KNOWN is read',
patch_text=patches + '\nset some_patch NX2NAVI_PATCH_SET_NOT_KNOWN 1\n')
print(f'cfg_rows_check self-test: {"FAILED" if bad else "ok"}')
return 1 if bad else 0
def main():
args = sys.argv[1:]
repo = Path(__file__).resolve().parents[1]
if args and args[0] != '--self-test':
repo = Path(args[0])
if '--self-test' in args:
return self_test(repo)
problems, nk, nm, nv, nn, wr = check(*read_repo(repo))
for p in problems:
print(p)
print(f'cfg_rows_check: known[] {nk} names, metas[] {nm} rows, {nv} value reads and {nn} name reads (wrappers: '
f'{", ".join(wr)}): {len(problems)} problem{"" if len(problems) == 1 else "s"}')
return 1 if problems else 0
if __name__ == '__main__':
sys.exit(main())