mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Check from the sources that no upscaler waits for the device where it lets something go.
tests/upscaler_idle_check.py reads vk_dlaa.c, vk_ffx.cpp and vk_ffx4.cpp with their comments and strings blanked and
fails when vkDeviceWaitIdle is called outside vk_upscaler_device_idle and the --probe-ffx-cache path, when
vk_upscaler_device_idle is called outside vk_upscaler_settle and vk_upscaler_gpu_wait, or when one of the ten places
that let something go does not call vk_upscaler_let_go, through which NX2NAVI_UPSCALER_IDLE_WAIT=1 brings the old wait
back. It also checks that switch's sections[] entry and nx2navi.ini.example line (cfg_rows_check has its known[] name
and metas[] row), that perf.c names every PERF_M_* kind (a kind without a name prints a null string on a slow frame's
line), and that pb_images notes PB.used after the last call that clears PB.
On this branch it passes. On the base of this branch's work (stability-pass, 1f547853) it reports the twelve waits of
C-6 and Cv-1, each with its function, and on the first commit of this branch, where the waits went through
vk_upscaler_device_idle, the same twelve places. docs/building.md "Checks" describes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -811,6 +811,14 @@ once the submission that last named it is done and its present gate is passed, t
|
||||
they were put and the rest keep theirs, that a full list refuses without overwriting, and that an entry a release puts
|
||||
waits for the next take. No GPU, no game files.
|
||||
|
||||
A change to where the upscalers free what they let go of (`runtime/vk_dlaa.c` "what the upscalers let go of while the
|
||||
GPU may still use it", `runtime/vk_ffx.cpp` and `runtime/vk_ffx4.cpp` `context_for`) passes
|
||||
`python tests\upscaler_idle_check.py`, which reads the sources: `vkDeviceWaitIdle` only in `vk_upscaler_device_idle`
|
||||
and the `--probe-ffx-cache` path, that function only behind the rollback switch, a full list and a refused creation,
|
||||
every place that lets something go calling `vk_upscaler_let_go`, `upscaler_idle_wait`'s `sections[]` entry and
|
||||
`nx2navi.ini.example` line, a name in `perf.c` for each perf mark kind, and `pb_images` noting the images' last use
|
||||
after it makes them. Any Python, no game files.
|
||||
|
||||
A change to which frames the flight recorder takes for a one-frame flash (`runtime/flight_flash.h`, `runtime/flight.c`
|
||||
`flight_cells`) passes `python tests\flight_flash_check.py`, which builds the header alone, draws pictures of the
|
||||
thumbnails' size and checks that a patch or a whole frame shown once is a flash, that a camera cut, a fade, a pan and
|
||||
|
||||
182
tests/upscaler_idle_check.py
Normal file
182
tests/upscaler_idle_check.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Static checks of the upscalers' waits for the device (runtime/vk_dlaa.c "what the upscalers let go of while the GPU
|
||||
may still use it", runtime/vk_ffx.cpp, runtime/vk_ffx4.cpp).
|
||||
|
||||
It fails when:
|
||||
- vkDeviceWaitIdle is called in vk_dlaa.c outside vk_upscaler_device_idle, in vk_ffx.cpp outside vk_ffx_probe_cache
|
||||
(the --probe-ffx-cache path), or anywhere in vk_ffx4.cpp: each such wait stalls the replaying thread with the queue
|
||||
lock held;
|
||||
- vk_upscaler_device_idle is called outside vk_upscaler_settle and vk_upscaler_gpu_wait (the rollback switch
|
||||
NX2NAVI_UPSCALER_IDLE_WAIT=1, a full list, a refused creation's failed wait);
|
||||
- one of the places that let something go (vk_dlaa.c free_size, free_snap, stale_sizes, release_kept, hudless_from,
|
||||
pb_images, retire_idle and evaluate, vk_ffx.cpp and vk_ffx4.cpp context_for) does not call vk_upscaler_let_go, which
|
||||
frees what the GPU is past and which the rollback switch turns into a wait for the device;
|
||||
- NX2NAVI_UPSCALER_IDLE_WAIT has no sections[] entry in cfg.c or no line in nx2navi.ini.example (its known[] name and
|
||||
its metas[] row, and a // that would hide them, are tests/cfg_rows_check.py's);
|
||||
- perf.c's mark_name has another count of names than host.h has PERF_M_* kinds: a kind without a name prints a null
|
||||
string on a slow frame's line;
|
||||
- vk_dlaa.c pb_images does not note PB.used after the last call that clears PB: idle_release then took the images it
|
||||
had just made for 600 presents unused and freed them while the frame that used them could be in flight.
|
||||
|
||||
No GPU, no game files, any Python. Usage: upscaler_idle_check.py [repository root]
|
||||
"""
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
WAIT_ALLOWED = { # where vkDeviceWaitIdle may be called
|
||||
'vk_dlaa.c': {'vk_upscaler_device_idle'},
|
||||
'vk_ffx.cpp': {'vk_ffx_probe_cache'},
|
||||
'vk_ffx4.cpp': set(),
|
||||
}
|
||||
IDLE_ALLOWED = {'vk_upscaler_settle', 'vk_upscaler_gpu_wait'} # vk_dlaa.c: who may call vk_upscaler_device_idle
|
||||
LET_GO = { # functions that let something go and must call vk_upscaler_let_go
|
||||
'vk_dlaa.c': ['free_size', 'free_snap', 'stale_sizes', 'release_kept', 'hudless_from', 'pb_images', 'retire_idle',
|
||||
'evaluate'],
|
||||
'vk_ffx.cpp': ['context_for'],
|
||||
'vk_ffx4.cpp': ['context_for'],
|
||||
}
|
||||
# A function definition: at column 0, a return type, the name, the parameters and the body's brace (the runtime's
|
||||
# style); declarations end with ';' before any '{'. Read in code whose string literals are blanked: extern "C" is
|
||||
# extern " " there.
|
||||
FUNC = re.compile(r'^(?:extern "[^"\n]*" )?(?:static )?(?:inline )?[A-Za-z_][\w \*]*?\b([A-Za-z_]\w*)\s*\([^;{}]*\)\s*\{',
|
||||
re.M)
|
||||
|
||||
|
||||
def blank_comments(src):
|
||||
"""src with comments and string literals replaced by spaces (newlines kept), so offsets stay and only code is read."""
|
||||
out, i, n = list(src), 0, len(src)
|
||||
while i < n:
|
||||
if src.startswith('//', i):
|
||||
j = src.find('\n', i)
|
||||
j = n if j < 0 else j
|
||||
for k in range(i, j):
|
||||
out[k] = ' '
|
||||
i = j
|
||||
elif src.startswith('/*', i):
|
||||
j = src.find('*/', i + 2)
|
||||
j = n if j < 0 else j + 2
|
||||
for k in range(i, j):
|
||||
if out[k] != '\n':
|
||||
out[k] = ' '
|
||||
i = j
|
||||
elif src[i] in '"\'':
|
||||
q, j = src[i], i + 1
|
||||
while j < n and src[j] != q and src[j] != '\n':
|
||||
j += 2 if src[j] == '\\' else 1
|
||||
for k in range(i + 1, min(j, n)):
|
||||
out[k] = ' '
|
||||
i = j + 1
|
||||
else:
|
||||
i += 1
|
||||
return ''.join(out)
|
||||
|
||||
|
||||
def functions(code):
|
||||
"""[(name, body start, body end)] of each function defined in code (comments blanked)."""
|
||||
found = []
|
||||
for m in FUNC.finditer(code):
|
||||
depth, k = 0, m.end() - 1
|
||||
while k < len(code):
|
||||
if code[k] == '{':
|
||||
depth += 1
|
||||
elif code[k] == '}':
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
break
|
||||
k += 1
|
||||
found.append((m.group(1), m.end() - 1, k))
|
||||
return found
|
||||
|
||||
|
||||
def enclosing(funcs, pos):
|
||||
for name, a, b in funcs:
|
||||
if a <= pos <= b:
|
||||
return name
|
||||
return None
|
||||
|
||||
|
||||
def calls(code, funcs, callee):
|
||||
"""[(line, enclosing function)] of each call of callee (its definition left out)."""
|
||||
out = []
|
||||
for m in re.finditer(r'\b%s\s*\(' % re.escape(callee), code):
|
||||
fn = enclosing(funcs, m.start())
|
||||
if fn is None: # the definition's own head, or a declaration
|
||||
continue
|
||||
out.append((code.count('\n', 0, m.start()) + 1, fn))
|
||||
return out
|
||||
|
||||
|
||||
def body(code, funcs, name):
|
||||
for n, a, b in funcs:
|
||||
if n == name:
|
||||
return code[a:b + 1]
|
||||
return None
|
||||
|
||||
|
||||
def main():
|
||||
root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path(__file__).resolve().parents[1]
|
||||
rt = root / 'runtime'
|
||||
fails = []
|
||||
sources = {}
|
||||
for f in WAIT_ALLOWED:
|
||||
code = blank_comments((rt / f).read_text(encoding='utf-8'))
|
||||
sources[f] = (code, functions(code))
|
||||
for f, allowed in WAIT_ALLOWED.items():
|
||||
code, funcs = sources[f]
|
||||
for line, fn in calls(code, funcs, 'vkDeviceWaitIdle'):
|
||||
if fn not in allowed:
|
||||
fails.append(f'{f}:{line}: vkDeviceWaitIdle in {fn}, allowed only in {sorted(allowed) or "none"}')
|
||||
for line, fn in calls(code, funcs, 'vk_upscaler_device_idle'):
|
||||
if f != 'vk_dlaa.c' or fn not in IDLE_ALLOWED:
|
||||
fails.append(f'{f}:{line}: vk_upscaler_device_idle in {fn}, allowed only in vk_dlaa.c '
|
||||
f'{sorted(IDLE_ALLOWED)}')
|
||||
nwaits = len(calls(*sources['vk_dlaa.c'], 'vkDeviceWaitIdle'))
|
||||
if nwaits != 1:
|
||||
fails.append(f'vk_dlaa.c: {nwaits} calls of vkDeviceWaitIdle, expected the one in vk_upscaler_device_idle')
|
||||
for f, names in LET_GO.items():
|
||||
code, funcs = sources[f]
|
||||
for name in names:
|
||||
b = body(code, funcs, name)
|
||||
if b is None:
|
||||
fails.append(f'{f}: no function {name}')
|
||||
elif not re.search(r'\bvk_upscaler_let_go\s*\(', b):
|
||||
fails.append(f'{f} {name}: lets something go without vk_upscaler_let_go')
|
||||
|
||||
key = 'UPSCALER_IDLE_WAIT'
|
||||
cfg = (rt / 'cfg.c').read_text(encoding='utf-8-sig')
|
||||
if not re.search(r'\{\s*"%s"\s*,\s*"display"\s*\}' % key, cfg):
|
||||
fails.append(f'cfg.c: {key} has no sections[] entry in "display"')
|
||||
ini = (root / 'nx2navi.ini.example').read_text(encoding='utf-8-sig')
|
||||
if not re.search(r'^#\s*%s\s*=' % key.lower(), ini, re.M):
|
||||
fails.append(f'nx2navi.ini.example: no {key.lower()} line')
|
||||
|
||||
host = (rt / 'host.h').read_text(encoding='utf-8')
|
||||
m = re.search(r'enum\s*\{\s*(PERF_M_FILE[^}]*)\}', host)
|
||||
kinds = [k.strip() for k in m.group(1).split(',') if k.strip() and k.strip() != 'PERF_M_N'] if m else []
|
||||
perf = (rt / 'perf.c').read_text(encoding='utf-8')
|
||||
m = re.search(r'mark_name\[PERF_M_N\]\s*=\s*\{(.*?)\};', perf, re.S)
|
||||
names = re.findall(r'"[^"]*"', m.group(1)) if m else []
|
||||
if not kinds or len(names) != len(kinds):
|
||||
fails.append(f'perf.c: mark_name has {len(names)} names for {len(kinds)} PERF_M kinds in host.h')
|
||||
|
||||
code, funcs = sources['vk_dlaa.c']
|
||||
b = body(code, funcs, 'pb_images')
|
||||
if b is None:
|
||||
fails.append('vk_dlaa.c: no function pb_images')
|
||||
else:
|
||||
clears = [mm.end() for mm in re.finditer(r'\b(?:pb_destroy|retire_pb)\s*\(\s*\)|memset\s*\(\s*&PB\b', b)]
|
||||
sets = [mm.start() for mm in re.finditer(r'\bPB\.used\s*=', b)]
|
||||
last_clear = max(clears) if clears else -1
|
||||
if not sets or max(sets) < last_clear:
|
||||
fails.append('vk_dlaa.c pb_images: PB.used is not noted after the last call that clears PB')
|
||||
|
||||
for f in fails:
|
||||
print('FAIL', f)
|
||||
print('upscaler_idle_check:', 'FAILED' if fails else
|
||||
f'passed (vkDeviceWaitIdle once in vk_dlaa.c, {sum(len(v) for v in LET_GO.values())} places let go, '
|
||||
f'{len(kinds)} perf mark kinds named)')
|
||||
return 1 if fails else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user