From dc3f424fde1c50600ceee86a5e03f050b8415d35 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 18:57:18 -0500 Subject: [PATCH] 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 --- docs/architecture.md | 37 ++++++----- runtime/gfx.h | 10 +-- runtime/nvn_api_obj.c | 16 +++-- runtime/vk_backend.c | 10 ++- runtime/vk_dlssg.c | 149 ++++++++++++++++++++++++++++++++++++++---- runtime/vk_dlssg.h | 4 ++ 6 files changed, 188 insertions(+), 38 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 250c5018..6ce79c75 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 diff --git a/runtime/gfx.h b/runtime/gfx.h index f67fefd6..eaeec226 100644 --- a/runtime/gfx.h +++ b/runtime/gfx.h @@ -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); diff --git a/runtime/nvn_api_obj.c b/runtime/nvn_api_obj.c index ece8be33..416604e6 100644 --- a/runtime/nvn_api_obj.c +++ b/runtime/nvn_api_obj.c @@ -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; diff --git a/runtime/vk_backend.c b/runtime/vk_backend.c index 529b0096..a70f6e4c 100644 --- a/runtime/vk_backend.c +++ b/runtime/vk_backend.c @@ -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, diff --git a/runtime/vk_dlssg.c b/runtime/vk_dlssg.c index acc0c135..df4d3926 100644 --- a/runtime/vk_dlssg.c +++ b/runtime/vk_dlssg.c @@ -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 #endif @@ -27,6 +37,7 @@ #include #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 } diff --git a/runtime/vk_dlssg.h b/runtime/vk_dlssg.h index cc9365bf..895fa584 100644 --- a/runtime/vk_dlssg.h +++ b/runtime/vk_dlssg.h @@ -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