mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -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>
3690 lines
226 KiB
C
3690 lines
226 KiB
C
// DLAA: NVIDIA DLSS at the scene's own size (NX2NAVI_DLAA, nx2navi.ini dlaa). NGX is loaded at run time from the driver's
|
|
// NGX core: on Windows _nvngx.dll, found through HKLM\SOFTWARE\NVIDIA Corporation\Global\NGXCore; on Linux
|
|
// libnvidia-ngx.so.1, which the NVIDIA driver installs on the library path. NGX loads the DLSS model from the folder of
|
|
// the executable: nvngx_dlss.dll on Windows, libnvidia-ngx-dlss.so.<version> on Linux. No NVIDIA SDK code or header is
|
|
// compiled in: the few NGX types used are declared here from the SDK ABI and the Linux core's shutdown interface.
|
|
//
|
|
// The core's parameter object is a C++ class whose virtual functions are called through its vtable: the Set overloads
|
|
// in slots 0-7 and the Get overloads in 8-15. MSVC (the Windows core) lays out each group of overloads in reverse
|
|
// declaration order, the Itanium C++ ABI (the Linux core, built with GCC) in declaration order. vtable_ok checks the
|
|
// layout with a set-then-get round trip before anything relies on it.
|
|
//
|
|
// NGX and DLAA's resources live as long as the device: vk_dlaa_init brings NGX up on the new device, and
|
|
// vk_dlaa_release (Linux) frees everything DLAA made and shuts NGX down before the device is destroyed.
|
|
//
|
|
// Where DLAA runs in the frame, the jitter and the motion vectors: see the sections below.
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <wchar.h>
|
|
|
|
#include "cfg.h"
|
|
#include "gfx.h"
|
|
#include "host.h"
|
|
#include "nvn_api.h"
|
|
#include "platform/platform.h"
|
|
#include "vk.h"
|
|
#include "vk_dlaa.h"
|
|
#include "vk_dlssg.h"
|
|
#include "vk_exec.h"
|
|
#include "vk_fg.h"
|
|
#include "vk_ffx.h"
|
|
#include "vk_rec.h"
|
|
#include "vk_up_defer.h"
|
|
#include "dlaa_mv_spv.h"
|
|
#include "dlaa_sharpen_spv.h"
|
|
|
|
typedef int NgxResult; // NVSDK_NGX_Result: 1 success, 0xBAD00000 | n failures
|
|
#define NGX_OK(r) (((unsigned)(r) & 0xFFF00000u) != 0xBAD00000u)
|
|
#define NGX_VERSION_API 0x15
|
|
#define NGX_FEATURE_SUPERSAMPLING 1
|
|
#define NGX_PERFQ_DLAA 5 // NVSDK_NGX_PerfQuality_Value: 0 performance, 1 balanced, 2 quality, 3 ultra performance
|
|
typedef struct NgxParam NgxParam;
|
|
typedef struct NgxHandle NgxHandle;
|
|
typedef struct { void *slot[17]; } NgxParamVtbl;
|
|
struct NgxParam { const NgxParamVtbl *vt; };
|
|
typedef struct { // NVSDK_NGX_ImageViewInfo_VK
|
|
VkImageView view;
|
|
VkImage image;
|
|
VkImageSubresourceRange range;
|
|
VkFormat format;
|
|
unsigned width, height;
|
|
} NgxImageView;
|
|
typedef struct { // NVSDK_NGX_Resource_VK
|
|
union { NgxImageView img; struct { VkBuffer buffer; unsigned bytes; } buf; } r;
|
|
int type; // 0 image view, 1 buffer
|
|
_Bool rw;
|
|
} NgxResourceVK;
|
|
|
|
typedef NgxResult (*PFN_Init)(unsigned long long app, const wchar_t *path, VkInstance, VkPhysicalDevice, VkDevice,
|
|
PFN_vkGetInstanceProcAddr, PFN_vkGetDeviceProcAddr, int version, const void *info);
|
|
typedef NgxResult (*PFN_ReqExt)(unsigned *ni, const char ***ie, unsigned *nd, const char ***de);
|
|
typedef NgxResult (*PFN_Params)(NgxParam **);
|
|
typedef NgxResult (*PFN_DestroyParams)(NgxParam *);
|
|
typedef NgxResult (*PFN_Create)(VkCommandBuffer, int feature, NgxParam *, NgxHandle **);
|
|
typedef NgxResult (*PFN_Evaluate)(VkCommandBuffer, const NgxHandle *, const NgxParam *, void *callback);
|
|
typedef NgxResult (*PFN_Release)(NgxHandle *);
|
|
#ifndef _WIN32
|
|
// NVSDK_NGX_VULKAN_Shutdown1 as the Linux core exports it: the device and a pointer that receives the devices NGX still
|
|
// serves afterwards (vk_dlaa_release).
|
|
typedef NgxResult (*PFN_Shutdown1)(VkDevice, unsigned *remaining);
|
|
#endif
|
|
|
|
static struct {
|
|
void *core; // the NGX core library (an HMODULE on Windows)
|
|
PFN_Init init;
|
|
PFN_ReqExt req_ext;
|
|
PFN_Params caps, alloc_params;
|
|
PFN_DestroyParams destroy_params;
|
|
PFN_Create create;
|
|
PFN_Evaluate evaluate;
|
|
PFN_Release release;
|
|
void *feat_inst_req; // NVSDK_NGX_VULKAN_GetFeatureInstanceExtensionRequirements (vk_ngxfg), NULL when the core lacks it
|
|
int tried, loaded, available;
|
|
int ngx_up; // vk_dlaa_init brought NGX up: DLSS serves this run
|
|
NgxParam *capsp; // the capability parameters (vk_dlaa_init), which DLSS's statistics callback reads and fills
|
|
int wanted; // -1 not read yet
|
|
#ifndef _WIN32
|
|
// Linux teardown (vk_dlaa_release)
|
|
PFN_Shutdown1 shutdown1; // required: core_load leaves DLAA off without it
|
|
VkDevice device; // what NGX was initialised with: set while vk_dlaa_release owes it a shutdown
|
|
NgxParam *caps_map; // GetCapabilityParameters' map: allocated for the caller, destroyed by vk_dlaa_release
|
|
#endif
|
|
} N = {.wanted = -1};
|
|
|
|
// DLSS Frame Generation through NGX itself (vk_ngxfg_*, at the end of this file): NX2NAVI_FRAMEGEN = dlss where
|
|
// Streamline does not run, which is every build but the Windows one.
|
|
static struct {
|
|
int max; // generated frames per rendered one this GPU allows (MultiFrameCountMax), 0: none
|
|
NgxHandle *feat;
|
|
NgxParam *p;
|
|
uint32_t w, h; // the picture the feature was created for
|
|
VkFormat fmt;
|
|
int failed, reset, evals;
|
|
} FG;
|
|
|
|
// Whether this run can generate frames through NGX: DLSS upscaling on at the start, as NGX comes up only then. The
|
|
// swapchain then gets the presenter's extra images and NGX is asked about frame generation; whether frames are
|
|
// generated is NX2NAVI_FRAMEGEN = dlss, read live (vk_ngxfg_on).
|
|
int vk_ngxfg_wanted(void) {
|
|
#ifdef _WIN32
|
|
return 0; // Streamline's path there (vk_dlssg.c)
|
|
#else
|
|
static int w = -1;
|
|
if (w < 0) w = cfg_bool("NX2NAVI_DLAA") && strcmp(cfg_value("NX2NAVI_TAA"), "fsr3");
|
|
return w;
|
|
#endif
|
|
}
|
|
|
|
typedef void (*SetI)(NgxParam *, const char *, int);
|
|
typedef void (*SetU)(NgxParam *, const char *, unsigned);
|
|
typedef void (*SetF)(NgxParam *, const char *, float);
|
|
typedef void (*SetP)(NgxParam *, const char *, void *);
|
|
typedef NgxResult (*GetI)(const NgxParam *, const char *, int *);
|
|
typedef NgxResult (*GetU)(const NgxParam *, const char *, unsigned *);
|
|
typedef NgxResult (*GetF)(const NgxParam *, const char *, float *);
|
|
typedef NgxResult (*GetP)(const NgxParam *, const char *, void **);
|
|
typedef NgxResult (*GetU64)(const NgxParam *, const char *, unsigned long long *);
|
|
// The SDK declares Set(u64), Set(float), Set(double), Set(u32), Set(int), Set(ID3D11Resource *), Set(ID3D12Resource *),
|
|
// Set(void *), then the Get overloads in the same order, then Reset.
|
|
#ifdef _WIN32
|
|
enum { S_P = 0, S_D12, S_D11, S_I, S_U32, S_D, S_F, S_U64, G_P, G_D12, G_D11, G_I, G_U32, G_D, G_F, G_U64 };
|
|
#else
|
|
enum { S_U64 = 0, S_F, S_D, S_U32, S_I, S_D11, S_D12, S_P, G_U64, G_F, G_D, G_U32, G_I, G_D11, G_D12, G_P };
|
|
#endif
|
|
static void p_seti(NgxParam *p, const char *n, int v) { ((SetI)p->vt->slot[S_I])(p, n, v); }
|
|
static void p_setu(NgxParam *p, const char *n, unsigned v) { ((SetU)p->vt->slot[S_U32])(p, n, v); }
|
|
static void p_setf(NgxParam *p, const char *n, float v) { ((SetF)p->vt->slot[S_F])(p, n, v); }
|
|
static void p_setp(NgxParam *p, const char *n, void *v) { ((SetP)p->vt->slot[S_P])(p, n, v); }
|
|
static int p_geti(const NgxParam *p, const char *n, int *v) { return NGX_OK(((GetI)p->vt->slot[G_I])(p, n, v)); }
|
|
static int p_getu(const NgxParam *p, const char *n, unsigned *v) { return NGX_OK(((GetU)p->vt->slot[G_U32])(p, n, v)); }
|
|
static int p_getf(const NgxParam *p, const char *n, float *v) { return NGX_OK(((GetF)p->vt->slot[G_F])(p, n, v)); }
|
|
static int p_getp(const NgxParam *p, const char *n, void **v) { return NGX_OK(((GetP)p->vt->slot[G_P])(p, n, v)); }
|
|
static int p_getull(const NgxParam *p, const char *n, unsigned long long *v) {
|
|
return NGX_OK(((GetU64)p->vt->slot[G_U64])(p, n, v));
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
#define DLSS_MODEL "nvngx_dlss.dll"
|
|
#else
|
|
#define DLSS_MODEL "libnvidia-ngx-dlss.so.<version>"
|
|
#endif
|
|
|
|
// NGX is brought up only when DLSS is wanted at start: its device extensions can only be asked for at device creation,
|
|
// and loading the driver's NGX core on every NVIDIA machine would cost start time and change the device with DLAA off.
|
|
// So DLSS applies after a restart. FSR 3.1 (amd_fidelityfx_vk.dll, vk_ffx.cpp) asks nothing of the device: NX2NAVI_TAA
|
|
// and dlaa follow the setting at once when NGX is not up, and vk_dlaa_frame loads the library the first time it is used.
|
|
int vk_dlaa_wanted(void) {
|
|
static int at_start = -1;
|
|
if (at_start < 0) at_start = cfg_bool("NX2NAVI_DLAA");
|
|
return at_start || (!N.ngx_up && cfg_bool("NX2NAVI_DLAA") && !strcmp(cfg_value("NX2NAVI_TAA"), "fsr3"));
|
|
}
|
|
|
|
// The temporal upscaler behind dlaa: NVIDIA DLSS through NGX, once NGX is up, or else AMD FSR 3.1, which takes the same
|
|
// inputs. The DLSS mode setting (NX2NAVI_DLSS) is the render scale for both.
|
|
static int fsr3(void) { return !N.ngx_up && !strcmp(cfg_value("NX2NAVI_TAA"), "fsr3"); }
|
|
static int fsr_frame; // fsr3() as vk_dlaa_init and then vk_dlaa_frame read it: the upscaler this frame runs with
|
|
// FSR 3.1 is the upscaler and its library loads (vk_ffx_available): widescreen.c shrinks the scene only for an upscaler
|
|
// that runs.
|
|
int vk_dlaa_fsr3(void) { return vk_dlaa_wanted() && fsr3() && vk_ffx_available(); }
|
|
|
|
// The upscaler the menu has set cannot start in this run: DLSS when NGX was never tried (a device made without its
|
|
// extensions), FSR 3.1 while NGX is up and serves the run. A DLSS this PC could not bring up is not waiting for a restart.
|
|
int vk_dlaa_needs_restart(void) {
|
|
if (!cfg_bool("NX2NAVI_DLAA")) return 0;
|
|
return strcmp(cfg_value("NX2NAVI_TAA"), "fsr3") ? !N.ngx_up && !N.tried : N.ngx_up;
|
|
}
|
|
|
|
#ifdef _WIN32
|
|
// The NGX core's folder when the driver has not written NGXCore\FullPath (seen with driver 617.42, whose key held
|
|
// only DLSS override values): the core sits in the driver's own DriverStore folder, the one its Vulkan driver is in.
|
|
// That is the display adapter's VulkanDriverName, where the Vulkan loader itself finds the driver.
|
|
static int ngx_dir_from_adapter(char *dir, DWORD size) {
|
|
HKEY cls;
|
|
if (RegOpenKeyExA(HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Control\\Class\\{4d36e968-e325-11ce-bfc1-08002be10318}",
|
|
0, KEY_READ, &cls) != ERROR_SUCCESS)
|
|
return 0;
|
|
int found = 0;
|
|
for (DWORD i = 0; !found; i++) {
|
|
char sub[64], val[MAX_PATH + 1] = {0}, probe[MAX_PATH + 32];
|
|
DWORD sn = sizeof sub, vn = MAX_PATH;
|
|
if (RegEnumKeyExA(cls, i, sub, &sn, NULL, NULL, NULL, NULL) != ERROR_SUCCESS) break;
|
|
if (RegGetValueA(cls, sub, "VulkanDriverName", RRF_RT_REG_SZ | RRF_RT_REG_MULTI_SZ, NULL, val, &vn) != ERROR_SUCCESS)
|
|
continue;
|
|
char *slash = strrchr(val, '\\');
|
|
if (!slash) continue;
|
|
*slash = 0;
|
|
snprintf(probe, sizeof probe, "%s\\_nvngx.dll", val);
|
|
if (GetFileAttributesA(probe) == INVALID_FILE_ATTRIBUTES) continue;
|
|
snprintf(dir, size, "%s", val);
|
|
found = 1;
|
|
}
|
|
RegCloseKey(cls);
|
|
return found;
|
|
}
|
|
#endif
|
|
|
|
static int core_load(void) {
|
|
if (N.tried) return N.loaded;
|
|
N.tried = 1;
|
|
#ifdef _WIN32
|
|
char dir[MAX_PATH] = {0}, path[MAX_PATH + 32];
|
|
DWORD n = sizeof dir;
|
|
if (RegGetValueA(HKEY_LOCAL_MACHINE, "SOFTWARE\\NVIDIA Corporation\\Global\\NGXCore", "FullPath", RRF_RT_REG_SZ,
|
|
NULL, dir, &n) != ERROR_SUCCESS && !ngx_dir_from_adapter(dir, sizeof dir)) {
|
|
rt_log("dlaa: the driver has no NGX core (registry NGXCore missing, none beside its Vulkan driver): DLAA off");
|
|
return 0;
|
|
}
|
|
snprintf(path, sizeof path, "%s\\_nvngx.dll", dir);
|
|
N.core = LoadLibraryA(path);
|
|
if (!N.core) {
|
|
rt_log("dlaa: cannot load %s (error %lu): DLAA off", path, GetLastError());
|
|
return 0;
|
|
}
|
|
#define FN(f, name) N.f = (void *)GetProcAddress((HMODULE)N.core, name)
|
|
#else
|
|
// The driver's NGX core; the dynamic loader finds it where the driver put it (/usr/lib, /usr/lib/x86_64-linux-gnu).
|
|
const char *path = "libnvidia-ngx.so.1";
|
|
N.core = plat_lib_open(path);
|
|
if (!N.core) {
|
|
rt_log("dlaa: cannot load %s (not an NVIDIA driver, or one without NGX): DLAA off", path);
|
|
return 0;
|
|
}
|
|
#define FN(f, name) N.f = plat_lib_sym(N.core, name)
|
|
#endif
|
|
FN(init, "NVSDK_NGX_VULKAN_Init_Ext2");
|
|
#ifndef _WIN32
|
|
FN(shutdown1, "NVSDK_NGX_VULKAN_Shutdown1"); // Linux: checked below
|
|
#endif
|
|
FN(req_ext, "NVSDK_NGX_VULKAN_RequiredExtensions");
|
|
FN(caps, "NVSDK_NGX_VULKAN_GetCapabilityParameters");
|
|
FN(alloc_params, "NVSDK_NGX_VULKAN_AllocateParameters");
|
|
FN(destroy_params, "NVSDK_NGX_VULKAN_DestroyParameters");
|
|
FN(create, "NVSDK_NGX_VULKAN_CreateFeature");
|
|
FN(evaluate, "NVSDK_NGX_VULKAN_EvaluateFeature");
|
|
FN(release, "NVSDK_NGX_VULKAN_ReleaseFeature");
|
|
FN(feat_inst_req, "NVSDK_NGX_VULKAN_GetFeatureInstanceExtensionRequirements"); // optional: frame generation's
|
|
#undef FN
|
|
if (!N.init || !N.req_ext || !N.caps || !N.alloc_params || !N.create || !N.evaluate || !N.release) {
|
|
rt_log("dlaa: the NGX core %s lacks Vulkan entry points: DLAA off", path);
|
|
return 0;
|
|
}
|
|
#ifndef _WIN32
|
|
// Without Shutdown1 the device cannot be taken back from NGX (vk_dlaa_release), and the process then segfaults after
|
|
// the Vulkan teardown (NVIDIA 615.78.08): DLAA stays off rather than run with a teardown known to crash. The
|
|
// parameter maps are destroyed there too.
|
|
if (!N.shutdown1 || !N.destroy_params) {
|
|
rt_log("dlaa: the NGX core %s lacks NVSDK_NGX_VULKAN_Shutdown1 or DestroyParameters, which its teardown needs: "
|
|
"DLAA off", path);
|
|
return 0;
|
|
}
|
|
#endif
|
|
N.loaded = 1;
|
|
rt_log("dlaa: NGX core %s", path);
|
|
return 1;
|
|
}
|
|
|
|
static int listed(const char *const *list, uint32_t n, const char *s) {
|
|
for (uint32_t i = 0; i < n; i++)
|
|
if (!strcmp(list[i], s)) return 1;
|
|
return 0;
|
|
}
|
|
|
|
// NVSDK_NGX_FeatureDiscoveryInfo with an application id (the layout of the SDK's struct, 64-bit), for the question
|
|
// which instance extensions frame generation wants.
|
|
typedef struct {
|
|
int sdk, feature;
|
|
struct { int type; union { struct { const char *id; int engine; const char *ver; } project; unsigned long long app; } v; } ident;
|
|
const wchar_t *path;
|
|
const void *info;
|
|
} NgxDiscovery;
|
|
typedef NgxResult (*PFN_FeatInstReq)(const NgxDiscovery *, uint32_t *, VkExtensionProperties **);
|
|
#define NGX_FEATURE_FRAME_GENERATION 11 // NVSDK_NGX_Feature_FrameGeneration
|
|
// The names NGX lists are copied: its list may not outlive the call, and the device creation keeps the pointers.
|
|
static char fg_names[16][VK_MAX_EXTENSION_NAME_SIZE];
|
|
static int fg_nnames;
|
|
static const char *fg_keep(const char *name) {
|
|
for (int i = 0; i < fg_nnames; i++)
|
|
if (!strcmp(fg_names[i], name)) return fg_names[i];
|
|
if (fg_nnames >= 16) return NULL;
|
|
snprintf(fg_names[fg_nnames], sizeof fg_names[0], "%s", name);
|
|
return fg_names[fg_nnames++];
|
|
}
|
|
|
|
void vk_dlaa_instance_exts(const char **ext, uint32_t *n, uint32_t max) {
|
|
unsigned ni = 0, nd = 0;
|
|
const char **ie = NULL, **de = NULL;
|
|
if (!vk_dlaa_wanted() || fsr3() || !core_load() || !NGX_OK(N.req_ext(&ni, &ie, &nd, &de))) return;
|
|
for (unsigned i = 0; i < ni; i++)
|
|
if (!listed(ext, *n, ie[i]) && *n < max) ext[(*n)++] = ie[i];
|
|
// Frame generation's own instance extensions (vk_ngxfg_evaluate). Its device extensions are not asked for: the
|
|
// core's GetFeatureDeviceExtensionRequirements answered "not implemented" (0xBAD00012) with NVIDIA's 615 Linux
|
|
// driver, and NGX's general list (vk_dlaa_device_exts) served the feature.
|
|
if (vk_ngxfg_wanted() && N.feat_inst_req) {
|
|
static const wchar_t here[] = L".";
|
|
NgxDiscovery d;
|
|
memset(&d, 0, sizeof d);
|
|
d.sdk = NGX_VERSION_API, d.feature = NGX_FEATURE_FRAME_GENERATION;
|
|
d.ident.type = 0, d.ident.v.app = 231313132ull; // the application id vk_dlaa_init gives NGX
|
|
d.path = here;
|
|
uint32_t k = 0;
|
|
VkExtensionProperties *pr = NULL;
|
|
if (NGX_OK(((PFN_FeatInstReq)N.feat_inst_req)(&d, &k, &pr)) && pr)
|
|
for (uint32_t i = 0; i < k; i++) {
|
|
const char *nm = fg_keep(pr[i].extensionName);
|
|
if (nm && !listed(ext, *n, nm) && *n < max) ext[(*n)++] = nm, rt_log("ngxfg: instance extension %s", nm);
|
|
}
|
|
}
|
|
}
|
|
|
|
void vk_dlaa_device_exts(const VkExtensionProperties *have, uint32_t nhave, const char **ext, uint32_t *n,
|
|
uint32_t max) {
|
|
unsigned ni = 0, nd = 0;
|
|
const char **ie = NULL, **de = NULL;
|
|
if (!vk_dlaa_wanted() || fsr3() || !core_load() || !NGX_OK(N.req_ext(&ni, &ie, &nd, &de))) return;
|
|
for (unsigned i = 0; i < nd; i++) {
|
|
int ok = 0;
|
|
for (uint32_t k = 0; k < nhave && !ok; k++) ok = !strcmp(have[k].extensionName, de[i]);
|
|
if (!ok) rt_log("dlaa: the device lacks %s, which NGX asks for", de[i]);
|
|
else if (!listed(ext, *n, de[i]) && *n < max) ext[(*n)++] = de[i];
|
|
}
|
|
}
|
|
|
|
static int vtable_ok(void) {
|
|
NgxParam *p = NULL;
|
|
if (!NGX_OK(N.alloc_params(&p)) || !p) return 0;
|
|
int i = 0;
|
|
unsigned u = 0;
|
|
float f = 0;
|
|
void *v = NULL;
|
|
p_seti(p, "NX2navi.Test.I", -12345);
|
|
p_setu(p, "NX2navi.Test.U", 4000000000u);
|
|
p_setf(p, "NX2navi.Test.F", 1.25f);
|
|
p_setp(p, "NX2navi.Test.P", (void *)p);
|
|
int ok = p_geti(p, "NX2navi.Test.I", &i) && i == -12345 && p_getu(p, "NX2navi.Test.U", &u) && u == 4000000000u &&
|
|
p_getf(p, "NX2navi.Test.F", &f) && f == 1.25f && p_getp(p, "NX2navi.Test.P", &v) && v == (void *)p;
|
|
if (N.destroy_params) N.destroy_params(p);
|
|
if (!ok) rt_log("dlaa: NGX parameter layout check failed (i=%d u=%u f=%g p=%p): DLAA off", i, u, f, v);
|
|
return ok;
|
|
}
|
|
|
|
#ifndef _WIN32
|
|
// UTF-8 to UTF-32 wchar_t, without the C library's locale (the runtime never sets one, so mbstowcs would stop at the
|
|
// first non-ASCII byte). Returns 0 on malformed input or when out has no room.
|
|
static int utf8_to_wide(const char *in, wchar_t *out, size_t cap) {
|
|
const unsigned char *u = (const unsigned char *)in;
|
|
size_t n = 0;
|
|
while (*u) {
|
|
unsigned c = *u++, more = c < 0x80 ? 0 : c >= 0xF0 ? 3 : c >= 0xE0 ? 2 : c >= 0xC0 ? 1 : 4;
|
|
if (more == 4) return 0;
|
|
if (more) c &= 0x3F >> more;
|
|
for (unsigned k = 0; k < more; k++, u++) {
|
|
if ((*u & 0xC0) != 0x80) return 0;
|
|
c = c << 6 | (*u & 0x3F);
|
|
}
|
|
if (n + 1 >= cap) return 0;
|
|
out[n++] = (wchar_t)c;
|
|
}
|
|
out[n] = 0;
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
void vk_dlaa_init(void) {
|
|
if (vk_dlaa_wanted() && fsr3()) {
|
|
fsr_frame = 1;
|
|
N.available = vk_ffx_load();
|
|
rt_log("dlaa: the temporal upscaler is AMD FSR 3.1%s", N.available ? "" : ", which is not available: the game's own scaling");
|
|
return;
|
|
}
|
|
if (!vk_dlaa_wanted() || !core_load()) return;
|
|
#ifdef _WIN32
|
|
wchar_t wdir[MAX_PATH];
|
|
DWORD len = GetModuleFileNameW(NULL, wdir, MAX_PATH);
|
|
while (len && wdir[len - 1] != L'\\') len--;
|
|
wdir[len] = 0;
|
|
#else
|
|
// The executable's folder, where NGX looks for libnvidia-ngx-dlss.so and writes its logs, as a wchar_t string:
|
|
// 32-bit code points here, as the Linux core expects.
|
|
char dir[PLAT_PATH_MAX];
|
|
wchar_t wdir[PLAT_PATH_MAX];
|
|
char *cut = plat_exe_path(dir, sizeof dir) ? plat_last_sep(dir) : NULL;
|
|
if (!cut || !utf8_to_wide((cut[1] = 0, dir), wdir, PLAT_PATH_MAX)) {
|
|
rt_log("dlaa: cannot name the executable's folder for NGX: DLAA off");
|
|
return;
|
|
}
|
|
#endif
|
|
// 231313132: the application id NVIDIA's DLSS samples use for builds without an NVIDIA-issued id
|
|
const VkCtx *c = vk_ctx();
|
|
NgxResult r = N.init(231313132ull, wdir, c->instance, c->phys, c->device, vkGetInstanceProcAddr,
|
|
vkGetDeviceProcAddr, NGX_VERSION_API, NULL);
|
|
if (!NGX_OK(r)) {
|
|
rt_log("dlaa: NGX init failed (%08x): DLAA off", (unsigned)r);
|
|
return;
|
|
}
|
|
#ifndef _WIN32
|
|
N.device = c->device; // NGX holds the device from here, also if the layout check below turns DLAA off
|
|
#endif
|
|
if (!vtable_ok()) return;
|
|
NgxParam *caps = NULL;
|
|
int avail = 0, driver = 0;
|
|
if (NGX_OK(N.caps(&caps)) && caps) {
|
|
#ifndef _WIN32
|
|
N.caps_map = caps; // allocated for us (the SDK header: free it with DestroyParameters): vk_dlaa_release does
|
|
#endif
|
|
p_geti(caps, "SuperSampling.Available", &avail);
|
|
p_geti(caps, "SuperSampling.NeedsUpdatedDriver", &driver);
|
|
}
|
|
N.available = avail != 0;
|
|
N.capsp = caps;
|
|
N.ngx_up = 1;
|
|
rt_log("dlaa: NGX up; DLSS %s%s", avail ? "available" : "NOT available (is " DLSS_MODEL " next to the executable?)",
|
|
driver ? "; the driver needs an update" : "");
|
|
if (vk_ngxfg_wanted() && caps) {
|
|
int fga = 0;
|
|
unsigned mx = 0;
|
|
p_geti(caps, "FrameGeneration.Available", &fga);
|
|
if (!p_getu(caps, "DLSSG.MultiFrameCountMax", &mx)) mx = 1;
|
|
// MultiFrameCountMax said 3 (4x) with the 310.9.16 library on an RTX 5090, yet that library evaluated 5
|
|
// generated frames without an error on NVIDIA's 615 driver, and NVIDIA ships up to 6x on RTX 50 cards: a GPU
|
|
// with multi-frame generation gets up to 5 (6x).
|
|
FG.max = fga ? (mx >= 3 ? 5 : mx < 1 ? 1 : (int)mx) : 0;
|
|
rt_log("ngxfg: DLSS Frame Generation %s (up to %dx)",
|
|
fga ? "available" : "NOT available (is libnvidia-ngx-dlssg.so.<version> next to the executable?)", FG.max + 1);
|
|
}
|
|
}
|
|
|
|
int vk_dlaa_available(void) { return vk_dlaa_wanted() && N.available; }
|
|
|
|
// ---------------------------------------------------------------- where DLAA runs in NX2navi's frame
|
|
// Measured (NX2NAVI_DUMP, frame 2100 at Lookout Landing, 2560x1440 scene): the G-buffer passes draw into five targets with
|
|
// the scene depth (d32f at the scene size), one of them the R11G11B10F light target; lighting and the transparents
|
|
// then draw into that light target alone, the last time with the scene depth attached; the first pass narrower than a
|
|
// quarter of the scene starts the bloom chain, which reads it. After the bloom come the tonemap and post passes, then
|
|
// the HUD is drawn into the same target and a last pass copies it to the 8-bit output.
|
|
// So: after the G-buffer, a pass at the scene size with exactly one colour target (R11G11B10F or RGBA16F) and the D32F
|
|
// depth arms the frame with that pair; the next pass at most a quarter as wide ends the scene (no jitter after it) and,
|
|
// when the scene is as wide as the output, runs DLAA on the armed target first. Bloom, tonemap, post and HUD then all
|
|
// work on the anti-aliased picture.
|
|
// Upscaling (a scene smaller than the output: NX2NAVI_DLSS quality/balanced/performance, or the game's own 5/6): measured
|
|
// at a 960x536 scene, 1920x1080 output, the bloom and its composite write back into the light target at the scene size,
|
|
// then one full-screen draw (the FXAA shader below, which also scales) reads it into an output-sized R11G11B10F target
|
|
// with no depth, and the HUD is drawn over that target at the output size. DLSS runs at the begin of that pass: it
|
|
// reads the light target and its result is blitted into the pass's target, whose scaling draw is then skipped. The
|
|
// HUD, drawn afterwards into the same target, is never jittered nor passed through DLSS.
|
|
// The scaling draw is not always that FXAA shader: at a 1280x720 scene and 1920x1080 output (1.4.3, dlss = quality)
|
|
// the game's tonemap (guest fs 7e114c4330dc212a) reads the light target and writes the output-sized target itself,
|
|
// over DLSS's picture, so the jittered scene reached the screen and the whole picture shook. So from DLSS's evaluation
|
|
// to the frame's end, draws in passes larger than the light target sample DLSS's output in its place
|
|
// (vk_dlaa_swap_img, applied by vk_exec_draw.c tex_info): whichever shader scales, it scales DLSS's picture.
|
|
#define OCAM_BLOCK 0x3500u // Ocarina's scene context block (vk_dlaa_draw)
|
|
#define OCAM_BYTES 0xb0u // its rows 0-10: the view and the projection
|
|
#define DLSS_SIZES 5 // scene and output sizes kept, each with its work images and a DLSS feature (D.sizes)
|
|
// A size no evaluation used for this many goes with its feature (stale_sizes): DLSS's history buffers and the work
|
|
// images at the output size held their VRAM after a switch of the DLSS mode, until four more sizes had come and gone.
|
|
#define SIZE_STALE 3600
|
|
static struct {
|
|
VkExecTarget color, depth; // the armed pair, valid in the frame it was armed
|
|
int armed, done, gbuf; // this frame: armed on the light target, DLAA ran, the G-buffer was drawn
|
|
int ended, up; // this frame: the scene is drawn (jitter over), DLSS wrote the scaled output
|
|
struct Size { // the work images of a scene size and its DLSS feature (dynamic resolution flips it)
|
|
uint32_t w, h, ow, oh; // render size, output size
|
|
VkImage out, mv;
|
|
VmaAllocation out_mem, mv_mem;
|
|
VkImageView out_view, mv_view;
|
|
VkImage sharp; // FSR 3.1's second sharpening pass writes here (fsr3_sharpen), at the output size
|
|
VmaAllocation sharp_mem;
|
|
VkImageView sharp_view;
|
|
VkImage mask, chars[2]; // the bias mask DLSS reads, the character class mask of this and last frame (R8)
|
|
VmaAllocation mask_mem, chars_mem[2];
|
|
VkImageView mask_view, chars_view[2];
|
|
int chars_cur, chars_ok; // chars[chars_cur] is written next; chars_ok: the other holds last frame's
|
|
VkImage dist[2]; // each pixel's view distance, this and last frame (R32F, dlaa_mv.comp depth_out)
|
|
VmaAllocation dist_mem[2];
|
|
VkImageView dist_view[2];
|
|
uint64_t pose_distance_frame;
|
|
float pose_history[4];
|
|
int pose_distance_valid;
|
|
int dist_cur, dist_ok; // as chars_cur and chars_ok
|
|
NgxHandle *feat; // created for this size; NULL while the size runs on another's (range_host) or after a release
|
|
int q; // the quality value feat was created with (perf_quality)
|
|
int ranged; // feat also takes the smaller render sizes from min_w x min_h up (range_note)
|
|
uint32_t min_w, min_h;
|
|
uint64_t used; // evals count when it last ran
|
|
uint64_t feat_used; // evals count when feat last ran: behind used while the size runs on another's
|
|
uint64_t feat_fence; // the submission that last created or evaluated feat (vk_exec_recording_fence)
|
|
} sizes[DLSS_SIZES], *last; // last: the one the previous DLAA frame ran with
|
|
struct Size *host; // the one whose feature that frame ran on: last, or the DLSS mode's (range_host)
|
|
int stepped; // this evaluation: another render size than the last one's, on the same feature
|
|
uint32_t ev_w, ev_h, ev_ow, ev_oh; // the sizes of the previous evaluation, 0 before the first
|
|
uint64_t steps_kept, steps_made; // changes of size that kept the history / that made a feature
|
|
VkImage last_color; // the target DLAA ran on last frame: another one means a new history
|
|
NgxParam *params;
|
|
int failed, reset;
|
|
uint64_t evals, frames_missed;
|
|
int share_off; // frames left that pick the output pass without the mode's share (vk_dlaa_pass, vk_dlaa_frame)
|
|
int shape_logged; // passes of another shape than the scene's turned down as the output pass (vk_dlaa_pass)
|
|
// jitter (vk_dlaa_viewport)
|
|
VkImage jdepth; // the scene depth of the last frame DLAA ran on: passes drawing with it are jittered
|
|
uint32_t jw, jh; // its size: on Ocarina's frame the camera's draws into any D32F depth of it are (jitter_draw)
|
|
float jx, jy; // the offset the next scene draws take, render pixels (window x right, y down)
|
|
float ux, uy; // the offset this frame's scene draws took (DLSS and the motion vectors get these)
|
|
uint32_t jframe;
|
|
int jit_ok, jit_used, hold; // jitter allowed this frame / a draw used it / frames left without it
|
|
uint64_t holds, jittered; // jitter holds; evaluations of a jittered scene
|
|
uint64_t jdraws, jfixed; // draws into a scene-size depth that took the jitter / that did not (full-screen)
|
|
// camera (vk_dlaa_draw) and motion vectors (mv_pass)
|
|
uint64_t cam_addr; // this frame's scene camera block (vk_dlaa_draw), 0: none seen
|
|
uint32_t cam_size, cam_vp; // its size and the offset of its view-projection rows
|
|
struct { // Ocarina: this frame's scene contexts of light-target draws
|
|
uint64_t addr, seq; // seq: vk_exec_submit_seq at the last draw that used it
|
|
uint32_t n; // draws
|
|
int got; // cam holds its rows 0-10 as the first draw read them (vk_usnap_read: 1, 2), 0 not yet
|
|
float cam[OCAM_BYTES / 4];
|
|
} views[8];
|
|
int nviews;
|
|
VkBuffer camb; // two CAM_SLOT slots: this frame's block and last frame's
|
|
VmaAllocation camb_mem;
|
|
VkBuffer mvm; // MVM_SLOTS pairs of blocks for dlaa_mv.comp: the identity and the reprojection (mv_pass)
|
|
VmaAllocation mvm_mem;
|
|
uint8_t *mvm_map;
|
|
uint32_t mvm_slot;
|
|
double cam_prev[16]; // last frame's view-projection (rows 3-6 of its block), for the reprojection
|
|
int cam_slot, cam_prev_ok; // the slot this frame writes / the other slot holds last frame's camera
|
|
VkDescriptorSetLayout mv_dsl;
|
|
VkPipelineLayout mv_layout;
|
|
VkPipeline mv_pipe;
|
|
VkSampler mv_samp;
|
|
int mv_ok; // -1 not built, 0 failed, 1 ready
|
|
uint64_t mv_frames, mv_none, masked; // masked: frames with the characters' bias mask
|
|
uint64_t why[3]; // history resets: scene size changed, no camera, scene target changed
|
|
uint64_t target_kept; // scene target changed and the history stayed (target_changed)
|
|
// frames without a camera and what mv_pass did about them (dlaa_cam_keep, cam_note)
|
|
uint64_t cam_none_draw, cam_none_snap; // no scene-context draw this frame / one, not in the evaluating submission's copy
|
|
uint64_t cam_at_draw; // camera taken from the bytes the draw read, where the evaluating submission lacks them
|
|
uint64_t cam_kept, cam_kept_max; // no camera, history kept with zero motion / runs cut short at KEEP_MAX
|
|
float cut_pos[3], cut_fwd[3]; // last frame's camera position and direction (cam_cut)
|
|
uint64_t cut_ns, cuts; // when it was read; camera cuts that started a new history
|
|
int cut_ok, cut_logged;
|
|
int keep_run; // frames in a row kept without a camera
|
|
uint64_t gap_logged, miss_logged, target_logged, second_logged;
|
|
uint64_t seq_present; // vk_exec_submit_seq at the last present (vk_dlaa_frame)
|
|
uint64_t seconds; // present intervals with a second scene after DLSS ran
|
|
int on; // the dlaa setting, read at each present (the menu switches it live)
|
|
int fgo; // frame generation runs and no upscaler does: inputs alone (fg_only_frame), read at each present
|
|
int preset, preset_dirty; // the DLSS model features are created with (preset_value); a change pending
|
|
VkExecTarget ids; // this frame's G-buffer target 0 (object classes in green), valid with ids_ok
|
|
int ids_ok;
|
|
unsigned cfg_seen; // cfg_version when preset was read
|
|
uint64_t last_eval_ns; // plat_now_ns of the previous evaluation (FrameTimeDeltaInMsec)
|
|
VkFfxImage fg_depth, fg_mv; // the last evaluation's depth and motion vectors, for frame generation
|
|
VkImageView fg_depth_view, fg_mv_view;
|
|
VkDlaaCamera fg_cam; // this frame's camera (mv_pass), fg_cam_ok when known
|
|
int fg_cam_ok;
|
|
float fg_jx, fg_jy;
|
|
int fg_fresh, fg_reset; // fresh: not handed out yet (vk_dlaa_fg_inputs)
|
|
uint64_t presents; // vk_dlaa_frame calls so far
|
|
uint64_t fg_at, hl_at; // presents when the inputs and the HUD-less picture were made (fg_current)
|
|
// the picture without the HUD for frame generation (vk_dlaa_fg_hudless): DLSS's output encoded to sRGB as the game's
|
|
// last pass encodes its picture, then copied bit for bit into an RGBA8 UNORM image, the format the present reads; with
|
|
// HDR on, DLSS's output as RGBA16F, as the game's float picture the present encodes (hl_srgb unused)
|
|
VkImage hl_srgb, hl;
|
|
VmaAllocation hl_srgb_mem, hl_mem;
|
|
VkImageView hl_view;
|
|
uint32_t hl_w, hl_h;
|
|
VkFormat hl_fmt;
|
|
int hl_fresh;
|
|
// before the bloom (NX2NAVI_DLSS_PREBLOOM, prebloom_run)
|
|
int pre_on; // the setting, read at each present
|
|
int pre; // this frame: the upscaler ran before the bloom chain; 1 back into the light target
|
|
// (no upscaling), 2 to the picture's size (PB), whose passes the retarget moves there
|
|
int pre_at; // where the last evaluation ran: 1 before the bloom, 0 at the game's upscaler
|
|
int comp; // this frame: the bloom composite was drawn into PB.comp (vk_dlaa_retarget)
|
|
VkImage comp_game; // the game's target that composite was meant for
|
|
uint32_t pic_w, pic_h; // the size of the game's upscaler's output, the last one seen (vk_dlaa_upscaler)
|
|
uint64_t pre_misses; // frames with the setting on that found no bloom chain to run before
|
|
} D = {.mv_ok = -1, .cfg_seen = ~0u};
|
|
|
|
// What this frame drew into a D32F depth at least DLAA_SCENE_MIN_W wide before DLSS ran (vk_dlaa_draw), for the line a
|
|
// frame without a camera logs (cam_note); cleared at each present (vk_dlaa_frame).
|
|
static struct {
|
|
uint32_t zdraws, zother; // draws into the scene depth / into another such depth than last frame's (jdepth)
|
|
uint32_t zother_w, zother_h;
|
|
uint32_t ocam, ocam_off; // of the scene-depth draws: with the scene context at c3 / of those, not into a scene
|
|
VkFormat ocam_off_fmt; // colour target (the format of the last such)
|
|
uint32_t c3_other, c3_size; // with another block at c3 (the size of the last such; 0: none bound)
|
|
uint64_t seq0; // vk_exec_submit_seq at the frame's first scene-depth draw
|
|
uint32_t one_n, one_w, one_h; // after arming: passes with one colour target and no depth (vk_dlaa_pass), the last's
|
|
VkFormat one_fmt; // size and format, for a frame without the DLAA point (vk_dlaa_frame)
|
|
uint32_t second; // after DLSS ran: passes shaped like the light target's (another scene)
|
|
VkImage second_img;
|
|
} F;
|
|
|
|
// What an evaluation left for frame generation is for the present of its own frame. Frame generation takes it only
|
|
// while its swapchain is in place, so with frame generation off the last evaluation's images stayed marked fresh for
|
|
// as long as it was off. Among them is the game's own depth target, which the game replaces when the scene's size
|
|
// changes: switching the upscaler off and on again with frame generation set (the menu's Upscaling row passes through
|
|
// its choices without one) handed that destroyed image to frame generation's preparation, and AMD's driver ended the
|
|
// process. Images made more than FG_INPUT_PRESENTS presents ago are therefore never handed out, and vk_dlaa_frame
|
|
// drops them when the upscaler goes off or on.
|
|
#define FG_INPUT_PRESENTS 4 // an evaluation is taken at its own present, or the one after when the present thread is behind
|
|
static int fg_current(uint64_t made) { return D.presents - made <= FG_INPUT_PRESENTS; }
|
|
|
|
int vk_dlaa_fg_hudless(VkImage *img, VkImageView *view, uint32_t *w, uint32_t *h, VkFormat *fmt) {
|
|
if (!D.hl_fresh) return 0;
|
|
D.hl_fresh = 0;
|
|
if (!fg_current(D.hl_at)) return 0;
|
|
*img = D.hl, *view = D.hl_view, *w = D.hl_w, *h = D.hl_h, *fmt = D.hl_fmt;
|
|
return 1;
|
|
}
|
|
|
|
int vk_dlaa_fg_views(VkImageView *depth, VkImageView *mv) {
|
|
*depth = D.fg_depth_view, *mv = D.fg_mv_view;
|
|
return D.fg_depth_view && D.fg_mv_view;
|
|
}
|
|
|
|
int vk_dlaa_pose_depth(uint64_t frame, uint32_t w, uint32_t h, VkDescriptorImageInfo *image, float history[4]) {
|
|
const struct Size *last = D.last;
|
|
if (!last || !last->pose_distance_valid || !last->dist_ok || last->w != w || last->h != h ||
|
|
!frame || last->pose_distance_frame != frame - 1 || fabsf(last->pose_history[2]) < 1e-6f) return 0;
|
|
*image = (VkDescriptorImageInfo){D.mv_samp, last->dist_view[last->dist_cur ^ 1], VK_IMAGE_LAYOUT_GENERAL};
|
|
memcpy(history, last->pose_history, sizeof last->pose_history);
|
|
return 1;
|
|
}
|
|
|
|
int vk_dlaa_fg_camera(VkDlaaCamera *c) {
|
|
if (!D.fg_cam_ok) return 0;
|
|
*c = D.fg_cam;
|
|
return 1;
|
|
}
|
|
|
|
// Metres in one unit of Ocarina's view space, for FSR's distance heuristics: one. Measured in Hyrule Field, the camera
|
|
// orbits Link about 4 units behind him (the nearest scene depth, 0.975, is 1 - 0.1 / 4) at about 3 units a second, and
|
|
// travels about 4.7 units a second while he runs; in centimetres (the 0.01 FSR had) he would be 4 cm from the camera.
|
|
#define VIEW_METERS 1.0f
|
|
|
|
int vk_dlaa_ffx_camera(VkFfxCamera *c) {
|
|
if (!D.fg_cam_ok) return 0;
|
|
*c = (VkFfxCamera){D.fg_cam.fov_y, D.fg_cam.znear, D.fg_cam.zfar, VIEW_METERS};
|
|
return 1;
|
|
}
|
|
|
|
int vk_dlaa_fg_inputs(VkFfxImage *depth, VkFfxImage *mv, float *jx, float *jy, int *reset) {
|
|
if (!D.fg_fresh || D.failed) return 0;
|
|
D.fg_fresh = 0;
|
|
if (!fg_current(D.fg_at)) return 0;
|
|
// reset: DLSS started a new history on this frame (evaluate; fg_only_frame by the same rules without an upscaler).
|
|
// jx, jy: 0 without an upscaler, whose frames are not jittered. Handed out as 0 before, so frame generation was never
|
|
// told of a camera cut, another scene size or another target, and interpolated across them: an area load
|
|
// generated frames between two unrelated pictures.
|
|
*depth = D.fg_depth, *mv = D.fg_mv, *jx = D.fg_jx, *jy = D.fg_jy, *reset = D.fg_reset;
|
|
return 1;
|
|
}
|
|
|
|
const VkTexImage *vk_dlaa_swap_img;
|
|
VkImageView vk_dlaa_swap_view;
|
|
|
|
static int dl_active(void) { return N.available && !D.failed && D.on; }
|
|
// The same for vk_exec_draw.c untranslated_dispatch_fallback: while it holds, DLSS or FSR 3.1 scales the scene and the
|
|
// stand-in of the game's own upscaler does not make the picture the screen shows (vk_dlaa_upscaler).
|
|
int vk_dlaa_active(void) { return dl_active(); }
|
|
// The frame is armed and ended (vk_dlaa_draw, vk_dlaa_pass, vk_dlaa_upscaler): the upscaler runs, or frame generation
|
|
// takes the scene's inputs without one (D.fgo, fg_only_frame).
|
|
static int inputs_on(void) { return dl_active() || D.fgo; }
|
|
int vk_dlaa_fg_only(void) { return D.fgo; }
|
|
// The upscaler this frame runs with, as vk_dlaa_frame read it. The menu can set NX2NAVI_TAA to dlss between that and
|
|
// evaluate; read again there while NGX is not up (DLSS starts only after a restart), it sent evaluate to DLSS's path
|
|
// with NGX's functions not loaded, a call to address 0 (issue #325).
|
|
static int use_fsr(void) { return !N.ngx_up && fsr_frame; }
|
|
|
|
static void dl_fail(const char *what, NgxResult r) {
|
|
rt_log("dlaa: %s failed (%08x): DLAA off for this run", what, (unsigned)r);
|
|
D.failed = 1;
|
|
}
|
|
|
|
static int image2d(uint32_t w, uint32_t h, VkFormat f, VkImageUsageFlags usage, VkImage *img, VmaAllocation *mem,
|
|
VkImageView *view) {
|
|
const VkCtx *c = vk_ctx();
|
|
VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = f,
|
|
.extent = {w, h, 1}, .mipLevels = 1, .arrayLayers = 1, .samples = VK_SAMPLE_COUNT_1_BIT,
|
|
.tiling = VK_IMAGE_TILING_OPTIMAL, .usage = usage,
|
|
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
|
|
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
|
if (vmaCreateImage(c->vma, &ii, &ai, img, mem, NULL) != VK_SUCCESS) return 0;
|
|
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = *img,
|
|
.viewType = VK_IMAGE_VIEW_TYPE_2D, .format = f,
|
|
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
|
|
return vkCreateImageView(c->device, &vi, NULL, view) == VK_SUCCESS;
|
|
}
|
|
|
|
// Which features may stay alive when another is made (NX2NAVI_DLSS_IDLE_FREE). A feature made while one with a larger
|
|
// input size is alive leaves that larger one drawing alternating bands of rows black or noise from then on, for as long
|
|
// as it is used; seen for the pause screen's feature (the picture's size in and out), in two ways:
|
|
// - a pause of 3,600 frames frees the gameplay feature (stale_sizes) and the unpause makes it again: every later pause
|
|
// was striped, in each of 8 scripted runs of pause, unpause, pause in which it had been freed and in none of 12 in
|
|
// which it had not, whatever the texture, write-back, HDR and frame generation settings;
|
|
// - a DLSS mode picked in the menu makes a feature of a third size at the unpause, with nothing freed: the next pause
|
|
// was striped (performance to ultra performance in play).
|
|
// Giving the kept feature its own sizes again at each evaluation changed nothing. A feature made while a smaller one
|
|
// is alive leaves the smaller one as it was (the pause screen's after the gameplay one, at every first pause). So when
|
|
// a feature is made, the alive ones with a larger input are released first, and after a release since the last
|
|
// creation all of them (release_kept); their images stay and each is made again when its size is next evaluated.
|
|
// Scripted on one build, each way with the rule and with 2: the long pause and the mode change were striped without it
|
|
// and clean with it. The input size may not be what matters: in every striped case the feature made was of another
|
|
// model (performance's or ultra performance's) than the pause screen's, and the one run in which balanced's feature,
|
|
// of the pause screen's model, was made again beside it without the rule was clean. The rule covers both readings.
|
|
// 1 (default) idle features are freed
|
|
// 0 idle features are never freed (about 0.5 GB more video memory after the first pause)
|
|
// 2 idle features are freed and nothing is released when a feature is made (as before; shows the fault)
|
|
static int feat_released;
|
|
|
|
static int idle_free(void) {
|
|
static int m = -1;
|
|
if (m < 0) m = cfg_int("NX2NAVI_DLSS_IDLE_FREE");
|
|
return m;
|
|
}
|
|
|
|
// The upscalers' waits for the device to be idle before they free what the GPU may still use (vk_dlaa.c, vk_ffx.cpp,
|
|
// vk_ffx4.cpp), with the queue lock held: the present's flip, the completion thread's hold release and every upload
|
|
// submit wait as long. NX2NAVI_TRACE=perf names them "upscaler device waits" on a slow frame's line.
|
|
void vk_upscaler_device_idle(void) {
|
|
uint64_t t0 = perf_mark_begin();
|
|
vk_queue_lock(), vkDeviceWaitIdle(vk_ctx()->device), vk_queue_unlock();
|
|
perf_mark_end(PERF_M_UPWAIT, t0, 0);
|
|
}
|
|
|
|
// A DLSS feature's release, timed as "upscaler releases" (NX2NAVI_TRACE=perf).
|
|
static void feat_release(NgxHandle *f) {
|
|
uint64_t t0 = perf_mark_begin();
|
|
N.release(f);
|
|
perf_mark_end(PERF_M_UPREL, t0, 0);
|
|
}
|
|
|
|
// ---- what the upscalers let go of while the GPU may still use it
|
|
// A size's images and feature (a third scene size, a DLSS model change, a size unused for SIZE_STALE evaluations), the
|
|
// features release_kept and stale_sizes release, a snapshot of the reactive mask, the HUD-less picture of another size,
|
|
// prebloom's images, what retire_idle gives back, and FSR 3.1's and FSR 4's contexts (vk_ffx.cpp, vk_ffx4.cpp
|
|
// context_for) were freed after a wait for the whole device to be idle, on the replaying thread with the queue lock
|
|
// held: the present's flip, the completion thread's hold release and every upload submit waited as long, at each
|
|
// pause-screen size, reload, window size, dynamic resolution step or mode change (16 to 45 ms in play together with a
|
|
// DLSS feature's create). Nothing waits for them now:
|
|
// - images only the replay uses go to vk_memory.c's deferred destruction (vk_defer_image, vk_defer_view), stamped with
|
|
// the submission being recorded, which comes after every submission that can name them;
|
|
// - a DLSS feature goes into dead with the fence of the last submission that created or evaluated it (feat_fence) and
|
|
// is released once the GPU is past it, which is what NGX asks of a feature's release; on this thread, which makes
|
|
// every NGX call (NGX is not thread safe);
|
|
// - an image frame generation can read at a present (a size's motion vectors, the HUD-less picture) goes into dead
|
|
// behind a present gate (FG_LATE_PRESENTS), and from there to the deferred destruction;
|
|
// - FSR contexts go into lists of their own in vk_ffx.cpp and vk_ffx4.cpp, taken at the same points.
|
|
// The lists are taken at every evaluation (evaluate), at every pass while none runs (retire_idle), and where something
|
|
// is let go of (vk_upscaler_let_go). What still waits: a feature or context creation refused while ones let go of are
|
|
// in flight waits for the GPU to pass those (vk_upscaler_gpu_wait), and a full list for the device (vk_upscaler_settle).
|
|
// NX2NAVI_UPSCALER_IDLE_WAIT=1 waits for the device wherever something is let go of, as up to 0.10.1.
|
|
enum { UPD_FEAT = 1, UPD_IMG, UPD_VIEW }; // dead's kinds: a DLSS feature, an image with its allocation (aux), a view
|
|
static UpdList dead; // DLAA's hook thread only (vk_dlaa_pass and what it runs), as evaluate's state
|
|
// A vk_dlaa.c image frame generation can read at a present goes this many presents after it was let go of. present_image
|
|
// (vk_backend.c) calls vk_dlaa_frame before it waits for the present thread to take the job before it, so 3 presents
|
|
// on, the present job that last read it has been submitted and flipped; 1 more for a present from another thread than
|
|
// the submitting one; and 4 more for frame generation that reads it during the flip on queues of its own (vk_dlssg.c:
|
|
// inputs valid until the present), which no timeline value of ours covers. The deferred destruction then stamps it with
|
|
// the submission being recorded, which comes after those presents.
|
|
#define FG_LATE_PRESENTS 8
|
|
|
|
// D.presents from the hook thread (vk_dlaa_frame counts it on the presenting thread).
|
|
static uint64_t presents_now(void) { return __atomic_load_n(&D.presents, __ATOMIC_RELAXED); }
|
|
|
|
// NX2NAVI_UPSCALER_IDLE_WAIT, read once: 1 waits for the device wherever an upscaler lets go of something.
|
|
int vk_upscaler_idle_wait(void) {
|
|
static int m = -1;
|
|
if (m < 0) m = cfg_int("NX2NAVI_UPSCALER_IDLE_WAIT") == 1;
|
|
return m;
|
|
}
|
|
|
|
// One entry of dead, as upd_take hands it out, timed as "upscaler releases". teardown (vk_dlaa_release: the device is
|
|
// idle and about to go): images and views are destroyed at once.
|
|
static void dead_release(const UpdEntry *e, void *teardown) {
|
|
const VkCtx *c = vk_ctx();
|
|
uint64_t t0 = perf_mark_begin();
|
|
if (e->kind == UPD_FEAT) {
|
|
if (N.release) N.release((NgxHandle *)e->handle);
|
|
} else if (e->kind == UPD_VIEW) {
|
|
if (teardown) vkDestroyImageView(c->device, (VkImageView)e->handle, NULL);
|
|
else vk_defer_view((VkImageView)e->handle);
|
|
} else if (e->kind == UPD_IMG) {
|
|
if (teardown) vmaDestroyImage(c->vma, (VkImage)e->handle, (VmaAllocation)e->aux);
|
|
else vk_defer_image((VkImage)e->handle, (VmaAllocation)e->aux);
|
|
}
|
|
perf_mark_end(PERF_M_UPREL, t0, 0);
|
|
}
|
|
|
|
// 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) {
|
|
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
|
|
// present can read keeps its present gate: the device's idle does not cover a present still to come.
|
|
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) {
|
|
if (vk_upscaler_idle_wait()) vk_upscaler_settle();
|
|
else upscalers_drain();
|
|
}
|
|
|
|
uint64_t vk_upscaler_gpu_wait(uint64_t v) {
|
|
const uint64_t issued = vk_sync_issued();
|
|
if (vk_upscaler_idle_wait()) {
|
|
vk_upscaler_device_idle();
|
|
return issued;
|
|
}
|
|
if (v > issued) v = issued; // a submission still being recorded cannot finish while this thread records it
|
|
if (v <= vk_sync_fence_done()) return v;
|
|
uint64_t t0 = perf_mark_begin();
|
|
vk_queue_lock(), vk_queue_unlock(); // the replay's held submissions go to the queue (gfx_present.c vk_queue_lock)
|
|
// a wait on the timeline from the host touches no queue, so no queue's lock is needed (unlike the device's idle)
|
|
const VkCtx *c = vk_ctx();
|
|
VkSemaphoreWaitInfo wi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, .semaphoreCount = 1,
|
|
.pSemaphores = &c->timeline, .pValues = &v};
|
|
VkResult r = vkWaitSemaphores(c->device, &wi, 2000000000ull);
|
|
perf_mark_end(PERF_M_UPWAIT, t0, 0);
|
|
if (r == VK_SUCCESS) return v;
|
|
vk_device_lost_check(r, "the upscaler's wait for the GPU");
|
|
rt_log("dlaa: the wait for the GPU to pass submission %llu failed (%d): waiting for the device instead",
|
|
(unsigned long long)v, (int)r);
|
|
vk_upscaler_device_idle();
|
|
return issued;
|
|
}
|
|
|
|
static void dead_put(int kind, void *h, void *aux, uint64_t fence, uint64_t presents) {
|
|
if (upd_put(&dead, kind, h, aux, fence, presents)) return;
|
|
// Full, which the GPU's progress and the presents do not let happen: room after a wait for the device, and if a
|
|
// present gate still holds every entry, they all go as before this list, the device idle.
|
|
static int told;
|
|
if (told++ < 4) rt_log("dlaa: %d things DLSS let go of still wait for the GPU or a present: a wait for the device",
|
|
UPD_SLOTS);
|
|
vk_upscaler_settle();
|
|
if (upd_put(&dead, kind, h, aux, fence, presents)) return;
|
|
upd_take_all(&dead, dead_release, NULL);
|
|
upd_put(&dead, kind, h, aux, fence, presents);
|
|
}
|
|
|
|
// A size let go of while the GPU may still use it: the images only the replay uses to the deferred destruction, the
|
|
// motion vectors (frame generation's, D.fg_mv) behind the present gate, the feature once the GPU is past its last
|
|
// create or evaluation. The entry is free at once.
|
|
static void retire_size(struct Size *e) {
|
|
vk_defer_view(e->out_view), vk_defer_image(e->out, e->out_mem);
|
|
vk_defer_view(e->sharp_view), vk_defer_image(e->sharp, e->sharp_mem);
|
|
vk_defer_view(e->mask_view), vk_defer_image(e->mask, e->mask_mem);
|
|
for (int i = 0; i < 2; i++) {
|
|
vk_defer_view(e->chars_view[i]), vk_defer_image(e->chars[i], e->chars_mem[i]);
|
|
vk_defer_view(e->dist_view[i]), vk_defer_image(e->dist[i], e->dist_mem[i]);
|
|
}
|
|
const uint64_t gate = presents_now() + FG_LATE_PRESENTS;
|
|
dead_put(UPD_VIEW, e->mv_view, NULL, 0, gate); // a view before its image: upd_take and vk_memory.c keep the order
|
|
dead_put(UPD_IMG, e->mv, e->mv_mem, 0, gate);
|
|
if (e->feat) dead_put(UPD_FEAT, e->feat, NULL, e->feat_fence, 0), feat_released = 1;
|
|
if (D.host == e) D.host = NULL;
|
|
memset(e, 0, sizeof *e);
|
|
}
|
|
|
|
// A size's feature alone (release_kept, stale_sizes), released once the GPU is past its last create or evaluation.
|
|
// count: it counts as a release for release_kept's next call (feat_released).
|
|
static void retire_feat(struct Size *s, int count) {
|
|
dead_put(UPD_FEAT, s->feat, NULL, s->feat_fence, 0);
|
|
s->feat = NULL;
|
|
if (count) feat_released = 1;
|
|
}
|
|
|
|
// The images and views of a size, then its feature. The device must be idle, or the GPU past every use of them:
|
|
// vk_dlaa_release (teardown), idle_release (unused for IDLE_FREE_PRESENTS presents). Whatever was not made is
|
|
// skipped, so a half-made size goes the same way.
|
|
static void destroy_size(struct Size *e) {
|
|
const VkCtx *c = vk_ctx();
|
|
if (e->out_view) vkDestroyImageView(c->device, e->out_view, NULL);
|
|
if (e->mv_view) vkDestroyImageView(c->device, e->mv_view, NULL);
|
|
if (e->out) vmaDestroyImage(c->vma, e->out, e->out_mem);
|
|
if (e->mv) vmaDestroyImage(c->vma, e->mv, e->mv_mem);
|
|
if (e->sharp_view) vkDestroyImageView(c->device, e->sharp_view, NULL);
|
|
if (e->sharp) vmaDestroyImage(c->vma, e->sharp, e->sharp_mem);
|
|
if (e->mask_view) vkDestroyImageView(c->device, e->mask_view, NULL);
|
|
if (e->mask) vmaDestroyImage(c->vma, e->mask, e->mask_mem);
|
|
for (int i = 0; i < 2; i++) {
|
|
if (e->chars_view[i]) vkDestroyImageView(c->device, e->chars_view[i], NULL);
|
|
if (e->chars[i]) vmaDestroyImage(c->vma, e->chars[i], e->chars_mem[i]);
|
|
if (e->dist_view[i]) vkDestroyImageView(c->device, e->dist_view[i], NULL);
|
|
if (e->dist[i]) vmaDestroyImage(c->vma, e->dist[i], e->dist_mem[i]);
|
|
}
|
|
if (e->feat && N.release) feat_release(e->feat), feat_released = 1;
|
|
if (D.host == e) D.host = NULL;
|
|
memset(e, 0, sizeof *e);
|
|
}
|
|
|
|
// A third scene size takes the least recently used one's entry (size_for): its images may still be in flight.
|
|
static void free_size(struct Size *e) {
|
|
retire_size(e);
|
|
vk_upscaler_let_go();
|
|
}
|
|
|
|
// The entry for a scene and output size: its output (RGBA16F at the output size, written by DLSS, then blitted into
|
|
// the target) and motion vectors (RG16F at the scene size, render pixels), both kept in GENERAL like every image of the
|
|
// back end. NX2navi's dynamic resolution switches between a few sizes; each keeps its images, and its own feature
|
|
// unless it runs on the DLSS mode's (range_host); the least recently used goes for a new one.
|
|
static struct Size *size_for(VkCommandBuffer cb, uint32_t w, uint32_t h, uint32_t ow, uint32_t oh) {
|
|
struct Size *e = NULL, *lru = &D.sizes[0];
|
|
for (int i = 0; i < DLSS_SIZES; i++) {
|
|
if (D.sizes[i].w == w && D.sizes[i].h == h && D.sizes[i].ow == ow && D.sizes[i].oh == oh) return &D.sizes[i];
|
|
if (!D.sizes[i].out && !e) e = &D.sizes[i];
|
|
if (D.sizes[i].used < lru->used) lru = &D.sizes[i];
|
|
}
|
|
if (!e) {
|
|
if (D.last == lru) D.last = NULL;
|
|
free_size(e = lru);
|
|
}
|
|
VkImageUsageFlags u = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
|
|
VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
VkImageUsageFlags u8 = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
if (!image2d(ow, oh, VK_FORMAT_R16G16B16A16_SFLOAT, u, &e->out, &e->out_mem, &e->out_view) ||
|
|
!image2d(w, h, VK_FORMAT_R16G16_SFLOAT, u | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, &e->mv, &e->mv_mem, &e->mv_view) ||
|
|
!image2d(w, h, VK_FORMAT_R8_UNORM, u8, &e->mask, &e->mask_mem, &e->mask_view) ||
|
|
!image2d(w, h, VK_FORMAT_R8_UNORM, u8, &e->chars[0], &e->chars_mem[0], &e->chars_view[0]) ||
|
|
!image2d(w, h, VK_FORMAT_R8_UNORM, u8, &e->chars[1], &e->chars_mem[1], &e->chars_view[1]) ||
|
|
!image2d(w, h, VK_FORMAT_R32_SFLOAT, u8, &e->dist[0], &e->dist_mem[0], &e->dist_view[0]) ||
|
|
!image2d(w, h, VK_FORMAT_R32_SFLOAT, u8, &e->dist[1], &e->dist_mem[1], &e->dist_view[1]))
|
|
return NULL;
|
|
VkImageMemoryBarrier2 ib[7];
|
|
VkImage im[7] = {e->out, e->mv, e->mask, e->chars[0], e->chars[1], e->dist[0], e->dist[1]};
|
|
for (int i = 0; i < 7; i++)
|
|
ib[i] = (VkImageMemoryBarrier2){.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
|
|
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
|
|
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = im[i],
|
|
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
|
|
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 7,
|
|
.pImageMemoryBarriers = ib};
|
|
vkCmdPipelineBarrier2(cb, &di);
|
|
e->w = w, e->h = h, e->ow = ow, e->oh = oh;
|
|
return e;
|
|
}
|
|
|
|
static void full_barrier(VkCommandBuffer cb) {
|
|
VkMemoryBarrier2 mb = {.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2,
|
|
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.srcAccessMask = VK_ACCESS_2_MEMORY_WRITE_BIT,
|
|
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT};
|
|
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .memoryBarrierCount = 1, .pMemoryBarriers = &mb};
|
|
vkCmdPipelineBarrier2(cb, &di);
|
|
}
|
|
|
|
static NgxResourceVK res_image(VkImageView v, VkImage img, VkFormat f, uint32_t w, uint32_t h, VkImageAspectFlags a,
|
|
int rw) {
|
|
NgxResourceVK r;
|
|
memset(&r, 0, sizeof r);
|
|
r.r.img = (NgxImageView){v, img, {a, 0, 1, 0, 1}, f, w, h};
|
|
r.type = 0;
|
|
r.rw = rw;
|
|
return r;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- timing (NX2NAVI_DLAA_TIMES=1)
|
|
// GPU timestamps around the camera motion dispatch, the per-object motion pass and DLSS's evaluation, and the CPU
|
|
// time of NGX's call and of draining the recorder before it, summed and logged every TIMES_SUMMARY frames:
|
|
// dlaa times: gpu camera motion a ms, object motion b ms, DLSS c ms (frames timed); cpu: recorder drain d ms, NGX
|
|
// evaluate e ms, motion pass recording f ms
|
|
// Each frame takes its own slot of TIMES_SLOTS; a slot's results are read when it comes round again (by then the GPU is
|
|
// long done with it; not ready: that frame is left out).
|
|
enum { T_MV0, T_MV1, T_OBJ, T_DL0, T_DL1, T_N };
|
|
#define TIMES_SLOTS 8u
|
|
#define TIMES_SUMMARY 600
|
|
static struct {
|
|
int on; // -1 not read
|
|
VkQueryPool pool;
|
|
uint32_t slot;
|
|
int used[TIMES_SLOTS]; // the slot holds a frame's timestamps (all T_N written)
|
|
double gpu[3]; // ms summed: camera motion, object motion, DLSS
|
|
uint64_t frames, cpu_drain, cpu_ngx, cpu_mv; // ns summed
|
|
} T = {.on = -1};
|
|
|
|
static int times_on(void) {
|
|
if (T.on < 0) {
|
|
T.on = rt_knob("DLAA_TIMES", "0");
|
|
VkQueryPoolCreateInfo qi = {.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO,
|
|
.queryType = VK_QUERY_TYPE_TIMESTAMP, .queryCount = TIMES_SLOTS * T_N};
|
|
if (T.on && vkCreateQueryPool(vk_ctx()->device, &qi, NULL, &T.pool) != VK_SUCCESS) T.on = 0;
|
|
}
|
|
return T.on;
|
|
}
|
|
|
|
static void times_mark(VkCommandBuffer cb, int which) {
|
|
if (times_on()) vkCmdWriteTimestamp2(cb, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, T.pool, T.slot * T_N + (uint32_t)which);
|
|
}
|
|
|
|
// At the start of a frame's timing (mv_pass): the slot's last results are read and it is reset.
|
|
static void times_begin(VkCommandBuffer cb) {
|
|
if (!times_on()) return;
|
|
T.slot = (T.slot + 1) % TIMES_SLOTS;
|
|
if (T.used[T.slot]) {
|
|
uint64_t q[T_N];
|
|
if (vkGetQueryPoolResults(vk_ctx()->device, T.pool, T.slot * T_N, T_N, sizeof q, q, sizeof q[0],
|
|
VK_QUERY_RESULT_64_BIT) == VK_SUCCESS) {
|
|
double ns = vk_ctx()->props.limits.timestampPeriod / 1e6; // ticks to ms
|
|
T.gpu[0] += (double)(q[T_MV1] - q[T_MV0]) * ns, T.gpu[1] += (double)(q[T_OBJ] - q[T_MV1]) * ns;
|
|
T.gpu[2] += (double)(q[T_DL1] - q[T_DL0]) * ns;
|
|
if (++T.frames % TIMES_SUMMARY == 0) {
|
|
rt_log("dlaa times: gpu camera motion %.3f ms, object motion %.3f ms, DLSS %.3f ms (%llu frames timed); "
|
|
"cpu: recorder drain %.3f ms, NGX evaluate %.3f ms, motion pass recording %.3f ms",
|
|
T.gpu[0] / TIMES_SUMMARY, T.gpu[1] / TIMES_SUMMARY, T.gpu[2] / TIMES_SUMMARY,
|
|
(unsigned long long)T.frames, T.cpu_drain / 1e6 / TIMES_SUMMARY, T.cpu_ngx / 1e6 / TIMES_SUMMARY,
|
|
T.cpu_mv / 1e6 / TIMES_SUMMARY);
|
|
T.gpu[0] = T.gpu[1] = T.gpu[2] = 0, T.cpu_drain = T.cpu_ngx = T.cpu_mv = 0;
|
|
}
|
|
}
|
|
}
|
|
T.used[T.slot] = 0;
|
|
vkCmdResetQueryPool(cb, T.pool, T.slot * T_N, T_N);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- motion vectors
|
|
// The camera's motion first (runtime/shaders/dlaa_mv.comp): this frame's scene camera block is copied out of the
|
|
// replay's uniform snapshot (vk_usnap.c, as of the submit call) into one slot of camb, last frame's stays in the other,
|
|
// and the compute pass reprojects every pixel's depth through the two (inv4 and mv_pass below). Then the per-object
|
|
// pass (vk_exec_draw.c, vk_mvobj_execute) writes the motion of whatever moves on its own over it.
|
|
#define CAM_BYTES 256u // vec4 rows 0-15: view, view-projection (3-6), projection, inverse view
|
|
#define CAM_BLOCK 0x940u // the whole block, kept for the per-object motion pass's vertex shaders (vk_exec_draw.c)
|
|
#define CAM_SLOT 0xa00u // camb slot stride
|
|
#define MVM_SLOTS 16u // frames the GPU may still be reading the reprojection blocks of
|
|
|
|
// dlaa_mv.comp's push constants in its layout (std430: dz at offset 32)
|
|
typedef struct {
|
|
float size[2], jitter[2], bias, prev_ok, zfit, zprev_ok;
|
|
float dz[4]; // the projection's [2][2], [2][3], [3][2], [3][3]
|
|
float zmask;
|
|
} MvPush;
|
|
|
|
static int mv_init(void) {
|
|
if (D.mv_ok >= 0) return D.mv_ok;
|
|
D.mv_ok = 0;
|
|
const VkCtx *c = vk_ctx();
|
|
VkDescriptorSetLayoutBinding b[10] = {
|
|
{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{2, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{3, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{4, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{5, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{6, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{7, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{8, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{9, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL}};
|
|
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
|
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
|
|
.bindingCount = 10, .pBindings = b};
|
|
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(MvPush)};
|
|
VkSamplerCreateInfo si = {.sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, .magFilter = VK_FILTER_NEAREST,
|
|
.minFilter = VK_FILTER_NEAREST, .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
|
|
.addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
|
.addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
|
|
.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE};
|
|
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof dlaa_mv_spv,
|
|
.pCode = dlaa_mv_spv};
|
|
VkShaderModule m = VK_NULL_HANDLE;
|
|
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = 2 * CAM_SLOT,
|
|
.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
|
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
|
|
if (vkCreateDescriptorSetLayout(c->device, &dci, NULL, &D.mv_dsl) != VK_SUCCESS) goto fail;
|
|
VkBufferCreateInfo mbi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = MVM_SLOTS * 2 * CAM_BYTES,
|
|
.usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT};
|
|
VmaAllocationCreateInfo mai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
|
|
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT |
|
|
VMA_ALLOCATION_CREATE_MAPPED_BIT};
|
|
VmaAllocationInfo minfo;
|
|
if (vmaCreateBuffer(c->vma, &mbi, &mai, &D.mvm, &D.mvm_mem, &minfo) != VK_SUCCESS) goto fail;
|
|
D.mvm_map = minfo.pMappedData;
|
|
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
|
.pSetLayouts = &D.mv_dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
|
|
if (vkCreatePipelineLayout(c->device, &li, NULL, &D.mv_layout) != VK_SUCCESS ||
|
|
vkCreateSampler(c->device, &si, NULL, &D.mv_samp) != VK_SUCCESS ||
|
|
vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS ||
|
|
vmaCreateBuffer(c->vma, &bi, &ai, &D.camb, &D.camb_mem, NULL) != VK_SUCCESS)
|
|
goto fail;
|
|
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
|
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
|
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
|
|
.layout = D.mv_layout};
|
|
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &D.mv_pipe);
|
|
vkDestroyShaderModule(c->device, m, NULL);
|
|
m = VK_NULL_HANDLE;
|
|
if (r != VK_SUCCESS) goto fail;
|
|
return D.mv_ok = 1;
|
|
fail:
|
|
if (m) vkDestroyShaderModule(c->device, m, NULL); // made before a later step failed
|
|
rt_log("dlaa: the motion-vector pass could not be set up: DLAA runs with zero motion");
|
|
return 0;
|
|
}
|
|
|
|
// The reprojection from this frame's clip space to last frame's, last * inverse(this), in double precision: the shader
|
|
// took each pixel back to world space through a float inverse of the view-projection, and with NX2navi's world
|
|
// coordinates in the thousands and its far plane the result was noise of a fraction of a pixel and more on distant
|
|
// surfaces, which DLSS followed into a shimmer like heat haze (roofs and trees, most of all in the upscaling modes).
|
|
// The shader is unchanged: its "this frame" block gets the identity and its "last frame" block this matrix, so it
|
|
// applies just the one matrix to each pixel. A still camera gives the identity, and zero motion exactly.
|
|
static int inv4(const double m[16], double o[16]) {
|
|
double a[4][8];
|
|
for (int r = 0; r < 4; r++)
|
|
for (int k = 0; k < 8; k++) a[r][k] = k < 4 ? m[r * 4 + k] : (k - 4 == r);
|
|
for (int col = 0; col < 4; col++) {
|
|
int piv = col;
|
|
for (int r = col + 1; r < 4; r++)
|
|
if (fabs(a[r][col]) > fabs(a[piv][col])) piv = r;
|
|
if (fabs(a[piv][col]) < 1e-300) return 0;
|
|
for (int k = 0; k < 8; k++) { double t = a[col][k]; a[col][k] = a[piv][k]; a[piv][k] = t; }
|
|
double d = a[col][col];
|
|
for (int k = 0; k < 8; k++) a[col][k] /= d;
|
|
for (int r = 0; r < 4; r++)
|
|
if (r != col) {
|
|
double f = a[r][col];
|
|
for (int k = 0; k < 8; k++) a[r][k] -= f * a[col][k];
|
|
}
|
|
}
|
|
for (int r = 0; r < 4; r++)
|
|
for (int k = 0; k < 4; k++) o[r * 4 + k] = a[r][k + 4];
|
|
return 1;
|
|
}
|
|
|
|
// Ocarina's camera from its scene context block (vk_dlaa_draw): its scene vertex shaders take the world position to
|
|
// the camera through rows 0-2 (a 3x4 view) and then to clip space through rows 7-10 (the projection, 4x4), so the
|
|
// view-projection is rows 7-10 times rows 0-2.
|
|
static void ocam_vp(const float *v, double out[16]) {
|
|
const float *P = v + 7 * 4;
|
|
double V[16] = {v[0], v[1], v[2], v[3], v[4], v[5], v[6], v[7], v[8], v[9], v[10], v[11], 0, 0, 0, 1};
|
|
for (int r = 0; r < 4; r++)
|
|
for (int k = 0; k < 4; k++) {
|
|
double acc = 0;
|
|
for (int j = 0; j < 4; j++) acc += (double)P[r * 4 + j] * V[j * 4 + k];
|
|
out[r * 4 + k] = acc;
|
|
}
|
|
}
|
|
|
|
// Frame generation's camera (vk_dlaa_fg_camera) from the Ocarina block: the projection is rows 7-10, the view rows 0-2
|
|
// (rows: right, up, back; the camera looks down -z), m the reprojection from this frame's clip space to last frame's.
|
|
// Each matrix is handed out transposed, for row vectors.
|
|
static void fg_store(float out[16], const double m[16]) {
|
|
for (int r = 0; r < 4; r++)
|
|
for (int k = 0; k < 4; k++) out[r * 4 + k] = (float)m[k * 4 + r];
|
|
}
|
|
// The view distance in front of the camera at which the projection P (row-major, column vectors, looking down -z) gives
|
|
// clip depth z_ndc = k: (P[10] z + P[11]) / (P[14] z + P[15]) = k, solved for z. INFINITY where no distance gives it.
|
|
static double plane_dist(const double P[16], double k) {
|
|
double den = P[10] - k * P[14];
|
|
if (fabs(den) <= 1e-9 * (fabs(P[10]) + fabs(P[14]))) return INFINITY;
|
|
return -(k * P[15] - P[11]) / den;
|
|
}
|
|
|
|
// What the depth buffer's 0 and 1 stand for, from the frame's projection and the device's depth mode. With NVN's
|
|
// NEAR_IS_MINUS_W (clip depth -w..w, which the pipelines keep through negativeOneToOne, vk_pipeline.c) a fragment
|
|
// stores z_ndc * 0.5 + 0.5, so 0 is z_ndc = -1; with NEAR_IS_ZERO it stores z_ndc. Both are 0..1 in the D32F target
|
|
// (the viewport's depth range, vk_exec_draw.c), and the -w..w mapping stores exactly what a 0..w projection with the
|
|
// same planes would: (f / (f - n)) (1 - n / distance), the convention DLSS, Streamline and FSR take. Ocarina's planes
|
|
// are not fixed: in Hyrule Field its z row is (-1.00001, -0.200001), near 0.1 and far about 25,000 (the view's units
|
|
// are metres), and the camera before it had far about 1,000. Taken as 0.1 and 1e8 (no far plane), a distance d reads
|
|
// as (f - n) d / (f - d): 1,000 as 1,042, 10,000 as 16,650, and the far plane's neighbourhood without bound. So the
|
|
// planes come from the projection every frame; a projection whose stored 0 is not in front of the camera keeps the last
|
|
// frame's planes.
|
|
static void proj_planes(const double P[16], VkDlaaCamera *c) {
|
|
int minus_w = nvn_depth_minus_w();
|
|
double at0 = plane_dist(P, minus_w ? -1.0 : 0.0), at1 = plane_dist(P, 1.0); // the distances stored as 0 and 1
|
|
int inverted = !(at0 < at1); // reversed depth: the near plane is stored as 1
|
|
double n = inverted ? at1 : at0, f = inverted ? at0 : at1;
|
|
if (!(n > 0) || !isfinite(n)) return;
|
|
c->inverted = inverted;
|
|
c->znear = (float)n;
|
|
c->infinite = !(f < VK_DLAA_FAR_INFINITE);
|
|
c->zfar = c->infinite ? VK_DLAA_FAR_INFINITE : (float)f;
|
|
// one line at the start and whenever the planes change (the first 20 times)
|
|
static float logged_n, logged_f;
|
|
static int logged_inv = -1, lines;
|
|
if (lines < 20 && (inverted != logged_inv || fabsf(c->znear - logged_n) > 1e-4f * logged_n ||
|
|
fabsf(c->zfar - logged_f) > 1e-4f * logged_f)) {
|
|
lines++, logged_inv = inverted, logged_n = c->znear, logged_f = c->zfar;
|
|
char fz[32];
|
|
if (c->infinite) snprintf(fz, sizeof fz, "infinite");
|
|
else snprintf(fz, sizeof fz, "%.6g", c->zfar);
|
|
rt_log("dlaa: the scene's projection: near %.6g, far %s, depth %s, clip depth %s (z row %.6g %.6g %.6g %.6g, "
|
|
"w row %.6g %.6g %.6g %.6g)", c->znear, fz, inverted ? "inverted (1 near)" : "0 near, 1 far",
|
|
minus_w ? "-w..w" : "0..w", P[8], P[9], P[10], P[11], P[12], P[13], P[14], P[15]);
|
|
}
|
|
}
|
|
|
|
static void fg_camera(const float *v, const double m[16]) {
|
|
double P[16], Pi[16], mi[16];
|
|
for (int i = 0; i < 16; i++) P[i] = v[28 + i];
|
|
if (!inv4(P, Pi) || !inv4(m, mi)) return;
|
|
VkDlaaCamera *c = &D.fg_cam;
|
|
fg_store(c->view_to_clip, P), fg_store(c->clip_to_view, Pi);
|
|
fg_store(c->clip_to_prev_clip, m), fg_store(c->prev_clip_to_clip, mi);
|
|
for (int i = 0; i < 3; i++) {
|
|
c->right[i] = v[i], c->up[i] = v[4 + i], c->fwd[i] = -v[8 + i];
|
|
// the position: -R^T t, R the view's 3x3 and t its last column
|
|
c->pos[i] = -(v[i] * v[3] + v[4 + i] * v[7] + v[8 + i] * v[11]);
|
|
}
|
|
c->fov_y = P[5] != 0 ? 2.0f * (float)atan(1.0 / fabs(P[5])) : 1.0f;
|
|
c->aspect = P[0] != 0 ? (float)fabs(P[5] / P[0]) : 1.777f;
|
|
if (!(c->znear > 0)) c->znear = 0.1f, c->zfar = VK_DLAA_FAR_INFINITE, c->infinite = 1; // until a projection gives them
|
|
proj_planes(P, c);
|
|
D.fg_cam_ok = 1;
|
|
// NX2NAVI_MOUSE_TRACE=1 (test runs, mouse_direct.c): the camera's direction each frame
|
|
static int trace = -1;
|
|
if (trace < 0) {
|
|
const char *e = cfg_get("NX2NAVI_MOUSE_TRACE");
|
|
trace = e && *e && strcmp(e, "0");
|
|
}
|
|
if (trace)
|
|
rt_log("mousetrace: cam t=%.3f fwd=%.6f,%.6f,%.6f pos=%.3f,%.3f,%.3f", plat_now_ns() / 1e6, c->fwd[0], c->fwd[1],
|
|
c->fwd[2], c->pos[0], c->pos[1], c->pos[2]);
|
|
}
|
|
|
|
// Frames without a camera (dlaa_cam_keep, NX2NAVI_DLAA_CAM_KEEP; 1 by default, 0 restores the old handling). Measured in
|
|
// Kokiri Forest during dialogue (Saria, dlss = performance): about 1 frame in 20 had no camera, and each started a new
|
|
// history twice, on that frame and on the next (nothing to reproject from): 24 to 26 resets per 300 frames. Those were
|
|
// frames the event order held past their present (vk_exec.c), which then ran in a submission without a uniform copy
|
|
// (the present thread's empty one), so the lookup at DLSS found nothing. With the change: no camera reset there in
|
|
// 3600 frames but the first. Two things change:
|
|
// - Ocarina's camera rows are taken from the bytes the scene draws themselves read (vk_dlaa_draw, views[].cam), not
|
|
// looked up again in the uniform copy of the submission DLSS runs in, which on some frames has none or lost the
|
|
// block to a GPU write.
|
|
// - A frame that has no camera at all keeps DLSS's history: its vectors are zero (the camera taken as still) and last
|
|
// frame's camera stays the one the next frame reprojects from. At most KEEP_MAX frames in a row: a longer stretch
|
|
// is something else (a cut scene with a camera of its own), and a still history there would trail what moves.
|
|
#define KEEP_MAX 6
|
|
|
|
static const char *const usnap_why[] = {"found", "no copy in this submission", "dropped by a GPU write in this submission",
|
|
"left out: a GPU write in flight", "not memory the CPU writes",
|
|
"not bound in this submission"};
|
|
|
|
// One line for each of the first frames without a camera: what the frame drew into the scene depth, which submission
|
|
// the scene context was seen in and why the evaluating one lacks it, and what was done.
|
|
static void cam_note(int view, int kept) {
|
|
uint64_t seq = vk_exec_submit_seq;
|
|
char snap[200] = "";
|
|
if (D.cam_addr)
|
|
snprintf(snap, sizeof snap, "; scene context %#llx last drawn %llu submission(s) before DLSS's, there: %s",
|
|
(unsigned long long)D.cam_addr, (unsigned long long)(view >= 0 ? seq - D.views[view].seq : 0),
|
|
usnap_why[vk_usnap_why(D.cam_addr, D.cam_size == OCAM_BLOCK ? OCAM_BYTES : D.cam_size)]);
|
|
rt_log("dlaa: frame %llu without a camera: %llu scene-depth draws over %llu submission(s) (%u into another D32 "
|
|
"depth %ux%u), %u with the scene context (%u not into a scene colour target, format %d), %u with another c3 "
|
|
"block (last size %#x)%s; %s",
|
|
(unsigned long long)__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1, (unsigned long long)F.zdraws,
|
|
(unsigned long long)(F.zdraws + F.zother ? seq - F.seq0 + 1 : 0), F.zother, F.zother_w, F.zother_h, F.ocam,
|
|
F.ocam_off, (int)F.ocam_off_fmt, F.c3_other, F.c3_size, snap,
|
|
kept == 1 ? "history kept, zero motion" : kept == 2 ? "new history (more than KEEP_MAX in a row)"
|
|
: "new history");
|
|
}
|
|
|
|
// Camera cuts (NX2NAVI_DLAA_CUT, default 1; 0: off). Only a lost camera (mv_pass), another scene size or another
|
|
// target started a new history, so DLSS and frame generation (vk_dlaa_fg_inputs) carried their history across a camera
|
|
// that jumped: an area load or a spawn moves it 5 to 180 m in one frame. Such a frame now starts a new history, as
|
|
// NVIDIA's DLSS guide asks for a camera cut: a move over 5 m, or a turn over 10 degrees together with a move over 0.5 m,
|
|
// both per 60 Hz frame and scaled by the time since the last camera. Camera traces of play in the Fire Temple
|
|
// (NX2NAVI_MOUSE_TRACE) had a median of 0.05 m and 0.08 degrees a frame. A turn alone (the mouse camera, which orbits
|
|
// about 3 m from Link) does not count, and the turn with a move counts only from CUT_TURN degrees: a quick swing of the
|
|
// orbiting camera turns 11 to 18 degrees and moves 0.9 to 1.4 m a frame, and taken for a cut at 10 degrees it started a
|
|
// new history at every frame of the swing (nine in a row in Hyrule Field), which at FSR's or DLSS's smaller modes showed
|
|
// the scene at its render size for those frames, blocky and flickering. Its motion vectors carry it as well as any other
|
|
// turn. Each cut is logged, up to 24.
|
|
#define CUT_TURN 30.0
|
|
// v: the Ocarina scene context's rows 0-2, the view (fg_camera).
|
|
static void cam_cut(const float *v) {
|
|
static int on = -1;
|
|
if (on < 0) on = rt_knob("DLAA_CUT", "1");
|
|
float pos[3], fwd[3];
|
|
for (int i = 0; i < 3; i++) {
|
|
fwd[i] = -v[8 + i];
|
|
pos[i] = -(v[i] * v[3] + v[4 + i] * v[7] + v[8 + i] * v[11]);
|
|
}
|
|
uint64_t now = plat_now_ns();
|
|
if (on && D.cut_ok) {
|
|
double dot = fwd[0] * D.cut_fwd[0] + fwd[1] * D.cut_fwd[1] + fwd[2] * D.cut_fwd[2];
|
|
double turn = acos(dot > 1 ? 1 : dot < -1 ? -1 : dot) * (180.0 / 3.14159265358979);
|
|
double dx = pos[0] - D.cut_pos[0], dy = pos[1] - D.cut_pos[1], dz = pos[2] - D.cut_pos[2];
|
|
double move = sqrt(dx * dx + dy * dy + dz * dz);
|
|
double k = (now - D.cut_ns) / 1e6 / (1000.0 / 60); // 60 Hz frames since the last camera
|
|
if (k < 0.25) k = 0.25;
|
|
if (k > 4) k = 4;
|
|
if (move > 5.0 * k || (turn > CUT_TURN * k && move > 0.5 * k)) {
|
|
D.reset = 1, D.cuts++;
|
|
if (D.cut_logged < 24)
|
|
D.cut_logged++, rt_log("dlaa: frame %llu: camera cut (moved %.2f m, turned %.1f degrees in %.1f ms): new "
|
|
"history (%llu so far)",
|
|
(unsigned long long)__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1,
|
|
move, turn, (now - D.cut_ns) / 1e6, (unsigned long long)D.cuts);
|
|
}
|
|
}
|
|
memcpy(D.cut_pos, pos, sizeof pos), memcpy(D.cut_fwd, fwd, sizeof fwd);
|
|
D.cut_ns = now, D.cut_ok = 1;
|
|
}
|
|
|
|
// The pass, recorded through the recorder like the replay's own commands. 0: no camera this frame (the vectors stay
|
|
// zero and DLSS starts a new history); 2: no camera, but the history is kept (the vectors stay zero, dlaa_cam_keep).
|
|
// bias > 0 with this frame's G-buffer classes (D.ids) also writes e->mask, the bias mask (dlaa_mv.comp); *masked says
|
|
// whether it did.
|
|
static int mv_pass(VkExec *ex, struct Size *e, VkImageView zv, float jx, float jy, float bias, int *masked) {
|
|
*masked = 0;
|
|
VkCommandBuffer cb = ex->cb;
|
|
uint32_t w = e->w, h = e->h;
|
|
static int keep = -1;
|
|
if (keep < 0) keep = rt_knob("DLAA_CAM_KEEP", "1");
|
|
VkDescriptorBufferInfo cam;
|
|
int view = -1; // Ocarina: the views[] entry of the camera
|
|
for (int i = 0; D.cam_size == OCAM_BLOCK && D.cam_addr && i < D.nviews; i++)
|
|
if (D.views[i].addr == D.cam_addr) view = i;
|
|
const float *held = keep && view >= 0 && D.views[view].got ? D.views[view].cam : NULL;
|
|
int found = mv_init() && D.cam_addr && vk_usnap_find(D.cam_addr, D.cam_size == OCAM_BLOCK ? OCAM_BYTES : D.cam_size, 4, &cam);
|
|
if (!found && held && D.mv_ok == 1) {
|
|
D.cam_at_draw++;
|
|
if (D.gap_logged < 40) {
|
|
D.gap_logged++;
|
|
rt_log("dlaa: frame %llu: camera read at the draw: scene context %#llx last drawn %llu submission(s) before "
|
|
"DLSS's, there: %s; the frame's scene-depth draws over %llu submission(s); %d held submissions",
|
|
(unsigned long long)__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1,
|
|
(unsigned long long)D.cam_addr, (unsigned long long)(vk_exec_submit_seq - D.views[view].seq),
|
|
usnap_why[vk_usnap_why(D.cam_addr, OCAM_BYTES)],
|
|
(unsigned long long)(F.zdraws + F.zother ? vk_exec_submit_seq - F.seq0 + 1 : 0), vk_exec_held());
|
|
}
|
|
}
|
|
else if (!found) {
|
|
D.mv_none++;
|
|
if (D.cam_addr) D.cam_none_snap++;
|
|
else D.cam_none_draw++;
|
|
e->chars_ok = e->dist_ok = 0;
|
|
int kept = 0;
|
|
if (keep && D.mv_ok == 1 && D.cam_prev_ok) {
|
|
if (D.keep_run < KEEP_MAX) kept = 1, D.keep_run++, D.cam_kept++;
|
|
else kept = 2, D.cam_kept_max++;
|
|
}
|
|
if (D.gap_logged < 40) D.gap_logged++, cam_note(view, kept);
|
|
if (kept == 1) return 2;
|
|
D.cam_prev_ok = 0;
|
|
return 0;
|
|
}
|
|
D.keep_run = 0;
|
|
// this frame's view-projection as the scene draws read it (Ocarina, dlaa_cam_keep) or as the snapshot holds it, for
|
|
// the reprojection; kept as last frame's for the next
|
|
const float *cr = held ? held
|
|
: D.cam_size == OCAM_BLOCK ? (const float *)vk_usnap_bytes(D.cam_addr, OCAM_BYTES)
|
|
: (const float *)vk_usnap_bytes(D.cam_addr + D.cam_vp, 64);
|
|
double cur_vp[16], m[16], ci[16];
|
|
int have_m = 0;
|
|
if (cr && D.cam_size == OCAM_BLOCK) ocam_vp(cr, cur_vp);
|
|
else if (cr)
|
|
for (int i = 0; i < 16; i++) cur_vp[i] = cr[i];
|
|
if (cr) {
|
|
if (D.cam_prev_ok && inv4(cur_vp, ci)) {
|
|
for (int r = 0; r < 4; r++)
|
|
for (int k = 0; k < 4; k++) {
|
|
double v = 0;
|
|
for (int j = 0; j < 4; j++) v += D.cam_prev[r * 4 + j] * ci[j * 4 + k];
|
|
m[r * 4 + k] = v;
|
|
}
|
|
have_m = 1;
|
|
}
|
|
}
|
|
D.fg_cam_ok = 0;
|
|
if (cr && have_m && D.cam_size == OCAM_BLOCK) fg_camera(cr, m);
|
|
if (cr && D.cam_size == OCAM_BLOCK) cam_cut(cr);
|
|
int s = D.cam_slot, had_prev = D.cam_prev_ok;
|
|
if (D.cam_size == CAM_BLOCK) { // the whole block, for the per-object motion pass (the deferred engine's only)
|
|
VkBufferCopy bc = {cam.offset, (VkDeviceSize)s * CAM_SLOT, CAM_BLOCK};
|
|
vkCmdCopyBuffer(cb, cam.buffer, D.camb, 1, &bc);
|
|
full_barrier(cb);
|
|
}
|
|
D.cam_slot ^= 1, D.cam_prev_ok = cr != NULL;
|
|
if (cr) memcpy(D.cam_prev, cur_vp, sizeof cur_vp);
|
|
if (!had_prev || !have_m) { // nothing to reproject into: zero motion, new history
|
|
e->chars_ok = e->dist_ok = 0;
|
|
return 0;
|
|
}
|
|
VkDescriptorImageInfo zi = {D.mv_samp, zv, VK_IMAGE_LAYOUT_GENERAL};
|
|
VkDescriptorImageInfo oi = {VK_NULL_HANDLE, e->mv_view, VK_IMAGE_LAYOUT_GENERAL};
|
|
uint32_t ms = D.mvm_slot++ % MVM_SLOTS;
|
|
float *blk = (float *)(D.mvm_map + (size_t)ms * 2 * CAM_BYTES); // rows 3-6 of each block are read
|
|
memset(blk, 0, 2 * CAM_BYTES);
|
|
for (int r = 0; r < 4; r++)
|
|
for (int k = 0; k < 4; k++)
|
|
blk[12 + r * 4 + k] = r == k ? 1.0f : 0.0f, blk[64 + 12 + r * 4 + k] = (float)m[r * 4 + k];
|
|
vmaFlushAllocation(vk_ctx()->vma, D.mvm_mem, (VkDeviceSize)ms * 2 * CAM_BYTES, 2 * CAM_BYTES);
|
|
VkDescriptorBufferInfo cur = {D.mvm, (VkDeviceSize)ms * 2 * CAM_BYTES, CAM_BYTES};
|
|
VkDescriptorBufferInfo prev = {D.mvm, (VkDeviceSize)ms * 2 * CAM_BYTES + CAM_BYTES, CAM_BYTES};
|
|
// The class mask needs the G-buffer at the scene size; otherwise bindings 4-7 get the mask images as stand-ins
|
|
// (the shader returns before reading them with bias 0).
|
|
const VkExecTarget *ids = &D.ids;
|
|
int use = bias > 0 && D.ids_ok && ids->image && ids->width == w && ids->height == h && !ids->is3d;
|
|
VkImageView iv = use ? vk_exec_target_view(ids->img, ids->format, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_COLOR_BIT,
|
|
ids->level, ids->base_layer, 1, 0)
|
|
: e->mask_view;
|
|
int cur_c = e->chars_cur;
|
|
VkDescriptorImageInfo ii = {D.mv_samp, iv, VK_IMAGE_LAYOUT_GENERAL};
|
|
VkDescriptorImageInfo mki = {VK_NULL_HANDLE, e->mask_view, VK_IMAGE_LAYOUT_GENERAL};
|
|
VkDescriptorImageInfo cpi = {VK_NULL_HANDLE, e->chars_view[cur_c ^ 1], VK_IMAGE_LAYOUT_GENERAL};
|
|
VkDescriptorImageInfo coi = {VK_NULL_HANDLE, e->chars_view[cur_c], VK_IMAGE_LAYOUT_GENERAL};
|
|
VkDescriptorImageInfo dpi = {VK_NULL_HANDLE, e->dist_view[e->dist_cur ^ 1], VK_IMAGE_LAYOUT_GENERAL};
|
|
VkDescriptorImageInfo doi = {VK_NULL_HANDLE, e->dist_view[e->dist_cur], VK_IMAGE_LAYOUT_GENERAL};
|
|
VkWriteDescriptorSet wr[10] = {
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 0, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &zi},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 1, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &oi},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 2, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .pBufferInfo = &cur},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 3, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, .pBufferInfo = &prev},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 4, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &ii},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 5, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &mki},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 6, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &cpi},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 7, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &coi},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 8, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &dpi},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 9, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &doi}};
|
|
times_begin(cb);
|
|
times_mark(cb, T_MV0);
|
|
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, D.mv_pipe);
|
|
rec_push(vk_pipeline_push_fn(), cb, VK_PIPELINE_BIND_POINT_COMPUTE, D.mv_layout, 0, 10, wr);
|
|
// The depth check (dlaa_mv.comp, depth_prev) where nothing else gives characters their own motion: Ocarina's frame,
|
|
// which has neither the per-object pass nor the class buffer. Its distances take the projection's depth terms from
|
|
// rows 7-10 of the scene context (ocam_vp). NX2NAVI_DLAA_ZFIT=0 (test): the camera's motion alone, as before.
|
|
static int zfit_on = -1;
|
|
if (zfit_on < 0) zfit_on = rt_knob("DLAA_ZFIT", "1");
|
|
int zfit = zfit_on && !use && cr && D.cam_size == OCAM_BLOCK;
|
|
MvPush pc = {{(float)w, (float)h}, {jx, jy}, use ? bias : 0.0f, use && e->chars_ok ? 1.0f : 0.0f,
|
|
zfit ? 1.0f : 0.0f, zfit && e->dist_ok ? 1.0f : 0.0f,
|
|
{zfit ? cr[38] : 0.0f, zfit ? cr[39] : 0.0f, zfit ? cr[42] : 0.0f, zfit ? cr[43] : 0.0f},
|
|
zfit ? bias : 0.0f};
|
|
vkCmdPushConstants(cb, D.mv_layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, &pc);
|
|
vkCmdDispatch(cb, (w + 7) / 8, (h + 7) / 8, 1);
|
|
full_barrier(cb);
|
|
times_mark(cb, T_MV1);
|
|
vk_mvcheck_camera(cb, e->mv, w, h, jx, jy); // NX2NAVI_MV_CHECK (test)
|
|
// the characters' own motion over the camera's (vk_exec_draw.c, per-object motion)
|
|
uint64_t c0 = T.on > 0 ? kern_now_ns() : 0;
|
|
int object_motion = D.cam_size == CAM_BLOCK;
|
|
// Not on a frame that starts a new history, nor on the first frame of another render size (D.stepped, evaluate):
|
|
// that frame was left out before the history could stay across a size, and it still is.
|
|
if (D.cam_size == OCAM_BLOCK)
|
|
object_motion = vk_fg_pose_prepare(ex, D.cam_addr, D.depth.image, w, h) && !D.reset && !D.stepped;
|
|
if (object_motion && vk_mvobj_execute(ex, D.cam_addr, e->mv_view, zv, D.depth.format, w, h)) full_barrier(cb);
|
|
if (T.on > 0) T.cpu_mv += kern_now_ns() - c0;
|
|
times_mark(cb, T_OBJ);
|
|
if (T.on > 0) T.used[T.slot] = 1; // the DLSS marks follow in evaluate
|
|
vk_mvcheck_final(cb, ex->fence, e->mv, D.color.image, D.color.format, D.color.level, D.color.base_layer, w, h, jx, jy);
|
|
D.mv_frames++;
|
|
if (use) e->chars_cur ^= 1, e->chars_ok = 1, D.masked++;
|
|
else e->chars_ok = 0;
|
|
if (zfit && bias > 0) D.masked++;
|
|
e->pose_distance_valid = zfit && D.fg_cam_ok;
|
|
e->pose_distance_frame = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
|
e->pose_history[0] = jx, e->pose_history[1] = jy;
|
|
e->pose_history[2] = D.fg_cam.view_to_clip[11];
|
|
e->pose_history[3] = D.fg_cam.view_to_clip[15];
|
|
e->dist_cur ^= 1, e->dist_ok = zfit;
|
|
*masked = use || (zfit && bias > 0);
|
|
return 1;
|
|
}
|
|
|
|
// Last frame's camera block for the per-object motion pass, recorded before this frame's mv_pass: that pass writes
|
|
// this frame's into slot cam_slot and leaves the other slot, last frame's, as it is. 0: none (no history).
|
|
int vk_dlaa_prev_camera(VkDescriptorBufferInfo *out, float jitter[2]) {
|
|
if (!inputs_on() || !D.cam_prev_ok || !D.camb) return 0;
|
|
if (D.cam_size != CAM_BLOCK) return 0; // the per-object pass knows the deferred engine's block only
|
|
*out = (VkDescriptorBufferInfo){D.camb, (VkDeviceSize)(D.cam_slot ^ 1) * CAM_SLOT, CAM_BLOCK};
|
|
jitter[0] = D.jit_ok ? D.jx : 0.0f, jitter[1] = D.jit_ok ? D.jy : 0.0f;
|
|
return 1;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- reactive mask
|
|
// Particles, smoke, sparks and other transparents have no motion vectors of their own: the motion image holds the
|
|
// surface's behind them, and DLSS carried their old colour along that surface (trails behind sparks, smoke smeared
|
|
// with the camera). DLSS takes a mask of where to trust this frame's colour over the history (NGX
|
|
// "DLSS.Input.Bias.Current.Color.Mask"). It is made from the picture itself, at no cost on the CPU: the light target is
|
|
// copied when the transparents start (the pass that arms DLAA, vk_dlaa_pass) and compared with what DLSS gets
|
|
// (runtime/dlaa_mask_spv.h, tools/make_dlaa_mask.py): the share of a pixel's colour that changed, from MASK_THR on,
|
|
// times MASK_GAIN. Off by default (NX2NAVI_DLSS_REACTIVE=1 turns it on, NX2NAVI_DLAA_MASKDEBUG=1 shows the mask instead of
|
|
// the picture): the pass that arms also draws the sky and blended fog layers over the solid scene, and the mask
|
|
// covered the sky, sunlit trees and roofs and a wide fog region besides the particles (seen with MASKDEBUG at 19:45 in
|
|
// the first save), where DLSS would give up its anti-aliasing. It needs the particle draws alone (drawn again into the
|
|
// mask, like the motion draws) before it can be on. In the upscaling modes the light target DLSS reads also holds the
|
|
// bloom by then; the copy would have to be compared at the end of the scene instead.
|
|
#include "dlaa_mask_spv.h"
|
|
#define MASK_THR 0.1f
|
|
#define MASK_GAIN 2.5f
|
|
#define SNAPS 3
|
|
|
|
static struct {
|
|
struct Snap { // per light target size and format: its copy at arming, and the mask
|
|
uint32_t w, h;
|
|
VkFormat f;
|
|
VkImage before, mask;
|
|
VmaAllocation before_mem, mask_mem;
|
|
VkImageView before_view, mask_view;
|
|
uint64_t used;
|
|
} s[SNAPS];
|
|
struct Snap *cur; // this frame's copy, NULL: none
|
|
VkDescriptorSetLayout dsl;
|
|
VkPipelineLayout layout;
|
|
VkPipeline pipe;
|
|
int ok, knob; // ok: -1 not built, 0 failed, 1 ready; knob: -1 not read
|
|
uint64_t made, missed; // frames with a mask; frames DLSS ran without the copy
|
|
} R = {.ok = -1, .knob = -1};
|
|
|
|
static int mask_init(void) {
|
|
if (R.knob < 0) R.knob = rt_knob("DLSS_REACTIVE", "0");
|
|
if (!R.knob) return 0;
|
|
if (R.ok >= 0) return R.ok;
|
|
R.ok = 0;
|
|
const VkCtx *c = vk_ctx();
|
|
VkDescriptorSetLayoutBinding b[3] = {
|
|
{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{1, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL}};
|
|
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
|
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
|
|
.bindingCount = 3, .pBindings = b};
|
|
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, 16};
|
|
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof dlaa_mask_spv,
|
|
.pCode = dlaa_mask_spv};
|
|
VkShaderModule m = VK_NULL_HANDLE;
|
|
if (vkCreateDescriptorSetLayout(c->device, &dci, NULL, &R.dsl) != VK_SUCCESS) goto fail;
|
|
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
|
.pSetLayouts = &R.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
|
|
if (vkCreatePipelineLayout(c->device, &li, NULL, &R.layout) != VK_SUCCESS ||
|
|
vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS)
|
|
goto fail;
|
|
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
|
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
|
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
|
|
.layout = R.layout};
|
|
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &R.pipe);
|
|
vkDestroyShaderModule(c->device, m, NULL);
|
|
m = VK_NULL_HANDLE;
|
|
if (r != VK_SUCCESS) goto fail;
|
|
return R.ok = 1;
|
|
fail:
|
|
if (m) vkDestroyShaderModule(c->device, m, NULL); // made before a later step failed
|
|
rt_log("dlaa: the reactive mask pass could not be set up: DLSS runs without it");
|
|
return 0;
|
|
}
|
|
|
|
// A snapshot's images and views. The device must be idle (vk_dlaa_release), or the GPU never used them (snap_for's
|
|
// half-made pair).
|
|
static void destroy_snap(struct Snap *s) {
|
|
const VkCtx *c = vk_ctx();
|
|
if (s->before_view) vkDestroyImageView(c->device, s->before_view, NULL);
|
|
if (s->mask_view) vkDestroyImageView(c->device, s->mask_view, NULL);
|
|
if (s->before) vmaDestroyImage(c->vma, s->before, s->before_mem);
|
|
if (s->mask) vmaDestroyImage(c->vma, s->mask, s->mask_mem);
|
|
memset(s, 0, sizeof *s);
|
|
}
|
|
|
|
// The same for a snapshot the GPU may still use (only the replay reads it): to the deferred destruction.
|
|
static void retire_snap(struct Snap *s) {
|
|
vk_defer_view(s->before_view), vk_defer_view(s->mask_view);
|
|
vk_defer_image(s->before, s->before_mem), vk_defer_image(s->mask, s->mask_mem);
|
|
memset(s, 0, sizeof *s);
|
|
}
|
|
|
|
static void free_snap(struct Snap *s) {
|
|
retire_snap(s); // a fourth size: the evicted images may still be in flight
|
|
vk_upscaler_let_go();
|
|
}
|
|
|
|
static struct Snap *snap_for(VkCommandBuffer cb, uint32_t w, uint32_t h, VkFormat f) {
|
|
static uint64_t tick;
|
|
struct Snap *e = NULL, *lru = &R.s[0];
|
|
for (int i = 0; i < SNAPS; i++) {
|
|
if (R.s[i].w == w && R.s[i].h == h && R.s[i].f == f) return R.s[i].used = ++tick, &R.s[i];
|
|
if (!R.s[i].before && !e) e = &R.s[i];
|
|
if (R.s[i].used < lru->used) lru = &R.s[i];
|
|
}
|
|
if (!e) free_snap(e = lru);
|
|
VkImageUsageFlags u = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
if (!image2d(w, h, f, u, &e->before, &e->before_mem, &e->before_view) ||
|
|
!image2d(w, h, VK_FORMAT_R8_UNORM, u | VK_IMAGE_USAGE_STORAGE_BIT, &e->mask, &e->mask_mem, &e->mask_view)) {
|
|
destroy_snap(e); // what was made of the pair, never used (an evicted one's images went above)
|
|
return NULL;
|
|
}
|
|
VkImageMemoryBarrier2 ib[2];
|
|
VkImage im[2] = {e->before, e->mask};
|
|
for (int i = 0; i < 2; i++)
|
|
ib[i] = (VkImageMemoryBarrier2){.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
|
|
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
|
|
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = im[i],
|
|
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
|
|
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 2,
|
|
.pImageMemoryBarriers = ib};
|
|
vkCmdPipelineBarrier2(cb, &di);
|
|
e->w = w, e->h = h, e->f = f, e->used = ++tick;
|
|
return e;
|
|
}
|
|
|
|
// At arming: the light target as it is before the transparents, outside any pass.
|
|
static void mask_snapshot(VkExec *ex) {
|
|
R.cur = NULL;
|
|
if (mask_init() <= 0) return;
|
|
const VkExecTarget *t = &D.color;
|
|
struct Snap *s = snap_for(ex->cb, t->width, t->height, t->format);
|
|
if (!s) return;
|
|
vk_exec_barrier(ex);
|
|
VkImageCopy c = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, t->level, t->base_layer, 1},
|
|
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .extent = {t->width, t->height, 1}};
|
|
vkCmdCopyImage(ex->cb, t->image, VK_IMAGE_LAYOUT_GENERAL, s->before, VK_IMAGE_LAYOUT_GENERAL, 1, &c);
|
|
full_barrier(ex->cb);
|
|
ex->recorded += 2;
|
|
R.cur = s;
|
|
}
|
|
|
|
// Before DLSS: the mask from the copy and the light target (view cv). NULL: none this frame.
|
|
static struct Snap *mask_pass(VkCommandBuffer cb, VkImageView cv, uint32_t w, uint32_t h) {
|
|
struct Snap *s = R.cur;
|
|
R.cur = NULL;
|
|
if (mask_init() <= 0) return NULL;
|
|
if (!s || s->w != w || s->h != h || s->f != D.color.format) return R.missed++, NULL;
|
|
VkDescriptorImageInfo bi = {D.mv_samp, s->before_view, VK_IMAGE_LAYOUT_GENERAL};
|
|
VkDescriptorImageInfo ai = {D.mv_samp, cv, VK_IMAGE_LAYOUT_GENERAL};
|
|
VkDescriptorImageInfo mi = {VK_NULL_HANDLE, s->mask_view, VK_IMAGE_LAYOUT_GENERAL};
|
|
VkWriteDescriptorSet wr[3] = {
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 0, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &bi},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 1, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &ai},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 2, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &mi}};
|
|
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, R.pipe);
|
|
rec_push(vk_pipeline_push_fn(), cb, VK_PIPELINE_BIND_POINT_COMPUTE, R.layout, 0, 3, wr);
|
|
float pc[4] = {(float)w, (float)h, MASK_THR, MASK_GAIN};
|
|
vkCmdPushConstants(cb, R.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, pc);
|
|
vkCmdDispatch(cb, (w + 7) / 8, (h + 7) / 8, 1);
|
|
full_barrier(cb);
|
|
R.made++;
|
|
return s;
|
|
}
|
|
|
|
// ---------------------------------------------------------------- teardown
|
|
// The HUD-less picture's images, a half-made pair too. The device must be idle, or the images not yet used.
|
|
static void pb_destroy(void); // the picture-size images of the upscaler before the bloom (prebloom_run)
|
|
static void hl_destroy(void) {
|
|
const VkCtx *c = vk_ctx();
|
|
if (D.hl_view) vkDestroyImageView(c->device, D.hl_view, NULL);
|
|
if (D.hl) vmaDestroyImage(c->vma, D.hl, D.hl_mem);
|
|
if (D.hl_srgb) vmaDestroyImage(c->vma, D.hl_srgb, D.hl_srgb_mem);
|
|
D.hl_view = VK_NULL_HANDLE, D.hl = D.hl_srgb = VK_NULL_HANDLE;
|
|
}
|
|
|
|
// The same for a HUD-less picture the GPU may still use: D.hl, which a present copies for frame generation
|
|
// (gfx_present.c hudless_record), behind the present gate; the sRGB copy, which only the replay uses, to the deferred
|
|
// destruction.
|
|
static void retire_hl(void) {
|
|
const uint64_t gate = presents_now() + FG_LATE_PRESENTS;
|
|
dead_put(UPD_VIEW, D.hl_view, NULL, 0, gate);
|
|
dead_put(UPD_IMG, D.hl, D.hl_mem, 0, gate);
|
|
vk_defer_image(D.hl_srgb, D.hl_srgb_mem);
|
|
D.hl_view = VK_NULL_HANDLE, D.hl = D.hl_srgb = VK_NULL_HANDLE;
|
|
}
|
|
|
|
// The device is idle here. Release NGX features and parameters before their Vulkan resources, then shut down NGX even
|
|
// if initialization succeeded but the layout check failed. The Linux core's Shutdown1 takes an extra output pointer
|
|
// compared with the SDK's public wrapper; it reports the number of devices still using NGX.
|
|
// ---- the free camera's view of the ground (debug_tools.c dbg_pad, ground avoidance). While it flies with that on,
|
|
// nine of the view distances mv_pass wrote (a grid over the lower part of the view, where the ground is) go back to
|
|
// the host, which works out how far below the camera the ground is (ground_under). A slot is read FC_SLOTS evaluations
|
|
// after its copy, when the GPU has long been past it.
|
|
int dbg_freecam_flying(void); // debug_tools.c
|
|
void dbg_freecam_ground(const float *dist, int n, float tan_half_y, float aspect);
|
|
#define FC_N 9
|
|
#define FC_SLOTS 4
|
|
static VkBuffer fc_buf;
|
|
static VmaAllocation fc_mem;
|
|
static float *fc_map;
|
|
static uint32_t fc_next;
|
|
static uint8_t fc_have[FC_SLOTS];
|
|
|
|
static void fcam_probe(VkCommandBuffer cb, const struct Size *e, uint32_t w, uint32_t h) {
|
|
if (!dbg_freecam_flying() || !e->dist_ok) { // dist_ok: mv_pass wrote this frame's distances (Ocarina's frame)
|
|
memset(fc_have, 0, sizeof fc_have);
|
|
return;
|
|
}
|
|
const VkCtx *c = vk_ctx();
|
|
if (!fc_buf) {
|
|
VkBufferCreateInfo bi = {.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = FC_SLOTS * FC_N * sizeof(float),
|
|
.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT};
|
|
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST,
|
|
.flags = VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT | VMA_ALLOCATION_CREATE_MAPPED_BIT};
|
|
VmaAllocationInfo info;
|
|
if (vmaCreateBuffer(c->vma, &bi, &ai, &fc_buf, &fc_mem, &info) != VK_SUCCESS) {
|
|
fc_buf = VK_NULL_HANDLE;
|
|
return;
|
|
}
|
|
fc_map = info.pMappedData;
|
|
}
|
|
uint32_t s = fc_next++ % FC_SLOTS;
|
|
if (fc_have[s]) {
|
|
vmaInvalidateAllocation(c->vma, fc_mem, s * FC_N * sizeof(float), FC_N * sizeof(float));
|
|
dbg_freecam_ground(fc_map + s * FC_N, FC_N, D.fg_cam_ok ? tanf(0.5f * D.fg_cam.fov_y) : 0.0f,
|
|
D.fg_cam_ok ? D.fg_cam.aspect : 0.0f);
|
|
}
|
|
// the replay's viewports have negative heights: row 0 is the top of the view
|
|
static const float fx[3] = {0.2f, 0.5f, 0.8f}, fy[3] = {0.6f, 0.78f, 0.96f};
|
|
VkBufferImageCopy r[FC_N];
|
|
for (int i = 0; i < FC_N; i++)
|
|
r[i] = (VkBufferImageCopy){.bufferOffset = (s * FC_N + i) * sizeof(float),
|
|
.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
|
.imageOffset = {(int32_t)(fx[i % 3] * (w - 1)), (int32_t)(fy[i / 3] * (h - 1)), 0},
|
|
.imageExtent = {1, 1, 1}};
|
|
vkCmdCopyImageToBuffer(cb, e->dist[e->dist_cur ^ 1], VK_IMAGE_LAYOUT_GENERAL, fc_buf, FC_N, r);
|
|
fc_have[s] = 1;
|
|
}
|
|
|
|
void vk_dlaa_release(void) {
|
|
#ifndef _WIN32
|
|
const VkCtx *c = vk_ctx();
|
|
if (!c) return;
|
|
upd_take_all(&dead, dead_release, (void *)1); // what was let go of: the features before NGX goes, the images at once
|
|
for (int i = 0; i < 5; i++)
|
|
if (D.sizes[i].feat) N.release(D.sizes[i].feat), D.sizes[i].feat = NULL;
|
|
if (FG.feat) N.release(FG.feat); // frame generation's feature and parameters (vk_ngxfg_evaluate), before NGX goes
|
|
if (FG.p) N.destroy_params(FG.p);
|
|
memset(&FG, 0, sizeof FG);
|
|
if (D.params) N.destroy_params(D.params);
|
|
if (N.caps_map) N.destroy_params(N.caps_map);
|
|
for (int i = 0; i < 5; i++) destroy_size(&D.sizes[i]);
|
|
for (int i = 0; i < SNAPS; i++) destroy_snap(&R.s[i]);
|
|
hl_destroy();
|
|
pb_destroy();
|
|
if (D.mv_pipe) vkDestroyPipeline(c->device, D.mv_pipe, NULL);
|
|
if (D.mv_layout) vkDestroyPipelineLayout(c->device, D.mv_layout, NULL);
|
|
if (D.mv_dsl) vkDestroyDescriptorSetLayout(c->device, D.mv_dsl, NULL);
|
|
if (D.mv_samp) vkDestroySampler(c->device, D.mv_samp, NULL);
|
|
if (D.camb) vmaDestroyBuffer(c->vma, D.camb, D.camb_mem);
|
|
if (D.mvm) vmaDestroyBuffer(c->vma, D.mvm, D.mvm_mem); // mapped for good: the destroy unmaps it
|
|
if (fc_buf) vmaDestroyBuffer(c->vma, fc_buf, fc_mem), fc_buf = VK_NULL_HANDLE, fc_map = NULL;
|
|
if (R.pipe) vkDestroyPipeline(c->device, R.pipe, NULL);
|
|
if (R.layout) vkDestroyPipelineLayout(c->device, R.layout, NULL);
|
|
if (R.dsl) vkDestroyDescriptorSetLayout(c->device, R.dsl, NULL);
|
|
if (T.pool) vkDestroyQueryPool(c->device, T.pool, NULL);
|
|
memset(&D, 0, sizeof D), D.mv_ok = -1, D.cfg_seen = ~0u;
|
|
memset(&R, 0, sizeof R), R.ok = -1, R.knob = -1;
|
|
memset(&T, 0, sizeof T), T.on = -1;
|
|
memset(&F, 0, sizeof F);
|
|
vk_dlaa_swap_img = NULL, vk_dlaa_swap_view = VK_NULL_HANDLE;
|
|
if (!N.device) return; // NGX never came up on this device: DLAA off, FSR 3.1, or its init failed
|
|
unsigned left = 0;
|
|
vk_queue_lock(); // NGX may wait for the device itself
|
|
NgxResult r = N.shutdown1(N.device, &left);
|
|
vk_queue_unlock();
|
|
rt_log("dlaa: NGX shut down (%08x, %u devices left)", (unsigned)r, left);
|
|
N.device = NULL, N.caps_map = NULL, N.ngx_up = 0, N.available = 0;
|
|
#endif
|
|
}
|
|
|
|
// ---------------------------------------------------------------- jitter
|
|
// The scene's draws (every pass drawing with the scene depth, until the DLAA point) have their viewport moved by a
|
|
// sub-pixel Halton(2,3) offset, which shifts what they rasterise as a jittered projection would; full-screen passes
|
|
// that shade by window position are unaffected. Jitter only follows a frame DLSS ran on: a jittered frame that DLSS
|
|
// does not resolve shakes on screen, so a missed DLAA point stops the jitter for a second.
|
|
static float halton(uint32_t i, uint32_t b) {
|
|
float f = 1, r = 0;
|
|
for (; i; i /= b) f /= (float)b, r += f * (float)(i % b);
|
|
return r;
|
|
}
|
|
|
|
// Whether a draw takes the jitter, before DLSS ran this frame. The deferred frame: every draw into the scene depth.
|
|
// Ocarina's frame: every draw that projects through the camera (it binds the scene context at c3, vk_dlaa_draw) into a
|
|
// D32F depth of the scene's size, whichever image that is. Ocarina draws its depth pre-pass into the scene depth, copies
|
|
// that depth into a second image of the same size, and then shades the same geometry into the light target against
|
|
// the copy with the depth test EQUAL. Jittering only the draws into the first image left the shading draws unjittered
|
|
// against jittered depth: EQUAL failed on nearly every pixel, the light target kept its clear colour, and the ground
|
|
// and characters went white while the sky stayed right. Its full-screen passes drawn with a scene depth
|
|
// attached (the bloom composite DLSS reads) do not take the jitter either: they read the jittered scene through their
|
|
// own texture coordinates, and moving them too would shift the picture DLSS gets by a second offset.
|
|
static int jitter_draw(const VkExec *ex) {
|
|
const VkExecTarget *z = &ex->depth;
|
|
if (!D.jit_ok || D.ended || !z->image) return 0;
|
|
if (D.cam_size != OCAM_BLOCK) return z->image == D.jdepth;
|
|
return z->format == VK_FORMAT_D32_SFLOAT && z->width == D.jw && z->height == D.jh &&
|
|
ex->ubo[0][0].size == OCAM_BLOCK && ex->ubo[0][0].addr;
|
|
}
|
|
|
|
void vk_dlaa_viewport(VkExec *ex, VkViewport *v) {
|
|
float sx, sy;
|
|
// a pass moved to the picture's size (vk_dlaa_retarget); without a game viewport it covers the pass already
|
|
if (ex->has_vp && vk_dlaa_target_scale(ex, &sx, &sy))
|
|
v->x *= sx, v->y *= sy, v->width *= sx, v->height *= sy;
|
|
ex->vp_jittered = jitter_draw(ex);
|
|
if (!ex->vp_jittered) return;
|
|
ex->pass_jit = 1;
|
|
v->x += D.jx, v->y += D.jy;
|
|
D.ux = D.jx, D.uy = D.jy, D.jit_used = 1;
|
|
}
|
|
|
|
void vk_dlaa_jitter_check(VkExec *ex) {
|
|
int j = jitter_draw(ex);
|
|
if (j != ex->vp_jittered) ex->dirty |= VK_EXEC_DIRTY_VIEWPORT;
|
|
const VkExecTarget *z = &ex->depth; // the draws into a scene-size depth, with the jitter or without
|
|
if (D.jit_ok && !D.ended && z->image && z->format == VK_FORMAT_D32_SFLOAT && z->width == D.jw && z->height == D.jh) {
|
|
if (j) D.jdraws++;
|
|
else D.jfixed++;
|
|
}
|
|
}
|
|
|
|
static int scene_color(VkFormat f);
|
|
|
|
static void dlaa_draw_locked(VkExec *ex);
|
|
void vk_dlaa_draw(VkExec *ex) {
|
|
if (!inputs_on() || D.ended) return;
|
|
static PlatMutex lock = PLAT_MUTEX_INIT; // the replay workers' scene draws count at once (vk_exec.c chunks)
|
|
int lk = vk_exec_shared();
|
|
if (lk) plat_mutex_lock(&lock);
|
|
dlaa_draw_locked(ex);
|
|
if (lk) plat_mutex_unlock(&lock);
|
|
}
|
|
|
|
static void dlaa_draw_locked(VkExec *ex) {
|
|
static int survey = -1; // NX2NAVI_DLAA_SURVEY=1 (test): what is drawn into the armed light target
|
|
if (survey < 0) survey = rt_knob("DLAA_SURVEY", "0");
|
|
static uint64_t dfn[16]; // scene draws writing depth, by depth test (reversed depth: GREATER/GEQUAL)
|
|
if (survey && ex->depth.image && ex->depth.image == D.jdepth && ex->pipe.ds.dt && ex->pipe.ds.dw)
|
|
dfn[ex->pipe.ds.df & 15]++;
|
|
if (survey && D.armed && ex->color[0].image == D.color.image) {
|
|
static uint64_t n[2][2][2], arms, frames, last_frame; // [blend][depth write][depth test]
|
|
const VkPipeState *s = &ex->pipe;
|
|
n[s->color.blend_en & 1][s->ds.dw][s->ds.dt]++;
|
|
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
|
if (f != last_frame) last_frame = f, frames++;
|
|
static uint64_t ps;
|
|
if (ex->pass_serial != ps) ps = ex->pass_serial, arms++;
|
|
if (frames % 300 == 0) {
|
|
static uint64_t logged;
|
|
if (logged != frames) {
|
|
logged = frames;
|
|
rt_log("dlaa survey: scene draws writing depth by depth test (NVN func 1-8: never less equal lequal "
|
|
"greater notequal gequal always): %llu %llu %llu %llu %llu %llu %llu %llu",
|
|
(unsigned long long)dfn[1], (unsigned long long)dfn[2], (unsigned long long)dfn[3],
|
|
(unsigned long long)dfn[4], (unsigned long long)dfn[5], (unsigned long long)dfn[6],
|
|
(unsigned long long)dfn[7], (unsigned long long)dfn[8]);
|
|
rt_log("dlaa survey: %llu frames, %.1f passes a frame into the light target after arming; draws a frame "
|
|
"(blend/depth write/depth test): 000 %.1f 001 %.1f 010 %.1f 011 %.1f 100 %.1f 101 %.1f 110 %.1f 111 %.1f",
|
|
(unsigned long long)frames, (double)arms / frames, (double)n[0][0][0] / frames, (double)n[0][0][1] / frames,
|
|
(double)n[0][1][0] / frames, (double)n[0][1][1] / frames, (double)n[1][0][0] / frames,
|
|
(double)n[1][0][1] / frames, (double)n[1][1][0] / frames, (double)n[1][1][1] / frames);
|
|
}
|
|
}
|
|
}
|
|
// The scene camera of a draw into the scene depth: the deferred engine binds a 0x940-byte block at c4 with the
|
|
// view-projection in rows 3-6 (0x30); Ocarina's opaque scene draws bind a 0x3500-byte scene context at c3, whose
|
|
// rows 0-2 and 7-10 their vertex shaders use as the view and the projection (ocam_vp). Its full-screen passes bind
|
|
// a 0x200-byte block at c3 instead, which is not the camera. Without a camera there is no camera motion and no
|
|
// jitter (next_jitter), so DLSS runs on each frame alone without shaking the picture.
|
|
const p_bind_buf *b = &ex->ubo[0][1]; // c4: the replay keeps user banks from c3 on
|
|
const VkExecTarget *z = &ex->depth;
|
|
if (!z->image || z->width < DLAA_SCENE_MIN_W || z->format != VK_FORMAT_D32_SFLOAT) return;
|
|
if (!F.zdraws && !F.zother) F.seq0 = vk_exec_submit_seq;
|
|
if (D.jdepth && z->image != D.jdepth) {
|
|
F.zother++, F.zother_w = z->width, F.zother_h = z->height;
|
|
return;
|
|
}
|
|
F.zdraws++;
|
|
if (ex->ubo[0][0].size == OCAM_BLOCK && ex->ubo[0][0].addr) {
|
|
F.ocam++;
|
|
if (!ex->color[0].image || !scene_color(ex->color[0].format)) F.ocam_off++, F.ocam_off_fmt = ex->color[0].format;
|
|
} else F.c3_other++, F.c3_size = ex->ubo[0][0].addr ? ex->ubo[0][0].size : 0;
|
|
if (b->size == CAM_BLOCK && b->addr) D.cam_addr = b->addr, D.cam_size = CAM_BLOCK, D.cam_vp = 0x30;
|
|
else if (ex->ubo[0][0].size == OCAM_BLOCK && ex->ubo[0][0].addr && ex->color[0].image &&
|
|
scene_color(ex->color[0].format)) { // Ocarina: the scene context most light-target draws use this frame
|
|
uint64_t a = ex->ubo[0][0].addr;
|
|
int i = 0, best = 0;
|
|
while (i < D.nviews && D.views[i].addr != a) i++;
|
|
if (i == D.nviews && D.nviews < 8) D.views[D.nviews].addr = a, D.views[D.nviews].n = 0, D.views[D.nviews++].got = 0;
|
|
if (i < D.nviews) {
|
|
D.views[i].n++, D.views[i].seq = vk_exec_submit_seq;
|
|
// The camera rows as this draw reads them, from its own submission's copy: DLSS runs in a later submission
|
|
// on some frames, whose copy may not hold the block (mv_pass, dlaa_cam_keep).
|
|
if (!D.views[i].got) D.views[i].got = vk_usnap_read(a, OCAM_BYTES, D.views[i].cam);
|
|
}
|
|
for (int k = 1; k < D.nviews; k++)
|
|
if (D.views[k].n > D.views[best].n) best = k;
|
|
D.cam_addr = D.views[best].addr, D.cam_size = OCAM_BLOCK, D.cam_vp = 0;
|
|
}
|
|
}
|
|
|
|
// NX2NAVI_DLAA_JITTER=0 (test): no jitter. Read once.
|
|
static int jitter_on(void) {
|
|
static int on = -1;
|
|
if (on < 0) on = rt_knob("DLAA_JITTER", "1");
|
|
return on;
|
|
}
|
|
|
|
// The jitter moves on right after DLAA ran, not at the present: the replay can record the next frame's scene before
|
|
// the present of this one is handled, and a step taken there landed in the middle of a frame (worse after an off/on
|
|
// switch, which shifted that order). Jitter needs a frame DLSS ran on; a frame without the DLAA point stops it.
|
|
// The sequence is as long as NVIDIA's DLSS guide asks for the upscaling ratio, 8 x (output / scene)^2 phases (18 in
|
|
// Quality, 24 in Balanced, 32 in Performance, 72 in Ultra Performance), and 16 at least, the length DLAA has used from the start.
|
|
static void next_jitter(uint32_t w, uint32_t ow) {
|
|
int nj = !jitter_on();
|
|
double r = w ? (double)ow / w : 1.0;
|
|
uint32_t phases = (uint32_t)ceil(8.0 * r * r - 1e-6);
|
|
if (phases < 16) phases = 16;
|
|
if (phases > 72) phases = 72;
|
|
uint32_t i = D.jframe++ % phases + 1;
|
|
D.jx = halton(i, 2) - 0.5f, D.jy = halton(i, 3) - 0.5f;
|
|
// Jitter only where the camera is known (vk_dlaa_draw): the deferred engine's block or Ocarina's scene context. On
|
|
// Ocarina's frame the draws that take it are the camera's into every scene-size depth (jitter_draw); taking only the
|
|
// draws into DLSS's own depth image turned the picture white, and jitter was off there until that was found.
|
|
D.jit_ok = !D.hold && !nj && (D.cam_size == CAM_BLOCK || D.cam_size == OCAM_BLOCK);
|
|
}
|
|
|
|
// The DLSS model (NX2NAVI_DLSS_PRESET, live): default takes NVIDIA's model per mode (evaluate: K for DLAA, Quality and
|
|
// Balanced, M for Performance, L for Ultra Performance), 0 here. A letter a..m takes that model in every mode: k pins
|
|
// K, j is the other transformer model, which NVIDIA describes as slightly less ghosting at the cost of extra flicker, e
|
|
// the older CNN model, cheaper on slower RTX cards. The value is an NVSDK_NGX_DLSS_Hint_Render_Preset (1 A ... 13 M),
|
|
// read when a feature is created; a change recreates the features (preset_check).
|
|
static int preset_value(void) {
|
|
const char *v = cfg_get("NX2NAVI_DLSS_PRESET");
|
|
int c = v && v[0] && !v[1] ? v[0] | 0x20 : 0;
|
|
return c >= 'a' && c <= 'm' ? c - 'a' + 1 : 0;
|
|
}
|
|
|
|
// How strongly DLSS favours the current frame on characters and where they just were (NX2NAVI_DLSS_CHAR_MASK, percent,
|
|
// live; 0 off). Where a character's vectors are the camera's only (dlaa_objmv = 0, or a draw the per-object pass
|
|
// skips), one that moves on its own drags a faint copy of itself through DLSS's history; the bias mask makes DLSS take
|
|
// less of the history there, at the cost of some of its anti-aliasing on those pixels. Ocarina's frame has no class
|
|
// buffer to find characters by: there the mask covers the pixels whose depth the camera's motion does not explain
|
|
// (dlaa_mv.comp, the depth check).
|
|
static float char_bias(void) {
|
|
const char *v = cfg_get("NX2NAVI_DLSS_CHAR_MASK");
|
|
long n = v && *v ? strtol(v, NULL, 10) : 65;
|
|
return n <= 0 ? 0.0f : n >= 100 ? 1.0f : (float)n / 100.0f;
|
|
}
|
|
|
|
// The quality setting DLSS is created with, from the scene's share of the output width.
|
|
static int perf_quality(uint32_t w, uint32_t ow) {
|
|
if (w >= ow) return NGX_PERFQ_DLAA;
|
|
double k = (double)w / ow;
|
|
return k >= 0.66 ? 2 : k >= 0.57 ? 1 : k >= 0.49 ? 0 : 3;
|
|
}
|
|
|
|
// Every 300 frames DLSS ran, the history resets and camera gaps of those frames, while there were any (at most 400 lines a
|
|
// run): the measure for dlaa_cam_keep at one spot of the game.
|
|
static void window_line(void) {
|
|
enum { N_ = 11 };
|
|
static uint64_t last[N_];
|
|
static int lines;
|
|
if (D.evals % 300) return;
|
|
uint64_t now[N_] = {D.why[0], D.why[1], D.why[2], D.mv_none, D.cam_none_draw, D.cam_none_snap, D.cam_kept,
|
|
D.cam_at_draw, D.frames_missed, D.seconds, D.target_kept}, d[N_], any = 0;
|
|
for (int i = 0; i < N_; i++) d[i] = now[i] - last[i], last[i] = now[i];
|
|
for (int i = 0; i < N_; i++) any |= d[i];
|
|
if (!any || lines >= 400) return;
|
|
lines++;
|
|
rt_log("dlaa: frames %llu-%llu: resets %llu size, %llu camera, %llu target (%llu target changes with the history "
|
|
"kept); %llu without a camera (%llu without a "
|
|
"scene-context draw, %llu not in DLSS's submission), %llu kept with zero motion; %llu cameras read at the draw; "
|
|
"%llu frames without the DLAA point, %llu with a second scene after DLSS ran",
|
|
(unsigned long long)(D.evals - 299), (unsigned long long)D.evals, (unsigned long long)d[0],
|
|
(unsigned long long)d[1], (unsigned long long)d[2], (unsigned long long)d[10], (unsigned long long)d[3],
|
|
(unsigned long long)d[4],
|
|
(unsigned long long)d[5], (unsigned long long)d[6], (unsigned long long)d[7], (unsigned long long)d[8],
|
|
(unsigned long long)d[9]);
|
|
}
|
|
|
|
// dst: the target DLSS's picture goes to, NULL for the light target itself (DLAA in place).
|
|
static void hudless(VkCommandBuffer cb, VkImage src, uint32_t w, uint32_t h);
|
|
|
|
// Another light target than last frame's. The game draws its scene into one of a few pooled targets of the same size
|
|
// and moves to another whenever its pass list changes, in the middle of play with the camera and the scene carrying
|
|
// on: 35 times in a walk of a minute through Kokiri Forest, three of them within eight frames on the ladder below
|
|
// the tree house.
|
|
// The history is the upscaler's own and not in the target, so where this frame's motion vectors are valid (cont: the
|
|
// camera of this frame and of the last one, mv_pass 1) the history stays. A new history at each change showed as a few
|
|
// soft, grainy frames every time, a flicker over the whole picture. Without valid vectors, and with
|
|
// NX2NAVI_DLAA_TARGET_KEEP=0 (test), a new history as before. The first few are logged with the new target's size, to
|
|
// tell a target the game made anew from the ones it alternates between.
|
|
static void target_changed(const VkExecTarget *t, int cont) {
|
|
static int keep = -1;
|
|
if (keep < 0) keep = rt_knob("DLAA_TARGET_KEEP", "1");
|
|
int kept = keep && cont && D.last_color;
|
|
if (D.last_color && D.target_logged < 16)
|
|
D.target_logged++, rt_log("dlaa: frame %llu: the light target changed from %p to %p (%ux%u format %d): %s",
|
|
(unsigned long long)__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED) + 1,
|
|
(void *)D.last_color, (void *)t->image, t->width, t->height, (int)t->format,
|
|
kept ? "the history stays (the camera carries on)" : "new history");
|
|
if (kept) D.target_kept++;
|
|
else D.reset = 1, D.why[2]++;
|
|
D.last_color = t->image;
|
|
}
|
|
|
|
// DLSS's own video memory, the features and their history (NGX's DLSS statistics callback with the capability
|
|
// parameters), for the log's VRAM breakdown (vk_dlaa_vram); -1 until known. Asked on the replay thread, which makes every
|
|
// other NGX call, every 600 evaluations.
|
|
static volatile int64_t dlss_vram = -1;
|
|
int64_t vk_dlaa_vram(void) { return __atomic_load_n(&dlss_vram, __ATOMIC_RELAXED); }
|
|
|
|
static void query_vram(void) {
|
|
void *cb = NULL;
|
|
unsigned long long n = 0;
|
|
if (fsr3() || !N.capsp || !p_getp(N.capsp, "DLSSGetStatsCallback", &cb) || !cb) return;
|
|
if (NGX_OK(((NgxResult (*)(NgxParam *))cb)(N.capsp)) && p_getull(N.capsp, "SizeInBytes", &n))
|
|
__atomic_store_n(&dlss_vram, (int64_t)n, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
// Frees the sizes other than cur that no evaluation used for SIZE_STALE evaluations: the GPU finished with them long
|
|
// ago (the CPU is never that many presents ahead), so their images and feature go at once (retire_size, the drain in
|
|
// vk_upscaler_let_go). host is the size whose feature cur ran on (cur itself, or the DLSS mode's: range_host), which
|
|
// stays as well. A size that is in use on another's feature can still hold one of its own from before that other was
|
|
// made: unused as long, it goes alone.
|
|
static void stale_sizes(const struct Size *cur, const struct Size *host) {
|
|
if (!idle_free()) return;
|
|
int any = 0;
|
|
for (int i = 0; i < DLSS_SIZES; i++) {
|
|
struct Size *s = &D.sizes[i];
|
|
if (s == host || !s->out) continue;
|
|
if (s != cur && D.evals >= s->used + SIZE_STALE) {
|
|
rt_log("dlaa: the DLSS feature at %ux%u -> %ux%u went unused for %d frames: freed", s->w, s->h, s->ow, s->oh,
|
|
SIZE_STALE);
|
|
if (D.last == s) D.last = NULL;
|
|
retire_size(s), any = 1;
|
|
} else if (s->feat && D.evals >= s->feat_used + SIZE_STALE) {
|
|
rt_log("dlaa: the DLSS feature at %ux%u -> %ux%u went unused for %d frames while its size ran on another "
|
|
"size's: released", s->w, s->h, s->ow, s->oh, SIZE_STALE);
|
|
retire_feat(s, 1), any = 1;
|
|
}
|
|
}
|
|
if (any) vk_upscaler_let_go();
|
|
}
|
|
|
|
// Before a feature is made: the features of the other sizes with a larger input go, or all of them (all: a feature was
|
|
// released since the last creation, feat_released); their images stay. The GPU may still be running an evaluation of
|
|
// theirs: those whose last submission it has passed are released here, before the creation, and the others once it
|
|
// has (retire_feat, vk_upscaler_let_go), after it. None of them is evaluated again: its size makes a new one.
|
|
static void release_kept(const struct Size *making, int all) {
|
|
int n = 0;
|
|
for (int i = 0; i < DLSS_SIZES; i++) {
|
|
struct Size *s = &D.sizes[i];
|
|
if (s == making || !s->feat) continue;
|
|
if (!all && (uint64_t)s->w * s->h <= (uint64_t)making->w * making->h) continue;
|
|
retire_feat(s, 0), n++;
|
|
if (D.last == s) D.last = NULL;
|
|
if (D.host == s) D.host = NULL;
|
|
}
|
|
if (!n) return;
|
|
vk_upscaler_let_go();
|
|
int left = upd_count(&dead, UPD_FEAT); // older ones have older fences and went first: those left are this call's
|
|
if (left > n) left = n;
|
|
char later[80] = "";
|
|
if (left) snprintf(later, sizeof later, "; %d of them once the GPU is past their last frame", left);
|
|
rt_log("dlaa: %d DLSS feature(s) released before the one for %ux%u is made (%s); made again when used%s", n,
|
|
making->w, making->h, all ? "after a release" : "larger inputs", later);
|
|
}
|
|
|
|
// The outer columns and rows of what a jittered pass draws into. The jitter moves the scene's viewport by under half a
|
|
// pixel (vk_dlaa_viewport), which keeps every pixel centre inside it, yet on an RX 7900 XT (AMD's Windows driver) the draws
|
|
// of a viewport moved left left the last column out, and of one moved up the last row: the driver evidently limits
|
|
// drawing to the whole pixels inside the viewport. Those pixels keep what was there before the pass: in a frame dump
|
|
// (NX2NAVI_DUMP, Hyrule Field) the depth's last row kept its clear value through every scene pass and the light target the
|
|
// sky's colour, and in a village the light target's clear colour, white. With NX2NAVI_DLAA_JITTER=0, and without DLSS or
|
|
// FSR, every row is drawn.
|
|
// So when a pass that took the jitter ends (vk_exec.c vk_exec_pass_end), the outermost row and column on each side of
|
|
// its colour targets and its depth are filled from the ones next to them, before anything reads them. Filling only the
|
|
// armed pair right before the upscaler read it (evaluate, which still does it for a target a dispatch wrote) was too
|
|
// late for whatever reads the scene in between: water reads the scene behind it and showed bright patches along the
|
|
// bottom in a fifth of the shots of a run through a stream (none with the jitter off), and the bloom and the
|
|
// anti-aliasing pass read the row too. (The white line in a player's clips had a second cause, the upscaler's picture
|
|
// never reaching the screen with the HUD hidden: vk_dlaa_upscaler.)
|
|
// A copied edge is one render pixel at the picture's border, which the upscaler blends with its history.
|
|
// NX2NAVI_DLAA_EDGE_FILL: 1 (default) at each jittered pass's end and before the upscaler; 2 only before the upscaler (as
|
|
// 0.9.1 did, for comparison); 0 not at all.
|
|
static int edge_fill_mode(void) {
|
|
static int on = -1;
|
|
if (on < 0) on = rt_knob("DLAA_EDGE_FILL", "1");
|
|
return on;
|
|
}
|
|
|
|
// One image's border from the texels next to it, in one copy: the columns and rows without their ends, then the four
|
|
// corners from their diagonal neighbours. Every source is inside the border and every destination on it, so no region
|
|
// reads what another writes. The image is in GENERAL and nothing is still drawing into it.
|
|
static void edge_copy(VkCommandBuffer cb, const VkExecTarget *g, VkImageAspectFlags aspect) {
|
|
int32_t w = (int32_t)g->width, h = (int32_t)g->height;
|
|
if (w < 4 || h < 4) return;
|
|
const VkImageSubresourceLayers sl = {aspect, g->level, g->base_layer, 1};
|
|
#define EDGE(sx, sy, dx, dy, ew, eh) \
|
|
{.srcSubresource = sl, .srcOffset = {sx, sy, 0}, .dstSubresource = sl, .dstOffset = {dx, dy, 0}, \
|
|
.extent = {(uint32_t)(ew), (uint32_t)(eh), 1}}
|
|
const VkImageCopy r[8] = {EDGE(1, 1, 0, 1, 1, h - 2), EDGE(w - 2, 1, w - 1, 1, 1, h - 2),
|
|
EDGE(1, 1, 1, 0, w - 2, 1), EDGE(1, h - 2, 1, h - 1, w - 2, 1),
|
|
EDGE(1, 1, 0, 0, 1, 1), EDGE(w - 2, 1, w - 1, 0, 1, 1),
|
|
EDGE(1, h - 2, 0, h - 1, 1, 1), EDGE(w - 2, h - 2, w - 1, h - 1, 1, 1)};
|
|
#undef EDGE
|
|
vkCmdCopyImage(cb, g->image, VK_IMAGE_LAYOUT_GENERAL, g->image, VK_IMAGE_LAYOUT_GENERAL, 8, r);
|
|
}
|
|
|
|
static int edge_target(const VkExecTarget *g) { return g->image && g->samples <= 1 && !g->is3d && !g->blocks; }
|
|
|
|
void vk_dlaa_pass_end(VkExec *ex) {
|
|
if (!ex->pass_jit) return;
|
|
ex->pass_jit = 0;
|
|
if (edge_fill_mode() != 1) return;
|
|
int any = edge_target(&ex->depth) && (ex->depth.aspect & VK_IMAGE_ASPECT_DEPTH_BIT);
|
|
for (uint32_t i = 0; i < ex->pass_ncolor && i < 8; i++) any |= edge_target(&ex->color[i]);
|
|
if (!any) return;
|
|
static int told;
|
|
if (!told++) rt_log("dlaa: the outer rows and columns of each jittered pass's targets are filled from the ones next to them");
|
|
vk_exec_barrier(ex); // the pass's draws are done, and its targets are in GENERAL
|
|
for (uint32_t i = 0; i < ex->pass_ncolor && i < 8; i++)
|
|
if (edge_target(&ex->color[i])) edge_copy(ex->cb, &ex->color[i], VK_IMAGE_ASPECT_COLOR_BIT), ex->recorded++;
|
|
if (edge_target(&ex->depth) && (ex->depth.aspect & VK_IMAGE_ASPECT_DEPTH_BIT))
|
|
edge_copy(ex->cb, &ex->depth, VK_IMAGE_ASPECT_DEPTH_BIT), ex->recorded++;
|
|
}
|
|
|
|
// Before the upscaler reads the armed pair (evaluate): once more, for a light target a dispatch wrote after its last
|
|
// pass. The pair is at level 0 of the scene's size.
|
|
static void edge_fill(VkCommandBuffer cb, const VkExecTarget *t, const VkExecTarget *z, int jittered) {
|
|
if (!edge_fill_mode() || !jittered) return;
|
|
if (edge_target(t)) edge_copy(cb, t, VK_IMAGE_ASPECT_COLOR_BIT);
|
|
if (edge_target(z) && z->width == t->width && z->height == t->height) edge_copy(cb, z, VK_IMAGE_ASPECT_DEPTH_BIT);
|
|
full_barrier(cb);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- FSR 3.1's second sharpening pass
|
|
// FSR 3.1 in its scaling modes is soft where FSR 4 is not, most of all while the camera moves. Measured on a village
|
|
// save while the camera turned at a steady rate (present screenshots every 0.4 s, the mean absolute Laplacian of the
|
|
// scene's luminance as the fine detail): FSR 3.1 Native AA 6.7, FSR 4 Performance 5.9 and Ultra Performance 5.4, FSR 3.1
|
|
// Performance 4.0 and Ultra Performance 3.3 with its own sharpening at the slider's default of 80, 5.0 and 4.1 with it
|
|
// at 100. FSR 3.1's tuning constants (velocity factor, accumulation per frame, shading change scale) gave nothing or
|
|
// took detail away. So below a render scale of three quarters FSR 3.1's own sharpening goes towards its maximum
|
|
// (vk_ffx.cpp vk_ffx_upscale) and its picture gets a second pass of RCAS here (shaders/dlaa_sharpen.comp), before the
|
|
// HUD is drawn over it: both grow as the scale falls, full from Performance down, and both follow the sharpness
|
|
// slider, 0 turning them off. NX2NAVI_FSR3_SHARPEN=0 (test): no second pass.
|
|
#define SHARPEN_STOPS 1.5f
|
|
static struct {
|
|
int ok; // -1 not tried
|
|
VkDescriptorSetLayout dsl;
|
|
VkPipelineLayout layout;
|
|
VkPipeline pipe;
|
|
} SH = {.ok = -1};
|
|
|
|
static int sharpen_init(void) {
|
|
if (SH.ok >= 0) return SH.ok;
|
|
SH.ok = 0;
|
|
const VkCtx *c = vk_ctx();
|
|
if (!mv_init() || !D.mv_samp) return 0; // its sampler serves the texel fetches
|
|
VkDescriptorSetLayoutBinding b[2] = {
|
|
{0, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL},
|
|
{1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 1, VK_SHADER_STAGE_COMPUTE_BIT, NULL}};
|
|
VkDescriptorSetLayoutCreateInfo dci = {.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
|
|
.flags = VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR,
|
|
.bindingCount = 2, .pBindings = b};
|
|
VkPushConstantRange pr = {VK_SHADER_STAGE_COMPUTE_BIT, 0, 4 * sizeof(float)};
|
|
VkShaderModuleCreateInfo mi = {.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO, .codeSize = sizeof dlaa_sharpen_spv,
|
|
.pCode = dlaa_sharpen_spv};
|
|
VkShaderModule m = VK_NULL_HANDLE;
|
|
if (vkCreateDescriptorSetLayout(c->device, &dci, NULL, &SH.dsl) != VK_SUCCESS) return 0;
|
|
VkPipelineLayoutCreateInfo li = {.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .setLayoutCount = 1,
|
|
.pSetLayouts = &SH.dsl, .pushConstantRangeCount = 1, .pPushConstantRanges = &pr};
|
|
if (vkCreatePipelineLayout(c->device, &li, NULL, &SH.layout) != VK_SUCCESS ||
|
|
vkCreateShaderModule(c->device, &mi, NULL, &m) != VK_SUCCESS)
|
|
return 0;
|
|
VkComputePipelineCreateInfo ci = {.sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO,
|
|
.stage = {.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
|
|
.stage = VK_SHADER_STAGE_COMPUTE_BIT, .module = m, .pName = "main"},
|
|
.layout = SH.layout};
|
|
VkResult r = vkCreateComputePipelines(c->device, VK_NULL_HANDLE, 1, &ci, NULL, &SH.pipe);
|
|
vkDestroyShaderModule(c->device, m, NULL);
|
|
if (r != VK_SUCCESS) {
|
|
rt_log("dlaa: FSR 3.1's second sharpening pass could not be set up");
|
|
return 0;
|
|
}
|
|
return SH.ok = 1;
|
|
}
|
|
|
|
// How much of the extra sharpening a render scale takes: none from three quarters of the output's width up, all of it
|
|
// from half (Performance) down. vk_ffx.cpp raises FSR 3.1's own sharpness by the same share.
|
|
float vk_dlaa_fsr3_boost(uint32_t w, uint32_t ow) {
|
|
double share = ow ? (double)w / ow : 1.0, k = (0.75 - share) / 0.25;
|
|
return (float)(k < 0 ? 0 : k > 1 ? 1 : k);
|
|
}
|
|
|
|
// FSR 3.1's picture e->out (w x h rendered, ow x oh out) sharpened once more, in place: RCAS into e->sharp and back.
|
|
static void fsr3_sharpen(VkCommandBuffer cb, struct Size *e, uint32_t w, uint32_t ow, uint32_t oh) {
|
|
static int on = -1;
|
|
if (on < 0) on = rt_knob("FSR3_SHARPEN", "1");
|
|
int slider = cfg_int("NX2NAVI_SHARPNESS");
|
|
float k = vk_dlaa_fsr3_boost(w, ow);
|
|
if (!on || slider <= 0 || k <= 0 || !sharpen_init()) return;
|
|
if (!e->sharp) {
|
|
VkImageUsageFlags u = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
if (!image2d(ow, oh, VK_FORMAT_R16G16B16A16_SFLOAT, u, &e->sharp, &e->sharp_mem, &e->sharp_view)) {
|
|
const VkCtx *c = vk_ctx();
|
|
if (e->sharp_view) vkDestroyImageView(c->device, e->sharp_view, NULL);
|
|
if (e->sharp) vmaDestroyImage(c->vma, e->sharp, e->sharp_mem);
|
|
e->sharp = VK_NULL_HANDLE, e->sharp_view = VK_NULL_HANDLE;
|
|
return;
|
|
}
|
|
VkImageMemoryBarrier2 ib = {.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
|
|
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
|
|
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = e->sharp,
|
|
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
|
|
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 1,
|
|
.pImageMemoryBarriers = &ib};
|
|
vkCmdPipelineBarrier2(cb, &di);
|
|
rt_log("dlaa: FSR 3.1's picture gets a second sharpening pass at this render scale (%u of %u wide)", w, ow);
|
|
}
|
|
// RCAS takes its sharpness in stops below the most (0). At full strength (0.4 stops at the slider's 80, as vk_fsr.c
|
|
// maps it for the present) the pass took Performance's fine detail to 8.2, past Native AA's 6.7: grain on every
|
|
// surface. It runs SHARPEN_STOPS weaker, and weaker still above Performance's scale.
|
|
const float stops = SHARPEN_STOPS + (100.0f - (float)(slider > 100 ? 100 : slider)) / 50.0f + 2.0f * (1.0f - k);
|
|
const float pc[4] = {(float)ow, (float)oh, stops, 0};
|
|
VkDescriptorImageInfo si = {D.mv_samp, e->out_view, VK_IMAGE_LAYOUT_GENERAL},
|
|
di = {VK_NULL_HANDLE, e->sharp_view, VK_IMAGE_LAYOUT_GENERAL};
|
|
VkWriteDescriptorSet wr[2] = {
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 0, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &si},
|
|
{.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, .dstBinding = 1, .descriptorCount = 1,
|
|
.descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, .pImageInfo = &di}};
|
|
vkCmdBindPipeline(cb, VK_PIPELINE_BIND_POINT_COMPUTE, SH.pipe);
|
|
rec_push(vk_pipeline_push_fn(), cb, VK_PIPELINE_BIND_POINT_COMPUTE, SH.layout, 0, 2, wr);
|
|
vkCmdPushConstants(cb, SH.layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof pc, pc);
|
|
vkCmdDispatch(cb, (ow + 15) / 16, (oh + 15) / 16, 1);
|
|
full_barrier(cb);
|
|
VkImageCopy cp = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
|
.extent = {ow, oh, 1}};
|
|
vkCmdCopyImage(cb, e->sharp, VK_IMAGE_LAYOUT_GENERAL, e->out, VK_IMAGE_LAYOUT_GENERAL, 1, &cp);
|
|
full_barrier(cb);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- one feature for a range of render sizes
|
|
// NX2NAVI_DLSS_RANGE (nx2navi.ini dlss_range; 1 on; 0, the default until it has run on a card: every render size has
|
|
// a feature of its own).
|
|
// The render size changes under one output size when dynamic resolution steps it (dynres.c) and when a screen renders
|
|
// at another share than play. With a feature per size, each new size cost a feature creation (tens of milliseconds and
|
|
// a wait for the GPU, release_kept) and each change a new history, a frame or more of soft picture.
|
|
// DLSS takes more than one render size on a feature. NGX's optimal settings callback (the capability parameters'
|
|
// "DLSSOptimalSettingsCallback") answers, for an output size and a quality value, with the smallest and the largest
|
|
// render size a feature of that mode takes ("DLSS.Get.Dynamic.Min.Render.Width" and its three siblings), both
|
|
// included; a mode or a DLSS library without dynamic resolution answers with one size for both. An evaluation names
|
|
// its render size as the sub-rectangle of its inputs ("DLSS.Render.Subrect.Dimensions.Width" and ".Height", which
|
|
// evaluate sets on every frame, and where it starts in each input, "DLSS.Input.Color.Subrect.Base.X" and the like); a
|
|
// size outside the range fails the evaluation. Feature creation has no flag for this (NVSDK_NGX_DLSS_Feature_Flags
|
|
// holds none). NVIDIA's DLSS guide adds two things: a feature keeps the model of the size it was created with, and it
|
|
// is to be created for the largest render size it will see, because an input larger than the one it was created for
|
|
// makes DLSS allocate its buffers again in the middle of play.
|
|
// So the feature of the DLSS mode the player set, the one created at that mode's own render size, also runs every
|
|
// smaller render size from DLSS's minimum up (range_host): dynamic resolution steps down from the player's mode and
|
|
// never above it. Such a frame is evaluated on that feature with its own images, all of them exactly its render size
|
|
// (size_for), as the sub-rectangle at their top left, and the history stays (evaluate). A size above the feature's
|
|
// own, below DLSS's minimum, of another shape, with another output size, or met while the player's mode is another
|
|
// one, has a feature of its own; so has every size where DLSS reports no range.
|
|
// A run in which the render size never changes creates its feature with the parameters it had without this, and
|
|
// evaluates it with the same ones; the query after the creation is the only call added.
|
|
// Two things still treat the first frame of another render size as a new start, although DLSS's history stays: the
|
|
// per-object motion pass leaves it out (mv_pass), and frame generation is told to start anew (D.fg_reset), since the
|
|
// depth and the motion vectors it gets are of another size than the frame before's.
|
|
static int range_refused; // DLSS failed an evaluation inside the range it reported (evaluate): off for this run
|
|
|
|
static int range_on(void) {
|
|
static int m = -1;
|
|
if (m < 0) m = cfg_int("NX2NAVI_DLSS_RANGE") != 0;
|
|
return m && !range_refused;
|
|
}
|
|
|
|
// The quality value of the DLSS mode the player set (NX2NAVI_DLSS): the one perf_quality gives a scene at that mode's
|
|
// share of the output, so the feature created at the mode's own render size carries it.
|
|
static int setting_quality(void) {
|
|
const char *m = cfg_value("NX2NAVI_DLSS");
|
|
return !strcmp(m, "quality") ? 2 : !strcmp(m, "balanced") ? 1 : !strcmp(m, "performance") ? 0
|
|
: !strcmp(m, "ultra_performance") ? 3 : NGX_PERFQ_DLAA;
|
|
}
|
|
|
|
// DLSS's answer for an output size and a quality value: the render size it would pick itself (opt) and the smallest
|
|
// and the largest a feature of that mode takes (lo, hi). 0: the DLSS library has no such callback, or it failed. Asked
|
|
// on the replay thread with the capability parameters, like the statistics (query_vram).
|
|
static int range_query(uint32_t ow, uint32_t oh, int q, unsigned opt[2], unsigned lo[2], unsigned hi[2]) {
|
|
NgxParam *c = N.capsp;
|
|
void *cb = NULL;
|
|
if (!c || !p_getp(c, "DLSSOptimalSettingsCallback", &cb) || !cb) return 0;
|
|
p_setu(c, "Width", ow);
|
|
p_setu(c, "Height", oh);
|
|
p_seti(c, "PerfQualityValue", q);
|
|
p_seti(c, "RTXValue", 0); // older DLSS libraries expect it set
|
|
if (!NGX_OK(((NgxResult (*)(NgxParam *))cb)(c))) return 0;
|
|
if (!p_getu(c, "OutWidth", &opt[0]) || !p_getu(c, "OutHeight", &opt[1])) return 0;
|
|
// a library without dynamic resolution leaves these four unset: the size it picked is then all of the range
|
|
lo[0] = hi[0] = opt[0], lo[1] = hi[1] = opt[1];
|
|
p_getu(c, "DLSS.Get.Dynamic.Max.Render.Width", &hi[0]);
|
|
p_getu(c, "DLSS.Get.Dynamic.Max.Render.Height", &hi[1]);
|
|
p_getu(c, "DLSS.Get.Dynamic.Min.Render.Width", &lo[0]);
|
|
p_getu(c, "DLSS.Get.Dynamic.Min.Render.Height", &lo[1]);
|
|
return 1;
|
|
}
|
|
|
|
// After s's feature is made: whether it also runs smaller render sizes, from which size up, and one line saying so.
|
|
// It does when it is the feature of the player's DLSS mode and its own size lies inside the range DLSS reports, above
|
|
// the range's smallest size.
|
|
static void range_note(struct Size *s) {
|
|
unsigned opt[2] = {0, 0}, lo[2] = {0, 0}, hi[2] = {0, 0};
|
|
s->ranged = 0;
|
|
if (!range_on()) return;
|
|
if (!range_query(s->ow, s->oh, s->q, opt, lo, hi)) {
|
|
rt_log("dlaa: the DLSS feature for %ux%u -> %ux%u runs its own render size only: this DLSS library gives no "
|
|
"optimal settings", s->w, s->h, s->ow, s->oh);
|
|
return;
|
|
}
|
|
const char *why = lo[0] == hi[0] && lo[1] == hi[1] ? "DLSS takes one render size for this output size and quality value"
|
|
: s->q != setting_quality() ? "it is not at the render size of the DLSS mode set"
|
|
: s->w < lo[0] || s->h < lo[1] || s->w > hi[0] || s->h > hi[1] ? "its own size is outside DLSS's range"
|
|
: s->w == lo[0] && s->h == lo[1] ? "its own size is the smallest of DLSS's range" : NULL;
|
|
if (why) {
|
|
rt_log("dlaa: the DLSS feature for %ux%u -> %ux%u runs its own render size only: %s (quality value %d: DLSS's "
|
|
"range %ux%u to %ux%u, its own choice %ux%u)", s->w, s->h, s->ow, s->oh, why, s->q, lo[0], lo[1], hi[0],
|
|
hi[1], opt[0], opt[1]);
|
|
return;
|
|
}
|
|
s->ranged = 1, s->min_w = lo[0], s->min_h = lo[1];
|
|
rt_log("dlaa: the DLSS feature for %ux%u -> %ux%u also runs the render sizes from %ux%u up to its own, the history "
|
|
"kept (quality value %d: DLSS's range %ux%u to %ux%u, its own choice %ux%u)", s->w, s->h, s->ow, s->oh, lo[0],
|
|
lo[1], s->q, lo[0], lo[1], hi[0], hi[1], opt[0], opt[1]);
|
|
}
|
|
|
|
// The size whose feature a frame of e's size runs on: the one of the player's DLSS mode when e's render size is inside
|
|
// what that feature takes, no larger than the size it was created for on either axis, no smaller than DLSS's minimum,
|
|
// and of its shape within 3 % (sizes round to multiples of 8; DLSS wants the render size's shape to be the output's);
|
|
// e itself otherwise. That feature may have been released since (release_kept): evaluate then makes it again, for its
|
|
// own size. Of two that fit, the one the frame before ran on, whose history then carries on.
|
|
static struct Size *range_host(struct Size *e) {
|
|
int q = setting_quality();
|
|
if (e->ranged && e->q == q) return e; // the mode's own size
|
|
struct Size *host = e;
|
|
for (int i = 0; i < DLSS_SIZES; i++) {
|
|
struct Size *s = &D.sizes[i];
|
|
if (s == e || !s->out || !s->ranged || s->q != q || s->ow != e->ow || s->oh != e->oh) continue;
|
|
if (e->w > s->w || e->h > s->h || e->w < s->min_w || e->h < s->min_h) continue;
|
|
if ((double)e->w * s->h > 1.03 * (double)e->h * s->w || (double)e->h * s->w > 1.03 * (double)e->w * s->h) continue;
|
|
if (s == D.host) return s;
|
|
if (host == e) host = s;
|
|
}
|
|
return host;
|
|
}
|
|
|
|
// Set when the scene the upscaler runs on has another size than the last one; vk_dlaa_resized_take reads it.
|
|
static volatile int32_t size_changed;
|
|
|
|
static void fg_only_frame(VkExec *ex, const VkExecTarget *dst);
|
|
|
|
static void evaluate(VkExec *ex, const VkExecTarget *dst) {
|
|
upscalers_drain(); // once a frame while the upscaler or frame generation's inputs run: what the GPU is past goes
|
|
if (D.fgo) { // no upscaler: frame generation's inputs alone, at the same point of the frame
|
|
fg_only_frame(ex, dst);
|
|
return;
|
|
}
|
|
const VkExecTarget *t = &D.color, *z = &D.depth;
|
|
if (!dst) dst = t;
|
|
uint32_t w = t->width, h = t->height, ow = dst->width, oh = dst->height;
|
|
vk_exec_tail_begin(ex); // NX2NAVI_TAIL_EARLY: from here on the frame's tail, in a command buffer of its own
|
|
VkCommandBuffer cb = ex->cb;
|
|
vk_exec_barrier(ex); // what drew the scene is done (no pass is open here)
|
|
vk_gpu_gap_note(VK_GAP_UPSCALER, (uint64_t)ow * oh);
|
|
if (D.preset_dirty) { // another model: features are created with it, so every size gets a new one
|
|
for (int i = 0; i < DLSS_SIZES; i++)
|
|
if (D.sizes[i].out) retire_size(&D.sizes[i]);
|
|
vk_upscaler_let_go();
|
|
D.last = NULL, D.preset_dirty = 0;
|
|
}
|
|
struct Size *e = size_for(cb, w, h, ow, oh);
|
|
if (!e) return dl_fail("work image creation", 0);
|
|
// Before the bloom the input is the scene's light before the exposure and the tonemap, at the game's upscaler the
|
|
// tonemapped picture: a history built from one does not hold for the other (prebloom_run).
|
|
{
|
|
const int at = D.pre ? 1 : 0;
|
|
static int told;
|
|
if (D.evals && at != D.pre_at) {
|
|
D.reset = 1;
|
|
if (told < 8)
|
|
told++, rt_log("dlaa: the upscaler now runs %s: a new history", at ? "before the bloom" : "after the tonemap");
|
|
}
|
|
D.pre_at = at;
|
|
}
|
|
// Read once: vk_dlaa_frame runs on the present's thread and can clear fsr_frame while this replay is between the
|
|
// checks below, which would send the rest of this function to DLSS's path without NGX after all.
|
|
const int fsr = use_fsr();
|
|
// The size whose DLSS feature this frame runs on: its own, or the DLSS mode's while that one takes this render size
|
|
// (range_host). On another size's feature DLSS reads a sub-rectangle of this frame's render size from each input.
|
|
// The light target, the motion vectors and the masks are exactly that size (size_for, mask_pass) and range_host
|
|
// has checked it against the size the feature was created for; the depth is the light target's size by the arming
|
|
// (vk_dlaa_pass), and a depth that did not cover the sub-rectangle keeps the frame on its own feature.
|
|
const int use_range = !fsr && N.ngx_up && range_on();
|
|
struct Size *host = use_range && z->width >= w && z->height >= h ? range_host(e) : e;
|
|
const int step = D.ev_w && (w != D.ev_w || h != D.ev_h || ow != D.ev_ow || oh != D.ev_oh);
|
|
D.stepped = 0;
|
|
if (e != D.last) {
|
|
if (use_range && host->feat && host == D.host) {
|
|
// another render size on the feature the frame before ran on: DLSS's history carries on. What this size
|
|
// keeps of its own last frame, the characters' mask and the distances (mv_pass), is from another moment.
|
|
D.stepped = 1, D.steps_kept += step, e->chars_ok = e->dist_ok = 0;
|
|
} else D.reset = 1, D.why[0]++, e->chars_ok = 0; // another size's history is from another moment
|
|
__atomic_store_n(&size_changed, 1, __ATOMIC_RELEASE);
|
|
} else if (use_range && host != D.host) D.reset = 1, D.why[0]++; // the same size on another feature: the DLSS mode changed
|
|
if (!fsr && !N.ngx_up) return; // neither runs this frame: vk_dlaa_frame turns DLAA off at the next present
|
|
if (!fsr && !D.params && !NGX_OK(N.alloc_params(&D.params))) return dl_fail("parameter allocation", 0);
|
|
NgxParam *p = D.params;
|
|
VkImageView zv = vk_exec_target_view(z->img, z->format, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_DEPTH_BIT, z->level,
|
|
z->base_layer, 1, 0);
|
|
float jx = D.jit_used ? D.ux : 0.0f, jy = D.jit_used ? D.uy : 0.0f;
|
|
edge_fill(cb, t, z, D.jit_used); // before the motion vectors read the depth
|
|
static int no_mv = -1;
|
|
if (no_mv < 0) no_mv = !rt_knob("DLAA_MV", "1"); // NX2NAVI_DLAA_MV=0: zero motion (test)
|
|
int masked = 0, mv = no_mv ? 0 : mv_pass(ex, e, zv, jx, jy, char_bias(), &masked);
|
|
if (mv != 1) {
|
|
VkClearColorValue zero = {{0, 0, 0, 0}};
|
|
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
vkCmdClearColorImage(cb, e->mv, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &all);
|
|
full_barrier(cb);
|
|
// no camera: the history cannot be moved along (2: kept as it is for a short gap, mv_pass dlaa_cam_keep)
|
|
if (!no_mv && mv == 0) D.reset = 1, D.why[1]++;
|
|
}
|
|
VkImageView cv = vk_exec_target_view(t->img, t->format, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_COLOR_BIT, t->level,
|
|
t->base_layer, 1, 0);
|
|
struct Snap *mk = D.mv_samp ? mask_pass(cb, cv, w, h) : NULL;
|
|
int timed = T.on > 0 && T.used[T.slot]; // mv_pass began this frame's slot
|
|
if (timed) times_mark(cb, T_DL0);
|
|
uint64_t c0 = timed ? kern_now_ns() : 0;
|
|
rec_drain(); // NGX records into the command buffer itself: the recorder thread must have made every call so far
|
|
if (timed) T.cpu_drain += kern_now_ns() - c0;
|
|
if (mv == 1) fcam_probe(cb, e, w, h); // mv_pass's distances, behind its barrier
|
|
if (fsr) { // FSR 3.1 (vk_ffx.cpp) in DLSS's place: the same images, then the same blit below
|
|
if (t->level || t->base_layer || z->level || z->base_layer) return dl_fail("FSR 3.1 on a target that is not level 0, layer 0", 0);
|
|
if (D.last_color != t->image) target_changed(t, mv == 1);
|
|
uint64_t now = plat_now_ns();
|
|
double dt = D.last_eval_ns ? (now - D.last_eval_ns) / 1e6 : 0;
|
|
D.last_eval_ns = now;
|
|
VkFfxImage fc = {t->image, t->format, w, h, 0}, fz = {z->image, z->format, w, h, 0},
|
|
fm = {e->mv, VK_FORMAT_R16G16_SFLOAT, w, h, 1}, fo = {e->out, VK_FORMAT_R16G16B16A16_SFLOAT, ow, oh, 1};
|
|
VkFfxCamera fcam;
|
|
if (!vk_dlaa_ffx_camera(&fcam)) memset(&fcam, 0, sizeof fcam); // no camera this frame: FSR's fixed values
|
|
if (!vk_ffx_upscale(cb, &fc, &fz, &fm, &fo, jx, jy, D.reset, dt > 0 && dt < 100 ? (float)dt : 16.67f, &fcam))
|
|
return dl_fail("FSR 3.1", 0);
|
|
static int told;
|
|
if (!told++) rt_log("dlaa: FSR 3.1 at %ux%u -> %ux%u (scene format %d, depth format %d)", w, h, ow, oh,
|
|
(int)t->format, (int)z->format);
|
|
goto evaluated;
|
|
}
|
|
float frame_ms = 0; // FrameTimeDeltaInMsec, taken once: the evaluation can run a second time (own_feature)
|
|
own_feature:
|
|
if (!host->feat) {
|
|
// The feature is made for the size of its entry: this frame's own, or the larger one of the DLSS mode's, which
|
|
// was released since (release_kept) and takes this frame's size as a sub-rectangle.
|
|
const uint32_t fw = host->w, fh = host->h;
|
|
if (idle_free() != 2) release_kept(host, feat_released);
|
|
feat_released = 0;
|
|
p_setu(p, "CreationNodeMask", 1);
|
|
p_setu(p, "VisibilityNodeMask", 1);
|
|
p_setu(p, "Width", fw);
|
|
p_setu(p, "Height", fh);
|
|
p_setu(p, "OutWidth", ow);
|
|
p_setu(p, "OutHeight", oh);
|
|
p_seti(p, "PerfQualityValue", perf_quality(fw, ow));
|
|
// IsHDR | MVLowRes | AutoExposure: linear scene light, motion vectors at the render size, no exposure texture
|
|
p_seti(p, "DLSS.Feature.Create.Flags", (1 << 0) | (1 << 1) | (1 << 6));
|
|
p_seti(p, "DLSS.Enable.Output.Subrects", 0);
|
|
// The model per mode (NVSDK_NGX_DLSS_Hint_Render_Preset: 1 A ... 11 K, 12 L, 13 M), as NVIDIA recommends for
|
|
// DLSS 4.5 (310.x): K (transformer) for DLAA, Quality and Balanced, M for Performance, L for Ultra Performance.
|
|
// M and L cost more on cards without FP8 (RTX 20/30). dlss_preset (D.preset, preset_check) takes one model for
|
|
// every mode instead.
|
|
int pk = D.preset ? D.preset : 11, pm = D.preset ? D.preset : 13, pl = D.preset ? D.preset : 12;
|
|
p_setu(p, "DLSS.Hint.Render.Preset.DLAA", (unsigned)pk);
|
|
p_setu(p, "DLSS.Hint.Render.Preset.UltraQuality", (unsigned)pk);
|
|
p_setu(p, "DLSS.Hint.Render.Preset.Quality", (unsigned)pk);
|
|
p_setu(p, "DLSS.Hint.Render.Preset.Balanced", (unsigned)pk);
|
|
p_setu(p, "DLSS.Hint.Render.Preset.Performance", (unsigned)pm);
|
|
p_setu(p, "DLSS.Hint.Render.Preset.UltraPerformance", (unsigned)pl);
|
|
// NX2NAVI_TRACE=perf: the create alone; the wait and the releases of release_kept have marks of their own
|
|
uint64_t t0 = perf_mark_begin();
|
|
NgxResult r = N.create(cb, NGX_FEATURE_SUPERSAMPLING, p, &host->feat);
|
|
perf_mark_end(PERF_M_NGX, t0, 0);
|
|
const int inflight = !NGX_OK(r) || !host->feat ? upd_count(&dead, UPD_FEAT) : 0;
|
|
if (inflight) {
|
|
// Refused while features let go of are still in flight (release_kept, retire_size): their video memory, or
|
|
// NGX's refusal of a feature with the parameters of a live one. The GPU finishes with them, they go, and the
|
|
// creation is tried once more: the only wait left on this path, for those features, not for the device.
|
|
const NgxResult r0 = r;
|
|
const uint64_t w0 = plat_now_ns();
|
|
upd_take(&dead, vk_upscaler_gpu_wait(upd_max_fence(&dead)), presents_now(), dead_release, NULL);
|
|
const double waited = (plat_now_ns() - w0) / 1e6;
|
|
host->feat = NULL;
|
|
t0 = perf_mark_begin();
|
|
r = N.create(cb, NGX_FEATURE_SUPERSAMPLING, p, &host->feat);
|
|
perf_mark_end(PERF_M_NGX, t0, 0);
|
|
rt_log("dlaa: DLSS feature creation refused (%08x) with %d released feature(s) still in flight: waited %.1f "
|
|
"ms for them, %s", (unsigned)r0, inflight, waited,
|
|
NGX_OK(r) && host->feat ? "made at the second try" : "refused again");
|
|
}
|
|
if (!NGX_OK(r) || !host->feat) return dl_fail("DLSS feature creation", r);
|
|
host->feat_fence = vk_exec_recording_fence(); // NGX records the creation into cb
|
|
D.reset = 1, D.steps_made += step;
|
|
int q = perf_quality(fw, ow), pr = q == 0 ? pm : q == 3 ? pl : pk;
|
|
rt_log("dlaa: DLSS feature created at %ux%u -> %ux%u, quality value %d, preset %c (scene format %d, depth format "
|
|
"%d)", fw, fh, ow, oh, q, 'A' + pr - 1, (int)t->format, (int)z->format);
|
|
host->q = q;
|
|
range_note(host); // NX2NAVI_DLSS_RANGE: which other render sizes it runs
|
|
}
|
|
if (D.last_color != t->image) target_changed(t, mv == 1);
|
|
NgxResourceVK rc = res_image(cv, t->image, t->format, w, h, VK_IMAGE_ASPECT_COLOR_BIT, 0);
|
|
NgxResourceVK ro = res_image(e->out_view, e->out, VK_FORMAT_R16G16B16A16_SFLOAT, ow, oh, VK_IMAGE_ASPECT_COLOR_BIT, 1);
|
|
NgxResourceVK rz = res_image(zv, z->image, z->format, w, h, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
|
|
NgxResourceVK rm = res_image(e->mv_view, e->mv, VK_FORMAT_R16G16_SFLOAT, w, h, VK_IMAGE_ASPECT_COLOR_BIT, 0);
|
|
p_setp(p, "Color", &rc);
|
|
p_setp(p, "Output", &ro);
|
|
p_setp(p, "Depth", &rz);
|
|
p_setp(p, "MotionVectors", &rm);
|
|
p_setf(p, "Jitter.Offset.X", jx);
|
|
p_setf(p, "Jitter.Offset.Y", jy);
|
|
p_setf(p, "Sharpness", 0.0f);
|
|
p_seti(p, "Reset", D.reset);
|
|
// One bias mask: the reactive one (NX2NAVI_DLSS_REACTIVE, a test) where it ran, else the characters' (mv_pass), else
|
|
// none. The parameter object is reused, so it is cleared explicitly.
|
|
NgxResourceVK rk = mk ? res_image(mk->mask_view, mk->mask, VK_FORMAT_R8_UNORM, w, h, VK_IMAGE_ASPECT_COLOR_BIT, 0)
|
|
: res_image(e->mask_view, e->mask, VK_FORMAT_R8_UNORM, w, h, VK_IMAGE_ASPECT_COLOR_BIT, 0);
|
|
p_setp(p, "DLSS.Input.Bias.Current.Color.Mask", mk || masked ? (void *)&rk : NULL);
|
|
p_setu(p, "DLSS.Input.Bias.Current.Color.Subrect.Base.X", 0);
|
|
p_setu(p, "DLSS.Input.Bias.Current.Color.Subrect.Base.Y", 0);
|
|
p_setf(p, "MV.Scale.X", 1.0f);
|
|
p_setf(p, "MV.Scale.Y", 1.0f);
|
|
p_setu(p, "DLSS.Render.Subrect.Dimensions.Width", w);
|
|
p_setu(p, "DLSS.Render.Subrect.Dimensions.Height", h);
|
|
if (host != e) {
|
|
// On the feature of a larger size, the render size above is a part of what the feature was created for: it
|
|
// starts at the top left of the light target, the depth and the motion vectors, as it does in the bias mask.
|
|
static const char *const base[6] = {"DLSS.Input.Color.Subrect.Base.X", "DLSS.Input.Color.Subrect.Base.Y",
|
|
"DLSS.Input.Depth.Subrect.Base.X", "DLSS.Input.Depth.Subrect.Base.Y",
|
|
"DLSS.Input.MV.Subrect.Base.X", "DLSS.Input.MV.Subrect.Base.Y"};
|
|
for (int i = 0; i < 6; i++) p_setu(p, base[i], 0);
|
|
}
|
|
p_setf(p, "DLSS.Pre.Exposure", 1.0f);
|
|
p_setf(p, "DLSS.Exposure.Scale", 1.0f);
|
|
// The time since the previous evaluation, which DLSS weighs its history by; 60 fps before the first and after a
|
|
// gap (a pause, a load), where no history carries over anyway.
|
|
if (!frame_ms) {
|
|
uint64_t now = plat_now_ns();
|
|
double dt = D.last_eval_ns ? (now - D.last_eval_ns) / 1e6 : 0;
|
|
D.last_eval_ns = now;
|
|
frame_ms = dt > 0 && dt < 100 ? (float)dt : 16.67f;
|
|
}
|
|
p_setf(p, "FrameTimeDeltaInMsec", frame_ms);
|
|
c0 = timed ? kern_now_ns() : 0;
|
|
NgxResult r = N.evaluate(cb, host->feat, p, NULL);
|
|
host->feat_fence = vk_exec_recording_fence(); // recorded into cb (a refusal too, for all this side knows)
|
|
if (timed) T.cpu_ngx += kern_now_ns() - c0;
|
|
if (!NGX_OK(r) && host != e) {
|
|
// DLSS refused this render size on the feature of another, inside the range it reported (range_note). Every
|
|
// size has its own feature from here on, as with NX2NAVI_DLSS_RANGE=0, and this frame runs again on its own,
|
|
// which starts a new history.
|
|
rt_log("dlaa: DLSS refused the render size %ux%u on the feature created for %ux%u -> %ux%u (%08x): every render "
|
|
"size has its own feature for the rest of this run", w, h, host->w, host->h, ow, oh, (unsigned)r);
|
|
range_refused = 1;
|
|
if (D.stepped) D.stepped = 0, D.steps_kept -= step;
|
|
host = e, D.reset = 1, D.why[0]++;
|
|
goto own_feature;
|
|
}
|
|
if (!NGX_OK(r)) return dl_fail("DLSS evaluation", r);
|
|
evaluated:
|
|
if (timed) times_mark(cb, T_DL1); // through the recorder, after NGX's own commands
|
|
// Frame generation gets the same new start with this frame's inputs (vk_dlaa_fg_inputs): a frame DLSS starts a
|
|
// new history on (a camera cut, another scene size or target) has no motion from the frame before that holds.
|
|
// Another scene size is one for frame generation also where DLSS's own history stayed (D.stepped): the depth and
|
|
// the motion vectors it reads are of that other size.
|
|
D.fg_reset = D.reset || D.stepped;
|
|
D.reset = 0;
|
|
D.last = e, D.host = host, e->used = host->used = host->feat_used = D.evals + 1;
|
|
D.ev_w = w, D.ev_h = h, D.ev_ow = ow, D.ev_oh = oh;
|
|
if (D.evals % 600 == 0) query_vram();
|
|
stale_sizes(e, host);
|
|
full_barrier(cb);
|
|
if (fsr && !vk_ffx_fsr4() && w < ow) fsr3_sharpen(cb, e, w, ow, oh); // FSR 3.1 made the picture
|
|
vk_mvcheck_output(cb, e->out, ow, oh); // NX2NAVI_MV_CHECK (test)
|
|
// the result into the scene target (DLAA: the bloom, the post passes and the HUD read it and draw over it) or into
|
|
// the scaling pass's target (the HUD draws over it)
|
|
VkImageBlit b = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
|
.srcOffsets = {{0, 0, 0}, {(int32_t)ow, (int32_t)oh, 1}},
|
|
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, dst->level, dst->base_layer, 1},
|
|
.dstOffsets = {{0, 0, 0}, {(int32_t)ow, (int32_t)oh, 1}}};
|
|
// NX2NAVI_DLAA_MVDEBUG=1 (test): the motion vectors in place of the picture, x in red and y in green, render pixels
|
|
// as scene light (the post passes still run over them)
|
|
static int mvdebug = -1;
|
|
if (mvdebug < 0) mvdebug = rt_knob("DLAA_MVDEBUG", "0");
|
|
static int maskdebug = -1;
|
|
if (maskdebug < 0) maskdebug = rt_knob("DLAA_MASKDEBUG", "0");
|
|
// (NX2NAVI_DLAA_MASKDEBUG=1 shows whichever bias mask DLSS got: the reactive one, or the characters')
|
|
VkImage shown = mvdebug ? e->mv : maskdebug && mk ? mk->mask : maskdebug && masked ? e->mask : e->out;
|
|
if (shown != e->out) b.srcOffsets[1] = (VkOffset3D){(int32_t)w, (int32_t)h, 1};
|
|
vkCmdBlitImage(cb, shown, VK_IMAGE_LAYOUT_GENERAL, dst->image, VK_IMAGE_LAYOUT_GENERAL, 1, &b, VK_FILTER_NEAREST);
|
|
full_barrier(cb);
|
|
if (dst != t) vk_dlaa_swap_img = t->img, vk_dlaa_swap_view = e->out_view; // the scaling draw reads DLSS's picture
|
|
// frame generation keeps the HUD out of its generated frames; before the bloom, DLSS's output is not yet the picture
|
|
// and the HUD-less one is taken at the game's upscaler (vk_dlaa_upscaler)
|
|
if (vk_dlaa_hudless_wanted && !D.pre) hudless(cb, e->out, ow, oh);
|
|
ex->recorded += 4;
|
|
vk_exec_draw_new_cb(); // NGX bound pipelines and descriptor sets of its own: the next draw binds everything again
|
|
D.jdepth = z->image, D.jw = z->width, D.jh = z->height;
|
|
// for frame generation at this frame's present (vk_dlaa_fg_inputs)
|
|
D.fg_depth = (VkFfxImage){z->image, z->format, w, h, 0};
|
|
D.fg_mv = (VkFfxImage){e->mv, VK_FORMAT_R16G16_SFLOAT, w, h, 1};
|
|
D.fg_depth_view = zv, D.fg_mv_view = e->mv_view;
|
|
D.fg_jx = jx, D.fg_jy = jy, D.fg_at = D.presents, D.fg_fresh = 1;
|
|
if (D.jit_used) D.jittered++;
|
|
next_jitter(w, ow);
|
|
if (++D.evals == 1 || D.evals == 300 || D.evals % 3600 == 0) // 300: one early line with the frames counted
|
|
rt_log("dlaa: %llu frames anti-aliased (scene %ux%u, output %ux%u; %llu frames without the DLAA point, %llu with camera "
|
|
"motion, %llu with the characters' mask, %llu without a camera, %llu jittered (%llu draws moved, %llu not), "
|
|
"%llu jitter holds; resets: %llu size, %llu camera, %llu target; reactive mask %llu, without its copy %llu; "
|
|
"no camera: %llu without a scene-context draw, %llu not in DLSS's submission; %llu kept with zero motion, "
|
|
"%llu runs past %d; %llu cameras read at the draw; changes of scene size: %llu with the history kept, "
|
|
"%llu with a new feature)",
|
|
(unsigned long long)D.evals, w, h, ow, oh, (unsigned long long)D.frames_missed,
|
|
(unsigned long long)D.mv_frames, (unsigned long long)D.masked, (unsigned long long)D.mv_none,
|
|
(unsigned long long)D.jittered, (unsigned long long)D.jdraws, (unsigned long long)D.jfixed,
|
|
(unsigned long long)D.holds, (unsigned long long)D.why[0], (unsigned long long)D.why[1],
|
|
(unsigned long long)D.why[2], (unsigned long long)R.made, (unsigned long long)R.missed,
|
|
(unsigned long long)D.cam_none_draw, (unsigned long long)D.cam_none_snap, (unsigned long long)D.cam_kept,
|
|
(unsigned long long)D.cam_kept_max, KEEP_MAX, (unsigned long long)D.cam_at_draw,
|
|
(unsigned long long)D.steps_kept, (unsigned long long)D.steps_made);
|
|
window_line();
|
|
}
|
|
|
|
// The HUD-less picture (vk_dlaa_fg_hudless): DLSS's output, which the HUD is then drawn over, through the encode the
|
|
// game's last pass gives its picture (linear to sRGB), as RGBA8 UNORM bytes like the picture the present reads. With
|
|
// HDR (NX2NAVI_HDR = 1) the game's picture stays float and linear up to the present, which encodes it to PQ
|
|
// (vk_hdr_encode): DLSS's output is kept as RGBA16F for the same encode. The game's own HDR (native) encodes inside the
|
|
// game, which this cannot follow: no HUD-less picture there.
|
|
// hudless_from: the same from any picture of that kind, src (GENERAL, level and layer of w x h, a format that can be
|
|
// blitted from), which fg_only_frame takes from the game's own scaling where no upscaler runs.
|
|
int vk_dlaa_hudless_wanted;
|
|
// src: the picture without the HUD, linear, w x h, in GENERAL: DLSS's output where the upscaler ran at the game's
|
|
// upscaler, the game's upscaler's output where it ran before the bloom (vk_dlaa_upscaler). hudless_from takes a level
|
|
// and a layer of src as well: the game's own scaled picture where frame generation runs without an upscaler
|
|
// (fg_only_frame).
|
|
static void hudless_from(VkCommandBuffer cb, VkImage src, uint32_t level, uint32_t layer, uint32_t w, uint32_t h);
|
|
static void hudless(VkCommandBuffer cb, VkImage src, uint32_t w, uint32_t h) { hudless_from(cb, src, 0, 0, w, h); }
|
|
static void hudless_from(VkCommandBuffer cb, VkImage src, uint32_t level, uint32_t layer, uint32_t w, uint32_t h) {
|
|
const VkCtx *c = vk_ctx();
|
|
int hdr = vk_hdr_wanted() && !vk_hdr_native();
|
|
if (vk_hdr_native()) return;
|
|
VkFormat fmt = hdr ? VK_FORMAT_R16G16B16A16_SFLOAT : VK_FORMAT_R8G8B8A8_UNORM;
|
|
if (D.hl && (D.hl_w != w || D.hl_h != h || D.hl_fmt != fmt)) {
|
|
retire_hl(); // the presents of the last frames may still read it
|
|
vk_upscaler_let_go();
|
|
}
|
|
if (!D.hl) {
|
|
VkImageView v2 = VK_NULL_HANDLE;
|
|
VkImageUsageFlags u = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT;
|
|
if ((!hdr && !image2d(w, h, VK_FORMAT_R8G8B8A8_SRGB, u, &D.hl_srgb, &D.hl_srgb_mem, &v2)) ||
|
|
!image2d(w, h, fmt, u | VK_IMAGE_USAGE_STORAGE_BIT, &D.hl, &D.hl_mem, &D.hl_view)) {
|
|
if (v2) vkDestroyImageView(c->device, v2, NULL); // the first image's view, when the second image failed
|
|
hl_destroy(); // and what of the two images was made
|
|
rt_log("dlaa: the HUD-less picture's images could not be made: frame generation sees the HUD");
|
|
vk_dlaa_hudless_wanted = 0;
|
|
return;
|
|
}
|
|
if (v2) vkDestroyImageView(c->device, v2, NULL);
|
|
D.hl_w = w, D.hl_h = h, D.hl_fmt = fmt;
|
|
VkImageMemoryBarrier2 ib[2];
|
|
VkImage im[2] = {D.hl, D.hl_srgb};
|
|
int nim = D.hl_srgb ? 2 : 1;
|
|
for (int i = 0; i < nim; i++)
|
|
ib[i] = (VkImageMemoryBarrier2){.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
|
|
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
|
|
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = im[i],
|
|
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
|
|
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = (uint32_t)nim,
|
|
.pImageMemoryBarriers = ib};
|
|
vkCmdPipelineBarrier2(cb, &di);
|
|
}
|
|
VkImageBlit b = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, level, layer, 1}, .srcOffsets = {{0, 0, 0}, {(int32_t)w, (int32_t)h, 1}},
|
|
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{0, 0, 0}, {(int32_t)w, (int32_t)h, 1}}};
|
|
vkCmdBlitImage(cb, src, VK_IMAGE_LAYOUT_GENERAL, hdr ? D.hl : D.hl_srgb, VK_IMAGE_LAYOUT_GENERAL, 1, &b,
|
|
VK_FILTER_NEAREST);
|
|
full_barrier(cb);
|
|
if (hdr) {
|
|
D.hl_at = D.presents, D.hl_fresh = 1;
|
|
return;
|
|
}
|
|
VkImageCopy cp = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
|
.extent = {w, h, 1}};
|
|
vkCmdCopyImage(cb, D.hl_srgb, VK_IMAGE_LAYOUT_GENERAL, D.hl, VK_IMAGE_LAYOUT_GENERAL, 1, &cp);
|
|
full_barrier(cb);
|
|
D.hl_at = D.presents, D.hl_fresh = 1;
|
|
}
|
|
|
|
static int scene_color(VkFormat f) {
|
|
return f == VK_FORMAT_B10G11R11_UFLOAT_PACK32 || f == VK_FORMAT_R16G16B16A16_SFLOAT;
|
|
}
|
|
|
|
// Two frame shapes. A deferred frame (the G-buffer, several colour targets at the scene size) arms on the light target
|
|
// drawn after it and ends the scene at the first pass a quarter as wide (the bloom), as described above. Ocarina's
|
|
// frame has no G-buffer: its light target (B10G11R11 at the scene size, with the D32F scene depth) is drawn into from the
|
|
// first opaque pass on, half- and quarter-size effect passes come and go in between. Measured (NX2NAVI_DUMP, Kokiri
|
|
// Forest, DLAA in a 2560x1440 picture): the last pass with the scene depth is the bloom composite into a second light
|
|
// target (each such pass at the scene's size takes over the arming, with its depth); the game's upscaler (its stand-in
|
|
// here, vk_tonemap.c) then fills a light-format target without depth, at the picture's size, which the HUD is drawn
|
|
// into; a last draw copies that target into the 8-bit sRGB output target, encoding only, since the game tonemaps before
|
|
// its post pass (vk_tonemap.c, vk_hdr.c). The scene ends at the start of the pass the HUD is drawn into: DLSS reads the
|
|
// armed pair and its picture goes into that target, which the HUD is drawn over and the last draw reads. With an
|
|
// upscaling share that target is larger than the scene; in DLAA it has the scene's size. A frame without it (the
|
|
// menus) ends at the last draw, at the 8-bit output target of the scene's size, and DLSS's picture goes back into the
|
|
// light target, which that draw reads.
|
|
static int srgb8(VkFormat f) { return f == VK_FORMAT_R8G8B8A8_SRGB || f == VK_FORMAT_B8G8R8A8_SRGB; }
|
|
|
|
// The output target DLAA runs at. With the game's own HDR (vk_hdr.c vk_hdr_native) the same pass, at the same point of
|
|
// the frame, tonemaps into its PQ picture (vk_hdr.c vk_hdr_native_picture) instead of the 8-bit sRGB one; matching sRGB
|
|
// alone left DLAA off for the whole run there (no frame anti-aliased) and with it the frame generation that takes
|
|
// DLAA's depth and motion.
|
|
static int output_target(VkFormat f) { return srgb8(f) || vk_hdr_native_picture(f); }
|
|
|
|
double wide_dlss_share(void); // widescreen.c: the DLSS mode's share of the picture (1 DLAA), 0 off
|
|
|
|
// ---------------------------------------------------------------- before the bloom (NX2NAVI_DLSS_PREBLOOM)
|
|
// What the upscaler gets at the game's upscaler (vk_dlaa_upscaler) is the armed pair of the bloom composite: measured on
|
|
// frame dumps (NX2NAVI_DUMP; 2880x1620 scene in a 3840x2160 picture, and 1280x720 in 2560x1440 with DLSS), the light
|
|
// target holds the lit scene with its fog, transparents and lens flares as linear light up to about 160; the one
|
|
// full-screen draw that reads it and the bloom's smallest target writes the second light target with the scene depth
|
|
// attached, and that shader adds the bloom, applies the exposure, converts to the tonemap's colour space and ends in the
|
|
// fitted filmic curve and a 0..1 clamp (vk_hdr.c tonemap_splice finds the same curve). Its output, 0..1 and linear
|
|
// (the post pass after it stores its square root), is what DLSS anti-aliased and enlarged: a picture already exposed,
|
|
// tonemapped and bloomed, which the IsHDR and auto-exposure flags describe wrongly.
|
|
// With the setting on, the upscaler runs at the start of the bloom chain's first pass instead: two light-format targets
|
|
// smaller than the scene, no depth, right after the transparents (prebloom_start). Its input is then the scene's linear
|
|
// light before exposure and tonemap, at the render size, with the scene depth.
|
|
// - Without upscaling (DLAA) DLSS's picture goes back into the light target and the game's own passes (bloom, flares,
|
|
// composite and tonemap, post pass, its upscaler at 1:1, HUD) all run on it as they are.
|
|
// - With upscaling the game's passes after it are drawn at the render size, and the post pass and the game's upscaler
|
|
// are compute dispatches whose sizes the game sets, so they cannot be moved to the picture's size. What is moved
|
|
// (vk_dlaa_retarget) are the render passes from the light target on: DLSS's picture goes into PB.scene at the
|
|
// picture's size; a pass drawing into the light target (the lens flares, additive, no depth test) draws into
|
|
// PB.scene, and the bloom composite into PB.comp, both at the picture's size without the depth attachment, with
|
|
// their viewports and scissors scaled (vk_dlaa_viewport, vk_dlaa_target_scale). The composite samples the light
|
|
// target through its texture coordinates, so it reads PB.scene in its place (vk_dlaa_swap_img). The bloom chain
|
|
// reads the light target at its own small sizes; DLSS's picture is scaled back into it first, so the bloom starts
|
|
// from the resolved picture and does not shimmer with the jitter. The post pass and the game's upscaler then run on
|
|
// the game's composite target, which this frame never wrote, and at the game's upscaler PB.comp goes over their
|
|
// picture, as DLSS's did before (vk_dlaa_upscaler).
|
|
// A frame without that bloom chain (menus, a loading screen) runs the upscaler at the game's upscaler as before.
|
|
static struct {
|
|
VkImage scene, comp; // DLSS's picture with the flares, and the composite's picture: B10G11R11, picture size
|
|
VmaAllocation scene_mem, comp_mem;
|
|
VkImageView scene_view, comp_view;
|
|
uint32_t w, h;
|
|
uint64_t used; // D.presents when last used (idle_release frees them after IDLE_FREE_PRESENTS)
|
|
} PB;
|
|
|
|
static void pb_destroy(void) {
|
|
const VkCtx *c = vk_ctx();
|
|
if (PB.scene_view) vkDestroyImageView(c->device, PB.scene_view, NULL);
|
|
if (PB.comp_view) vkDestroyImageView(c->device, PB.comp_view, NULL);
|
|
if (PB.scene) vmaDestroyImage(c->vma, PB.scene, PB.scene_mem);
|
|
if (PB.comp) vmaDestroyImage(c->vma, PB.comp, PB.comp_mem);
|
|
memset(&PB, 0, sizeof PB);
|
|
}
|
|
|
|
// The same for images the GPU may still use (only the replay reads them): to the deferred destruction. PB.used stays.
|
|
static void retire_pb(void) {
|
|
vk_defer_view(PB.scene_view), vk_defer_view(PB.comp_view);
|
|
vk_defer_image(PB.scene, PB.scene_mem), vk_defer_image(PB.comp, PB.comp_mem);
|
|
const uint64_t used = PB.used;
|
|
memset(&PB, 0, sizeof PB);
|
|
PB.used = used;
|
|
}
|
|
|
|
// The two images at w x h, made again for another picture size (the old ones once the GPU is past them, as hudless).
|
|
static int pb_images(VkCommandBuffer cb, uint32_t w, uint32_t h) {
|
|
PB.used = D.presents;
|
|
if (PB.scene && PB.w == w && PB.h == h) return 1;
|
|
if (PB.scene || PB.comp) {
|
|
retire_pb();
|
|
vk_upscaler_let_go();
|
|
}
|
|
VkImageUsageFlags u = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT |
|
|
VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT;
|
|
const VkFormat f = VK_FORMAT_B10G11R11_UFLOAT_PACK32;
|
|
if (!image2d(w, h, f, u, &PB.scene, &PB.scene_mem, &PB.scene_view) ||
|
|
!image2d(w, h, f, u, &PB.comp, &PB.comp_mem, &PB.comp_view)) {
|
|
pb_destroy();
|
|
return 0;
|
|
}
|
|
VkImageMemoryBarrier2 ib[2];
|
|
VkImage im[2] = {PB.scene, PB.comp};
|
|
for (int i = 0; i < 2; i++)
|
|
ib[i] = (VkImageMemoryBarrier2){.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2,
|
|
.srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
|
|
.dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT,
|
|
.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL,
|
|
.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
|
|
.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = im[i],
|
|
.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}};
|
|
VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 2,
|
|
.pImageMemoryBarriers = ib};
|
|
vkCmdPipelineBarrier2(cb, &di);
|
|
// used again for the new images, which this frame uses: left at 0 by a clear of the old ones, idle_release took
|
|
// them for 600 presents unused and freed them at the next present, without a wait for the GPU
|
|
PB.w = w, PB.h = h, PB.used = D.presents;
|
|
rt_log("dlaa: prebloom: the passes after the upscaler draw at %ux%u (two B10G11R11 images, %.0f MiB)", w, h,
|
|
2.0 * w * h * 4 / 1048576.0);
|
|
return 1;
|
|
}
|
|
|
|
// The bloom chain's first pass, on Ocarina's frame: two light-format targets, each smaller than the armed light target
|
|
// both ways, no depth. In the dumps (2880x1620 and 1280x720 scenes) it is the only such pass and it comes right after
|
|
// the last pass of transparents into the light target; its targets are 480x270 at both scene sizes. The passes in
|
|
// between that read the light target at a third or half of its size have one target, or a light target with an R8 one.
|
|
static int prebloom_start(const VkExec *ex) {
|
|
if (ex->pass_ncolor != 2 || ex->depth.image) return 0;
|
|
for (int i = 0; i < 2; i++) {
|
|
const VkExecTarget *t = &ex->color[i];
|
|
if (!t->image || t->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || t->samples > 1 || t->is3d ||
|
|
t->width >= D.color.width || t->height >= D.color.height)
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// Runs the upscaler at the bloom chain's first pass (prebloom_start), before that pass begins. 0: not here this frame
|
|
// (the picture's size not known yet, or not of the scene's shape): the game's upscaler runs it as before.
|
|
static int prebloom_run(VkExec *ex) {
|
|
const uint32_t w = D.color.width, h = D.color.height;
|
|
const double share = wide_dlss_share();
|
|
const int native = share <= 0 || share >= 1;
|
|
uint32_t ow = D.pic_w, oh = D.pic_h;
|
|
if (!native) {
|
|
int known = ow > w && oh > h && (double)ow * h <= 1.03 * (double)oh * w && (double)oh * w <= 1.03 * (double)ow * h;
|
|
if (!known || !pb_images(ex->cb, ow, oh)) {
|
|
if (D.pre_misses++ < 4)
|
|
rt_log("dlaa: prebloom: the picture's size is not known yet (last %ux%u for a %ux%u scene): this frame "
|
|
"runs the upscaler after the tonemap", ow, oh, w, h);
|
|
return 0;
|
|
}
|
|
}
|
|
vk_exec_hook_turn(); // as in vk_dlaa_pass: the replay workers' draws read the jitter and its end
|
|
D.ended = D.done = 1;
|
|
D.pre = native ? 1 : 2;
|
|
const uint64_t evals = D.evals;
|
|
if (native) {
|
|
evaluate(ex, NULL); // into the light target, which the bloom and everything after it read
|
|
} else {
|
|
D.up = 1;
|
|
VkExecTarget dst = {.image = PB.scene, .format = VK_FORMAT_B10G11R11_UFLOAT_PACK32,
|
|
.aspect = VK_IMAGE_ASPECT_COLOR_BIT, .layers = 1, .width = ow, .height = oh, .samples = 1};
|
|
evaluate(ex, &dst);
|
|
}
|
|
if (D.evals == evals) { // the evaluation failed (dl_fail) or did not run: nothing moves to the picture's size
|
|
D.pre = 0, D.up = 0;
|
|
return 1;
|
|
}
|
|
if (D.pre == 2) {
|
|
// the composite samples the light target: PB.scene, which the lens flares are then drawn into, in its place
|
|
vk_dlaa_swap_img = D.color.img, vk_dlaa_swap_view = PB.scene_view;
|
|
// and the bloom chain reads the light target itself, at its own sizes: the resolved picture, scaled back down
|
|
VkImageBlit b = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
|
.srcOffsets = {{0, 0, 0}, {(int32_t)ow, (int32_t)oh, 1}},
|
|
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, D.color.level, D.color.base_layer, 1},
|
|
.dstOffsets = {{0, 0, 0}, {(int32_t)w, (int32_t)h, 1}}};
|
|
vkCmdBlitImage(ex->cb, PB.scene, VK_IMAGE_LAYOUT_GENERAL, D.color.image, VK_IMAGE_LAYOUT_GENERAL, 1, &b,
|
|
VK_FILTER_LINEAR);
|
|
full_barrier(ex->cb);
|
|
ex->recorded += 2;
|
|
}
|
|
static int told;
|
|
if (!told++)
|
|
rt_log("dlaa: prebloom: %s runs before the bloom, at the start of its first pass (two %ux%u targets, format %d), "
|
|
"on the light target %ux%u format %d with the depth %ux%u format %d; %s",
|
|
use_fsr() ? "FSR 3.1" : "DLSS", ex->color[0].width, ex->color[0].height, (int)ex->color[0].format, w, h,
|
|
(int)D.color.format, D.depth.width, D.depth.height, (int)D.depth.format,
|
|
native ? "its picture goes back into the light target (no upscaling)"
|
|
: "its picture is the picture's size, where the lens flares and the bloom composite are drawn");
|
|
return 1;
|
|
}
|
|
|
|
// vk_exec.c vk_exec_pass_begin, in every replay thread, once the pass's targets are resolved and the pass hooks ran:
|
|
// after the upscaler ran before the bloom to the picture's size (prebloom_run), a pass drawing into the light target
|
|
// draws into PB.scene and the bloom composite (one light-format target of the scene's size, with the scene depth) into
|
|
// PB.comp, each at the picture's size and without its depth attachment (neither tests depth in the dumps). Its draws'
|
|
// viewports and scissors follow (vk_dlaa_viewport, vk_dlaa_target_scale). A pass of the same targets begun again (the
|
|
// pass broken by a copy) keeps what it got: its targets are not resolved again.
|
|
void vk_dlaa_retarget(VkExec *ex) {
|
|
if (D.pre != 2 || !PB.scene || ex->pass_ncolor != 1) return;
|
|
VkExecTarget *c = &ex->color[0];
|
|
if (!c->image || c->image == PB.scene || c->image == PB.comp) return;
|
|
if (c->format != VK_FORMAT_B10G11R11_UFLOAT_PACK32 || c->samples > 1 || c->is3d || c->layers != 1) return;
|
|
VkImage to = VK_NULL_HANDLE;
|
|
int comp = 0;
|
|
if (c->image == D.color.image) to = PB.scene;
|
|
else if (c->width == D.color.width && c->height == D.color.height && ex->depth.image == D.depth.image &&
|
|
(!D.comp_game || D.comp_game == c->image))
|
|
to = PB.comp, comp = 1;
|
|
if (!to) return;
|
|
static int told[2];
|
|
if (!told[comp]++)
|
|
rt_log("dlaa: prebloom: %s (%ux%u format %d, %s) drawn at %ux%u", comp ? "the bloom composite" : "a pass into the light target",
|
|
c->width, c->height, (int)c->format, ex->depth.image ? "with the scene depth" : "no depth", PB.w, PB.h);
|
|
if (comp) D.comp_game = c->image, D.comp = 1;
|
|
c->image = to, c->img = NULL;
|
|
c->width = PB.w, c->height = PB.h;
|
|
c->level = c->base_layer = 0, c->scale = 1, c->blocks = 0, c->view_type = VK_IMAGE_VIEW_TYPE_2D;
|
|
memset(&ex->depth, 0, sizeof ex->depth);
|
|
ex->pass_depth = ex->pass_stencil = VK_FORMAT_UNDEFINED;
|
|
ex->pipe.depth_fmt = 0;
|
|
ex->pass_w = PB.w, ex->pass_h = PB.h, ex->pass_scale = 1, ex->pass_layers = 1, ex->pass_noatt = 0;
|
|
}
|
|
|
|
// The scale from the game's viewport and scissor to a pass vk_dlaa_retarget moved to the picture's size; 0 for any
|
|
// other pass.
|
|
int vk_dlaa_target_scale(const VkExec *ex, float *sx, float *sy) {
|
|
if (D.pre != 2 || !PB.scene || !D.color.width || !D.color.height) return 0;
|
|
VkImage t = ex->color[0].image;
|
|
if (!t || (t != PB.scene && t != PB.comp)) return 0;
|
|
*sx = (float)PB.w / (float)D.color.width, *sy = (float)PB.h / (float)D.color.height;
|
|
return 1;
|
|
}
|
|
|
|
// At the game's upscaler, with the upscaler run before the bloom: notes the picture's size, and puts the composite
|
|
// drawn at the picture's size over the game's upscaler's output (2), or leaves that output, which is already made from
|
|
// DLSS's picture (1). The HUD-less picture for frame generation is the result either way.
|
|
static void prebloom_upscaler(VkExec *ex, VkImage out, uint32_t w, uint32_t h) {
|
|
VkCommandBuffer cb = ex->cb;
|
|
if (D.pre == 2) {
|
|
if (!D.comp) {
|
|
static int told;
|
|
if (told < 8)
|
|
told++, rt_log("dlaa: prebloom: frame %llu drew no bloom composite after the upscaler: the game's "
|
|
"picture stays", (unsigned long long)__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED));
|
|
return;
|
|
}
|
|
// the same size but for a picture size changed since the upscaler ran: scaled then, for that frame
|
|
VkImageBlit b = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
|
.srcOffsets = {{0, 0, 0}, {(int32_t)PB.w, (int32_t)PB.h, 1}},
|
|
.dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
|
|
.dstOffsets = {{0, 0, 0}, {(int32_t)w, (int32_t)h, 1}}};
|
|
vkCmdBlitImage(cb, PB.comp, VK_IMAGE_LAYOUT_GENERAL, out, VK_IMAGE_LAYOUT_GENERAL, 1, &b,
|
|
PB.w == w && PB.h == h ? VK_FILTER_NEAREST : VK_FILTER_LINEAR);
|
|
full_barrier(cb);
|
|
ex->recorded += 2;
|
|
static int told;
|
|
if (!told++) rt_log("dlaa: prebloom: the composite drawn at %ux%u goes into the game's upscaler's %ux%u output",
|
|
PB.w, PB.h, w, h);
|
|
}
|
|
if (vk_dlaa_hudless_wanted) hudless(cb, out, w, h), ex->recorded += 2;
|
|
}
|
|
|
|
// Ocarina's own upscaler is the place for DLSS or FSR: its stand-in (vk_exec_draw.c untranslated_dispatch_fallback) has
|
|
// just scaled the scene into the image the rest of the frame shows, and the upscaler's picture goes over it there.
|
|
// Until this hook the upscaler ran at the pass after it (vk_dlaa_pass below), which was taken for the pass the HUD is
|
|
// drawn into, the stand-in's output image as its target. With the HUD hidden the game draws no such pass: the next pass
|
|
// is the frame's last, which encodes the stand-in's image into the 8-bit output. DLSS's or FSR's picture was then
|
|
// written into that 8-bit target and the last draw drew the stand-in's picture over it: the screen showed the
|
|
// jittered scene at its render size, scaled with a plain filter. On a player's save with the HUD off that was every
|
|
// frame: blocks of render pixels, a picture that shook with the jitter by up to one render pixel, the undrawn edge
|
|
// row in white, and the upscaler's cost for nothing; with the HUD on (every test save until then) the same settings
|
|
// gave the upscaler's picture. Found by dumping the same frame of both saves (NX2NAVI_DUMP): three HUD passes before the
|
|
// last one in one, none in the other.
|
|
// The image is the stand-in's output when it has the light target's format and the scene's shape and is no smaller
|
|
// than the scene (the movie network's and other tensor programs' outputs are RGBA8 or smaller). A frame without the
|
|
// dispatch (NX2NAVI_CS_FALLBACK=0, another engine) still ends at vk_dlaa_pass. NX2NAVI_DLAA_AT_UPSCALER=0 (test): as before.
|
|
void vk_dlaa_upscaler(VkExec *ex, VkImage out, VkFormat fmt, uint32_t w, uint32_t h) {
|
|
static int on = -1;
|
|
if (on < 0) on = rt_knob("DLAA_AT_UPSCALER", "1");
|
|
if (!inputs_on() || !out || fmt != VK_FORMAT_B10G11R11_UFLOAT_PACK32) return;
|
|
if (D.armed && !D.gbuf && w >= D.color.width && h >= D.color.height) D.pic_w = w, D.pic_h = h; // prebloom_run
|
|
if (D.pre) { // set only where the upscaler runs (vk_dlaa_pass: prebloom needs dl_active)
|
|
prebloom_upscaler(ex, out, w, h);
|
|
return;
|
|
}
|
|
if (!on || !D.armed || D.done || D.gbuf) return;
|
|
uint32_t sw = D.color.width, sh = D.color.height;
|
|
if (w < sw || h < sh || out == D.color.image) return;
|
|
if ((double)w * sh > 1.03 * (double)h * sw || (double)h * sw > 1.03 * (double)w * sh) return; // not the scene's shape
|
|
static int told;
|
|
if (!told++)
|
|
rt_log("dlaa: the upscaler runs where the game's own does: scene %ux%u into its %ux%u output", sw, sh, w, h);
|
|
VkExecTarget dst = {.image = out, .format = fmt, .aspect = VK_IMAGE_ASPECT_COLOR_BIT, .layers = 1, .width = w,
|
|
.height = h, .samples = 1};
|
|
vk_exec_hook_turn(); // as in vk_dlaa_pass: the replay workers' draws read the jitter and its end
|
|
D.ended = D.done = D.up = 1;
|
|
evaluate(ex, &dst);
|
|
}
|
|
|
|
// ---- what idle_release (below) leaves after the upscaler is switched off
|
|
// idle_release frees the DLSS sizes and the HUD-less picture some presents after the setting goes off. What it does not
|
|
// reach stayed for the process: the reactive mask's snapshots, the FSR 3.1 and FSR 4 upscale contexts, and the DLSS
|
|
// sizes after a failure (dl_fail turns the upscaler off without the setting changing, so D.on stays set). They go
|
|
// here, RETIRE_PRESENTS presents after the upscaler stopped running and frame generation stopped making inputs of its
|
|
// own from a size (inputs_on), on the replay thread, which makes every evaluation, once the GPU is past them, as
|
|
// free_size does for a size it replaces. Switched on again, the upscaler makes them anew, as at its first frame in a
|
|
// run.
|
|
#define RETIRE_PRESENTS 600
|
|
static uint64_t retire_off_at; // replay: D.presents when the upscaler stopped running
|
|
static uint64_t retired_n; // replay: times this gave memory back
|
|
|
|
static int up_holds(void) {
|
|
for (int i = 0; i < SNAPS; i++)
|
|
if (R.s[i].before) return 1;
|
|
if (D.failed)
|
|
for (int i = 0; i < DLSS_SIZES; i++)
|
|
if (D.sizes[i].out || D.sizes[i].feat) return 1;
|
|
return vk_ffx_holds();
|
|
}
|
|
|
|
static void retire_idle(void) {
|
|
uint64_t now = presents_now();
|
|
// no evaluation runs to take what the upscalers let go of (evaluate): it goes here once the GPU is past it
|
|
if (!inputs_on()) upscalers_drain();
|
|
if (!idle_free()) return; // as idle_release
|
|
if (inputs_on()) { // the upscaler runs, or frame generation alone makes its inputs from a size (D.fgo)
|
|
retire_off_at = 0;
|
|
return;
|
|
}
|
|
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]);
|
|
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]);
|
|
D.last = NULL, D.host = NULL, D.fg_fresh = 0, D.fg_mv_view = VK_NULL_HANDLE, D.fg_mv = (VkFfxImage){0};
|
|
}
|
|
vk_upscaler_let_go();
|
|
retired_n++;
|
|
rt_log("dlaa: the upscaler has not run for %d presents (%s): its mask snapshots and FSR contexts%s are freed (%llu "
|
|
"time(s)); made again when it runs", RETIRE_PRESENTS, D.failed ? "it failed" : "switched off",
|
|
D.failed ? " and DLSS sizes" : "", (unsigned long long)retired_n);
|
|
retire_off_at = 0;
|
|
}
|
|
|
|
void vk_dlaa_pass(VkExec *ex) {
|
|
const VkExecTarget *c = &ex->color[0], *z = &ex->depth;
|
|
retire_idle();
|
|
if (!inputs_on()) return;
|
|
if (D.done) { // diagnostics: another scene drawn into a light target in the same present interval
|
|
// (not before the bloom: the flares and the composite after it are of that shape, and expected)
|
|
if (!D.pre && ex->pass_ncolor == 1 && c->image && scene_color(c->format) && z->image && z->format == VK_FORMAT_D32_SFLOAT &&
|
|
c->width == D.color.width && c->height == D.color.height && z->width == c->width)
|
|
F.second++, F.second_img = c->image;
|
|
return;
|
|
}
|
|
static int deferred; // a scene-size G-buffer was seen: the deferred shape
|
|
unsigned o[2], s[2];
|
|
res_sizes(o, s);
|
|
// Ocarina's builds carry no scene or output patch sets, so res_sizes reports placeholders there: the sizes come
|
|
// from the frame itself (the light target's, and the output pass's).
|
|
int scene = z->image && z->format == VK_FORMAT_D32_SFLOAT && z->width >= DLAA_SCENE_MIN_W;
|
|
// The G-buffer: several colour targets with the D32F scene depth. Only the light target drawn after it arms the
|
|
// frame; at night the game draws into the light target alone with the scene depth before the G-buffer too, and
|
|
// DLAA then ran at a small pass ahead of the whole scene, which reached the screen jittered.
|
|
// A pass of that shape smaller than the light target already armed is not one: Ocarina draws a half-size effect
|
|
// into three targets with a half-size D32F depth after its scene, and taking that for a G-buffer switched every
|
|
// later frame to the deferred shape, where DLSS's and FSR 3.1's smaller modes never found their output pass.
|
|
if (ex->pass_ncolor >= 3 && scene && !(D.armed && (z->width < D.color.width || z->height < D.color.height))) {
|
|
if (!deferred)
|
|
rt_log("dlaa: a G-buffer pass (%d colour targets, depth %ux%u, target 0 format %d): the deferred frame shape",
|
|
ex->pass_ncolor, z->width, z->height, c->image ? (int)c->format : 0);
|
|
D.gbuf = 1, D.armed = 0, deferred = 1;
|
|
// target 0 is the object-class buffer (RGBA8, the scene size): the characters' bias mask reads its green. A
|
|
// smaller pass of the same shape (640x360, seen while loading) does not replace the scene's in the same frame.
|
|
if (c->image && c->format == VK_FORMAT_R8G8B8A8_UNORM && c->width == z->width && c->height == z->height &&
|
|
c->samples <= 1 && !c->is3d && (!D.ids_ok || c->width >= D.ids.width))
|
|
D.ids = *c, D.ids_ok = 1;
|
|
return;
|
|
}
|
|
// the scene depth is D32F; a D16 target of the scene size also serves one composite pass before the transparents.
|
|
// A smaller pair never takes over an armed one: the camera rune draws a half-size RGBA16F target with a half-size
|
|
// D32F depth after the lighting (852x480 under a 1704x960 scene), and DLSS ran on that and wrote its picture into
|
|
// the scene's own light target, which left every object black.
|
|
if ((D.gbuf || !deferred) && ex->pass_ncolor == 1 && c->image && scene_color(c->format) && scene &&
|
|
c->width == z->width && c->height == z->height &&
|
|
(!D.armed || (c->width >= D.color.width && c->height >= D.color.height)) && c->samples <= 1 && !c->is3d) {
|
|
int first = !D.armed;
|
|
D.color = *c, D.depth = *z, D.armed = 1;
|
|
static int told;
|
|
if (first && !told++)
|
|
rt_log("dlaa: first scene armed: light target %ux%u format %d, depth %ux%u", c->width, c->height, (int)c->format,
|
|
z->width, z->height);
|
|
if (first) mask_snapshot(ex); // the lit scene, before the transparents (reactive mask)
|
|
return;
|
|
}
|
|
if (!D.armed) return;
|
|
if (!deferred) { // Ocarina's shape: the output pass ends the scene and runs DLSS
|
|
// NX2NAVI_DLSS_PREBLOOM: before the bloom chain's first pass instead, where there is one (prebloom_run)
|
|
if (D.pre_on && dl_active() && prebloom_start(ex) && prebloom_run(ex)) return; // not for frame generation alone
|
|
int one = ex->pass_ncolor == 1 && c->image && !z->image && c->image != D.color.image && c->samples <= 1 &&
|
|
!c->is3d;
|
|
if (one) F.one_n++, F.one_w = c->width, F.one_h = c->height, F.one_fmt = c->format;
|
|
// the output pass is the picture's size, the scene over the mode's share: at Ultra Performance (854 wide) a
|
|
// 960x540 post pass comes first, and DLSS wrote there, which left a 540-line picture stretched to the output
|
|
// A frame whose scene is not at that share (the first presents before the scale patch takes, a dynamic
|
|
// resolution step) has no such pass: after a frame that missed DLSS the first pass wider than the scene is
|
|
// taken for 120 frames (D.share_off, vk_dlaa_frame), so a mismatch costs one frame rather than every frame.
|
|
double share = wide_dlss_share();
|
|
int native = share <= 0 || share >= 1; // no upscaling share: the scene is as large as the picture
|
|
int big = D.share_off || native || c->width >= 0.95 * D.color.width / share;
|
|
int same = c->width == D.color.width && c->height == D.color.height;
|
|
// The output pass has the scene's shape: the game redraws its 2000x2000 area map into an RGBA8 target of its
|
|
// own on a room change and on the pause menu, one colour target and no depth, larger than the scene both ways.
|
|
// Taken for the output pass, DLSS wrote the game's picture into the map, and the minimap showed that picture
|
|
// behind its room shapes until the pause menu drew the map again. A pass whose width over height is more than
|
|
// 3% from the scene's (sizes round to multiples of 8) is not the output; the ultrawide sets widen the scene
|
|
// with the picture, so their output pass still matches.
|
|
int shape = (double)c->width * D.color.height <= 1.03 * (double)c->height * D.color.width &&
|
|
(double)c->height * D.color.width <= 1.03 * (double)c->width * D.color.height;
|
|
if (one && !shape && big && c->width > D.color.width && c->height > D.color.height && D.shape_logged < 8)
|
|
D.shape_logged++, rt_log("dlaa: a %ux%u pass is not the output pass: its shape is not the scene's %ux%u",
|
|
c->width, c->height, D.color.width, D.color.height);
|
|
if (one && shape && big && c->width > D.color.width && c->height > D.color.height) {
|
|
// DLSS runs: the replay workers' draws read the jitter and its end, and list the per-object motion it
|
|
// takes (vk_exec.c "replay workers"); the G-buffer and the arming above change nothing they read
|
|
vk_exec_hook_turn();
|
|
D.ended = D.done = D.up = 1;
|
|
evaluate(ex, c);
|
|
} else if (one && same && native && scene_color(c->format)) {
|
|
// DLAA in gameplay: the pass the HUD is drawn into is also at the scene's size, in the light format, and
|
|
// the game's upscaler (its stand-in here) has just written the picture there. Waiting for the 8-bit output
|
|
// pass, as in a frame without it, put DLSS's picture back into the armed light target (the bloom
|
|
// composite's), which nothing reads after the upscaler took it: the frame showed no DLAA at all. With an
|
|
// upscaling share the HUD's pass is the larger one above, so a pass of this shape is not it and is ignored
|
|
// there as before.
|
|
static int told;
|
|
if (!told++)
|
|
rt_log("dlaa: the pass the HUD is drawn into is at the scene's size (%ux%u, format %d): DLSS writes there",
|
|
c->width, c->height, (int)c->format);
|
|
vk_exec_hook_turn();
|
|
D.ended = D.done = D.up = 1;
|
|
evaluate(ex, c);
|
|
} else if (one && same && output_target(c->format)) {
|
|
vk_exec_hook_turn();
|
|
D.ended = D.done = 1;
|
|
evaluate(ex, NULL);
|
|
}
|
|
return;
|
|
}
|
|
if (!D.ended) {
|
|
if (!ex->pass_w || ex->pass_w * 4 > D.color.width) return;
|
|
vk_exec_hook_turn();
|
|
D.ended = 1;
|
|
if (D.color.width >= o[0]) D.done = 1, evaluate(ex, NULL); // DLAA in place; else wait for the scaling pass
|
|
return;
|
|
}
|
|
// the game's scaling pass: one colour target of the output size, no depth
|
|
if (ex->pass_ncolor == 1 && c->image && !z->image && scene_color(c->format) && c->width > D.color.width &&
|
|
c->width == o[0] && c->height == o[1] && c->samples <= 1 && !c->is3d) {
|
|
vk_exec_hook_turn();
|
|
D.done = D.up = 1;
|
|
evaluate(ex, c);
|
|
}
|
|
}
|
|
|
|
// NX2NAVI_DLSS, read at each present (the menu switches it live): the scene size of the mode (resolution.c) and the
|
|
// texture bias that goes with it while DLSS or FSR 3.1 runs. NVIDIA's DLSS guide asks for log2(render / output) - 1 on
|
|
// top of the game's own bias: the render scale's part (res_dlss_lod_bias) samples textures for the output's resolution,
|
|
// and the -1 sharper still, since the jittered frames DLSS accumulates resolve the detail one level finer: -1 at DLAA,
|
|
// -1.58 in Quality (67 %), -2 in Performance. The -1 comes only with the jitter (jitter_on): without it the same pixel
|
|
// samples the same spot every frame, and a sharper level would shimmer instead. The sampler clamps the sum to the
|
|
// device's limit (vk_texture.c make_sampler).
|
|
#define JITTER_LOD_BIAS (-1.0f)
|
|
static void dlss_mode(void) {
|
|
static char mode[32];
|
|
static int bias_on = -1;
|
|
const char *m = dynres_override(); // dynamic resolution's step (dynres.c) while it differs from the setting
|
|
if (!m) m = cfg_value("NX2NAVI_DLSS");
|
|
if (strcmp(m, mode)) {
|
|
snprintf(mode, sizeof mode, "%s", m);
|
|
res_dlss_mode(m);
|
|
bias_on = -1;
|
|
}
|
|
int b = dl_active();
|
|
if (b != bias_on)
|
|
bias_on = b, vk_sampler_dlss_bias(b ? res_dlss_lod_bias() + (jitter_on() ? JITTER_LOD_BIAS : 0.0f) : 0.0f);
|
|
}
|
|
|
|
|
|
// The preset, read again when a setting changed (the menu); a different one recreates the features at the next DLAA run.
|
|
static void preset_check(void) {
|
|
unsigned v = cfg_version();
|
|
if (v == D.cfg_seen) return;
|
|
D.cfg_seen = v;
|
|
int p = preset_value();
|
|
if (p == D.preset) return;
|
|
if (D.evals) {
|
|
if (p) rt_log("dlaa: DLSS preset %c in every mode; the features are created again", 'A' + p - 1);
|
|
else rt_log("dlaa: DLSS presets per mode; the features are created again");
|
|
}
|
|
D.preset = p, D.preset_dirty = 1, D.reset = 1;
|
|
}
|
|
|
|
// The scene's size after a setting changed (a DLSS or FSR 3.1 mode picked in the menu). The game makes its scene targets
|
|
// again at the new size and they start empty, so the first frames drawn into them are wrong: the first one is lit
|
|
// without what the frame before it left (a dark scene), and two frames later the scene can be black with the interface
|
|
// intact (Ultra Performance and DLAA). Measured by capturing every present over six mode changes, 2026-11-10: bad
|
|
// frames 3 and 5 presents after the change, with every other setting the same. evaluate notes a scene of another size;
|
|
// vk_dlaa_frame counts the presents since a setting last changed, so a game that draws two sizes in turn holds nothing.
|
|
static int setting_presents; // vk_dlaa_frame: presents left in which that counts as a mode change
|
|
#define SETTING_PRESENTS 60
|
|
|
|
// vk_backend.c frame_hold, each present after vk_dlaa_frame: 1 once when the scene took a new size after a setting
|
|
// changed. The previous picture then stays on screen over the frames described above.
|
|
int vk_dlaa_resized_take(void) {
|
|
if (!__atomic_exchange_n(&size_changed, 0, __ATOMIC_ACQ_REL) || !setting_presents) return 0;
|
|
setting_presents = 0;
|
|
return 1;
|
|
}
|
|
|
|
// What DLSS holds while nothing uses it (NX2NAVI_DLSS_IDLE_FREE 1 or 2, the default 1). With the upscaler switched off
|
|
// in the menu, the sizes kept their images and features for the rest of the run, since only an evaluation frees a size
|
|
// (stale_sizes): at a 5120x2142 output that is the output, sharpening, mask, class and distance images, about 0.4 GiB,
|
|
// and about 0.5 GiB per feature. With frame generation off and DLSS on, the HUD-less picture (hudless) stayed the same
|
|
// way. After IDLE_FREE_PRESENTS presents without their use they go here, on the replaying thread that makes every
|
|
// other NGX call, without waiting for the GPU: the last evaluation that used them is that many presents back, and the
|
|
// CPU is never more than a few ahead. A feature made later is made with none alive, the case release_kept leaves
|
|
// clean. What it costs: switching DLSS on again more than IDLE_FREE_PRESENTS presents after switching it off makes
|
|
// the size's images and feature again, which its first evaluation waits for, as at the first frame DLSS ran in a run.
|
|
#define IDLE_FREE_PRESENTS 600
|
|
static uint64_t off_at; // D.presents when DLSS went off
|
|
static void idle_release(void) {
|
|
if (!idle_free()) return;
|
|
if (D.hl && D.presents - D.hl_at >= IDLE_FREE_PRESENTS && !D.hl_fresh) {
|
|
rt_log("dlaa: the HUD-less picture for frame generation went unused for %d presents: freed", IDLE_FREE_PRESENTS);
|
|
hl_destroy();
|
|
}
|
|
if (PB.scene && D.presents - PB.used >= IDLE_FREE_PRESENTS) { // prebloom off, or no upscaling, for that long
|
|
rt_log("dlaa: prebloom: the picture-size images went unused for %d presents: freed", IDLE_FREE_PRESENTS);
|
|
pb_destroy();
|
|
}
|
|
if (D.on || D.presents - off_at < IDLE_FREE_PRESENTS) return;
|
|
int n = 0;
|
|
for (int i = 0; i < DLSS_SIZES; i++) {
|
|
struct Size *s = &D.sizes[i];
|
|
// Frame generation without an upscaler (fg_only_frame) makes its motion images in the size of the scene's
|
|
// size at every frame, and that size can change from one frame to the next: a size it used within the last
|
|
// IDLE_FREE_PRESENTS evaluations may still be in flight, and destroy_size does not wait for the GPU. Such a
|
|
// size stays, and so does the last one; a DLSS feature made for the same sizes (DLAA) goes, since the upscaler
|
|
// that used it has been off for that long.
|
|
if (D.fgo && (s == D.last || s->used + IDLE_FREE_PRESENTS > (uint64_t)D.evals)) {
|
|
if (s->feat) {
|
|
rt_log("dlaa: the DLSS feature at %ux%u went unused for %d presents with the upscaler off: released; "
|
|
"its size's motion images stay for frame generation", s->w, s->h, IDLE_FREE_PRESENTS);
|
|
feat_release(s->feat), s->feat = NULL, feat_released = 1;
|
|
}
|
|
if (D.host == s) D.host = NULL;
|
|
continue;
|
|
}
|
|
if (s->out || s->feat) {
|
|
rt_log("dlaa: the DLSS size %ux%u -> %ux%u went unused for %d presents with the upscaler off: freed",
|
|
s->w, s->h, s->ow, s->oh, IDLE_FREE_PRESENTS);
|
|
destroy_size(s), n++;
|
|
}
|
|
}
|
|
// frame generation's motion vectors were a size's (evaluate); none is handed out this late (fg_current). Without an
|
|
// upscaler they are the size kept above, which this frame's inputs may still be.
|
|
if (n && !D.fgo)
|
|
D.last = NULL, D.host = NULL, D.fg_fresh = 0, D.fg_mv_view = VK_NULL_HANDLE, D.fg_mv = (VkFfxImage){0};
|
|
}
|
|
|
|
// ---------------------------------------------------------------- frame generation without an upscaler
|
|
// Frame generation (vk_dlssg.c, vk_fg.cpp, vk_fg4.cpp, gfx_present.c's NGX presenter) takes the frame's depth, motion
|
|
// vectors, camera and picture without the HUD from this file. With an upscaler they are what its evaluation used
|
|
// (evaluate). With frame generation running and no upscaler (dlaa = 0, or DLSS set where it is not available: D.fgo,
|
|
// vk_dlaa_frame) the frame is armed and ended in the same places (vk_dlaa_draw, vk_dlaa_pass, vk_dlaa_upscaler), and
|
|
// evaluate hands it to fg_only_frame, which leaves the upscaler's evaluation out:
|
|
// - the camera is the scene context the scene's draws use (vk_dlaa_draw), and the motion vectors come from the same pass
|
|
// at the scene's size (mv_pass, the per-object motion included), into a size's images made for the scene's size
|
|
// alone: no DLSS feature, and the image the upscaler's picture would go to has the scene's size;
|
|
// - nothing is jittered: next_jitter is never called, so jit_ok stays 0 and no viewport moves (vk_dlaa_viewport). A
|
|
// jittered frame that nothing resolves shakes on screen. The vectors and frame generation get a jitter of 0;
|
|
// - the picture without the HUD is copied from the target the upscaler's picture would have gone to, which the game's
|
|
// own scaling (vk_exec_draw.c untranslated_dispatch_fallback, vk_tonemap.c) has just written at the picture's size,
|
|
// or from the light target itself where the frame ends in place. A deferred frame's scaling pass (vk_dlaa_pass)
|
|
// draws its picture inside the pass that ends the scene, so its target does not hold it yet: such a frame has none;
|
|
// - nothing is drawn over the frame: the game's scaling draw stays (D.up 0) and no pass reads another picture
|
|
// (vk_dlaa_swap_img).
|
|
// The scene can then be smaller than the picture (the game's own render scale), as in the upscaler's scaling modes:
|
|
// the depth and the vectors have the scene's size and the picture without the HUD the picture's, which every frame
|
|
// generation takes as it does there (vk_dlssg.c vk_dlssg_frame: the vectors scaled by the depth's size, the output by
|
|
// the swapchain's; vk_fg.cpp vk_fg_frame: the depth's size as the render size, the context made for up to the
|
|
// picture's; vk_dlaa.c vk_ngxfg_evaluate: the vectors' and the depth's own rectangles; gfx_present.c hudless_record:
|
|
// the picture without the HUD scaled to the swapchain like the picture).
|
|
|
|
// Frame generation that takes this frame's inputs: NVIDIA's through Streamline (vk_dlssg.c, set up with the device),
|
|
// AMD's swapchain in place (vk_fg.cpp, either library) or NVIDIA's through NGX, set now (vk_ngxfg_on).
|
|
static int fg_runs(void) { return vk_dlssg_active() || vk_fg_active() || vk_ngxfg_on(); }
|
|
|
|
// One line at the first present and one at each change: whether frame generation runs, and where its inputs come from.
|
|
static void fg_state_line(int on, int fgo) {
|
|
static int last = -1;
|
|
const int st = !fg_runs() ? 0 : fgo ? 2 : on && !D.failed ? 1 : 3;
|
|
if (st == last) return;
|
|
last = st;
|
|
if (st == 0) rt_log("dlaa: frame generation does not run: no inputs are made for it");
|
|
else if (st == 1) rt_log("dlaa: frame generation runs with its inputs from the upscaler (%s)", use_fsr() ? "FSR" : "DLSS");
|
|
else if (st == 2)
|
|
rt_log("dlaa: frame generation runs without an upscaler: the scene's depth, motion vectors and camera, no "
|
|
"jitter, and the picture without the HUD from the game's own scaling");
|
|
else rt_log("dlaa: frame generation runs, but the upscaler or its inputs failed earlier in this run: none are made");
|
|
}
|
|
|
|
// vkCmdBlitImage reads an image of format f with optimal tiling into hudless_from's float, UNORM or sRGB images: the
|
|
// format must have BLIT_SRC, and an integer format may only be blitted into another integer one, which linear
|
|
// filtering (no integer format has it) rules out. The upscaler's targets (B10G11R11, RGBA16F, RGBA8) have all of that by
|
|
// the Vulkan spec's required formats; anything else is asked once per format.
|
|
static int blit_from(VkFormat f) {
|
|
static VkFormat known[8];
|
|
static int ok[8], n;
|
|
for (int i = 0; i < n; i++)
|
|
if (known[i] == f) return ok[i];
|
|
VkFormatProperties fp;
|
|
vkGetPhysicalDeviceFormatProperties(vk_ctx()->phys, f, &fp);
|
|
const VkFormatFeatureFlags need = VK_FORMAT_FEATURE_BLIT_SRC_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT;
|
|
int r = (fp.optimalTilingFeatures & need) == need;
|
|
if (!r) rt_log("dlaa: the picture's format %d cannot be copied for frame generation: no picture without the HUD", (int)f);
|
|
if (n < 8) known[n] = f, ok[n++] = r;
|
|
return r;
|
|
}
|
|
|
|
static void fg_only_frame(VkExec *ex, const VkExecTarget *dst) {
|
|
const VkExecTarget *t = &D.color, *z = &D.depth;
|
|
const uint32_t w = t->width, h = t->height;
|
|
D.up = 0; // the game's scaling draw makes the picture (vk_dlaa_skip_draw)
|
|
D.stepped = 0;
|
|
vk_exec_barrier(ex); // what drew the scene, and the game's scaling into dst, is done (no pass is open here)
|
|
VkCommandBuffer cb = ex->cb;
|
|
struct Size *e = size_for(cb, w, h, w, h);
|
|
if (!e) {
|
|
rt_log("dlaa: the motion images for frame generation could not be made: none for the rest of this run");
|
|
D.failed = 1;
|
|
return;
|
|
}
|
|
// another size: its own distances and masks are from another moment, and the vectors start anew
|
|
if (e != D.last) D.reset = 1, D.why[0]++, e->chars_ok = e->dist_ok = 0;
|
|
VkImageView zv = vk_exec_target_view(z->img, z->format, VK_IMAGE_VIEW_TYPE_2D, VK_IMAGE_ASPECT_DEPTH_BIT, z->level,
|
|
z->base_layer, 1, 0);
|
|
static int no_mv = -1;
|
|
if (no_mv < 0) no_mv = !rt_knob("DLAA_MV", "1"); // NX2NAVI_DLAA_MV=0: zero motion (test), as in evaluate
|
|
// no jitter, and bias 0: the bias mask is DLSS's alone
|
|
int masked = 0, mv = no_mv ? 0 : mv_pass(ex, e, zv, 0.0f, 0.0f, 0.0f, &masked);
|
|
if (mv != 1) {
|
|
VkClearColorValue zero = {{0, 0, 0, 0}};
|
|
VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
|
|
vkCmdClearColorImage(cb, e->mv, VK_IMAGE_LAYOUT_GENERAL, &zero, 1, &all);
|
|
full_barrier(cb);
|
|
if (!no_mv && mv == 0) D.reset = 1, D.why[1]++; // no camera: frame generation starts anew
|
|
}
|
|
if (D.last_color != t->image) target_changed(t, mv == 1);
|
|
if (mv == 1) fcam_probe(cb, e, w, h); // mv_pass's distances, behind its barrier
|
|
const VkExecTarget *pic = dst ? dst : t;
|
|
int hl = vk_dlaa_hudless_wanted && (!dst || !D.gbuf) && blit_from(pic->format);
|
|
if (hl) hudless_from(cb, pic->image, pic->level, pic->base_layer, pic->width, pic->height);
|
|
D.fg_reset = D.reset;
|
|
D.reset = 0;
|
|
D.last = e, e->used = D.evals + 1, D.evals++;
|
|
ex->recorded += 2;
|
|
vk_exec_draw_new_cb(); // the motion pass bound a pipeline and descriptors of its own: the next draw binds all again
|
|
D.jdepth = z->image, D.jw = z->width, D.jh = z->height; // the scene depth the camera's draws go to (vk_dlaa_draw)
|
|
D.fg_depth = (VkFfxImage){z->image, z->format, w, h, 0};
|
|
D.fg_mv = (VkFfxImage){e->mv, VK_FORMAT_R16G16_SFLOAT, w, h, 1};
|
|
D.fg_depth_view = zv, D.fg_mv_view = e->mv_view;
|
|
D.fg_jx = D.fg_jy = 0.0f, D.fg_at = D.presents, D.fg_fresh = 1;
|
|
D.jit_ok = 0;
|
|
static uint64_t frames, still;
|
|
frames++, still += mv != 1;
|
|
if (frames == 1 || frames == 300 || frames % 3600 == 0)
|
|
rt_log("dlaa: frame generation without an upscaler: %llu frames prepared (scene %ux%u, picture %ux%u%s), %llu "
|
|
"without camera motion", (unsigned long long)frames, w, h, pic->width, pic->height,
|
|
hl ? "" : ", no picture without the HUD", (unsigned long long)still);
|
|
}
|
|
|
|
void vk_dlaa_frame(void) {
|
|
static unsigned cfg_seen;
|
|
static int cfg_known;
|
|
unsigned v = cfg_version();
|
|
D.presents++;
|
|
if (cfg_known && v != cfg_seen) setting_presents = SETTING_PRESENTS;
|
|
else if (setting_presents) setting_presents--;
|
|
cfg_seen = v, cfg_known = 1;
|
|
vk_dlaa_swap_img = NULL, vk_dlaa_swap_view = VK_NULL_HANDLE;
|
|
D.pre = D.comp = 0, D.comp_game = VK_NULL_HANDLE; // the frame before was recorded up to its present
|
|
int pre_on = cfg_bool("NX2NAVI_DLSS_PREBLOOM"); // live: the evaluation point changes at the next frame
|
|
if (pre_on != D.pre_on) rt_log("dlaa: prebloom %s", pre_on ? "on: the upscaler runs before the bloom where the frame has one"
|
|
: "off: the upscaler runs at the game's upscaler");
|
|
D.pre_on = pre_on;
|
|
dlss_mode();
|
|
preset_check();
|
|
if (!N.ngx_up) { // FSR 3.1 follows the setting
|
|
fsr_frame = fsr3();
|
|
N.available = fsr_frame && cfg_bool("NX2NAVI_DLAA") && vk_ffx_load();
|
|
}
|
|
int on = N.available && cfg_bool("NX2NAVI_DLAA");
|
|
// Frame generation without an upscaler takes this frame's inputs alone (fg_only_frame); switching in either
|
|
// direction starts anew as switching the upscaler does below, and the frame before gets nothing to frame generation.
|
|
int fgo = !on && !D.failed && fg_runs();
|
|
fg_state_line(on, fgo);
|
|
if (fgo != D.fgo) {
|
|
D.fgo = fgo;
|
|
off_at = D.presents; // idle_release: the upscaler's sizes count as unused from here
|
|
D.reset = 1, D.cam_prev_ok = 0, D.jit_ok = D.jit_used = D.armed = D.done = D.gbuf = D.ended = D.up = 0;
|
|
D.hold = 0;
|
|
D.fg_fresh = D.hl_fresh = 0;
|
|
memset(&F, 0, sizeof F);
|
|
if (on == D.on) return;
|
|
}
|
|
if (on != D.on) {
|
|
rt_log("dlaa: %s", on ? "on" : "off");
|
|
D.on = on;
|
|
off_at = D.presents;
|
|
// either way the next DLAA frame starts a new history from an unjittered frame
|
|
D.reset = 1, D.cam_prev_ok = 0, D.jit_ok = D.jit_used = D.armed = D.done = D.gbuf = D.ended = D.up = 0;
|
|
D.hold = 0;
|
|
D.fg_fresh = D.hl_fresh = 0; // frame generation gets nothing made before the switch (fg_current)
|
|
memset(&F, 0, sizeof F);
|
|
return;
|
|
}
|
|
idle_release();
|
|
if (!inputs_on()) return;
|
|
if (D.share_off) D.share_off--;
|
|
if (!D.done && D.armed) D.share_off = 120; // vk_dlaa_pass: the output pass without the mode's share for a while
|
|
if (!D.done) {
|
|
D.frames_missed++;
|
|
// the first few after DLSS first ran: what the frame drew, to tell a frame drawn another way from a loading screen
|
|
if (D.evals && D.miss_logged < 24) {
|
|
D.miss_logged++;
|
|
char arm[64] = "never armed";
|
|
if (D.armed) snprintf(arm, sizeof arm, "armed on a %ux%u light target", D.color.width, D.color.height);
|
|
rt_log("dlaa: frame %llu without the DLAA point: %s; %u scene-depth draws (%u into another D32 depth); %u "
|
|
"later passes with one colour target and no depth (last %ux%u format %d); %llu submissions since the "
|
|
"last present, %d held submissions waiting for a signal",
|
|
(unsigned long long)__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), arm, F.zdraws, F.zother,
|
|
F.one_n, F.one_w, F.one_h, (int)F.one_fmt, (unsigned long long)(vk_exec_submit_seq - D.seq_present),
|
|
vk_exec_held());
|
|
}
|
|
}
|
|
if (D.done && F.second) {
|
|
D.seconds++;
|
|
if (D.second_logged < 24)
|
|
D.second_logged++, rt_log("dlaa: frame %llu: a second scene after DLSS ran (%u passes into light target %p; "
|
|
"DLSS ran on %p); %llu submissions since the last present, %d held",
|
|
(unsigned long long)__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED),
|
|
F.second, (void *)F.second_img, (void *)D.color.image,
|
|
(unsigned long long)(vk_exec_submit_seq - D.seq_present), vk_exec_held());
|
|
}
|
|
D.seq_present = vk_exec_submit_seq;
|
|
if (D.jit_used && !D.done) D.hold = 60, D.holds++; // jittered but not resolved: that frame shook
|
|
else if (D.hold) D.hold--;
|
|
if (!D.done) D.jit_ok = 0;
|
|
D.armed = D.done = D.jit_used = D.gbuf = D.ended = D.up = D.ids_ok = 0;
|
|
D.cam_addr = 0, D.nviews = 0;
|
|
memset(&F, 0, sizeof F);
|
|
}
|
|
|
|
// ---------------------------------------------------------------- the game's FXAA (NX2NAVI_FXAA, live)
|
|
// NX2navi's FXAA is two full-screen passes after the bloom, just before the HUD: one copies the scene target into a
|
|
// scratch target, the other draws the anti-aliased result back over the scene (found by diffing frame dumps with the
|
|
// game's own switch, ModelSceneExtensionParam EnableFXAA, on and off). With fxaa = 0 the draws of that second pass's
|
|
// fragment shader are skipped, so the scene reaches the HUD as it was. Shaders are known by the FNV-1a hash of their
|
|
// SPIR-V, as vk_dump.c names them; a translator change gives new hashes and the switch then does nothing.
|
|
// With a scene smaller than the output (the game's own 5/6 render scale, or a DLSS mode) the same shader draws the
|
|
// scaled picture into the output-sized target, in a pass without the depth attachment the in-place FXAA has: there it
|
|
// is always drawn, whatever fxaa says (skipping it would leave the 3D picture out), unless DLSS already wrote that
|
|
// target this frame.
|
|
// The stock translator's hash, the one with tools/xlate patch 0010 (PCSS, PR #89) applied, the one with patches 0010
|
|
// and 0011 (the shadow filter spread) applied, and that one with the zero_locals job flag (Mesa, vk_caps.c).
|
|
// A scaling draw this list misses still scales DLSS's picture, not
|
|
// the jittered scene: after DLSS ran, passes larger than the light target sample DLSS's output (vk_dlaa_swap_img).
|
|
static const uint64_t FXAA_FS[] = {0xf450003ebeb3dc52ull, 0x0a12f4bc2a9a4fbeull, 0x5c43ae18948d52cbull,
|
|
0xaad42238801d31d8ull};
|
|
|
|
static uint64_t spirv_hash(const GfxShader *s) {
|
|
static struct { const GfxShader *s; uint64_t h; } tab[8192];
|
|
uintptr_t k = ((uintptr_t)s >> 4) * 0x9E3779B97F4A7C15ull;
|
|
for (uint32_t i = 0, j = (uint32_t)(k >> 51); i < 8192; i++, j = (j + 1) & 8191) {
|
|
if (tab[j].s == s) return tab[j].h;
|
|
if (!tab[j].s) {
|
|
uint64_t h = gfx_shader_spirv_fnv(s); // kept by the shader: a compressed one is not decoded on this thread
|
|
tab[j].s = s, tab[j].h = h;
|
|
return h;
|
|
}
|
|
}
|
|
return 0; // table full: no skip
|
|
}
|
|
|
|
int vk_dlaa_skip_draw(VkExec *ex, const GfxShader *fs) {
|
|
static uint64_t frame_read = ~0ull;
|
|
static int fxaa = 1;
|
|
// fxaa = 0 reports once whether it found the game's FXAA draw: the first skip, or a minute of frames (3600) with
|
|
// none, which is what a translator change that moves FXAA_FS looks like on every GPU, with or without DLSS
|
|
static uint64_t fx_off_frames;
|
|
static int fx_found, fx_reported;
|
|
uint64_t f = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
|
|
if (f != frame_read) { // once a frame: the menu switches it live
|
|
frame_read = f, fxaa = cfg_bool("NX2NAVI_FXAA");
|
|
if (!fxaa && !fx_found && !fx_reported && ++fx_off_frames == 3600) {
|
|
fx_reported = 1;
|
|
rt_log("dlaa: fxaa = 0 found no draw of the game's FXAA in %llu frames (its fragment shader is not in "
|
|
"FXAA_FS): the FXAA stays on", (unsigned long long)fx_off_frames);
|
|
}
|
|
}
|
|
if (fxaa && !D.up) return 0; // nothing to skip: no shader lookup per draw at the defaults
|
|
if (!fs) return 0;
|
|
uint64_t h = spirv_hash(fs);
|
|
int known = 0;
|
|
for (size_t i = 0; i < sizeof FXAA_FS / sizeof FXAA_FS[0]; i++) known |= h == FXAA_FS[i];
|
|
if (!known) return 0;
|
|
if (!ex->depth.image) { // the scaling pass, which DLSS replaced
|
|
static int told;
|
|
if (D.up && !told++) rt_log("dlaa: the game's scaling draw left out, DLSS wrote it (fragment shader %016llx)",
|
|
(unsigned long long)h);
|
|
return D.up;
|
|
}
|
|
if (!fxaa && !fx_found) {
|
|
fx_found = 1;
|
|
rt_log("dlaa: fxaa = 0 skips the game's FXAA (fragment shader %016llx)", (unsigned long long)h);
|
|
}
|
|
return !fxaa;
|
|
}
|
|
|
|
|
|
// ---------------------------------------------------------------- DLSS Frame Generation through NGX (vk_ngxfg)
|
|
// gfx_present.c records this into the present command buffer once the frame's picture, HUD included, is in bb. It
|
|
// generates count frames between the previous call's picture and this one into out[0..count-1] (MultiFrameIndex 1 to
|
|
// count) and a copy of bb into real; gfx_present.c's presenter shows them in that order, then the rendered one. The
|
|
// inputs are the ones the Streamline path gets (vk_dlssg.c): the depth, motion vectors, jitter and camera of the frame
|
|
// DLSS last ran on, and the picture without the HUD. NGX comes up only with DLSS upscaling on at the start
|
|
// (vk_ngxfg_wanted); switched off in the menu after that, the frames armed for frame generation alone give the same
|
|
// (fg_only_frame, no jitter).
|
|
int vk_ngxfg_max(void) { return vk_ngxfg_wanted() && N.available && !FG.failed ? FG.max : 0; }
|
|
int vk_ngxfg_on(void) { return vk_ngxfg_max() && !strcmp(cfg_value("NX2NAVI_FRAMEGEN"), "dlss"); }
|
|
void vk_ngxfg_reset(void) { FG.reset = 1; }
|
|
|
|
int vk_ngxfg_evaluate(VkCommandBuffer cb, const VkNgxImage *bb, const VkNgxImage *hudless, const VkNgxImage *out, int count,
|
|
const VkNgxImage *real) {
|
|
if (!vk_ngxfg_max() || count < 1) return 0;
|
|
if (count > FG.max) count = FG.max;
|
|
VkFfxImage d, m;
|
|
VkImageView dv, mvv;
|
|
VkDlaaCamera c;
|
|
float jx = 0, jy = 0;
|
|
int reset = 0;
|
|
if (!vk_dlaa_fg_inputs(&d, &m, &jx, &jy, &reset) || !vk_dlaa_fg_views(&dv, &mvv) || !vk_dlaa_fg_camera(&c)) {
|
|
FG.reset = 1; // a frame without its inputs: the next generation has no valid previous frame
|
|
return 0;
|
|
}
|
|
if (!FG.p && !NGX_OK(N.alloc_params(&FG.p))) {
|
|
FG.failed = 1;
|
|
rt_log("ngxfg: parameter allocation failed: frame generation off");
|
|
return 0;
|
|
}
|
|
NgxParam *p = FG.p;
|
|
if (FG.feat && (FG.w != bb->w || FG.h != bb->h || FG.fmt != bb->fmt)) {
|
|
N.release(FG.feat);
|
|
FG.feat = NULL;
|
|
}
|
|
if (!FG.feat) {
|
|
p_setu(p, "CreationNodeMask", 1);
|
|
p_setu(p, "VisibilityNodeMask", 1);
|
|
p_setu(p, "Width", bb->w);
|
|
p_setu(p, "Height", bb->h);
|
|
p_setu(p, "DLSSG.BackbufferFormat", (unsigned)bb->fmt);
|
|
uint64_t t0 = perf_mark_begin();
|
|
NgxResult r = N.create(cb, NGX_FEATURE_FRAME_GENERATION, p, &FG.feat);
|
|
perf_mark_end(PERF_M_NGX, t0, 0);
|
|
if (!NGX_OK(r) || !FG.feat) {
|
|
FG.failed = 1, FG.feat = NULL;
|
|
rt_log("ngxfg: DLSS Frame Generation feature creation failed (%08x): frame generation off", (unsigned)r);
|
|
return 0;
|
|
}
|
|
FG.w = bb->w, FG.h = bb->h, FG.fmt = bb->fmt, FG.reset = 1;
|
|
rt_log("ngxfg: DLSS Frame Generation created at %ux%u (format %d), depth and motion %ux%u", bb->w, bb->h,
|
|
(int)bb->fmt, d.width, d.height);
|
|
}
|
|
NgxResourceVK rb = res_image(bb->view, bb->img, bb->fmt, bb->w, bb->h, VK_IMAGE_ASPECT_COLOR_BIT, 0);
|
|
NgxResourceVK rz = res_image(dv, d.image, d.format, d.width, d.height, VK_IMAGE_ASPECT_DEPTH_BIT, 0);
|
|
NgxResourceVK rm = res_image(mvv, m.image, m.format, m.width, m.height, VK_IMAGE_ASPECT_COLOR_BIT, 0);
|
|
NgxResourceVK rh = hudless ? res_image(hudless->view, hudless->img, hudless->fmt, hudless->w, hudless->h,
|
|
VK_IMAGE_ASPECT_COLOR_BIT, 0) : rb;
|
|
NgxResourceVK rr = res_image(real->view, real->img, real->fmt, real->w, real->h, VK_IMAGE_ASPECT_COLOR_BIT, 1);
|
|
p_setp(p, "DLSSG.Backbuffer", &rb);
|
|
p_setp(p, "DLSSG.Depth", &rz);
|
|
p_setp(p, "DLSSG.MVecs", &rm);
|
|
p_setp(p, "DLSSG.HUDLess", hudless ? &rh : NULL);
|
|
p_setp(p, "DLSSG.UI", NULL);
|
|
p_setp(p, "DLSSG.UIAlpha", NULL);
|
|
p_setp(p, "DLSSG.BidirectionalDistortionField", NULL);
|
|
p_setp(p, "DLSSG.OutputReal", &rr);
|
|
p_setp(p, "DLSSG.OutputDisableInterpolation", NULL);
|
|
p_setp(p, "DLSSG.CameraViewToClip", c.view_to_clip);
|
|
p_setp(p, "DLSSG.ClipToCameraView", c.clip_to_view);
|
|
p_setp(p, "DLSSG.ClipToLensClip", NULL);
|
|
p_setp(p, "DLSSG.ClipToPrevClip", c.clip_to_prev_clip);
|
|
p_setp(p, "DLSSG.PrevClipToClip", c.prev_clip_to_clip);
|
|
p_setf(p, "DLSSG.JitterOffsetX", jx);
|
|
p_setf(p, "DLSSG.JitterOffsetY", jy);
|
|
p_setf(p, "DLSSG.MvecScaleX", 1.0f / (float)m.width); // the vectors are in render pixels
|
|
p_setf(p, "DLSSG.MvecScaleY", 1.0f / (float)m.height);
|
|
p_setf(p, "DLSSG.CameraPinholeOffsetX", 0.0f);
|
|
p_setf(p, "DLSSG.CameraPinholeOffsetY", 0.0f);
|
|
static const char *const pos[] = {"DLSSG.CameraPosX", "DLSSG.CameraPosY", "DLSSG.CameraPosZ"};
|
|
static const char *const up[] = {"DLSSG.CameraUpX", "DLSSG.CameraUpY", "DLSSG.CameraUpZ"};
|
|
static const char *const rt[] = {"DLSSG.CameraRightX", "DLSSG.CameraRightY", "DLSSG.CameraRightZ"};
|
|
static const char *const fw[] = {"DLSSG.CameraFwdX", "DLSSG.CameraFwdY", "DLSSG.CameraFwdZ"};
|
|
for (int i = 0; i < 3; i++)
|
|
p_setf(p, pos[i], c.pos[i]), p_setf(p, up[i], c.up[i]), p_setf(p, rt[i], c.right[i]), p_setf(p, fw[i], c.fwd[i]);
|
|
p_setf(p, "DLSSG.CameraNear", c.znear); // the frame's own planes (proj_planes), as vk_dlssg.c gives them
|
|
p_setf(p, "DLSSG.CameraFar", c.zfar);
|
|
p_setf(p, "DLSSG.CameraFOV", c.fov_y);
|
|
p_setf(p, "DLSSG.CameraAspectRatio", c.aspect);
|
|
p_setu(p, "DLSSG.ColorBuffersHDR", 0);
|
|
p_setu(p, "DLSSG.DepthInverted", (unsigned)c.inverted);
|
|
p_setu(p, "DLSSG.CameraMotionIncluded", 1);
|
|
p_setu(p, "DLSSG.Reset", (unsigned)(reset || FG.reset));
|
|
p_setu(p, "DLSSG.AutomodeOverrideReset", 0);
|
|
p_setu(p, "DLSSG.NotRenderingGameFrames", 0);
|
|
p_setu(p, "DLSSG.OrthoProjection", 0);
|
|
p_setu(p, "DLSSG.MvecDilated", 0);
|
|
p_setu(p, "DLSSG.MenuDetectionEnabled", 0);
|
|
struct { const char *base; uint32_t w, h; } rects[] = {
|
|
{"DLSSG.MVecsSubrect", m.width, m.height}, {"DLSSG.DepthSubrect", d.width, d.height},
|
|
{"DLSSG.HUDLessSubrect", bb->w, bb->h}, {"DLSSG.InputBackbufferSubrect", bb->w, bb->h},
|
|
{"DLSSG.OutputInterpolatedSubrect", bb->w, bb->h}, {"DLSSG.OutputRealSubrect", bb->w, bb->h}};
|
|
for (size_t i = 0; i < sizeof rects / sizeof *rects; i++) {
|
|
char k[64];
|
|
snprintf(k, sizeof k, "%sBaseX", rects[i].base), p_setu(p, k, 0);
|
|
snprintf(k, sizeof k, "%sBaseY", rects[i].base), p_setu(p, k, 0);
|
|
snprintf(k, sizeof k, "%sWidth", rects[i].base), p_setu(p, k, rects[i].w);
|
|
snprintf(k, sizeof k, "%sHeight", rects[i].base), p_setu(p, k, rects[i].h);
|
|
}
|
|
p_setu(p, "DLSSG.MultiFrameCount", (unsigned)count);
|
|
int made = 0;
|
|
for (int i = 0; i < count; i++) {
|
|
NgxResourceVK ro = res_image(out[i].view, out[i].img, out[i].fmt, out[i].w, out[i].h, VK_IMAGE_ASPECT_COLOR_BIT, 1);
|
|
p_setp(p, "DLSSG.OutputInterpolated", &ro);
|
|
p_setu(p, "DLSSG.MultiFrameIndex", (unsigned)(i + 1));
|
|
NgxResult r = N.evaluate(cb, FG.feat, p, NULL);
|
|
if (!NGX_OK(r)) {
|
|
static int told;
|
|
if (told++ < 5) rt_log("ngxfg: evaluation %d of %d failed (%08x)", i + 1, count, (unsigned)r);
|
|
break;
|
|
}
|
|
made++;
|
|
}
|
|
full_barrier(cb);
|
|
FG.reset = 0;
|
|
if (made && (++FG.evals == 1 || FG.evals % 3600 == 0))
|
|
rt_log("ngxfg: %d frames with generation (%d generated per rendered frame)", FG.evals, made);
|
|
return made;
|
|
}
|