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())