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 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-13 19:34:20 -05:00
parent ed87d30628
commit 61f08a177d
3 changed files with 35 additions and 2 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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')