From 86e6d62d35e1c7b72370cce8c48ff3d4b87482c7 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 18:59:02 -0500 Subject: [PATCH] Add a list of what an upscaler lets go of, released once the GPU is past it, and its unit check. runtime/vk_up_defer.h holds, per owner, a DLSS feature, an FSR context or an image frame generation can still read, each with the timeline value of the last submission that named it and an optional present count. upd_take releases, in the order they were put, the entries whose value the GPU has reached and whose present count has passed; nothing in it waits or locks, so the owner checks at its own calls on the thread that makes its other upscaler calls. It is the piece the next commits use to take the upscalers' device-idle waits off the replaying thread: those waits stall the present's flip, the completion thread and upload submits with the queue lock held, at every pause-screen size, reload, window size or mode change. Nothing uses it yet. tests/up_defer_check.py builds tests/up_defer_check.c with the header as C and as C++ (vk_ffx.cpp and vk_ffx4.cpp include it) with -Wall -Wextra -Werror and checks both gates, the order, a full list, a null handle and a release that puts. It passed, and failed with each of three faults put into a copy of the header. Co-Authored-By: Claude Opus 5.5 --- docs/building.md | 5 +++ runtime/vk_up_defer.h | 78 ++++++++++++++++++++++++++++++++++++++++ tests/up_defer_check.c | 80 +++++++++++++++++++++++++++++++++++++++++ tests/up_defer_check.py | 46 ++++++++++++++++++++++++ 4 files changed, 209 insertions(+) create mode 100644 runtime/vk_up_defer.h create mode 100644 tests/up_defer_check.c create mode 100644 tests/up_defer_check.py diff --git a/docs/building.md b/docs/building.md index 29d4aa2b..e9d91bd2 100644 --- a/docs/building.md +++ b/docs/building.md @@ -805,6 +805,11 @@ DLL, and passes a DLL with every export `nx2_sl.h` declares; that every function `tools/make_launcher.py` and `tools/make_kit.py` make that check of their `--dlssg` DLL before they download anything. The DLL itself is checked when `tools/build_dlssg.py` builds it, or with `--check `. No MSVC, no Streamline SDK, no game files. +A change to the list of what the upscalers let go of while the GPU may still use it (`runtime/vk_up_defer.h`) passes +`python tests\up_defer_check.py`, which builds the header alone, as C and as C++, and checks that an entry goes only +once the submission that last named it is done and its present gate is passed, that the ready ones go in the order +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 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 diff --git a/runtime/vk_up_defer.h b/runtime/vk_up_defer.h new file mode 100644 index 00000000..ed617c5f --- /dev/null +++ b/runtime/vk_up_defer.h @@ -0,0 +1,78 @@ +// What an upscaler lets go of while the GPU may still use it (vk_dlaa.c, vk_ffx.cpp, vk_ffx4.cpp): a DLSS feature, an +// FSR upscaling context, or an image frame generation can still read at a present. Each owner keeps a list and +// releases an entry with its own callback, on the thread that makes the upscaler's other calls (vk_dlaa.c's pass +// hooks), once the GPU timeline has reached the value of the last submission that named it (fence; 0: none did) and +// the present count has reached presents (0: no such gate). Nothing here waits: the owner checks at its own calls +// (upd_take), and only a full list or a refused creation falls back to a wait, in the owner. +// A header of its own so that tests/up_defer_check.c checks it without a GPU; vk_ffx.cpp and vk_ffx4.cpp include it as +// C++. +#pragma once +#include + +#define UPD_SLOTS 48 + +typedef struct { + uint64_t fence; // timeline value of the last submission that named it (vk_exec_recording_fence), 0 none + uint64_t presents; // released once the present count has reached this, 0 at once + int kind; // the owner's: what handle and aux are + void *handle, *aux; +} UpdEntry; + +typedef struct { + UpdEntry e[UPD_SLOTS]; + int n; + uint64_t put, released; // so far +} UpdList; + +typedef void (*UpdRelease)(const UpdEntry *e, void *arg); + +// 1 when queued, or nothing to queue (a null handle: a half-made object's missing part); 0 when the list is full, and +// the caller makes room with its fallback wait first. +static inline int upd_put(UpdList *l, int kind, void *handle, void *aux, uint64_t fence, uint64_t presents) { + if (!handle) return 1; + if (l->n >= UPD_SLOTS) return 0; + UpdEntry *e = &l->e[l->n++]; // field by field, which C++ takes as C does + e->fence = fence, e->presents = presents, e->kind = kind, e->handle = handle, e->aux = aux; + l->put++; + return 1; +} + +static inline int upd_ready(const UpdEntry *e, uint64_t done, uint64_t presents) { + return e->fence <= done && e->presents <= presents; +} + +// Releases, in the order they were put, the entries whose fence is done and whose present gate is passed; the others +// stay, in their order. The ready ones leave the list before any callback runs, so a callback may put new entries, +// which wait for the next take. Returns how many were released. +static inline int upd_take(UpdList *l, uint64_t done, uint64_t presents, UpdRelease release, void *arg) { + UpdEntry go[UPD_SLOTS]; + int k = 0, ng = 0; + for (int i = 0; i < l->n; i++) { + if (upd_ready(&l->e[i], done, presents)) go[ng++] = l->e[i]; + else l->e[k++] = l->e[i]; + } + l->n = k; + for (int i = 0; i < ng; i++) release(&go[i], arg); + l->released += (uint64_t)ng; + return ng; +} + +// Every entry whatever its gates: the device is idle and nothing reads them any more (teardown). +static inline int upd_take_all(UpdList *l, UpdRelease release, void *arg) { + return upd_take(l, UINT64_MAX, UINT64_MAX, release, arg); +} + +// The highest fence among the entries (0: none named by a submission), for a fallback wait. +static inline uint64_t upd_max_fence(const UpdList *l) { + uint64_t m = 0; + for (int i = 0; i < l->n; i++) + if (l->e[i].fence > m) m = l->e[i].fence; + return m; +} + +// The entries of kind still waiting, or of every kind for a kind below 0. +static inline int upd_count(const UpdList *l, int kind) { + int n = 0; + for (int i = 0; i < l->n; i++) n += kind < 0 || l->e[i].kind == kind; + return n; +} diff --git a/tests/up_defer_check.c b/tests/up_defer_check.c new file mode 100644 index 00000000..22cada70 --- /dev/null +++ b/tests/up_defer_check.c @@ -0,0 +1,80 @@ +// runtime/vk_up_defer.h: what the upscalers let go of is released only once the GPU timeline has passed its last +// submission and, for what a present can read, once enough presents went by. Built and run as C and as C++ by +// tests/up_defer_check.py; prints each failed check and exits 1 on any. +#include +#include + +#include "vk_up_defer.h" + +static int fails; +#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0) + +static int got[64], ngot; +static UpdList *reenter; // a callback that puts another entry (a release that lets go of something else) +static void rel(const UpdEntry *e, void *arg) { + (void)arg; + got[ngot++] = (int)(intptr_t)e->handle; + if (reenter && e->kind == 9) upd_put(reenter, 1, (void *)(intptr_t)99, NULL, 0, 0); +} +static void *h(int i) { return (void *)(intptr_t)i; } + +int main(void) { + UpdList l; + memset(&l, 0, sizeof l); + // a DLSS feature last evaluated in submission 10, a context last dispatched in 12, a never-used one (fence 0) + CHECK(upd_put(&l, 1, h(1), NULL, 10, 0) && upd_put(&l, 2, h(2), NULL, 12, 0) && upd_put(&l, 2, h(3), NULL, 0, 0), + "puts into an empty list"); + CHECK(upd_put(&l, 1, NULL, NULL, 5, 0) && l.n == 3, "a null handle is nothing to queue, and no failure"); + ngot = 0; + CHECK(upd_take(&l, 9, 0, rel, NULL) == 1 && ngot == 1 && got[0] == 3, "at 9 only the never-used one goes"); + ngot = 0; + CHECK(upd_take(&l, 10, 0, rel, NULL) == 1 && got[0] == 1, "at 10 the feature of 10 goes"); + CHECK(l.n == 1 && l.e[0].handle == h(2), "the context of 12 stays, in place"); + ngot = 0; + CHECK(upd_take(&l, 12, 0, rel, NULL) == 1 && got[0] == 2 && l.n == 0, "at 12 it goes"); + // an image a present may read: the fence alone does not free it, the present count must pass too + upd_put(&l, 3, h(4), NULL, 0, 108); + ngot = 0; + CHECK(upd_take(&l, 1000, 107, rel, NULL) == 0, "the present gate holds at 107"); + CHECK(upd_take(&l, 1000, 108, rel, NULL) == 1 && got[0] == 4, "and opens at 108"); + // both gates: the later one decides + upd_put(&l, 3, h(5), NULL, 50, 200); + CHECK(upd_take(&l, 49, 300, rel, NULL) == 0 && upd_take(&l, 60, 199, rel, NULL) == 0, "both gates must pass"); + ngot = 0; + CHECK(upd_take(&l, 50, 200, rel, NULL) == 1 && got[0] == 5, "both passed"); + // order: the ready ones in the order put, the rest kept in order + for (int i = 0; i < 6; i++) upd_put(&l, 1, h(10 + i), NULL, i % 2 ? 100 : 20, 0); + ngot = 0; + upd_take(&l, 20, 0, rel, NULL); + CHECK(ngot == 3 && got[0] == 10 && got[1] == 12 && got[2] == 14, "ready ones in order (%d)", ngot); + CHECK(l.n == 3 && l.e[0].handle == h(11) && l.e[1].handle == h(13) && l.e[2].handle == h(15), "the rest in order"); + CHECK(upd_max_fence(&l) == 100 && upd_count(&l, 1) == 3 && upd_count(&l, 2) == 0 && upd_count(&l, -1) == 3, + "max fence and counts"); + // take_all: the device is idle, everything goes whatever its gates + upd_put(&l, 3, h(20), NULL, 0, UINT64_MAX - 1); + ngot = 0; + CHECK(upd_take_all(&l, rel, NULL) == 4 && l.n == 0 && got[3] == 20, "take_all empties the list"); + CHECK(upd_max_fence(&l) == 0 && upd_count(&l, -1) == 0, "an empty list has no fence and no entry"); + // full: the caller is told, and nothing is overwritten + int ok = 1; + for (int i = 0; i < UPD_SLOTS; i++) ok &= upd_put(&l, 1, h(100 + i), NULL, 7, 0); + CHECK(ok && !upd_put(&l, 1, h(999), NULL, 7, 0) && l.n == UPD_SLOTS, "a full list refuses"); + CHECK(l.e[UPD_SLOTS - 1].handle == h(100 + UPD_SLOTS - 1), "and keeps its last entry"); + ngot = 0; + upd_take(&l, 7, 0, rel, NULL); + CHECK(ngot == UPD_SLOTS && l.n == 0, "and empties at its fence"); + // a callback may put: the new entry waits for the next take + reenter = &l; + upd_put(&l, 9, h(30), NULL, 1, 0); + ngot = 0; + upd_take(&l, 5, 0, rel, NULL); + CHECK(ngot == 1 && l.n == 1 && l.e[0].handle == h(99), "an entry put by a callback stays for the next take"); + reenter = NULL; + ngot = 0; + upd_take(&l, 5, 0, rel, NULL); + CHECK(ngot == 1 && got[0] == 99, "and goes then"); + CHECK(l.put == l.released, "every entry put was released (%llu, %llu)", (unsigned long long)l.put, + (unsigned long long)l.released); + if (!fails) printf("up_defer_check: passed\n"); + return fails ? 1 : 0; +} diff --git a/tests/up_defer_check.py b/tests/up_defer_check.py new file mode 100644 index 00000000..ea64898a --- /dev/null +++ b/tests/up_defer_check.py @@ -0,0 +1,46 @@ +"""Build and run tests/up_defer_check.c: runtime/vk_up_defer.h, the list of what the upscalers let go of while the GPU +may still use it (vk_dlaa.c, vk_ffx.cpp, vk_ffx4.cpp). Built as C, and as C++ since the two FSR files include the +header from C++. + +No GPU, no game files, any Python. + +Usage: up_defer_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 (as tests/diff/common.py picks it, without its Unicorn).""" + 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' / 'up_defer' + work.mkdir(parents=True, exist_ok=True) + env = dict(os.environ) + if os.name == 'nt' and MSYS_BIN.exists(): + env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '') + src = str(ROOT / 'tests' / 'up_defer_check.c') + rc = 0 + for lang, flags in (('c', ['-std=gnu11']), ('c++', ['-x', 'c++', '-std=gnu++17', '-pedantic'])): + exe = work / (f'up_defer_check_{lang.replace("+", "x")}' + ('.exe' if os.name == 'nt' else '')) + subprocess.check_call([compiler()] + flags + ['-O1', '-Wall', '-Wextra', '-Werror', '-I', str(ROOT / 'runtime'), + src, '-o', str(exe)], env=env) + print(f'up_defer_check: as {lang}:', flush=True) + rc |= subprocess.run([str(exe)], env=env).returncode + return rc + + +if __name__ == '__main__': + sys.exit(main())