Files
NX2navi/runtime/vk_ffx.cpp
chrg6 ca5e5eb5a4 Add AMD FSR 3.1 upscaling, and run DLSS's smaller modes on a 16:9 picture too.
The Upscaling choice gains FSR 3.1 Native AA, Quality, Balanced, Performance and Ultra Performance. FSR 3.1 runs
through AMD's signed amd_fidelityfx_vk.dll (loaded at run time; the build kit ships it) in DLSS's place, on the same
scene light, depth and camera motion vectors and at the same render scales. Its Vulkan back end needs
VK_KHR_get_memory_requirements2, which the device now enables when FSR 3.1 is chosen.

A half-size effect pass with three targets and a half-size depth was taken for a G-buffer, which switched every later
frame to the deferred frame shape: there the output pass of a smaller scene was never found, so DLSS Quality,
Balanced, Performance and Ultra Performance did nothing on a 16:9 picture. Such a pass smaller than the scene is now
left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:31:06 -05:00

172 lines
6.9 KiB
C++

// AMD FidelityFX through its API library (amd_fidelityfx_vk.dll next to the executable, loaded at run time): FSR 3.1
// temporal upscaling here, in the place of DLSS in vk_dlaa.c, and frame generation (vk_ffx_fg_*, below). The API's
// headers are in third_party/ffx_api (MIT); they are C++ in places, hence this file. Nothing of it runs unless the
// upscaler or frame generation asks for FSR 3.1.
//
// FSR 3.1 takes the same inputs vk_dlaa.c makes for DLSS: the scene's light target (linear HDR), its D32F depth
// (OpenGL-style, an infinite far plane, not inverted) and the camera motion vectors in render pixels from the current
// frame to the previous one, the convention both share, so the vectors go in unscaled.
#ifdef _WIN32
#include <windows.h>
#endif
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <wchar.h>
#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"
extern "C" {
#include "cfg.h"
#include "host.h"
#include "vk.h"
#include "vk_ffx.h"
}
namespace {
struct Api {
void *lib = nullptr;
PfnFfxCreateContext create = nullptr;
PfnFfxDestroyContext destroy = nullptr;
PfnFfxConfigure configure = nullptr;
PfnFfxQuery query = nullptr;
PfnFfxDispatch dispatch = nullptr;
int tried = 0, ok = 0;
} A;
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("ffx: %s %s", type == FFX_API_MESSAGE_TYPE_ERROR ? "error:" : "warning:", s);
}
// One upscaling context per render and output size (the menu switches the mode live, so a few may exist); never
// destroyed before shutdown, so none is released while the GPU may still run its work.
struct Up {
ffxContext ctx = nullptr;
uint32_t w = 0, h = 0, ow = 0, oh = 0;
} U[6];
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);
}
Up *context_for(uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) {
for (Up &u : U)
if (u.ctx && u.w == w && u.h == h && u.ow == ow && u.oh == oh) return &u;
Up *slot = nullptr;
for (Up &u : U)
if (!u.ctx) { slot = &u; break; }
if (!slot) return nullptr;
const VkCtx *c = vk_ctx();
ffxCreateBackendVKDesc be = {};
be.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK;
be.vkDevice = c->device;
be.vkPhysicalDevice = c->phys;
be.vkDeviceProcAddr = vkGetDeviceProcAddr;
ffxCreateContextDescUpscale d = {};
d.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_UPSCALE;
d.header.pNext = &be.header;
d.flags = FFX_UPSCALE_ENABLE_HIGH_DYNAMIC_RANGE | FFX_UPSCALE_ENABLE_DEPTH_INFINITE | FFX_UPSCALE_ENABLE_AUTO_EXPOSURE;
d.maxRenderSize = {w, h};
d.maxUpscaleSize = {ow, oh};
d.fpMessage = message;
ffxReturnCode_t r = A.create(&slot->ctx, &d.header, nullptr);
if (r != FFX_API_RETURN_OK) {
rt_log("ffx: FSR 3.1 context creation at %ux%u -> %ux%u failed (%u)", w, h, ow, oh, (unsigned)r);
slot->ctx = nullptr;
return nullptr;
}
slot->w = w, slot->h = h, slot->ow = ow, slot->oh = oh;
ffxQueryGetProviderVersion v = {};
v.header.type = FFX_API_QUERY_DESC_TYPE_GET_PROVIDER_VERSION;
A.query(&slot->ctx, &v.header);
rt_log("ffx: FSR 3.1 upscaling at %ux%u -> %ux%u (%s)", w, h, ow, oh, v.versionName ? v.versionName : "provider unknown");
return slot;
}
} // namespace
extern "C" int vk_ffx_load(void) {
if (A.tried) return A.ok;
A.tried = 1;
#ifdef _WIN32
char path[MAX_PATH];
DWORD n = GetModuleFileNameA(NULL, path, MAX_PATH);
while (n && path[n - 1] != '\\') n--;
snprintf(path + n, sizeof path - n, "amd_fidelityfx_vk.dll");
HMODULE m = LoadLibraryA(path);
if (!m) {
rt_log("ffx: %s not found: FSR 3.1 off", path);
return 0;
}
A.lib = m;
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");
#endif
A.ok = A.create && A.destroy && A.configure && A.query && A.dispatch;
rt_log("ffx: amd_fidelityfx_vk.dll %s", A.ok ? "loaded" : "lacks the API entry points: FSR 3.1 off");
return A.ok;
}
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, float fov_y) {
if (!vk_ffx_load()) return 0;
Up *u = context_for(color->width, color->height, out->width, out->height);
if (!u) return 0;
ffxDispatchDescUpscale d = {};
d.header.type = FFX_API_DISPATCH_DESC_TYPE_UPSCALE;
d.commandList = cb;
// Every image is in GENERAL, which the backend 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};
// The sharpness slider (0-100) drives FSR's own RCAS here, as it does for FSR 1
int sharp = cfg_int("NX2NAVI_SHARPNESS");
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;
d.cameraNear = 0.1f; // Ocarina's projection (vk_dlaa.c ocam_vp): near 0.1, far at infinity
d.cameraFar = 1e8f;
d.cameraFovAngleVertical = fov_y > 0 ? fov_y : 1.0f;
d.viewSpaceToMetersFactor = 0.01f; // the game's units are centimetres
ffxReturnCode_t r = A.dispatch(&u->ctx, &d.header);
if (r != FFX_API_RETURN_OK) {
static int told;
if (told++ < 8) rt_log("ffx: FSR 3.1 dispatch failed (%u)", (unsigned)r);
return 0;
}
return 1;
}
extern "C" void vk_ffx_release(void) {
if (!A.ok) return;
for (Up &u : U)
if (u.ctx) A.destroy(&u.ctx, nullptr), u.ctx = nullptr;
}