aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDuck <350528965+Duck912@users.noreply.github.com>2026-11-03 15:44:36 -0500
committerGitHub <noreply@github.com>2026-11-03 15:44:36 -0500
commit05e3b9d8b08414d3163cde36de61514cb4aa2400 (patch)
tree499efe72b68afd9e1741e5c1331adce7b6ee8ecc
parentf347ae083af5595083a5706ff857462ed8c11596 (diff)
parent1889602c7a0dc17850692594eae2e77e16ac13fc (diff)
Merge pull request #434 from Duck912/streamline-dlss
video_core: NVIDIA DLSS through NVIDIA Streamline in place of a game's DLSS
-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/blit_image.cpp18
-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.cpp425
-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.cpp466
-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.cpp46
-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/CMakeLists.txt4
-rw-r--r--src/yuzu/configuration/configure_graphics.cpp21
-rw-r--r--src/yuzu/configuration/nvidia_dlss_panel.cpp556
-rw-r--r--src/yuzu/configuration/nvidia_dlss_panel.h34
24 files changed, 1932 insertions, 24 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..342cc725c0 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. Use the "
+ "download button below.\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/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp
index 0628da1d0f..4a274b2915 100644
--- a/src/video_core/renderer_vulkan/blit_image.cpp
+++ b/src/video_core/renderer_vulkan/blit_image.cpp
@@ -644,10 +644,8 @@ void RecordShaderReadBarrier(Scheduler& scheduler, const ImageView& image_view)
});
}
-void BeginRenderPass(vk::CommandBuffer& cmdbuf, const Framebuffer* framebuffer) {
- const VkRenderPass render_pass = framebuffer->RenderPass();
- const VkFramebuffer framebuffer_handle = framebuffer->Handle();
- const VkExtent2D render_area = framebuffer->RenderArea();
+void BeginRenderPass(vk::CommandBuffer& cmdbuf, VkRenderPass render_pass,
+ VkFramebuffer framebuffer_handle, VkExtent2D render_area) {
const VkRenderPassBeginInfo renderpass_bi{
.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO,
.pNext = nullptr,
@@ -784,11 +782,17 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView
};
const VkPipelineLayout layout = *one_texture_pipeline_layout;
const VkPipeline pipeline = FindOrEmplaceColorPipeline(key);
+ // Read the framebuffer now: the texture cache's framebuffer slots can move before the
+ // worker records this.
+ const VkRenderPass render_pass = dst_framebuffer->RenderPass();
+ const VkFramebuffer framebuffer = dst_framebuffer->Handle();
+ const VkExtent2D render_area = dst_framebuffer->RenderArea();
scheduler.RequestOutsideRenderPassOperationContext();
- scheduler.Record([this, dst_framebuffer, src_image_view, src_image, src_sampler, dst_region,
- src_region, src_size, pipeline, layout](vk::CommandBuffer cmdbuf) {
+ scheduler.Record([this, render_pass, framebuffer, render_area, src_image_view, src_image,
+ src_sampler, dst_region, src_region, src_size, pipeline,
+ layout](vk::CommandBuffer cmdbuf) {
TransitionImageLayout(cmdbuf, src_image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
- BeginRenderPass(cmdbuf, dst_framebuffer);
+ BeginRenderPass(cmdbuf, render_pass, framebuffer, render_area);
const VkDescriptorSet descriptor_set = one_texture_descriptor_allocator.Commit();
// The source was just transitioned: describe it in that layout.
UpdateOneTextureDescriptorSet(device, descriptor_set, src_sampler, src_image_view,
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..4fa9e240c4
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp
@@ -0,0 +1,425 @@
+// 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};
+ // The caller draws the output this frame even when DLSS writes nothing to it: clear it
+ // then, rather than show undefined contents (FSR 3.1 takes over from the next frame).
+ const auto clear_output = [&] {
+ const VkImageSubresourceRange range{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ };
+ const auto barrier = [&](VkAccessFlags src_access, VkAccessFlags dst_access) {
+ return VkImageMemoryBarrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = src_access,
+ .dstAccessMask = dst_access,
+ .oldLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = output_image_handle,
+ .subresourceRange = range,
+ };
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
+ barrier(VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT,
+ VK_ACCESS_TRANSFER_WRITE_BIT));
+ cmdbuf.ClearColorImage(output_image_handle, VK_IMAGE_LAYOUT_GENERAL,
+ VkClearColorValue{}, range);
+ cmdbuf.PipelineBarrier(
+ VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0,
+ barrier(VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT));
+ };
+ 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));
+ }
+ clear_output();
+ 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));
+ }
+ clear_output();
+ 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);
+ if (result != sl::Result::eOk) {
+ clear_output();
+ }
+ });
+ // 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..4f8fd796c1
--- /dev/null
+++ b/src/video_core/vulkan_common/streamline.cpp
@@ -0,0 +1,466 @@
+// 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/fs_util.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",
+ Common::FS::PathToUTF8String(files.app_model));
+ 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 {}: {}",
+ Common::FS::PathToUTF8String(target), 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",
+ Common::FS::PathToUTF8String(path));
+ return false;
+ }
+ state.module = LoadLibraryW(path.c_str());
+ if (!state.module) {
+ LOG_ERROR(Render_Vulkan, "Could not load {} (error {})", Common::FS::PathToUTF8String(path),
+ 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",
+ Common::FS::PathToUTF8String(files.folder));
+ 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",
+ Common::FS::PathToUTF8String(files.folder));
+ }
+ }
+
+ // 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(),
+ Common::FS::PathToUTF8String(std::filesystem::path{state.plugin_dir}),
+ 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..2499f46f87 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,26 @@ 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();
+ // Streamline may ask for no extensions at all.
+ const bool all = 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.empty() ? "none" : 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/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index f5895edd10..f05da88c40 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -74,6 +74,8 @@ add_executable(yuzu
configuration/configure_general.ui
configuration/configure_graphics.cpp
configuration/configure_graphics.h
+ configuration/nvidia_dlss_panel.cpp
+ configuration/nvidia_dlss_panel.h
configuration/configure_graphics.ui
configuration/configure_graphics_advanced.cpp
configuration/configure_graphics_advanced.h
@@ -407,6 +409,8 @@ endif()
target_link_libraries(yuzu PRIVATE nlohmann_json::nlohmann_json)
target_link_libraries(yuzu PRIVATE common core input_common frontend_common network video_core qt_common)
target_link_libraries(yuzu PRIVATE Boost::headers Qt6::Widgets Qt6::Charts Qt6::Concurrent)
+# Unpacks NVIDIA's files from the Streamline release (nvidia_dlss_panel.cpp).
+target_link_libraries(yuzu PRIVATE ZLIB::ZLIB)
target_link_libraries(yuzu PRIVATE ${PLATFORM_LIBRARIES} Threads::Threads)
if (ENABLE_OPENGL)
target_link_libraries(yuzu PRIVATE glad)
diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp
index 074b6f405b..09ae6adf6e 100644
--- a/src/yuzu/configuration/configure_graphics.cpp
+++ b/src/yuzu/configuration/configure_graphics.cpp
@@ -35,6 +35,7 @@
#include "ui_configure_graphics.h"
#include "yuzu/configuration/configuration_shared.h"
#include "yuzu/configuration/configure_graphics.h"
+#include "yuzu/configuration/nvidia_dlss_panel.h"
#include "yuzu/configuration/shared_widget.h"
#ifdef HAS_RESHADE
@@ -226,6 +227,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()) {
@@ -323,6 +330,20 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
hold_graphics.emplace(setting->Id(), widget);
#endif
+#ifdef EGLANTINE_STREAMLINE
+ } else if (setting->Id() == Settings::values.nvidia_dlss.Id()) {
+ // NVIDIA's files and a download button under the checkbox.
+ QWidget* group = new QWidget(this);
+ QVBoxLayout* group_layout = new QVBoxLayout(group);
+ group_layout->setContentsMargins(0, 0, 0, 0);
+ group_layout->setSpacing(2);
+ group_layout->addWidget(widget);
+ NvidiaDlssPanel* panel = new NvidiaDlssPanel(group);
+ group_layout->addWidget(panel);
+ panel->setVisible(widget->checkbox->isChecked());
+ connect(widget->checkbox, &QCheckBox::toggled, panel, &QWidget::setVisible);
+ hold_graphics.emplace(setting->Id(), group);
+#endif
} else if (setting->Id() == Settings::values.frame_gen.Id()) {
frame_gen_checkbox = widget->checkbox;
hold_graphics.emplace(setting->Id(), widget);
diff --git a/src/yuzu/configuration/nvidia_dlss_panel.cpp b/src/yuzu/configuration/nvidia_dlss_panel.cpp
new file mode 100644
index 0000000000..040ea90557
--- /dev/null
+++ b/src/yuzu/configuration/nvidia_dlss_panel.cpp
@@ -0,0 +1,556 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifdef EGLANTINE_STREAMLINE
+
+#include "common/httplib.h"
+
+#include <algorithm>
+#include <cstdlib>
+#include <filesystem>
+#include <fstream>
+#include <functional>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include <QApplication>
+#include <QDesktopServices>
+#include <QDialog>
+#include <QDialogButtonBox>
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QMessageBox>
+#include <QPlainTextEdit>
+#include <QProgressDialog>
+#include <QPushButton>
+#include <QStringList>
+#include <QUrl>
+#include <QVBoxLayout>
+#include <fmt/format.h>
+#include <zlib.h>
+
+#include "common/common_types.h"
+#include "common/fs/fs_util.h"
+#include "common/logging.h"
+#include "video_core/vulkan_common/streamline.h"
+#include "yuzu/configuration/nvidia_dlss_panel.h"
+
+#ifdef YUZU_BUNDLED_OPENSSL
+#include <openssl/cert.h>
+#endif
+
+namespace {
+
+namespace Streamline = Vulkan::Streamline;
+
+constexpr const char* RELEASE_PAGE = "https://github.com/NVIDIA-RTX/Streamline/releases";
+constexpr const char* RELEASE_VERSION = "2.14.16";
+constexpr const char* ZIP_HOST = "https://github.com";
+constexpr const char* ZIP_PATH =
+ "/NVIDIA-RTX/Streamline/releases/download/v2.14.16/streamline-sdk-v2.14.16.zip";
+constexpr const char* INTERPOSER_ENTRY = "bin/x64/sl.interposer.dll";
+constexpr const char* MODEL_ENTRY = "bin/x64/nvngx_dlss.dll";
+constexpr const char* STREAMLINE_LICENSE_ENTRY = "license.txt";
+constexpr const char* DLSS_LICENSE_ENTRY = "bin/x64/nvngx_dlss.license.txt";
+
+QString Tr(const char* text) {
+ return QCoreApplication::translate("NvidiaDlssPanel", text);
+}
+
+u16 U16(const std::string& data, size_t at) {
+ return static_cast<u16>(static_cast<u8>(data[at]) | static_cast<u8>(data[at + 1]) << 8);
+}
+
+u32 U32(const std::string& data, size_t at) {
+ return static_cast<u32>(U16(data, at)) | static_cast<u32>(U16(data, at + 2)) << 16;
+}
+
+u64 U64(const std::string& data, size_t at) {
+ return static_cast<u64>(U32(data, at)) | static_cast<u64>(U32(data, at + 4)) << 32;
+}
+
+struct ZipEntry {
+ u16 method{};
+ u32 crc{};
+ u64 compressed{};
+ u64 size{};
+ u64 offset{};
+};
+
+/// Reads single files out of a zip on a web server with range requests, so only the files
+/// needed are downloaded (about 32 MB of the 276 MB release).
+class RemoteZip {
+public:
+ /// Follows GitHub's redirect to the file's (short-lived) download address.
+ bool Resolve() {
+ host = ZIP_HOST;
+ path = ZIP_PATH;
+ for (int redirect = 0; redirect < 5; ++redirect) {
+ const auto result = MakeClient()->Get(path, httplib::Headers{{"Range", "bytes=0-0"}});
+ if (!result) {
+ error = httplib::to_string(result.error());
+ return false;
+ }
+ if (result->status == 206) {
+ // "bytes 0-0/<size>"; the server doesn't take "last N bytes" ranges.
+ const std::string range = result->get_header_value("Content-Range");
+ const size_t slash = range.rfind('/');
+ zip_size = slash == std::string::npos
+ ? 0
+ : std::strtoull(range.c_str() + slash + 1, nullptr, 10);
+ if (zip_size == 0) {
+ error = "no file size";
+ return false;
+ }
+ return true;
+ }
+ const std::string location = result->get_header_value("Location");
+ const size_t scheme = location.find("://");
+ const size_t slash =
+ scheme == std::string::npos ? std::string::npos : location.find('/', scheme + 3);
+ if (result->status < 300 || result->status >= 400 || slash == std::string::npos) {
+ error = fmt::format("HTTP {}", result->status);
+ return false;
+ }
+ host = location.substr(0, slash);
+ path = location.substr(slash);
+ }
+ error = "too many redirects";
+ return false;
+ }
+
+ bool Open() {
+ if (!Resolve()) {
+ return false;
+ }
+ const u64 tail_start = zip_size > 65558 ? zip_size - 65558 : 0;
+ const std::optional<std::string> tail =
+ Fetch(fmt::format("bytes={}-{}", tail_start, zip_size - 1));
+ if (!tail) {
+ return false;
+ }
+ const size_t end = tail->rfind(std::string{"PK\x05\x06", 4});
+ if (end == std::string::npos || end + 22 > tail->size()) {
+ error = "no zip directory";
+ return false;
+ }
+ u64 directory_size = U32(*tail, end + 12);
+ u64 directory_offset = U32(*tail, end + 16);
+ if (directory_offset == 0xFFFFFFFF && end >= 20 &&
+ tail->compare(end - 20, 4, std::string{"PK\x06\x07", 4}) == 0) {
+ const u64 record = U64(*tail, end - 20 + 8);
+ const std::optional<std::string> zip64 =
+ Fetch(fmt::format("bytes={}-{}", record, record + 55));
+ if (!zip64 || zip64->size() < 56) {
+ return false;
+ }
+ directory_size = U64(*zip64, 40);
+ directory_offset = U64(*zip64, 48);
+ }
+ const std::optional<std::string> directory = Fetch(
+ fmt::format("bytes={}-{}", directory_offset, directory_offset + directory_size - 1));
+ if (!directory) {
+ return false;
+ }
+ for (size_t at = 0; at + 46 <= directory->size() && U32(*directory, at) == 0x02014b50;) {
+ const u16 name_length = U16(*directory, at + 28);
+ const u16 extra_length = U16(*directory, at + 30);
+ const u16 comment_length = U16(*directory, at + 32);
+ ZipEntry entry{
+ .method = U16(*directory, at + 10),
+ .crc = U32(*directory, at + 16),
+ .compressed = U32(*directory, at + 20),
+ .size = U32(*directory, at + 24),
+ .offset = U32(*directory, at + 42),
+ };
+ const size_t extra_end = at + 46 + name_length + extra_length;
+ if (extra_end + comment_length > directory->size()) {
+ error = "bad zip directory";
+ return false;
+ }
+ const std::string name = directory->substr(at + 46, name_length);
+ // Zip64 sizes and offset, in this order, for the fields that overflowed.
+ for (size_t extra = at + 46 + name_length; extra + 4 <= extra_end;) {
+ const u16 id = U16(*directory, extra);
+ const u16 length = U16(*directory, extra + 2);
+ const size_t record_end = std::min<size_t>(extra + 4 + length, extra_end);
+ if (id == 0x0001) {
+ size_t field = extra + 4;
+ for (u64* value : {&entry.size, &entry.compressed, &entry.offset}) {
+ if (*value == 0xFFFFFFFF && field + 8 <= record_end) {
+ *value = U64(*directory, field);
+ field += 8;
+ }
+ }
+ }
+ extra += 4 + length;
+ }
+ entries.emplace(name, entry);
+ at += 46 + name_length + extra_length + comment_length;
+ }
+ return true;
+ }
+
+ const ZipEntry* Find(const std::string& name) const {
+ const auto it = entries.find(name);
+ return it == entries.end() ? nullptr : &it->second;
+ }
+
+ std::optional<std::string> Read(const ZipEntry& entry,
+ const std::function<bool(u64)>& progress = {}) {
+ const std::optional<std::string> header =
+ Fetch(fmt::format("bytes={}-{}", entry.offset, entry.offset + 29));
+ if (!header) {
+ return std::nullopt;
+ }
+ if (header->size() < 30 || U32(*header, 0) != 0x04034b50) {
+ error = "bad zip entry";
+ return std::nullopt;
+ }
+ const u64 start = entry.offset + 30 + U16(*header, 26) + U16(*header, 28);
+ std::string data;
+ if (entry.compressed != 0) {
+ std::optional<std::string> fetched =
+ Fetch(fmt::format("bytes={}-{}", start, start + entry.compressed - 1), progress);
+ if (!fetched) {
+ return std::nullopt;
+ }
+ data = std::move(*fetched);
+ }
+ std::string out;
+ if (entry.method == 0) {
+ out = std::move(data);
+ } else if (entry.method == 8) {
+ out.resize(entry.size);
+ z_stream stream{};
+ if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) {
+ error = "zlib";
+ return std::nullopt;
+ }
+ stream.next_in = reinterpret_cast<Bytef*>(data.data());
+ stream.avail_in = static_cast<uInt>(data.size());
+ stream.next_out = reinterpret_cast<Bytef*>(out.data());
+ stream.avail_out = static_cast<uInt>(out.size());
+ const int result = inflate(&stream, Z_FINISH);
+ inflateEnd(&stream);
+ if (result != Z_STREAM_END || stream.total_out != entry.size) {
+ error = "could not unpack";
+ return std::nullopt;
+ }
+ } else {
+ error = "unknown zip compression";
+ return std::nullopt;
+ }
+ const uLong crc = crc32(0L, reinterpret_cast<const Bytef*>(out.data()),
+ static_cast<uInt>(out.size()));
+ if (crc != entry.crc) {
+ error = "damaged download (checksum)";
+ return std::nullopt;
+ }
+ return out;
+ }
+
+ bool canceled{};
+ std::string error;
+
+private:
+ std::unique_ptr<httplib::Client> MakeClient() const {
+ auto client = std::make_unique<httplib::Client>(host);
+ client->set_connection_timeout(30);
+ client->set_read_timeout(30);
+ client->set_write_timeout(30);
+#ifdef YUZU_BUNDLED_OPENSSL
+ client->load_ca_cert_store(kCert, sizeof(kCert));
+#endif
+ return client;
+ }
+
+ std::optional<std::string> Fetch(const std::string& range,
+ const std::function<bool(u64)>& progress = {}) {
+ std::string body;
+ int status = 0;
+ const auto result = MakeClient()->Get(
+ path, httplib::Headers{{"Range", range}},
+ [&status](const httplib::Response& response) {
+ status = response.status;
+ return response.status == 206;
+ },
+ [&](const char* data, size_t length) {
+ body.append(data, length);
+ if (progress && !progress(body.size())) {
+ canceled = true;
+ return false;
+ }
+ return true;
+ });
+ if (!result || status != 206) {
+ error = result ? fmt::format("HTTP {}", status) : httplib::to_string(result.error());
+ return std::nullopt;
+ }
+ return body;
+ }
+
+ std::string host;
+ std::string path;
+ u64 zip_size{};
+ std::map<std::string, ZipEntry> entries;
+};
+
+QString Named(const QString& name, const std::string& version) {
+ return version.empty() ? name : QStringLiteral("%1 %2").arg(name, QString::fromStdString(version));
+}
+
+QString PathText(const std::filesystem::path& path) {
+ return QString::fromStdWString(path.wstring());
+}
+
+} // Anonymous namespace
+
+NvidiaDlssPanel::NvidiaDlssPanel(QWidget* parent) : QWidget(parent) {
+ QVBoxLayout* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(24, 0, 0, 8);
+ layout->setSpacing(6);
+
+ QHBoxLayout* status_row = new QHBoxLayout;
+ status_row->setSpacing(8);
+ icon_label = new QLabel(this);
+ icon_label->setTextFormat(Qt::RichText);
+ icon_label->setAlignment(Qt::AlignTop | Qt::AlignHCenter);
+ icon_label->setFixedWidth(16);
+ status_label = new QLabel(this);
+ status_label->setTextFormat(Qt::RichText);
+ status_label->setWordWrap(true);
+ status_row->addWidget(icon_label);
+ status_row->addWidget(status_label, 1);
+
+ QHBoxLayout* button_row = new QHBoxLayout;
+ button_row->setContentsMargins(24, 0, 0, 0);
+ download_button = new QPushButton(this);
+ folder_button = new QPushButton(Tr("Open Folder"), this);
+ QLabel* about = new QLabel(
+ QStringLiteral("<a href=\"%1\">%2</a>").arg(QString::fromLatin1(RELEASE_PAGE),
+ Tr("Where do these files come from?")),
+ this);
+ about->setOpenExternalLinks(true);
+ about->setToolTip(Tr("NVIDIA's Streamline releases on GitHub. The files are in bin\\x64 of "
+ "the release zip."));
+ button_row->addWidget(download_button);
+ button_row->addWidget(folder_button);
+ button_row->addStretch(1);
+ button_row->addWidget(about);
+
+ layout->addLayout(status_row);
+ layout->addLayout(button_row);
+
+ connect(download_button, &QPushButton::clicked, this, [this] { Download(); });
+ connect(folder_button, &QPushButton::clicked, this, [] {
+ const std::filesystem::path folder = Streamline::FindDlssFiles().folder;
+ std::error_code ec;
+ std::filesystem::create_directories(folder, ec);
+ QDesktopServices::openUrl(QUrl::fromLocalFile(PathText(folder)));
+ });
+ Refresh();
+}
+
+void NvidiaDlssPanel::showEvent(QShowEvent* event) {
+ Refresh();
+ QWidget::showEvent(event);
+}
+
+void NvidiaDlssPanel::Refresh() {
+ const Streamline::DlssFiles files = Streamline::FindDlssFiles();
+ const bool has_loader = !files.interposer.empty();
+ const bool has_model = !files.model.empty() || !files.app_model.empty();
+ const QString dlss = !files.model.empty()
+ ? Named(QStringLiteral("DLSS"), files.model_version)
+ : Tr("%1 from the NVIDIA app")
+ .arg(Named(QStringLiteral("DLSS"), files.app_model_version));
+
+ QString status;
+ QString button;
+ if (has_loader && has_model) {
+ status = Tr("<b>Ready.</b> Found %1 and %2.")
+ .arg(Named(QStringLiteral("NVIDIA Streamline"), files.interposer_version), dlss);
+ } else if (has_model) {
+ status = Tr("<b>One file needed:</b> NVIDIA's Streamline loader (sl.interposer.dll). "
+ "%1 will be used. Until then, games use AMD FSR 3.1.")
+ .arg(dlss);
+ button = Tr("Download from NVIDIA (1 MB)");
+ } else if (has_loader) {
+ status = Tr("<b>One file needed:</b> NVIDIA DLSS (nvngx_dlss.dll). Until then, games use "
+ "AMD FSR 3.1.");
+ button = Tr("Download from NVIDIA (31 MB)");
+ } else {
+ status = Tr("<b>NVIDIA's files needed:</b> the Streamline loader and DLSS "
+ "(sl.interposer.dll and nvngx_dlss.dll). Until then, games use AMD FSR 3.1.");
+ button = Tr("Download from NVIDIA (32 MB)");
+ }
+ const bool ready = has_loader && has_model;
+ icon_label->setText(ready ? QStringLiteral("<span style=\"color:#2ea043\">&#10004;</span>")
+ : QStringLiteral("<span style=\"color:#d29922\">&#9888;</span>"));
+ status_label->setText(status);
+ status_label->setToolTip(Tr("Folder: %1").arg(PathText(files.folder)));
+ download_button->setText(button);
+ download_button->setVisible(!ready);
+}
+
+void NvidiaDlssPanel::Download() {
+ const Streamline::DlssFiles files = Streamline::FindDlssFiles();
+ std::vector<std::string> wanted;
+ if (files.interposer.empty()) {
+ wanted.emplace_back(INTERPOSER_ENTRY);
+ }
+ if (files.model.empty() && files.app_model.empty()) {
+ wanted.emplace_back(MODEL_ENTRY);
+ }
+ if (wanted.empty()) {
+ return;
+ }
+ const std::filesystem::path folder = Streamline::UserFolder();
+ const auto fail = [this](const std::string& error) {
+ LOG_ERROR(Frontend, "NVIDIA DLSS download failed: {}", error);
+ QMessageBox::warning(this, Tr("NVIDIA DLSS"),
+ Tr("Could not download NVIDIA's files: %1")
+ .arg(QString::fromStdString(error)));
+ Refresh();
+ };
+
+ RemoteZip zip;
+ std::string licences;
+ {
+ QProgressDialog busy(Tr("Contacting NVIDIA's download..."), QString{}, 0, 0, this);
+ busy.setWindowTitle(Tr("NVIDIA DLSS"));
+ busy.setWindowModality(Qt::WindowModal);
+ busy.setMinimumDuration(0);
+ busy.show();
+ QApplication::processEvents();
+ if (!zip.Open()) {
+ busy.close();
+ fail(zip.error);
+ return;
+ }
+ std::vector<std::pair<QString, const char*>> texts{
+ {Tr("NVIDIA Streamline"), STREAMLINE_LICENSE_ENTRY}};
+ if (std::ranges::find(wanted, MODEL_ENTRY) != wanted.end()) {
+ texts.emplace_back(Tr("NVIDIA DLSS"), DLSS_LICENSE_ENTRY);
+ }
+ for (const auto& [title, entry_name] : texts) {
+ const ZipEntry* const entry = zip.Find(entry_name);
+ const std::optional<std::string> text = entry ? zip.Read(*entry) : std::nullopt;
+ licences += fmt::format("==== {} ====\n\n{}\n\n", title.toStdString(),
+ text ? *text : std::string{"(see the release on GitHub)"});
+ }
+ }
+
+ QStringList names;
+ for (const std::string& entry : wanted) {
+ names << QString::fromStdString(
+ Common::FS::PathToUTF8String(std::filesystem::path{entry}.filename()));
+ }
+ QDialog dialog(this);
+ dialog.setWindowTitle(Tr("Download NVIDIA DLSS"));
+ QVBoxLayout* layout = new QVBoxLayout(&dialog);
+ QLabel* intro = new QLabel(
+ Tr("Eglantine will download %1 from NVIDIA's Streamline %2 release on GitHub into %3.<br>"
+ "These files are NVIDIA's, under NVIDIA's licence terms below.")
+ .arg(names.join(QStringLiteral(" and ")), QString::fromLatin1(RELEASE_VERSION),
+ PathText(folder)),
+ &dialog);
+ intro->setWordWrap(true);
+ QPlainTextEdit* licence_text = new QPlainTextEdit(QString::fromStdString(licences), &dialog);
+ licence_text->setReadOnly(true);
+ licence_text->setMinimumSize(640, 380);
+ QDialogButtonBox* buttons = new QDialogButtonBox(&dialog);
+ buttons->addButton(Tr("Accept and Download"), QDialogButtonBox::AcceptRole);
+ buttons->addButton(QDialogButtonBox::Cancel);
+ connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
+ layout->addWidget(intro);
+ layout->addWidget(licence_text, 1);
+ layout->addWidget(buttons);
+ if (dialog.exec() != QDialog::Accepted) {
+ return;
+ }
+
+ u64 total = 0;
+ std::vector<const ZipEntry*> entries;
+ for (const std::string& name : wanted) {
+ const ZipEntry* const entry = zip.Find(name);
+ if (!entry) {
+ fail(name + " is not in the release");
+ return;
+ }
+ entries.push_back(entry);
+ total += entry->compressed;
+ }
+ QProgressDialog progress(Tr("Downloading NVIDIA's DLSS files..."), Tr("Cancel"), 0, 1000,
+ this);
+ progress.setWindowTitle(Tr("NVIDIA DLSS"));
+ progress.setWindowModality(Qt::WindowModal);
+ progress.setMinimumDuration(0);
+ progress.setValue(0);
+ QApplication::processEvents();
+ // The download address from the first step only lasts a few minutes.
+ if (!zip.Resolve()) {
+ progress.close();
+ fail(zip.error);
+ return;
+ }
+ std::error_code ec;
+ std::filesystem::create_directories(folder, ec);
+ u64 done = 0;
+ for (size_t i = 0; i < wanted.size(); ++i) {
+ const std::optional<std::string> contents = zip.Read(*entries[i], [&](u64 received) {
+ progress.setValue(static_cast<int>((done + received) * 1000 / std::max<u64>(total, 1)));
+ QApplication::processEvents();
+ return !progress.wasCanceled();
+ });
+ if (!contents) {
+ progress.close();
+ if (!zip.canceled) {
+ fail(zip.error);
+ }
+ return;
+ }
+ done += entries[i]->compressed;
+ const std::filesystem::path target = folder / std::filesystem::path{wanted[i]}.filename();
+ std::filesystem::path partial = target;
+ partial += L".download";
+ {
+ std::ofstream file(partial, std::ios::binary | std::ios::trunc);
+ file.write(contents->data(), static_cast<std::streamsize>(contents->size()));
+ if (!file) {
+ progress.close();
+ fail("could not write " + Common::FS::PathToUTF8String(partial));
+ return;
+ }
+ }
+ const bool signed_ok = wanted[i] == INTERPOSER_ENTRY ? Streamline::IsStreamlineModule(partial)
+ : Streamline::IsSignedByNvidia(partial);
+ if (!signed_ok) {
+ std::filesystem::remove(partial, ec);
+ progress.close();
+ fail(Common::FS::PathToUTF8String(target.filename()) + " is not signed by NVIDIA");
+ return;
+ }
+ std::filesystem::rename(partial, target, ec);
+ if (ec) {
+ std::filesystem::remove(partial, ec);
+ progress.close();
+ fail("could not save " + Common::FS::PathToUTF8String(target));
+ return;
+ }
+ LOG_INFO(Frontend, "Downloaded {} from NVIDIA's Streamline {} release",
+ Common::FS::PathToUTF8String(target), RELEASE_VERSION);
+ }
+ progress.setValue(1000);
+ progress.close();
+ Refresh();
+ QMessageBox::information(this, Tr("NVIDIA DLSS"),
+ Tr("NVIDIA DLSS is ready. It is used the next time a game starts."));
+}
+
+#endif
diff --git a/src/yuzu/configuration/nvidia_dlss_panel.h b/src/yuzu/configuration/nvidia_dlss_panel.h
new file mode 100644
index 0000000000..1ab39178eb
--- /dev/null
+++ b/src/yuzu/configuration/nvidia_dlss_panel.h
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Shown under the NVIDIA DLSS setting: which of NVIDIA's files were found, and
+// a button that fetches missing ones from NVIDIA's own Streamline release.
+
+#pragma once
+
+#ifdef EGLANTINE_STREAMLINE
+
+#include <QWidget>
+
+class QLabel;
+class QPushButton;
+
+class NvidiaDlssPanel : public QWidget {
+public:
+ explicit NvidiaDlssPanel(QWidget* parent);
+
+ void Refresh();
+
+protected:
+ void showEvent(QShowEvent* event) override;
+
+private:
+ void Download();
+
+ QLabel* icon_label;
+ QLabel* status_label;
+ QPushButton* download_button;
+ QPushButton* folder_button;
+};
+
+#endif