diff options
Diffstat (limited to 'src/video_core')
| -rw-r--r-- | src/video_core/CMakeLists.txt | 18 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/blit_image.cpp | 18 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/renderer_vulkan.cpp | 6 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/renderer_vulkan.h | 3 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_streamline_dlss.cpp | 425 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_streamline_dlss.h | 80 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_texture_cache.cpp | 122 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_texture_cache.h | 17 | ||||
| -rw-r--r-- | src/video_core/texture_cache/texture_cache.h | 4 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/streamline.cpp | 466 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/streamline.h | 63 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/streamline_api.h | 36 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_device.cpp | 46 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_device.h | 7 | ||||
| -rw-r--r-- | src/video_core/vulkan_common/vulkan_instance.cpp | 20 |
15 files changed, 1307 insertions, 24 deletions
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; } |
