aboutsummaryrefslogtreecommitdiff
path: root/src/video_core/renderer_vulkan
diff options
context:
space:
mode:
Diffstat (limited to 'src/video_core/renderer_vulkan')
-rw-r--r--src/video_core/renderer_vulkan/blit_image.cpp18
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.cpp6
-rw-r--r--src/video_core/renderer_vulkan/renderer_vulkan.h3
-rw-r--r--src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp6
-rw-r--r--src/video_core/renderer_vulkan/vk_master_semaphore.cpp8
-rw-r--r--src/video_core/renderer_vulkan/vk_streamline_dlss.cpp425
-rw-r--r--src/video_core/renderer_vulkan/vk_streamline_dlss.h80
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp122
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h17
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 {