mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
vk_ffx.cpp context_for (two places) and vk_ffx4.cpp context_for (three) waited for the whole device to be idle, with the queue lock held, on the thread that replays the game's submissions, before they destroyed an upscale context of another render size, the least recently used one for a new output size, or (FSR 4) every idle one after a refused creation: a stall of the present's flip, the completion thread and upload submits at every pause-screen size, reload, window size, dynamic resolution step or mode change with FSR on. A context that goes is now let go of (retire_up): it goes into a list (vk_up_defer.h) with the timeline value of the submission of its last upscale (Up.fence, set after each dispatch) and is destroyed on the same thread once the GPU is past it, at the points where vk_dlaa.c takes its own list (every evaluation, every pass while none runs, and after something is let go of). The 90-second trims, vk_ffx_trim_idle and vk_ffx_release_idle (the upscaler off, which no longer needs retire_idle's wait) let go of theirs the same way. FSR 4's create descriptors, which the API asks to stay live until the context is destroyed, move from the slot into a calloc'd Made that goes with the context, so the slot can take a new context while the old one waits. A refused FSR 4 creation now waits on the timeline from the host for the contexts let go of, no queue touched (vk_upscaler_gpu_wait), and tries again when any went; with NX2NAVI_UPSCALER_IDLE_WAIT=1 that wait and every let go wait for the device as before. docs/architecture.md says where the upscalers' releases go. C-5 is narrowed: on the default path no upscaler waits for the device any more, so these waits no longer touch the frame generation swapchain's own queues; vk_fg.cpp, vk_fg4.cpp, gfx_present.c, vk_hdr.c, vk_fsr.c and vk_color.c keep theirs. Not reproduced before the change (no FSR session log here). Every runtime file compiles, vk_dlaa.c, vk_ffx.cpp and vk_ffx4.cpp with -Wall -Wextra and no warning; vk_ffx4.cpp's non-Windows stubs compile against vk_ffx.h; up_defer_check passes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
522 lines
25 KiB
C++
522 lines
25 KiB
C++
// AMD FSR 4 (FSR Upscaling 4.x, the machine-learned upscaler) through the libraries of AMD's FSR SDK 2.4:
|
|
// amd_fidelityfx_loader_vk.dll and amd_fidelityfx_upscaler_vk.dll next to the executable, loaded at run time. The SDK's
|
|
// headers are in third_party/ffx_sdk (MIT), at the paths they have in the SDK. FSR 3.1 and frame generation stay with
|
|
// vk_ffx.cpp and the library of SDK 1.1.4: the two sets of headers name the same types with other layouts, so nothing
|
|
// of the API crosses between the two files, and vk_ffx.cpp's vk_ffx_upscale calls in here first.
|
|
//
|
|
// The loader answers which upscaler versions it can run on a device (ffxQueryDescGetVersions with the device given):
|
|
// 4.1.2, 3.1.6 and 2.3.4 on a Radeon the ML upscaler supports (RX 6000, 7000 and 9000 cards and some Ryzen APUs, with
|
|
// AMD's driver 26.10.1 or newer on Windows), 3.1.6 and 2.3.4 on any other. FSR 4 runs only where a 4.x version is
|
|
// listed, and its contexts are made with that version asked for by its id, so the loader cannot hand out another.
|
|
// Where it is not listed, or a context or a dispatch fails, vk_ffx.cpp's FSR 3.1 serves the frame as before.
|
|
//
|
|
// The inputs are the ones FSR 3.1 takes (vk_ffx.cpp): the scene's light target in linear HDR, its D32F depth (not
|
|
// inverted, the far plane at infinity) and the camera motion vectors in render pixels from this frame to the previous
|
|
// one. FSR 4 needs no reactive or transparency mask.
|
|
#define VK_FFX_NO_API // vk_ffx.h: without the API declarations of the other set of headers
|
|
#ifndef _WIN32
|
|
// AMD publishes the SDK's libraries for Windows only: in any other build FSR 4 is never listed, vk_ffx.cpp's FSR 3.1
|
|
// path is all there is, and the SDK's headers are not read.
|
|
extern "C" {
|
|
#include "vk.h"
|
|
#include "vk_ffx.h"
|
|
}
|
|
extern "C" int vk_ffx4_available(void) { return 0; }
|
|
extern "C" const char *vk_ffx4_name(void) { return "AMD FSR 4"; }
|
|
extern "C" int vk_ffx4_upscale(VkCommandBuffer, const VkFfxImage *, const VkFfxImage *, const VkFfxImage *, const VkFfxImage *,
|
|
float, float, int, float, const VkFfxCamera *) {
|
|
return 0;
|
|
}
|
|
extern "C" void vk_ffx4_release(void) {}
|
|
extern "C" int vk_ffx4_holds(void) { return 0; }
|
|
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 <windows.h>
|
|
#include <atomic>
|
|
#include <mutex>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <wchar.h>
|
|
|
|
#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"
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct Api {
|
|
PfnFfxCreateContext create = nullptr;
|
|
PfnFfxDestroyContext destroy = nullptr;
|
|
PfnFfxConfigure configure = nullptr;
|
|
PfnFfxQuery query = nullptr;
|
|
PfnFfxDispatch dispatch = nullptr;
|
|
} A;
|
|
|
|
std::mutex g_lock; // the probe: the menu asks from its thread, the replay from its own
|
|
std::atomic<int> g_state{-1}; // -1 not probed; 0 FSR 4 does not run here; 1 it does
|
|
std::atomic<int> g_off{0}; // FAIL_MAX failures in a row: FSR 3.1 serves until the menu asks again (vk_ffx4_retry)
|
|
uint64_t g_version; // the id of the 4.x version the loader listed for the device
|
|
char g_name[48] = "AMD FSR 4"; // "AMD FSR 4.1.2", for the menu and the log
|
|
int g_failures; // contexts or dispatches that failed in a row
|
|
const int FAIL_MAX = 4;
|
|
// After a context creation failed, FSR 3.1 serves the frames until the next try this much later: a save loading fills
|
|
// the video memory with the area's textures for a few frames, and three tries in three frames there turned FSR 4 off.
|
|
uint64_t g_retry_ns;
|
|
const uint64_t RETRY_NS = 2ull * 1000000000ull;
|
|
|
|
void message(uint32_t type, const wchar_t *msg) {
|
|
char s[512];
|
|
size_t n = 0;
|
|
for (; msg && msg[n] && n < sizeof s - 1; n++) s[n] = msg[n] < 128 ? (char)msg[n] : '?';
|
|
s[n] = 0;
|
|
static int told;
|
|
if (told++ < 40) rt_log("ffx4: %s %s", type == FFX_API_MESSAGE_TYPE_ERROR ? "error:" : "warning:", s);
|
|
}
|
|
|
|
// a library of the SDK next to the executable; 0 when the path does not fit
|
|
int beside_exe(const char *name, char *path, size_t size) {
|
|
DWORD n = GetModuleFileNameA(NULL, path, (DWORD)size);
|
|
while (n && path[n - 1] != '\\') n--;
|
|
return snprintf(path + n, size - n, "%s", name) < (int)(size - n);
|
|
}
|
|
|
|
int file_there(const char *path) {
|
|
DWORD attr = GetFileAttributesA(path);
|
|
return attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY);
|
|
}
|
|
|
|
int g_loader = -1; // under g_lock: -1 not tried, 0 the loader is not there or does not load, 1 its entry points are in A
|
|
|
|
// Loads the SDK's loader from beside the executable. Under g_lock, once the device exists (-1 before).
|
|
int load(void) {
|
|
if (g_loader >= 0) return g_loader;
|
|
const VkCtx *c = vk_ctx();
|
|
if (!c || !c->device) return -1; // asked before the device: again later
|
|
char loader[MAX_PATH];
|
|
if (!beside_exe("amd_fidelityfx_loader_vk.dll", loader, sizeof loader) || !file_there(loader))
|
|
return g_loader = 0; // not shipped beside this build: nothing is said, FSR 3.1 is what the menu offers
|
|
HMODULE m = LoadLibraryA(loader);
|
|
if (!m) {
|
|
rt_log("ffx4: %s does not load (error %lu): FSR 4 off", loader, (unsigned long)GetLastError());
|
|
return g_loader = 0;
|
|
}
|
|
A.create = (PfnFfxCreateContext)(void *)GetProcAddress(m, "ffxCreateContext");
|
|
A.destroy = (PfnFfxDestroyContext)(void *)GetProcAddress(m, "ffxDestroyContext");
|
|
A.configure = (PfnFfxConfigure)(void *)GetProcAddress(m, "ffxConfigure");
|
|
A.query = (PfnFfxQuery)(void *)GetProcAddress(m, "ffxQuery");
|
|
A.dispatch = (PfnFfxDispatch)(void *)GetProcAddress(m, "ffxDispatch");
|
|
if (!A.create || !A.destroy || !A.configure || !A.query || !A.dispatch) {
|
|
rt_log("ffx4: amd_fidelityfx_loader_vk.dll lacks the API entry points: FSR 4 off");
|
|
return g_loader = 0;
|
|
}
|
|
return g_loader = 1;
|
|
}
|
|
|
|
// The versions the loader lists for one kind of context on this device, in its order: their count, at most cap.
|
|
// Under g_lock, the loader loaded.
|
|
int versions(uint64_t type, uint64_t *ids, const char **names, int cap) {
|
|
const VkCtx *c = vk_ctx();
|
|
VkDeviceContext dc = {c->device, c->phys, vkGetDeviceProcAddr};
|
|
uint64_t n = 0;
|
|
ffxQueryDescGetVersions q = {};
|
|
q.header.type = FFX_API_QUERY_DESC_TYPE_GET_VERSIONS;
|
|
q.createDescType = type;
|
|
q.device = &dc;
|
|
q.outputCount = &n;
|
|
if (A.query(nullptr, &q.header) != FFX_API_RETURN_OK || !n) return 0; // the count
|
|
if (n > (uint64_t)cap) n = (uint64_t)cap;
|
|
q.versionIds = ids;
|
|
q.versionNames = names;
|
|
if (A.query(nullptr, &q.header) != FFX_API_RETURN_OK) return 0;
|
|
return (int)n;
|
|
}
|
|
|
|
// Asks the loader for the device's upscaler versions. Under g_lock; -1 before the device exists.
|
|
int probe(void) {
|
|
int loaded = load();
|
|
if (loaded <= 0) return loaded;
|
|
char upscaler[MAX_PATH];
|
|
if (!beside_exe("amd_fidelityfx_upscaler_vk.dll", upscaler, sizeof upscaler) || !file_there(upscaler)) return 0;
|
|
uint64_t ids[16] = {};
|
|
const char *names[16] = {};
|
|
int n = versions(FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE, ids, names, 16);
|
|
if (!n) {
|
|
rt_log("ffx4: the FSR SDK's loader lists no upscaler for this device: FSR 4 off");
|
|
return 0;
|
|
}
|
|
char list[200] = "";
|
|
size_t len = 0;
|
|
int best = -1, best_major = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
const char *nm = names[i] ? names[i] : "?";
|
|
len += (size_t)snprintf(list + len, sizeof list - len, "%s%s", i ? ", " : "", nm);
|
|
if (len >= sizeof list) len = sizeof list - 1;
|
|
int major = atoi(nm); // "4.1.2", and "4.1.2 *" for a version the driver brings
|
|
if (major >= 4 && major > best_major) best = i, best_major = major;
|
|
}
|
|
if (best < 0) {
|
|
rt_log("ffx4: upscaler versions for this device: %s; none is FSR 4 (it needs a Radeon RX 6000, 7000 or 9000 card "
|
|
"or a supported Ryzen APU, and AMD's driver 26.10.1 or newer): FSR 3.1 stays", list);
|
|
return 0;
|
|
}
|
|
g_version = ids[best];
|
|
snprintf(g_name, sizeof g_name, "AMD FSR %.24s", names[best]); // the name is the library's memory: copied
|
|
for (char *p = g_name; *p; p++)
|
|
if (*p == '*') *p = 0; // the driver's mark
|
|
for (size_t k = strlen(g_name); k && g_name[k - 1] == ' '; k--) g_name[k - 1] = 0;
|
|
rt_log("ffx4: upscaler versions for this device: %s; FSR 4 is %s (id %#llx)", list, g_name, (unsigned long long)g_version);
|
|
return 1;
|
|
}
|
|
|
|
// One context per render size and output size, kept as vk_ffx.cpp keeps FSR 3.1's (see there): a new render size
|
|
// makes a context of its own (50 to 70 ms and up to about 217 MiB of video memory) and the one before goes a few frames
|
|
// later, so a mode change costs a creation; with the game's dynamic resolution on one context with
|
|
// FFX_UPSCALE_ENABLE_DYNAMIC_RESOLUTION takes every step. Contexts kept for every mode tried, as once, piled up to six
|
|
// 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"): 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
|
|
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;
|
|
// 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.
|
|
const uint64_t UP_IDLE_NS = 90ull * 1000000000ull;
|
|
|
|
// 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"
|
|
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();
|
|
}
|
|
|
|
FfxApiResource image(const VkFfxImage *i, uint32_t state, uint32_t usage) {
|
|
VkImageCreateInfo ci = {};
|
|
ci.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
|
|
ci.imageType = VK_IMAGE_TYPE_2D;
|
|
ci.format = i->format;
|
|
ci.extent = {i->width, i->height, 1};
|
|
ci.mipLevels = 1;
|
|
ci.arrayLayers = 1;
|
|
ci.usage = i->storage ? VK_IMAGE_USAGE_STORAGE_BIT : 0;
|
|
return ffxApiGetResourceVK((void *)i->image, ffxApiGetImageResourceDescriptionVK(i->image, ci, usage), state);
|
|
}
|
|
|
|
// A context in slot for render sizes up to mw x mh and the output size ow x oh; NULL when it is not made.
|
|
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();
|
|
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")) 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, &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;
|
|
A.query(&slot->ctx, &v.header);
|
|
FfxApiEffectMemoryUsage mem = {};
|
|
ffxQueryDescUpscaleGetGPUMemoryUsage mq = {};
|
|
mq.header.type = FFX_API_QUERY_DESC_TYPE_UPSCALE_GPU_MEMORY_USAGE;
|
|
mq.gpuMemoryUsageUpscaler = &mem;
|
|
A.query(&slot->ctx, &mq.header);
|
|
slot->vram = mem.totalUsageInBytes;
|
|
rt_log("ffx4: FSR %s upscaling to %ux%u from render sizes up to %ux%u, made in %.0f ms, %.0f MiB of video memory",
|
|
v.versionName ? v.versionName : "4", ow, oh, mw, mh, (double)(plat_now_ns() - t0) / 1e6,
|
|
(double)mem.totalUsageInBytes / 1048576.0);
|
|
// 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); // never used by the GPU
|
|
free(m);
|
|
*slot = Up();
|
|
return nullptr;
|
|
}
|
|
return slot;
|
|
}
|
|
|
|
// 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; 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();
|
|
const bool drs = cfg_bool("NX2NAVI_DYN_RES");
|
|
Up *found = nullptr;
|
|
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 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;
|
|
}
|
|
Up *slot = nullptr, *lru = nullptr;
|
|
for (Up &u : U) {
|
|
if (!u.ctx) {
|
|
if (!slot) slot = &u;
|
|
continue;
|
|
}
|
|
if (up_tick - u.used >= UP_RECENT && (!lru || u.used < lru->used)) lru = &u;
|
|
}
|
|
if (!slot && lru) {
|
|
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;
|
|
// the render size itself; for dynamic resolution, every size up to the output's (and a scene at the output size)
|
|
uint32_t mw = !drs ? w : w > ow ? w : ow, mh = !drs ? h : h > oh ? h : oh;
|
|
Up *u = create(slot, mw, mh, ow, oh, drs);
|
|
if (!u) {
|
|
int freed = 0;
|
|
for (Up &o : U)
|
|
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);
|
|
// 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;
|
|
}
|
|
|
|
// Failures in a row: FAIL_MAX of them turn FSR 4 off until the menu asks for it again (vk_ffx4_retry); a success
|
|
// starts the count again, so a creation that failed once in a while does not add up over a session.
|
|
void failed(const char *what, unsigned code) {
|
|
if (++g_failures == FAIL_MAX) {
|
|
rt_log("ffx4: %s failed %d times in a row (last %u): FSR 4 is off, FSR 3.1 takes its place until FSR 4 is "
|
|
"chosen again in the menu", what, FAIL_MAX, code);
|
|
g_off.store(1, std::memory_order_release);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
extern "C" int vk_ffx4_available(void) {
|
|
int s = g_state.load(std::memory_order_acquire);
|
|
if (s >= 0) return s;
|
|
std::lock_guard<std::mutex> hold(g_lock);
|
|
s = g_state.load(std::memory_order_acquire);
|
|
if (s >= 0) return s;
|
|
s = probe();
|
|
if (s >= 0) g_state.store(s, std::memory_order_release);
|
|
return s > 0;
|
|
}
|
|
|
|
extern "C" const char *vk_ffx4_name(void) { return g_name; }
|
|
|
|
extern "C" int vk_ffx4_failed(void) { return g_off.load(std::memory_order_acquire); }
|
|
|
|
// The menu chose FSR 4 again: after failures that turned it off, it is tried anew.
|
|
extern "C" void vk_ffx4_retry(void) {
|
|
if (!g_off.exchange(0, std::memory_order_acq_rel)) return;
|
|
g_failures = 0, g_retry_ns = 0;
|
|
rt_log("ffx4: FSR 4 chosen again in the menu: tried anew");
|
|
}
|
|
|
|
// 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) 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,
|
|
const VkFfxImage *out, float jx, float jy, int reset, float dt_ms, const VkFfxCamera *cam) {
|
|
if (!vk_ffx4_available() || g_off.load(std::memory_order_acquire)) return 0;
|
|
uint64_t now = plat_now_ns();
|
|
if (g_failures && now < g_retry_ns) return 0; // FSR 3.1 serves until the next try
|
|
Up *u = context_for(color->width, color->height, out->width, out->height);
|
|
if (!u) {
|
|
g_retry_ns = now + RETRY_NS;
|
|
failed("making a context", 0);
|
|
return 0;
|
|
}
|
|
ffxDispatchDescUpscale d = {};
|
|
d.header.type = FFX_API_DISPATCH_DESC_TYPE_UPSCALE;
|
|
d.commandList = cb;
|
|
// Every image is in GENERAL, which the back end reads as its unordered-access state, and is left there
|
|
d.color = image(color, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS, 0);
|
|
d.depth = image(depth, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS, 0);
|
|
d.motionVectors = image(mv, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS, 0);
|
|
d.output = image(out, FFX_API_RESOURCE_STATE_UNORDERED_ACCESS, FFX_API_RESOURCE_USAGE_UAV);
|
|
d.jitterOffset = {jx, jy};
|
|
d.motionVectorScale = {1.0f, 1.0f};
|
|
d.renderSize = {color->width, color->height};
|
|
d.upscaleSize = {out->width, out->height};
|
|
int sharp = cfg_int("NX2NAVI_SHARPNESS"); // the slider drives the upscaler's own sharpening, as for FSR 3.1
|
|
d.enableSharpening = sharp > 0;
|
|
d.sharpness = sharp > 100 ? 1.0f : sharp / 100.0f;
|
|
d.frameTimeDelta = dt_ms;
|
|
d.preExposure = 1.0f;
|
|
d.reset = reset != 0;
|
|
// the frame's camera (vk_dlaa.c proj_planes), as FSR 3.1 gets it (vk_ffx.cpp)
|
|
d.cameraNear = cam && cam->znear > 0 ? cam->znear : 0.1f;
|
|
d.cameraFar = cam && cam->zfar > 0 ? cam->zfar : 1e8f;
|
|
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);
|
|
failed("a dispatch", (unsigned)r);
|
|
return 0;
|
|
}
|
|
g_failures = 0;
|
|
return 1;
|
|
}
|
|
|
|
// The loader's calls for vk_fg4.cpp (vk_ffx4_api.h).
|
|
extern "C" int vk_ffx4_api_file(const char *name) {
|
|
char path[MAX_PATH];
|
|
return beside_exe(name, path, sizeof path) && file_there(path);
|
|
}
|
|
extern "C" int vk_ffx4_api_versions(uint64_t create_type, uint64_t *ids, const char **names, int cap) {
|
|
std::lock_guard<std::mutex> hold(g_lock);
|
|
return load() > 0 ? versions(create_type, ids, names, cap) : 0;
|
|
}
|
|
extern "C" ffxReturnCode_t vk_ffx4_api_create(ffxContext *ctx, ffxCreateContextDescHeader *desc) {
|
|
return A.create ? A.create(ctx, desc, nullptr) : FFX_API_RETURN_ERROR;
|
|
}
|
|
extern "C" ffxReturnCode_t vk_ffx4_api_destroy(ffxContext *ctx) {
|
|
return A.destroy ? A.destroy(ctx, nullptr) : FFX_API_RETURN_ERROR;
|
|
}
|
|
extern "C" ffxReturnCode_t vk_ffx4_api_configure(ffxContext *ctx, const ffxConfigureDescHeader *desc) {
|
|
return A.configure ? A.configure(ctx, desc) : FFX_API_RETURN_ERROR;
|
|
}
|
|
extern "C" ffxReturnCode_t vk_ffx4_api_query(ffxContext *ctx, ffxQueryDescHeader *desc) {
|
|
return A.query ? A.query(ctx, desc) : FFX_API_RETURN_ERROR;
|
|
}
|
|
extern "C" ffxReturnCode_t vk_ffx4_api_dispatch(ffxContext *ctx, const ffxDispatchDescHeader *desc) {
|
|
return A.dispatch ? A.dispatch(ctx, desc) : FFX_API_RETURN_ERROR;
|
|
}
|
|
|
|
extern "C" int vk_ffx4_holds(void) {
|
|
for (Up &u : U)
|
|
if (u.ctx) return 1;
|
|
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), 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;
|
|
int n = 0;
|
|
for (Up &u : U)
|
|
if (__atomic_load_n(&u.ctx, __ATOMIC_RELAXED)) n++, bytes += __atomic_load_n(&u.vram, __ATOMIC_RELAXED);
|
|
*contexts = n;
|
|
return bytes;
|
|
}
|
|
#endif // _WIN32
|