Generate frames with DLSS on Linux through NGX itself: 2x to 6x or dynamic, switched while the game runs.

DLSS Frame Generation went through Streamline, which only the Windows build has, so framegen = dlss did
nothing on Linux. There it now calls NGX's frame generation feature directly (vk_dlaa.c vk_ngxfg_*), with
the depth, motion vectors, jitter, camera and HUD-less picture the Streamline path gets. With it on, each
frame is composed into an image of its own instead of a swapchain image, the generated frames are made at
the end of the frame's command buffer, and a presenter thread shows them and then the rendered frame, each
copied into a swapchain image it acquires itself, spaced over the time between rendered frames
(gfx_present.c ng_*). fg_mult is the multiplier: 2 to 6, or dynamic for only the frames the display's
refresh rate needs. NVIDIA's libnvidia-ngx-dlssg.so.<version> goes beside the game; it is not in the
repository or any kit.

Pull request #124 by spidercraft. Its base was the present path of 0.5, so it is ported by hand:
- taken as written: the NGX calls and the presenter (the block is lifted from the pull request's diff);
- wired again into present_image, create_swapchain, the output rate and the HUD-less picture as they
  are now, with every new branch behind vk_ngxfg_wanted, which is 0 in the Windows build: the code is
  compiled there and never reached;
- added: frame generation's feature and parameters are released before NGX shuts down, the presenter's
  images, semaphores and command pool go when the device is released and are made again for a new one,
  and a failed submit on this path settles its fence without an acquire semaphore;
- the menu: the Frame generation row follows the setting at once on Linux and says "after restart" only
  when the game started without DLSS upscaling; the multiplier row lists what the GPU allows and Dynamic;
- the overlay row of the pull request it builds on (#123) is not part of this.
It touches vk_dlaa.c, which another session is changing: one field in N, the FG state after it, one FN
line, the instance extension query, the capability read in vk_dlaa_init, the release, and a block appended
at the end.

Known gap, as in the pull request: on this path fps_limit counts the frames the game draws. The Windows
files compile without new warnings; nothing was run, and the Linux path is not compiled here.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-10 06:45:59 -05:00
parent d01f0d7052
commit eb2608296b
9 changed files with 727 additions and 18 deletions

View File

@@ -125,6 +125,13 @@ or to use another DLSS version, put the library there yourself: NVIDIA publishes
`v<version>`, and `tools/dlss_pin.py` names the version the kit carries. Later builds keep a library they did not
place. Without it DLSS does nothing on Linux.
DLSS Frame Generation (RTX 40 and 50 cards) runs through NGX itself on Linux, without Streamline. It needs NVIDIA's
`libnvidia-ngx-dlssg.so.<version>` beside `ocarina` (the same repository, `lib/Linux_x86_64/rel/`; no kit carries it)
and DLSS upscaling on when the game starts. *Frame generation* in the Screen menu then offers NVIDIA DLSS, which
switches at once, with a *DLSS multiplier* of 2x to 6x (above 2x on RTX 50 cards) or Dynamic, which generates only the
frames needed to reach the display's refresh rate (`framegen = dlss` and `fg_mult` in `nx2navi.ini`). The log says
`ngxfg: DLSS Frame Generation available` when NGX found the library.
`tools/make_appimage.py` packs this kit into one `NX2navi-Linux-<version>-x86_64.AppImage` with its own Python, for
players without a source checkout: it checks for the packages above, installs zbic with pip on its first run, picks
`--jobs` from the memory, builds into `~/Games/Ocarina` (or the folder given after the dump) and writes `play.sh`

View File

@@ -387,6 +387,12 @@
# With fps_limit unset it is the display's rate.
# fsr3 takes effect at once, dlss (on or off) at the next start.
# framegen = off
# On Linux dlss runs through NVIDIA's NGX itself, without Streamline: it needs dlaa = 1 with DLSS when the game starts
# and NVIDIA's libnvidia-ngx-dlssg.so.<version> (from NVIDIA's DLSS SDK repository) beside the game, then switches at
# once, and its multiplier is fg_mult: 2 to 6 (above 2 needs an RTX 50 GPU) or dynamic, which generates only the frames
# needed to reach the display's refresh rate at the rate the game draws. There fps_limit counts the frames the game
# draws, and framegen_multiplier is not used.
# fg_mult = 2
# DLSS frame generation's multiplier: the frames on screen for each frame the game draws, 2, 3 or 4 (one, two or three
# generated). 2 needs an RTX 40 GPU or newer, 3 and 4 an RTX 50 and a current driver. A value above what the GPU and
# driver allow (Streamline reports it) is lowered to that, with a line in the log; the menu offers only what they allow.

View File

@@ -42,7 +42,7 @@ static const char *const known[] = {
"COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT",
"DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLSS_IDLE_FREE", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR",
"DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES",
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREE_CORES",
"FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREE_CORES",
"FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_BLOWOUT", "HDR_CONTRAST",
"HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GAME_NITS", "HDR_GRADE", "HDR_HIGHLIGHT_SAT", "HDR_HIGHLIGHTS", "HDR_HUE",
"HDR_LIVE",
@@ -88,7 +88,7 @@ static const struct { const char *key, *section; } sections[] = {
{"HDR_HIGHLIGHT_SAT", "display"}, {"HDR_BLOWOUT", "display"}, {"HDR_FLARE", "display"}, {"HDR_HUE", "display"},
{"HDR_GRADE", "display"}, {"SHADOW_RES", "display"},
{"DLAA", "display"}, {"DLSS", "display"}, {"DLSS_PRESET", "display"}, {"DLSS_IDLE_FREE", "display"}, {"DLSS_CHAR_MASK", "display"}, {"FXAA", "display"},
{"FRAMEGEN", "display"}, {"FRAMEGEN_MULTIPLIER", "display"},
{"FRAMEGEN", "display"}, {"FRAMEGEN_MULTIPLIER", "display"}, {"FG_MULT", "display"},
{"DEVICE_PROFILE", "display"}, {"ASSUME_FOCUS", "display"},
{"DISCORD_RPC", "discord"}, {"DISCORD_APP_ID", "discord"}, {"DISCORD_IMAGE", "discord"},
{"SHADOW_SCALE", "display"}, {"SHADOW_RANGE", "display"}, {"CONTACT_SHADOWS", "display"},
@@ -145,6 +145,8 @@ static const CfgMeta metas[] = {
{"DLSS_PRESET", "default", "default|k|j", LIVE}, {"DLSS_IDLE_FREE", "1", "0|1|2", RESTART}, {"TAA", "dlss", "dlss|fsr3", LIVE}, // vk_dlaa.c fsr3
{"FRAMEGEN", "off", "off|fsr3|dlss", LIVE}, // vk_fg.cpp, vk_dlssg.c
{"FRAMEGEN_MULTIPLIER", "2", "2|3|4", LIVE}, // vk_dlssg.c: frames on screen per rendered frame with DLSS
// the same where DLSS's frame generation runs through NGX itself (gfx_present.c ng_multiplier): 2x to 6x or dynamic
{"FG_MULT", "2", "2|3|4|5|6|dynamic", LIVE},
{"DLSS_CHAR_MASK", "65", "0|35|65|100", LIVE}, {"FXAA", "1", "0|1", LIVE},
{"GAME_ASPECT", "16:9", "16:9|window|21:9|32:9", RESTART}, // widescreen.c
{"GAME_RES", "1440", "1080|1440|1800|2160|2880|window", RESTART}, // widescreen.c: the picture's height

View File

@@ -32,6 +32,7 @@
#define FRAMES 2
#define MAX_IMAGES 16
#define NG_MAX 5 // generated frames per rendered one at most (DLSS Frame Generation through NGX, 6x)
#define VSYNC_NS 1000000000ull
#define VSYNC_HZ 60
@@ -349,8 +350,10 @@ static void drop_swapchain_locked(VkCtx *c) {
// read: a resize that arrives while the swapchain is being made (AMD's frame generation swapchain takes tens of ms)
// sets it again, where clearing it at the end lost that resize and kept a swapchain of the old size, whose acquires
// then timed out frame after frame behind AMD's presenter (the picture stopped).
static void ng_drain(void);
static int create_swapchain(void) {
flip_locked(); // the acquired image goes out before its swapchain is replaced
ng_drain(); // and what the NGX frame generation presenter still holds (nothing unless it runs)
plat_xchg32(&g_dirty, 0);
VkCtx *c = vk_ctx();
VkSurfaceCapabilitiesKHR caps;
@@ -374,9 +377,12 @@ static int create_swapchain(void) {
if (fmts[i].format == VK_FORMAT_B8G8R8A8_UNORM && fmts[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { sf = fmts[i]; found = 1; }
for (uint32_t i = 0; i < nf && !found; i++)
if (fmts[i].format == VK_FORMAT_R8G8B8A8_UNORM && fmts[i].colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) { sf = fmts[i]; found = 1; }
uint32_t count = caps.minImageCount + 1;
// NGX frame generation's presenter holds the generated frames' images too, and the rendered frame waits for it:
// room for both
const uint32_t ngx = vk_ngxfg_wanted() ? 1 : 0, most = ngx ? 12 : 8;
uint32_t count = caps.minImageCount + 1 + (ngx ? 2u + NG_MAX : 0u);
if (caps.maxImageCount && count > caps.maxImageCount) count = caps.maxImageCount;
if (count > 8) count = 8;
if (count > most) count = most;
vk_queue_lock();
vkDeviceWaitIdle(c->device);
@@ -846,9 +852,12 @@ static int put_picture(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat
// vk_fg_presents, vk_dlssg.c vk_dlssg_shown) over half a second, sampled at each of the game's presents
// (gfx_output_fps).
static volatile double g_out_fps;
static uint64_t ng_presented(void); // frames the NGX frame generation presenter showed (0: it does not run)
static int ng_running(void); // that presenter is in use
static void output_rate(void) {
static uint64_t t0, n0;
uint64_t n = vk_dlssg_active() ? vk_dlssg_shown() : vk_fg_presents(g_sc), t = plat_now_ns();
uint64_t ng_n = ng_presented();
uint64_t n = ng_n ? ng_n : vk_dlssg_active() ? vk_dlssg_shown() : vk_fg_presents(g_sc), t = plat_now_ns();
if (!n) { g_out_fps = 0, t0 = 0; return; }
if (!t0 || n < n0) { t0 = t, n0 = n; return; }
if (t - t0 >= 500000000ull) g_out_fps = (double)(n - n0) * 1e9 / (double)(t - t0), t0 = t, n0 = n;
@@ -866,7 +875,7 @@ static VkImage hudless_record(VkCommandBuffer cb, unsigned slot, int w, int h, c
VkImageView pv;
uint32_t pw, ph;
VkFormat pf;
if (!vk_fg_active() || !vk_dlaa_fg_hudless(&pic, &pv, &pw, &ph, &pf) || (int)pw != w || (int)ph != h)
if (!(vk_fg_active() || ng_running()) || !vk_dlaa_fg_hudless(&pic, &pv, &pw, &ph, &pf) || (int)pw != w || (int)ph != h)
return VK_NULL_HANDLE;
VkCtx *c = vk_ctx();
if (g_hl && (g_hl_w != g_sc_ext.width || g_hl_h != g_sc_ext.height || g_hl_fmt != g_sc_fmt)) {
@@ -911,6 +920,406 @@ static VkImage hudless_record(VkCommandBuffer cb, unsigned slot, int w, int h, c
return g_hl;
}
// ---------------------------------------------------------------- DLSS Frame Generation through NGX (vk_ngxfg_*)
// NX2NAVI_FRAMEGEN = dlss where Streamline does not run (every build but the Windows one; vk_ngxfg_wanted is 0 there
// and none of this is reached). Each frame is composed into an image of its own (ng.bb) instead of a swapchain image.
// At the end of the frame's command buffer DLSS Frame Generation makes the generated frames from it
// (vk_ngxfg_evaluate, the multiplier less one) and the picture is copied aside. The presenter thread then shows them
// in order, each copied into a swapchain image it acquires itself, and the rendered frame last, spaced evenly over the
// time between two rendered frames, as NVIDIA's frame generation guide paces them. Two sets of images take turns: a
// set is written again two frames later, after the presenter's copies from it are done (the timeline ng.copied).
// Every frame goes through the presenter while it runs, so the timelines advance in order.
// Three things froze the picture while this was written, and the code keeps away from each:
// - a frame's own command buffer never waits for a swapchain acquire: on Wayland an acquire's semaphore can signal
// only after a later present, and every later present would come from that command buffer;
// - a timeline wait is never submitted ahead of its signal on the same queue (ng_wait_signaled);
// - an acquire never blocks while it holds the queue lock the presenter presents with (ng_acquire).
typedef struct { VkImage img; VmaAllocation mem; VkImageView view; } NgImg;
static struct {
int tried, on, fresh; // tried: 0 nothing made, 1 the objects below exist, 2 it cannot run here
int thread_up; // the presenter thread runs (started once, kept over a release)
uint32_t w, h;
VkFormat fmt;
NgImg bb, out[2][NG_MAX], real[2];
VkSemaphore rendered, copied; // timelines: frame n's command buffer submitted/done, the presenter done with frame n
uint64_t n; // frames handed to the presenter
VkCommandPool pool;
VkCommandBuffer cb[2][NG_MAX + 1]; // per shown frame: the generated ones, then the rendered one
VkSemaphore acq[2][NG_MAX + 1];
PlatMutex m;
PlatCond cv;
int busy, pending;
struct { uint32_t idx; int count, set, shown; uint64_t n, t; VkSwapchainKHR sc; } job;
uint64_t signaled; // the last value of ng.copied the presenter submitted a signal for (ng.m)
uint64_t last_t, interval; // average time between rendered frames (ns)
volatile uint64_t presents; // frames presented while it runs, generated ones included
} ng = {.m = PLAT_MUTEX_INIT};
// An acquire that never blocks while it holds the queue lock: the swapchain is shared by the game thread and the
// presenter (both acquire, the presenter presents), and an image is freed only by a present, which needs that lock.
// A blocking acquire under the lock waited for the present it held off: the picture froze (1 s acquire timeouts).
static VkResult ng_acquire(VkSwapchainKHR sc, uint64_t timeout, VkSemaphore sem, uint32_t *idx) {
VkDevice dev = vk_ctx()->device;
uint64_t end = plat_now_ns() + timeout;
for (;;) {
vk_queue_lock();
VkResult r = vkAcquireNextImageKHR(dev, sc, 0, sem, VK_NULL_HANDLE, idx);
vk_queue_unlock();
if (r != VK_NOT_READY && r != VK_TIMEOUT) return r;
if (plat_now_ns() >= end) return VK_TIMEOUT;
plat_sleep_ns(200000);
}
}
static void ng_drain(void) { // waits until the presenter has shown everything handed to it
if (!ng.tried) return;
plat_mutex_lock(&ng.m);
while (ng.pending || ng.busy) plat_cond_wait(&ng.cv, &ng.m);
plat_mutex_unlock(&ng.m);
}
static void ng_sleep_until(uint64_t t) {
uint64_t now = plat_now_ns();
if (t > now && t - now < 100000000ull) plat_sleep_ns(t - now);
}
static void ng_present(VkSwapchainKHR sc, uint32_t idx) {
VkPresentInfoKHR pi = {.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR, .waitSemaphoreCount = 1,
.pWaitSemaphores = &g_render_done[idx], .swapchainCount = 1, .pSwapchains = &sc,
.pImageIndices = &idx};
vk_queue_lock();
VkResult r = vkQueuePresentKHR(vk_ctx()->gfx_queue, &pi);
vk_queue_unlock();
vk_device_fault_report(r);
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1);
__atomic_add_fetch(&ng.presents, 1, __ATOMIC_RELAXED);
}
static void ng_signal_copied(uint64_t v) { // after everything submitted before it (frame v's copies included)
VkSemaphoreSubmitInfo sig = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = ng.copied, .value = v,
.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT};
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .signalSemaphoreInfoCount = 1, .pSignalSemaphoreInfos = &sig};
vk_queue_lock();
vkQueueSubmit2(vk_ctx()->gfx_queue, 1, &si, VK_NULL_HANDLE);
vk_queue_unlock();
plat_mutex_lock(&ng.m);
ng.signaled = v;
plat_cond_broadcast(&ng.cv);
plat_mutex_unlock(&ng.m);
}
// A frame's submission waits for ng.copied >= n - 2 on the GPU. That signal must be submitted first: one queue runs
// its submissions in order, and a wait queued ahead of its signal stopped the queue for good (the picture froze).
static void ng_wait_signaled(uint64_t v) {
plat_mutex_lock(&ng.m);
while (ng.signaled < v) plat_cond_wait(&ng.cv, &ng.m);
plat_mutex_unlock(&ng.m);
}
// Acquires a swapchain image, copies img (GENERAL, the swapchain's size and format) into it once frame n's command
// buffer is done (ng.rendered), and presents it. Only the presenter acquires while it runs: the frame's own command
// buffer never waits for an acquire, whose image the compositor may give back only after a later present.
static int ng_show(VkImage img, int set, int slot, uint64_t n) {
VkCtx *c = vk_ctx();
uint32_t j = 0;
VkResult r = ng_acquire(ng.job.sc, 1000000000ull, ng.acq[set][slot], &j);
if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) {
static int told;
if (told++ < 5) rt_log("ngxfg: presenter acquire failed (%d)", (int)r);
if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_TIMEOUT) plat_xchg32(&g_dirty, 1);
return 0;
}
VkCommandBuffer cb = ng.cb[set][slot];
vkResetCommandBuffer(cb, 0);
VkCommandBufferBeginInfo bi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO,
.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT};
vkBeginCommandBuffer(cb, &bi);
sync_barrier(cb, g_sc_img[j], VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
0, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
VkImageCopy cp = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.extent = {ng.w, ng.h, 1}};
vkCmdCopyImage(cb, img, VK_IMAGE_LAYOUT_GENERAL, g_sc_img[j], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cp);
sync_barrier(cb, g_sc_img[j], VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_NONE, 0);
vkEndCommandBuffer(cb);
VkSemaphoreSubmitInfo wait[2] = {
{.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = ng.acq[set][slot], .stageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT},
{.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = ng.rendered, .value = n,
.stageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT}};
VkSemaphoreSubmitInfo sig = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = g_render_done[j],
.stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT};
VkCommandBufferSubmitInfo cbi = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_SUBMIT_INFO, .commandBuffer = cb};
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .waitSemaphoreInfoCount = 2, .pWaitSemaphoreInfos = wait,
.commandBufferInfoCount = 1, .pCommandBufferInfos = &cbi, .signalSemaphoreInfoCount = 1,
.pSignalSemaphoreInfos = &sig};
vk_queue_lock();
r = vkQueueSubmit2(c->gfx_queue, 1, &si, VK_NULL_HANDLE);
vk_queue_unlock();
if (r != VK_SUCCESS) { vk_device_fault_report(r); return 0; }
ng_present(ng.job.sc, j);
return 1;
}
static void ng_run(void) {
VkCtx *c = vk_ctx();
uint64_t t = ng.job.t, n = ng.job.n;
int set = ng.job.set, count = ng.job.count;
ng.job.shown = 0;
if (ng.last_t && t > ng.last_t) {
uint64_t d = t - ng.last_t;
if (d > 2000000ull && d < 100000000ull) ng.interval = ng.interval ? (ng.interval * 7 + d) / 8 : d;
}
ng.last_t = t;
uint64_t step = ng.interval / (uint64_t)(count + 1);
if (n > 2) { // the command buffers and acquire semaphores of this set: their last use (frame n - 2) is over
uint64_t v = n - 2;
VkSemaphoreWaitInfo wi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_WAIT_INFO, .semaphoreCount = 1, .pSemaphores = &ng.copied,
.pValues = &v};
vkWaitSemaphores(c->device, &wi, 2000000000ull);
}
for (int i = 0; i < count; i++) {
if (i) ng_sleep_until(t + (uint64_t)i * step); // the first one at once: the least latency
if (!ng_show(ng.out[set][i].img, set, i, n)) break;
ng.job.shown = i + 1;
}
static uint64_t jobs, gen_ok, gen_want;
jobs++, gen_want += (uint64_t)count;
if (count) ng_sleep_until(t + (uint64_t)count * step);
ng_show(ng.real[set].img, set, NG_MAX, n); // the rendered frame, last
ng_signal_copied(n); // after the copies out of this set
gen_ok += (uint64_t)ng.job.shown;
if (jobs == 60 || jobs % 1200 == 0)
rt_log("ngxfg: presenter: %llu rendered frames, %llu of %llu generated frames shown, %llu presents; %.2f ms between rendered frames",
(unsigned long long)jobs, (unsigned long long)gen_ok, (unsigned long long)gen_want,
(unsigned long long)__atomic_load_n(&ng.presents, __ATOMIC_RELAXED), (double)ng.interval / 1e6);
}
static void ng_thread(void *arg) {
(void)arg;
for (;;) {
plat_mutex_lock(&ng.m);
while (!ng.pending) {
if (!plat_cond_wait_ms(&ng.cv, &ng.m, 1500) && ng.on && ng.n) { // nothing for 1.5 s: where the GPU stands
uint64_t rv = 0, cv = 0;
vkGetSemaphoreCounterValue(vk_ctx()->device, ng.rendered, &rv);
vkGetSemaphoreCounterValue(vk_ctx()->device, ng.copied, &cv);
static int told;
if (told++ < 3)
rt_log("ngxfg: watchdog: no frame for 1.5 s; frames handed %llu, copied signal submitted %llu; GPU rendered %llu copied %llu",
(unsigned long long)ng.n, (unsigned long long)ng.signaled, (unsigned long long)rv, (unsigned long long)cv);
}
}
ng.pending = 0, ng.busy = 1;
plat_cond_broadcast(&ng.cv);
plat_mutex_unlock(&ng.m);
ng_run();
plat_mutex_lock(&ng.m);
ng.busy = 0;
plat_cond_broadcast(&ng.cv);
plat_mutex_unlock(&ng.m);
}
}
static void ng_free_image(NgImg *im) {
VkCtx *c = vk_ctx();
if (im->view) vkDestroyImageView(c->device, im->view, NULL);
if (im->img) vmaDestroyImage(c->vma, im->img, im->mem);
memset(im, 0, sizeof *im);
}
static int ng_image(NgImg *im, uint32_t w, uint32_t h, VkFormat f) {
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 = VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED};
VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE};
if (vmaCreateImage(c->vma, &ii, &ai, &im->img, &im->mem, NULL) != VK_SUCCESS) return im->img = VK_NULL_HANDLE, 0;
VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = im->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, &im->view) == VK_SUCCESS;
}
// The presenter and the images for the current swapchain (g_pl held). 0: frames go out the plain way.
static int ng_ready(void) {
if (!vk_ngxfg_wanted()) return 0; // the Windows build, or a run that started without DLSS upscaling
int want = vk_ngxfg_on() && !g_sc_hdr && !vk_fg_active() && !vk_dlssg_active();
if (!vk_fg_wanted()) vk_dlaa_hudless_wanted = want; // the HUD-less picture DLSS-G tells the HUD apart by
if (!want) { // off (the menu switched it off live): what the presenter holds goes out first
if (ng.on) ng_drain(), ng.on = 0, rt_log("ngxfg: frame generation off");
return 0;
}
if (ng.on && ng.w == g_sc_ext.width && ng.h == g_sc_ext.height && ng.fmt == g_sc_fmt) return 1;
VkCtx *c = vk_ctx();
if (!ng.tried) {
ng.tried = 1;
VkFormatProperties fp;
vkGetPhysicalDeviceFormatProperties(c->phys, g_sc_fmt, &fp);
if (!(fp.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT)) {
rt_log("ngxfg: the swapchain format %d is no storage format here: frame generation off", (int)g_sc_fmt);
return ng.on = 0, ng.tried = 2, 0;
}
if (!ng.thread_up) plat_cond_init(&ng.cv);
VkSemaphoreTypeCreateInfo tl = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_TYPE_CREATE_INFO, .semaphoreType = VK_SEMAPHORE_TYPE_TIMELINE};
VkSemaphoreCreateInfo tci = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO, .pNext = &tl};
vkCreateSemaphore(c->device, &tci, NULL, &ng.rendered);
vkCreateSemaphore(c->device, &tci, NULL, &ng.copied);
VkSemaphoreCreateInfo semi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO};
VkCommandPoolCreateInfo pci = {.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO,
.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT, .queueFamilyIndex = c->gfx_family};
vkCreateCommandPool(c->device, &pci, NULL, &ng.pool);
for (int s = 0; s < 2; s++) {
VkCommandBufferAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO, .commandPool = ng.pool,
.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY, .commandBufferCount = NG_MAX + 1};
vkAllocateCommandBuffers(c->device, &ai, ng.cb[s]);
for (int i = 0; i <= NG_MAX; i++) vkCreateSemaphore(c->device, &semi, NULL, &ng.acq[s][i]);
}
if (!ng.thread_up && !plat_thread_spawn(ng_thread, NULL, "fg present", 0)) {
rt_log("ngxfg: the presenter thread did not start: frame generation off");
return ng.on = 0, ng.tried = 2, 0;
}
ng.thread_up = 1;
}
if (ng.tried == 2) return 0;
ng_drain();
vk_queue_lock();
vkDeviceWaitIdle(c->device);
vk_queue_unlock();
ng_free_image(&ng.bb);
for (int s = 0; s < 2; s++) {
ng_free_image(&ng.real[s]);
for (int i = 0; i < NG_MAX; i++) ng_free_image(&ng.out[s][i]);
}
uint32_t w = g_sc_ext.width, h = g_sc_ext.height;
int ok = ng_image(&ng.bb, w, h, g_sc_fmt);
for (int s = 0; s < 2 && ok; s++) {
ok = ng_image(&ng.real[s], w, h, g_sc_fmt);
for (int i = 0; i < NG_MAX && ok; i++) ok = ng_image(&ng.out[s][i], w, h, g_sc_fmt);
}
if (!ok) {
rt_log("ngxfg: frame generation images (%ux%u) could not be made: frame generation off", w, h);
return ng.on = 0;
}
ng.w = w, ng.h = h, ng.fmt = g_sc_fmt, ng.fresh = 1, ng.on = 1;
vk_ngxfg_reset();
rt_log("ngxfg: presenter ready at %ux%u (up to %dx)", w, h, vk_ngxfg_max() + 1);
return 1;
}
// NX2NAVI_FG_MULT: 2 to 6, or dynamic: only the frames needed to reach the display's refresh rate at the rendered frame
// rate (240 Hz at 60 fps: 4x), between 2x and what the GPU allows, as NVIDIA describes its dynamic multi frame
// generation; a new multiplier must hold for 30 frames before it is taken, so it does not flicker.
static int ng_multiplier(int max) {
const char *v = cfg_value("NX2NAVI_FG_MULT");
int m;
if (!strcmp(v, "dynamic")) {
static int cur = 2, cand, held;
double hz = gfx_window_refresh_hz(), ms = (double)ng.interval / 1e6;
int fit = hz > 0 && ms > 0 ? (int)(hz * ms / 1000.0 + 0.1) : 2; // shown frames per rendered one that fit
if (fit < 2) fit = 2;
if (fit > max + 1) fit = max + 1;
if (fit != cur) {
held = fit == cand ? held + 1 : 0, cand = fit;
if (held >= 30) cur = fit, held = 0;
} else held = 0;
m = cur;
} else {
m = atoi(v);
}
if (m < 2) m = 2;
if (m > max + 1) m = max + 1;
return m;
}
// Records the copy of the frame's picture and DLSS-G's frames (the end of present_image's command buffer, dst in
// layout *last). Returns the number of generated frames, -1 when the frame goes out the plain way.
static int ng_record(VkCommandBuffer cb, VkImage dst, VkImageLayout *last, VkPipelineStageFlags2 *stage, VkAccessFlags2 *write,
VkImage hudless, int *set, int gen) {
if (!ng.on) return -1;
if (ng.fresh) { // every image to GENERAL, once
NgImg *all[1 + 2 + 2 * NG_MAX];
int k = 0;
all[k++] = &ng.bb;
for (int s = 0; s < 2; s++) {
all[k++] = &ng.real[s];
for (int i = 0; i < NG_MAX; i++) all[k++] = &ng.out[s][i];
}
for (int i = 0; i < k; i++)
sync_barrier(cb, all[i]->img, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_NONE, 0,
VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT);
ng.fresh = 0;
}
*set = (int)((ng.n + 1) & 1);
// the frame's picture is in dst (ng.bb): to GENERAL for DLSS-G and the copy into this set's rendered-frame image
sync_barrier(cb, dst, *last, VK_IMAGE_LAYOUT_GENERAL, *stage, *write, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_READ_BIT);
*last = VK_IMAGE_LAYOUT_GENERAL, *stage = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, *write = 0;
int made = 0;
if (!gen || menu_is_open()) {
vk_ngxfg_reset(); // generation pauses while the menu is open
} else {
int max = vk_ngxfg_max(), want = ng_multiplier(max) - 1;
VkNgxImage bb = {ng.bb.img, ng.bb.view, ng.fmt, ng.w, ng.h};
VkNgxImage hl = {hudless, g_hl_view, g_hl_fmt, g_hl_w, g_hl_h};
VkNgxImage out[NG_MAX], real = {ng.real[*set].img, ng.real[*set].view, ng.fmt, ng.w, ng.h};
for (int i = 0; i < NG_MAX; i++) out[i] = (VkNgxImage){ng.out[*set][i].img, ng.out[*set][i].view, ng.fmt, ng.w, ng.h};
int hl_ok = hudless && g_hl_w == ng.w && g_hl_h == ng.h && g_hl_fmt == ng.fmt;
made = vk_ngxfg_evaluate(cb, &bb, hl_ok ? &hl : NULL, out, want, &real);
}
// the rendered frame for the presenter (ng.bb is drawn again by the next frame)
sync_barrier(cb, ng.real[*set].img, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,
VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT);
VkImageCopy cp = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1},
.extent = {ng.w, ng.h, 1}};
vkCmdCopyImage(cb, dst, VK_IMAGE_LAYOUT_GENERAL, ng.real[*set].img, VK_IMAGE_LAYOUT_GENERAL, 1, &cp);
sync_barrier(cb, ng.real[*set].img, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT,
VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, VK_ACCESS_2_MEMORY_READ_BIT);
*stage = VK_PIPELINE_STAGE_2_TRANSFER_BIT, *write = 0;
return made;
}
// gfx_window_release_vk, the device idle: the presenter shows what it holds, then its images, semaphores and command
// buffers go with the device. The thread stays and waits; a later device makes the objects again (ng_ready).
static void ng_release(void) {
if (!ng.tried) return;
ng_drain();
VkCtx *c = vk_ctx();
vk_queue_lock(), vkDeviceWaitIdle(c->device), vk_queue_unlock(); // what the presenter submitted while it drained
ng_free_image(&ng.bb);
for (int s = 0; s < 2; s++) {
ng_free_image(&ng.real[s]);
for (int i = 0; i < NG_MAX; i++) ng_free_image(&ng.out[s][i]);
}
if (ng.rendered) vkDestroySemaphore(c->device, ng.rendered, NULL);
if (ng.copied) vkDestroySemaphore(c->device, ng.copied, NULL);
for (int s = 0; s < 2; s++)
for (int i = 0; i <= NG_MAX; i++)
if (ng.acq[s][i]) vkDestroySemaphore(c->device, ng.acq[s][i], NULL), ng.acq[s][i] = VK_NULL_HANDLE;
if (ng.pool) vkDestroyCommandPool(c->device, ng.pool, NULL); // its command buffers with it
ng.rendered = ng.copied = VK_NULL_HANDLE, ng.pool = VK_NULL_HANDLE;
plat_mutex_lock(&ng.m);
ng.on = ng.tried = 0, ng.n = ng.signaled = 0; // new timelines start at 0
plat_mutex_unlock(&ng.m);
}
static uint64_t ng_presented(void) { return ng.on ? __atomic_load_n(&ng.presents, __ATOMIC_RELAXED) : 0; }
static int ng_running(void) { return ng.on; }
// Hands the submitted frame to the presenter (it shows count generated frames, then swapchain image idx).
static void ng_hand(uint32_t idx, int count, int set, uint64_t n) {
plat_mutex_lock(&ng.m);
while (ng.pending) plat_cond_wait(&ng.cv, &ng.m);
ng.job.idx = idx, ng.job.count = count < 0 ? 0 : count, ng.job.set = set, ng.job.n = n, ng.job.t = plat_now_ns();
ng.job.sc = g_sc;
ng.pending = 1;
plat_cond_broadcast(&ng.cv);
plat_mutex_unlock(&ng.m);
}
static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w, int h, const int crop[4], int defer) {
int general = layout == VK_IMAGE_LAYOUT_GENERAL;
{ // the presented image's format, when it changes (an HDR picture comes in another one)
@@ -932,7 +1341,10 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
goto out;
}
uint32_t idx = 0;
VkResult r = vk_dlssg_active() ? vk_dlssg_acquire(c->device, g_sc, 1000000000ull, g_acquire[slot], &idx)
// NGX frame generation: the frame is composed into ng.bb and the presenter shows it (ng_show); no acquire here
const int ngp = ng_ready();
VkResult r = ngp ? VK_SUCCESS
: vk_dlssg_active() ? vk_dlssg_acquire(c->device, g_sc, 1000000000ull, g_acquire[slot], &idx)
: vk_fg_active() ? vk_fg_acquire(g_sc, 1000000000ull, g_acquire[slot], &idx)
: vkAcquireNextImageKHR(c->device, g_sc, 1000000000ull, g_acquire[slot], VK_NULL_HANDLE, &idx);
if (r == VK_ERROR_OUT_OF_DATE_KHR) {
@@ -980,7 +1392,8 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
g_sc_hdr ? VK_FORMAT_R8G8B8A8_UNORM : g_sc_fmt, g_sc_ext.width,
g_sc_ext.height, cw, ch, g_scaler};
// without a source image (gfx_present_boot): a black frame with the boot screen
int overlay = g_sc_view[idx] &&
VkImageView dst_view = ngp ? ng.bb.view : g_sc_view[idx];
int overlay = dst_view &&
(src ? menu_overlay_frame(__atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED), &mt, &menu_shot)
: menu_boot_frame(&mt));
// NX2NAVI_SHOT_EVERY=<s> with NX2NAVI_SHOT_DIR=<dir>: the presented frame as shot_<n>.png every s seconds
@@ -1013,7 +1426,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
vkBeginCommandBuffer(cb, &bi);
uint64_t crumb = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
vk_crumb(cb, VK_CRUMB_PRESENT, crumb);
VkImage dst = g_sc_img[idx];
VkImage dst = ngp ? ng.bb.img : g_sc_img[idx];
sync_barrier(cb, dst, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0,
VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT);
const int rect[4] = {cx, cy, cw, ch};
@@ -1060,7 +1473,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT,
VK_ACCESS_2_COLOR_ATTACHMENT_READ_BIT | VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT);
VkRenderingAttachmentInfo ca = {.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO,
.imageView = menu_view ? menu_view : g_sc_view[idx],
.imageView = menu_view ? menu_view : dst_view,
.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,
.loadOp = menu_view ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD,
.storeOp = VK_ATTACHMENT_STORE_OP_STORE};
@@ -1069,7 +1482,7 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
vkCmdBeginRendering(cb, &ri);
menu_overlay_draw(cb);
vkCmdEndRendering(cb);
if (menu_view) vk_hdr_ui_compose(cb, g_sc_view[idx], g_sc_fmt, g_sc_ext.width, g_sc_ext.height);
if (menu_view) vk_hdr_ui_compose(cb, dst_view, g_sc_fmt, g_sc_ext.width, g_sc_ext.height);
last = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
last_stage = VK_PIPELINE_STAGE_2_COLOR_ATTACHMENT_OUTPUT_BIT;
last_write = VK_ACCESS_2_COLOR_ATTACHMENT_WRITE_BIT;
@@ -1096,7 +1509,12 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
if (src) vk_fg_frame(cb, g_sc, g_sc_fmt, g_sc_ext, hudless, g_hl_view, to); // frame generation: this frame's preparation
if (src) output_rate();
if (src) vk_dlssg_frame(cb, g_sc_ext); // DLSS Frame Generation: constants and tags (vk_dlssg.c)
sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, last_stage, last_write, VK_PIPELINE_STAGE_2_NONE, 0);
// through the presenter: the picture stays in ng.bb, and DLSS Frame Generation's frames and the copy for the
// presenter are recorded; otherwise the swapchain image is ready to present
int ng_set = 0;
const int ng_count = ngp ? ng_record(cb, dst, &last, &last_stage, &last_write, hudless, &ng_set, src != VK_NULL_HANDLE) : -1;
const int via_ng = ng_count >= 0;
if (!via_ng) sync_barrier(cb, dst, last, VK_IMAGE_LAYOUT_PRESENT_SRC_KHR, last_stage, last_write, VK_PIPELINE_STAGE_2_NONE, 0);
vk_crumb(cb, VK_CRUMB_PRESENT_END, crumb);
vkEndCommandBuffer(cb);
@@ -1108,10 +1526,25 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
VkSubmitInfo2 si = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .waitSemaphoreInfoCount = 1, .pWaitSemaphoreInfos = &wait,
.commandBufferInfoCount = 1, .pCommandBufferInfos = &cbi, .signalSemaphoreInfoCount = 1,
.pSignalSemaphoreInfos = &sig};
const uint64_t ng_n = ng.n + 1; // through the presenter: this frame's number on its timelines
if (via_ng) {
// No acquire to wait for. The frame writes one of the presenter's two image sets: it waits on the GPU until
// the presenter is done with the frame before last, which used that set, and that signal is submitted first
// (ng_wait_signaled). It signals the timeline the presenter's copies wait for instead of a render-done semaphore.
wait = (VkSemaphoreSubmitInfo){.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = ng.copied,
.value = ng_n > 2 ? ng_n - 2 : 0, .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT};
sig = (VkSemaphoreSubmitInfo){.sType = VK_STRUCTURE_TYPE_SEMAPHORE_SUBMIT_INFO, .semaphore = ng.rendered,
.value = ng_n, .stageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT};
si.waitSemaphoreInfoCount = ng_n > 2 ? 1u : 0u;
if (ng_n > 2) ng_wait_signaled(ng_n - 2);
}
vk_queue_lock();
r = vkQueueSubmit2(c->gfx_queue, 1, &si, g_fence[slot]);
vk_queue_unlock();
if (r == VK_SUCCESS) {
if (r == VK_SUCCESS && via_ng) { // the presenter shows the generated frames, then this one
ng.n = ng_n;
ng_hand(idx, ng_count, ng_set, ng_n);
} else if (r == VK_SUCCESS) {
g_flip_on = 1;
g_flip_idx = idx;
if (!defer) flip_locked();
@@ -1121,7 +1554,8 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w
// The fence was reset after the acquire and the acquire's semaphore is signalled: left so, this slot's fence
// wait would time out (5 s) at every later present and the picture stop for good. An empty batch takes the
// semaphore and signals the fence; the acquired image goes back with the swapchain, made again next frame.
VkSubmitInfo2 settle = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .waitSemaphoreInfoCount = 1,
// (through the presenter nothing was acquired: the batch only signals the fence)
VkSubmitInfo2 settle = {.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO_2, .waitSemaphoreInfoCount = via_ng ? 0u : 1u,
.pWaitSemaphoreInfos = &wait};
vk_queue_lock();
VkResult r2 = vkQueueSubmit2(c->gfx_queue, 1, &settle, g_fence[slot]);
@@ -1177,6 +1611,7 @@ void gfx_window_release_vk(void) {
vk_queue_lock(), vkDeviceWaitIdle(c->device), vk_queue_unlock();
vk_queue_unlock();
g_flip_on = 0; // its blit is done (device idle); the swapchain goes away unpresented
ng_release(); // NGX frame generation's presenter shows nothing more; its images go before the device
menu_vk_release();
vk_fsr_release();
vk_color_release();

View File

@@ -67,6 +67,8 @@ int vk_hdr_wanted(void);
int vk_hdr_active(void);
int vk_hdr_needs_restart(void);
int vk_dlaa_needs_restart(void);
int vk_ngxfg_wanted(void); // vk_dlaa.c: DLSS's frame generation through NGX itself can run in this run
int vk_ngxfg_max(void); // ... and how many frames it may generate per rendered one (0: it cannot)
int vk_ffx_available(void); // vk_ffx.cpp: FSR 3.1 upscaling can run (asked without loading the library)
int vk_ffx_framegen_available(void); // ... and FSR 3.1 frame generation
double frame_step_limit_hz(void);
@@ -941,6 +943,36 @@ static void row_framegen_mult(bool restart) {
if (nv >= 0 && nv < n) opt_apply("NX2NAVI_FRAMEGEN_MULTIPLIER", values[nv]);
}
// NVIDIA's frame generation runs through NGX itself, not through Streamline (vk_dlaa.c vk_ngxfg_*, gfx_present.c
// ng_*): every build but the Windows one.
static bool framegen_ngx(void) {
#ifdef _WIN32
return false;
#else
return true;
#endif
}
// Its multiplier there (cfg.c FG_MULT, gfx_present.c ng_multiplier): 2x up to what the GPU allows, or Dynamic. Left out
// while frame generation is set but cannot run: the frame generation row says why.
static void row_framegen_mult_ngx(void) {
static const char *const mult[] = {"2", "3", "4", "5", "6"};
static const char *const mult_names[] = {"2x", "3x", "4x", "5x", "6x"};
const int max = vk_ngxfg_max(); // generated frames per rendered one
if (max < 1) return;
const char *values[6], *names[6];
int n = 0;
for (; n < max && n < 5; n++) values[n] = mult[n], names[n] = mult_names[n];
const int top = n - 1;
values[n] = "dynamic", names[n++] = menu_text(UI_MENU_FG_MULT_DYNAMIC);
const char *v = cfg_value("NX2NAVI_FG_MULT");
int cur = strcmp(v, "dynamic") ? top : n - 1; // a multiplier above what the GPU allows shows as the highest
for (int i = 0; i < n; i++)
if (!strcmp(v, values[i])) cur = i;
int nv = opt_choice_index("NX2NAVI_FG_MULT", UI_MENU_FRAMEGEN_MULT, names, n, cur, 0, UI_MENU_FG_MULT_DESC, nullptr);
if (nv >= 0 && nv < n) opt_apply("NX2NAVI_FG_MULT", values[nv]);
}
// Frame generation (vk_fg.cpp, at once; NVIDIA's after a restart); it needs a temporal upscaler's motion vectors
static void row_framegen(void) {
// the generated frames are made from DLSS's or FSR 3.1's motion vectors: without one of them it would do nothing,
@@ -969,16 +1001,20 @@ static void row_framegen(void) {
if (i == cur) at = n;
values[n] = all_values[i], names[n++] = all_names[i];
}
// AMD's follows the setting; NVIDIA's is set up with the device, so only a change to or from it waits for a restart
const bool restart = dlss && (cur == 2) != (vk_dlssg_wanted() != 0);
// AMD's follows the setting; NVIDIA's is set up with the device, so only a change to or from it waits for a restart.
// Through NGX it follows the setting too, once NGX is up, which needs DLSS upscaling on when the game starts.
const bool ngx = framegen_ngx();
const bool restart = ngx ? dlss && cur == 2 && !vk_ngxfg_wanted() : dlss && (cur == 2) != (vk_dlssg_wanted() != 0);
if (restart) note = UI_MENU_AFTER_RESTART;
else if (cur == 1 && !fsr) note = UI_MENU_FSR3_OFF; // set where it cannot load: "in use: off" and why
// NVIDIA's set since the start but not running: "in use: off" and why (vk_dlssg_status; the log has the details)
const int st = cur == 2 && !restart ? vk_dlssg_status() : DLSSG_RUNS;
// NVIDIA's set since the start but not running: "in use: off" and why (vk_dlssg_status; the log has the details).
// Through NGX that is NVIDIA's frame generation library missing beside the game, or a GPU it does not run on.
const int st = cur != 2 || restart ? DLSSG_RUNS : ngx ? (vk_ngxfg_max() ? DLSSG_RUNS : DLSSG_NO_FILES) : vk_dlssg_status();
if (st == DLSSG_NO_GPU) note = UI_MENU_FRAMEGEN_OFF_GPU;
else if (st == DLSSG_NO_FILES) note = UI_MENU_FRAMEGEN_OFF_FILES;
int nv = opt_choice_index("NX2NAVI_FRAMEGEN", UI_MENU_FRAMEGEN, names, n, at, 0, desc, menu_text(note));
if (nv >= 0 && nv < n) opt_apply("NX2NAVI_FRAMEGEN", values[nv]);
else if (cur == 2 && upscaled && dlss && ngx) row_framegen_mult_ngx();
else if (cur == 2 && upscaled && dlss) row_framegen_mult(restart);
}

View File

@@ -562,6 +562,10 @@ static const char *const text_en[UI_TEXT_COUNT] = {
"here.",
[UI_MENU_FRAMEGEN_NEEDS_NONE] = "needs DLSS or FSR 3.1, which do not run here",
[UI_MENU_FRAMEGEN_MULT] = "DLSS multiplier",
[UI_MENU_FG_MULT_DESC] = "Frames on screen for each frame the game draws with DLSS Frame Generation: 2x adds one "
"generated frame, up to 6x on an RTX 50 card. Dynamic adds only the frames needed to "
"reach your display's refresh rate. Changes take effect at once.",
[UI_MENU_FG_MULT_DYNAMIC] = "Dynamic",
[UI_MENU_FRAMEGEN_MULT_DESC] = "Frames on screen for each frame the game draws: 2x adds one generated frame, 3x "
"two, 4x three. Higher is smoother but adds a little delay. The frame limit counts "
"the frames on screen, so the game draws the limit divided by this: set the limit "

View File

@@ -283,6 +283,8 @@ typedef enum {
// DLSS frame generation (ui/menu.cpp row_framegen, row_framegen_mult): the multiplier row, its notes when this GPU
// and driver allow less than 4x, and the frame generation row's notes when DLSS's does not run
UI_MENU_FRAMEGEN_MULT, UI_MENU_FRAMEGEN_MULT_DESC,
// the multiplier row where DLSS's frame generation runs through NGX itself (ui/menu.cpp row_framegen_mult_ngx)
UI_MENU_FG_MULT_DESC, UI_MENU_FG_MULT_DYNAMIC,
UI_MENU_FRAMEGEN_MULT_RTX50, // this GPU and driver do 2x only
UI_MENU_FRAMEGEN_MULT_UPTO, // printf: "%d", the highest multiplier this GPU and driver allow
UI_MENU_FRAMEGEN_MULT_OLD_DLL, // nx2navi_dlssg.dll is older than multi frame generation

View File

@@ -78,6 +78,7 @@ static struct {
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
@@ -90,6 +91,30 @@ static struct {
#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);
@@ -217,6 +242,7 @@ static int core_load(void) {
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);
@@ -243,12 +269,51 @@ static int listed(const char *const *list, uint32_t n, const char *s) {
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,
@@ -354,6 +419,18 @@ void vk_dlaa_init(void) {
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; }
@@ -1250,6 +1327,9 @@ void vk_dlaa_release(void) {
if (!c) return;
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]);
@@ -1994,3 +2074,129 @@ int vk_dlaa_skip_draw(VkExec *ex, const GfxShader *fs) {
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.
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 DLSS's 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);
NgxResult r = N.create(cb, NGX_FEATURE_FRAME_GENERATION, p, &FG.feat);
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", 0.1f); // Ocarina's projection, as vk_dlssg.c gives it
p_setf(p, "DLSSG.CameraFar", 1e8f);
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", 0);
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;
}

View File

@@ -8,6 +8,17 @@
#define DLAA_SCENE_MIN_W 320u
int vk_dlaa_wanted(void); // the setting is on (the device may still lack DLSS)
// DLSS Frame Generation through NGX itself (vk_dlaa.c vk_ngxfg_*, gfx_present.c's presenter): NX2NAVI_FRAMEGEN = dlss
// where Streamline does not run (every build but the Windows one, where these all answer 0).
typedef struct { VkImage img; VkImageView view; VkFormat fmt; uint32_t w, h; } VkNgxImage; // in GENERAL
int vk_ngxfg_wanted(void); // this run can generate frames through NGX (DLSS upscaling on at the start)
int vk_ngxfg_max(void); // generated frames per rendered one the GPU allows (1 = 2x, 5 = 6x), 0 when unavailable
int vk_ngxfg_on(void); // ... and NX2NAVI_FRAMEGEN = dlss now (the menu switches it live)
void vk_ngxfg_reset(void); // the next generation starts without history (a resize, the menu)
// Records count generated frames into out[] and a copy of bb into real; returns how many were made (0: none, and real
// is not written).
int vk_ngxfg_evaluate(VkCommandBuffer cb, const VkNgxImage *bb, const VkNgxImage *hudless, const VkNgxImage *out, int count,
const VkNgxImage *real);
int vk_dlaa_fsr3(void); // the setting is on and its upscaler is AMD FSR 3.1, which loads
int vk_dlaa_needs_restart(void); // the menu's upscaler cannot start in this run
// Frame generation (vk_fg.cpp): the depth and motion vectors of the frame DLSS or FSR 3.1 last ran on, with its jitter