diff options
| author | Duck <350528965+Duck912@users.noreply.github.com> | 2026-11-03 15:44:36 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-11-03 15:44:36 -0500 |
| commit | 05e3b9d8b08414d3163cde36de61514cb4aa2400 (patch) | |
| tree | 499efe72b68afd9e1741e5c1331adce7b6ee8ecc | |
| parent | f347ae083af5595083a5706ff857462ed8c11596 (diff) | |
| parent | 1889602c7a0dc17850692594eae2e77e16ac13fc (diff) | |
Merge pull request #434 from Duck912/streamline-dlss
video_core: NVIDIA DLSS through NVIDIA Streamline in place of a game's DLSS
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\">✔</span>") + : QStringLiteral("<span style=\"color:#d29922\">⚠</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 |
