Run a frame's bookkeeping that is still owed on the present thread before the frame's blit, so the blit never reads the upscaler state of a frame vk_dlaa_frame has not closed.

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>
This commit is contained in:
chrg6
2026-11-13 19:42:03 -05:00
parent effdbd8dc0
commit 618cfc01f4
9 changed files with 196 additions and 59 deletions

View File

@@ -94,13 +94,13 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
- Several submitting threads. The game submits from several threads and presents from one of them. Their replays take
turns on the submission lock (`vk_sync.c`). The state a present closes for the frame (the DLSS state, the motion
tables, the frame count conditional draws go by) is reset between two replays: by the present when the lock is free,
else first thing in the next submission (`vk_frame_owed.h`, `NX2NAVI_PRESENT_ORDER`). A present held for
event-ordered work is put out once and in order by whichever thread's submission finds its work done
(`vk_present_held.h`); submitting threads only try its lock, and the presenting thread waits for it only while
another thread puts a held present out. A recording two threads submit at once gets a copy of its own for the
second, for its deferred binds (`nvn_submit_guard.h`). Held work is asked about without the lock from a published
copy (`vk_evo_peek.h`). The log counts the overlaps with `NX2NAVI_TRACE=perf` ("enters a submit ... while"), and at
exit what they cost ("frame bookkeeping: ...").
else first thing in the next submission or by the present thread before the frame's blit, whichever comes first
(`vk_frame_owed.h`, `NX2NAVI_PRESENT_ORDER`). A present held for event-ordered work is put out once and in order by
whichever thread's submission finds its work done (`vk_present_held.h`); submitting threads only try its lock, and
the presenting thread waits for it only while another thread puts a held present out. A recording two threads
submit at once gets a copy of its own for the second, for its deferred binds (`nvn_submit_guard.h`). Held work is
asked about without the lock from a published copy (`vk_evo_peek.h`). The log counts the overlaps with
`NX2NAVI_TRACE=perf` ("enters a submit ... while"), and at exit what they cost ("frame bookkeeping: ...").
- Driver data. The NVN driver keeps a per-stage constant bank (c0). The back end fills it as the driver does: storage
and uniform tables, push constants and, for NVN 2 compute, the dispatch's group counts in bytes 0-0xb.
- Storage buffers the shaders reach through pointer tables in memory are bound at the address the table holds

View File

@@ -775,11 +775,12 @@ held elsewhere a submitting thread gives up at once while the presenting thread
game files.
A change to where a frame's bookkeeping at its present runs (`runtime/vk_frame_owed.h`, `runtime/vk_backend.c`
`present_image` and `frame_end`, `runtime/vk_sync.c` `submit` and `vk_sync_try_locked`) passes
`python tests\frame_owed_check.py`, which builds the header with the platform layer: one presenting and three
replaying threads, some replays holding the lock 40 ms, over 20,000 frames; every frame's bookkeeping runs once, in
order, never during a replay and before any replay that took the lock after its present, and no present waits for a
replay. No GPU, no game files.
`present_image`, `present_frame_closed` and `frame_end`, `runtime/vk_sync.c` `submit`, `vk_sync_try_locked` and
`vk_sync_locked`) passes `python tests\frame_owed_check.py`, which builds the header with the platform layer: one
presenting thread, three replaying threads, some replays holding the lock 40 ms, and a present thread that blits each
frame handed to it, over 20,000 frames; every frame's bookkeeping runs once, in order, never during a replay, before
any replay that took the lock after its present and before the frame's blit, and no present waits for a replay. No
GPU, no game files.
A change to a synchronous submission's deferred binds (`runtime/nvn_submit_guard.h`, `runtime/nvn_api_cmd.c`
`nvn_submit_prepare`, `nvn_submit_done`, `resolve_deferred`) passes `python tests\submit_clone_check.py`, which builds the