mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
With DLSS-G, Reflex's sleep held each frame's start with no limit while the game's pacer still held the limit on the present thread and at the window image's acquire (A-16, P-16): the pacer's wait came after the picture was drawn, so the pad was read up to a period before the picture left, on top of Reflex's own wait for the GPU. vk_dlssg.c vk_dlssg_pace, on the presenting guest thread in nvnQueuePresentTexture, asks dlssg_pace.h which limit Reflex should hold and stores it; vk_dlssg_frame on the present thread hands a changed limit to Streamline before its present (reflex_apply, under the present lock, which keeps Streamline's options calls on one thread at a time), as does each new swapchain, which has neither mode nor limit. GfxBackend.frame_paced now returns the period the pacer still holds: vk_backend.c vk_be_frame_paced gives the game's ceiling (frame_step_ceiling_ns, 240) while Reflex holds the limit, 0 where a driver's low latency holds it (vk_lowlat_pace, unchanged), else the period; during a movie the back ends get 0 and the pacer keeps the movie's period and catch-up slots as before. The pacer takes the limit back before the first host present, after 200 ms without one, with a DLL older than nx2sl_reflex2 (one log line at start), for a limit Streamline refused, and for the run when the frames show Reflex does not hold it; the "frame rate:" lines name who holds it, the "dlssg: present N" line counts the frame starts Reflex held, and Reflex's sleep now counts as pacing (perf_end_any, on whichever thread acquires). No wait is added on a drawing thread: the limiter's wait moves from the acquire's sleep_until and the present thread's slot to Reflex's sleep a few lines later in the same acquire, and the present thread's options call returns at once. docs/architecture.md names Reflex as a limiter. Not reproduced before the change (no RTX 40 or 50 here); vk_dlssg.c, vk_backend.c and nvn_api_obj.c compile clean with the dev build's flags, and dlssg_pace_check, fg_reset_check and frame_step_check pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
449 lines
24 KiB
C
449 lines
24 KiB
C
// DLSS Frame Generation (NX2NAVI_FRAMEGEN = dlss; applies after a restart) through nx2navi_dlssg.dll
|
|
// (runtime/dlssg/nx2_sl.cpp), which drives NVIDIA Streamline. Like AMD's frame generation (vk_fg.cpp) it takes the
|
|
// depth, motion vectors and camera vk_dlaa.c makes for DLSS or FSR 3.1 upscaling, or without an upscaler for frame
|
|
// generation alone (vk_dlaa.c fg_only_frame: the same pass at the scene's size, no jitter); it needs an RTX 40 or 50 GPU.
|
|
//
|
|
// The DLL and Streamline (sl.interposer.dll and its plugins, nvngx_dlssg.dll) sit next to the game. The instance and
|
|
// device are created through Streamline's proxies (vk_device.c), so it can add the extensions, features and queues
|
|
// DLSS-G needs; the present path calls Streamline's swapchain functions (gfx_present.c), and every present records the
|
|
// frame's constants and tags first (vk_dlssg_frame). Generation pauses while the menu is open.
|
|
//
|
|
// Multi frame generation: NX2NAVI_FRAMEGEN_MULTIPLIER (2, 3 or 4, live) is how many frames the display gets per
|
|
// rendered frame, so one, two or three generated ones. Streamline says how many this GPU and driver can generate
|
|
// (numFramesToGenerateMax: one where only 2x runs, three on an RTX 50); a higher setting is clamped to it with a log
|
|
// line. The frame limit counts the frames the game draws and the generated ones come on top (frame_rate.c
|
|
// limit_game_hz): 60 at 4x is 60 drawn and 240 shown. Only a limit of display is divided by the multiplier in effect
|
|
// (vk_dlssg_multiplier), since a frame past the monitor's refresh is never seen.
|
|
//
|
|
// DLSS-G needs Reflex, whose low-latency mode paces the game (NX2NAVI_REFLEX, default 1; 0 off, 2 on with boost): the
|
|
// game's frame sleeps in Reflex where it starts (its acquire of a window image), and each frame carries the latency
|
|
// markers from that start to the host present, all with the frame's number, the count of the game's presents up to it.
|
|
// The mode is the swapchain's, so it is set again after each one (vk_dlssg_create_swapchain).
|
|
//
|
|
// One limiter. Reflex's sleep also holds the frame limit (its frame limit, the driver's minimum interval), and the
|
|
// game's own pacer then keeps only the game's ceiling (vk_dlssg_pace, nvn_api_obj.c nvnQueuePresentTexture): the pacer's
|
|
// wait came after the picture was drawn, so the pad was read up to a period before the picture left, and Reflex's sleep
|
|
// at the frame's start already waited for the GPU. The pacer keeps the limit while a movie plays, before the first host
|
|
// present, while none went out for DLSSG_HOLD_IDLE_NS, with a DLL older than nx2sl_reflex2 or a limit Streamline
|
|
// refused, and for the rest of the run once the frames show Reflex does not hold it (dlssg_pace.h). The guest thread
|
|
// that presents decides and the present thread hands the limit to Streamline before its present (vk_dlssg_frame, under
|
|
// the present lock, which keeps Streamline's options calls on one thread at a time); the pacer stands aside only once
|
|
// Streamline took that limit.
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cfg.h"
|
|
#include "dlssg_pace.h"
|
|
#include "host.h"
|
|
#include "vk.h"
|
|
#include "vk_dlaa.h"
|
|
#include "vk_dlssg.h"
|
|
#include "dlssg/nx2_sl.h"
|
|
#include "ui/menu.h"
|
|
|
|
static struct {
|
|
int wanted, loaded, active;
|
|
int fail; // DLSSG_NO_FILES or DLSSG_UNAVAILABLE when it was wanted and does not run (vk_dlssg_status)
|
|
int mode, pending; // generation as the driver has it, and the frames since the host asked for the other state
|
|
int shown_mult; // vk_dlssg_multiplier: the frames per game frame as last handed to the DLL (1 while off)
|
|
PFN_nx2sl_frames_max frames_max; // NULL with an older DLL, which generates one frame
|
|
PFN_nx2sl_init init;
|
|
PFN_nx2sl_instance_proc gipa;
|
|
PFN_nx2sl_device_proc gdpa;
|
|
PFN_nx2sl_available available;
|
|
PFN_nx2sl_log_unavailable log_unavailable;
|
|
PFN_nx2sl_frame frame;
|
|
PFN_nx2sl_present_marker marker;
|
|
PFN_nx2sl_sleep sleep;
|
|
PFN_nx2sl_submit_end submit_end;
|
|
PFN_nx2sl_shown shown;
|
|
PFN_nx2sl_reflex reflex;
|
|
PFN_nx2sl_reflex2 reflex2; // NULL with an older DLL: Reflex holds no frame limit, the pacer keeps it
|
|
uint32_t game; // the game's presents (vk_dlssg_game_present)
|
|
uint32_t cur; // the frame the present thread blits and presents next (vk_dlssg_present_frame)
|
|
uint32_t started; // the last frame vk_dlssg_frame_start started
|
|
uint64_t hosted_ns; // kern_now_ns after the last host present
|
|
// Reflex's frame limit, in microseconds (0 none). want: what vk_dlssg_pace asks for (the presenting guest thread
|
|
// writes it); set: what Streamline last took (written under the present lock, reflex_apply); refused: Streamline
|
|
// refused a limit, so the pacer keeps it for the run. Atomics.
|
|
uint32_t limit_want, limit_set;
|
|
int limit_refused;
|
|
DlssgPace pace; // the presenting guest thread's (vk_dlssg_pace)
|
|
int pace_said; // who held the limit as last logged: 1 Reflex, 0 the pacer, -1 not yet
|
|
// the frame starts Reflex's sleep held, and their time, since the last statistics line (stats); atomics
|
|
uint64_t n_hold, hold_ns, n_unheld;
|
|
PFN_nx2sl_shutdown shutdown;
|
|
PFN_vkCreateSwapchainKHR create_sc;
|
|
PFN_vkDestroySwapchainKHR destroy_sc;
|
|
PFN_vkGetSwapchainImagesKHR images;
|
|
PFN_vkAcquireNextImageKHR acquire;
|
|
PFN_vkQueuePresentKHR present;
|
|
} S = {.wanted = -1, .fail = DLSSG_NO_FILES, .shown_mult = 1, .pace_said = -1};
|
|
|
|
static void sl_line(const char *line) { rt_log("%s", line); }
|
|
|
|
static int reflex_mode(void) {
|
|
static int m = -1;
|
|
if (m < 0) {
|
|
const char *s = cfg_get("NX2NAVI_REFLEX");
|
|
m = s && *s ? atoi(s) : 1;
|
|
if (m < 0 || m > 2) m = 1;
|
|
}
|
|
return m;
|
|
}
|
|
|
|
// Reflex's mode and frame limit to Streamline: under the present lock (vk_dlssg_frame, vk_dlssg_create_swapchain), or
|
|
// once at the device's creation before any present. Streamline's slReflexSetOptions is not thread safe, and every
|
|
// caller is one of these. A limit Streamline refuses leaves the limit to the pacer for the run.
|
|
static void reflex_apply(uint32_t us) {
|
|
if (!S.reflex2) {
|
|
S.reflex(reflex_mode());
|
|
return;
|
|
}
|
|
if (__atomic_load_n(&S.limit_refused, __ATOMIC_ACQUIRE)) us = 0;
|
|
int ok = S.reflex2(reflex_mode(), us);
|
|
if (!ok && us) {
|
|
__atomic_store_n(&S.limit_refused, 1, __ATOMIC_RELEASE);
|
|
rt_log("dlssg: Streamline did not take Reflex's frame limit of %.3f ms: the game's own limiter keeps the limit "
|
|
"for the rest of the run", us / 1000.0);
|
|
ok = S.reflex2(reflex_mode(), 0), us = 0;
|
|
}
|
|
__atomic_store_n(&S.limit_set, ok ? us : 0, __ATOMIC_RELEASE);
|
|
}
|
|
|
|
// Reflex may hold the frame limit: a DLL with nx2sl_reflex2 whose limit Streamline did not refuse.
|
|
static int limit_allowed(void) {
|
|
return S.reflex2 && !__atomic_load_n(&S.limit_refused, __ATOMIC_ACQUIRE);
|
|
}
|
|
|
|
// With any upscaler or none: vk_dlaa.c makes the inputs either way (vk_dlaa_frame says which, at the start and at each
|
|
// change).
|
|
int vk_dlssg_wanted(void) {
|
|
if (S.wanted < 0) S.wanted = !strcmp(cfg_value("NX2NAVI_FRAMEGEN"), "dlss");
|
|
return S.wanted;
|
|
}
|
|
|
|
int vk_dlssg_early(void) {
|
|
if (!vk_dlssg_wanted()) return 0;
|
|
if (S.loaded) return S.loaded > 0;
|
|
S.loaded = -1;
|
|
#ifdef _WIN32
|
|
wchar_t dir[MAX_PATH], path[MAX_PATH + 32];
|
|
DWORD n = GetModuleFileNameW(NULL, dir, MAX_PATH);
|
|
while (n && dir[n - 1] != L'\\') n--;
|
|
dir[n ? n - 1 : 0] = 0;
|
|
swprintf(path, MAX_PATH + 32, L"%ls\\nx2navi_dlssg.dll", dir);
|
|
HMODULE m = LoadLibraryW(path);
|
|
if (!m) {
|
|
rt_log("dlssg: nx2navi_dlssg.dll is not next to the game: DLSS Frame Generation off");
|
|
return 0;
|
|
}
|
|
PFN_nx2sl_version ver = (PFN_nx2sl_version)(void *)GetProcAddress(m, "nx2sl_version");
|
|
S.init = (PFN_nx2sl_init)(void *)GetProcAddress(m, "nx2sl_init");
|
|
S.gipa = (PFN_nx2sl_instance_proc)(void *)GetProcAddress(m, "nx2sl_instance_proc");
|
|
S.gdpa = (PFN_nx2sl_device_proc)(void *)GetProcAddress(m, "nx2sl_device_proc");
|
|
S.available = (PFN_nx2sl_available)(void *)GetProcAddress(m, "nx2sl_available");
|
|
S.log_unavailable = (PFN_nx2sl_log_unavailable)(void *)GetProcAddress(m, "nx2sl_log_unavailable");
|
|
S.frame = (PFN_nx2sl_frame)(void *)GetProcAddress(m, "nx2sl_frame");
|
|
S.marker = (PFN_nx2sl_present_marker)(void *)GetProcAddress(m, "nx2sl_present_marker");
|
|
S.sleep = (PFN_nx2sl_sleep)(void *)GetProcAddress(m, "nx2sl_sleep");
|
|
S.submit_end = (PFN_nx2sl_submit_end)(void *)GetProcAddress(m, "nx2sl_submit_end");
|
|
S.shown = (PFN_nx2sl_shown)(void *)GetProcAddress(m, "nx2sl_shown");
|
|
S.shutdown = (PFN_nx2sl_shutdown)(void *)GetProcAddress(m, "nx2sl_shutdown");
|
|
S.reflex = (PFN_nx2sl_reflex)(void *)GetProcAddress(m, "nx2sl_reflex");
|
|
S.frames_max = (PFN_nx2sl_frames_max)(void *)GetProcAddress(m, "nx2sl_frames_max"); // optional: 2x only without
|
|
if (!S.frames_max) rt_log("dlssg: nx2navi_dlssg.dll is older than multi frame generation: 2x at most");
|
|
S.reflex2 = (PFN_nx2sl_reflex2)(void *)GetProcAddress(m, "nx2sl_reflex2"); // optional: the pacer keeps the limit
|
|
if (!S.reflex2) rt_log("dlssg: nx2navi_dlssg.dll is older than Reflex's frame limit: the game's own limiter keeps it");
|
|
if (!ver || ver() != NX2SL_VERSION || !S.init || !S.gipa || !S.gdpa || !S.available || !S.frame || !S.marker ||
|
|
!S.sleep || !S.submit_end || !S.shown || !S.reflex) {
|
|
rt_log("dlssg: nx2navi_dlssg.dll is from another build: DLSS Frame Generation off");
|
|
return 0;
|
|
}
|
|
if (!S.init(dir, sl_line)) return 0;
|
|
S.loaded = 1;
|
|
return 1;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
VkResult vk_dlssg_create_instance(const VkInstanceCreateInfo *ci, VkInstance *out) {
|
|
PFN_vkCreateInstance f = S.loaded > 0 ? (PFN_vkCreateInstance)S.gipa()(VK_NULL_HANDLE, "vkCreateInstance") : NULL;
|
|
return f ? f(ci, NULL, out) : vkCreateInstance(ci, NULL, out);
|
|
}
|
|
|
|
// An instance function through the interposer while Streamline is loaded, else NULL (the caller takes the loader's).
|
|
// The window surface goes through it: Streamline learns the window a swapchain shows from vkCreateWin32SurfaceKHR, and
|
|
// without that its swapchain hook found "no window corresponding to this application" and generated no frames.
|
|
PFN_vkVoidFunction vk_dlssg_instance_proc(VkInstance inst, const char *name) {
|
|
return S.loaded > 0 ? S.gipa()(inst, name) : NULL;
|
|
}
|
|
|
|
// The interposer's vkCreateDevice finds the device's instance in a map its own vkEnumeratePhysicalDevices fills; a
|
|
// physical device enumerated through the loader alone left it a null instance, so the plugins got an empty instance
|
|
// table and sl.common called a null GetPhysicalDeviceMemoryProperties while it set up. Enumerate through it as well.
|
|
VkResult vk_dlssg_enumerate(VkInstance inst, uint32_t *n, VkPhysicalDevice *pd) {
|
|
PFN_vkEnumeratePhysicalDevices f =
|
|
S.loaded > 0 ? (PFN_vkEnumeratePhysicalDevices)S.gipa()(inst, "vkEnumeratePhysicalDevices") : NULL;
|
|
return f ? f(inst, n, pd) : vkEnumeratePhysicalDevices(inst, n, pd);
|
|
}
|
|
|
|
VkResult vk_dlssg_create_device(VkInstance inst, VkPhysicalDevice pd, const VkDeviceCreateInfo *ci, VkDevice *out) {
|
|
PFN_vkCreateDevice f = S.loaded > 0 ? (PFN_vkCreateDevice)S.gipa()(inst, "vkCreateDevice") : NULL;
|
|
return f ? f(pd, ci, NULL, out) : vkCreateDevice(pd, ci, NULL, out);
|
|
}
|
|
|
|
void vk_dlssg_device_ready(VkPhysicalDevice physical, VkDevice dev) {
|
|
if (S.loaded <= 0) return;
|
|
if (!S.available()) {
|
|
S.fail = DLSSG_UNAVAILABLE;
|
|
if (S.log_unavailable) S.log_unavailable(physical);
|
|
return;
|
|
}
|
|
PFN_vkGetDeviceProcAddr g = S.gdpa();
|
|
S.create_sc = (PFN_vkCreateSwapchainKHR)g(dev, "vkCreateSwapchainKHR");
|
|
S.destroy_sc = (PFN_vkDestroySwapchainKHR)g(dev, "vkDestroySwapchainKHR");
|
|
S.images = (PFN_vkGetSwapchainImagesKHR)g(dev, "vkGetSwapchainImagesKHR");
|
|
S.acquire = (PFN_vkAcquireNextImageKHR)g(dev, "vkAcquireNextImageKHR");
|
|
S.present = (PFN_vkQueuePresentKHR)g(dev, "vkQueuePresentKHR");
|
|
S.active = S.create_sc && S.destroy_sc && S.images && S.acquire && S.present;
|
|
rt_log("dlssg: %s", S.active ? "DLSS Frame Generation on the swapchain" : "the swapchain hooks are missing: off");
|
|
if (S.active) reflex_apply(0); // no swapchain and no present yet: the mode alone
|
|
}
|
|
|
|
int vk_dlssg_active(void) { return S.active; }
|
|
|
|
int vk_dlssg_status(void) {
|
|
if (S.active) return DLSSG_RUNS;
|
|
return vk_dlssg_wanted() ? S.fail : DLSSG_NOT_WANTED;
|
|
}
|
|
|
|
int vk_dlssg_multiplier_of(const char *value, int max, const char **why) {
|
|
const char *w = NULL;
|
|
long n = 2;
|
|
if (value && *value) {
|
|
char *end;
|
|
n = strtol(value, &end, 10);
|
|
if (*end == 'x' || *end == 'X') end++;
|
|
if (*end || end == value || n < 2) w = "is not 2, 3 or 4", n = 2;
|
|
else if (n > 4) w = "is above 4, the most there is", n = 4;
|
|
}
|
|
if (max >= 2 && n > max) w = "is above what this GPU and driver allow", n = max;
|
|
if (why) *why = w;
|
|
return (int)n;
|
|
}
|
|
|
|
// Streamline's limit as a multiplier: 2 to 4, or 0 while it has reported none (the first frames). An older DLL
|
|
// generates one frame: 2.
|
|
static int gpu_multiplier_max(int *old_dll) {
|
|
if (old_dll) *old_dll = !S.frames_max;
|
|
if (!S.frames_max) return 2;
|
|
uint32_t f = S.frames_max();
|
|
return !f ? 0 : f >= 3 ? 4 : (int)f + 1;
|
|
}
|
|
|
|
int vk_dlssg_multiplier_max(int *old_dll) {
|
|
if (old_dll) *old_dll = 0;
|
|
return S.active ? gpu_multiplier_max(old_dll) : 0;
|
|
}
|
|
|
|
int vk_dlssg_multiplier(void) { return S.active ? __atomic_load_n(&S.shown_mult, __ATOMIC_RELAXED) : 1; }
|
|
|
|
// The multiplier the setting gives here (present thread), clamped to the GPU's limit; one log line each time the value
|
|
// or the limit changes and the value is not taken as it is. Until Streamline reports its limit, one generated frame,
|
|
// and no log line about the GPU yet.
|
|
static int multiplier_wanted(void) {
|
|
static char last[24];
|
|
static int last_max = -1, m = 2;
|
|
const char *value = cfg_value("NX2NAVI_FRAMEGEN_MULTIPLIER");
|
|
int max = gpu_multiplier_max(NULL);
|
|
if (max == last_max && !strncmp(value, last, sizeof last - 1)) return m;
|
|
snprintf(last, sizeof last, "%s", value);
|
|
last_max = max;
|
|
const char *why;
|
|
int n = vk_dlssg_multiplier_of(value, max, &why);
|
|
if (why) rt_log("dlssg: framegen_multiplier = %s %s (up to %dx here): %dx", value, why, max ? max : 4, n);
|
|
m = max ? n : 2;
|
|
return m;
|
|
}
|
|
|
|
// vk_device.c: while Streamline is loaded, every device entry point the runtime fetches goes through its interposer, so
|
|
// its plugins see the calls they hook (Reflex's frame timing and DLSS-G's presents). NULL keeps the loader's.
|
|
PFN_vkGetDeviceProcAddr vk_dlssg_dpa(void) { return S.loaded > 0 && S.gdpa ? S.gdpa() : NULL; }
|
|
|
|
// gfx_present.c create_swapchain, under the present and queue locks: the new swapchain has neither the mode nor the
|
|
// limit, so both go to Streamline again, the limit as the presenting guest thread last asked for it.
|
|
VkResult vk_dlssg_create_swapchain(VkDevice d, const VkSwapchainCreateInfoKHR *ci, VkSwapchainKHR *sc) {
|
|
VkResult r = S.create_sc(d, ci, NULL, sc);
|
|
if (r == VK_SUCCESS) reflex_apply(__atomic_load_n(&S.limit_want, __ATOMIC_ACQUIRE));
|
|
return r;
|
|
}
|
|
void vk_dlssg_destroy_swapchain(VkDevice d, VkSwapchainKHR sc) { S.destroy_sc(d, sc, NULL); }
|
|
VkResult vk_dlssg_get_images(VkDevice d, VkSwapchainKHR sc, uint32_t *n, VkImage *img) { return S.images(d, sc, n, img); }
|
|
VkResult vk_dlssg_acquire(VkDevice d, VkSwapchainKHR sc, uint64_t t, VkSemaphore sem, uint32_t *idx) {
|
|
return S.acquire(d, sc, t, sem, VK_NULL_HANDLE, idx);
|
|
}
|
|
|
|
// One line after the first 600 blits of a game frame and every 3600 after that (600 with NX2NAVI_TRACE=perf), on the
|
|
// present thread outside the queue lock (vk_dlssg_frame): who holds the frame limit, and how many frame starts Reflex's
|
|
// sleep held and for how long.
|
|
static void stats(void) {
|
|
static uint64_t shown, next = 600;
|
|
if (++shown < next) return;
|
|
next = shown + (rt_trace & RT_TRACE_PERF ? 600 : 3600);
|
|
uint64_t n = __atomic_exchange_n(&S.n_hold, 0, __ATOMIC_RELAXED);
|
|
uint64_t ns = __atomic_exchange_n(&S.hold_ns, 0, __ATOMIC_RELAXED);
|
|
uint64_t un = __atomic_exchange_n(&S.n_unheld, 0, __ATOMIC_RELAXED);
|
|
uint32_t us = __atomic_load_n(&S.limit_set, __ATOMIC_ACQUIRE);
|
|
char limit[96];
|
|
if (us) snprintf(limit, sizeof limit, "Reflex holds frame starts to %.3f ms", us / 1000.0);
|
|
else snprintf(limit, sizeof limit, "the game's own limiter holds the frame limit");
|
|
rt_log("dlssg: present %llu: %s; %llu frame starts held by Reflex's sleep, %.2f ms on average; %llu not held",
|
|
(unsigned long long)shown, limit, (unsigned long long)n, n ? (double)ns / (double)n / 1e6 : 0.0,
|
|
(unsigned long long)un);
|
|
}
|
|
|
|
VkResult vk_dlssg_present(VkQueue q, const VkPresentInfoKHR *pi) {
|
|
S.marker(0, S.cur);
|
|
VkResult r = S.present(q, pi);
|
|
S.marker(1, S.cur);
|
|
__atomic_store_n(&S.hosted_ns, kern_now_ns(), __ATOMIC_RELEASE);
|
|
return r;
|
|
}
|
|
|
|
// Turning generation on or off is an slDLSSGSetOptions call and Streamline's own rebuild, and it can be seen: around a
|
|
// menu, a fade or a swapchain change the depth and motion-vector views come and go for a frame or two. Following them
|
|
// frame by frame switched generation off and on dozens of times a second (a run logged 32 changes, in pairs, a frame
|
|
// apart), so a change waits until the new state has held for FG_SETTLE_FRAMES frames.
|
|
#define FG_SETTLE_FRAMES 12
|
|
|
|
// Reflex holds the frame's start until the time the driver computes from the measured render and queue times, and no
|
|
// sooner than its frame limit after the frame before (vk_dlssg_pace), so the game's CPU work ends about when the GPU is
|
|
// free for it; then the frame's first markers. Once a frame: the game may acquire again before it presents. No hold
|
|
// while no host present went out for DLSSG_HOLD_IDLE_NS (the window minimized: gfx_present.c skips the host presents
|
|
// while the game runs on): the driver then has no present to time the frame by, and the sleep would wait out
|
|
// Streamline's half-second timeout each frame. The guest thread gave its core up for the acquire (NVN_BLOCKING). The
|
|
// sleep counts as the pacer's wait (PERF_PACE), whose place it takes while Reflex holds the limit, on whichever guest
|
|
// thread acquires (perf_end_any), as the acquire's own waits do.
|
|
void vk_dlssg_frame_start(void) {
|
|
if (!S.active) return;
|
|
uint32_t f = __atomic_load_n(&S.game, __ATOMIC_RELAXED) + 1;
|
|
if (f == S.started) return;
|
|
S.started = f;
|
|
uint64_t hosted = __atomic_load_n(&S.hosted_ns, __ATOMIC_ACQUIRE), t0 = kern_now_ns(); // in this order: t0 >= hosted
|
|
int hold = hosted && t0 - hosted < DLSSG_HOLD_IDLE_NS;
|
|
uint64_t p0 = hold ? perf_begin() : 0;
|
|
S.sleep(f, hold);
|
|
if (!hold) {
|
|
__atomic_add_fetch(&S.n_unheld, 1, __ATOMIC_RELAXED);
|
|
return;
|
|
}
|
|
perf_end_any(PERF_PACE, p0);
|
|
__atomic_add_fetch(&S.hold_ns, kern_now_ns() - t0, __ATOMIC_RELAXED);
|
|
__atomic_add_fetch(&S.n_hold, 1, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
// Who holds the frame limit, as a log line when it changes (the first 40). why: the reason the pacer holds it.
|
|
static void pace_say(int reflex, uint32_t us, const char *why) {
|
|
static int lines;
|
|
if (S.pace_said == reflex || lines >= 40) return;
|
|
S.pace_said = reflex, lines++;
|
|
if (reflex)
|
|
rt_log("frame rate: Reflex holds the frame limit where each frame starts (%.3f ms%s); the game's own limiter "
|
|
"keeps only its ceiling", us / 1000.0, S.pace.shown ? ", the frames on screen" : "");
|
|
else rt_log("frame rate: the game's own limiter (DLSS Frame Generation: %s)", why);
|
|
}
|
|
|
|
int vk_dlssg_pace(uint64_t period_ns) {
|
|
if (!S.active) return 0;
|
|
uint64_t hosted = __atomic_load_n(&S.hosted_ns, __ATOMIC_ACQUIRE), now = kern_now_ns();
|
|
int allowed = limit_allowed(), mult = vk_dlssg_multiplier(), found;
|
|
uint32_t applied = __atomic_load_n(&S.limit_set, __ATOMIC_ACQUIRE), want;
|
|
int held = dlssg_pace_step(&S.pace, now, period_ns, mult, hosted, allowed, applied, &want, &found);
|
|
__atomic_store_n(&S.limit_want, want, __ATOMIC_RELEASE); // the present thread hands it over (vk_dlssg_frame)
|
|
double ms = (double)period_ns / 1e6;
|
|
if (found == DLSSG_PACE_FAST)
|
|
rt_log("dlssg: Reflex does not hold the frame limit (over half of %d frames came sooner than three quarters of "
|
|
"%.2f ms): the game's own limiter holds it for the rest of the run", DLSSG_PACE_WINDOW, ms);
|
|
else if (found == DLSSG_PACE_SHOWN)
|
|
rt_log("dlssg: Reflex held the frames on screen to the limit, not the frames the game draws (over half of %d "
|
|
"came about %d times %.2f ms apart): it is given %.2f ms over the multiplier from now on",
|
|
DLSSG_PACE_WINDOW, mult, ms, ms);
|
|
else if (found == DLSSG_PACE_SLOW)
|
|
rt_log("dlssg: the frames still came about %d times %.2f ms apart: the game's own limiter holds the limit for the "
|
|
"rest of the run", mult, ms);
|
|
if (held) pace_say(1, want, NULL);
|
|
else if (!want) // (a limit Streamline has yet to take is no change: the present thread hands it over next)
|
|
pace_say(0, 0, !allowed ? (!S.reflex2 ? "nx2navi_dlssg.dll is older than Reflex's frame limit"
|
|
: "Streamline refused the limit")
|
|
: S.pace.bad ? "Reflex did not hold the limit"
|
|
: !period_ns ? "a movie plays, or no limit"
|
|
: hosted ? "no host present for 200 ms"
|
|
: "no host present yet");
|
|
return held;
|
|
}
|
|
|
|
void vk_dlssg_game_present(void) { __atomic_add_fetch(&S.game, 1, __ATOMIC_RELAXED); }
|
|
|
|
void vk_dlssg_submitted(uint64_t frame) {
|
|
if (S.active) S.submit_end((uint32_t)frame);
|
|
}
|
|
|
|
void vk_dlssg_present_frame(uint64_t frame) { S.cur = (uint32_t)frame; }
|
|
|
|
uint64_t vk_dlssg_shown(void) { return S.active ? S.shown() : 0; }
|
|
|
|
void vk_dlssg_frame(VkCommandBuffer cb, VkExtent2D ext) {
|
|
if (!S.active) return;
|
|
VkFfxImage d, m;
|
|
VkImageView dv, mvv;
|
|
VkDlaaCamera c;
|
|
float jx = 0, jy = 0;
|
|
int reset = 0;
|
|
int have = vk_dlaa_fg_inputs(&d, &m, &jx, &jy, &reset) && vk_dlaa_fg_views(&dv, &mvv) && vk_dlaa_fg_camera(&c);
|
|
int want = have && !menu_is_open();
|
|
if (want == S.mode) S.pending = 0;
|
|
else if (++S.pending >= FG_SETTLE_FRAMES) { S.mode = want; S.pending = 0; }
|
|
else want = S.mode; // hold the state it has until the new one stays
|
|
int on = want, mult = multiplier_wanted();
|
|
__atomic_store_n(&S.shown_mult, on ? mult : 1, __ATOMIC_RELAXED); // the pacer's divisor (frame_rate.c)
|
|
// Reflex's frame limit as the presenting guest thread last asked for it (vk_dlssg_pace), before this present
|
|
uint32_t limit = __atomic_load_n(&S.limit_want, __ATOMIC_ACQUIRE);
|
|
if (S.reflex2 && limit != __atomic_load_n(&S.limit_set, __ATOMIC_ACQUIRE)) reflex_apply(limit);
|
|
stats();
|
|
Nx2SlImage zd = {d.image, dv, d.format, d.width, d.height, VK_IMAGE_LAYOUT_GENERAL,
|
|
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT};
|
|
Nx2SlImage zm = {m.image, mvv, m.format, m.width, m.height, VK_IMAGE_LAYOUT_GENERAL,
|
|
VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT};
|
|
Nx2SlCamera k;
|
|
memset(&k, 0, sizeof k);
|
|
if (have) {
|
|
memcpy(k.view_to_clip, c.view_to_clip, 64), memcpy(k.clip_to_view, c.clip_to_view, 64);
|
|
memcpy(k.clip_to_prev_clip, c.clip_to_prev_clip, 64), memcpy(k.prev_clip_to_clip, c.prev_clip_to_clip, 64);
|
|
memcpy(k.pos, c.pos, 12), memcpy(k.up, c.up, 12), memcpy(k.right, c.right, 12), memcpy(k.fwd, c.fwd, 12);
|
|
// the frame's own near and far planes (vk_dlaa.c proj_planes); nx2navi_dlssg.dll tells Streamline the depth is
|
|
// not inverted, which holds for every projection the game has shown (0 near, 1 far)
|
|
k.znear = c.znear, k.zfar = c.zfar, k.fov_y = c.fov_y, k.aspect = c.aspect;
|
|
static int told;
|
|
if (c.inverted && !told++) rt_log("dlssg: the scene's depth is inverted, which Streamline is not told");
|
|
k.jitter[0] = jx, k.jitter[1] = jy;
|
|
// render pixels: the scene's size, which is the output's or smaller (an upscaling mode, or the game's own scale
|
|
// without an upscaler); Streamline takes the depth and the vectors at their own size and the output at ext
|
|
k.mv_scale[0] = 1.0f / (float)d.width, k.mv_scale[1] = 1.0f / (float)d.height;
|
|
k.reset = reset;
|
|
}
|
|
// the frames to generate between two rendered ones: the multiplier less the rendered one, 0 off
|
|
S.frame(cb, have ? &zd : NULL, have ? &zm : NULL, NULL, have ? &k : NULL, ext.width, ext.height, on ? mult - 1 : 0, S.cur);
|
|
}
|
|
|
|
void vk_dlssg_release(void) {
|
|
if (S.loaded > 0 && S.shutdown) S.shutdown();
|
|
S.active = 0;
|
|
__atomic_store_n(&S.limit_set, 0, __ATOMIC_RELEASE); // a device made again starts without a limit
|
|
}
|