mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The review of cd2fd454 found that with present_order = 1 the present thread could record and queue frame N's blit before frame_end(N) ran: when the presenting thread's try of the submission lock failed, frame_end(N) waited for the next submission, and that could be the present thread's own blit submission, which present_main makes only after vk_present_blit. The blit reads what vk_dlaa_frame resets when the upscaler or frame generation is switched on or off (gfx_present.c hudless_record through vk_dlaa_fg_hudless; vk_fg.cpp, vk_fg4.cpp and vk_dlssg.c through vk_dlaa_fg_inputs, vk_dlaa_fg_views and vk_dlaa_fg_camera). vk_dlaa_frame drops those inputs at a switch because handing them to frame generation ended an AMD process before, and in 0.10.1 vk_dlaa_frame(N) always ran before blit N. So a switch frame inside an overlap window could hand frame generation the inputs the switch should have dropped. Now present_image tells the present thread when it left the frame owed (PresentJob.owed), and present_main first makes sure that frame's bookkeeping ran (present_frame_closed, vk_frame_owed.h fo_close): when a submission did it meanwhile, done says so (it is now stored with release after each frame's bookkeeping and read with acquire, fo_ran) and no lock is taken; otherwise the present thread takes the submission lock, waiting for a replay in progress (vk_sync.c vk_sync_locked), and runs what is owed. frame_end(N) therefore always comes before blit N, as in 0.10.1. The present thread is not one of the game's threads, and it waits for the same lock for the blit's submission a moment later anyway. It waited for that lock with its busy flag set before too, and nothing that holds the lock waits for the present thread, so no deadlock is added. The cost, only for a frame still owed when the present thread takes it: the blit's recording no longer overlaps the replay it waits for, which can lengthen a drawing thread's existing wait in vk_present_drain by that recording. The common frame, whose present got the lock, takes no lock here. The log's frame bookkeeping line (exit, and trace = perf every 600 presents) counts these frames and the time they took the present thread, the wait included. tests/frame_owed_check now has a present thread that takes each frame from the presenting thread (one in waiting at most), blits it and submits: no blit comes before its frame's bookkeeping. Its "old" argument leaves the catch-up out and fails, as the branch had it: 2608 and 2706 blits before their frame's bookkeeping in two runs of 20,000 frames. docs/building.md, docs/architecture.md and nx2navi.ini.example say where the bookkeeping runs. Not reproduced before the change. Checks that passed: frame_owed_check (20,000 frames; 12,695 owed, 1,888 of them caught up by the present thread, 0 blits before their bookkeeping, the longest present 0.003 ms), pass_cc_check of every runtime file (headers changed), no new -Wall -Wextra warnings in vk_backend.c and vk_sync.c. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
59 lines
2.8 KiB
Python
59 lines
2.8 KiB
Python
"""Build and run tests/frame_owed_check.c: runtime/vk_frame_owed.h, a frame's bookkeeping at its present in replay order
|
|
(vk_backend.c "a frame's bookkeeping in replay order", vk_sync.c submit). One presenting thread, three replaying
|
|
threads that hold the lock for their replays, a few of them for 40 ms, and a present thread that takes each frame
|
|
from the presenting thread, blits it and submits, over 20,000 frames: every frame's bookkeeping runs once and in frame
|
|
order, never during a replay, before the body of any replay that took the lock after the frame was presented and
|
|
before the present thread's blit of the frame (vk_backend.c present_frame_closed), and no present takes 20 ms, so the
|
|
presenting thread never waits for a replay. Step by step: a present with the lock free runs its own frame's, presents
|
|
past a held lock leave theirs to the next holder, which runs them in order, an older frame number runs nothing again,
|
|
and the present thread takes the lock only for a frame still owed. The exe's "old" argument leaves the present
|
|
thread's catch-up out and fails on the blits that come before their frame's bookkeeping.
|
|
|
|
No GPU, no game files, any Python.
|
|
|
|
Usage: frame_owed_check.py
|
|
"""
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
|
|
|
|
|
def compiler():
|
|
"""$CC, else MSYS2 UCRT64's clang, else clang on PATH."""
|
|
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 main():
|
|
work = ROOT / 'build' / 'test' / 'frame_owed'
|
|
work.mkdir(parents=True, exist_ok=True)
|
|
win = os.name == 'nt'
|
|
exe = work / ('frame_owed_check.exe' if win else 'frame_owed_check')
|
|
env = dict(os.environ)
|
|
if win and MSYS_BIN.exists():
|
|
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
|
rt = ROOT / 'runtime'
|
|
plat = [rt / 'platform' / ('win32.c' if win else 'linux.c'), rt / 'platform' / 'common.c']
|
|
libs = ['-lbcrypt', '-lws2_32', '-lpsapi', '-lwinmm', '-ldbghelp', '-lshell32', '-lole32', '-luser32', '-ladvapi32'] if win \
|
|
else ['-lpthread']
|
|
subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-g', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing',
|
|
'-march=x86-64-v3', '-I', str(rt), str(ROOT / 'tests' / 'frame_owed_check.c'),
|
|
*map(str, plat), '-o', str(exe), '-lm', *libs], env=env)
|
|
try:
|
|
return subprocess.run([str(exe)], env=env, timeout=300).returncode
|
|
except subprocess.TimeoutExpired:
|
|
print('frame_owed_check: FAIL: the check did not end within 300 s')
|
|
return 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|