diff --git a/docs/architecture.md b/docs/architecture.md index b5a11d4f..ca2dccdd 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -377,7 +377,11 @@ keeps per-core data without locks and relies on that, so the host models it (`ru (a quarter of the video memory budget) stays as the safety net for a burst of area changes, and young textures may pass it, up to twice it, while the card and the system's commit have room. Times are wall seconds from a clock of the presents, not present counts. What a switched-off upscaler or SMAA held is freed after it has gone unused for a - while (`vk_dlaa.c` `idle_release`, `vk_fsr.c` `idle_note`, `vk_aa.c` `work_idle`). + while (`vk_dlaa.c` `idle_release`, `vk_fsr.c` `idle_note`, `vk_aa.c` `work_idle`). What DLSS or FSR lets go of at + a change of size or mode (a network, an FSR context, a work image) goes once the GPU is past the last submission that + used it, and an image frame generation reads at a present some presents later, without a wait for the device on the + replaying thread (`vk_dlaa.c` "what the upscalers let go of", `vk_up_defer.h`; `upscaler_idle_wait = 1` waits as + before). - Translated shaders at rest: the shader memory cache keeps every shader for the whole session, but a shader's SPIR-V is read only while modules are made of it. A background thread compresses (zstd, checked by decoding it) the SPIR-V of each shader no reader used for `shader_compress_idle_s` seconds (30) and frees the plain copy; a reader pins it diff --git a/runtime/vk_dlaa.c b/runtime/vk_dlaa.c index 5bf4105c..425491f8 100644 --- a/runtime/vk_dlaa.c +++ b/runtime/vk_dlaa.c @@ -803,7 +803,9 @@ static void dead_release(const UpdEntry *e, void *teardown) { // What the upscalers let go of goes once the GPU is past it: DLSS's here, FSR's in vk_ffx.cpp and vk_ffx4.cpp. static void upscalers_drain(void) { - if (dead.n) upd_take(&dead, vk_sync_fence_done(), presents_now(), dead_release, NULL); + const uint64_t done = vk_sync_fence_done(); + if (dead.n) upd_take(&dead, done, presents_now(), dead_release, NULL); + vk_ffx_dead_take(done); // returns at once with both of its lists empty } // A wait for the device, then everything the upscalers let go of that a submission made so far names goes. An image a @@ -812,6 +814,7 @@ void vk_upscaler_settle(void) { const uint64_t issued = vk_sync_issued(); // read before the wait: every submission up to it is done after it vk_upscaler_device_idle(); upd_take(&dead, issued, presents_now(), dead_release, NULL); + vk_ffx_dead_take(issued); } void vk_upscaler_let_go(void) { @@ -3054,7 +3057,6 @@ static void retire_idle(void) { if (!retire_off_at) retire_off_at = now ? now : 1; if (now < retire_off_at + RETIRE_PRESENTS || !up_holds()) return; for (int i = 0; i < SNAPS; i++) retire_snap(&R.s[i]); - if (vk_ffx_holds()) vk_upscaler_device_idle(); // vk_ffx_release_idle destroys the FSR contexts at once vk_ffx_release_idle(); if (D.failed) { // as idle_release does for a size: frame generation is handed nothing of one from here on for (int i = 0; i < DLSS_SIZES; i++) retire_size(&D.sizes[i]); diff --git a/runtime/vk_ffx.cpp b/runtime/vk_ffx.cpp index 1b2cf1ef..39d444ec 100644 --- a/runtime/vk_ffx.cpp +++ b/runtime/vk_ffx.cpp @@ -21,12 +21,14 @@ #include "third_party/ffx_api/ffx_api.h" #include "third_party/ffx_api/ffx_upscale.h" #include "third_party/ffx_api/vk/ffx_api_vk.h" +#include "vk_up_defer.h" extern "C" { #include "cfg.h" #include "host.h" #include "platform/platform.h" #include "vk.h" +#include "vk_exec.h" #include "vk_ffx.h" #include "vk_fg.h" } @@ -119,37 +121,58 @@ ffxReturnCode_t create_counted(ffxContext *ctx, ffxCreateContextDescHeader *desc // each dispatch and without that flag, preceded an access violation in AMD's driver inside FSR 4's dispatch at the // first frame after a switch from 67 % to 100 % (vk_ffx4.cpp keeps its contexts the same way). With the game's dynamic // resolution on (NX2NAVI_DYN_RES, dynres.c), whose steps change the render size every few seconds, one context with -// that flag takes them all. A context of another render size goes once no command buffer can still hold it -// (UP_RECENT), so 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 is destroyed for the new -// size once the GPU is idle, as vk_dlaa.c free_size does for DLSS. +// that flag takes them all. A context of another render size goes once it is not among the recent ones (UP_RECENT), so +// 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. 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 bool drs = false; // made for dynamic resolution (FFX_UPSCALE_ENABLE_DYNAMIC_RESOLUTION) uint64_t used = 0, used_ns = 0; // up_tick and the time when context_for last returned it uint64_t vram = 0; // its video memory as the library reported it at its creation, 0 not told + uint64_t fence = 0; // the submission of its last upscale (vk_exec_recording_fence), 0 none } U[4]; uint64_t up_tick; -// A context used in the last few calls may sit in a command buffer that is not submitted yet, where waiting for the -// GPU does not cover it: a frame makes one or two calls (the scene, and a second scene after it), so this is a few -// frames back. +// The contexts used in the last few calls stay: a frame makes one or two calls (the scene, and a second scene after +// it), so this is a few frames back. That is the policy only: a context that goes waits for the GPU through its fence, +// also when the command buffer that names it is not submitted yet. const uint64_t UP_RECENT = 8; // A context no upscale used for this long goes (trim): FSR 3.1's after a switch to FSR 4, and a second scene's (the -// pause screen's) a while after it closed. Unused that long, no command buffer names it. +// pause screen's) a while after it closed. const uint64_t UP_IDLE_NS = 90ull * 1000000000ull; -void destroy_up(Up *u, const char *why) { - rt_log("ffx: the FSR 3.1 context for %ux%u (render sizes up to %ux%u) destroyed: %s", u->ow, u->oh, u->mw, u->mh, why); +// The contexts let go of, destroyed once the GPU is past their last upscale (vk_ffx_dead_take). Only the thread that +// makes the upscales (vk_dlaa.c's pass hooks) touches it; vk_ffx_release at teardown. +UpdList g_dead; +const int DEAD_CTX = 3; // its one kind + +void dead_release(const UpdEntry *e, void *) { uint64_t t0 = perf_mark_begin(); // NX2NAVI_TRACE=perf: "upscaler releases" - A.destroy(&u->ctx, nullptr); + ffxContext c = (ffxContext)e->handle; + A.destroy(&c, nullptr); perf_mark_end(PERF_M_UPREL, t0, 0); +} + +void retire_up(Up *u, const char *why) { + rt_log("ffx: the FSR 3.1 context for %ux%u (render sizes up to %ux%u) let go of (destroyed once the GPU is past its " + "last upscale): %s", u->ow, u->oh, u->mw, u->mh, why); + if (!upd_put(&g_dead, DEAD_CTX, u->ctx, nullptr, u->fence, 0)) { + // full, which the GPU's progress does not let happen: room after a wait for the device, or else all of them go + // as before this list, the device idle + vk_upscaler_settle(); + if (!upd_put(&g_dead, DEAD_CTX, u->ctx, nullptr, u->fence, 0)) { + upd_take_all(&g_dead, dead_release, nullptr); + upd_put(&g_dead, DEAD_CTX, u->ctx, nullptr, u->fence, 0); + } + } *u = Up(); } void trim(uint64_t now_ns) { for (Up &u : U) - if (u.ctx && now_ns - u.used_ns > UP_IDLE_NS) destroy_up(&u, "no upscale used it for 90 s"); + if (u.ctx && now_ns - u.used_ns > UP_IDLE_NS) retire_up(&u, "no upscale used it for 90 s"); } FfxApiResource image(const VkFfxImage *i, uint32_t state, uint32_t usage) { @@ -194,12 +217,11 @@ Up *context_for(uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) { if (u.ctx && u.ow == ow && u.oh == oh && u.drs == drs && (drs ? w <= u.mw && h <= u.mh : w == u.mw && h == u.mh)) found = &u; const VkCtx *c = vk_ctx(); - int waited = 0; - for (Up &u : U) // the contexts of this output size for another render size, once no command buffer holds them - if (u.ctx && &u != found && u.ow == ow && u.oh == oh && up_tick - u.used >= UP_RECENT) { - if (!waited++) vk_upscaler_device_idle(); // their work may be in flight - destroy_up(&u, "the render size changed"); - } + int gone = 0; + for (Up &u : U) // the contexts of this output size for another render size, once they are not recent + if (u.ctx && &u != found && u.ow == ow && u.oh == oh && up_tick - u.used >= UP_RECENT) + retire_up(&u, "the render size changed"), gone++; // their work may be in flight + if (gone) vk_upscaler_let_go(); if (found) { found->used = up_tick, found->used_ns = now; return found; @@ -213,8 +235,8 @@ Up *context_for(uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) { if (up_tick - u.used >= UP_RECENT && (!lru || u.used < lru->used)) lru = &u; } if (!slot && lru) { - vk_upscaler_device_idle(); // its work may still be in flight - destroy_up(lru, "its slot went to another output size"); + retire_up(lru, "its slot went to another output size"); // its work may still be in flight + vk_upscaler_let_go(); slot = lru; } if (!slot) return nullptr; @@ -315,10 +337,20 @@ extern "C" int vk_ffx_fsr4(void) { return cfg_bool("NX2NAVI_FSR4") && vk_ffx4_av extern "C" int vk_ffx_trim_idle(uint64_t recent) { int n = 0; for (Up &u : U) - if (u.ctx && up_tick - u.used >= recent) destroy_up(&u, "room for an FSR 4 context"), n++; + if (u.ctx && up_tick - u.used >= recent) retire_up(&u, "room for an FSR 4 context"), n++; return n; } +extern "C" int vk_ffx_dead_take(uint64_t done) { + int n = g_dead.n ? upd_take(&g_dead, done, UINT64_MAX, dead_release, nullptr) : 0; + return n + vk_ffx4_dead_take(done); +} + +extern "C" uint64_t vk_ffx_dead_fence(void) { + uint64_t a = upd_max_fence(&g_dead), b = vk_ffx4_dead_fence(); + return a > b ? a : b; +} + extern "C" int vk_ffx_upscale(VkCommandBuffer cb, const VkFfxImage *color, const VkFfxImage *depth, const VkFfxImage *mv, const VkFfxImage *out, float jx, float jy, int reset, float dt_ms, const VkFfxCamera *cam) { if (!vk_ffx_load()) return 0; @@ -370,6 +402,7 @@ extern "C" int vk_ffx_upscale(VkCommandBuffer cb, const VkFfxImage *color, const d.cameraFovAngleVertical = cam && cam->fov_y > 0 ? cam->fov_y : 1.0f; d.viewSpaceToMetersFactor = cam && cam->meters > 0 ? cam->meters : 1.0f; ffxReturnCode_t r = A.dispatch(&u->ctx, &d.header); + u->fence = vk_exec_recording_fence(); // recorded into cb (a refusal too, for all this side knows) if (r != FFX_API_RETURN_OK) { static int told; if (told++ < 8) rt_log("ffx: FSR 3.1 dispatch failed (%u)", (unsigned)r); @@ -440,25 +473,28 @@ extern "C" ffxReturnCode_t vk_ffx_dispatch(ffxContext *ctx, const ffxDispatchDes return A.ok ? A.dispatch(ctx, desc) : FFX_API_RETURN_ERROR; } -// The upscaler is off (vk_dlaa.c retire_idle; the device is idle): FSR 3.1's and FSR 4's upscale contexts go, made again -// at the next upscale. Frame generation's contexts are not these. +// The upscaler is off (vk_dlaa.c retire_idle): FSR 3.1's and FSR 4's upscale contexts are let go of, made again at the +// next upscale; the caller's vk_upscaler_let_go frees them once the GPU is past them. Frame generation's contexts are +// not these. extern "C" int vk_ffx_holds(void) { for (Up &u : U) if (u.ctx) return 1; return vk_ffx4_holds(); } extern "C" void vk_ffx_release_idle(void) { - vk_ffx4_release(); + vk_ffx4_release_idle(); if (!A.ok) return; for (Up &u : U) - if (u.ctx) destroy_up(&u, "the upscaler is off"); + if (u.ctx) retire_up(&u, "the upscaler is off"); } +// Teardown and the probe: the device is idle. extern "C" void vk_ffx_release(void) { vk_ffx4_release(); if (!A.ok) return; for (Up &u : U) if (u.ctx) A.destroy(&u.ctx, nullptr), u.ctx = nullptr; + upd_take_all(&g_dead, dead_release, nullptr); } // The memory report's FSR figures (vk_memory.c outside_log): the video memory of the upscaling contexts that exist, diff --git a/runtime/vk_ffx.h b/runtime/vk_ffx.h index f1e554eb..ad1c5796 100644 --- a/runtime/vk_ffx.h +++ b/runtime/vk_ffx.h @@ -44,13 +44,18 @@ int vk_ffx_framegen_available(void); // frame's camera. 0 on failure (the caller falls back as it does when DLSS fails). int vk_ffx_upscale(VkCommandBuffer cb, const VkFfxImage *color, const VkFfxImage *depth, const VkFfxImage *mv, const VkFfxImage *out, float jx, float jy, int reset, float dt_ms, const VkFfxCamera *cam); -void vk_ffx_release(void); // before the device goes away -// The upscale contexts of FSR 3.1 and FSR 4 (vk_dlaa.c retire_idle, the device idle): any held, and give them back. +void vk_ffx_release(void); // before the device goes away (the device idle) +// The upscale contexts of FSR 3.1 and FSR 4 (vk_dlaa.c retire_idle): any held, and let them all go (the caller's +// vk_upscaler_let_go frees them once the GPU is past them). int vk_ffx_holds(void); void vk_ffx_release_idle(void); -// FSR 3.1's contexts that no upscale used in the last `recent` calls, destroyed with the device idle (the caller waited -// for it): room for an FSR 4 context whose creation failed. Their count. +// FSR 3.1's contexts that no upscale used in the last `recent` calls, let go of: room for an FSR 4 context whose +// creation failed, whose caller makes their memory free (vk_ffx_dead_take after vk_upscaler_gpu_wait). Their count. int vk_ffx_trim_idle(uint64_t recent); +// The FSR 3.1 and FSR 4 contexts let go of: those whose last upscale's submission is at most done are destroyed (their +// count); the highest such submission among those left (0: none). +int vk_ffx_dead_take(uint64_t done); +uint64_t vk_ffx_dead_fence(void); // The device's functions as the library is given them (ffxCreateBackendVKDesc.vkDeviceProcAddr): its pipeline creates // go through the runtime's pipeline caches (vk_pipeline.c vk_pipeline_create_foreign; NX2NAVI_FFX_PIPE_CACHE=0: not). VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vk_ffx_proc_addr(VkDevice dev, const char *name); @@ -66,6 +71,9 @@ int vk_ffx4_upscale(VkCommandBuffer cb, const VkFfxImage *color, const VkFfxImag const VkFfxCamera *cam); // as vk_ffx_upscale void vk_ffx4_release(void); int vk_ffx4_holds(void); // an FSR 4 upscale context is made +void vk_ffx4_release_idle(void); // every FSR 4 upscale context let go of (vk_ffx_release_idle) +int vk_ffx4_dead_take(uint64_t done); // FSR 4's part of vk_ffx_dead_take +uint64_t vk_ffx4_dead_fence(void); // FSR 4's part of vk_ffx_dead_fence int vk_ffx4_failed(void); // it failed three times in a row: FSR 3.1 serves until vk_ffx4_retry void vk_ffx4_retry(void); // the menu chose FSR 4 again: try it anew after such failures void vk_ffx4_trim(uint64_t now_ns); // at each upscale: FSR 4's contexts unused for a while go diff --git a/runtime/vk_ffx4.cpp b/runtime/vk_ffx4.cpp index c5862695..049e9744 100644 --- a/runtime/vk_ffx4.cpp +++ b/runtime/vk_ffx4.cpp @@ -33,6 +33,9 @@ extern "C" int vk_ffx4_failed(void) { return 0; } extern "C" void vk_ffx4_retry(void) {} extern "C" void vk_ffx4_trim(uint64_t) {} extern "C" uint64_t vk_ffx4_vram(int *contexts) { return *contexts = 0, 0; } +extern "C" void vk_ffx4_release_idle(void) {} +extern "C" int vk_ffx4_dead_take(uint64_t) { return 0; } +extern "C" uint64_t vk_ffx4_dead_fence(void) { return 0; } #else #include #include @@ -45,12 +48,14 @@ extern "C" uint64_t vk_ffx4_vram(int *contexts) { return *contexts = 0, 0; } #include "third_party/ffx_sdk/upscalers/include/ffx_upscale.h" #include "vk_ffx4_api.h" +#include "vk_up_defer.h" extern "C" { #include "cfg.h" #include "host.h" #include "platform/platform.h" #include "vk.h" +#include "vk_exec.h" #include "vk_ffx.h" } @@ -187,29 +192,57 @@ int probe(void) { // beside FSR 3.1's and three creations that failed (the card out of memory) turned FSR 4 off for the run; one context // for every render size, as next, preceded an access violation in AMD's driver inside the dispatch after a switch from // 67 % to 100 %. The descriptors a context was made from stay with it, as the API asks ("must remain live until -// ffxDestroyContext"). +// ffxDestroyContext"): in a Made of its own on the heap, so that a context let go of (retire_up) keeps them until it is +// destroyed, once the GPU is past its last upscale (g_dead), while its slot already holds the next one. +struct Made { + ffxCreateContextDescUpscale desc; + ffxCreateBackendVKDesc backend; + ffxOverrideVersion version; + ffxCreateContextDescUpscaleVersion built; +}; 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 bool drs = false; // made for dynamic resolution (FFX_UPSCALE_ENABLE_DYNAMIC_RESOLUTION) uint64_t used = 0, used_ns = 0; // up_tick and the time when context_for last returned it - ffxCreateContextDescUpscale desc = {}; - ffxCreateBackendVKDesc backend = {}; - ffxOverrideVersion version = {}; - ffxCreateContextDescUpscaleVersion built = {}; + Made *made = nullptr; // the descriptors it was made from (calloc), freed after its destroy uint64_t vram = 0; // its video memory as the library reported it at its creation + uint64_t fence = 0; // the submission of its last upscale (vk_exec_recording_fence), 0 none } U[4]; uint64_t up_tick; -const uint64_t UP_RECENT = 8; // contexts used this recently may sit in a command buffer not submitted yet +// Contexts used this recently stay (as vk_ffx.cpp's UP_RECENT): the policy only, as one that goes waits for the GPU +// through its fence. +const uint64_t UP_RECENT = 8; // A context no upscale used for this long goes (vk_ffx4_trim): FSR 4's after a switch to FSR 3.1, and a second scene's -// (the pause screen's) a while after it closed. Unused that long, no command buffer names it. +// (the pause screen's) a while after it closed. const uint64_t UP_IDLE_NS = 90ull * 1000000000ull; -void destroy_up(Up *u, const char *why) { - rt_log("ffx4: the %ux%u context (render sizes up to %ux%u) destroyed: %s", u->ow, u->oh, u->mw, u->mh, why); +// The contexts let go of, destroyed once the GPU is past their last upscale (vk_ffx4_dead_take): handle the context, +// aux its Made. Only the thread that makes the upscales (vk_dlaa.c's pass hooks) touches it; vk_ffx4_release at +// teardown. +UpdList g_dead; +const int DEAD_CTX4 = 4; // its one kind + +void dead_release(const UpdEntry *e, void *) { uint64_t t0 = perf_mark_begin(); // NX2NAVI_TRACE=perf: "upscaler releases" - A.destroy(&u->ctx, nullptr); + ffxContext c = (ffxContext)e->handle; + A.destroy(&c, nullptr); + free(e->aux); perf_mark_end(PERF_M_UPREL, t0, 0); +} + +void retire_up(Up *u, const char *why) { + rt_log("ffx4: the %ux%u context (render sizes up to %ux%u) let go of (destroyed once the GPU is past its last " + "upscale): %s", u->ow, u->oh, u->mw, u->mh, why); + if (!upd_put(&g_dead, DEAD_CTX4, u->ctx, u->made, u->fence, 0)) { + // full, which the GPU's progress does not let happen: room after a wait for the device, or else all of them go + // as before this list, the device idle + vk_upscaler_settle(); + if (!upd_put(&g_dead, DEAD_CTX4, u->ctx, u->made, u->fence, 0)) { + upd_take_all(&g_dead, dead_release, nullptr); + upd_put(&g_dead, DEAD_CTX4, u->ctx, u->made, u->fence, 0); + } + } *u = Up(); } @@ -229,32 +262,39 @@ FfxApiResource image(const VkFfxImage *i, uint32_t state, uint32_t usage) { Up *create(Up *slot, uint32_t mw, uint32_t mh, uint32_t ow, uint32_t oh, bool drs) { const VkCtx *c = vk_ctx(); *slot = Up(); - slot->built.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE_VERSION; - slot->built.version = FFX_UPSCALER_VERSION; - slot->version.header.type = FFX_API_DESC_TYPE_OVERRIDE_VERSION; - slot->version.header.pNext = &slot->built.header; - slot->version.versionId = g_version; - slot->backend.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK; - slot->backend.header.pNext = &slot->version.header; - slot->backend.deviceContext = {c->device, c->phys, vkGetDeviceProcAddr}; - slot->desc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE; - slot->desc.header.pNext = &slot->backend.header; - slot->desc.flags = + Made *m = (Made *)calloc(1, sizeof *m); // zeroed, as the descriptors' {} were + if (!m) { + rt_log("ffx4: no memory for the descriptors of a %s context", g_name); + return nullptr; + } + m->built.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE_VERSION; + m->built.version = FFX_UPSCALER_VERSION; + m->version.header.type = FFX_API_DESC_TYPE_OVERRIDE_VERSION; + m->version.header.pNext = &m->built.header; + m->version.versionId = g_version; + m->backend.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK; + m->backend.header.pNext = &m->version.header; + m->backend.deviceContext = {c->device, c->phys, vkGetDeviceProcAddr}; + m->desc.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE; + m->desc.header.pNext = &m->backend.header; + m->desc.flags = FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE; // a finite far plane, as FSR 3.1's // NX2NAVI_FFX_DEBUG=1 (test): the library checks the values of each dispatch and says what it finds (message) - if (rt_knob("FFX_DEBUG", "0")) slot->desc.flags |= FFX_UPSCALE_ENABLE_DEBUG_CHECKING; - if (drs) slot->desc.flags |= FFX_UPSCALE_ENABLE_DYNAMIC_RESOLUTION; - slot->desc.maxRenderSize = {mw, mh}; - slot->desc.maxUpscaleSize = {ow, oh}; - slot->desc.fpMessage = message; + if (rt_knob("FFX_DEBUG", "0")) m->desc.flags |= FFX_UPSCALE_ENABLE_DEBUG_CHECKING; + if (drs) m->desc.flags |= FFX_UPSCALE_ENABLE_DYNAMIC_RESOLUTION; + m->desc.maxRenderSize = {mw, mh}; + m->desc.maxUpscaleSize = {ow, oh}; + m->desc.fpMessage = message; uint64_t t0 = plat_now_ns(); - ffxReturnCode_t r = A.create(&slot->ctx, &slot->desc.header, nullptr); + ffxReturnCode_t r = A.create(&slot->ctx, &m->desc.header, nullptr); if (r != FFX_API_RETURN_OK || !slot->ctx) { rt_log("ffx4: %s context creation for %ux%u (render sizes up to %ux%u) failed (%u)", g_name, ow, oh, mw, mh, (unsigned)r); + free(m); *slot = Up(); return nullptr; } + slot->made = m; slot->mw = mw, slot->mh = mh, slot->ow = ow, slot->oh = oh, slot->drs = drs; ffxQueryGetProviderVersion v = {}; v.header.type = FFX_API_QUERY_DESC_TYPE_GET_PROVIDER_VERSION; @@ -271,7 +311,8 @@ Up *create(Up *slot, uint32_t mw, uint32_t mh, uint32_t ow, uint32_t oh, bool dr // The provider is asked for the version it runs: anything but a 4.x here means the loader gave another upscaler if (v.versionName && atoi(v.versionName) < 4) { rt_log("ffx4: the context runs FSR %s, not FSR 4: FSR 4 off", v.versionName); - A.destroy(&slot->ctx, nullptr); + A.destroy(&slot->ctx, nullptr); // never used by the GPU + free(m); *slot = Up(); return nullptr; } @@ -279,9 +320,10 @@ Up *create(Up *slot, uint32_t mw, uint32_t mh, uint32_t ow, uint32_t oh, bool dr } // The context for a render size and an output size: the one made for them, else a new one, in a free slot or in place -// of the least recently used one, with the device idle first; the others of that output size go once no command -// buffer can hold them. A creation that fails is tried once more after every context not in use has gone, FSR 3.1's -// idle ones too (vk_ffx_trim_idle): what it lacked was most often video memory. +// of the least recently used one; the others of that output size go once they are not recent. What goes is let go of +// and destroyed once the GPU is past it (retire_up). A creation that fails is tried once more after every context not +// in use has gone, FSR 3.1's idle ones too (vk_ffx_trim_idle), and the GPU has finished with them and with those let go +// of before: what it lacked was most often video memory. Up *context_for(uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) { up_tick++; uint64_t now = plat_now_ns(); @@ -290,12 +332,11 @@ Up *context_for(uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) { for (Up &u : U) if (u.ctx && u.ow == ow && u.oh == oh && u.drs == drs && (drs ? w <= u.mw && h <= u.mh : w == u.mw && h == u.mh)) found = &u; - int waited = 0; - for (Up &u : U) // the contexts of this output size for another render size, once no command buffer holds them - if (u.ctx && &u != found && u.ow == ow && u.oh == oh && up_tick - u.used >= UP_RECENT) { - if (!waited++) vk_upscaler_device_idle(); // their work may be in flight - destroy_up(&u, "the render size changed"); - } + int gone = 0; + for (Up &u : U) // the contexts of this output size for another render size, once they are not recent + if (u.ctx && &u != found && u.ow == ow && u.oh == oh && up_tick - u.used >= UP_RECENT) + retire_up(&u, "the render size changed"), gone++; // their work may be in flight + if (gone) vk_upscaler_let_go(); if (found) { found->used = up_tick, found->used_ns = now; return found; @@ -309,8 +350,8 @@ Up *context_for(uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) { if (up_tick - u.used >= UP_RECENT && (!lru || u.used < lru->used)) lru = &u; } if (!slot && lru) { - vk_upscaler_device_idle(); // its work may still be in flight - destroy_up(lru, "its slot went to another output size"); + retire_up(lru, "its slot went to another output size"); // its work may still be in flight + vk_upscaler_let_go(); slot = lru; } if (!slot) return nullptr; @@ -319,11 +360,13 @@ Up *context_for(uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) { Up *u = create(slot, mw, mh, ow, oh, drs); if (!u) { int freed = 0; - vk_upscaler_device_idle(); for (Up &o : U) - if (o.ctx && up_tick - o.used >= UP_RECENT) destroy_up(&o, "room for a context whose creation failed"), freed++; + if (o.ctx && up_tick - o.used >= UP_RECENT) retire_up(&o, "room for a context whose creation failed"), freed++; freed += vk_ffx_trim_idle(UP_RECENT); - if (freed) u = create(slot, mw, mh, ow, oh, drs); + // the creation needs their memory now: a wait for the GPU to pass them all, this file's and FSR 3.1's (with + // NX2NAVI_UPSCALER_IDLE_WAIT=1 for the device, as before), and they go + const int dead = vk_ffx_dead_take(vk_upscaler_gpu_wait(vk_ffx_dead_fence())); + if (freed || dead) u = create(slot, mw, mh, ow, oh, drs); } if (u) u->used = up_tick, u->used_ns = now; return u; @@ -366,7 +409,7 @@ extern "C" void vk_ffx4_retry(void) { // Called at every upscale, with FSR 4 or FSR 3.1 (vk_ffx.cpp vk_ffx_upscale), on the thread that records them. extern "C" void vk_ffx4_trim(uint64_t now_ns) { for (Up &u : U) - if (u.ctx && now_ns - u.used_ns > UP_IDLE_NS) destroy_up(&u, "no upscale used it for 90 s"); + if (u.ctx && now_ns - u.used_ns > UP_IDLE_NS) retire_up(&u, "no upscale used it for 90 s"); } extern "C" int vk_ffx4_upscale(VkCommandBuffer cb, const VkFfxImage *color, const VkFfxImage *depth, const VkFfxImage *mv, @@ -404,6 +447,7 @@ extern "C" int vk_ffx4_upscale(VkCommandBuffer cb, const VkFfxImage *color, cons d.cameraFovAngleVertical = cam && cam->fov_y > 0 ? cam->fov_y : 1.0f; d.viewSpaceToMetersFactor = cam && cam->meters > 0 ? cam->meters : 1.0f; ffxReturnCode_t r = A.dispatch(&u->ctx, &d.header); + u->fence = vk_exec_recording_fence(); // recorded into cb (a refusal too, for all this side knows) if (r != FFX_API_RETURN_OK) { static int told; if (told++ < 8) rt_log("ffx4: %s dispatch failed (%u)", g_name, (unsigned)r); @@ -445,12 +489,26 @@ extern "C" int vk_ffx4_holds(void) { return 0; } +// Teardown and the probe: the device is idle. extern "C" void vk_ffx4_release(void) { if (!A.destroy) return; for (Up &u : U) - if (u.ctx) A.destroy(&u.ctx, nullptr), u = Up(); + if (u.ctx) A.destroy(&u.ctx, nullptr), free(u.made), u = Up(); + upd_take_all(&g_dead, dead_release, nullptr); } +// The upscaler is off (vk_ffx.cpp vk_ffx_release_idle): every context is let go of. +extern "C" void vk_ffx4_release_idle(void) { + for (Up &u : U) + if (u.ctx) retire_up(&u, "the upscaler is off"); +} + +extern "C" int vk_ffx4_dead_take(uint64_t done) { + return g_dead.n ? upd_take(&g_dead, done, UINT64_MAX, dead_release, nullptr) : 0; +} + +extern "C" uint64_t vk_ffx4_dead_fence(void) { return upd_max_fence(&g_dead); } + // FSR 4's upscaling contexts that exist and their video memory, for the memory report (vk_ffx.cpp vk_ffx_vram). extern "C" uint64_t vk_ffx4_vram(int *contexts) { uint64_t bytes = 0;