aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDuck912 <350528965+Duck912@users.noreply.github.com>2026-11-03 12:13:35 -0500
committerDuck912 <350528965+Duck912@users.noreply.github.com>2026-11-03 12:33:16 -0500
commit63e87a22c60394bc4f427ee5f8d6ddbce7b0ac7f (patch)
tree959ffe32400de4cd727c0c5655e391591d6485a6
parent1d4ccdbb0cf01add647247a4ab64b5060b1bb8eb (diff)
video_core: NVIDIA DLSS through NVIDIA Streamline in place of a game's DLSS
Adds a Graphics setting, "Use NVIDIA DLSS in place of a game's DLSS" (nvidia_dlss, off by default). When it is on, in Windows builds configured with EGLANTINE_STREAMLINE_SDK, the evaluation Eglantine already replaces with FSR 3.1 goes to NVIDIA DLSS on GeForce RTX cards, with the same inputs (colour, depth, motion vectors, jitter). When DLSS can't run, FSR 3.1 runs as before. Only Streamline's MIT-licensed headers are compiled in; nothing of NVIDIA's is linked or shipped. At run time Eglantine looks for sl.interposer.dll in the user folder's "streamline" folder (or EDEN_STREAMLINE_DIR, or "streamline" next to the executable), checks its Streamline signature and starts Streamline with manual hooking. Streamline's plugins come from that folder or the NVIDIA app's downloads. DLSS itself is nvngx_dlss.dll in the same folder, else the newest DLSS 310 or later that the NVIDIA app has downloaded, checked for NVIDIA's Authenticode signature and copied as streamline/nvidia-app/nvngx_dlss.dll. Like other tools that call DLSS outside a game, it uses its own fixed project id with the custom engine type. - Preset K for all modes (EDEN_NX2_DLSS_PRESET overrides it): the default Performance model (M) and L draw thin dark horizontal lines in some frames at 2x resolution scale; K doesn't. - HDR input with auto exposure; the mode follows the render/output ratio. - Buffer device address stays enabled when DLSS needs it, for DLSS only. - The upscaler waits for the device instead of submitting when it is destroyed: by then the game's channels are released, and a submit read the state tracker's stale flags (a crash when stopping a game). With the setting off, or in builds without the SDK path (the setting is hidden there), nothing changes. Testing: Windows 11, RTX 5080, Fire Emblem: Fortune's Weave docked at 1x (720p -> 1440p) and 2x (1440p -> 2880p), CLI runs and GUI play, stopping and restarting games with DLSS on. Not tested on other GPUs or games. Assisted-by: Claude Opus 5.5 (implementation, debugging) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>
-rw-r--r--src/common/fs/fs_paths.h1
-rw-r--r--src/common/fs/path_util.cpp1
-rw-r--r--src/common/fs/path_util.h1
-rw-r--r--src/common/settings.h2
-rw-r--r--src/qt_common/config/shared_translation.cpp5
-rw-r--r--src/video_core/CMakeLists.txt18
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.cpp6
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.h3
-rw-r--r--src/video_core/renderer_vulkan/vk_streamline_dlss.cpp386
-rw-r--r--src/video_core/renderer_vulkan/vk_streamline_dlss.h80
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp122
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h17
-rw-r--r--src/video_core/texture_cache/texture_cache.h4
-rw-r--r--src/video_core/vulkan_common/streamline.cpp463
-rw-r--r--src/video_core/vulkan_common/streamline.h63
-rw-r--r--src/video_core/vulkan_common/streamline_api.h36
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp47
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h7
-rw-r--r--src/video_core/vulkan_common/vulkan_instance.cpp20
-rw-r--r--src/yuzu/configuration/configure_graphics.cpp6
20 files changed, 1271 insertions, 17 deletions
diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h
index ae25846fc2..763a8759dd 100644
--- a/src/common/fs/fs_paths.h
+++ b/src/common/fs/fs_paths.h
@@ -28,6 +28,7 @@
#define SCREENSHOTS_DIR "screenshots"
#define SDMC_DIR "sdmc"
#define SHADER_DIR "shader"
+#define STREAMLINE_DIR "streamline"
#define TAS_DIR "tas"
#define ICONS_DIR "icons"
diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp
index 82ea8f6cb2..f77da2e301 100644
--- a/src/common/fs/path_util.cpp
+++ b/src/common/fs/path_util.cpp
@@ -168,6 +168,7 @@ public:
GenerateEdenPath(EdenPath::SDMCDir, eden_path / SDMC_DIR);
GenerateEdenPath(EdenPath::TASDir, eden_path / TAS_DIR);
GenerateEdenPath(EdenPath::IconsDir, eden_path / ICONS_DIR);
+ GenerateEdenPath(EdenPath::StreamlineDir, eden_path / STREAMLINE_DIR);
// config
GenerateEdenPath(EdenPath::ConfigDir, eden_path_config);
// cache
diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h
index e8fa5614ae..940dbb9186 100644
--- a/src/common/fs/path_util.h
+++ b/src/common/fs/path_util.h
@@ -34,6 +34,7 @@ enum class EdenPath {
ShaderDir, // Where shaders are stored.
TASDir, // Where TAS scripts are stored.
IconsDir, // Where Icons for Windows shortcuts are stored.
+ StreamlineDir, // Where the user-supplied NVIDIA Streamline loader is stored.
};
// migration/compat dirs
diff --git a/src/common/settings.h b/src/common/settings.h
index 6669f98f4c..dc442a7180 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -378,6 +378,8 @@ struct Values {
Specialization::Percentage,
true,
true};
+ // NVIDIA DLSS (through NVIDIA Streamline) in place of a game's DLSS.
+ SwitchableSetting<bool> nvidia_dlss{linkage, false, "nvidia_dlss", Category::Renderer};
SwitchableSetting<AspectRatio, true> aspect_ratio{linkage,
AspectRatio::R16_9,
"aspect_ratio",
diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp
index c5907e1b65..b395b2959c 100644
--- a/src/qt_common/config/shared_translation.cpp
+++ b/src/qt_common/config/shared_translation.cpp
@@ -149,6 +149,11 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QObject* parent) {
tr("Strength of RCAS (AMD's contrast-adaptive sharpening) on the upscaler used in place "
"of a game's DLSS (AMD FSR 3.1); 0% turns it off.\nAlso sets the sharpening "
"of the AMD FSR 1 and SGSR window adapting filters."));
+ INSERT(Settings, nvidia_dlss, tr("Use NVIDIA DLSS in place of a game's DLSS"),
+ tr("Uses NVIDIA DLSS on GeForce RTX cards in place of a Switch 2 game's own DLSS, "
+ "instead of AMD FSR 3.1.\nNeeds NVIDIA's Streamline loader, and NVIDIA DLSS unless "
+ "the NVIDIA app has downloaded it; Eglantine does not ship NVIDIA's files.\nTakes "
+ "effect when a game starts."));
INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"),
tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA "
"can produce a more stable picture in lower resolutions."));
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index d0e84c324c..e474d08fba 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -499,4 +499,22 @@ if (ANDROID AND ARCHITECTURE_arm64)
target_link_libraries(video_core PRIVATE adrenotools)
endif()
+# NVIDIA DLSS through NVIDIA Streamline, in place of a game's DLSS (before FSR 3.1) when the
+# setting is on. Only Streamline's MIT-licensed headers are compiled in and nothing of NVIDIA's is
+# linked or copied: sl.interposer.dll and the DLSS plugins are loaded at run time from the user's PC.
+set(EGLANTINE_STREAMLINE_SDK "" CACHE PATH "NVIDIA Streamline SDK directory (headers only)")
+if (EGLANTINE_STREAMLINE_SDK AND WIN32)
+ target_sources(video_core PRIVATE
+ renderer_vulkan/vk_streamline_dlss.cpp
+ renderer_vulkan/vk_streamline_dlss.h
+ vulkan_common/streamline.cpp
+ vulkan_common/streamline.h
+ vulkan_common/streamline_api.h
+ )
+ target_include_directories(video_core PRIVATE ${EGLANTINE_STREAMLINE_SDK}/include)
+ target_link_libraries(video_core PRIVATE version wintrust crypt32)
+ target_compile_definitions(video_core PUBLIC EGLANTINE_STREAMLINE)
+ message(STATUS "NVIDIA DLSS through Streamline: ${EGLANTINE_STREAMLINE_SDK}")
+endif()
+
create_target_directory_groups(video_core)
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.cpp b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
index 27ca531186..fc0eea84ab 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.cpp
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.cpp
@@ -41,6 +41,9 @@
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
#include "video_core/vulkan_common/vulkan_surface.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
+#ifdef EGLANTINE_STREAMLINE
+#include "video_core/vulkan_common/streamline.h"
+#endif
#ifdef __ANDROID__
#include <jni.h>
#endif
@@ -130,6 +133,9 @@ try
, gpu(gpu_)
, library(OpenLibrary(context.get()))
, dld()
+#ifdef EGLANTINE_STREAMLINE
+ , streamline_started(Streamline::Start())
+#endif
// Create raw Vulkan instance first
, instance(CreateInstance(*library,
dld,
diff --git a/src/video_core/renderer_vulkan/renderer_vulkan.h b/src/video_core/renderer_vulkan/renderer_vulkan.h
index 87422dd935..67587cde37 100644
--- a/src/video_core/renderer_vulkan/renderer_vulkan.h
+++ b/src/video_core/renderer_vulkan/renderer_vulkan.h
@@ -83,6 +83,9 @@ private:
std::shared_ptr<Common::DynamicLibrary> library;
vk::InstanceDispatch dld;
+#ifdef EGLANTINE_STREAMLINE
+ bool streamline_started; ///< NVIDIA Streamline starts before the instance.
+#endif
// Keep original handles for compatibility with existing code
vk::Instance instance;
diff --git a/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp
new file mode 100644
index 0000000000..aef32735f9
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp
@@ -0,0 +1,386 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// NVIDIA DLSS (through NVIDIA Streamline) in place of a game's DLSS evaluation.
+
+#include <array>
+#include <cstdlib>
+#include <string_view>
+
+#include "common/logging.h"
+#include "video_core/renderer_vulkan/vk_scheduler.h"
+#include "video_core/renderer_vulkan/vk_streamline_dlss.h"
+#include "video_core/vulkan_common/streamline.h"
+#include "video_core/vulkan_common/streamline_api.h"
+#include "video_core/vulkan_common/vulkan_device.h"
+#include "video_core/vulkan_common/vulkan_memory_allocator.h"
+
+namespace Vulkan {
+namespace {
+
+constexpr VkFormat OUTPUT_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT;
+// Streamline clears the output before DLSS writes it.
+constexpr VkImageUsageFlags OUTPUT_USAGE = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT |
+ VK_IMAGE_USAGE_TRANSFER_SRC_BIT |
+ VK_IMAGE_USAGE_TRANSFER_DST_BIT;
+
+int EnvInt(const char* name, int fallback) {
+ const char* const value = std::getenv(name);
+ return value ? std::atoi(value) : fallback;
+}
+
+// DLSS validates the render size against the mode's range: pick the mode for this ratio.
+sl::DLSSMode ModeFor(VkExtent2D render, VkExtent2D upscale) {
+ const f32 ratio = static_cast<f32>(render.width) / static_cast<f32>(upscale.width);
+ if (ratio >= 0.99f) {
+ return sl::DLSSMode::eDLAA;
+ }
+ if (ratio >= 0.65f) {
+ return sl::DLSSMode::eMaxQuality;
+ }
+ if (ratio >= 0.57f) {
+ return sl::DLSSMode::eBalanced;
+ }
+ if (ratio >= 0.49f) {
+ return sl::DLSSMode::eMaxPerformance;
+ }
+ return sl::DLSSMode::eUltraPerformance;
+}
+
+// Preset K for every mode: NVIDIA's defaults for Performance/Ultra Performance (M, L) draw thin
+// horizontal lines in Fire Emblem at 2x resolution scale. EDEN_NX2_DLSS_PRESET=J|K|L|M picks
+// another model, EDEN_NX2_DLSS_PRESET=default leaves the choice to NVIDIA.
+sl::DLSSPreset Preset() {
+ const char* const value = std::getenv("EDEN_NX2_DLSS_PRESET");
+ if (value && value[0] >= 'J' && value[0] <= 'M') {
+ return static_cast<sl::DLSSPreset>(static_cast<u32>(sl::DLSSPreset::ePresetJ) +
+ static_cast<u32>(value[0] - 'J'));
+ }
+ if (value && std::string_view{value} == "default") {
+ return sl::DLSSPreset::eDefault;
+ }
+ return sl::DLSSPreset::ePresetK;
+}
+
+sl::float4x4 Identity() {
+ sl::float4x4 matrix;
+ matrix.setRow(0, {1.0f, 0.0f, 0.0f, 0.0f});
+ matrix.setRow(1, {0.0f, 1.0f, 0.0f, 0.0f});
+ matrix.setRow(2, {0.0f, 0.0f, 1.0f, 0.0f});
+ matrix.setRow(3, {0.0f, 0.0f, 0.0f, 1.0f});
+ return matrix;
+}
+
+VkImageMemoryBarrier Barrier(const StreamlineDlssUpscaler::Image& image, VkAccessFlags src_access,
+ VkAccessFlags dst_access, VkImageLayout old_layout,
+ VkImageLayout new_layout) {
+ VkImageSubresourceRange range = image.range;
+ range.aspectMask = image.barrier_aspect;
+ return VkImageMemoryBarrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = src_access,
+ .dstAccessMask = dst_access,
+ .oldLayout = old_layout,
+ .newLayout = new_layout,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = image.image,
+ .subresourceRange = range,
+ };
+}
+
+sl::Resource Resource(VkImage image, VkImageView view, VkFormat format, VkImageLayout layout,
+ VkExtent2D size, VkImageUsageFlags usage) {
+ sl::Resource resource{sl::ResourceType::eTex2d, image, nullptr, view,
+ static_cast<uint32_t>(layout)};
+ resource.width = size.width;
+ resource.height = size.height;
+ resource.nativeFormat = static_cast<uint32_t>(format);
+ resource.mipLevels = 1;
+ resource.arrayLayers = 1;
+ resource.flags = 0;
+ resource.usage = usage;
+ return resource;
+}
+
+} // Anonymous namespace
+
+StreamlineDlssUpscaler::StreamlineDlssUpscaler(const Device& device_, Scheduler& scheduler_,
+ MemoryAllocator& memory_allocator_)
+ : device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_} {
+ if (!Streamline::IsDlssAvailable()) {
+ throw vk::Exception(VK_ERROR_FEATURE_NOT_PRESENT);
+ }
+ const sl::Result result = Streamline::GetApi().get_feature_function(
+ sl::kFeatureDLSS, "slDLSSSetOptions", set_options);
+ if (result != sl::Result::eOk || !set_options) {
+ LOG_ERROR(Render_Vulkan, "Streamline's DLSS has no slDLSSSetOptions (result {})",
+ static_cast<int>(result));
+ throw vk::Exception(VK_ERROR_FEATURE_NOT_PRESENT);
+ }
+ point_sampler = device.GetLogical().CreateSampler(VkSamplerCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .magFilter = VK_FILTER_NEAREST,
+ .minFilter = VK_FILTER_NEAREST,
+ .mipmapMode = VK_SAMPLER_MIPMAP_MODE_NEAREST,
+ .addressModeU = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .addressModeV = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE,
+ .mipLodBias = 0.0f,
+ .anisotropyEnable = VK_FALSE,
+ .maxAnisotropy = 1.0f,
+ .compareEnable = VK_FALSE,
+ .compareOp = VK_COMPARE_OP_NEVER,
+ .minLod = 0.0f,
+ .maxLod = VK_LOD_CLAMP_NONE,
+ .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK,
+ .unnormalizedCoordinates = VK_FALSE,
+ });
+}
+
+StreamlineDlssUpscaler::~StreamlineDlssUpscaler() {
+ // Not scheduler.Finish(): this runs when the renderer goes away, after the game's channels are
+ // released, and a submit would touch their state. Nothing more gets recorded by then.
+ void(device.GetLogical().WaitIdle());
+ if (Streamline::IsDlssAvailable()) {
+ Streamline::GetApi().free_resources(sl::kFeatureDLSS, sl::ViewportHandle{0u});
+ }
+}
+
+void StreamlineDlssUpscaler::CreateOutput(VkExtent2D upscale_size) {
+ if (output_image) {
+ scheduler.Finish(); // the old output may still be in use
+ }
+ output_image = memory_allocator.CreateImage(VkImageCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = OUTPUT_FORMAT,
+ .extent = {upscale_size.width, upscale_size.height, 1},
+ .mipLevels = 1,
+ .arrayLayers = 1,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .tiling = VK_IMAGE_TILING_OPTIMAL,
+ .usage = OUTPUT_USAGE,
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .queueFamilyIndexCount = 0,
+ .pQueueFamilyIndices = nullptr,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ });
+ output_view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .image = *output_image,
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = OUTPUT_FORMAT,
+ .components{},
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ },
+ });
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([image = *output_image](vk::CommandBuffer cmdbuf) {
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0,
+ VkImageMemoryBarrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = 0,
+ .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
+ .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = image,
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ },
+ });
+ });
+}
+
+bool StreamlineDlssUpscaler::Upscale(const Inputs& in) {
+ if (failed) {
+ return false;
+ }
+ bool reset = in.reset;
+ bool options_changed = false;
+ if (!output_image || in.render_size.width != render_extent.width ||
+ in.render_size.height != render_extent.height ||
+ in.upscale_size.width != upscale_extent.width ||
+ in.upscale_size.height != upscale_extent.height) {
+ CreateOutput(in.upscale_size);
+ render_extent = in.render_size;
+ upscale_extent = in.upscale_size;
+ options_changed = true;
+ reset = true;
+ }
+
+ // The game's own setup (Fortune's Weave: render-size motion vectors, reversed depth). Its
+ // colour is linear and goes above 1 with no exposure passed, so by default DLSS gets the HDR
+ // path with its own auto exposure. EDEN_NX2_DLSS_HDR=0 / EDEN_NX2_DLSS_AUTO_EXPOSURE=0 turn
+ // those off.
+ sl::DLSSOptions options{};
+ options.mode = ModeFor(in.render_size, in.upscale_size);
+ options.outputWidth = in.upscale_size.width;
+ options.outputHeight = in.upscale_size.height;
+ options.colorBuffersHDR = EnvInt("EDEN_NX2_DLSS_HDR", 1) != 0 ? sl::eTrue : sl::eFalse;
+ options.useAutoExposure =
+ EnvInt("EDEN_NX2_DLSS_AUTO_EXPOSURE", 1) != 0 ? sl::eTrue : sl::eFalse;
+ const sl::DLSSPreset preset = Preset();
+ options.dlaaPreset = preset;
+ options.qualityPreset = preset;
+ options.balancedPreset = preset;
+ options.performancePreset = preset;
+ options.ultraPerformancePreset = preset;
+ options.ultraQualityPreset = preset;
+ if (options_changed) {
+ LOG_INFO(Render_Vulkan, "NVIDIA DLSS (Streamline): {}x{} -> {}x{}, mode {}, preset {}",
+ in.render_size.width, in.render_size.height, in.upscale_size.width,
+ in.upscale_size.height, static_cast<int>(options.mode),
+ static_cast<int>(preset));
+ }
+
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([this, in, reset, options, options_changed, index = frame_index++,
+ output_image_handle = *output_image,
+ output_view_handle = *output_view](vk::CommandBuffer cmdbuf) {
+ const Streamline::Api& api = Streamline::GetApi();
+ const sl::ViewportHandle viewport{0u};
+ if (options_changed) {
+ const auto set = reinterpret_cast<PFun_slDLSSSetOptions*>(set_options);
+ if (const sl::Result result = set(viewport, options); result != sl::Result::eOk) {
+ if (!failed.exchange(true)) {
+ LOG_ERROR(Render_Vulkan,
+ "Streamline rejected the DLSS options (result {}); using FSR 3.1",
+ static_cast<int>(result));
+ }
+ return;
+ }
+ }
+ sl::FrameToken* frame{};
+ if (const sl::Result result = api.get_new_frame_token(frame, &index);
+ result != sl::Result::eOk || !frame) {
+ if (!failed.exchange(true)) {
+ LOG_ERROR(Render_Vulkan, "Streamline gave no frame token (result {}); using FSR 3.1",
+ static_cast<int>(result));
+ }
+ return;
+ }
+
+ constexpr VkAccessFlags WRITES =
+ VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT |
+ VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_TRANSFER_WRITE_BIT;
+ // The inputs come from the game's draws and dispatches and stay in GENERAL otherwise;
+ // DLSS reads them as sampled images.
+ const std::array to_read{
+ Barrier(in.color, WRITES, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL),
+ Barrier(in.depth, WRITES, VK_ACCESS_SHADER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL),
+ Barrier(in.motion_vectors, WRITES, VK_ACCESS_SHADER_READ_BIT,
+ VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL),
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, to_read);
+
+ // DLSS super resolution only reads the jitter, motion and depth conventions; the camera
+ // fields are for frame generation, which this doesn't use.
+ const f32 rw = static_cast<f32>(in.render_size.width);
+ const f32 rh = static_cast<f32>(in.render_size.height);
+ sl::Constants constants{};
+ constants.cameraViewToClip = Identity();
+ constants.clipToCameraView = Identity();
+ constants.clipToLensClip = Identity();
+ constants.clipToPrevClip = Identity();
+ constants.prevClipToClip = Identity();
+ constants.jitterOffset = {in.jitter[0], in.jitter[1]};
+ // Streamline wants motion in [-1,1] screen units; ours turns into render pixels.
+ constants.mvecScale = {in.motion_vector_scale[0] / rw, in.motion_vector_scale[1] / rh};
+ constants.cameraPinholeOffset = {0.0f, 0.0f};
+ constants.cameraPos = {0.0f, 0.0f, 0.0f};
+ constants.cameraUp = {0.0f, 1.0f, 0.0f};
+ constants.cameraRight = {1.0f, 0.0f, 0.0f};
+ constants.cameraFwd = {0.0f, 0.0f, 1.0f};
+ constants.cameraNear = 0.1f;
+ constants.cameraFar = 1000.0f;
+ constants.cameraFOV = 1.0f;
+ constants.cameraAspectRatio = rw / rh;
+ constants.depthInverted = sl::eTrue;
+ constants.cameraMotionIncluded = sl::eTrue;
+ constants.motionVectors3D = sl::eFalse;
+ constants.reset = reset ? sl::eTrue : sl::eFalse;
+ constants.motionVectorsJittered = sl::eFalse;
+ api.set_constants(constants, *frame, viewport);
+
+ sl::Resource color = Resource(in.color.image, in.color.view, in.color.format,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, in.render_size,
+ VK_IMAGE_USAGE_SAMPLED_BIT);
+ sl::Resource depth = Resource(in.depth.image, in.depth.view, in.depth.format,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, in.render_size,
+ VK_IMAGE_USAGE_SAMPLED_BIT);
+ sl::Resource motion = Resource(in.motion_vectors.image, in.motion_vectors.view,
+ in.motion_vectors.format,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, in.render_size,
+ VK_IMAGE_USAGE_SAMPLED_BIT);
+ sl::Resource output = Resource(output_image_handle, output_view_handle, OUTPUT_FORMAT,
+ VK_IMAGE_LAYOUT_GENERAL, in.upscale_size, OUTPUT_USAGE);
+ const sl::Extent render_area{0, 0, in.render_size.width, in.render_size.height};
+ const sl::Extent output_area{0, 0, in.upscale_size.width, in.upscale_size.height};
+ // Local tags: everything stays put until the evaluation is recorded.
+ sl::ResourceTag color_tag{&color, sl::kBufferTypeScalingInputColor,
+ sl::ResourceLifecycle::eValidUntilEvaluate, &render_area};
+ sl::ResourceTag depth_tag{&depth, sl::kBufferTypeDepth,
+ sl::ResourceLifecycle::eValidUntilEvaluate, &render_area};
+ sl::ResourceTag motion_tag{&motion, sl::kBufferTypeMotionVectors,
+ sl::ResourceLifecycle::eValidUntilEvaluate, &render_area};
+ sl::ResourceTag output_tag{&output, sl::kBufferTypeScalingOutputColor,
+ sl::ResourceLifecycle::eValidUntilEvaluate, &output_area};
+ const sl::BaseStructure* inputs[] = {&viewport, &color_tag, &depth_tag, &motion_tag,
+ &output_tag};
+ const sl::Result result =
+ api.evaluate_feature(sl::kFeatureDLSS, *frame, inputs, std::size(inputs),
+ reinterpret_cast<sl::CommandBuffer*>(*cmdbuf));
+ if (result != sl::Result::eOk && !failed.exchange(true)) {
+ LOG_ERROR(Render_Vulkan, "NVIDIA DLSS evaluation failed (Streamline result {}); "
+ "using FSR 3.1 from now on",
+ static_cast<int>(result));
+ }
+
+ const std::array to_general{
+ Barrier(in.color, VK_ACCESS_SHADER_READ_BIT, 0,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL),
+ Barrier(in.depth, VK_ACCESS_SHADER_READ_BIT, 0,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL),
+ Barrier(in.motion_vectors, VK_ACCESS_SHADER_READ_BIT, 0,
+ VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL),
+ };
+ const VkMemoryBarrier output_written{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, output_written, {},
+ to_general);
+ });
+ // DLSS binds its own pipelines and descriptors on the command buffer.
+ scheduler.InvalidateState();
+ return true;
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_streamline_dlss.h b/src/video_core/renderer_vulkan/vk_streamline_dlss.h
new file mode 100644
index 0000000000..9f8704fe15
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.h
@@ -0,0 +1,80 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// NVIDIA DLSS (through NVIDIA Streamline) in place of a game's DLSS
+// evaluation. NVIDIA's DLLs come from the user's PC; see vulkan_common/streamline.h.
+
+#pragma once
+
+#include <array>
+#include <atomic>
+
+#include "common/common_types.h"
+#include "video_core/vulkan_common/vulkan_wrapper.h"
+
+namespace Vulkan {
+
+class Device;
+class MemoryAllocator;
+class Scheduler;
+
+class StreamlineDlssUpscaler {
+public:
+ struct Image {
+ VkImageView view;
+ VkImage image;
+ VkFormat format;
+ VkImageSubresourceRange range; ///< The view's level and layer, with the view's aspect.
+ VkImageAspectFlags barrier_aspect; ///< Every aspect of the image's format.
+ };
+
+ struct Inputs {
+ Image color; ///< Jittered linear colour at the render size.
+ Image depth; ///< Device depth (reversed) at the render size.
+ Image motion_vectors; ///< Motion to the previous frame at the render size.
+ VkExtent2D render_size;
+ VkExtent2D upscale_size;
+ /// This frame's sample offset in render pixels, as the game hands its own DLSS.
+ std::array<f32, 2> jitter;
+ /// Turns a motion vector into render pixels (the render size for UV offsets).
+ std::array<f32, 2> motion_vector_scale;
+ bool reset;
+ };
+
+ /// Throws vk::Exception when Streamline has no DLSS for this device.
+ explicit StreamlineDlssUpscaler(const Device& device, Scheduler& scheduler,
+ MemoryAllocator& memory_allocator);
+ ~StreamlineDlssUpscaler();
+
+ /// Records one evaluation into the output image (RGBA16F at the upscale size, GENERAL
+ /// layout). Returns false when DLSS can't run, so the caller can fall back to FSR 3.1.
+ bool Upscale(const Inputs& inputs);
+
+ [[nodiscard]] VkImage OutputImage() const noexcept {
+ return *output_image;
+ }
+ [[nodiscard]] VkImageView OutputView() const noexcept {
+ return *output_view;
+ }
+ /// Nearest-neighbour clamp sampler (normalized coordinates), for reading the output.
+ [[nodiscard]] VkSampler PointSampler() const noexcept {
+ return *point_sampler;
+ }
+
+private:
+ void CreateOutput(VkExtent2D upscale_size);
+
+ const Device& device;
+ Scheduler& scheduler;
+ MemoryAllocator& memory_allocator;
+ void* set_options{}; ///< slDLSSSetOptions
+ VkExtent2D render_extent{};
+ VkExtent2D upscale_extent{};
+ u32 frame_index{};
+ vk::Image output_image;
+ vk::ImageView output_view;
+ vk::Sampler point_sampler;
+ std::atomic<bool> failed{};
+};
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index e3f427dead..d7093997c4 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -39,6 +39,9 @@
#include "video_core/texture_cache/util.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_memory_allocator.h"
+#ifdef EGLANTINE_STREAMLINE
+#include "video_core/vulkan_common/streamline.h"
+#endif
#include "video_core/vulkan_common/vulkan_wrapper.h"
#include "video_core/textures/decoders.h"
@@ -1556,6 +1559,21 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie
if (!WantsTemporalUpscaleDepth()) {
return false;
}
+ const VkExtent2D extent = dst_framebuffer->RenderArea();
+ // A gap in the evaluations (a menu, a movie, a load) starts the history again.
+ const auto now = std::chrono::steady_clock::now();
+ const f32 frame_time_ms =
+ fsr3_last_evaluation
+ ? std::chrono::duration<f32, std::milli>(now - *fsr3_last_evaluation).count()
+ : 1000.0f / 30.0f;
+ const bool reset = !fsr3_last_evaluation || frame_time_ms > 250.0f;
+ fsr3_last_evaluation = now;
+#ifdef EGLANTINE_STREAMLINE
+ if (UpscaleWithDlss(dst_framebuffer, input_color, motion_vectors, depth, render_size, jitter,
+ motion_scale, reset)) {
+ return true;
+ }
+#endif
if (!fsr3_upscaler) {
if (!Fsr3Upscaler::IsSupported(device)) {
LOG_WARNING(Render_Vulkan, "FSR 3.1 needs formats or features this device lacks; "
@@ -1574,15 +1592,6 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie
return false;
}
}
- const VkExtent2D extent = dst_framebuffer->RenderArea();
- // A gap in the evaluations (a menu, a movie, a load) starts the history again.
- const auto now = std::chrono::steady_clock::now();
- const f32 frame_time_ms =
- fsr3_last_evaluation
- ? std::chrono::duration<f32, std::milli>(now - *fsr3_last_evaluation).count()
- : 1000.0f / 30.0f;
- const bool reset = !fsr3_last_evaluation || frame_time_ms > 250.0f;
- fsr3_last_evaluation = now;
// FSR's own RCAS, set by the sharpness slider: FSR takes 0-1 for 2 to 0 stops, and the slider
// stores stops x 100, so this is the percentage the slider shows; 0% (stored 200) is off.
// EDEN_NX2_TEMPORAL_SHARPEN=<stops>|off overrides the slider.
@@ -1608,15 +1617,28 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie
.sharpen = sharpen,
.sharpness = 1.0f - stops / 2.0f,
});
- // Draw the result into the DLSS output, which keeps its own format; the blit leaves FSR's
- // output image in SHADER_READ_ONLY_OPTIMAL, so return it to GENERAL for the next frame.
+ DrawUpscaledOutput(dst_framebuffer, fsr3_upscaler->OutputView(), fsr3_upscaler->OutputImage(),
+ fsr3_upscaler->PointSampler());
+ static bool logged{};
+ if (!logged) {
+ logged = true;
+ LOG_INFO(Render_Vulkan, "DLSS replaced by FSR 3.1: {}x{} -> {}x{}", render_size[0],
+ render_size[1], extent.width, extent.height);
+ }
+ return true;
+}
+
+void TextureCacheRuntime::DrawUpscaledOutput(Framebuffer* dst_framebuffer, VkImageView view,
+ VkImage output, VkSampler sampler) {
+ // Draw the result into the DLSS output, which keeps its own format; the blit leaves the
+ // upscaler's output image in SHADER_READ_ONLY_OPTIMAL, so return it to GENERAL for the next
+ // frame.
+ const VkExtent2D extent = dst_framebuffer->RenderArea();
const Region2D region{
.start = {0, 0},
.end = {static_cast<s32>(extent.width), static_cast<s32>(extent.height)},
};
- const VkImage output = fsr3_upscaler->OutputImage();
- blit_image_helper.BlitColor(dst_framebuffer, fsr3_upscaler->OutputView(), output,
- fsr3_upscaler->PointSampler(), region, region,
+ blit_image_helper.BlitColor(dst_framebuffer, view, output, sampler, region, region,
{extent.width, extent.height, 1});
scheduler.RequestOutsideRenderPassOperationContext();
scheduler.Record([output](vk::CommandBuffer cmdbuf) {
@@ -1641,14 +1663,84 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, barrier);
});
+}
+
+#ifdef EGLANTINE_STREAMLINE
+bool TextureCacheRuntime::UpscaleWithDlss(Framebuffer* dst_framebuffer, ImageView& input_color,
+ ImageView& motion_vectors, ImageView& depth,
+ std::array<u32, 2> render_size,
+ std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale, bool reset) {
+ // EDEN_NX2_DLSS_REPLACEMENT=fsr keeps FSR 3.1 for A/B runs.
+ static const bool force_fsr = [] {
+ const char* const value = std::getenv("EDEN_NX2_DLSS_REPLACEMENT");
+ return value && std::string_view{value} == "fsr";
+ }();
+ if (force_fsr || dlss_unsupported) {
+ return false;
+ }
+ if (!dlss_upscaler) {
+ // Off in the settings, or Streamline didn't start: it has said why.
+ if (!Streamline::IsDlssAvailable()) {
+ dlss_unsupported = true;
+ return false;
+ }
+ try {
+ dlss_upscaler =
+ std::make_unique<StreamlineDlssUpscaler>(device, scheduler, memory_allocator);
+ } catch (const vk::Exception& exception) {
+ LOG_WARNING(Render_Vulkan, "NVIDIA DLSS unavailable ({}); using FSR 3.1",
+ exception.what());
+ dlss_unsupported = true;
+ return false;
+ }
+ }
+ const auto image = [&](ImageView& view, VkImageView handle, VkImageAspectFlags aspect) {
+ return StreamlineDlssUpscaler::Image{
+ .view = handle,
+ .image = view.ImageHandle(),
+ .format = MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, view.format)
+ .format,
+ .range{
+ .aspectMask = aspect,
+ .baseMipLevel = static_cast<u32>(view.range.base.level),
+ .levelCount = 1,
+ .baseArrayLayer = static_cast<u32>(view.range.base.layer),
+ .layerCount = 1,
+ },
+ .barrier_aspect = ImageAspectMask(view.format),
+ };
+ };
+ const VkExtent2D extent = dst_framebuffer->RenderArea();
+ const bool upscaled = dlss_upscaler->Upscale({
+ .color = image(input_color, input_color.Handle(Shader::TextureType::Color2D),
+ VK_IMAGE_ASPECT_COLOR_BIT),
+ .depth = image(depth, depth.DepthView(), VK_IMAGE_ASPECT_DEPTH_BIT),
+ .motion_vectors = image(motion_vectors,
+ motion_vectors.Handle(Shader::TextureType::Color2D),
+ VK_IMAGE_ASPECT_COLOR_BIT),
+ .render_size = {render_size[0], render_size[1]},
+ .upscale_size = extent,
+ .jitter = jitter,
+ .motion_vector_scale = {motion_scale[0] * static_cast<f32>(render_size[0]),
+ motion_scale[1] * static_cast<f32>(render_size[1])},
+ .reset = reset,
+ });
+ if (!upscaled) {
+ dlss_unsupported = true;
+ return false;
+ }
+ DrawUpscaledOutput(dst_framebuffer, dlss_upscaler->OutputView(), dlss_upscaler->OutputImage(),
+ dlss_upscaler->PointSampler());
static bool logged{};
if (!logged) {
logged = true;
- LOG_INFO(Render_Vulkan, "DLSS replaced by FSR 3.1: {}x{} -> {}x{}", render_size[0],
+ LOG_INFO(Render_Vulkan, "DLSS replaced by NVIDIA DLSS: {}x{} -> {}x{}", render_size[0],
render_size[1], extent.width, extent.height);
}
return true;
}
+#endif
bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst,
ImageView& input_color, ImageView& motion_vectors,
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index e7dbb97628..44bd21615b 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -17,6 +17,9 @@
#include "shader_recompiler/shader_info.h"
#include "video_core/renderer_vulkan/vk_compute_pass.h"
#include "video_core/renderer_vulkan/vk_fsr3_upscaler.h"
+#ifdef EGLANTINE_STREAMLINE
+#include "video_core/renderer_vulkan/vk_streamline_dlss.h"
+#endif
#include "video_core/renderer_vulkan/vk_render_pass_cache.h"
#include "video_core/renderer_vulkan/vk_staging_buffer_pool.h"
#include "video_core/texture_cache/image_view_base.h"
@@ -208,6 +211,20 @@ public:
bool fsr3_unsupported{};
std::optional<std::chrono::steady_clock::time_point> fsr3_last_evaluation;
+ /// Draws an upscaler's RGBA16F output into the DLSS output and returns it to GENERAL.
+ void DrawUpscaledOutput(Framebuffer* dst_framebuffer, VkImageView view, VkImage image,
+ VkSampler sampler);
+#ifdef EGLANTINE_STREAMLINE
+ /// NVIDIA's DLSS (through NVIDIA Streamline) in place of the game's,
+ /// before FSR 3.1, when the setting is on. Returns false to fall back to FSR 3.1.
+ bool UpscaleWithDlss(Framebuffer* dst_framebuffer, ImageView& input_color,
+ ImageView& motion_vectors, ImageView& depth,
+ std::array<u32, 2> render_size, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale, bool reset);
+ std::unique_ptr<StreamlineDlssUpscaler> dlss_upscaler;
+ bool dlss_unsupported{};
+#endif
+
/// TemporalUpscale's history: its previous result (images[current]) and the image the next
/// result is stored to.
struct TemporalHistory {
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index ea82ab02e7..a34c591aca 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -1124,8 +1124,8 @@ bool TextureCache<P>::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds
has_deleted_images = false;
const ImageId motion_id =
FindOrInsertImage(motion_info, motion_vectors.Address(), RelaxedOptions{});
- // The depth only feeds FSR 3.1 (looked up only when FSR can run); without it the stand-in
- // runs.
+ // The depth only feeds NVIDIA DLSS and FSR 3.1 (looked up only when they can run); without
+ // it the stand-in runs.
ImageId depth_id{};
if (depth && motion_id && runtime.WantsTemporalUpscaleDepth()) {
depth_id = FindOrInsertImage(ImageInfo(*depth), depth->Address(), RelaxedOptions{});
diff --git a/src/video_core/vulkan_common/streamline.cpp b/src/video_core/vulkan_common/streamline.cpp
new file mode 100644
index 0000000000..966de82cb5
--- /dev/null
+++ b/src/video_core/vulkan_common/streamline.cpp
@@ -0,0 +1,463 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// NVIDIA DLSS through NVIDIA Streamline, with Streamline's manual hooking.
+
+#include <algorithm>
+#include <array>
+#include <cstdlib>
+#include <filesystem>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <type_traits>
+#include <vector>
+
+#include <fmt/format.h>
+
+#include "common/fs/path_util.h"
+#include "common/logging.h"
+#include "common/settings.h"
+#include "video_core/vulkan_common/streamline.h"
+#include "video_core/vulkan_common/streamline_api.h"
+
+#include <sl_security.h>
+
+#include <softpub.h>
+#include <wintrust.h>
+
+namespace Vulkan::Streamline {
+namespace {
+
+// A custom engine names itself with its own project id; an application id from NVIDIA is only
+// needed without an engine type. Keep it fixed: the driver may key behaviour off it.
+constexpr const char* PROJECT_ID = "8d3e2f9a-6b41-4c7e-9a52-1f0c3b7d5e64";
+constexpr const wchar_t* INTERPOSER = L"sl.interposer.dll";
+constexpr const wchar_t* MODEL = L"nvngx_dlss.dll";
+// The first DLSS with preset K.
+constexpr u32 MIN_DLSS_MAJOR = 310;
+
+using Version = std::array<u32, 4>;
+
+struct State {
+ HMODULE module{};
+ Api api{};
+ std::wstring plugin_dir;
+ std::wstring model_dir;
+ std::vector<std::string> instance_extensions;
+ std::vector<std::string> device_extensions;
+ bool started{};
+ bool attached{};
+};
+
+State& GetState() {
+ static State state;
+ return state;
+}
+
+int LogLevel() {
+ const char* const value = std::getenv("EDEN_NX2_DLSS_LOG");
+ return value ? std::atoi(value) : 0;
+}
+
+void LogFromStreamline(sl::LogType type, const char* message) {
+ std::string_view text{message ? message : ""};
+ while (!text.empty() && (text.back() == '\n' || text.back() == '\r')) {
+ text.remove_suffix(1);
+ }
+ switch (type) {
+ case sl::LogType::eError:
+ LOG_ERROR(Render_Vulkan, "Streamline: {}", text);
+ break;
+ case sl::LogType::eWarn:
+ LOG_WARNING(Render_Vulkan, "Streamline: {}", text);
+ break;
+ default:
+ if (LogLevel() > 0) {
+ LOG_INFO(Render_Vulkan, "Streamline: {}", text);
+ }
+ break;
+ }
+}
+
+/// Where the user may put sl.interposer.dll: EDEN_STREAMLINE_DIR, the user folder's "streamline"
+/// folder, then a "streamline" folder next to the executable.
+std::vector<std::filesystem::path> SearchDirs() {
+ std::vector<std::filesystem::path> dirs;
+ if (const char* const dir = std::getenv("EDEN_STREAMLINE_DIR"); dir && *dir) {
+ dirs.emplace_back(dir);
+ }
+ dirs.push_back(UserFolder());
+ dirs.push_back(Common::FS::GetExeDirectory() / "streamline");
+ return dirs;
+}
+
+template <typename T>
+bool Resolve(HMODULE module, const char* name, T*& function) {
+ function = reinterpret_cast<T*>(GetProcAddress(module, name));
+ if (!function) {
+ LOG_ERROR(Render_Vulkan, "sl.interposer.dll has no {}", name);
+ }
+ return function != nullptr;
+}
+
+std::optional<Version> ReadVersion(const std::filesystem::path& path) {
+ DWORD handle = 0;
+ const DWORD size = GetFileVersionInfoSizeW(path.c_str(), &handle);
+ if (size == 0) {
+ return std::nullopt;
+ }
+ std::vector<u8> data(size);
+ VS_FIXEDFILEINFO* info{};
+ UINT length = 0;
+ if (!GetFileVersionInfoW(path.c_str(), 0, size, data.data()) ||
+ !VerQueryValueW(data.data(), L"\\", reinterpret_cast<void**>(&info), &length) || !info) {
+ return std::nullopt;
+ }
+ return Version{HIWORD(info->dwFileVersionMS), LOWORD(info->dwFileVersionMS),
+ HIWORD(info->dwFileVersionLS), LOWORD(info->dwFileVersionLS)};
+}
+
+std::string VersionText(const std::filesystem::path& path) {
+ const std::optional<Version> version = path.empty() ? std::nullopt : ReadVersion(path);
+ if (!version) {
+ return {};
+ }
+ const Version& v = *version;
+ return v[3] == 0 ? fmt::format("{}.{}.{}", v[0], v[1], v[2])
+ : fmt::format("{}.{}.{}.{}", v[0], v[1], v[2], v[3]);
+}
+
+/// The NVIDIA app keeps every DLSS it downloads under ProgramData; pick the newest.
+std::filesystem::path FindAppModel() {
+ const wchar_t* const program_data = _wgetenv(L"ProgramData");
+ if (!program_data) {
+ return {};
+ }
+ const std::filesystem::path root =
+ std::filesystem::path{program_data} / L"NVIDIA" / L"NGX" / L"models" / L"dlss" / L"versions";
+ std::filesystem::path best_path;
+ Version best{};
+ std::error_code ec;
+ for (const auto& version_dir : std::filesystem::directory_iterator(root, ec)) {
+ std::error_code files_ec;
+ for (const auto& file :
+ std::filesystem::directory_iterator(version_dir.path() / L"files", files_ec)) {
+ if (file.path().extension() != L".bin") {
+ continue;
+ }
+ const std::optional<Version> version = ReadVersion(file.path());
+ if (version && (*version)[0] >= MIN_DLSS_MAJOR && *version > best) {
+ best = *version;
+ best_path = file.path();
+ }
+ }
+ }
+ return best_path;
+}
+
+/// Copies the NVIDIA app's DLSS into our folder as nvngx_dlss.dll, the name Streamline loads.
+std::filesystem::path UseAppModel(const DlssFiles& files) {
+ const std::filesystem::path dir = UserFolder() / L"nvidia-app";
+ const std::filesystem::path target = dir / MODEL;
+ std::error_code ec;
+ const bool current = std::filesystem::is_regular_file(target, ec) &&
+ std::filesystem::file_size(target, ec) ==
+ std::filesystem::file_size(files.app_model, ec) &&
+ ReadVersion(target) == ReadVersion(files.app_model);
+ if (!current) {
+ if (!IsSignedByNvidia(files.app_model)) {
+ LOG_WARNING(Render_Vulkan, "The NVIDIA app's DLSS {} is not signed by NVIDIA",
+ files.app_model.string());
+ return {};
+ }
+ std::filesystem::create_directories(dir, ec);
+ std::filesystem::copy_file(files.app_model, target,
+ std::filesystem::copy_options::overwrite_existing, ec);
+ if (ec) {
+ LOG_WARNING(Render_Vulkan, "Could not copy the NVIDIA app's DLSS to {}: {}",
+ target.string(), ec.message());
+ return {};
+ }
+ }
+ return dir;
+}
+
+bool Load(State& state, const std::filesystem::path& path) {
+ if (!IsStreamlineModule(path)) {
+ LOG_ERROR(Render_Vulkan, "{} is not signed by NVIDIA; not loading it",
+ path.string());
+ return false;
+ }
+ state.module = LoadLibraryW(path.c_str());
+ if (!state.module) {
+ LOG_ERROR(Render_Vulkan, "Could not load {} (error {})", path.string(), GetLastError());
+ return false;
+ }
+ Api& api = state.api;
+ const HMODULE m = state.module;
+ if (Resolve(m, "slInit", api.init) && Resolve(m, "slShutdown", api.shutdown) &&
+ Resolve(m, "slIsFeatureSupported", api.is_feature_supported) &&
+ Resolve(m, "slGetFeatureRequirements", api.get_feature_requirements) &&
+ Resolve(m, "slGetFeatureVersion", api.get_feature_version) &&
+ Resolve(m, "slGetFeatureFunction", api.get_feature_function) &&
+ Resolve(m, "slGetNewFrameToken", api.get_new_frame_token) &&
+ Resolve(m, "slSetConstants", api.set_constants) &&
+ Resolve(m, "slEvaluateFeature", api.evaluate_feature) &&
+ Resolve(m, "slFreeResources", api.free_resources) &&
+ Resolve(m, "slSetVulkanInfo", api.set_vulkan_info)) {
+ return true;
+ }
+ FreeLibrary(state.module);
+ state.module = nullptr;
+ state.api = {};
+ return false;
+}
+
+std::vector<std::string> Names(uint32_t count, const char** names) {
+ std::vector<std::string> result;
+ for (uint32_t i = 0; i < count; ++i) {
+ result.emplace_back(names[i]);
+ }
+ return result;
+}
+
+} // Anonymous namespace
+
+std::filesystem::path UserFolder() {
+ return Common::FS::GetEdenPath(Common::FS::EdenPath::StreamlineDir);
+}
+
+bool IsStreamlineModule(const std::filesystem::path& path) {
+ return sl::security::verifyEmbeddedSignature(path.c_str());
+}
+
+bool IsSignedByNvidia(const std::filesystem::path& path) {
+ const std::wstring file_path = std::filesystem::absolute(path).wstring();
+ WINTRUST_FILE_INFO file{};
+ file.cbStruct = sizeof(file);
+ file.pcwszFilePath = file_path.c_str();
+ WINTRUST_DATA data{};
+ data.cbStruct = sizeof(data);
+ data.dwUIChoice = WTD_UI_NONE;
+ data.fdwRevocationChecks = WTD_REVOKE_NONE;
+ data.dwUnionChoice = WTD_CHOICE_FILE;
+ data.pFile = &file;
+ data.dwStateAction = WTD_STATEACTION_VERIFY;
+ data.dwProvFlags = WTD_CACHE_ONLY_URL_RETRIEVAL;
+ GUID action = WINTRUST_ACTION_GENERIC_VERIFY_V2;
+ bool valid = WinVerifyTrust(nullptr, &action, &data) == ERROR_SUCCESS;
+ if (valid) {
+ CRYPT_PROVIDER_DATA* const provider = WTHelperProvDataFromStateData(data.hWVTStateData);
+ CRYPT_PROVIDER_SGNR* const signer =
+ provider ? WTHelperGetProvSignerFromChain(provider, 0, FALSE, 0) : nullptr;
+ CRYPT_PROVIDER_CERT* const cert = signer ? WTHelperGetProvCertFromChain(signer, 0) : nullptr;
+ wchar_t name[128]{};
+ valid = cert &&
+ CertGetNameStringW(cert->pCert, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0, nullptr, name,
+ static_cast<DWORD>(std::size(name))) > 1 &&
+ std::wstring_view{name} == L"NVIDIA Corporation";
+ }
+ data.dwStateAction = WTD_STATEACTION_CLOSE;
+ WinVerifyTrust(nullptr, &action, &data);
+ return valid;
+}
+
+DlssFiles FindDlssFiles() {
+ DlssFiles files;
+ files.folder = UserFolder();
+ std::error_code ec;
+ for (const std::filesystem::path& dir : SearchDirs()) {
+ if (std::filesystem::is_regular_file(dir / INTERPOSER, ec)) {
+ files.folder = dir;
+ files.interposer = dir / INTERPOSER;
+ break;
+ }
+ }
+ if (std::filesystem::is_regular_file(files.folder / MODEL, ec)) {
+ files.model = files.folder / MODEL;
+ }
+ files.app_model = FindAppModel();
+ files.interposer_version = VersionText(files.interposer);
+ files.model_version = VersionText(files.model);
+ files.app_model_version = VersionText(files.app_model);
+ return files;
+}
+
+bool Start() {
+ State& state = GetState();
+ if (state.started) {
+ return true;
+ }
+ if (!Settings::values.nvidia_dlss.GetValue()) {
+ return false;
+ }
+ if (!state.module) {
+ const DlssFiles files = FindDlssFiles();
+ if (files.interposer.empty()) {
+ LOG_WARNING(Render_Vulkan,
+ "NVIDIA DLSS is on, but sl.interposer.dll (NVIDIA Streamline) is not in "
+ "{} (the Graphics settings can download it); using FSR 3.1",
+ files.folder.string());
+ return false;
+ }
+ if (!Load(state, files.interposer)) {
+ return false;
+ }
+ state.plugin_dir = files.folder.wstring();
+ state.model_dir.clear();
+ if (files.model.empty() && !files.app_model.empty()) {
+ if (const std::filesystem::path dir = UseAppModel(files); !dir.empty()) {
+ state.model_dir = dir.wstring();
+ LOG_INFO(Render_Vulkan, "Using the NVIDIA app's DLSS {}", files.app_model_version);
+ }
+ } else if (files.model.empty()) {
+ LOG_WARNING(Render_Vulkan, "No nvngx_dlss.dll in {} and no DLSS from the NVIDIA app",
+ files.folder.string());
+ }
+ }
+
+ // Streamline's plugins come from the loader's folder or the NVIDIA app's downloads (OTA); the
+ // DLSS model from the loader's folder, else the NVIDIA app's copy.
+ const wchar_t* plugin_paths[] = {state.plugin_dir.c_str(), state.model_dir.c_str()};
+ const sl::Feature features[] = {sl::kFeatureDLSS};
+ sl::Preferences preferences{};
+ preferences.logLevel = LogLevel() >= 2 ? sl::LogLevel::eVerbose : sl::LogLevel::eDefault;
+ preferences.logMessageCallback = LogFromStreamline;
+ preferences.pathsToPlugins = plugin_paths;
+ preferences.numPathsToPlugins = state.model_dir.empty() ? 1 : 2;
+ preferences.flags = sl::PreferenceFlags::eDisableCLStateTracking |
+ sl::PreferenceFlags::eUseManualHooking | sl::PreferenceFlags::eAllowOTA |
+ sl::PreferenceFlags::eLoadDownloadedPlugins;
+ preferences.featuresToLoad = features;
+ preferences.numFeaturesToLoad = 1;
+ preferences.engine = sl::EngineType::eCustom;
+ preferences.engineVersion = "1.0";
+ preferences.projectId = PROJECT_ID;
+ preferences.renderAPI = sl::RenderAPI::eVulkan;
+ const Api& api = state.api;
+ if (const sl::Result result = api.init(preferences, sl::kSDKVersion);
+ result != sl::Result::eOk) {
+ LOG_ERROR(Render_Vulkan, "NVIDIA Streamline failed to start (result {}); using FSR 3.1",
+ static_cast<int>(result));
+ return false;
+ }
+ sl::FeatureRequirements requirements{};
+ if (const sl::Result result = api.get_feature_requirements(sl::kFeatureDLSS, requirements);
+ result != sl::Result::eOk) {
+ LOG_WARNING(Render_Vulkan,
+ "Streamline found no DLSS plugin (result {}); put sl.dlss.dll next to "
+ "sl.interposer.dll or install the NVIDIA app. Using FSR 3.1",
+ static_cast<int>(result));
+ api.shutdown();
+ return false;
+ }
+ if (requirements.vkNumComputeQueuesRequired != 0 ||
+ requirements.vkNumGraphicsQueuesRequired != 0) {
+ LOG_WARNING(Render_Vulkan, "This DLSS wants extra Vulkan queues; using FSR 3.1");
+ api.shutdown();
+ return false;
+ }
+ state.instance_extensions =
+ Names(requirements.vkNumInstanceExtensions, requirements.vkInstanceExtensions);
+ state.device_extensions =
+ Names(requirements.vkNumDeviceExtensions, requirements.vkDeviceExtensions);
+ // The KHR and EXT buffer device address extensions can't both be enabled.
+ const auto& device_extensions = state.device_extensions;
+ if (std::ranges::find(device_extensions, VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME) !=
+ device_extensions.end()) {
+ std::erase(state.device_extensions, VK_EXT_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME);
+ }
+ sl::FeatureVersion version{};
+ api.get_feature_version(sl::kFeatureDLSS, version);
+ LOG_INFO(Render_Vulkan, "NVIDIA Streamline {} started from {} (DLSS {})",
+ version.versionSL.toStr(), std::filesystem::path{state.plugin_dir}.string(),
+ version.versionNGX.toStr());
+ state.started = true;
+ return true;
+}
+
+bool WantsDevice() {
+ const State& state = GetState();
+ return state.started && !state.attached;
+}
+
+std::vector<std::string> InstanceExtensions() {
+ const State& state = GetState();
+ return state.started ? state.instance_extensions : std::vector<std::string>{};
+}
+
+std::vector<std::string> DeviceExtensions() {
+ const State& state = GetState();
+ return state.started ? state.device_extensions : std::vector<std::string>{};
+}
+
+bool AttachDevice(VkInstance instance, VkPhysicalDevice physical, VkDevice device,
+ u32 graphics_family, vk::DeviceDispatch& dld) {
+ State& state = GetState();
+ if (!WantsDevice()) {
+ return false;
+ }
+ sl::VulkanInfo info{};
+ info.device = device;
+ info.instance = instance;
+ info.physicalDevice = physical;
+ info.computeQueueFamily = graphics_family;
+ info.graphicsQueueFamily = graphics_family;
+ if (const sl::Result result = state.api.set_vulkan_info(info); result != sl::Result::eOk) {
+ LOG_ERROR(Render_Vulkan, "Streamline did not take the Vulkan device (result {})",
+ static_cast<int>(result));
+ Stop();
+ return false;
+ }
+ sl::AdapterInfo adapter{};
+ adapter.vkPhysicalDevice = physical;
+ if (const sl::Result result = state.api.is_feature_supported(sl::kFeatureDLSS, adapter);
+ result != sl::Result::eOk) {
+ LOG_WARNING(Render_Vulkan,
+ "NVIDIA DLSS is not supported on this GPU or driver (Streamline result {}); "
+ "using FSR 3.1",
+ static_cast<int>(result));
+ Stop();
+ return false;
+ }
+ // Manual hooking: Streamline does its per-frame work in these.
+ const auto hook = [&](auto& function, const char* name) {
+ if (const FARPROC proxy = GetProcAddress(state.module, name)) {
+ function = reinterpret_cast<std::remove_reference_t<decltype(function)>>(proxy);
+ } else {
+ LOG_WARNING(Render_Vulkan, "sl.interposer.dll has no {}", name);
+ }
+ };
+ hook(dld.vkCreateSwapchainKHR, "vkCreateSwapchainKHR");
+ hook(dld.vkDestroySwapchainKHR, "vkDestroySwapchainKHR");
+ hook(dld.vkGetSwapchainImagesKHR, "vkGetSwapchainImagesKHR");
+ hook(dld.vkAcquireNextImageKHR, "vkAcquireNextImageKHR");
+ hook(dld.vkQueuePresentKHR, "vkQueuePresentKHR");
+ hook(dld.vkDeviceWaitIdle, "vkDeviceWaitIdle");
+ state.attached = true;
+ LOG_INFO(Render_Vulkan, "NVIDIA DLSS is available through Streamline");
+ return true;
+}
+
+bool IsDlssAvailable() {
+ const State& state = GetState();
+ return state.started && state.attached;
+}
+
+void Stop() {
+ State& state = GetState();
+ if (!state.started) {
+ return;
+ }
+ const sl::Result result = state.api.shutdown();
+ state.started = false;
+ state.attached = false;
+ LOG_INFO(Render_Vulkan, "NVIDIA Streamline stopped (result {})", static_cast<int>(result));
+}
+
+const Api& GetApi() {
+ return GetState().api;
+}
+
+} // namespace Vulkan::Streamline
diff --git a/src/video_core/vulkan_common/streamline.h b/src/video_core/vulkan_common/streamline.h
new file mode 100644
index 0000000000..47e55f7150
--- /dev/null
+++ b/src/video_core/vulkan_common/streamline.h
@@ -0,0 +1,63 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// NVIDIA DLSS through NVIDIA Streamline. Only Streamline's MIT-licensed
+// headers are built in: NVIDIA's DLLs are loaded at run time from the user's PC, and none ship
+// with the emulator.
+
+#pragma once
+
+#include <filesystem>
+#include <string>
+#include <vector>
+
+#include "common/common_types.h"
+#include "video_core/vulkan_common/vulkan_wrapper.h"
+
+namespace Vulkan::Streamline {
+
+/// NVIDIA's files DLSS needs, as found on this PC. Paths are empty when missing.
+struct DlssFiles {
+ std::filesystem::path folder; ///< Where the loader is, or where it should go
+ std::filesystem::path interposer; ///< sl.interposer.dll (Streamline's loader)
+ std::filesystem::path model; ///< nvngx_dlss.dll next to the loader
+ std::filesystem::path app_model; ///< Newest DLSS the NVIDIA app downloaded
+ std::string interposer_version;
+ std::string model_version;
+ std::string app_model_version;
+};
+
+[[nodiscard]] DlssFiles FindDlssFiles();
+
+/// The user folder's "streamline" folder.
+[[nodiscard]] std::filesystem::path UserFolder();
+
+/// Streamline's own modules carry a second, Streamline-only signature.
+[[nodiscard]] bool IsStreamlineModule(const std::filesystem::path& path);
+
+/// A valid Authenticode signature from NVIDIA Corporation (DLSS models).
+[[nodiscard]] bool IsSignedByNvidia(const std::filesystem::path& path);
+
+/// When the NVIDIA DLSS setting is on, loads the user's sl.interposer.dll (checking NVIDIA's
+/// signature) and starts Streamline for DLSS. Call before creating the renderer's instance.
+bool Start();
+
+/// Whether Streamline is running and waiting for the renderer's device.
+[[nodiscard]] bool WantsDevice();
+
+/// Instance and device extensions DLSS asks for (empty when Streamline isn't running).
+[[nodiscard]] std::vector<std::string> InstanceExtensions();
+[[nodiscard]] std::vector<std::string> DeviceExtensions();
+
+/// Hands the renderer's device to Streamline and routes the swapchain calls through it, as
+/// Streamline's manual hooking needs. Stops Streamline and returns false when DLSS can't run.
+bool AttachDevice(VkInstance instance, VkPhysicalDevice physical, VkDevice device,
+ u32 graphics_family, vk::DeviceDispatch& dld);
+
+/// Whether DLSS can run on the attached device.
+[[nodiscard]] bool IsDlssAvailable();
+
+/// Shuts Streamline down; call before the device is destroyed.
+void Stop();
+
+} // namespace Vulkan::Streamline
diff --git a/src/video_core/vulkan_common/streamline_api.h b/src/video_core/vulkan_common/streamline_api.h
new file mode 100644
index 0000000000..0a82c1dd7d
--- /dev/null
+++ b/src/video_core/vulkan_common/streamline_api.h
@@ -0,0 +1,36 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Streamline's API as loaded from the user's sl.interposer.dll. Only for the
+// Streamline sources: it pulls in <windows.h> and Streamline's (MIT-licensed) headers.
+
+#pragma once
+
+#include <windows.h>
+
+#include "video_core/vulkan_common/vulkan_wrapper.h"
+
+#include <sl.h>
+#include <sl_dlss.h>
+#include <sl_helpers_vk.h>
+
+namespace Vulkan::Streamline {
+
+struct Api {
+ PFun_slInit* init{};
+ PFun_slShutdown* shutdown{};
+ PFun_slIsFeatureSupported* is_feature_supported{};
+ PFun_slGetFeatureRequirements* get_feature_requirements{};
+ PFun_slGetFeatureVersion* get_feature_version{};
+ PFun_slGetFeatureFunction* get_feature_function{};
+ PFun_slGetNewFrameToken* get_new_frame_token{};
+ PFun_slSetConstants* set_constants{};
+ PFun_slEvaluateFeature* evaluate_feature{};
+ PFun_slFreeResources* free_resources{};
+ PFun_slSetVulkanInfo* set_vulkan_info{};
+};
+
+/// The loaded functions; only valid while IsDlssAvailable().
+[[nodiscard]] const Api& GetApi();
+
+} // namespace Vulkan::Streamline
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp
index 01e13cb384..6309599877 100644
--- a/src/video_core/vulkan_common/vulkan_device.cpp
+++ b/src/video_core/vulkan_common/vulkan_device.cpp
@@ -32,6 +32,9 @@
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
#include "video_core/gpu_logging/gpu_logging.h"
+#ifdef EGLANTINE_STREAMLINE
+#include "video_core/vulkan_common/streamline.h"
+#endif
#if defined(__ANDROID__) && defined(ARCHITECTURE_arm64)
#include <adrenotools/bcenabler.h>
@@ -743,6 +746,15 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
RemoveExtensionFeature(extensions.descriptor_buffer, features.descriptor_buffer,
VK_EXT_DESCRIPTOR_BUFFER_EXTENSION_NAME);
}
+#ifdef EGLANTINE_STREAMLINE
+ // NVIDIA's DLSS needs buffer device addresses: keep the feature enabled for
+ // it, but leave Eglantine's own buffers as they are without descriptor buffers.
+ const bool dlss_wants_device_address =
+ streamline_extensions && features.buffer_device_address.bufferDeviceAddress;
+ if (!extensions.descriptor_buffer && dlss_wants_device_address) {
+ extensions.buffer_device_address = false;
+ } else
+#endif
if (!extensions.descriptor_buffer) {
RemoveExtensionFeature(extensions.buffer_device_address, features.buffer_device_address,
VK_KHR_BUFFER_DEVICE_ADDRESS_EXTENSION_NAME);
@@ -753,6 +765,15 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
graphics_queue = logical.GetQueue(graphics_family);
present_queue = logical.GetQueue(present_family);
+#ifdef EGLANTINE_STREAMLINE
+ if (streamline_extensions) {
+ streamline_attached =
+ Streamline::AttachDevice(instance, physical, *logical, graphics_family, dld);
+ } else if (Streamline::WantsDevice()) {
+ Streamline::Stop();
+ }
+#endif
+
VmaVulkanFunctions functions{};
functions.vkGetInstanceProcAddr = dld.vkGetInstanceProcAddr;
functions.vkGetDeviceProcAddr = dld.vkGetDeviceProcAddr;
@@ -806,6 +827,11 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
}
Device::~Device() {
+#ifdef EGLANTINE_STREAMLINE
+ if (streamline_attached) {
+ Streamline::Stop();
+ }
+#endif
SaveStaticPipelineCache();
ShutdownGPULogging();
for (auto& pools : separate_pools) {
@@ -1123,6 +1149,27 @@ bool Device::GetSuitability(bool requires_swapchain) {
extensions.robustness_2 = false;
}
+#ifdef EGLANTINE_STREAMLINE
+ // The extensions NVIDIA's DLSS asks for, where the device has them all.
+ if (Streamline::WantsDevice()) {
+ const std::vector<std::string> wanted = Streamline::DeviceExtensions();
+ const bool all =
+ !wanted.empty() && std::ranges::all_of(wanted, [&](const std::string& name) {
+ return supported_extensions.contains(name);
+ });
+ std::string names;
+ for (const std::string& name : wanted) {
+ if (all) {
+ loaded_extensions.insert(name);
+ }
+ names += (names.empty() ? "" : ", ") + name;
+ }
+ streamline_extensions = all;
+ LOG_INFO(Render_Vulkan, "NVIDIA DLSS device extensions: {} ({})",
+ all ? "enabled" : "unavailable", names);
+ }
+#endif
+
#undef FEATURE_EXTENSION
#undef EXTENSION
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index edc091318c..3115f4547c 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -289,6 +289,11 @@ public:
return physical;
}
+ /// Returns the instance the device was created from.
+ VkInstance GetInstance() const {
+ return instance;
+ }
+
VkPipelineCache StaticPipelineCache() const noexcept {
return *static_pipeline_cache;
}
@@ -1326,6 +1331,8 @@ private:
// Telemetry parameters
std::set<std::string, std::less<>> supported_extensions; ///< Reported Vulkan extensions.
std::set<std::string, std::less<>> loaded_extensions; ///< Loaded Vulkan extensions.
+ bool streamline_extensions{}; ///< NVIDIA DLSS's extensions are loaded.
+ bool streamline_attached{}; ///< Streamline has this device.
std::vector<size_t> valid_heap_memory; ///< Heaps used.
size_t main_heap{}; ///< Largest device-local heap, where images live.
u64 heap_size_limit{}; ///< EDEN_NX2_VRAM_LIMIT_MIB for device-local heaps, 0 if unset.
diff --git a/src/video_core/vulkan_common/vulkan_instance.cpp b/src/video_core/vulkan_common/vulkan_instance.cpp
index 7f3d8ed41f..3ab924e37f 100644
--- a/src/video_core/vulkan_common/vulkan_instance.cpp
+++ b/src/video_core/vulkan_common/vulkan_instance.cpp
@@ -18,6 +18,11 @@
#include "core/frontend/emu_window.h"
#include "video_core/vulkan_common/vulkan_instance.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
+#ifdef EGLANTINE_STREAMLINE
+#include <algorithm>
+#include <string>
+#include "video_core/vulkan_common/streamline.h"
+#endif
namespace Vulkan {
namespace {
@@ -82,6 +87,21 @@ namespace {
#endif
if (enable_validation && AreExtensionsSupported(dld, properties, std::array{VK_EXT_DEBUG_UTILS_EXTENSION_NAME}))
extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
+#ifdef EGLANTINE_STREAMLINE
+ // What NVIDIA's DLSS asks for through Streamline, where available. Static: the returned
+ // pointers must outlive this call.
+ static std::vector<std::string> streamline;
+ streamline = Streamline::InstanceExtensions();
+ for (const std::string& name : streamline) {
+ const bool available = std::ranges::any_of(properties, [&](const VkExtensionProperties& p) {
+ return name == p.extensionName;
+ });
+ const bool listed = std::ranges::any_of(extensions, [&](const char* e) { return name == e; });
+ if (available && !listed) {
+ extensions.push_back(name.c_str());
+ }
+ }
+#endif
return extensions;
}
diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp
index 074b6f405b..3a1c3bf2c2 100644
--- a/src/yuzu/configuration/configure_graphics.cpp
+++ b/src/yuzu/configuration/configure_graphics.cpp
@@ -226,6 +226,12 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
continue;
}
#endif
+#ifndef EGLANTINE_STREAMLINE
+ // Only builds with NVIDIA Streamline (Windows) can use it.
+ if (setting->Id() == Settings::values.nvidia_dlss.Id()) {
+ continue;
+ }
+#endif
ConfigurationShared::Widget* widget = [&]() {
if (setting->Id() == Settings::values.fsr_sharpening_slider.Id()) {