mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
On Linux the Screen tab listed the five FSR 3.1 upscaling modes and AMD's frame generation, but only the Windows build loads amd_fidelityfx_vk.dll: a player who picked a mode got the scene at the mode's share of the picture with nothing to scale it back up. vk_ffx_available answers from the load's result once it ran, and before that from the library file being there, so the menu can ask every frame. Where it says no, the menu lists neither, a mode that is still set shows "in use: off", the scene keeps the game's own share (vk_dlaa_fsr3), frame generation keeps the plain swapchain with one log line, and the texts name what does run. Pull request #371 by mooki000, taken as it is; two hunks merged by hand (vk_fg_wanted, which now also stays off after a failure, and a README line that had gained Texture filtering). It touches vk_dlaa.c, which another session is changing: vk_dlaa_fsr3 gains the availability test and one log text changes. On Windows the menu changes only where the DLL is missing. The Windows files compile; nothing was run. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
285 lines
14 KiB
C++
285 lines
14 KiB
C++
// Frame generation with AMD FSR 3.1 (NX2NAVI_FRAMEGEN = fsr3; applies after a restart): AMD's frame generation
|
|
// swapchain (amd_fidelityfx_vk.dll, vk_ffx.cpp loads it) takes the place of gfx_present.c's swapchain. Every frame
|
|
// the game presents, the swapchain presents a generated frame between it and the one before, on its own present
|
|
// queue and pacing threads, so the screen shows twice the game's frame rate.
|
|
//
|
|
// The generated frame is built from the presented picture and the scene's depth and camera motion vectors, which
|
|
// vk_dlaa.c makes for DLSS and FSR 3.1 upscaling: frame generation needs one of those on (DLAA or Native AA included).
|
|
// The HUD is interpolated with the picture (it hardly moves); the menu is not, since generation pauses while it is
|
|
// open. The device creates two more queues of the graphics family for it (vk_device.c, vk_fg_queue_count).
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
#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_framegeneration.h"
|
|
#include "third_party/ffx_api/vk/ffx_api_vk.h"
|
|
|
|
extern "C" {
|
|
#include "cfg.h"
|
|
#include "host.h"
|
|
#include "platform/platform.h"
|
|
#include "vk.h"
|
|
#include "vk_dlaa.h"
|
|
#include "vk_dlssg.h"
|
|
#include "vk_ffx.h"
|
|
#include "vk_fg.h"
|
|
#include "ui/menu.h"
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct {
|
|
VkQueue present = VK_NULL_HANDLE, acquire = VK_NULL_HANDLE;
|
|
ffxContext sc = nullptr; // the swapchain context
|
|
ffxContext fg = nullptr; // the frame generation context
|
|
VkExtent2D fg_ext = {0, 0};
|
|
VkExtent2D fg_max = {0, 0}; // the context's largest render size
|
|
VkFormat fg_fmt = VK_FORMAT_UNDEFINED;
|
|
ffxQueryDescSwapchainReplacementFunctionsVK fn = {};
|
|
uint64_t frame = 0;
|
|
int failed = 0;
|
|
uint64_t generated = 0, paused = 0, no_inputs = 0, hudless = 0;
|
|
int on = 0; // the last configured frame had generation on (vk_fg_generating)
|
|
} F;
|
|
|
|
VkResult submit_game(uint32_t n, const VkSubmitInfo *si, VkFence fence) {
|
|
vk_queue_lock(); // the game's queue is shared with the recorder and the present path
|
|
VkResult r = vkQueueSubmit(vk_ctx()->gfx_queue, n, si, fence);
|
|
vk_queue_unlock();
|
|
return r;
|
|
}
|
|
|
|
ffxReturnCode_t generate(ffxDispatchDescFrameGeneration *d, void *user) {
|
|
F.generated++;
|
|
return vk_ffx_dispatch((ffxContext *)user, &d->header);
|
|
}
|
|
|
|
FfxApiResource image(const VkFfxImage *i) {
|
|
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;
|
|
return ffxApiGetResourceVK((void *)i->image, ffxApiGetImageResourceDescriptionVK(i->image, ci, 0),
|
|
FFX_API_RESOURCE_STATE_UNORDERED_ACCESS);
|
|
}
|
|
|
|
void fail(const char *what, unsigned r) {
|
|
rt_log("fg: %s failed (%u): frame generation off for this run", what, r);
|
|
F.failed = 1;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
// Follows the setting: gfx_present.c's window_settings_live remakes the swapchain when this stops matching vk_fg_active.
|
|
// It needs DLSS's or FSR 3.1's motion vectors, so it also needs one of them on, and the FSR 3.1 library: a build
|
|
// without it keeps the plain swapchain whatever the setting says, with one line in the log. Not on a device made through
|
|
// Streamline's proxies (NVIDIA's frame generation set at the start, vk_dlssg_dpa): AMD's swapchain there crashed in
|
|
// NVIDIA's driver when the menu switched from NVIDIA's frame generation to AMD's. That switch waits for a restart,
|
|
// as the menu says (menu.cpp row_framegen). Not after a failure (fail): the present code then takes AMD's swapchain
|
|
// down as it does for a switch in the menu, and a plain one follows.
|
|
extern "C" int vk_fg_wanted(void) {
|
|
if (F.failed || strcmp(cfg_value("NX2NAVI_FRAMEGEN"), "fsr3") || !cfg_bool("NX2NAVI_DLAA") || !vk_dlaa_wanted() ||
|
|
vk_dlssg_dpa())
|
|
return 0;
|
|
if (vk_ffx_framegen_available()) return 1;
|
|
static int told;
|
|
if (!__atomic_exchange_n(&told, 1, __ATOMIC_RELAXED))
|
|
rt_log("fg: AMD FSR 3.1 frame generation is set, but FSR 3.1 is not available here: off");
|
|
return 0;
|
|
}
|
|
|
|
// Always the three queues: the setting can be switched on during a run, and idle queues cost nothing.
|
|
extern "C" uint32_t vk_fg_queue_count(void) { return 3; }
|
|
|
|
extern "C" void vk_fg_queues(VkQueue present, VkQueue acquire) { F.present = present, F.acquire = acquire; }
|
|
|
|
// AMD's swapchain is in place: the window's swapchain handle is AMD's object, and every swapchain call has to go
|
|
// through AMD's functions. That holds after a failure too, until the present code has replaced the swapchain
|
|
// (vk_fg_wanted): the plain Vulkan calls must never get that handle.
|
|
extern "C" int vk_fg_active(void) { return F.sc != nullptr; }
|
|
|
|
extern "C" int vk_fg_generating(void) { return vk_fg_active() && !F.failed && __atomic_load_n(&F.on, __ATOMIC_RELAXED); }
|
|
|
|
extern "C" VkResult vk_fg_create_swapchain(const VkSwapchainCreateInfoKHR *ci, VkSwapchainKHR *sc) {
|
|
if (F.sc) return F.fn.pOutCreateSwapchainFFXAPI(vk_ctx()->device, ci, nullptr, sc, F.sc);
|
|
if (!vk_fg_wanted() || F.failed || !F.present || !vk_ffx_load()) return VK_ERROR_FEATURE_NOT_PRESENT;
|
|
const VkCtx *c = vk_ctx();
|
|
ffxCreateContextDescFrameGenerationSwapChainVK d = {};
|
|
d.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FGSWAPCHAIN_VK;
|
|
d.physicalDevice = c->phys;
|
|
d.device = c->device;
|
|
VkSwapchainKHR none = VK_NULL_HANDLE;
|
|
d.swapchain = &none;
|
|
d.createInfo = *ci;
|
|
d.createInfo.oldSwapchain = VK_NULL_HANDLE;
|
|
d.gameQueue = {c->gfx_queue, c->gfx_family, submit_game};
|
|
d.asyncComputeQueue = {VK_NULL_HANDLE, c->gfx_family, nullptr};
|
|
d.presentQueue = {F.present, c->gfx_family, nullptr};
|
|
d.imageAcquireQueue = {F.acquire, c->gfx_family, nullptr};
|
|
ffxReturnCode_t r = vk_ffx_create(&F.sc, &d.header);
|
|
if (r != FFX_API_RETURN_OK) {
|
|
F.sc = nullptr;
|
|
fail("the frame generation swapchain", r);
|
|
return VK_ERROR_INITIALIZATION_FAILED;
|
|
}
|
|
F.fn.header.type = FFX_API_QUERY_DESC_TYPE_FGSWAPCHAIN_FUNCTIONS_VK;
|
|
if (vk_ffx_query(&F.sc, &F.fn.header) != FFX_API_RETURN_OK || !F.fn.pOutQueuePresentKHR) {
|
|
vk_ffx_destroy(&F.sc), F.sc = nullptr; // with it goes the swapchain it made: nothing was handed out yet
|
|
fail("the swapchain functions", 0);
|
|
return VK_ERROR_INITIALIZATION_FAILED;
|
|
}
|
|
*sc = none;
|
|
rt_log("fg: AMD FSR 3.1 frame generation swapchain, %ux%u", ci->imageExtent.width, ci->imageExtent.height);
|
|
return VK_SUCCESS;
|
|
}
|
|
|
|
extern "C" void vk_fg_destroy_swapchain(VkSwapchainKHR sc) {
|
|
if (F.sc && sc) F.fn.pOutDestroySwapchainFFXAPI(vk_ctx()->device, sc, nullptr, F.sc);
|
|
}
|
|
|
|
extern "C" VkResult vk_fg_get_images(VkSwapchainKHR sc, uint32_t *n, VkImage *img) {
|
|
return F.fn.pOutGetSwapchainImagesKHR(vk_ctx()->device, sc, n, img);
|
|
}
|
|
|
|
extern "C" VkResult vk_fg_acquire(VkSwapchainKHR sc, uint64_t timeout, VkSemaphore sem, uint32_t *idx) {
|
|
return F.fn.pOutAcquireNextImageKHR(vk_ctx()->device, sc, timeout, sem, VK_NULL_HANDLE, idx);
|
|
}
|
|
|
|
extern "C" VkResult vk_fg_present(const VkPresentInfoKHR *pi) {
|
|
return F.fn.pOutQueuePresentKHR(vk_ctx()->gfx_queue, pi);
|
|
}
|
|
|
|
extern "C" uint64_t vk_fg_presents(VkSwapchainKHR sc) {
|
|
return vk_fg_active() && F.fn.pOutGetLastPresentCountFFXAPI && sc ? F.fn.pOutGetLastPresentCountFFXAPI(sc) : 0;
|
|
}
|
|
|
|
// The HDR10 metadata goes to AMD's swapchain through its own call (vk_hdr.c vk_hdr_metadata).
|
|
extern "C" PFN_vkSetHdrMetadataEXT vk_fg_hdr_metadata_fn(void) {
|
|
return vk_fg_active() ? F.fn.pOutSetHdrMetadataEXT : nullptr;
|
|
}
|
|
|
|
// An HDR10 swapchain (10-bit PQ or 16-bit float): frame generation is told its inputs are HDR.
|
|
static bool hdr_format(VkFormat f) {
|
|
return f == VK_FORMAT_A2B10G10R10_UNORM_PACK32 || f == VK_FORMAT_A2R10G10B10_UNORM_PACK32 ||
|
|
f == VK_FORMAT_R16G16B16A16_SFLOAT;
|
|
}
|
|
|
|
extern "C" void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt, VkExtent2D ext, VkImage hudless,
|
|
VkImageView hudless_view, const int pic[4]) {
|
|
(void)hudless_view;
|
|
if (!vk_fg_active() || F.failed) return;
|
|
vk_dlaa_hudless_wanted = 1; // the upscaling pass keeps a copy of the picture before the HUD (vk_dlaa.c hudless)
|
|
const VkCtx *c = vk_ctx();
|
|
VkFfxImage depth, mv;
|
|
float jx = 0, jy = 0;
|
|
int reset = 0;
|
|
int have = vk_dlaa_fg_inputs(&depth, &mv, &jx, &jy, &reset);
|
|
// The context's largest render size covers the depth and motion vectors it is given. They have the scene's size,
|
|
// which DLAA and the widescreen pictures make larger than the window, and a context made for the window's size alone
|
|
// wrote past its own resources: a crash in amd_fidelityfx_vk.dll, device faults, striped frames. The scene is never
|
|
// larger than the picture (widescreen.c wide_picture), so the context is made for that once; remaking it while the
|
|
// swapchain still generated from the old one hung the present. A frame with a larger depth anyway is not generated.
|
|
unsigned pw, ph;
|
|
wide_picture(&pw, &ph);
|
|
VkExtent2D max_render = {ext.width > pw ? ext.width : pw, ext.height > ph ? ext.height : ph};
|
|
if (F.fg && (F.fg_ext.width != ext.width || F.fg_ext.height != ext.height || F.fg_fmt != fmt)) {
|
|
vk_queue_lock(), vkDeviceWaitIdle(c->device), vk_queue_unlock(); // a new window size: the context is made for its display size
|
|
vk_ffx_destroy(&F.fg);
|
|
F.fg = nullptr;
|
|
}
|
|
if (!F.fg) {
|
|
ffxCreateBackendVKDesc be = {};
|
|
be.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_BACKEND_VK;
|
|
be.vkDevice = c->device;
|
|
be.vkPhysicalDevice = c->phys;
|
|
be.vkDeviceProcAddr = vkGetDeviceProcAddr;
|
|
// the HUD-less picture has the swapchain's format (gfx_present.c hudless_record)
|
|
ffxCreateContextDescFrameGenerationHudless hl = {};
|
|
hl.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FRAMEGENERATION_HUDLESS;
|
|
hl.header.pNext = &be.header;
|
|
hl.hudlessBackBufferFormat = ffxApiGetSurfaceFormatVK(fmt);
|
|
ffxCreateContextDescFrameGeneration d = {};
|
|
d.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FRAMEGENERATION;
|
|
d.header.pNext = &hl.header;
|
|
d.flags = FFX_FRAMEGENERATION_ENABLE_DEPTH_INFINITE | (hdr_format(fmt) ? FFX_FRAMEGENERATION_ENABLE_HIGH_DYNAMIC_RANGE : 0);
|
|
d.displaySize = {ext.width, ext.height};
|
|
d.maxRenderSize = {max_render.width, max_render.height};
|
|
d.backBufferFormat = ffxApiGetSurfaceFormatVK(fmt);
|
|
ffxReturnCode_t r = vk_ffx_create(&F.fg, &d.header);
|
|
if (r != FFX_API_RETURN_OK) return F.fg = nullptr, fail("the frame generation context", r);
|
|
F.fg_ext = ext, F.fg_fmt = fmt, F.fg_max = max_render;
|
|
rt_log("fg: frame generation context for %ux%u%s, render size up to %ux%u", ext.width, ext.height,
|
|
hdr_format(fmt) ? " (HDR)" : "", max_render.width, max_render.height);
|
|
}
|
|
if (have && (depth.width > F.fg_max.width || depth.height > F.fg_max.height)) {
|
|
static int told;
|
|
if (!told++)
|
|
rt_log("fg: a %ux%u scene is larger than the context's %ux%u: those frames are not generated", depth.width,
|
|
depth.height, F.fg_max.width, F.fg_max.height);
|
|
have = 0;
|
|
}
|
|
int on = have && !menu_is_open();
|
|
__atomic_store_n(&F.on, on, __ATOMIC_RELAXED);
|
|
if (!have) F.no_inputs++;
|
|
else if (!on) F.paused++;
|
|
F.frame++;
|
|
ffxConfigureDescFrameGeneration cfg = {};
|
|
cfg.header.type = FFX_API_CONFIGURE_DESC_TYPE_FRAMEGENERATION;
|
|
cfg.swapChain = (void *)sc;
|
|
cfg.frameGenerationCallback = generate;
|
|
cfg.frameGenerationCallbackUserContext = &F.fg;
|
|
cfg.frameGenerationEnabled = on != 0;
|
|
cfg.allowAsyncWorkloads = false;
|
|
// the picture's rectangle: with black bars beside or below it (a window of another shape) a generation over the
|
|
// whole swapchain warped the picture's edges into the bars
|
|
cfg.generationRect = pic && pic[2] > 0 && pic[3] > 0 ? FfxApiRect2D{pic[0], pic[1], pic[2], pic[3]}
|
|
: FfxApiRect2D{0, 0, (int32_t)ext.width, (int32_t)ext.height};
|
|
if (on && hudless) {
|
|
VkFfxImage h = {hudless, fmt, ext.width, ext.height, 1};
|
|
cfg.HUDLessColor = image(&h);
|
|
F.hudless++;
|
|
}
|
|
cfg.frameID = F.frame;
|
|
ffxReturnCode_t r = vk_ffx_configure(&F.fg, &cfg.header);
|
|
if (r != FFX_API_RETURN_OK) return fail("frame generation configuration", r);
|
|
if (!on) return;
|
|
ffxDispatchDescFrameGenerationPrepare p = {};
|
|
p.header.type = FFX_API_DISPATCH_DESC_TYPE_FRAMEGENERATION_PREPARE;
|
|
p.frameID = F.frame;
|
|
p.commandList = cb;
|
|
p.renderSize = {depth.width, depth.height};
|
|
p.jitterOffset = {jx, jy};
|
|
p.motionVectorScale = {1.0f, 1.0f}; // render pixels, current to previous (vk_dlaa.c mv_pass), as FSR takes them
|
|
static uint64_t last_ns;
|
|
uint64_t now = plat_now_ns();
|
|
double dt = last_ns ? (now - last_ns) / 1e6 : 16.67;
|
|
last_ns = now;
|
|
p.frameTimeDelta = dt > 0 && dt < 100 ? (float)dt : 16.67f;
|
|
p.cameraNear = 0.1f; // Ocarina's projection: near 0.1, far at infinity (vk_ffx.cpp)
|
|
p.cameraFar = 1e8f;
|
|
p.cameraFovAngleVertical = 1.0f;
|
|
p.viewSpaceToMetersFactor = 0.01f;
|
|
p.depth = image(&depth);
|
|
p.motionVectors = image(&mv);
|
|
r = vk_ffx_dispatch(&F.fg, &p.header);
|
|
if (r != FFX_API_RETURN_OK) return fail("frame generation preparation", r);
|
|
if (F.frame == 2 || F.frame % 3600 == 0)
|
|
rt_log("fg: %llu frames presented, %llu generated (%llu with the HUD kept out); paused %llu (menu), %llu without depth "
|
|
"and motion", (unsigned long long)F.frame, (unsigned long long)F.generated, (unsigned long long)F.hudless,
|
|
(unsigned long long)F.paused, (unsigned long long)F.no_inputs);
|
|
}
|
|
|
|
extern "C" void vk_fg_release(void) {
|
|
if (F.fg) vk_ffx_destroy(&F.fg), F.fg = nullptr;
|
|
if (F.sc) vk_ffx_destroy(&F.sc), F.sc = nullptr;
|
|
__atomic_store_n(&F.on, 0, __ATOMIC_RELAXED);
|
|
}
|