diff --git a/runtime/cfg.c b/runtime/cfg.c index 208c446f..d69e1263 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -41,7 +41,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_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", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", - "FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FREEZE_IDLE", "FREE_CORES", + "FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLUSH_SPAN", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FREEZE_IDLE", "FREE_CORES", "FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE", "HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD", "LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_SOUND_VOLUME", "MENU_TEST", @@ -119,7 +119,7 @@ static const CfgMeta metas[] = { {"UPSCALER", "fsr", "fsr|bilinear", LIVE}, {"SHARPNESS", "80", "0..100", LIVE}, {"DLAA", "0", "0|1", RESTART}, {"DLSS", "dlaa", "dlaa|quality|balanced|performance|ultra_performance", LIVE}, {"DLSS_PRESET", "default", "default|k|j", LIVE}, {"TAA", "dlss", "dlss|fsr3", RESTART}, // vk_dlaa.c fsr3 - {"FRAMEGEN", "off", "off|fsr3", RESTART}, // vk_fg.cpp + {"FRAMEGEN", "off", "off|fsr3|dlss", RESTART}, // vk_fg.cpp, vk_dlssg.c {"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", "1440|1800|2160|2880|window", RESTART}, // widescreen.c: the picture's height diff --git a/runtime/dlssg/nx2_sl.cpp b/runtime/dlssg/nx2_sl.cpp new file mode 100644 index 00000000..ebeb78b2 --- /dev/null +++ b/runtime/dlssg/nx2_sl.cpp @@ -0,0 +1,218 @@ +// nx2navi_dlssg.dll: DLSS Frame Generation through NVIDIA Streamline, behind the C interface of nx2_sl.h. Built with +// MSVC against the Streamline SDK's headers (tools/build_dlssg.py; the SDK stays outside the repository), so its C++ +// types have the ABI of Streamline's own DLLs. sl.interposer.dll and its plugins are loaded from the game's folder at +// run time; this DLL links none of them. +// +// Manual hooking (the SDK's ProgrammingGuideManualHooking): the game creates its instance and device through the +// 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 gets a frame token, 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. +#define NX2SL_BUILD +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include "nx2_sl.h" + +namespace { + +HMODULE g_mod; +Nx2SlLog g_log; +PFun_slInit *p_init; +PFun_slShutdown *p_shutdown; +PFun_slIsFeatureLoaded *p_loaded; +PFun_slSetTagForFrame *p_tag; +PFun_slSetConstants *p_constants; +PFun_slGetNewFrameToken *p_token; +PFun_slGetFeatureFunction *p_feature; +PFun_slDLSSGSetOptions *p_dlssg_options; +PFun_slDLSSGGetState *p_dlssg_state; +PFun_slPCLSetMarker *p_marker; +PFun_slReflexSetOptions *p_reflex; +PFN_vkGetInstanceProcAddr p_gipa; +PFN_vkGetDeviceProcAddr p_gdpa; +sl::FrameToken *g_frame; +int g_available = -1, g_mode = -1; +uint64_t g_presented; +uint32_t g_generated; + +void say(const char *fmt, ...) { + if (!g_log) return; + char s[512]; + va_list a; + va_start(a, fmt); + vsnprintf(s, sizeof s, fmt, a); + va_end(a); + g_log(s); +} + +void sl_log(sl::LogType type, const char *msg) { + if (type == sl::LogType::eInfo) return; // only warnings and errors reach the game's log + static int n; + if (n++ < 200) say("dlssg: streamline %s: %s", type == sl::LogType::eError ? "error" : "warning", msg); +} + +template T *proc(const char *name) { return reinterpret_cast(GetProcAddress(g_mod, name)); } + +void matrix(sl::float4x4 &m, const float *v) { + for (int r = 0; r < 4; r++) m[r] = sl::float4(v[r * 4], v[r * 4 + 1], v[r * 4 + 2], v[r * 4 + 3]); +} + +sl::Resource resource(const Nx2SlImage *i) { + sl::Resource r(sl::ResourceType::eTex2d, (void *)i->image, nullptr, (void *)i->view, (uint32_t)i->layout); + r.width = i->width; + r.height = i->height; + r.nativeFormat = (uint32_t)i->format; + r.mipLevels = 1; + r.arrayLayers = 1; + r.usage = (uint32_t)i->usage; + return r; +} + +} // namespace + +extern "C" NX2SL_API int nx2sl_version(void) { return NX2SL_VERSION; } + +extern "C" NX2SL_API int nx2sl_init(const wchar_t *dir, Nx2SlLog log) { + g_log = log; + wchar_t path[MAX_PATH]; + swprintf(path, MAX_PATH, L"%ls\\sl.interposer.dll", dir); + g_mod = LoadLibraryW(path); + if (!g_mod) return say("dlssg: sl.interposer.dll not found next to the game: DLSS Frame Generation off"), 0; + p_init = proc("slInit"); + p_shutdown = proc("slShutdown"); + p_loaded = proc("slIsFeatureLoaded"); + p_tag = proc("slSetTagForFrame"); + p_constants = proc("slSetConstants"); + p_token = proc("slGetNewFrameToken"); + p_feature = proc("slGetFeatureFunction"); + p_gipa = reinterpret_cast(GetProcAddress(g_mod, "vkGetInstanceProcAddr")); + p_gdpa = reinterpret_cast(GetProcAddress(g_mod, "vkGetDeviceProcAddr")); + if (!p_init || !p_tag || !p_constants || !p_token || !p_feature || !p_gipa || !p_gdpa) + return say("dlssg: sl.interposer.dll lacks its entry points"), 0; + static const sl::Feature features[] = {sl::kFeatureDLSS_G, sl::kFeatureReflex, sl::kFeaturePCL}; + const wchar_t *paths[] = {dir}; + sl::Preferences p{}; + p.showConsole = false; + p.logLevel = sl::LogLevel::eDefault; + p.pathsToPlugins = paths; + p.numPathsToPlugins = 1; + p.pathToLogsAndData = dir; + p.logMessageCallback = sl_log; + // the game tracks its own command buffer state; no plugin downloads at run time + p.flags = sl::PreferenceFlags::eDisableCLStateTracking | sl::PreferenceFlags::eUseManualHooking; + p.featuresToLoad = features; + p.numFeaturesToLoad = 3; + p.applicationId = 231313132; // the id NVIDIA's samples use for builds without an issued one (as vk_dlaa.c's NGX) + p.engine = sl::EngineType::eCustom; + p.renderAPI = sl::RenderAPI::eVulkan; + sl::Result r = p_init(p, sl::kSDKVersion); + if (r != sl::Result::eOk) return say("dlssg: slInit failed (%d): DLSS Frame Generation off", (int)r), 0; + say("dlssg: Streamline up"); + return 1; +} + +extern "C" NX2SL_API PFN_vkGetInstanceProcAddr nx2sl_instance_proc(void) { return p_gipa; } +extern "C" NX2SL_API PFN_vkGetDeviceProcAddr nx2sl_device_proc(void) { return p_gdpa; } + +extern "C" NX2SL_API int nx2sl_available(void) { + if (g_available >= 0) return g_available; + bool loaded = false; + g_available = p_loaded && p_loaded(sl::kFeatureDLSS_G, loaded) == sl::Result::eOk && loaded; + void *f = nullptr; + if (g_available && p_feature(sl::kFeatureDLSS_G, "slDLSSGSetOptions", f) == sl::Result::eOk) p_dlssg_options = (PFun_slDLSSGSetOptions *)f; + if (g_available && p_feature(sl::kFeatureDLSS_G, "slDLSSGGetState", f) == sl::Result::eOk) p_dlssg_state = (PFun_slDLSSGGetState *)f; + if (p_feature(sl::kFeaturePCL, "slPCLSetMarker", f) == sl::Result::eOk) p_marker = (PFun_slPCLSetMarker *)f; + if (p_feature(sl::kFeatureReflex, "slReflexSetOptions", f) == sl::Result::eOk) p_reflex = (PFun_slReflexSetOptions *)f; + if (!p_dlssg_options || !p_marker) g_available = 0; + if (g_available && p_reflex) { // DLSS-G needs Reflex's low-latency mode + sl::ReflexOptions o{}; + o.mode = sl::ReflexMode::eLowLatency; + p_reflex(o); + } + say("dlssg: DLSS Frame Generation %s", g_available ? "available" : "not available on this GPU or driver"); + return g_available; +} + +extern "C" NX2SL_API int 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) { + if (g_available != 1) return 0; + if (p_token(g_frame, nullptr) != sl::Result::eOk || !g_frame) return 0; + p_marker(sl::PCLMarker::eSimulationStart, *g_frame); + p_marker(sl::PCLMarker::eSimulationEnd, *g_frame); + p_marker(sl::PCLMarker::eRenderSubmitStart, *g_frame); + sl::ViewportHandle vp(0); + if (enable && depth && mv && cam) { + sl::Constants c{}; + matrix(c.cameraViewToClip, cam->view_to_clip); + matrix(c.clipToCameraView, cam->clip_to_view); + matrix(c.clipToPrevClip, cam->clip_to_prev_clip); + matrix(c.prevClipToClip, cam->prev_clip_to_clip); + c.jitterOffset = sl::float2(cam->jitter[0], cam->jitter[1]); + c.mvecScale = sl::float2(cam->mv_scale[0], cam->mv_scale[1]); + c.cameraPinholeOffset = sl::float2(0, 0); + c.cameraPos = sl::float3(cam->pos[0], cam->pos[1], cam->pos[2]); + c.cameraUp = sl::float3(cam->up[0], cam->up[1], cam->up[2]); + c.cameraRight = sl::float3(cam->right[0], cam->right[1], cam->right[2]); + c.cameraFwd = sl::float3(cam->fwd[0], cam->fwd[1], cam->fwd[2]); + c.cameraNear = cam->znear; + c.cameraFar = cam->zfar; + c.cameraFOV = cam->fov_y; + c.cameraAspectRatio = cam->aspect; + c.depthInverted = sl::Boolean::eFalse; + c.cameraMotionIncluded = sl::Boolean::eTrue; + c.motionVectors3D = sl::Boolean::eFalse; + c.reset = cam->reset ? sl::Boolean::eTrue : sl::Boolean::eFalse; + p_constants(c, *g_frame, vp); + sl::Resource rd = resource(depth), rm = resource(mv), rh = hudless ? resource(hudless) : sl::Resource{}; + sl::Extent ed{0, 0, depth->width, depth->height}, em{0, 0, mv->width, mv->height}; + sl::Extent eh{0, 0, hudless ? hudless->width : 0, hudless ? hudless->height : 0}; + sl::ResourceTag tags[3] = {sl::ResourceTag(&rd, sl::kBufferTypeDepth, sl::ResourceLifecycle::eValidUntilPresent, &ed), + sl::ResourceTag(&rm, sl::kBufferTypeMotionVectors, sl::ResourceLifecycle::eValidUntilPresent, &em), + sl::ResourceTag(&rh, sl::kBufferTypeHUDLessColor, sl::ResourceLifecycle::eValidUntilPresent, &eh)}; + p_tag(*g_frame, vp, tags, hudless ? 3 : 2, (sl::CommandBuffer *)cb); + } + if ((enable != 0) != g_mode) { + sl::DLSSGOptions o{}; + o.mode = enable ? sl::DLSSGMode::eOn : sl::DLSSGMode::eOff; + o.numFramesToGenerate = 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; + } + p_marker(sl::PCLMarker::eRenderSubmitEnd, *g_frame); + return 1; +} + +extern "C" NX2SL_API void nx2sl_present_marker(int after) { + if (g_available != 1 || !g_frame) return; + p_marker(after ? sl::PCLMarker::ePresentEnd : sl::PCLMarker::ePresentStart, *g_frame); + if (after) { + g_presented++; + if (p_dlssg_state && (g_presented == 600 || g_presented % 3600 == 0)) { + sl::DLSSGState s{}; + if (p_dlssg_state(sl::ViewportHandle(0), s, nullptr) == sl::Result::eOk) { + g_generated = s.numFramesActuallyPresented; + say("dlssg: %llu frames, %u presented per frame, status %u", (unsigned long long)g_presented, + s.numFramesActuallyPresented, (unsigned)s.status); + } + } + } +} + +extern "C" NX2SL_API void nx2sl_stats(uint64_t *presented, uint32_t *generated_per_frame) { + *presented = g_presented, *generated_per_frame = g_generated; +} + +extern "C" NX2SL_API void nx2sl_shutdown(void) { + if (p_shutdown && g_available >= 0) p_shutdown(); +} diff --git a/runtime/dlssg/nx2_sl.h b/runtime/dlssg/nx2_sl.h new file mode 100644 index 00000000..908b3674 --- /dev/null +++ b/runtime/dlssg/nx2_sl.h @@ -0,0 +1,56 @@ +// The C interface of nx2navi_dlssg.dll (runtime/dlssg/nx2_sl.cpp), which runtime/vk_dlssg.c loads for DLSS Frame +// Generation. The DLL is built with MSVC against NVIDIA's Streamline SDK (its C++ types have MSVC's ABI); nothing of +// the SDK is in this repository, and the game itself is built without it. +#pragma once +#include +#include + +#define NX2SL_VERSION 1 + +typedef struct { + VkImage image; + VkImageView view; + VkFormat format; + uint32_t width, height; + VkImageLayout layout; // the layout it is in when Streamline reads it (at the present) + VkImageUsageFlags usage; +} Nx2SlImage; + +typedef struct { + float view_to_clip[16], clip_to_view[16], clip_to_prev_clip[16], prev_clip_to_clip[16]; // row-major + float pos[3], up[3], right[3], fwd[3]; + float znear, zfar, fov_y, aspect; + float jitter[2]; // render pixels + float mv_scale[2]; // what makes the motion vectors fractions of the render size + int reset; +} Nx2SlCamera; + +typedef void (*Nx2SlLog)(const char *line); + +#ifdef __cplusplus +extern "C" { +#endif +#ifdef NX2SL_BUILD +#define NX2SL_API __declspec(dllexport) +#else +#define NX2SL_API +#endif +typedef int (*PFN_nx2sl_version)(void); +// slInit with DLSS-G, Reflex and PCL for Vulkan; dir: the folder of sl.interposer.dll and the plugins (the game's) +typedef int (*PFN_nx2sl_init)(const wchar_t *dir, Nx2SlLog log); +// The interposer's proc address functions: vkCreateInstance and vkCreateDevice through them add what DLSS-G needs +// (extensions, features, queues) and tell Streamline the device; the swapchain functions through them are its hooks. +typedef PFN_vkGetInstanceProcAddr (*PFN_nx2sl_instance_proc)(void); +typedef PFN_vkGetDeviceProcAddr (*PFN_nx2sl_device_proc)(void); +// 1 when DLSS-G loaded and supports this GPU (after the device exists) +typedef int (*PFN_nx2sl_available)(void); +// Each present, before the present call: a new frame, its latency markers up to the render's end, its constants and +// tags, and frame generation on or off. 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); +typedef void (*PFN_nx2sl_present_marker)(int after); +typedef void (*PFN_nx2sl_stats)(uint64_t *presented, uint32_t *generated_per_frame); +typedef void (*PFN_nx2sl_shutdown)(void); +#ifdef __cplusplus +} +#endif diff --git a/runtime/gfx_present.c b/runtime/gfx_present.c index 68420af4..538d7815 100644 --- a/runtime/gfx_present.c +++ b/runtime/gfx_present.c @@ -25,6 +25,8 @@ #include "ui/menu.h" #include "vk.h" #include "vk_fg.h" +#include "vk_dlaa.h" +#include "vk_dlssg.h" #define FRAMES 2 #define MAX_IMAGES 16 @@ -290,7 +292,8 @@ static void flip_locked(void) { .pWaitSemaphores = &g_render_done[g_flip_idx], .swapchainCount = 1, .pSwapchains = &g_sc, .pImageIndices = &g_flip_idx}; vk_queue_lock(); - VkResult r = vk_fg_active() ? vk_fg_present(&pi) : vkQueuePresentKHR(vk_ctx()->gfx_queue, &pi); + VkResult r = vk_dlssg_active() ? vk_dlssg_present(vk_ctx()->gfx_queue, &pi) + : vk_fg_active() ? vk_fg_present(&pi) : 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); @@ -354,7 +357,8 @@ static int create_swapchain(void) { old = g_sc = VK_NULL_HANDLE; r = vk_fg_create_swapchain(&ci, &g_sc); } - if (r != VK_SUCCESS) r = vkCreateSwapchainKHR(c->device, &ci, NULL, &g_sc); + if (r != VK_SUCCESS && vk_dlssg_active()) r = vk_dlssg_create_swapchain(c->device, &ci, &g_sc); // Streamline's hook + else if (r != VK_SUCCESS) r = vkCreateSwapchainKHR(c->device, &ci, NULL, &g_sc); if (r != VK_SUCCESS) { rt_log("gfx: vkCreateSwapchainKHR failed (%d)", (int)r); g_sc = old; @@ -366,7 +370,8 @@ static int create_swapchain(void) { if (g_sc_view[i]) vkDestroyImageView(c->device, g_sc_view[i], NULL); g_sc_view[i] = VK_NULL_HANDLE; } - if (old) vkDestroySwapchainKHR(c->device, old, NULL); + if (old && vk_dlssg_active()) vk_dlssg_destroy_swapchain(c->device, old); + else if (old) vkDestroySwapchainKHR(c->device, old, NULL); g_sc_fmt = sf.format; g_sc_hdr = hdr; vk_hdr_set_active(hdr); @@ -379,7 +384,8 @@ static int create_swapchain(void) { g_sc_n = n < MAX_IMAGES ? n : MAX_IMAGES; memset(g_sc_img, 0, sizeof g_sc_img); vk_fg_get_images(g_sc, &g_sc_n, g_sc_img); - } else vkGetSwapchainImagesKHR(c->device, g_sc, &g_sc_n, g_sc_img); + } else if (vk_dlssg_active()) vk_dlssg_get_images(c->device, g_sc, &g_sc_n, g_sc_img); + else vkGetSwapchainImagesKHR(c->device, g_sc, &g_sc_n, g_sc_img); VkSemaphoreCreateInfo semi = {.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO}; for (uint32_t i = 0; i < g_sc_n; i++) { vkCreateSemaphore(c->device, &semi, NULL, &g_render_done[i]); @@ -730,6 +736,103 @@ static int movie_overlay(VkCommandBuffer cb, int slot, VkImage dst) { return 1; } +// The game image's crop into dst at `to`: colour adjustment (or the HDR10 encode), then FSR or the bilinear blit; dst in +// TRANSFER_DST_OPTIMAL. Returns vk_fsr_record's result (0: the blit scaled it). +static int put_picture(VkCommandBuffer cb, unsigned slot, VkImage src, VkFormat fmt, int w, int h, const int rect[4], VkImage dst, + const int to[4], int general) { + int cw = rect[2], ch = rect[3]; + // Colour adjustment (NX2NAVI_COLOR_*, vk_color.c), or on an HDR10 swapchain the PQ encode (vk_hdr.c; the adjustment is + // for SDR): a copy of the crop takes the game image's place below + VkImage graded = !general ? VK_NULL_HANDLE : g_sc_hdr ? vk_hdr_encode(cb, slot, src, fmt, rect) + : vk_color_record(cb, slot, src, fmt, rect); + const int grect[4] = {0, 0, cw, ch}; + VkImage psrc = graded ? graded : src; + VkFormat pfmt = graded ? VK_FORMAT_R16G16B16A16_SFLOAT : fmt; + const int *prect = graded ? grect : rect; + // FSR (NX2NAVI_UPSCALER): EASU when the picture's rectangle is larger than the crop, the shrink and RCAS when it is + // smaller; else the bilinear blit + int fsr = general ? vk_fsr_record(cb, slot, psrc, pfmt, graded ? cw : w, graded ? ch : h, prect, dst, to) : 0; + if (!fsr && src) { + if (general) // a GENERAL source (vk_texture.c images): earlier submissions' writes become visible to the blit here + sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); + VkImageBlit region = { + .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .srcOffsets = {{prect[0], prect[1], 0}, {prect[0] + cw, prect[1] + ch, 1}}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{to[0], to[1], 0}, {to[0] + to[2], to[1] + to[3], 1}}}; + vkCmdBlitImage(cb, psrc, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_LINEAR); + if (general) // later submissions that write the source wait for the blit's read + sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0, + VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 0); + } + return fsr; +} + +// The display's frame rate with frame generation: the swapchain's present count (generated frames included, vk_fg.cpp) +// over half a second, sampled at each of the game's presents (gfx_output_fps). +static volatile double g_out_fps; +static void output_rate(void) { + static uint64_t t0, n0; + uint64_t n = 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; +} +double gfx_output_fps(void) { return g_out_fps; } + +static VkImage g_hl; // frame generation's HUD-less picture at the swapchain's size (hudless_record) +static VmaAllocation g_hl_mem; +static VkImageView g_hl_view; +static uint32_t g_hl_w, g_hl_h; +static VkFormat g_hl_fmt; + +static VkImage hudless_record(VkCommandBuffer cb, unsigned slot, int w, int h, const int rect[4], const int to[4], int general) { + VkImage pic; + VkImageView pv; + uint32_t pw, ph; + if (!vk_fg_active() || !vk_dlaa_fg_hudless(&pic, &pv, &pw, &ph) || (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)) { + vkDeviceWaitIdle(c->device); + vkDestroyImageView(c->device, g_hl_view, NULL); + vmaDestroyImage(c->vma, g_hl, g_hl_mem); + g_hl = VK_NULL_HANDLE; + } + if (!g_hl) { + VkImageCreateInfo ii = {.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, .imageType = VK_IMAGE_TYPE_2D, .format = g_sc_fmt, + .extent = {g_sc_ext.width, g_sc_ext.height, 1}, .mipLevels = 1, .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT | + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT}; + VmaAllocationCreateInfo ai = {.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE}; + if (vmaCreateImage(c->vma, &ii, &ai, &g_hl, &g_hl_mem, NULL) != VK_SUCCESS) return g_hl = VK_NULL_HANDLE; + VkImageViewCreateInfo vi = {.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, .image = g_hl, + .viewType = VK_IMAGE_VIEW_TYPE_2D, .format = g_sc_fmt, + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}}; + if (vkCreateImageView(c->device, &vi, NULL, &g_hl_view) != VK_SUCCESS) g_hl_view = VK_NULL_HANDLE; + g_hl_w = g_sc_ext.width, g_hl_h = g_sc_ext.height, g_hl_fmt = g_sc_fmt; + rt_log("gfx: frame generation's HUD-less picture at %ux%u", g_hl_w, g_hl_h); + } + // the steps' work images were just read for the presented picture: they are written again only after that + sync_barrier(cb, g_hl, VK_IMAGE_LAYOUT_UNDEFINED, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT); + if (to[2] != (int)g_sc_ext.width || to[3] != (int)g_sc_ext.height) { + VkClearColorValue black = {.float32 = {0, 0, 0, 1}}; + VkImageSubresourceRange all = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + vkCmdClearColorImage(cb, g_hl, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, &black, 1, &all); + sync_barrier(cb, g_hl, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_ACCESS_2_TRANSFER_WRITE_BIT); + } + put_picture(cb, slot, pic, VK_FORMAT_R8G8B8A8_UNORM, w, h, rect, g_hl, to, general); + sync_barrier(cb, g_hl, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT); + return g_hl; +} + 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; gfx_window_start(); @@ -747,13 +850,15 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w goto out; } uint32_t idx = 0; - VkResult r = 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); + VkResult r = 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) { plat_xchg32(&g_dirty, 1); if (!create_swapchain()) goto out; - r = 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); + r = 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); } vk_device_fault_report(r); if (r != VK_SUCCESS && r != VK_SUBOPTIMAL_KHR) goto out; // lost, not ready: skip the frame @@ -829,31 +934,21 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT); } - // Colour adjustment (NX2NAVI_COLOR_*, vk_color.c), or on an HDR10 swapchain the PQ encode (vk_hdr.c; the adjustment is - // for SDR): a copy of the crop takes the game image's place below - VkImage graded = !general ? VK_NULL_HANDLE : g_sc_hdr ? vk_hdr_encode(cb, slot, src, fmt, rect) - : vk_color_record(cb, slot, src, fmt, rect); - const int grect[4] = {0, 0, cw, ch}; - VkImage psrc = graded ? graded : src; - VkFormat pfmt = graded ? VK_FORMAT_R16G16B16A16_SFLOAT : fmt; - const int *prect = graded ? grect : rect; - // FSR (NX2NAVI_UPSCALER): EASU when the picture's rectangle is larger than the crop, the shrink and RCAS when it is - // smaller; else the bilinear blit - int fsr = general ? vk_fsr_record(cb, slot, psrc, pfmt, graded ? cw : w, graded ? ch : h, prect, dst, to) : 0; + int fsr = put_picture(cb, slot, src, fmt, w, h, rect, dst, to, general); if (src) g_scaler = fsr == 2 ? 3 : fsr ? 2 : cw != to[2] || ch != to[3]; - if (!fsr && src) { - if (general) // a GENERAL source (vk_texture.c images): earlier submissions' writes become visible to the blit here - sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, - VK_ACCESS_2_MEMORY_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_READ_BIT); - VkImageBlit region = { - .srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, - .srcOffsets = {{prect[0], prect[1], 0}, {prect[0] + cw, prect[1] + ch, 1}}, - .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{to[0], to[1], 0}, {to[0] + to[2], to[1] + to[3], 1}}}; - vkCmdBlitImage(cb, psrc, general ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, dst, - VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, ®ion, VK_FILTER_LINEAR); - if (general) // later submissions that write the source wait for the blit's read - sync_barrier(cb, psrc, VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_GENERAL, VK_PIPELINE_STAGE_2_TRANSFER_BIT, 0, - VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, 0); + // Frame generation without the HUD in its generated frames: the picture as it would be without the HUD (vk_dlaa.c, + // DLSS's or FSR 3.1's output before the HUD was drawn over it) goes through the same steps into an image of the + // swapchain's size, which frame generation compares with the presented one to find the HUD and leave it unwarped + VkImage hudless = src && !g_sc_hdr ? hudless_record(cb, slot, w, h, rect, to, general) : VK_NULL_HANDLE; + static int show_hl = -1; // NX2NAVI_FG_HUDLESS_SHOW=1 (test): the HUD-less picture on screen instead + if (show_hl < 0) show_hl = cfg_get("NX2NAVI_FG_HUDLESS_SHOW") && !strcmp(cfg_get("NX2NAVI_FG_HUDLESS_SHOW"), "1"); + if (hudless && show_hl) { + VkImageCopy cp = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .extent = {g_sc_ext.width, g_sc_ext.height, 1}}; + vkCmdCopyImage(cb, hudless, VK_IMAGE_LAYOUT_GENERAL, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &cp); + sync_barrier(cb, dst, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, + VK_PIPELINE_STAGE_2_TRANSFER_BIT, VK_ACCESS_2_TRANSFER_WRITE_BIT, VK_PIPELINE_STAGE_2_TRANSFER_BIT, + VK_ACCESS_2_TRANSFER_WRITE_BIT); } if (!g_sc_hdr && src) movie_overlay(cb, slot, dst); // a movie frame over the game image (SDR swapchains) // last: the image's layout, and the stage and access of its last write, as each step leaves it @@ -901,7 +996,9 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w last_write = 0; record_thumb(cb, dst, slot); } - if (src) vk_fg_frame(cb, g_sc, g_sc_fmt, g_sc_ext); // frame generation: this frame's preparation (vk_fg.cpp) + if (src) vk_fg_frame(cb, g_sc, g_sc_fmt, g_sc_ext, hudless, g_hl_view); // 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); vk_crumb(cb, VK_CRUMB_PRESENT_END, crumb); vkEndCommandBuffer(cb); @@ -991,6 +1088,7 @@ void gfx_window_release_vk(void) { } if (g_pool) vkDestroyCommandPool(c->device, g_pool, NULL); if (g_sc && vk_fg_active()) vk_fg_destroy_swapchain(g_sc), vk_fg_release(); + else if (g_sc && vk_dlssg_active()) vk_dlssg_destroy_swapchain(c->device, g_sc); else if (g_sc) vkDestroySwapchainKHR(c->device, g_sc, NULL); if (g_surface) vkDestroySurfaceKHR(c->instance, g_surface, NULL); g_sc = VK_NULL_HANDLE; g_surface = VK_NULL_HANDLE; g_pool = VK_NULL_HANDLE; g_sc_n = 0; diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index db0211d5..7df22bc8 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -759,10 +759,13 @@ static void row_upscaling(void) { // Frame generation (vk_fg.cpp), after a restart; it needs a temporal upscaler's motion vectors static void row_framegen(void) { - static const char *const values[] = {"off", "fsr3"}; - const char *names[] = {menu_text(UI_MENU_FRAMEGEN_OFF), "AMD FSR 3.1"}; - int cur = strcmp(cfg_value("NX2NAVI_FRAMEGEN"), "fsr3") ? 0 : 1; - int nv = opt_choice_index("NX2NAVI_FRAMEGEN", UI_MENU_FRAMEGEN, names, 2, cur, 1, UI_MENU_FRAMEGEN_DESC, + 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"); + int cur = !strcmp(v, "fsr3") ? 1 : !strcmp(v, "dlss") ? 2 : 0; + // NVIDIA DLSS (vk_dlssg.c) is listed only while it is set: Streamline still fails in its plugin start-up here + int n = cur == 2 ? 3 : 2; + int nv = opt_choice_index("NX2NAVI_FRAMEGEN", UI_MENU_FRAMEGEN, names, n, cur, 0, UI_MENU_FRAMEGEN_DESC, menu_text(UI_MENU_FRAMEGEN_NOTE)); if (nv >= 0) opt_apply("NX2NAVI_FRAMEGEN", values[nv]); } @@ -1166,7 +1169,11 @@ static void perf_tick(uint64_t now, const MenuTarget *t) { g_perf_frames++; if (d > g_perf_max_ns) g_perf_max_ns = d; if (g_perf_win_ns >= 500000000ull) { - snprintf(g_perf_text, sizeof g_perf_text, menu_text(UI_PERF_LINE_FPS), g_perf_frames * 1e9 / g_perf_win_ns); + double out = gfx_output_fps(); // with frame generation: the display's rate too + if (out > 0) + snprintf(g_perf_text, sizeof g_perf_text, menu_text(UI_PERF_LINE_FPS_FG), g_perf_frames * 1e9 / g_perf_win_ns, out); + else + snprintf(g_perf_text, sizeof g_perf_text, menu_text(UI_PERF_LINE_FPS), g_perf_frames * 1e9 / g_perf_win_ns); g_perf_win_ns = g_perf_max_ns = 0; g_perf_frames = 0; } @@ -1323,6 +1330,12 @@ static void perf_full_draw(const MenuTarget *t, float scale) { perf_cell(UI_MENU_OV_FMT_FPS, s.fps); perf_cell(UI_MENU_OV_LOW1, s.low1_fps); perf_cell(UI_MENU_OV_LOW01, s.low01_fps); + if (gfx_output_fps() > 0) { // frame generation: what the display gets, generated frames included + perf_label(UI_MENU_OV_FG_LABEL, fps_c); + perf_cell(UI_MENU_OV_FMT_FPS, (float)gfx_output_fps()); + ImGui::TableNextColumn(); + ImGui::TableNextColumn(); + } perf_label(UI_MENU_OV_FRAME_LABEL, fps_c); perf_cell(UI_MENU_OV_FRAME_MS, s.frame_ms_avg); perf_cell(UI_MENU_OV_MAX_MS, s.frame_ms_max); diff --git a/runtime/ui/menu.h b/runtime/ui/menu.h index 0e37e38b..82eeeba5 100644 --- a/runtime/ui/menu.h +++ b/runtime/ui/menu.h @@ -74,6 +74,7 @@ unsigned gfx_window_dpi(void); // the window's DPI (96 at 100% d // The present mode the swapchain got, as a NX2NAVI_VK_PRESENT value ("fifo" when mailbox is missing); "" before the first // swapchain. Any thread. const char *gfx_window_present_mode(void); +double gfx_output_fps(void); // frames the display gets per second with frame generation (generated ones too); 0 when off #ifdef __cplusplus } diff --git a/runtime/ui_text.c b/runtime/ui_text.c index ffc7a816..34104462 100644 --- a/runtime/ui_text.c +++ b/runtime/ui_text.c @@ -463,7 +463,8 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_OV_FPS] = "%.0f FPS", [UI_MENU_OV_FRAME_MS] = "%.1f ms", [UI_MENU_OV_GAME_PCT] = "game %.0f%%", [UI_MENU_OV_MAX_MS] = "max %.1f ms", - [UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FRAME_LABEL] = "Frame", [UI_MENU_OV_FMT_FPS] = "%.0f", + [UI_MENU_OV_FPS_LABEL] = "FPS", [UI_MENU_OV_FG_LABEL] = "FG", [UI_PERF_LINE_FPS_FG] = "%.0f fps (%.0f frame gen)", + [UI_MENU_OV_FRAME_LABEL] = "Frame", [UI_MENU_OV_FMT_FPS] = "%.0f", [UI_MENU_OV_LOW1] = "1%% low %.0f", [UI_MENU_OV_LOW01] = "0.1%% low %.0f", [UI_MENU_OV_GPU] = "GPU", [UI_MENU_OV_CPU] = "CPU", [UI_MENU_OV_RAM] = "RAM", [UI_MENU_OV_VRAM] = "VRAM", [UI_MENU_OV_TEMP] = "Temperature", [UI_MENU_OV_POWER] = "Power", [UI_MENU_OV_CLOCK] = "Clock", @@ -517,7 +518,9 @@ static const char *const text_en[UI_TEXT_COUNT] = { [UI_MENU_FRAMEGEN_DESC] = "Shows a generated frame between every two the game draws, for about twice the frame rate " "on a fast display. It needs DLSS or FSR 3.1 under Upscaling (any mode), whose motion " "vectors it uses. AMD FSR 3.1 frame generation runs on any GPU (amd_fidelityfx_vk.dll next " - "to the game's .exe). It pauses while this menu is open. Takes effect after a restart.", + "to the game's .exe); NVIDIA DLSS frame generation needs an RTX 40 or 50 GPU and " + "NVIDIA's Streamline files next to the .exe. It pauses while this menu is open. Takes " + "effect after a restart.", [UI_MENU_FRAMEGEN_OFF] = "Off", [UI_MENU_FRAMEGEN_NOTE] = "after restart; needs DLSS or FSR 3.1", [UI_MENU_GAME_RES] = "Resolution", diff --git a/runtime/ui_text.h b/runtime/ui_text.h index ef6bf247..db0a4d7d 100644 --- a/runtime/ui_text.h +++ b/runtime/ui_text.h @@ -213,7 +213,7 @@ typedef enum { // Performance overlay UI_MENU_OV_FPS, UI_MENU_OV_FRAME_MS, UI_MENU_OV_GAME_PCT, UI_MENU_OV_MAX_MS, - UI_MENU_OV_FPS_LABEL, UI_MENU_OV_FRAME_LABEL, UI_MENU_OV_FMT_FPS, // the frame rows: their labels, the rate + UI_MENU_OV_FPS_LABEL, UI_MENU_OV_FG_LABEL, UI_PERF_LINE_FPS_FG, UI_MENU_OV_FRAME_LABEL, UI_MENU_OV_FMT_FPS, // the frame rows: their labels, the rate UI_MENU_OV_LOW1, UI_MENU_OV_LOW01, // printf: the fps of the slowest 1% / 0.1% of the frames, no unit (the overlay column stays narrow) UI_MENU_OV_GPU, UI_MENU_OV_CPU, UI_MENU_OV_RAM, UI_MENU_OV_VRAM, UI_MENU_OV_TEMP, UI_MENU_OV_POWER, UI_MENU_OV_CLOCK, UI_MENU_OV_NONE, diff --git a/runtime/vk_device.c b/runtime/vk_device.c index 2fafa763..3cb9564d 100644 --- a/runtime/vk_device.c +++ b/runtime/vk_device.c @@ -11,6 +11,7 @@ #include "platform/platform.h" #include "vk_dlaa.h" #include "vk_fg.h" +#include "vk_dlssg.h" static uint32_t g_gfx_queues = 1; // queues the graphics family offers (pick_device) @@ -69,6 +70,7 @@ static void mesa_cache_beside_exe(void) { } static int create_instance(void) { + vk_dlssg_early(); // DLSS Frame Generation: Streamline before any Vulkan call of the process (its slInit asks so) mesa_cache_beside_exe(); const char *ext[12]; uint32_t n_ext = (uint32_t)gfx_window_vk_extensions(ext, 7); @@ -78,7 +80,9 @@ static int create_instance(void) { rt_log("vk: NX2NAVI_VK_VALIDATION set but %s is not installed; continuing without", layers[0]); validate = 0; } - if (validate) ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME; + // DLSS Frame Generation: Streamline's plugins call its naming functions even when they say it is off + if (validate || (vk_dlssg_wanted() && instance_ext_present(VK_EXT_DEBUG_UTILS_EXTENSION_NAME))) + ext[n_ext++] = VK_EXT_DEBUG_UTILS_EXTENSION_NAME; if (vk_hdr_wanted() && instance_ext_present(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) // HDR10 surface formats ext[n_ext++] = VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME; vk_dlaa_instance_exts(ext, &n_ext, 12); @@ -88,7 +92,7 @@ static int create_instance(void) { .enabledExtensionCount = n_ext, .ppEnabledExtensionNames = ext, .enabledLayerCount = validate ? 1 : 0, .ppEnabledLayerNames = layers}; rdoc_init(); - VkResult r = vkCreateInstance(&ci, NULL, &g_vk.instance); + VkResult r = vk_dlssg_create_instance(&ci, &g_vk.instance); if (r != VK_SUCCESS) { rt_log("vk: vkCreateInstance failed (%d); is a Vulkan 1.3 driver installed?", (int)r); return 0; } g_vk.validation = validate; if (validate) { @@ -228,7 +232,7 @@ static int create_device(void) { vkEnumerateDeviceExtensionProperties(g_vk.phys, NULL, &ne, NULL); VkExtensionProperties *ep = calloc(ne ? ne : 1, sizeof *ep); vkEnumerateDeviceExtensionProperties(g_vk.phys, NULL, &ne, ep); - const char *ext[48]; + const char *ext[64]; uint32_t n_ext = 0; for (size_t i = 0; i < sizeof k_required_ext / sizeof *k_required_ext; i++) ext[n_ext++] = k_required_ext[i]; @@ -270,8 +274,19 @@ static int create_device(void) { vk_dlaa_device_exts(ep, ne, ext, &n_ext, 40); // NGX's; the eight slots after it stay for the eight below // FSR 3.1 (vk_ffx.cpp): its Vulkan back end looks vkGetBufferMemoryRequirements2KHR up by that name, which resolves // only with the extension enabled (the core function has no suffix); without it the back end called address 0 - if (vk_dlaa_fsr3() && has_ext(ep, ne, VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME)) + if ((vk_dlaa_fsr3() || vk_fg_wanted() || vk_dlssg_wanted()) && has_ext(ep, ne, VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME)) ext[n_ext++] = VK_KHR_GET_MEMORY_REQUIREMENTS_2_EXTENSION_NAME; + // DLSS Frame Generation (vk_dlssg.c): Streamline's plugins look functions of extensions core in Vulkan 1.3 up by + // their suffixed names too, which resolve only with the extension itself enabled + if (vk_dlssg_wanted()) { + static const char *const promoted[] = {VK_KHR_TIMELINE_SEMAPHORE_EXTENSION_NAME, VK_KHR_SYNCHRONIZATION_2_EXTENSION_NAME, + VK_KHR_DYNAMIC_RENDERING_EXTENSION_NAME, VK_KHR_CREATE_RENDERPASS_2_EXTENSION_NAME, + VK_KHR_BIND_MEMORY_2_EXTENSION_NAME, VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME, + VK_KHR_COPY_COMMANDS_2_EXTENSION_NAME, VK_KHR_MAINTENANCE_1_EXTENSION_NAME, + VK_KHR_MAINTENANCE_2_EXTENSION_NAME, VK_KHR_MAINTENANCE_3_EXTENSION_NAME}; + for (size_t i = 0; i < sizeof promoted / sizeof *promoted && n_ext < 56; i++) + if (has_ext(ep, ne, promoted[i])) ext[n_ext++] = promoted[i]; + } if (has_ext(ep, ne, VK_EXT_MEMORY_BUDGET_EXTENSION_NAME)) { g_vk.has_memory_budget = 1; ext[n_ext++] = VK_EXT_MEMORY_BUDGET_EXTENSION_NAME; } // GPU timestamps on the CPU clock (vk_sync.c): the perf line splits GPU idle time into waiting for a submission or not. if (has_ext(ep, ne, VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME)) { g_vk.has_calibrated = 2; ext[n_ext++] = VK_KHR_CALIBRATED_TIMESTAMPS_EXTENSION_NAME; } @@ -358,9 +373,10 @@ static int create_device(void) { } VkDeviceCreateInfo di = {.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO, .pNext = &c->f2, .queueCreateInfoCount = nqi, .pQueueCreateInfos = qi, .enabledExtensionCount = n_ext, .ppEnabledExtensionNames = ext}; - VkResult r = vkCreateDevice(g_vk.phys, &di, NULL, &g_vk.device); + VkResult r = vk_dlssg_create_device(g_vk.instance, g_vk.phys, &di, &g_vk.device); free(c); if (r != VK_SUCCESS) { rt_log("vk: vkCreateDevice failed (%d)", (int)r); return 0; } + vk_dlssg_device_ready(g_vk.device); vkGetDeviceQueue(g_vk.device, g_vk.gfx_family, 0, &g_vk.gfx_queue); if (nq_gfx >= 2) { VkQueue present, acquire; diff --git a/runtime/vk_dlaa.c b/runtime/vk_dlaa.c index 1a4b7611..09207e1c 100644 --- a/runtime/vk_dlaa.c +++ b/runtime/vk_dlaa.c @@ -355,10 +355,38 @@ static struct { unsigned cfg_seen; // cfg_version when preset was read uint64_t last_eval_ns; // plat_now_ns of the previous evaluation (FrameTimeDeltaInMsec) VkFfxImage fg_depth, fg_mv; // the last evaluation's depth and motion vectors, for frame generation + VkImageView fg_depth_view, fg_mv_view; + VkDlaaCamera fg_cam; // this frame's camera (mv_pass), fg_cam_ok when known + int fg_cam_ok; float fg_jx, fg_jy; int fg_fresh, fg_reset; // fresh: not handed out yet (vk_dlaa_fg_inputs) + // the picture without the HUD for frame generation (vk_dlaa_fg_hudless): DLSS's output encoded to sRGB as the game's + // last pass encodes its picture, then copied bit for bit into an RGBA8 UNORM image, the format the present reads + VkImage hl_srgb, hl; + VmaAllocation hl_srgb_mem, hl_mem; + VkImageView hl_view; + uint32_t hl_w, hl_h; + int hl_fresh; } D = {.mv_ok = -1, .cfg_seen = ~0u}; +int vk_dlaa_fg_hudless(VkImage *img, VkImageView *view, uint32_t *w, uint32_t *h) { + if (!D.hl_fresh) return 0; + D.hl_fresh = 0; + *img = D.hl, *view = D.hl_view, *w = D.hl_w, *h = D.hl_h; + return 1; +} + +int vk_dlaa_fg_views(VkImageView *depth, VkImageView *mv) { + *depth = D.fg_depth_view, *mv = D.fg_mv_view; + return D.fg_depth_view && D.fg_mv_view; +} + +int vk_dlaa_fg_camera(VkDlaaCamera *c) { + if (!D.fg_cam_ok) return 0; + *c = D.fg_cam; + return 1; +} + int vk_dlaa_fg_inputs(VkFfxImage *depth, VkFfxImage *mv, float *jx, float *jy, int *reset) { if (!D.fg_fresh || D.failed) return 0; D.fg_fresh = 0; @@ -640,6 +668,30 @@ static void ocam_vp(const float *v, double out[16]) { } } +// Frame generation's camera (vk_dlaa_fg_camera) from the Ocarina block: the projection is rows 7-10, the view rows 0-2 +// (rows: right, up, back; the camera looks down -z), m the reprojection from this frame's clip space to last frame's. +// Each matrix is handed out transposed, for row vectors. +static void fg_store(float out[16], const double m[16]) { + for (int r = 0; r < 4; r++) + for (int k = 0; k < 4; k++) out[r * 4 + k] = (float)m[k * 4 + r]; +} +static void fg_camera(const float *v, const double m[16]) { + double P[16], Pi[16], mi[16]; + for (int i = 0; i < 16; i++) P[i] = v[28 + i]; + if (!inv4(P, Pi) || !inv4(m, mi)) return; + VkDlaaCamera *c = &D.fg_cam; + fg_store(c->view_to_clip, P), fg_store(c->clip_to_view, Pi); + fg_store(c->clip_to_prev_clip, m), fg_store(c->prev_clip_to_clip, mi); + for (int i = 0; i < 3; i++) { + c->right[i] = v[i], c->up[i] = v[4 + i], c->fwd[i] = -v[8 + i]; + // the position: -R^T t, R the view's 3x3 and t its last column + c->pos[i] = -(v[i] * v[3] + v[4 + i] * v[7] + v[8 + i] * v[11]); + } + c->fov_y = P[5] != 0 ? 2.0f * (float)atan(1.0 / fabs(P[5])) : 1.0f; + c->aspect = P[0] != 0 ? (float)fabs(P[5] / P[0]) : 1.777f; + D.fg_cam_ok = 1; +} + // The pass, recorded through the recorder like the replay's own commands. 0: no camera this frame (the vectors stay // zero and DLSS starts a new history). bias > 0 with this frame's G-buffer classes (D.ids) also writes e->mask, the // bias mask (dlaa_mv.comp); *masked says whether it did. @@ -674,6 +726,8 @@ static int mv_pass(VkExec *ex, struct Size *e, VkImageView zv, float jx, float j have_m = 1; } } + D.fg_cam_ok = 0; + if (cr && have_m && D.cam_size == OCAM_BLOCK) fg_camera(cr, m); int s = D.cam_slot, had_prev = D.cam_prev_ok; if (D.cam_size == CAM_BLOCK) { // the whole block, for the per-object motion pass (the deferred engine's only) VkBufferCopy bc = {cam.offset, (VkDeviceSize)s * CAM_SLOT, CAM_BLOCK}; @@ -1039,6 +1093,8 @@ static int perf_quality(uint32_t w, uint32_t ow) { } // dst: the target DLSS's picture goes to, NULL for the light target itself (DLAA in place). +static void hudless(VkCommandBuffer cb, struct Size *e, uint32_t w, uint32_t h); + static void evaluate(VkExec *ex, const VkExecTarget *dst) { const VkExecTarget *t = &D.color, *z = &D.depth; if (!dst) dst = t; @@ -1179,12 +1235,14 @@ evaluated: VK_FILTER_NEAREST); full_barrier(cb); if (dst != t) vk_dlaa_swap_img = t->img, vk_dlaa_swap_view = e->out_view; // the scaling draw reads DLSS's picture + if (vk_dlaa_hudless_wanted) hudless(cb, e, ow, oh); // frame generation keeps the HUD out of its generated frames ex->recorded += 4; vk_exec_draw_new_cb(); // NGX bound pipelines and descriptor sets of its own: the next draw binds everything again D.jdepth = z->image; // for frame generation at this frame's present (vk_dlaa_fg_inputs) D.fg_depth = (VkFfxImage){z->image, z->format, w, h, 0}; D.fg_mv = (VkFfxImage){e->mv, VK_FORMAT_R16G16_SFLOAT, w, h, 1}; + D.fg_depth_view = zv, D.fg_mv_view = e->mv_view; D.fg_jx = jx, D.fg_jy = jy, D.fg_fresh = 1; next_jitter(w, ow); if (++D.evals == 1 || D.evals == 300 || D.evals % 3600 == 0) // 300: one early line with the frames counted @@ -1197,6 +1255,54 @@ evaluated: (unsigned long long)D.why[2], (unsigned long long)R.made, (unsigned long long)R.missed); } +// The HUD-less picture (vk_dlaa_fg_hudless): DLSS's output, which the HUD is then drawn over, through the encode the +// game's last pass gives its picture (linear to sRGB), as RGBA8 UNORM bytes like the picture the present reads. +int vk_dlaa_hudless_wanted; +static void hudless(VkCommandBuffer cb, struct Size *e, uint32_t w, uint32_t h) { + const VkCtx *c = vk_ctx(); + if (D.hl && (D.hl_w != w || D.hl_h != h)) { + vkDeviceWaitIdle(c->device); + vkDestroyImageView(c->device, D.hl_view, NULL); + vmaDestroyImage(c->vma, D.hl, D.hl_mem), vmaDestroyImage(c->vma, D.hl_srgb, D.hl_srgb_mem); + D.hl = D.hl_srgb = VK_NULL_HANDLE; + } + if (!D.hl) { + VkImageView v2 = VK_NULL_HANDLE; + VkImageUsageFlags u = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_SAMPLED_BIT; + if (!image2d(w, h, VK_FORMAT_R8G8B8A8_SRGB, u, &D.hl_srgb, &D.hl_srgb_mem, &v2) || + !image2d(w, h, VK_FORMAT_R8G8B8A8_UNORM, u | VK_IMAGE_USAGE_STORAGE_BIT, &D.hl, &D.hl_mem, &D.hl_view)) { + rt_log("dlaa: the HUD-less picture's images could not be made: frame generation sees the HUD"); + vk_dlaa_hudless_wanted = 0; + return; + } + if (v2) vkDestroyImageView(c->device, v2, NULL); + D.hl_w = w, D.hl_h = h; + VkImageMemoryBarrier2 ib[2]; + VkImage im[2] = {D.hl_srgb, D.hl}; + for (int i = 0; i < 2; i++) + ib[i] = (VkImageMemoryBarrier2){.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2, + .srcStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .dstStageMask = VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT, + .dstAccessMask = VK_ACCESS_2_MEMORY_READ_BIT | VK_ACCESS_2_MEMORY_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, .image = im[i], + .subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}}; + VkDependencyInfo di = {.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO, .imageMemoryBarrierCount = 2, + .pImageMemoryBarriers = ib}; + vkCmdPipelineBarrier2(cb, &di); + } + VkImageBlit b = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .srcOffsets = {{0, 0, 0}, {(int32_t)w, (int32_t)h, 1}}, + .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstOffsets = {{0, 0, 0}, {(int32_t)w, (int32_t)h, 1}}}; + vkCmdBlitImage(cb, e->out, VK_IMAGE_LAYOUT_GENERAL, D.hl_srgb, VK_IMAGE_LAYOUT_GENERAL, 1, &b, VK_FILTER_NEAREST); + full_barrier(cb); + VkImageCopy cp = {.srcSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, .dstSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}, + .extent = {w, h, 1}}; + vkCmdCopyImage(cb, D.hl_srgb, VK_IMAGE_LAYOUT_GENERAL, D.hl, VK_IMAGE_LAYOUT_GENERAL, 1, &cp); + full_barrier(cb); + D.hl_fresh = 1; +} + static int scene_color(VkFormat f) { return f == VK_FORMAT_B10G11R11_UFLOAT_PACK32 || f == VK_FORMAT_R16G16B16A16_SFLOAT; } diff --git a/runtime/vk_dlaa.h b/runtime/vk_dlaa.h index a120a81b..b4061798 100644 --- a/runtime/vk_dlaa.h +++ b/runtime/vk_dlaa.h @@ -12,7 +12,20 @@ int vk_dlaa_fsr3(void); // Frame generation (vk_fg.cpp): the depth and motion vectors of the frame DLSS or FSR 3.1 last ran on, with its jitter // and whether its history reset; 1 once per evaluation (0: none since the last call). The images stay valid until the // next frame's scene draws over them, after this present. -int vk_dlaa_fg_inputs(VkFfxImage *depth, VkFfxImage *mv, float *jx, float *jy, int *reset); // ... and its temporal upscaler is AMD FSR 3.1 (NX2NAVI_TAA = fsr3) instead of DLSS +int vk_dlaa_fg_inputs(VkFfxImage *depth, VkFfxImage *mv, float *jx, float *jy, int *reset); +// ... their views, and this frame's camera for DLSS Frame Generation (vk_dlssg.c): matrices for row vectors (v * M, the +// rows stored in order), as Streamline takes them; 0 without a camera that frame. +typedef struct { + float view_to_clip[16], clip_to_view[16], clip_to_prev_clip[16], prev_clip_to_clip[16]; + float pos[3], up[3], right[3], fwd[3]; + float fov_y, aspect; +} VkDlaaCamera; +int vk_dlaa_fg_views(VkImageView *depth, VkImageView *mv); +// The picture without the HUD at the size of the game's picture, RGBA8 UNORM bytes like it (sRGB-encoded), made at +// each evaluation while vk_dlaa_hudless_wanted (frame generation sets it); 1 once per evaluation. +extern int vk_dlaa_hudless_wanted; +int vk_dlaa_fg_hudless(VkImage *img, VkImageView *view, uint32_t *w, uint32_t *h); +int vk_dlaa_fg_camera(VkDlaaCamera *c); // ... and its temporal upscaler is AMD FSR 3.1 (NX2NAVI_TAA = fsr3) instead of DLSS // vk_device.c: NGX's instance and device extensions join the lists (room for max names); vk_dlaa_init after the // device exists brings NGX up and checks that DLSS is available. void vk_dlaa_instance_exts(const char **ext, uint32_t *n, uint32_t max); diff --git a/runtime/vk_dlssg.c b/runtime/vk_dlssg.c new file mode 100644 index 00000000..dfab1b37 --- /dev/null +++ b/runtime/vk_dlssg.c @@ -0,0 +1,157 @@ +// DLSS Frame Generation (NX2NAVI_FRAMEGEN = dlss; applies after a restart) through nx2navi_dlssg.dll +// (runtime/dlssg/nx2_sl.cpp), which drives NVIDIA Streamline. Like AMD's frame generation (vk_fg.cpp) it needs DLSS or +// FSR 3.1 upscaling on, whose depth, motion vectors and camera it hands over, and an RTX 40 or 50 GPU. +// +// The DLL and Streamline (sl.interposer.dll and its plugins, nvngx_dlssg.dll) sit next to the game. The instance and +// device are created through Streamline's proxies (vk_device.c), so it can add the extensions, features and queues +// DLSS-G needs; the present path calls Streamline's swapchain functions (gfx_present.c), and every present records the +// frame's constants and tags first (vk_dlssg_frame). Generation pauses while the menu is open. +#ifdef _WIN32 +#include +#endif +#include +#include + +#include "cfg.h" +#include "host.h" +#include "vk.h" +#include "vk_dlaa.h" +#include "vk_dlssg.h" +#include "dlssg/nx2_sl.h" +#include "ui/menu.h" + +static struct { + int wanted, loaded, active; + PFN_nx2sl_init init; + PFN_nx2sl_instance_proc gipa; + PFN_nx2sl_device_proc gdpa; + PFN_nx2sl_available available; + PFN_nx2sl_frame frame; + PFN_nx2sl_present_marker marker; + PFN_nx2sl_shutdown shutdown; + PFN_vkCreateSwapchainKHR create_sc; + PFN_vkDestroySwapchainKHR destroy_sc; + PFN_vkGetSwapchainImagesKHR images; + PFN_vkAcquireNextImageKHR acquire; + PFN_vkQueuePresentKHR present; +} S = {.wanted = -1}; + +static void sl_line(const char *line) { rt_log("%s", line); } + +int vk_dlssg_wanted(void) { + if (S.wanted < 0) { + S.wanted = !strcmp(cfg_value("NX2NAVI_FRAMEGEN"), "dlss"); + if (S.wanted && !vk_dlaa_wanted()) { + rt_log("dlssg: frame generation needs DLSS or FSR 3.1 upscaling (any mode): off"); + S.wanted = 0; + } + } + return S.wanted; +} + +int vk_dlssg_early(void) { + if (!vk_dlssg_wanted()) return 0; + if (S.loaded) return S.loaded > 0; + S.loaded = -1; +#ifdef _WIN32 + wchar_t dir[MAX_PATH], path[MAX_PATH + 32]; + DWORD n = GetModuleFileNameW(NULL, dir, MAX_PATH); + while (n && dir[n - 1] != L'\\') n--; + dir[n ? n - 1 : 0] = 0; + swprintf(path, MAX_PATH + 32, L"%ls\\nx2navi_dlssg.dll", dir); + HMODULE m = LoadLibraryW(path); + if (!m) { + rt_log("dlssg: nx2navi_dlssg.dll is not next to the game: DLSS Frame Generation off"); + return 0; + } + PFN_nx2sl_version ver = (PFN_nx2sl_version)(void *)GetProcAddress(m, "nx2sl_version"); + S.init = (PFN_nx2sl_init)(void *)GetProcAddress(m, "nx2sl_init"); + S.gipa = (PFN_nx2sl_instance_proc)(void *)GetProcAddress(m, "nx2sl_instance_proc"); + S.gdpa = (PFN_nx2sl_device_proc)(void *)GetProcAddress(m, "nx2sl_device_proc"); + S.available = (PFN_nx2sl_available)(void *)GetProcAddress(m, "nx2sl_available"); + S.frame = (PFN_nx2sl_frame)(void *)GetProcAddress(m, "nx2sl_frame"); + S.marker = (PFN_nx2sl_present_marker)(void *)GetProcAddress(m, "nx2sl_present_marker"); + S.shutdown = (PFN_nx2sl_shutdown)(void *)GetProcAddress(m, "nx2sl_shutdown"); + if (!ver || ver() != NX2SL_VERSION || !S.init || !S.gipa || !S.gdpa || !S.available || !S.frame || !S.marker) { + rt_log("dlssg: nx2navi_dlssg.dll is from another build: DLSS Frame Generation off"); + return 0; + } + if (!S.init(dir, sl_line)) return 0; + S.loaded = 1; + return 1; +#else + return 0; +#endif +} + +VkResult vk_dlssg_create_instance(const VkInstanceCreateInfo *ci, VkInstance *out) { + PFN_vkCreateInstance f = S.loaded > 0 ? (PFN_vkCreateInstance)S.gipa()(VK_NULL_HANDLE, "vkCreateInstance") : NULL; + return f ? f(ci, NULL, out) : vkCreateInstance(ci, NULL, out); +} + +VkResult vk_dlssg_create_device(VkInstance inst, VkPhysicalDevice pd, const VkDeviceCreateInfo *ci, VkDevice *out) { + PFN_vkCreateDevice f = S.loaded > 0 ? (PFN_vkCreateDevice)S.gipa()(inst, "vkCreateDevice") : NULL; + return f ? f(pd, ci, NULL, out) : vkCreateDevice(pd, ci, NULL, out); +} + +void vk_dlssg_device_ready(VkDevice dev) { + if (S.loaded <= 0 || !S.available()) return; + PFN_vkGetDeviceProcAddr g = S.gdpa(); + S.create_sc = (PFN_vkCreateSwapchainKHR)g(dev, "vkCreateSwapchainKHR"); + S.destroy_sc = (PFN_vkDestroySwapchainKHR)g(dev, "vkDestroySwapchainKHR"); + S.images = (PFN_vkGetSwapchainImagesKHR)g(dev, "vkGetSwapchainImagesKHR"); + S.acquire = (PFN_vkAcquireNextImageKHR)g(dev, "vkAcquireNextImageKHR"); + S.present = (PFN_vkQueuePresentKHR)g(dev, "vkQueuePresentKHR"); + S.active = S.create_sc && S.destroy_sc && S.images && S.acquire && S.present; + rt_log("dlssg: %s", S.active ? "DLSS Frame Generation on the swapchain" : "the swapchain hooks are missing: off"); +} + +int vk_dlssg_active(void) { return S.active; } + +VkResult vk_dlssg_create_swapchain(VkDevice d, const VkSwapchainCreateInfoKHR *ci, VkSwapchainKHR *sc) { + return S.create_sc(d, ci, NULL, sc); +} +void vk_dlssg_destroy_swapchain(VkDevice d, VkSwapchainKHR sc) { S.destroy_sc(d, sc, NULL); } +VkResult vk_dlssg_get_images(VkDevice d, VkSwapchainKHR sc, uint32_t *n, VkImage *img) { return S.images(d, sc, n, img); } +VkResult vk_dlssg_acquire(VkDevice d, VkSwapchainKHR sc, uint64_t t, VkSemaphore sem, uint32_t *idx) { + return S.acquire(d, sc, t, sem, VK_NULL_HANDLE, idx); +} + +VkResult vk_dlssg_present(VkQueue q, const VkPresentInfoKHR *pi) { + S.marker(0); + VkResult r = S.present(q, pi); + S.marker(1); + return r; +} + +void vk_dlssg_frame(VkCommandBuffer cb, VkExtent2D ext) { + if (!S.active) return; + VkFfxImage d, m; + VkImageView dv, mvv; + VkDlaaCamera c; + float jx = 0, jy = 0; + int reset = 0; + int have = vk_dlaa_fg_inputs(&d, &m, &jx, &jy, &reset) && vk_dlaa_fg_views(&dv, &mvv) && vk_dlaa_fg_camera(&c); + int on = have && !menu_is_open(); + Nx2SlImage zd = {d.image, dv, d.format, d.width, d.height, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT}; + Nx2SlImage zm = {m.image, mvv, m.format, m.width, m.height, VK_IMAGE_LAYOUT_GENERAL, + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT}; + Nx2SlCamera k; + memset(&k, 0, sizeof k); + if (have) { + memcpy(k.view_to_clip, c.view_to_clip, 64), memcpy(k.clip_to_view, c.clip_to_view, 64); + memcpy(k.clip_to_prev_clip, c.clip_to_prev_clip, 64), memcpy(k.prev_clip_to_clip, c.prev_clip_to_clip, 64); + memcpy(k.pos, c.pos, 12), memcpy(k.up, c.up, 12), memcpy(k.right, c.right, 12), memcpy(k.fwd, c.fwd, 12); + k.znear = 0.1f, k.zfar = 1e8f, k.fov_y = c.fov_y, k.aspect = c.aspect; // Ocarina's projection (vk_ffx.cpp) + k.jitter[0] = jx, k.jitter[1] = jy; + 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); +} + +void vk_dlssg_release(void) { + if (S.loaded > 0 && S.shutdown) S.shutdown(); + S.active = 0; +} diff --git a/runtime/vk_dlssg.h b/runtime/vk_dlssg.h new file mode 100644 index 00000000..b4474c8b --- /dev/null +++ b/runtime/vk_dlssg.h @@ -0,0 +1,20 @@ +// DLSS Frame Generation through NVIDIA Streamline (vk_dlssg.c, nx2navi_dlssg.dll). +#pragma once +#include +#include + +int vk_dlssg_wanted(void); // NX2NAVI_FRAMEGEN = dlss with DLSS or FSR 3.1 upscaling on (read once) +int vk_dlssg_early(void); // before the instance exists: loads the DLL and Streamline; 1 when its proxies are there +// vk_device.c: the instance and device through Streamline's proxies when vk_dlssg_early succeeded, else the loader's +VkResult vk_dlssg_create_instance(const VkInstanceCreateInfo *ci, VkInstance *out); +VkResult vk_dlssg_create_device(VkInstance inst, VkPhysicalDevice pd, const VkDeviceCreateInfo *ci, VkDevice *out); +void vk_dlssg_device_ready(VkDevice dev); // after the device: checks DLSS-G and takes the swapchain hooks +int vk_dlssg_active(void); +// gfx_present.c's swapchain calls while active +VkResult vk_dlssg_create_swapchain(VkDevice d, const VkSwapchainCreateInfoKHR *ci, VkSwapchainKHR *sc); +void vk_dlssg_destroy_swapchain(VkDevice d, VkSwapchainKHR sc); +VkResult vk_dlssg_get_images(VkDevice d, VkSwapchainKHR sc, uint32_t *n, VkImage *img); +VkResult vk_dlssg_acquire(VkDevice d, VkSwapchainKHR sc, uint64_t timeout, VkSemaphore sem, uint32_t *idx); +VkResult vk_dlssg_present(VkQueue q, const VkPresentInfoKHR *pi); +void vk_dlssg_frame(VkCommandBuffer cb, VkExtent2D ext); // each present, before its command buffer is submitted +void vk_dlssg_release(void); diff --git a/runtime/vk_fg.cpp b/runtime/vk_fg.cpp index 07cfdd68..50b6081e 100644 --- a/runtime/vk_fg.cpp +++ b/runtime/vk_fg.cpp @@ -42,7 +42,7 @@ struct { ffxQueryDescSwapchainReplacementFunctionsVK fn = {}; uint64_t frame = 0; int failed = 0; - uint64_t generated = 0, paused = 0, no_inputs = 0; + uint64_t generated = 0, paused = 0, no_inputs = 0, hudless = 0; } F; VkResult submit_game(uint32_t n, const VkSubmitInfo *si, VkFence fence) { @@ -141,8 +141,15 @@ extern "C" VkResult vk_fg_present(const VkPresentInfoKHR *pi) { return F.fn.pOutQueuePresentKHR(vk_ctx()->gfx_queue, pi); } -extern "C" void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt, VkExtent2D ext) { +extern "C" uint64_t vk_fg_presents(VkSwapchainKHR sc) { + return vk_fg_active() && F.fn.pOutGetLastPresentCountFFXAPI && sc ? F.fn.pOutGetLastPresentCountFFXAPI(sc) : 0; +} + +extern "C" void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt, VkExtent2D ext, VkImage hudless, + VkImageView hudless_view) { + (void)hudless_view; if (!vk_fg_active()) return; + vk_dlaa_hudless_wanted = 1; // the upscaling pass keeps a copy of the picture before the HUD (vk_dlaa.c hudless) const VkCtx *c = vk_ctx(); if (F.fg && (F.fg_ext.width != ext.width || F.fg_ext.height != ext.height || F.fg_fmt != fmt)) { vkDeviceWaitIdle(c->device); // a new window size: the context is made for its display size @@ -155,9 +162,14 @@ extern "C" void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt, be.vkDevice = c->device; be.vkPhysicalDevice = c->phys; be.vkDeviceProcAddr = vkGetDeviceProcAddr; + // the HUD-less picture has the swapchain's format (gfx_present.c hudless_record) + ffxCreateContextDescFrameGenerationHudless hl = {}; + hl.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FRAMEGENERATION_HUDLESS; + hl.header.pNext = &be.header; + hl.hudlessBackBufferFormat = ffxApiGetSurfaceFormatVK(fmt); ffxCreateContextDescFrameGeneration d = {}; d.header.type = FFX_API_CREATE_CONTEXT_DESC_TYPE_FRAMEGENERATION; - d.header.pNext = &be.header; + d.header.pNext = &hl.header; d.flags = FFX_FRAMEGENERATION_ENABLE_DEPTH_INFINITE; d.displaySize = {ext.width, ext.height}; d.maxRenderSize = {ext.width, ext.height}; @@ -183,6 +195,11 @@ extern "C" void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt, cfg.frameGenerationEnabled = on != 0; cfg.allowAsyncWorkloads = false; cfg.generationRect = {0, 0, (int32_t)ext.width, (int32_t)ext.height}; + if (on && hudless) { + VkFfxImage h = {hudless, fmt, ext.width, ext.height, 1}; + cfg.HUDLessColor = image(&h); + F.hudless++; + } cfg.frameID = F.frame; ffxReturnCode_t r = vk_ffx_configure(&F.fg, &cfg.header); if (r != FFX_API_RETURN_OK) return fail("frame generation configuration", r); @@ -208,9 +225,9 @@ extern "C" void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt, r = vk_ffx_dispatch(&F.fg, &p.header); if (r != FFX_API_RETURN_OK) return fail("frame generation preparation", r); if (F.frame == 2 || F.frame % 3600 == 0) - rt_log("fg: %llu frames presented, %llu generated; paused %llu (menu), %llu without depth and motion", - (unsigned long long)F.frame, (unsigned long long)F.generated, (unsigned long long)F.paused, - (unsigned long long)F.no_inputs); + rt_log("fg: %llu frames presented, %llu generated (%llu with the HUD kept out); paused %llu (menu), %llu without depth " + "and motion", (unsigned long long)F.frame, (unsigned long long)F.generated, (unsigned long long)F.hudless, + (unsigned long long)F.paused, (unsigned long long)F.no_inputs); } extern "C" void vk_fg_release(void) { diff --git a/runtime/vk_fg.h b/runtime/vk_fg.h index 2218a86d..015b3f06 100644 --- a/runtime/vk_fg.h +++ b/runtime/vk_fg.h @@ -16,5 +16,8 @@ VkResult vk_fg_acquire(VkSwapchainKHR sc, uint64_t timeout, VkSemaphore sem, uin VkResult vk_fg_present(const VkPresentInfoKHR *pi); // on the game's queue, its lock held by the caller // Each present, before its command buffer is submitted: configures generation for this frame and records its // preparation (the frame's depth and motion vectors, vk_dlaa_fg_inputs) into cb. -void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt, VkExtent2D ext); +// hudless: this frame's picture without the HUD at the swapchain's size and format, in GENERAL (VK_NULL_HANDLE: none), +// from which generation tells the HUD apart and keeps it out of the generated frames. +void vk_fg_frame(VkCommandBuffer cb, VkSwapchainKHR sc, VkFormat fmt, VkExtent2D ext, VkImage hudless, VkImageView hudless_view); +uint64_t vk_fg_presents(VkSwapchainKHR sc); // frames the swapchain presented, generated ones included (0: off) void vk_fg_release(void); diff --git a/tools/build_dlssg.py b/tools/build_dlssg.py new file mode 100644 index 00000000..aab670e8 --- /dev/null +++ b/tools/build_dlssg.py @@ -0,0 +1,65 @@ +"""Build nx2navi_dlssg.dll (runtime/dlssg/nx2_sl.cpp, DLSS Frame Generation through NVIDIA Streamline) with MSVC. + + python tools/build_dlssg.py --sl [--out build/dlssg] + +Streamline's C++ types have MSVC's ABI, so this one DLL is built with Visual Studio's compiler (the Build Tools are +enough; vswhere finds them), against the SDK's headers, which stay where they are. The Vulkan headers are MSYS2's. +The build kit ships the DLL ready-made beside Streamline's own runtime DLLs (tools/make_kit.py --dlssg). +""" +import argparse +import os +import shutil +import subprocess +import sys +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +VSWHERE = Path(os.environ.get('ProgramFiles(x86)', r'C:\Program Files (x86)')) / 'Microsoft Visual Studio' / 'Installer' / 'vswhere.exe' + + +def vcvars(): + if not VSWHERE.is_file(): + sys.exit('Visual Studio (or its Build Tools) is not installed: vswhere.exe is missing') + path = subprocess.run([str(VSWHERE), '-latest', '-products', '*', '-requires', + 'Microsoft.VisualStudio.Component.VC.Tools.x86.x64', '-property', 'installationPath'], + capture_output=True, text=True).stdout.strip() + bat = Path(path) / 'VC' / 'Auxiliary' / 'Build' / 'vcvars64.bat' + if not bat.is_file(): + sys.exit(f'no vcvars64.bat under {path or "(no Visual Studio with the C++ tools)"}') + return bat + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument('--sl', type=Path, required=True, help='the Streamline SDK folder (holds include/sl.h)') + ap.add_argument('--vulkan', type=Path, default=Path(r'C:\msys64\ucrt64\include'), help='folder holding vulkan/vulkan.h') + ap.add_argument('--out', type=Path, default=ROOT / 'build' / 'dlssg') + a = ap.parse_args() + if not (a.sl / 'include' / 'sl.h').is_file(): + sys.exit(f'{a.sl / "include" / "sl.h"} is missing') + a.out.mkdir(parents=True, exist_ok=True) + # only the Vulkan headers: MSYS2's own C library headers must not shadow MSVC's + inc = a.out / 'include' + for d in ('vulkan', 'vk_video'): + if (a.vulkan / d).is_dir(): + shutil.copytree(a.vulkan / d, inc / d, dirs_exist_ok=True) + src = ROOT / 'runtime' / 'dlssg' / 'nx2_sl.cpp' + cmd = (f'call "{vcvars()}" >nul && cl /nologo /LD /O2 /EHsc /std:c++17 /W3 /DNOMINMAX /DWIN32_LEAN_AND_MEAN ' + f'/I"{a.sl / "include"}" /I"{inc}" /I"{src.parent}" "{src}" /Fo"{a.out}\\\\" ' + f'/Fe"{a.out / "nx2navi_dlssg.dll"}" /link /NOLOGO') + with tempfile.NamedTemporaryFile('w', suffix='.cmd', delete=False) as f: + # vcvars64.bat runs vswhere itself and needs it on PATH + f.write('@echo off\r\nset "PATH=%PATH%;' + str(VSWHERE.parent) + '"\r\n' + cmd + '\r\n') + script = f.name + try: + r = subprocess.run(['cmd', '/c', script]) + finally: + os.unlink(script) + if r.returncode or not (a.out / 'nx2navi_dlssg.dll').is_file(): + sys.exit(f'the build failed ({r.returncode})') + print(a.out / 'nx2navi_dlssg.dll') + + +if __name__ == '__main__': + main()