diff options
Diffstat (limited to 'src/video_core/renderer_vulkan')
| -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_fsr3_upscaler.cpp | 6 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_master_semaphore.cpp | 8 | ||||
| -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 |
9 files changed, 659 insertions, 26 deletions
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_fsr3_upscaler.cpp b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp index 99275800d1..b199005b33 100644 --- a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp +++ b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp @@ -151,9 +151,9 @@ public: const std::array<VkSampler, 2>& samplers) : Pass(device_, scheduler, descriptor_pool, MakeLayout(kinds, samplers), code) {} - void Dispatch(Scheduler& scheduler, ComputePassDescriptorQueue& queue, + void Dispatch(Scheduler& scheduler_, ComputePassDescriptorQueue& queue, std::span<const Resource> resources, u32 groups_x, u32 groups_y) { - queue.Acquire(scheduler, resources.size()); + queue.Acquire(scheduler_, resources.size()); for (const Resource& resource : resources) { if (resource.buffer) { queue.AddBuffer(resource.buffer, resource.offset, resource.size); @@ -162,7 +162,7 @@ public: } } const void* const descriptor_data{queue.UpdateData()}; - scheduler.Record([this, descriptor_data, groups_x, groups_y](vk::CommandBuffer cmdbuf) { + scheduler_.Record([this, descriptor_data, groups_x, groups_y](vk::CommandBuffer cmdbuf) { const VkDescriptorSet set = descriptor_allocator.Commit(); device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, descriptor_data); cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline); diff --git a/src/video_core/renderer_vulkan/vk_master_semaphore.cpp b/src/video_core/renderer_vulkan/vk_master_semaphore.cpp index be5a1144ce..ba364d99a0 100644 --- a/src/video_core/renderer_vulkan/vk_master_semaphore.cpp +++ b/src/video_core/renderer_vulkan/vk_master_semaphore.cpp @@ -104,7 +104,13 @@ void MasterSemaphore::Wait(u64 tick) { } // If none of the above is hit, fallback to a regular wait - while (!semaphore.Wait(tick)) { + try { + while (!semaphore.Wait(tick)) { + } + } catch (const vk::Exception& exception) { + // The throw ends the process; say why first (a device loss reached nothing else here). + LOG_CRITICAL(Render_Vulkan, "Waiting for GPU tick {} failed: {}", tick, exception.what()); + throw; } Refresh(); 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 { |
