mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Let Reflex hold the frame limit with DLSS Frame Generation, and the game's pacer keep only its ceiling while it does.
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>
This commit is contained in:
@@ -255,20 +255,20 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
|
||||
free again one period after its slot, and its acquire also waits for the GPU to finish the image's previous frame
|
||||
(`NX2NAVI_ACQUIRE_WAIT`); `frame_step_present` gives `ocarina_step` the slot times, snapped to the paced rate within
|
||||
3%.
|
||||
- One limiter is the goal: the driver's low latency (`vk_lowlat.c`), RivaTuner Statistics Server (`rtss_limit.c`) or
|
||||
the game's pacer holds the frame limit, and the log's "frame rate:" and "lowlat:" lines name the one in charge.
|
||||
The pacer is the fallback wherever the other limiter's hold is not proven: before the first host present, while a
|
||||
movie plays (the pacer keeps the movie's slots, which no other limiter knows), once no picture went out for 200 ms (a
|
||||
minimised window, `frame_hold`), for the rest of the run once the frames show the other limiter letting them through
|
||||
faster than the limit or once it was not seen holding them for 30 s, and after any failure. While RTSS holds the
|
||||
limit the pacer keeps only the 240-frame ceiling
|
||||
the game's code needs (`rtss_decide.h` `rtss_pace_period`). Frame generation keeps RTSS out (it would count the
|
||||
generated frames), and the game's pacer holds the drawn frames beside it. The exceptions as they stand: `pace_smooth`
|
||||
still waits on the present thread while RTSS holds the limit; DLSS Frame Generation's Reflex sleeps where the frame
|
||||
starts while the pacer holds the limit; at a play limit equal to a movie's rate (60, the default, for the game's 60
|
||||
fps movies) RTSS's profile keeps that limit and RTSS holds the movie beside the pacer; once RTSS hooks the game with
|
||||
the limit in its profile it holds the frames beside the pacer until it is seen holding them, for 30 s at most, after
|
||||
which the profile goes back for the run; and a limit set in RTSS itself
|
||||
- One limiter is the goal: the driver's low latency (`vk_lowlat.c`), Reflex with DLSS Frame Generation (`vk_dlssg.c`),
|
||||
RivaTuner Statistics Server (`rtss_limit.c`) or the game's pacer holds the frame limit, and the log's "frame rate:"
|
||||
and "lowlat:" lines name the one in charge. The pacer is the fallback wherever the other limiter's hold is not
|
||||
proven: before the first host present, while a movie plays (the pacer keeps the movie's slots, which no other
|
||||
limiter knows), once no picture went out for 200 ms (a minimised window, `frame_hold`), for the rest of the run once
|
||||
the frames show the other limiter letting them through faster than the limit (for RTSS also once it was not seen
|
||||
holding them for 30 s), and after any failure. While RTSS or Reflex holds the limit the pacer keeps only the
|
||||
240-frame ceiling the game's code needs (`rtss_decide.h` `rtss_pace_period`, `frame_rate.c` `frame_step_ceiling_ns`).
|
||||
Frame generation keeps RTSS out (it would count the generated frames); with AMD's frame generation the game's pacer
|
||||
holds the drawn frames beside it. The exceptions as they stand: `pace_smooth` still waits on the present thread while
|
||||
RTSS holds the limit; at a play limit equal to a movie's rate (60, the default, for the game's 60 fps movies) RTSS's
|
||||
profile keeps that limit and RTSS holds the movie beside the pacer; once RTSS hooks the game with the limit in its
|
||||
profile it holds the frames beside the pacer until it is seen holding them, for 30 s at most, after which the
|
||||
profile goes back for the run; and a limit set in RTSS itself
|
||||
still holds the game where the game keeps RTSS out, which the log ("two limiters") and the
|
||||
frame rate row of the menu point out, and the same where RTSS is out of it for the run because its profile cannot be
|
||||
written, its hooks library does not load or it never showed its hold, which the log points out the same way (the
|
||||
@@ -327,7 +327,14 @@ keeps per-core data without locks and relies on that, so the host models it (`ru
|
||||
limit and the pacer holds nothing (`nvn_api_obj.c` `nvnQueuePresentTexture`, `vk_lowlat_pace`); the pacer takes the
|
||||
limit back while a movie plays, while no present goes out, and for the rest of the run when the frames show that the
|
||||
driver does not hold it. DLSS Frame Generation brings its own low latency (`vk_dlssg.c`), and the direct path is off
|
||||
in such a run; NVIDIA's is also off while another frame generation presents the frames.
|
||||
in such a run; NVIDIA's is also off while another frame generation presents the frames. There Reflex's sleep holds
|
||||
the frame limit the same way, through Streamline's frame limit (`nx2_sl.cpp` `nx2sl_reflex2`), with the same
|
||||
exceptions, a DLL older than that export and a limit Streamline refuses besides (`vk_dlssg.c` `vk_dlssg_pace`,
|
||||
`dlssg_pace.h`); the pacer stands aside only once Streamline took the limit and keeps the game's ceiling of 240
|
||||
(`frame_rate.c` `frame_step_ceiling_ns`). Since the SDK does not say whether the driver's interval counts the frames
|
||||
the game draws or every frame on screen, the frames are checked: about the multiplier periods apart switches Reflex
|
||||
to the period over the multiplier once, and the same again or frames too soon give the limit to the pacer for the
|
||||
run.
|
||||
The device gets the drivers' extensions only in a run that starts with the setting on (`vk_device.c`
|
||||
`create_device`): turning it on takes a restart, and with it off the device is made as it was before the mode.
|
||||
- ASTC textures are uploaded as they are where the device samples ASTC, and decoded by a compute shader everywhere
|
||||
|
||||
@@ -209,10 +209,12 @@ typedef struct {
|
||||
// first thing a frame does, the back end holds the frame's start here, before the input is sampled (vk_lowlat.c
|
||||
// vk_lowlat_frame_start). NULL: nothing to do.
|
||||
void (*input_read)(void);
|
||||
// nvnQueuePresentTexture, with the pacer's period in ns (0: it holds nothing): 1 when the back end holds the next
|
||||
// frame's start to that period itself (vk_lowlat.c vk_lowlat_pace, a driver's low-latency sleep), so the pacer
|
||||
// waits for neither the present's slot nor the window image. NULL: never.
|
||||
int (*frame_paced)(uint64_t period_ns);
|
||||
// nvnQueuePresentTexture, with the pacer's period in ns (0: it holds nothing; also during a movie, whose slots only
|
||||
// the pacer keeps): the period the pacer still holds, for both the present's slot and the window image. period_ns
|
||||
// while nothing else holds the limit; less when the back end holds the next frame's start to the period itself:
|
||||
// the game's ceiling (frame_rate.c frame_step_ceiling_ns) where Reflex holds it with DLSS Frame Generation
|
||||
// (vk_dlssg.c vk_dlssg_pace), 0 where a driver's low-latency sleep does (vk_lowlat.c vk_lowlat_pace). NULL: never.
|
||||
uint64_t (*frame_paced)(uint64_t period_ns);
|
||||
void (*finish)(void);
|
||||
void (*read_texels)(const tex_t *t, const view_t *v, const NVNcopyRegion *r, uint8_t *dst, uint64_t row, uint64_t img);
|
||||
void (*write_texels)(const tex_t *t, const view_t *v, const NVNcopyRegion *r, const uint8_t *src, uint64_t row, uint64_t img);
|
||||
|
||||
@@ -896,11 +896,17 @@ NVN_API(nvnQueuePresentTexture) {
|
||||
}
|
||||
if (hitch_every && o->presents % (uint64_t)hitch_every == 0) plat_wait_ns((uint64_t)hitch_ms * 1000000);
|
||||
uint64_t period = present_period(o), now = kern_now_ns();
|
||||
// A driver's low-latency sleep (vk_lowlat.c) holds the next frame to the period where the frame starts, before
|
||||
// its pad read, so this pacer holds nothing: the present goes out as soon as it is drawn and the image is free at
|
||||
// once. A wait here as well would be a second one, after the picture is drawn. Not while a movie plays: its slots
|
||||
// are kept through a late frame (pace), which only this pacer does, so the back end is told there is no period.
|
||||
if (gfx_backend()->frame_paced && gfx_backend()->frame_paced(frame_step_movie_hz() > 0 ? 0 : period)) period = 0;
|
||||
// A driver's low-latency sleep (vk_lowlat.c) or Reflex with DLSS Frame Generation (vk_dlssg.c) holds the next frame
|
||||
// to the period where the frame starts, before its pad read, so this pacer holds at most the game's ceiling: the
|
||||
// present goes out as soon as it is drawn and the image is free again at once, or a ceiling's period after its
|
||||
// slot. A wait here as well would be a second one, after the picture is drawn. Not while a movie plays: its slots
|
||||
// are kept through a late frame (pace), which only this pacer does, so the back end is told there is no period and
|
||||
// the pacer keeps the movie's.
|
||||
if (gfx_backend()->frame_paced) {
|
||||
int movie = frame_step_movie_hz() > 0;
|
||||
uint64_t held = gfx_backend()->frame_paced(movie ? 0 : period);
|
||||
if (!movie) period = held;
|
||||
}
|
||||
uint64_t slot = now + pace(&o->pace, now, period);
|
||||
gfx_backend()->present(t, o->crop, slot);
|
||||
o->ticket[idx] = o->presents;
|
||||
|
||||
@@ -962,6 +962,14 @@ static void vk_be_frame_start(void) {
|
||||
}
|
||||
static void vk_be_input_read(void) { vk_lowlat_frame_start(1); }
|
||||
|
||||
// The period nvnQueuePresentTexture's pacer keeps (GfxBackend.frame_paced). At most one of the two holds the limit in a
|
||||
// run: DLSS Frame Generation's Reflex, which leaves the pacer its ceiling, or a driver's low latency, which leaves it
|
||||
// nothing. Each lets go of the limit when given 0 (a movie plays).
|
||||
static uint64_t vk_be_frame_paced(uint64_t period_ns) {
|
||||
if (vk_dlssg_pace(period_ns)) return frame_step_ceiling_ns();
|
||||
return vk_lowlat_pace(period_ns) ? 0 : period_ns;
|
||||
}
|
||||
|
||||
const GfxBackend gfx_vulkan_backend = {
|
||||
.name = "vulkan",
|
||||
.pool_init = vk_be_pool_init,
|
||||
@@ -979,7 +987,7 @@ const GfxBackend gfx_vulkan_backend = {
|
||||
.present = vk_be_present,
|
||||
.frame_start = vk_be_frame_start,
|
||||
.input_read = vk_be_input_read,
|
||||
.frame_paced = vk_lowlat_pace,
|
||||
.frame_paced = vk_be_frame_paced,
|
||||
.finish = vk_be_finish,
|
||||
.read_texels = vk_be_read_texels,
|
||||
.write_texels = vk_be_write_texels,
|
||||
|
||||
@@ -19,6 +19,16 @@
|
||||
// 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
|
||||
@@ -27,6 +37,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg.h"
|
||||
#include "dlssg_pace.h"
|
||||
#include "host.h"
|
||||
#include "vk.h"
|
||||
#include "vk_dlaa.h"
|
||||
@@ -51,17 +62,27 @@ static struct {
|
||||
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};
|
||||
} S = {.wanted = -1, .fail = DLSSG_NO_FILES, .shown_mult = 1, .pace_said = -1};
|
||||
|
||||
static void sl_line(const char *line) { rt_log("%s", line); }
|
||||
|
||||
@@ -75,6 +96,30 @@ static int reflex_mode(void) {
|
||||
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) {
|
||||
@@ -112,6 +157,8 @@ int vk_dlssg_early(void) {
|
||||
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");
|
||||
@@ -166,7 +213,7 @@ void vk_dlssg_device_ready(VkPhysicalDevice physical, VkDevice dev) {
|
||||
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) S.reflex(reflex_mode());
|
||||
if (S.active) reflex_apply(0); // no swapchain and no present yet: the mode alone
|
||||
}
|
||||
|
||||
int vk_dlssg_active(void) { return S.active; }
|
||||
@@ -229,9 +276,11 @@ static int multiplier_wanted(void) {
|
||||
// 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) S.reflex(reflex_mode());
|
||||
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); }
|
||||
@@ -240,34 +289,103 @@ VkResult vk_dlssg_acquire(VkDevice d, VkSwapchainKHR sc, uint64_t t, VkSemaphore
|
||||
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_RELAXED);
|
||||
__atomic_store_n(&S.hosted_ns, kern_now_ns(), __ATOMIC_RELEASE);
|
||||
return r;
|
||||
}
|
||||
|
||||
// Reflex holds the frame's start until the time the driver computes from the measured render and queue times, 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 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.
|
||||
#define HOLD_IDLE_NS 200000000ull
|
||||
|
||||
// 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_RELAXED);
|
||||
S.sleep(f, hosted && kern_now_ns() - hosted < HOLD_IDLE_NS);
|
||||
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); }
|
||||
@@ -294,6 +412,10 @@ void vk_dlssg_frame(VkCommandBuffer cb, VkExtent2D ext) {
|
||||
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,
|
||||
@@ -322,4 +444,5 @@ void vk_dlssg_frame(VkCommandBuffer cb, VkExtent2D ext) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -22,6 +22,10 @@ VkResult vk_dlssg_present(VkQueue q, const VkPresentInfoKHR *pi);
|
||||
void vk_dlssg_frame(VkCommandBuffer cb, VkExtent2D ext); // each present, before its command buffer is submitted
|
||||
// Frames are numbered by the game's presents (frame n ends with the n-th). Reflex's frame timing while active:
|
||||
void vk_dlssg_frame_start(void); // the game starts a frame (nvn_api_obj.c nvnWindowAcquireTexture): Reflex's sleep
|
||||
// The game presents (nvn_api_obj.c nvnQueuePresentTexture, through vk_backend.c vk_be_frame_paced) with the pacer's
|
||||
// period, 0 during a movie: 1 when Reflex holds the frame limit where the frame starts (dlssg_pace.h), so the pacer
|
||||
// keeps only the game's ceiling. Atomics only, no wait; 0 while DLSS-G does not run.
|
||||
int vk_dlssg_pace(uint64_t period_ns);
|
||||
void vk_dlssg_game_present(void); // the game presents it (vk_backend.c vk_be_present): counts it
|
||||
// its submissions went out (vk_backend.c: vk_be_present, or replay_main with asynchronous replay): the render-submit-end
|
||||
// marker; frame is gfx_counters.frames of its present, the same count
|
||||
|
||||
Reference in New Issue
Block a user