From 61f08a177d271785a295bb0ba69539bfe6a044c3 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 19:34:20 -0500 Subject: [PATCH] Try an FSR 3.1 context creation once more after the GPU is past the contexts let go of, instead of failing the run. The review of 09027478 found that FSR 3.1 lost its fallback for a refused creation. Up to 0.10.1, vk_ffx.cpp context_for waited for the device and destroyed the other render size's or the least recently used context before create_counted, so their video memory was free for the new one. Since 09027478 they only go into g_dead, and the drain in vk_upscaler_let_go destroys those whose last upscale the GPU is past. When it is still behind and the library refuses the new context for want of memory, context_for returned nullptr, vk_ffx_upscale 0, and evaluate's dl_fail turned DLAA off for the run. DLSS (evaluate) and FSR 4 (context_for) already wait for the GPU to pass what they let go of and try again. context_for now does the same when the creation is refused and FSR 3.1's or FSR 4's list of contexts let go of is not empty: vk_upscaler_gpu_wait (a timeline wait for those submissions, not the device's idle; the device with upscaler_idle_wait = 1), vk_ffx_dead_take, and a second create_counted when something was destroyed. A line says the refusal, the wait and the outcome. upscaler_idle_check now also fails when vk_dlaa.c evaluate or vk_ffx.cpp or vk_ffx4.cpp context_for has no vk_upscaler_gpu_wait (a scratch copy without the retry fails it), and docs/building.md says so. Not reproduced before the change (found by review). Checks that passed: vk_ffx.cpp compiles with -Wall -Wextra and no warning, upscaler_idle_check, tools/check_content.py --staged. Co-Authored-By: Claude Opus 5.5 --- docs/building.md | 4 +++- runtime/vk_ffx.cpp | 17 ++++++++++++++++- tests/upscaler_idle_check.py | 16 ++++++++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/docs/building.md b/docs/building.md index cd8a2956..6ac588d3 100644 --- a/docs/building.md +++ b/docs/building.md @@ -815,7 +815,9 @@ A change to where the upscalers free what they let go of (`runtime/vk_dlaa.c` "w GPU may still use it", `runtime/vk_ffx.cpp` and `runtime/vk_ffx4.cpp` `context_for`) passes `python tests\upscaler_idle_check.py`, which reads the sources: `vkDeviceWaitIdle` only in `vk_upscaler_device_idle` and the `--probe-ffx-cache` path, that function only behind the rollback switch, a full list and a refused creation, -every place that lets something go calling `vk_upscaler_let_go`, `upscaler_idle_wait`'s `sections[]` entry and +every place that lets something go calling `vk_upscaler_let_go`, every creation of a DLSS feature or an FSR context +(`vk_dlaa.c` `evaluate`, `vk_ffx.cpp` and `vk_ffx4.cpp` `context_for`) trying a refusal once more after a wait for the +GPU to pass what was let go of (`vk_upscaler_gpu_wait`), `upscaler_idle_wait`'s `sections[]` entry and `nx2navi.ini.example` line, a name in `perf.c` for each perf mark kind, and `pb_images` noting the images' last use after it makes them. Any Python, no game files. diff --git a/runtime/vk_ffx.cpp b/runtime/vk_ffx.cpp index 39d444ec..b40c7572 100644 --- a/runtime/vk_ffx.cpp +++ b/runtime/vk_ffx.cpp @@ -125,7 +125,8 @@ ffxReturnCode_t create_counted(ffxContext *ctx, ffxCreateContextDescHeader *desc // a switch keeps at most two of an output size for a few frames, and the game's second scenes, of another output size, // keep their own. When all slots are taken, the least recently used one goes for the new size. A context that goes is // let go of (retire_up) and destroyed once the GPU is past the submission of its last upscale (g_dead, vk_dlaa.c "what -// the upscalers let go of"), without a wait for the device. +// the upscalers let go of"), without a wait for the device; a creation refused while some are still in flight waits +// for the GPU to pass them, and tries once more once they are destroyed (context_for). struct Up { ffxContext ctx = nullptr; uint32_t mw = 0, mh = 0, ow = 0, oh = 0; // the render size it was made for (the largest it takes with drs); the output size @@ -259,6 +260,20 @@ Up *context_for(uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) { d.maxUpscaleSize = {ow, oh}; d.fpMessage = message; ffxReturnCode_t r = create_counted(&slot->ctx, &d.header); + if (r != FFX_API_RETURN_OK && vk_ffx_dead_fence()) { + // Refused while contexts let go of (this file's and FSR 4's) are still in flight: most often their video + // memory, which a wait for the device freed before the creation up to 0.10.1. The GPU finishes with them + // (vk_upscaler_gpu_wait: those submissions only, not the device), they go, and the creation is tried once + // more, as vk_ffx4.cpp context_for and vk_dlaa.c evaluate do, rather than the upscaler failing for the run. + const ffxReturnCode_t r0 = r; + const uint64_t w0 = plat_now_ns(); + const int gone2 = vk_ffx_dead_take(vk_upscaler_gpu_wait(vk_ffx_dead_fence())); + const double waited = (double)(plat_now_ns() - w0) / 1e6; + if (gone2) slot->ctx = nullptr, r = create_counted(&slot->ctx, &d.header); + rt_log("ffx: FSR 3.1 context creation refused (%u) with contexts let go of still in flight: waited %.1f ms for " + "the GPU, %d destroyed, %s", (unsigned)r0, waited, gone2, + !gone2 ? "not tried again" : r == FFX_API_RETURN_OK ? "made at the second try" : "refused again"); + } if (r != FFX_API_RETURN_OK) { rt_log("ffx: FSR 3.1 context creation for %ux%u (render sizes up to %ux%u) failed (%u)", ow, oh, mw, mh, (unsigned)r); slot->ctx = nullptr; diff --git a/tests/upscaler_idle_check.py b/tests/upscaler_idle_check.py index 827d57e1..5fafa5f3 100644 --- a/tests/upscaler_idle_check.py +++ b/tests/upscaler_idle_check.py @@ -10,6 +10,9 @@ It fails when: - one of the places that let something go (vk_dlaa.c free_size, free_snap, stale_sizes, release_kept, hudless_from, pb_images, retire_idle and evaluate, vk_ffx.cpp and vk_ffx4.cpp context_for) does not call vk_upscaler_let_go, which frees what the GPU is past and which the rollback switch turns into a wait for the device; +- one of the places that create a DLSS feature or an FSR context (vk_dlaa.c evaluate, vk_ffx.cpp and vk_ffx4.cpp + context_for) does not call vk_upscaler_gpu_wait: a creation refused while what was let go of is still in flight + then fails for want of its video memory, where the wait for the device of 0.10.1 had freed it first; - NX2NAVI_UPSCALER_IDLE_WAIT has no sections[] entry in cfg.c or no line in nx2navi.ini.example (its known[] name and its metas[] row, and a // that would hide them, are tests/cfg_rows_check.py's); - perf.c's mark_name has another count of names than host.h has PERF_M_* kinds: a kind without a name prints a null @@ -35,6 +38,11 @@ LET_GO = { # functions that let something go and must call vk_upscaler_let_go 'vk_ffx.cpp': ['context_for'], 'vk_ffx4.cpp': ['context_for'], } +REFUSED = { # functions that create a feature or context and must retry a refusal after vk_upscaler_gpu_wait + 'vk_dlaa.c': ['evaluate'], + 'vk_ffx.cpp': ['context_for'], + 'vk_ffx4.cpp': ['context_for'], +} # A function definition: at column 0, a return type, the name, the parameters and the body's brace (the runtime's # style); declarations end with ';' before any '{'. Read in code whose string literals are blanked: extern "C" is # extern " " there. @@ -141,6 +149,14 @@ def main(): fails.append(f'{f}: no function {name}') elif not re.search(r'\bvk_upscaler_let_go\s*\(', b): fails.append(f'{f} {name}: lets something go without vk_upscaler_let_go') + for f, names in REFUSED.items(): + code, funcs = sources[f] + for name in names: + b = body(code, funcs, name) + if b is None: + fails.append(f'{f}: no function {name}') + elif not re.search(r'\bvk_upscaler_gpu_wait\s*\(', b): + fails.append(f'{f} {name}: no retry of a refused creation after vk_upscaler_gpu_wait') key = 'UPSCALER_IDLE_WAIT' cfg = (rt / 'cfg.c').read_text(encoding='utf-8-sig')