diff --git a/README.md b/README.md index 04a6ffe7..eacc442a 100644 --- a/README.md +++ b/README.md @@ -32,10 +32,12 @@ Report problems as issues, following [CONTRIBUTING.md](CONTRIBUTING.md). - Upscaling and anti-aliasing: NVIDIA DLSS (DLAA, Quality, Balanced, Performance, Ultra Performance; RTX GPUs) or AMD FSR 3.1 (Native AA and the same modes; any GPU), which render the 3D scene at the mode's share and keep the HUD sharp; or AMD FSR 1 or bilinear scaling to the window, with a sharpness setting; black bars or stretch -- Frame generation: NVIDIA DLSS Frame Generation (RTX 40 and 50 GPUs, paced with Reflex) or AMD FSR 3.1 (any GPU), a generated +- Frame generation: NVIDIA DLSS Frame Generation (RTX 40 and 50 GPUs, paced with Reflex) at 2x, or 3x and 4x on RTX 50 + GPUs (one, two or three generated frames for every frame the game draws), or AMD FSR 3.1 (any GPU), a generated frame between every two the game draws, with the HUD kept out of it, for about twice the frame rate on a fast display; in a window of another shape than the picture, frames are generated inside the picture only, so the black - bars stay clean. The frame limit can also be handed to RivaTuner Statistics Server + bars stay clean. With frame generation on, the frame limit counts the frames on screen. The frame limit can also be + handed to RivaTuner Statistics Server - HDR on HDR displays: the game's own native HDR, or its SDR picture tone mapped to the display's peak, with exposure, highlights, shadows, contrast, saturation and gamma in the menu - The game's known shaders prepared when the game is built, and the pipelines met in play built again in the background at each start, so play rarely stops to compile them; a shader or pipeline met for the first time can diff --git a/docs/README-BuildKit.md b/docs/README-BuildKit.md index f8096007..06727f90 100644 --- a/docs/README-BuildKit.md +++ b/docs/README-BuildKit.md @@ -108,8 +108,9 @@ beside the game. "Preparing shaders" once while the driver rebuilds them. With a driver that has Vulkan's graphics pipeline library it lasts at most about 20 seconds once the shaders are translated, and the remaining pipelines are built in the background while you play. -- **Screen** in the menu has Upscaling (DLSS on NVIDIA RTX cards, FSR 3.1 on any card), Frame generation (FSR 3.1; it - needs DLSS or FSR 3.1 upscaling on), HDR (the game's picture tone mapped to your display, with its own grading rows, or Native, the game's own HDR; turn HDR on in Windows first) and the +- **Screen** in the menu has Upscaling (DLSS on NVIDIA RTX cards, FSR 3.1 on any card), Frame generation (NVIDIA DLSS + on RTX 40 and 50 cards, with a DLSS multiplier of 2x, or 3x and 4x on RTX 50 cards; FSR 3.1 on any card; it needs + DLSS or FSR 3.1 upscaling on, and the frame limit then counts the frames on screen), HDR (the game's picture tone mapped to your display, with its own grading rows, or Native, the game's own HDR; turn HDR on in Windows first) and the resolution. Those marked "after restart" take effect when the game starts again. - **Controls** in the menu has Button prompts: the game's own, or Xbox, PlayStation or keyboard and mouse ones when the build drew them (a kit with `prompt-art`). It takes effect after a restart. The keyboard and mouse prompts show diff --git a/nx2navi.ini.example b/nx2navi.ini.example index 7e4141b1..721d1b0b 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -14,6 +14,8 @@ # 36 do, while 60 does not. vk_present = immediate shows a frame as soon as it is presented, without this quantisation, # at the cost of tearing. A monitor running with variable refresh (FreeSync, G-Sync) follows the frames instead, so # there any limit inside its range is even. +# With frame generation on (framegen, [display]) the limit counts the frames on screen, generated ones included: the +# game draws the limit divided by the multiplier. # fps_limit = 60 # Where RivaTuner Statistics Server is installed and running, the limit goes into its profile for the game's # executable and RTSS does the limiting instead of the game; closing the game puts back what the profile held. @@ -313,6 +315,19 @@ # The game's own FXAA after the post effects; with DLAA on it is not needed. Changes while the game runs take effect at # once. # fxaa = 1 +# Frame generation: off, fsr3 (AMD FSR 3.1 on any GPU: one generated frame between every two the game draws, the HUD +# kept out of it) or dlss (NVIDIA DLSS Frame Generation on GeForce RTX 40 and 50 GPUs, paced with Reflex; it needs +# nx2navi_dlssg.dll and NVIDIA Streamline's files next to the executable, which the setup puts there). Both need DLSS or +# FSR 3.1 upscaling (dlaa = 1). While frame generation runs, fps_limit counts the frames on screen, generated ones +# included: the game draws the limit divided by the multiplier (2 for fsr3), so set the limit to the display's rate. +# fsr3 takes effect at once, dlss (on or off) at the next start. +# framegen = off +# 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. +# Each step adds a little latency, and on a 60 Hz display 4 leaves the game drawing 15 fps. Changes while the game runs +# take effect at once. +# framegen_multiplier = 2 # HDR output: hdr = 1 sends the picture as HDR10 through the HDR tone mapper, hdr = native as the game renders it in # its own HDR mode (both need HDR on in Windows, "Use HDR", or a Wayland compositor with colour management; without it diff --git a/runtime/cfg.c b/runtime/cfg.c index 5c283e18..46e7fed9 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -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", "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_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", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FREEZE_IDLE", "FREE_CORES", + "FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "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_PEAK_NITS", "HDR_SATURATION", "HDR_SHADOWS", "HDR_TONEMAP", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD", @@ -84,6 +84,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_CHAR_MASK", "display"}, {"FXAA", "display"}, + {"FRAMEGEN", "display"}, {"FRAMEGEN_MULTIPLIER", "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"}, @@ -135,6 +136,7 @@ static const CfgMeta metas[] = { {"DLAA", "0", "0|1", LIVE}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance|ultra_performance", LIVE}, {"DLSS_PRESET", "default", "default|k|j", LIVE}, {"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 {"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 diff --git a/runtime/cfg_probe.c b/runtime/cfg_probe.c index 83bb317b..5c1ce5f4 100644 --- a/runtime/cfg_probe.c +++ b/runtime/cfg_probe.c @@ -10,6 +10,7 @@ #include "input.h" #include "input_map.h" #include "platform/platform.h" +#include "vk_dlssg.h" static int fails; #define CHECK(c, what) do { if (!(c)) { fails++; fprintf(stderr, "FAIL cfg: %s\n", what); } } while (0) @@ -161,6 +162,24 @@ int probe_cfg(void) { CHECK(m && EQ(m->def, "default") && m->values == NULL && !m->live, "meta: audio_device is free text, default, needs a restart"); CHECK(cfg_meta("BIND_A") == NULL && cfg_meta("FPS_COUNTER") == NULL && cfg_meta("NO_SUCH") == NULL, "meta: keys the menu does not show are NULL"); + m = cfg_meta("FRAMEGEN_MULTIPLIER"); + CHECK(m && EQ(m->def, "2") && EQ(m->values, "2|3|4") && m->live, "meta: framegen_multiplier is 2|3|4, 2 by default, live"); + + // DLSS frame generation's multiplier (vk_dlssg.c): the setting clamped to what the GPU and driver allow, with the + // reason vk_dlssg_frame logs when it is not taken as it is + const char *why = ""; + CHECK(vk_dlssg_multiplier_of("3", 4, &why) == 3 && !why, "multiplier: 3 where 4x runs"); + CHECK(vk_dlssg_multiplier_of("4x", 4, &why) == 4 && !why, "multiplier: 4x reads as 4"); + CHECK(vk_dlssg_multiplier_of("4", 2, &why) == 2 && why, "multiplier: 4 where 2x is the most is 2, with a reason"); + CHECK(vk_dlssg_multiplier_of("4", 3, &why) == 3 && why, "multiplier: 4 where 3x is the most is 3, with a reason"); + CHECK(vk_dlssg_multiplier_of("9", 0, &why) == 4 && why, "multiplier: above 4 is 4, with a reason"); + CHECK(vk_dlssg_multiplier_of("1", 4, &why) == 2 && why, "multiplier: below 2 is 2, with a reason"); + CHECK(vk_dlssg_multiplier_of("fast", 4, &why) == 2 && why, "multiplier: not a number is 2, with a reason"); + CHECK(vk_dlssg_multiplier_of("3y", 4, &why) == 2 && why, "multiplier: a number with other text after it is 2"); + CHECK(vk_dlssg_multiplier_of("", 3, &why) == 2 && !why && vk_dlssg_multiplier_of(NULL, 3, NULL) == 2, + "multiplier: unset is 2"); + CHECK(vk_dlssg_multiplier_of("4", 0, &why) == 4 && !why, "multiplier: kept as it is while the GPU's limit is not known"); + CHECK(vk_dlssg_multiplier_max(NULL) == 0 && vk_dlssg_multiplier() == 1, "multiplier: 1, no limit, while DLSS-G does not run"); // cfg_reset: the key's active lines leave nx2navi.ini, the commented example stays static const char rbase[] = "[audio]\n# volume = 100\n[input]\nmotion = 0\n"; diff --git a/runtime/dlssg/nx2_sl.cpp b/runtime/dlssg/nx2_sl.cpp index 147340fc..7a7ad8a2 100644 --- a/runtime/dlssg/nx2_sl.cpp +++ b/runtime/dlssg/nx2_sl.cpp @@ -7,13 +7,16 @@ // interposer's vkCreateInstance and vkCreateDevice, which add what DLSS-G needs, and calls the interposer's // swapchain functions; everything else goes to the Vulkan loader as before. Each frame of the game sleeps in Reflex // where it starts and gets the PCL latency markers, the camera constants and the depth and motion vector tags (valid -// until the present), then the present goes through the interposer, which inserts the generated frame. +// until the present), then the present goes through the interposer, which inserts the generated frames: one, two or +// three between two rendered frames (2x, 3x or 4x), up to what the GPU and driver allow (numFramesToGenerateMax). #define NX2SL_BUILD #include #include #include +#include + #include #include #include @@ -40,7 +43,9 @@ PFun_slReflexSetOptions *p_reflex; PFun_slReflexSleep *p_sleep; PFN_vkGetInstanceProcAddr p_gipa; PFN_vkGetDeviceProcAddr p_gdpa; -int g_available = -1, g_mode = -1; +int g_available = -1; +int g_frames = -1; // the frames to generate as last set (0 off), -1 before the first nx2sl_frame +std::atomic g_max{0}; // numFramesToGenerateMax as Streamline last reported it, 0 before it did uint64_t g_presented, g_shown; void say(const char *fmt, ...) { @@ -141,7 +146,14 @@ extern "C" NX2SL_API int nx2sl_available(void) { if (p_feature(sl::kFeatureReflex, "slReflexSetOptions", f) == sl::Result::eOk) p_reflex = (PFun_slReflexSetOptions *)f; if (p_feature(sl::kFeatureReflex, "slReflexSleep", f) == sl::Result::eOk) p_sleep = (PFun_slReflexSleep *)f; if (!p_dlssg_options || !p_marker) g_available = 0; - say("dlssg: DLSS Frame Generation %s", g_available ? "available" : "not available on this GPU or driver"); + // How many frames this GPU and driver generate per rendered frame: known once the device exists (else after the + // first presents, nx2sl_present_marker) + sl::DLSSGState s{}; + if (g_available && p_dlssg_state && p_dlssg_state(sl::ViewportHandle(0), s, nullptr) == sl::Result::eOk) + g_max.store(s.numFramesToGenerateMax); + if (!g_available) say("dlssg: DLSS Frame Generation not available on this GPU or driver"); + else if (g_max.load()) say("dlssg: DLSS Frame Generation available, up to %ux", g_max.load() + 1); + else say("dlssg: DLSS Frame Generation available (its most frames per rendered frame not reported yet)"); return g_available; } @@ -182,7 +194,11 @@ extern "C" NX2SL_API int nx2sl_frame(VkCommandBuffer cb, const Nx2SlImage *depth sl::FrameToken *t = token(frame); if (!t) return 0; sl::ViewportHandle vp(0); - if (enable && depth && mv && cam) { + // never more than Streamline allows: one while it has not said + uint32_t max = g_max.load(); + int n = enable > 0 ? enable : 0; + if (n > (int)(max ? max : 1)) n = (int)(max ? max : 1); + if (n && depth && mv && cam) { sl::Constants c{}; matrix(c.cameraViewToClip, cam->view_to_clip); matrix(c.clipToCameraView, cam->clip_to_view); @@ -212,13 +228,14 @@ extern "C" NX2SL_API int nx2sl_frame(VkCommandBuffer cb, const Nx2SlImage *depth sl::ResourceTag(&rh, sl::kBufferTypeHUDLessColor, sl::ResourceLifecycle::eValidUntilPresent, &eh)}; if ((t = token(frame))) p_tag(*t, vp, tags, hudless ? 3 : 2, (sl::CommandBuffer *)cb); } - if ((enable != 0) != g_mode) { + if (n != g_frames) { sl::DLSSGOptions o{}; - o.mode = enable ? sl::DLSSGMode::eOn : sl::DLSSGMode::eOff; - o.numFramesToGenerate = 1; + o.mode = n ? sl::DLSSGMode::eOn : sl::DLSSGMode::eOff; + o.numFramesToGenerate = n ? (uint32_t)n : 1; sl::Result r = p_dlssg_options(vp, o); - say("dlssg: frame generation %s (%d), output %ux%u", enable ? "on" : "off", (int)r, out_w, out_h); - g_mode = enable != 0; + if (n) say("dlssg: frame generation on, %dx (%d generated per rendered frame) (%d), output %ux%u", n + 1, n, (int)r, out_w, out_h); + else say("dlssg: frame generation off (%d), output %ux%u", (int)r, out_w, out_h); + g_frames = n; } return 1; } @@ -229,15 +246,22 @@ extern "C" NX2SL_API void nx2sl_present_marker(int after, uint32_t frame) { p_marker(after ? sl::PCLMarker::ePresentEnd : sl::PCLMarker::ePresentStart, *t); if (!after) return; g_presented++; - // Asked after each present, numFramesActuallyPresented is what that present showed: 2 with a generated frame. + // Asked after each present, numFramesActuallyPresented is what that present showed: 2 with one generated frame, 4 + // with three. sl::DLSSGState s{}; if (!p_dlssg_state || p_dlssg_state(sl::ViewportHandle(0), s, nullptr) != sl::Result::eOk) return; g_shown += s.numFramesActuallyPresented; + if (s.numFramesToGenerateMax && s.numFramesToGenerateMax != g_max.load()) { // a 0 now and then is no news + say("dlssg: up to %ux on this GPU and driver", s.numFramesToGenerateMax + 1); + g_max.store(s.numFramesToGenerateMax); + } if (g_presented == 600 || g_presented % 3600 == 0) say("dlssg: %llu frames, %u presented per frame, status %u", (unsigned long long)g_presented, s.numFramesActuallyPresented, (unsigned)s.status); } +extern "C" NX2SL_API uint32_t nx2sl_frames_max(void) { return g_available == 1 ? g_max.load() : 0; } + extern "C" NX2SL_API uint64_t nx2sl_shown(void) { return g_shown; } extern "C" NX2SL_API void nx2sl_shutdown(void) { diff --git a/runtime/dlssg/nx2_sl.h b/runtime/dlssg/nx2_sl.h index 44dab8f8..9728e7db 100644 --- a/runtime/dlssg/nx2_sl.h +++ b/runtime/dlssg/nx2_sl.h @@ -54,10 +54,16 @@ typedef void (*PFN_nx2sl_sleep)(uint32_t frame, int hold); // The frame's submissions went out (at the game's present; with asynchronous replay, when the replay reaches it): its // render-submit-end marker. typedef void (*PFN_nx2sl_submit_end)(uint32_t frame); -// Each host present, before the present call: the frame's constants and tags, and frame generation on or off. Then +// Each host present, before the present call: the frame's constants and tags, and frame generation on or off. enable is +// the number of frames to generate between two rendered ones (0 off; 1, 2 or 3 for 2x, 3x or 4x), at most what +// nx2sl_frames_max reports; a DLL without nx2sl_frames_max (an older build) generates one for any nonzero value. Then // nx2sl_present_marker(0) before and (1) after the present. typedef int (*PFN_nx2sl_frame)(VkCommandBuffer cb, const Nx2SlImage *depth, const Nx2SlImage *mv, const Nx2SlImage *hudless, const Nx2SlCamera *cam, uint32_t out_w, uint32_t out_h, int enable, uint32_t frame); +// The most frames DLSS-G can generate between two rendered ones on this GPU and driver (Streamline's +// numFramesToGenerateMax: 1 on a GPU that does 2x, 3 on one that does 4x); 0 until Streamline reports it. Asked after +// the device exists and refreshed after each present. Optional: an older DLL lacks it, and the game then asks for one. +typedef uint32_t (*PFN_nx2sl_frames_max)(void); typedef void (*PFN_nx2sl_present_marker)(int after, uint32_t frame); // The frames the display got, generated ones included, counted after each present (on the present thread) typedef uint64_t (*PFN_nx2sl_shown)(void); diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 647d4fde..8fb59f03 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -52,6 +52,7 @@ #include "host.h" #include "layout.h" #include "platform/platform.h" +#include "vk_dlssg.h" #include "vk_fg.h" #define STEP_SAMPLES 5 @@ -199,6 +200,10 @@ static int movie_cap_hz(void) { return hz && ov_movie_live() ? hz : 0; } +// Frames on screen per game frame while frame generation runs: 2 with AMD's (vk_fg.cpp, one generated frame), the +// multiplier with NVIDIA's (vk_dlssg.c, one to three); 1 while neither generates (off, or paused for the menu). +static int generated_factor(void) { return vk_fg_generating() ? 2 : vk_dlssg_multiplier(); } + // RivaTuner Statistics Server (rtss_limit.c) gets the same limit; while it limits this process the pacer holds no // frames, so the two never stack. NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it out. uint64_t frame_step_cap_ns(void) { @@ -208,7 +213,8 @@ uint64_t frame_step_cap_ns(void) { const char *e = cfg_get("NX2NAVI_RTSS"); rtss_allowed = !(e && !strcmp(e, "0")); } - int rtss = rtss_allowed && !vk_fg_wanted(); + int rtss = rtss_allowed && !vk_fg_wanted() && !vk_dlssg_active(); + int gen = generated_factor(); int movie_hz = movie_cap_hz(); static int movie_was, movie_logs; if (!!movie_hz != movie_was) { @@ -222,14 +228,15 @@ uint64_t frame_step_cap_ns(void) { rtss_limit_want(rtss ? 0 : -1); return 0; } - // unlimited during a movie: the movie's rate, doubled for frame generation's halving below - double hz = limit_hz == -2 ? movie_hz * (vk_fg_generating() ? 2 : 1) + // unlimited during a movie: the movie's rate, times frame generation's factor for the division below + double hz = limit_hz == -2 ? movie_hz * gen : shown_hz(limit_hz ? limit_hz : gfx_window_refresh_hz()); double max = frame_free_running() ? LIMIT_FREE_MAX_HZ : LIMIT_MAX_HZ; if (hz > max) hz = max; - // Frame generation presents a generated frame between each two of the game's (vk_fg.cpp): the game is paced at - // half the limit then, so the screen shows the limit - if (vk_fg_generating()) hz /= 2; + // Frame generation presents one to three generated frames between each two of the game's: the game is paced at the + // limit over that factor (half, a third, a quarter), so the screen shows the limit. With VSync and the limit at the + // display's rate, the generated frames then fill the display's refreshes and none is dropped. + if (gen > 1) hz /= gen; if (movie_hz && hz > movie_hz) hz = movie_hz; rtss_limit_want(rtss ? (int64_t)(hz * 1000 + 0.5) : -1); if (rtss_limit_active()) return 0; diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 580f66b8..0c17468e 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -52,6 +52,7 @@ extern "C" { #include "sysinfo.h" #include "ui/menu.h" #include "ui_text.h" +#include "vk_dlssg.h" // host.h and vk.h pull in cpu.h, which is C only: the functions the menu needs from them, as declared there. void rt_log(const char *fmt, ...) __attribute__((format(printf, 1, 2))); void vk_queue_lock(void); @@ -63,7 +64,6 @@ const char *const *vk_color_preset(const char *name); int vk_hdr_wanted(void); int vk_hdr_active(void); int vk_dlaa_needs_restart(void); -int vk_dlssg_wanted(void); double frame_step_limit_hz(void); int frame_limit_adjustable(void); int rtss_limit_state(void); @@ -882,11 +882,32 @@ static void row_hdr(void) { opt_slider("NX2NAVI_HDR_GRADE", UI_MENU_HDR_GRADE, 0, 100, UI_MENU_HDR_GRADE_DESC, 5, "%.0f %%"); } +// DLSS frame generation's multiplier (NX2NAVI_FRAMEGEN_MULTIPLIER, vk_dlssg.c, at once): 2x, 3x or 4x as far as this +// GPU and driver allow (vk_dlssg_multiplier_max, from Streamline). While DLSS's frame generation runs, the row lists only +// those and shows the value in effect, a higher setting clamped to it, with a note saying why the rest are missing; +// before the restart that starts it, or before Streamline reported its limit, it lists all three. Left out while it is +// set but does not run: the frame generation row says why. +static void row_framegen_mult(bool restart) { + if (!restart && !vk_dlssg_active()) return; + int old_dll = 0, max = restart ? 0 : vk_dlssg_multiplier_max(&old_dll); + const char *note = nullptr; + static char upto[64]; + if (max == 2) note = menu_text(old_dll ? UI_MENU_FRAMEGEN_MULT_OLD_DLL : UI_MENU_FRAMEGEN_MULT_RTX50); + else if (max == 3) snprintf(upto, sizeof upto, menu_text(UI_MENU_FRAMEGEN_MULT_UPTO), max), note = upto; + if (!max) max = 4; // not known yet: every multiplier, the setting as it is + static const char *const values[] = {"2", "3", "4"}; + static const char *const names[] = {"2x", "3x", "4x"}; + int n = max - 1, cur = vk_dlssg_multiplier_of(cfg_value("NX2NAVI_FRAMEGEN_MULTIPLIER"), max, nullptr) - 2; + int nv = opt_choice_index("NX2NAVI_FRAMEGEN_MULTIPLIER", UI_MENU_FRAMEGEN_MULT, names, n, cur, 0, + UI_MENU_FRAMEGEN_MULT_DESC, note); + if (nv >= 0 && nv < n) opt_apply("NX2NAVI_FRAMEGEN_MULTIPLIER", 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 - const bool dlss = dlss_offered(); - if (!cfg_bool("NX2NAVI_DLAA")) opt_disable_next(dlss ? UI_MENU_FRAMEGEN_NEEDS : UI_MENU_FRAMEGEN_NEEDS_FSR); + const bool dlss = dlss_offered(), upscaled = cfg_bool("NX2NAVI_DLAA"); + if (!upscaled) opt_disable_next(dlss ? UI_MENU_FRAMEGEN_NEEDS : UI_MENU_FRAMEGEN_NEEDS_FSR); static const char *const values[] = {"off", "fsr3", "dlss"}; const char *names[] = {menu_text(UI_MENU_FRAMEGEN_OFF), "AMD FSR 3.1", "NVIDIA DLSS"}; const char *v = cfg_value("NX2NAVI_FRAMEGEN"); @@ -896,10 +917,15 @@ static void row_framegen(void) { int n = dlss || cur == 2 ? 3 : 2; // 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); + UiTextId note = restart ? UI_MENU_AFTER_RESTART : dlss ? UI_MENU_FRAMEGEN_NOTE : UI_MENU_FRAMEGEN_NOTE_FSR; + // 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; + 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, cur, 0, - dlss ? UI_MENU_FRAMEGEN_DESC : UI_MENU_FRAMEGEN_DESC_FSR, - menu_text(restart ? UI_MENU_AFTER_RESTART : dlss ? UI_MENU_FRAMEGEN_NOTE : UI_MENU_FRAMEGEN_NOTE_FSR)); + dlss ? UI_MENU_FRAMEGEN_DESC : UI_MENU_FRAMEGEN_DESC_FSR, menu_text(note)); if (nv >= 0 && nv < n) opt_apply("NX2NAVI_FRAMEGEN", values[nv]); + else if (cur == 2 && upscaled && dlss) row_framegen_mult(restart); } // The picture's height (widescreen.c picture_height): 1080, the game's 1440 or more. @@ -1381,6 +1407,9 @@ static void perf_picture(const MenuTarget *t, PerfPanel *p) { snprintf(v[8], sizeof v[8], "%s", menu_text(!strcmp(hdr, "native") ? UI_MENU_HDR_NATIVE : cfg_bool("NX2NAVI_HDR") ? UI_MENU_HDR_TONEMAPPED : UI_MENU_FRAMEGEN_OFF)); const char *fg = cfg_value("NX2NAVI_FRAMEGEN"); snprintf(v[9], sizeof v[9], "%s", !strcmp(fg, "fsr3") ? "AMD FSR 3.1" : !strcmp(fg, "dlss") ? "NVIDIA DLSS" : menu_text(UI_MENU_FRAMEGEN_OFF)); + if (!strcmp(fg, "dlss") && vk_dlssg_active()) // its multiplier as set, clamped to what this GPU allows + snprintf(v[9], sizeof v[9], "NVIDIA DLSS %dx", + vk_dlssg_multiplier_of(cfg_value("NX2NAVI_FRAMEGEN_MULTIPLIER"), vk_dlssg_multiplier_max(nullptr), nullptr)); // The 3D scene's size is known only where resolution.c sets it (the builds with its patch sets); Ocarina sizes its // own scene dynamically and res_sizes would report a fixed 1600x900, so the line is left out p->scene_known = res_adjustable() != 0; diff --git a/runtime/ui_text.c b/runtime/ui_text.c index b46b3bae..624dc831 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -521,6 +521,17 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_FRAMEGEN_NEEDS_FSR] = "choose an FSR 3.1 mode under Upscaling first", [UI_MENU_FRAMEGEN_NOTE_FSR] = "needs FSR 3.1", [UI_MENU_FRAMEGEN_DESC_FSR] = "Adds generated frames for smoother motion. Needs FSR 3.1.", + [UI_MENU_FRAMEGEN_MULT] = "DLSS multiplier", + [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 " + "to your display's rate. 2x needs a GeForce RTX 40 GPU or newer, 3x and 4x an RTX " + "50.", + [UI_MENU_FRAMEGEN_MULT_RTX50] = "3x and 4x need an RTX 50 GPU", + [UI_MENU_FRAMEGEN_MULT_UPTO] = "up to %dx on this GPU and driver", + [UI_MENU_FRAMEGEN_MULT_OLD_DLL] = "2x only: update the DLSS frame generation files", + [UI_MENU_FRAMEGEN_OFF_GPU] = "in use: off, needs an RTX 40 GPU or newer", + [UI_MENU_FRAMEGEN_OFF_FILES] = "in use: off, its files are missing", [UI_MENU_GAME_RES] = "Resolution", [UI_MENU_GAME_RES_DESC] = "Height of the game's picture. Higher is sharper but costs more GPU power.", [UI_MENU_GAME_RES_NATIVE] = "Native", diff --git a/runtime/ui_text.h b/runtime/ui_text.h index cb2ed3db..3e07493f 100644 --- a/runtime/ui_text.h +++ b/runtime/ui_text.h @@ -255,6 +255,13 @@ typedef enum { // the same rows' texts on a GPU DLSS does not run on (not NVIDIA): the menu leaves DLSS out there UI_MENU_UPSCALING_DESC_FSR, UI_MENU_FRAMEGEN_DESC_FSR, UI_MENU_FRAMEGEN_NOTE_FSR, UI_MENU_FRAMEGEN_NEEDS_FSR, + // 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, + 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 + UI_MENU_FRAMEGEN_OFF_GPU, UI_MENU_FRAMEGEN_OFF_FILES, UI_TEXT_COUNT } UiTextId; diff --git a/runtime/vk_dlssg.c b/runtime/vk_dlssg.c index 536cf793..3017248b 100644 --- a/runtime/vk_dlssg.c +++ b/runtime/vk_dlssg.c @@ -7,6 +7,12 @@ // DLSS-G needs; the present path calls Streamline's swapchain functions (gfx_present.c), and every present records the // frame's constants and tags first (vk_dlssg_frame). Generation pauses while the menu is open. // +// Multi frame generation: NX2NAVI_FRAMEGEN_MULTIPLIER (2, 3 or 4, live) is how many frames the display gets per +// rendered frame, so one, two or three generated ones. Streamline says how many this GPU and driver can generate +// (numFramesToGenerateMax: one where only 2x runs, three on an RTX 50); a higher setting is clamped to it with a log +// line. frame_rate.c divides the frame limit by the multiplier in effect (vk_dlssg_multiplier), so the display gets the +// limit and the game renders a half, a third or a quarter of it. +// // DLSS-G needs Reflex, whose low-latency mode paces the game (NX2NAVI_REFLEX, default 1; 0 off, 2 on with boost): the // game's frame sleeps in Reflex where it starts (its acquire of a window image), and each frame carries the latency // markers from that start to the host present, all with the frame's number, the count of the game's presents up to it. @@ -28,7 +34,10 @@ static struct { int wanted, loaded, active; + int fail; // DLSSG_NO_FILES or DLSSG_NO_GPU when it was wanted and does not run (vk_dlssg_status) int mode, pending; // generation as the driver has it, and the frames since the host asked for the other state + int shown_mult; // vk_dlssg_multiplier: the frames per game frame as last handed to the DLL (1 while off) + PFN_nx2sl_frames_max frames_max; // NULL with an older DLL, which generates one frame PFN_nx2sl_init init; PFN_nx2sl_instance_proc gipa; PFN_nx2sl_device_proc gdpa; @@ -49,7 +58,7 @@ static struct { PFN_vkGetSwapchainImagesKHR images; PFN_vkAcquireNextImageKHR acquire; PFN_vkQueuePresentKHR present; -} S = {.wanted = -1}; +} S = {.wanted = -1, .fail = DLSSG_NO_FILES, .shown_mult = 1}; static void sl_line(const char *line) { rt_log("%s", line); } @@ -101,6 +110,8 @@ int vk_dlssg_early(void) { S.shown = (PFN_nx2sl_shown)(void *)GetProcAddress(m, "nx2sl_shown"); S.shutdown = (PFN_nx2sl_shutdown)(void *)GetProcAddress(m, "nx2sl_shutdown"); S.reflex = (PFN_nx2sl_reflex)(void *)GetProcAddress(m, "nx2sl_reflex"); + S.frames_max = (PFN_nx2sl_frames_max)(void *)GetProcAddress(m, "nx2sl_frames_max"); // optional: 2x only without + if (!S.frames_max) rt_log("dlssg: nx2navi_dlssg.dll is older than multi frame generation: 2x at most"); 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"); @@ -141,7 +152,11 @@ VkResult vk_dlssg_create_device(VkInstance inst, VkPhysicalDevice pd, const VkDe } void vk_dlssg_device_ready(VkDevice dev) { - if (S.loaded <= 0 || !S.available()) return; + if (S.loaded <= 0) return; + if (!S.available()) { + S.fail = DLSSG_NO_GPU; + return; + } PFN_vkGetDeviceProcAddr g = S.gdpa(); S.create_sc = (PFN_vkCreateSwapchainKHR)g(dev, "vkCreateSwapchainKHR"); S.destroy_sc = (PFN_vkDestroySwapchainKHR)g(dev, "vkDestroySwapchainKHR"); @@ -155,6 +170,60 @@ void vk_dlssg_device_ready(VkDevice dev) { int vk_dlssg_active(void) { return S.active; } +int vk_dlssg_status(void) { + if (S.active) return DLSSG_RUNS; + return vk_dlssg_wanted() ? S.fail : DLSSG_NOT_WANTED; +} + +int vk_dlssg_multiplier_of(const char *value, int max, const char **why) { + const char *w = NULL; + long n = 2; + if (value && *value) { + char *end; + n = strtol(value, &end, 10); + if (*end == 'x' || *end == 'X') end++; + if (*end || end == value || n < 2) w = "is not 2, 3 or 4", n = 2; + else if (n > 4) w = "is above 4, the most there is", n = 4; + } + if (max >= 2 && n > max) w = "is above what this GPU and driver allow", n = max; + if (why) *why = w; + return (int)n; +} + +// Streamline's limit as a multiplier: 2 to 4, or 0 while it has reported none (the first frames). An older DLL +// generates one frame: 2. +static int gpu_multiplier_max(int *old_dll) { + if (old_dll) *old_dll = !S.frames_max; + if (!S.frames_max) return 2; + uint32_t f = S.frames_max(); + return !f ? 0 : f >= 3 ? 4 : (int)f + 1; +} + +int vk_dlssg_multiplier_max(int *old_dll) { + if (old_dll) *old_dll = 0; + return S.active ? gpu_multiplier_max(old_dll) : 0; +} + +int vk_dlssg_multiplier(void) { return S.active ? __atomic_load_n(&S.shown_mult, __ATOMIC_RELAXED) : 1; } + +// The multiplier the setting gives here (present thread), clamped to the GPU's limit; one log line each time the value +// or the limit changes and the value is not taken as it is. Until Streamline reports its limit, one generated frame, +// and no log line about the GPU yet. +static int multiplier_wanted(void) { + static char last[24]; + static int last_max = -1, m = 2; + const char *value = cfg_value("NX2NAVI_FRAMEGEN_MULTIPLIER"); + int max = gpu_multiplier_max(NULL); + if (max == last_max && !strncmp(value, last, sizeof last - 1)) return m; + snprintf(last, sizeof last, "%s", value); + last_max = max; + const char *why; + int n = vk_dlssg_multiplier_of(value, max, &why); + if (why) rt_log("dlssg: framegen_multiplier = %s %s (up to %dx here): %dx", value, why, max ? max : 4, n); + m = max ? n : 2; + return m; +} + // vk_device.c: while Streamline is loaded, every device entry point the runtime fetches goes through its interposer, so // its plugins see the calls they hook (Reflex's frame timing and DLSS-G's presents). NULL keeps the loader's. PFN_vkGetDeviceProcAddr vk_dlssg_dpa(void) { return S.loaded > 0 && S.gdpa ? S.gdpa() : NULL; } @@ -222,7 +291,8 @@ void vk_dlssg_frame(VkCommandBuffer cb, VkExtent2D ext) { if (want == S.mode) S.pending = 0; else if (++S.pending >= FG_SETTLE_FRAMES) { S.mode = want; S.pending = 0; } else want = S.mode; // hold the state it has until the new one stays - int on = want; + int on = want, mult = multiplier_wanted(); + __atomic_store_n(&S.shown_mult, on ? mult : 1, __ATOMIC_RELAXED); // the pacer's divisor (frame_rate.c) 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, @@ -238,7 +308,8 @@ void vk_dlssg_frame(VkCommandBuffer cb, VkExtent2D ext) { k.mv_scale[0] = 1.0f / (float)d.width, k.mv_scale[1] = 1.0f / (float)d.height; // render pixels k.reset = reset; } - S.frame(cb, have ? &zd : NULL, have ? &zm : NULL, NULL, have ? &k : NULL, ext.width, ext.height, on, S.cur); + // the frames to generate between two rendered ones: the multiplier less the rendered one, 0 off + S.frame(cb, have ? &zd : NULL, have ? &zm : NULL, NULL, have ? &k : NULL, ext.width, ext.height, on ? mult - 1 : 0, S.cur); } void vk_dlssg_release(void) { diff --git a/runtime/vk_dlssg.h b/runtime/vk_dlssg.h index c6038cb3..ac191860 100644 --- a/runtime/vk_dlssg.h +++ b/runtime/vk_dlssg.h @@ -29,3 +29,22 @@ void vk_dlssg_submitted(uint64_t frame); void vk_dlssg_present_frame(uint64_t frame); // the present thread's next blit and present are of this frame uint64_t vk_dlssg_shown(void); // the frames the display got, generated ones included (present thread); 0 when off void vk_dlssg_release(void); + +// The multiplier: the frames the display gets per rendered frame, 2, 3 or 4 (one to three generated), set by +// NX2NAVI_FRAMEGEN_MULTIPLIER and applied at once. +// What a setting value gives where the GPU and driver allow up to max (2 to 4): the value when it is 2, 3 or 4 ("3x" +// too) and at most max, else the nearest that is; unset or empty, 2. *why (may be NULL): the reason the value was not +// taken as it is, for the log, or NULL. +int vk_dlssg_multiplier_of(const char *value, int max, const char **why); +// The highest multiplier while DLSS-G runs: 2 to 4 from Streamline's numFramesToGenerateMax; 0 while DLSS-G does not +// run, or before Streamline reported its limit (the first frames; one frame is generated until then). *old_dll (may be +// NULL): 1 when the limit of 2 is nx2navi_dlssg.dll's, a build older than its nx2sl_frames_max, not the GPU's. +int vk_dlssg_multiplier_max(int *old_dll); +// The frames the display gets per game frame now: the multiplier while generation runs, 1 while it is off or paused +// (the menu). frame_rate.c paces the game at the frame limit over it. Any thread. +int vk_dlssg_multiplier(void); +// Why DLSS-G runs or not: DLSSG_NOT_WANTED (NX2NAVI_FRAMEGEN was not dlss at start, or no DLSS or FSR 3.1 upscaling), +// DLSSG_NO_FILES (nx2navi_dlssg.dll, Streamline or its swapchain hooks missing or failing), DLSSG_NO_GPU (Streamline +// loaded, but DLSS-G does not run on this GPU or driver). +enum { DLSSG_NOT_WANTED, DLSSG_RUNS, DLSSG_NO_FILES, DLSSG_NO_GPU }; +int vk_dlssg_status(void);