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
3.8 KiB
C
59 lines
3.8 KiB
C
// A frame's bookkeeping at its present (vk_backend.c frame_end: the replay's per-frame state that a present closes), in
|
|
// replay order. Header only, so that tests/frame_owed_check.c checks it without a device.
|
|
//
|
|
// The replay reads and writes that state as it records, under the submission lock (vk_sync.c sub_lock). With the
|
|
// replay on the submitting threads the game presents on one thread while another may be replaying, and running the
|
|
// bookkeeping then reset the state in the middle of that replay. The presenting thread does not wait for the replay:
|
|
// it marks its frame owed and runs the bookkeeping itself only when it gets the lock at once; otherwise the next thread
|
|
// to take the lock (vk_sync.c submit) runs every owed frame's bookkeeping, in frame order, before its own replay. A frame
|
|
// caught up behind a later present runs as two presents with no submission between them always did.
|
|
//
|
|
// The present thread's blit of a frame reads what that frame's bookkeeping closed (vk_dlaa.c vk_dlaa_frame: the
|
|
// inputs frame generation must not get across an upscaler or frame generation switch), so the bookkeeping must come
|
|
// before it, as it always did when it ran at the present. A frame left owed may still be owed when the present thread
|
|
// takes it: fo_close runs what is owed then, under the lock, waiting for a replay in progress as the present thread's
|
|
// own blit submission does a moment later. The present thread is not one of the game's threads.
|
|
#pragma once
|
|
#include <stdint.h>
|
|
|
|
typedef struct {
|
|
uint64_t owed; // the newest presented frame (atomic: the presenting thread raises it, the lock's holder reads it)
|
|
uint64_t done; // the newest frame whose bookkeeping ran (the lock's holder writes it; atomic for fo_ran)
|
|
} FrameOwed;
|
|
|
|
typedef void (*FrameEnd)(uint64_t frame, void *arg);
|
|
|
|
// The lock's holder: whether a frame waits for its bookkeeping (one atomic load, for every submission).
|
|
static inline int fo_waiting(FrameOwed *o) {
|
|
return __atomic_load_n(&o->owed, __ATOMIC_ACQUIRE) != __atomic_load_n(&o->done, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
// The lock's holder: the bookkeeping of every frame presented since the last run, in frame order; returns how many.
|
|
// done moves past a frame only once its bookkeeping returned (fo_ran reads it without the lock).
|
|
static inline uint64_t fo_run(FrameOwed *o, FrameEnd end, void *arg) {
|
|
uint64_t owed = __atomic_load_n(&o->owed, __ATOMIC_ACQUIRE), d = __atomic_load_n(&o->done, __ATOMIC_RELAXED), n = 0;
|
|
while (d < owed) end(++d, arg), __atomic_store_n(&o->done, d, __ATOMIC_RELEASE), n++;
|
|
return n;
|
|
}
|
|
|
|
// Any thread: whether frame's bookkeeping ran; when it did, what it wrote is visible to the caller (the release of done
|
|
// in fo_run).
|
|
static inline int fo_ran(FrameOwed *o, uint64_t frame) { return __atomic_load_n(&o->done, __ATOMIC_ACQUIRE) >= frame; }
|
|
|
|
// The presenting thread, frame numbers rising (a smaller one than owed leaves it: that frame's bookkeeping ran or is
|
|
// owed already). try_run(arg) takes the lock without waiting, runs fo_run under it and returns 1, or returns 0 when
|
|
// another thread holds it. Returns whether the bookkeeping ran here.
|
|
static inline int fo_present(FrameOwed *o, uint64_t frame, int (*try_run)(void *), void *arg) {
|
|
uint64_t was = __atomic_load_n(&o->owed, __ATOMIC_RELAXED);
|
|
while (was < frame && !__atomic_compare_exchange_n(&o->owed, &was, frame, 1, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {}
|
|
return try_run(arg);
|
|
}
|
|
|
|
// The present thread, before it uses a presented frame (its blit): when the frame's bookkeeping has not run, run_locked
|
|
// (arg) takes the lock, waiting for it, and runs fo_run under it. Returns whether it had to; afterwards fo_ran(frame).
|
|
static inline int fo_close(FrameOwed *o, uint64_t frame, void (*run_locked)(void *), void *arg) {
|
|
if (fo_ran(o, frame)) return 0;
|
|
run_locked(arg);
|
|
return 1;
|
|
}
|