From bb8fdfac21434db0bfd62c56b0a409ce26070a38 Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:06:30 -0500 Subject: qt_common: name the FSR filter and frame generation entries by what they do The window filter "AMD FidelityFX Super Resolution 3" (present/fsr3.cpp) runs FSR 1's EASU and RCAS with RCAS at 0.8x the slider and no temporal blending, and the frame generation type "FSR 3" (present/fsr3_frame_gen.cpp) is Eglantine's own optical-flow interpolation, not AMD's. Both names suggested AMD's FSR 3 upscaler. Label them "AMD FidelityFX Super Resolution 1, softer sharpening" (FSR 1 Soft) and "Optical Flow Interpolation (built-in)" (Optical Flow), and the plain FSR filter "AMD FidelityFX Super Resolution 1 (FSR 1)". Only the display text changes; setting ids and enum values stay, so configs carry over. Assisted-by: Claude Opus 5.5 (investigation and drafting) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/qt_common/config/shared_translation.cpp | 6 +++--- src/qt_common/config/shared_translation.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index 37f5a89f26..e346828888 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -487,7 +487,7 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) { translations->insert({Settings::EnumMetadata::Index(), { PAIR(FrameGenType, Lsfg, tr("Lossless Scaling (LSFG)")), - PAIR(FrameGenType, Fsr3, tr("AMD FidelityFX Super Resolution 3 (FSR 3)")), + PAIR(FrameGenType, Fsr3, tr("Optical Flow Interpolation (built-in)")), }}); translations->insert({Settings::EnumMetadata::Index(), { @@ -497,7 +497,7 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) { PAIR(ScalingFilter, Gaussian, tr("Gaussian")), PAIR(ScalingFilter, Lanczos, tr("Lanczos")), PAIR(ScalingFilter, ScaleForce, tr("ScaleForce")), - PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX Super Resolution")), + PAIR(ScalingFilter, Fsr, tr("AMD FidelityFX Super Resolution 1 (FSR 1)")), PAIR(ScalingFilter, Area, tr("Area")), PAIR(ScalingFilter, Mmpx, tr("MMPX")), PAIR(ScalingFilter, ZeroTangent, tr("Zero-Tangent")), @@ -506,7 +506,7 @@ std::unique_ptr ComboboxEnumeration(QObject* parent) { PAIR(ScalingFilter, Spline1, tr("Spline-1")), PAIR(ScalingFilter, Sgsr, tr("Snapdragon Game Super Resolution")), PAIR(ScalingFilter, SgsrEdge, tr("Snapdragon Game Super Resolution EdgeDir")), - PAIR(ScalingFilter, Fsr3, tr("AMD FidelityFX Super Resolution 3")), + PAIR(ScalingFilter, Fsr3, tr("AMD FidelityFX Super Resolution 1, softer sharpening")), }}); translations->insert({Settings::EnumMetadata::Index(), { diff --git a/src/qt_common/config/shared_translation.h b/src/qt_common/config/shared_translation.h index 15d427d4aa..7f29071acc 100644 --- a/src/qt_common/config/shared_translation.h +++ b/src/qt_common/config/shared_translation.h @@ -35,7 +35,7 @@ static const std::map anti_aliasing_texts_map = static const std::map frame_gen_type_texts_map = { {Settings::FrameGenType::Lsfg, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "LSFG"))}, - {Settings::FrameGenType::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 3"))}, + {Settings::FrameGenType::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Optical Flow"))}, }; static const std::map scaling_filter_texts_map = { @@ -55,12 +55,12 @@ static const std::map scaling_filter_texts_map {Settings::ScalingFilter::Lanczos, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Lanczos"))}, {Settings::ScalingFilter::ScaleForce, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "ScaleForce"))}, - {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR"))}, + {Settings::ScalingFilter::Fsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 1"))}, {Settings::ScalingFilter::Area, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "Area"))}, {Settings::ScalingFilter::Mmpx, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "MMPX"))}, {Settings::ScalingFilter::Sgsr, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR"))}, {Settings::ScalingFilter::SgsrEdge, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "SGSR EdgeDir"))}, - {Settings::ScalingFilter::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 3"))}, + {Settings::ScalingFilter::Fsr3, QStringLiteral(QT_TRANSLATE_NOOP("MainWindow", "FSR 1 Soft"))}, }; static const std::map use_docked_mode_texts_map = { -- cgit v1.2.3 From 960ebcc73d998c7c596c3377b0ec8a1d6eefccf1 Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:07:08 -0500 Subject: video_core: sharpen the temporal DLSS stand-in Fire Emblem: Fortune's Weave's DLSS input matches Eden's (same 720p image, same detail), but the stand-in's 1440p output was blurrier than a plain bilinear 2x of it (Laplacian 0.0066 vs 0.0081; Eden's real DLSS 0.0111), and the image drifted darker over time: - the sample kernel was about twice too wide for the output resolution; - the history was kept in the output's B10G11R11 format, whose 5-6 bit mantissas round away a frame's 0.03-0.1 share; - the 1-sigma neighbourhood clip removed detail that only some jitter positions sample (railings, thin edges). The history is now an RGBA16F pair the shader writes as a storage image, with no per-frame copy. Still pixels use a third of the kernel width and a 1.5-sigma clip, so detail from many jitter positions accumulates. Moving pixels keep the old kernel and 1-sigma clip, and so do pixels that moved recently or whose history falls far outside the neighbourhood (content that changes without motion vectors), remembered in the history's alpha. Static output goes from 0.0066 to 0.0102 with no brightness drift and no new ghosting. With resolution scaling the stand-in used to fall back to a bilinear blit. The images are now scaled up as BlitImage does, the jitter is scaled to host pixels and the history follows the framebuffer's render area. RCAS (temporal_sharpen.frag) sharpens a copy of each result, never the history, in range-compressed space since the output is HDR. Its strength is the sharpness slider, relabelled "RCAS Sharpness" (0% turns it off). EDEN_NX2_TEMPORAL_KERNEL, _CLIP, _KERNEL_MOTION, _CLIP_MOTION and _WEIGHT tune the filter (all four filter values at 1 give the old one), and EDEN_NX2_TEMPORAL_SHARPEN=|off overrides the slider. Assisted-by: Claude Opus 5.5 (debugging, implementation and drafting) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/qt_common/config/shared_translation.cpp | 6 +- src/video_core/host_shaders/CMakeLists.txt | 1 + src/video_core/host_shaders/temporal_sharpen.frag | 49 +++++ src/video_core/host_shaders/temporal_upscale.frag | 85 ++++++-- src/video_core/renderer_vulkan/blit_image.cpp | 166 +++++++++++++-- src/video_core/renderer_vulkan/blit_image.h | 25 ++- .../renderer_vulkan/vk_texture_cache.cpp | 227 ++++++++++++--------- src/video_core/renderer_vulkan/vk_texture_cache.h | 8 +- src/video_core/texture_cache/texture_cache.h | 20 +- 9 files changed, 440 insertions(+), 147 deletions(-) create mode 100644 src/video_core/host_shaders/temporal_sharpen.frag (limited to 'src') diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index e346828888..fda319cc6d 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -145,8 +145,10 @@ std::unique_ptr InitializeTranslations(QObject* parent) { "Higher resolutions require more VRAM and bandwidth.\n" "Options lower than 1X can cause artifacts.")); INSERT(Settings, scaling_filter, tr("Window Adapting Filter:"), QString()); - INSERT(Settings, fsr_sharpening_slider, tr("FSR Sharpness:"), - tr("Determines how sharpened the image will look using FSR's or SGSR's dynamic contrast.")); + INSERT(Settings, fsr_sharpening_slider, tr("RCAS Sharpness:"), + tr("Strength of RCAS (AMD's contrast-adaptive sharpening) on the temporal upscaler used " + "in place of a game's DLSS; 0% turns it off.\nAlso sets the sharpening of the AMD FSR 1 " + "and SGSR window adapting filters.")); INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"), tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA " "can produce a more stable picture in lower resolutions.")); diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 9ff7563ff7..1678395679 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -72,6 +72,7 @@ set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/smaa_blending_weight_calculation.frag ${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.vert ${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.frag + ${CMAKE_CURRENT_SOURCE_DIR}/temporal_sharpen.frag ${CMAKE_CURRENT_SOURCE_DIR}/temporal_upscale.frag ${CMAKE_CURRENT_SOURCE_DIR}/vulkan_blit_depth_stencil.frag ${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.frag diff --git a/src/video_core/host_shaders/temporal_sharpen.frag b/src/video_core/host_shaders/temporal_sharpen.frag new file mode 100644 index 0000000000..45a13013cb --- /dev/null +++ b/src/video_core/host_shaders/temporal_sharpen.frag @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// RCAS (FidelityFX FSR 1's contrast-adaptive sharpening) on the DLSS stand-in's result, before the +// game post-processes it. The input is linear HDR; RCAS's limits assume 0 to 1, so it sharpens in +// the stand-in's range-compressed space. + +#version 460 core +#extension GL_ARB_separate_shader_objects : enable +#extension GL_ARB_shading_language_420pack : enable +#extension GL_GOOGLE_include_directive : enable +#extension GL_EXT_shader_explicit_arithmetic_types : require + +layout(binding = 0) uniform sampler2D InputTexture; + +layout(push_constant) uniform PushConstants { + // The vertex stage's texture scale and offset come first. + layout(offset = 16) uvec4 Const0; // FsrRcasCon +}; + +layout(location = 0) in vec2 texcoord; +layout(location = 0) out vec4 color; + +#define A_GPU 1 +#define A_GLSL 1 +#include "ffx_a.h" + +vec3 Compress(vec3 c) { + return c / (1.0 + max(max(c.r, c.g), c.b)); +} + +vec3 Expand(vec3 c) { + c = clamp(c, vec3(0.0), vec3(1.0 - 1.0 / 65504.0)); + return c / (1.0 - max(max(c.r, c.g), c.b)); +} + +#define FSR_RCAS_F 1 +AF4 FsrRcasLoadF(ASU2 p) { + return AF4(Compress(texelFetch(InputTexture, p, 0).rgb), 1.0); +} +void FsrRcasInputF(inout AF1 r, inout AF1 g, inout AF1 b) {} + +#include "ffx_fsr1.h" + +void main() { + AF3 c; + FsrRcasF(c.r, c.g, c.b, AU2(gl_FragCoord.xy), Const0); + color = vec4(Expand(c), 1.0); +} diff --git a/src/video_core/host_shaders/temporal_upscale.frag b/src/video_core/host_shaders/temporal_upscale.frag index 0e76df2b43..ee31e035b1 100644 --- a/src/video_core/host_shaders/temporal_upscale.frag +++ b/src/video_core/host_shaders/temporal_upscale.frag @@ -11,12 +11,17 @@ layout(binding = 0) uniform sampler2D input_color; layout(binding = 1) uniform sampler2D motion_vectors; layout(binding = 2) uniform sampler2D history; +layout(binding = 3, rgba16f) uniform writeonly image2D next_history; layout(push_constant) uniform PushConstants { // The vertex stage's texture scale and offset come first. layout(offset = 16) vec2 jitter; // this frame's sample offset, in input pixels vec2 motion_scale; // motion vector -> UV offset to the previous frame float current_weight; // largest weight of this frame; 1 ignores the history + float kernel_scale; // accumulation kernel distance scale + float clip_width; // neighbourhood clip of still pixels, in deviations + float clip_motion; // clip of pixels moving a pixel or more per frame + float kernel_motion; // kernel distance scale of moving pixels }; layout(location = 0) in vec2 texcoord; // 0 to 1 across the output @@ -83,50 +88,92 @@ void main() { const vec2 target = texcoord * vec2(input_size); const ivec2 nearest = ivec2(floor(target + jitter)); + // The longest motion around the pixel, so that an edge follows the object in front. + vec2 motion = vec2(0.0); + float longest = -1.0; + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + // The motion vectors can be at another scale than the colour (resolution scaling). + const ivec2 input_texel = clamp(nearest + ivec2(x, y), ivec2(0), input_size - 1); + const ivec2 texel = clamp(ivec2((vec2(input_texel) + 0.5) * vec2(motion_size) / + vec2(input_size)), + ivec2(0), motion_size - 1); + const vec2 candidate = texelFetch(motion_vectors, texel, 0).rg; + const float length_squared = dot(candidate, candidate); + if (length_squared > longest) { + longest = length_squared; + motion = candidate; + } + } + } + const vec2 previous = texcoord + motion * motion_scale; + // Still pixels: a narrow kernel (detail from many jitter positions accumulates) + // and a looser clip (keeps detail only some positions sample, e.g. thin bars). Moving pixels + // (a quarter pixel or more per frame): the wide kernel and tight clip, as their history is unreliable. + // The history's alpha remembers recent motion, decaying over a few frames: background that a + // moving object has just uncovered has no motion of its own but a history of that object. + const vec2 history_position = previous * vec2(textureSize(history, 0)); + const float moved = smoothstep(0.02, 0.25, length(motion * motion_scale * vec2(textureSize(history, 0)))); + const float moving = max(moved, texture(history, history_position).a); + const float kernel = mix(kernel_scale, kernel_motion, moving); + const float clip = mix(clip_width, clip_motion, moving); + vec3 filtered = vec3(0.0); float total_weight = 0.0; float nearest_weight = 0.0; vec3 sum = vec3(0.0); vec3 squares = vec3(0.0); - vec2 motion = vec2(0.0); - float longest = -1.0; for (int y = -1; y <= 1; ++y) { for (int x = -1; x <= 1; ++x) { const ivec2 texel = clamp(nearest + ivec2(x, y), ivec2(0), input_size - 1); const vec3 sample_color = RgbToYCoCg(Compress(texelFetch(input_color, texel, 0).rgb)); // A Blackman-Harris-like window over the sample's distance to the output pixel. - const vec2 distance = vec2(texel) + 0.5 - jitter - target; + const vec2 distance = (vec2(texel) + 0.5 - jitter - target) * kernel; const float weight = exp(-2.29 * dot(distance, distance)); filtered += sample_color * weight; total_weight += weight; nearest_weight = max(nearest_weight, weight); sum += sample_color; squares += sample_color * sample_color; - // The longest motion around the pixel, so that an edge follows the object in front. - const vec2 candidate = - texelFetch(motion_vectors, clamp(texel, ivec2(0), motion_size - 1), 0).rg; - const float length_squared = dot(candidate, candidate); - if (length_squared > longest) { - longest = length_squared; - motion = candidate; - } } } - const vec3 current = filtered / total_weight; + const vec3 current = filtered / max(total_weight, 1e-6); const vec3 mean = sum / 9.0; const vec3 deviation = sqrt(max(squares / 9.0 - mean * mean, vec3(0.0))); - const vec3 low = mean - deviation; - const vec3 high = mean + deviation; + const vec3 low = mean - clip * deviation; + const vec3 high = mean + clip * deviation; - const vec2 previous = texcoord + motion * motion_scale; // A frame whose samples land close to this pixel counts more; one whose samples land between // pixels mostly keeps the history (from other jitter positions) there. float weight = current_weight * nearest_weight; + vec3 result = current; + float remembered = max(moved, 0.85 * moving); if (current_weight >= 1.0 || any(lessThan(previous, vec2(0.0))) || any(greaterThan(previous, vec2(1.0)))) { - weight = 1.0; + // No usable history: the wide kernel, as a narrow one would show the input's pixels. + vec3 wide = vec3(0.0); + float wide_total = 0.0; + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + const ivec2 texel = clamp(nearest + ivec2(x, y), ivec2(0), input_size - 1); + const vec2 distance = vec2(texel) + 0.5 - jitter - target; + const float wide_weight = exp(-2.29 * dot(distance, distance)); + wide += RgbToYCoCg(Compress(texelFetch(input_color, texel, 0).rgb)) * wide_weight; + wide_total += wide_weight; + } + } + result = wide / wide_total; + remembered = 1.0; + } else { + const vec3 unclipped = RgbToYCoCg(Compress(SampleHistory(history_position))); + const vec3 past = ClipToBox(unclipped, low, high); + result = mix(past, current, weight); + // A history far outside the neighbourhood means the content changed without + // motion vectors (some animations have none): treat the pixel as moving for a while. + const vec3 extent = 0.5 * (high - low) + 1.0 / 65504.0; + const vec3 units = abs(unclipped - 0.5 * (low + high)) / extent; + remembered = max(remembered, smoothstep(1.25, 2.5, max(units.x, max(units.y, units.z)))); } - const vec2 history_position = previous * vec2(textureSize(history, 0)); - const vec3 past = ClipToBox(RgbToYCoCg(Compress(SampleHistory(history_position))), low, high); - color = vec4(Expand(YCoCgToRgb(mix(past, current, weight))), 1.0); + color = vec4(Expand(YCoCgToRgb(result)), 1.0); + imageStore(next_history, ivec2(gl_FragCoord.xy), vec4(color.rgb, remembered)); } diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp index edd151b563..b2ff7d9bf2 100644 --- a/src/video_core/renderer_vulkan/blit_image.cpp +++ b/src/video_core/renderer_vulkan/blit_image.cpp @@ -7,6 +7,7 @@ #include #include "video_core/renderer_vulkan/vk_texture_cache.h" +#include "video_core/fsr.h" #include "common/div_ceil.h" #include "common/settings.h" @@ -33,6 +34,7 @@ #include "video_core/host_shaders/convert_non_msaa_to_msaa_depth_stencil_frag_spv.h" #include "video_core/host_shaders/convert_s8d24_to_abgr8_frag_spv.h" #include "video_core/host_shaders/full_screen_triangle_vert_spv.h" +#include "video_core/host_shaders/temporal_sharpen_frag_spv.h" #include "video_core/host_shaders/temporal_upscale_frag_spv.h" #include "video_core/host_shaders/vulkan_blit_depth_stencil_frag_spv.h" #include "video_core/host_shaders/vulkan_color_clear_frag_spv.h" @@ -98,9 +100,19 @@ struct TemporalUpscalePushConstants { std::array jitter; std::array motion_scale; float current_weight; + float kernel_scale; + float clip_width; + float clip_motion; + float kernel_motion; float padding; }; +// The vertex stage's PushConstants first, then temporal_sharpen.frag's FsrRcasCon. +struct TemporalSharpenPushConstants { + PushConstants vertex; + std::array rcas; +}; + struct MSAACopyPushConstants { std::array dst_offset; std::array src_offset; @@ -119,10 +131,27 @@ constexpr std::array TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS{ TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<0>, TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<1>, }; -constexpr std::array THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS{ +// temporal_upscale.frag: input colour, motion vectors and history, then the next history (storage). +constexpr std::array TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_BINDINGS{ TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<0>, TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<1>, TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<2>, + VkDescriptorSetLayoutBinding{ + .binding = 3, + .descriptorType = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, + .pImmutableSamplers = nullptr, + }, +}; +constexpr DescriptorBankInfo TEMPORAL_UPSCALE_DESCRIPTOR_BANK_INFO{ + .uniform_buffers = 0, + .storage_buffers = 0, + .texture_buffers = 0, + .image_buffers = 0, + .textures = 3, + .images = 1, + .score = 2, }; constexpr VkDescriptorSetLayoutCreateInfo ONE_TEXTURE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, @@ -148,12 +177,12 @@ constexpr VkDescriptorSetLayoutCreateInfo TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_CRE .bindingCount = static_cast(TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()), .pBindings = TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(), }; -constexpr VkDescriptorSetLayoutCreateInfo THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{ +constexpr VkDescriptorSetLayoutCreateInfo TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = 0, - .bindingCount = static_cast(THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()), - .pBindings = THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(), + .bindingCount = static_cast(TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()), + .pBindings = TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(), }; template inline constexpr VkPushConstantRange PUSH_CONSTANT_RANGE{ @@ -379,13 +408,14 @@ void UpdateOneTextureDescriptorSet(const Device& device, VkDescriptorSet descrip device.GetLogical().UpdateDescriptorSets(write_descriptor_set, nullptr); } -void UpdateThreeTexturesDescriptorSet(const Device& device, VkDescriptorSet descriptor_set, - VkSampler sampler, std::array image_views) { - std::array image_infos{}; - std::array write_descriptor_sets{}; - for (u32 binding = 0; binding < 3; ++binding) { +void UpdateTemporalUpscaleDescriptorSet(const Device& device, VkDescriptorSet descriptor_set, + VkSampler sampler, std::array image_views) { + std::array image_infos{}; + std::array write_descriptor_sets{}; + for (u32 binding = 0; binding < 4; ++binding) { + const bool storage = binding == 3; image_infos[binding] = VkDescriptorImageInfo{ - .sampler = sampler, + .sampler = storage ? VK_NULL_HANDLE : sampler, .imageView = image_views[binding], .imageLayout = VK_IMAGE_LAYOUT_GENERAL, }; @@ -396,7 +426,8 @@ void UpdateThreeTexturesDescriptorSet(const Device& device, VkDescriptorSet desc .dstBinding = binding, .dstArrayElement = 0, .descriptorCount = 1, - .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, + .descriptorType = storage ? VK_DESCRIPTOR_TYPE_STORAGE_IMAGE + : VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, .pImageInfo = &image_infos[binding], .pBufferInfo = nullptr, .pTexelBufferView = nullptr, @@ -638,14 +669,14 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_, ONE_TEXTURE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)), two_textures_set_layout(device.GetLogical().CreateDescriptorSetLayout( TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)), - three_textures_set_layout(device.GetLogical().CreateDescriptorSetLayout( - THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)), + temporal_upscale_set_layout(device.GetLogical().CreateDescriptorSetLayout( + TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)), one_texture_descriptor_allocator{ descriptor_pool.Allocator(device_, scheduler_, *one_texture_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<1>)}, two_textures_descriptor_allocator{ descriptor_pool.Allocator(device_, scheduler_, *two_textures_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<2>)}, - three_textures_descriptor_allocator{ - descriptor_pool.Allocator(device_, scheduler_, *three_textures_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<3>)}, + temporal_upscale_descriptor_allocator{descriptor_pool.Allocator( + device_, scheduler_, *temporal_upscale_set_layout, TEMPORAL_UPSCALE_DESCRIPTOR_BANK_INFO)}, one_texture_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo( one_texture_set_layout.address(), PUSH_CONSTANT_RANGE))), @@ -655,9 +686,14 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_, PUSH_CONSTANT_RANGE))), temporal_upscale_pipeline_layout( device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo( - three_textures_set_layout.address(), + temporal_upscale_set_layout.address(), PUSH_CONSTANT_RANGE))), + temporal_sharpen_pipeline_layout( + device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo( + one_texture_set_layout.address(), + PUSH_CONSTANT_RANGE))), clear_color_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo( nullptr, PUSH_CONSTANT_RANGE))), msaa_copy_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo( @@ -670,6 +706,7 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_, full_screen_vert(BuildShader(device, FULL_SCREEN_TRIANGLE_VERT_SPV)), blit_color_to_color_frag(BuildShader(device, BLIT_COLOR_FLOAT_FRAG_SPV)), temporal_upscale_frag(BuildShader(device, TEMPORAL_UPSCALE_FRAG_SPV)), + temporal_sharpen_frag(BuildShader(device, TEMPORAL_SHARPEN_FRAG_SPV)), blit_color_msaa_frag(BuildShader(device, BLIT_COLOR_MSAA_FRAG_SPV)), blit_depth_stencil_frag(device.IsExtShaderStencilExportSupported() ? BuildShader(device, VULKAN_BLIT_DEPTH_STENCIL_FRAG_SPV) @@ -762,20 +799,26 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView void BlitImageHelper::TemporalUpscale(const Framebuffer* dst_framebuffer, const ImageView& input_color, const ImageView& motion_vectors, - VkImageView history, const Region2D& dst_region, + VkImageView history, VkImageView next_history, + const Region2D& dst_region, std::array jitter, std::array motion_scale, - f32 current_weight) { + f32 current_weight, f32 kernel_scale, f32 clip_width, + f32 clip_motion, f32 kernel_motion) { const VkPipelineLayout layout = *temporal_upscale_pipeline_layout; const VkPipeline pipeline = FindOrEmplaceTemporalUpscalePipeline(dst_framebuffer->RenderPass()); const VkSampler sampler = *linear_sampler; - const std::array views{input_color.Handle(Shader::TextureType::Color2D), + const std::array views{input_color.Handle(Shader::TextureType::Color2D), motion_vectors.Handle(Shader::TextureType::Color2D), - history}; + history, next_history}; const TemporalUpscalePushConstants push_constants{ .vertex = {.tex_scale = {1.0f, 1.0f}, .tex_offset = {0.0f, 0.0f}}, .jitter = jitter, .motion_scale = motion_scale, .current_weight = current_weight, + .kernel_scale = kernel_scale, + .clip_width = clip_width, + .clip_motion = clip_motion, + .kernel_motion = kernel_motion, .padding = 0.0f, }; RecordShaderReadBarrier(scheduler, input_color); @@ -783,8 +826,34 @@ void BlitImageHelper::TemporalUpscale(const Framebuffer* dst_framebuffer, scheduler.RequestRenderpass(dst_framebuffer); scheduler.Record([this, dst_region, pipeline, layout, sampler, views, push_constants](vk::CommandBuffer cmdbuf) { - const VkDescriptorSet descriptor_set = three_textures_descriptor_allocator.Commit(); - UpdateThreeTexturesDescriptorSet(device, descriptor_set, sampler, views); + const VkDescriptorSet descriptor_set = temporal_upscale_descriptor_allocator.Commit(); + UpdateTemporalUpscaleDescriptorSet(device, descriptor_set, sampler, views); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, descriptor_set, + nullptr); + BindBlitState(cmdbuf, dst_region); + cmdbuf.PushConstants(layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + push_constants); + cmdbuf.Draw(3, 1, 0, 0); + }); + scheduler.InvalidateState(); +} + +void BlitImageHelper::TemporalSharpen(const Framebuffer* dst_framebuffer, VkImageView source, + const Region2D& dst_region, f32 sharpness) { + const VkPipelineLayout layout = *temporal_sharpen_pipeline_layout; + const VkPipeline pipeline = FindOrEmplaceTemporalSharpenPipeline(dst_framebuffer->RenderPass()); + const VkSampler sampler = *nearest_sampler; + TemporalSharpenPushConstants push_constants{ + .vertex = {.tex_scale = {1.0f, 1.0f}, .tex_offset = {0.0f, 0.0f}}, + .rcas = {}, + }; + FSR::FsrRcasCon(push_constants.rcas.data(), sharpness); + scheduler.RequestRenderpass(dst_framebuffer); + scheduler.Record([this, dst_region, pipeline, layout, sampler, source, + push_constants](vk::CommandBuffer cmdbuf) { + const VkDescriptorSet descriptor_set = one_texture_descriptor_allocator.Commit(); + UpdateOneTextureDescriptorSet(device, descriptor_set, sampler, source); cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, descriptor_set, nullptr); @@ -1432,6 +1501,59 @@ VkPipeline BlitImageHelper::FindOrEmplaceTemporalUpscalePipeline(VkRenderPass re return *temporal_upscale_pipelines.back(); } +VkPipeline BlitImageHelper::FindOrEmplaceTemporalSharpenPipeline(VkRenderPass renderpass) { + const auto it = std::ranges::find(temporal_sharpen_keys, renderpass); + if (it != temporal_sharpen_keys.end()) { + return *temporal_sharpen_pipelines[std::distance(temporal_sharpen_keys.begin(), it)]; + } + temporal_sharpen_keys.push_back(renderpass); + const std::array stages = MakeStages(*full_screen_vert, *temporal_sharpen_frag); + const VkPipelineColorBlendAttachmentState blend_attachment{ + .blendEnable = VK_FALSE, + .srcColorBlendFactor = VK_BLEND_FACTOR_ZERO, + .dstColorBlendFactor = VK_BLEND_FACTOR_ZERO, + .colorBlendOp = VK_BLEND_OP_ADD, + .srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO, + .dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO, + .alphaBlendOp = VK_BLEND_OP_ADD, + .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | + VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT, + }; + const VkPipelineColorBlendStateCreateInfo color_blend_create_info{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .logicOpEnable = VK_FALSE, + .logicOp = VK_LOGIC_OP_CLEAR, + .attachmentCount = 1, + .pAttachments = &blend_attachment, + .blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}, + }; + const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device); + temporal_sharpen_pipelines.push_back(device.GetLogical().CreateGraphicsPipeline({ + .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stageCount = static_cast(stages.size()), + .pStages = stages.data(), + .pVertexInputState = &PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .pInputAssemblyState = &input_assembly_ci, + .pTessellationState = nullptr, + .pViewportState = &PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .pDepthStencilState = nullptr, + .pColorBlendState = &color_blend_create_info, + .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .layout = *temporal_sharpen_pipeline_layout, + .renderPass = renderpass, + .subpass = 0, + .basePipelineHandle = VK_NULL_HANDLE, + .basePipelineIndex = 0, + }, device.StaticPipelineCache())); + return *temporal_sharpen_pipelines.back(); +} + VkPipeline BlitImageHelper::FindOrEmplaceDepthStencilPipeline(const BlitImagePipelineKey& key) { const auto it = std::ranges::find(blit_depth_stencil_keys, key); if (it != blit_depth_stencil_keys.end()) { diff --git a/src/video_core/renderer_vulkan/blit_image.h b/src/video_core/renderer_vulkan/blit_image.h index 303beeea5c..6c3685a0a6 100644 --- a/src/video_core/renderer_vulkan/blit_image.h +++ b/src/video_core/renderer_vulkan/blit_image.h @@ -83,11 +83,20 @@ public: const Region2D& src_region, const Extent3D& src_size); /// Temporal upscale (temporal_upscale.frag) into dst_framebuffer: the jittered input colour, - /// accumulated with the history image reprojected along the motion vectors. + /// accumulated with the history image reprojected along the motion vectors. The result is also + /// stored to next_history (a storage image of the output's size). void TemporalUpscale(const Framebuffer* dst_framebuffer, const ImageView& input_color, const ImageView& motion_vectors, VkImageView history, - const Region2D& dst_region, std::array jitter, - std::array motion_scale, f32 current_weight); + VkImageView next_history, const Region2D& dst_region, + std::array jitter, + std::array motion_scale, f32 current_weight, + f32 kernel_scale, f32 clip_width, f32 clip_motion, + f32 kernel_motion); + + /// RCAS (temporal_sharpen.frag) of source into dst_framebuffer; sharpness in stops as + /// FsrRcasCon takes it (0 is the strongest). + void TemporalSharpen(const Framebuffer* dst_framebuffer, VkImageView source, + const Region2D& dst_region, f32 sharpness); void BlitColorMSAA(const Framebuffer* dst_framebuffer, const ImageView& src_image_view, const Region2D& dst_region, const Region2D& src_region); @@ -179,6 +188,8 @@ private: [[nodiscard]] VkPipeline FindOrEmplaceTemporalUpscalePipeline(VkRenderPass renderpass); + [[nodiscard]] VkPipeline FindOrEmplaceTemporalSharpenPipeline(VkRenderPass renderpass); + [[nodiscard]] VkPipeline FindOrEmplaceDepthStencilPipeline(const BlitImagePipelineKey& key); [[nodiscard]] VkPipeline FindOrEmplaceClearColorPipeline(const BlitImagePipelineKey& key); @@ -216,19 +227,21 @@ private: vk::DescriptorSetLayout one_texture_set_layout; vk::DescriptorSetLayout two_textures_set_layout; - vk::DescriptorSetLayout three_textures_set_layout; + vk::DescriptorSetLayout temporal_upscale_set_layout; DescriptorAllocator one_texture_descriptor_allocator; DescriptorAllocator two_textures_descriptor_allocator; - DescriptorAllocator three_textures_descriptor_allocator; + DescriptorAllocator temporal_upscale_descriptor_allocator; vk::PipelineLayout one_texture_pipeline_layout; vk::PipelineLayout two_textures_pipeline_layout; vk::PipelineLayout temporal_upscale_pipeline_layout; + vk::PipelineLayout temporal_sharpen_pipeline_layout; vk::PipelineLayout clear_color_pipeline_layout; vk::PipelineLayout msaa_copy_pipeline_layout; vk::PipelineLayout msaa_copy_depth_stencil_pipeline_layout; vk::ShaderModule full_screen_vert; vk::ShaderModule blit_color_to_color_frag; vk::ShaderModule temporal_upscale_frag; + vk::ShaderModule temporal_sharpen_frag; vk::ShaderModule blit_color_msaa_frag; vk::ShaderModule blit_depth_stencil_frag; vk::ShaderModule blit_depth_frag; @@ -261,6 +274,8 @@ private: std::vector blit_color_pipelines; std::vector temporal_upscale_keys; std::vector temporal_upscale_pipelines; + std::vector temporal_sharpen_keys; + std::vector temporal_sharpen_pipelines; std::vector blit_depth_stencil_keys; std::vector blit_depth_stencil_pipelines; std::vector clear_color_keys; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 30981fe413..ceee8b5dc5 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -1544,48 +1544,55 @@ bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageVie } const VkFormat format = MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, dst.format).format; - const VkExtent2D extent{dst.size.width, dst.size.height}; - const VkImage output = dst.ImageHandle(); + // The framebuffer's area is the output's size on the host (scaled with resolution scaling). + const VkExtent2D extent = dst_framebuffer->RenderArea(); + // The history is kept at half-float precision: in the output's format (B10G11R11 here, 5-6 + // mantissa bits) a frame's small share of the blend rounded away, so the history never + // converged on the detail the jittered frames add. + constexpr VkFormat history_format = VK_FORMAT_R16G16B16A16_SFLOAT; bool reset = false; - if (!temporal_history.image || temporal_history.format != format || + if (!temporal_history.images[0] || temporal_history.format != format || temporal_history.extent.width != extent.width || temporal_history.extent.height != extent.height) { - if (temporal_history.image) { - scheduler.Finish(); // the old history may still be sampled + if (temporal_history.images[0]) { + scheduler.Finish(); // the old history may still be in use } - temporal_history.image = memory_allocator.CreateImage(VkImageCreateInfo{ - .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .imageType = VK_IMAGE_TYPE_2D, - .format = format, - .extent = {extent.width, extent.height, 1}, - .mipLevels = 1, - .arrayLayers = 1, - .samples = VK_SAMPLE_COUNT_1_BIT, - .tiling = VK_IMAGE_TILING_OPTIMAL, - .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT, - .sharingMode = VK_SHARING_MODE_EXCLUSIVE, - .queueFamilyIndexCount = 0, - .pQueueFamilyIndices = nullptr, - .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, - }); - temporal_history.view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{ - .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, - .pNext = nullptr, - .flags = 0, - .image = *temporal_history.image, - .viewType = VK_IMAGE_VIEW_TYPE_2D, - .format = format, - .components{}, - .subresourceRange{ - .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .baseMipLevel = 0, - .levelCount = 1, - .baseArrayLayer = 0, - .layerCount = 1, - }, - }); + for (size_t i = 0; i < temporal_history.images.size(); ++i) { + temporal_history.images[i] = memory_allocator.CreateImage(VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = history_format, + .extent = {extent.width, extent.height, 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_STORAGE_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }); + temporal_history.views[i] = device.GetLogical().CreateImageView(VkImageViewCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .image = *temporal_history.images[i], + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = history_format, + .components{}, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + } + temporal_history.current = 0; temporal_history.format = format; temporal_history.extent = extent; reset = true; @@ -1593,24 +1600,15 @@ bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageVie static_cast(format)); } - // The history holds this pass's previous result, copied right after drawing it: the game may - // draw into its DLSS output afterwards (post-processing), which must not feed back. The history - // stays in the GENERAL layout, which the blit helper's descriptors use. - const VkImage history = *temporal_history.image; - const VkImageSubresourceLayers output_layers{ - .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .mipLevel = static_cast(dst.range.base.level), - .baseArrayLayer = static_cast(dst.range.base.layer), - .layerCount = 1, - }; - static constexpr VkImageSubresourceLayers history_layers{ - .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .mipLevel = 0, - .baseArrayLayer = 0, - .layerCount = 1, - }; + // The shader reads the previous result from one history image and stores this one to the + // other: the game may draw into its DLSS output afterwards (post-processing), which must not + // feed back. Both stay in the GENERAL layout, which the blit helper's descriptors use. + const size_t read_index = temporal_history.current; + const size_t write_index = read_index ^ 1; + const VkImage history = *temporal_history.images[read_index]; + const VkImage next_history = *temporal_history.images[write_index]; const auto barrier = [](VkImage image, VkAccessFlags src_access, VkAccessFlags dst_access, - VkImageLayout old_layout, const VkImageSubresourceLayers& layers) { + VkImageLayout old_layout) { return VkImageMemoryBarrier{ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, .pNext = nullptr, @@ -1623,20 +1621,24 @@ bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageVie .image = image, .subresourceRange{ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, - .baseMipLevel = layers.mipLevel, + .baseMipLevel = 0, .levelCount = 1, - .baseArrayLayer = layers.baseArrayLayer, + .baseArrayLayer = 0, .layerCount = 1, }, }; }; if (reset) { - // A new history image has no contents yet; the first frame does not read it. + // New history images have no contents yet; the first frame does not use what it reads. scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([history, barrier](vk::CommandBuffer cmdbuf) { - cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - 0, barrier(history, 0, VK_ACCESS_SHADER_READ_BIT, - VK_IMAGE_LAYOUT_UNDEFINED, history_layers)); + scheduler.Record([history, next_history, barrier](vk::CommandBuffer cmdbuf) { + constexpr VkAccessFlags access = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT; + const std::array barriers{ + barrier(history, 0, access, VK_IMAGE_LAYOUT_UNDEFINED), + barrier(next_history, 0, access, VK_IMAGE_LAYOUT_UNDEFINED), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, {}, {}, barriers); }); } const Region2D dst_region{ @@ -1644,42 +1646,79 @@ bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageVie .end = {static_cast(extent.width), static_cast(extent.height)}, }; // Each frame weighs at most 0.1 against the history (scaled by how close its samples land to - // the pixel; the neighbourhood clip limits what older samples contribute). - constexpr f32 current_weight = 0.1f; + // the pixel; the neighbourhood clip limits what older samples contribute). Still pixels use a + // sample kernel a third as wide as for moving ones (the jittered frames fill in the detail) and + // a clip of 1.5 deviations instead of 1. + // EDEN_NX2_TEMPORAL_WEIGHT / _KERNEL / _CLIP / _CLIP_MOTION / _KERNEL_MOTION + // override them (all 1 = the previous stand-in's filter). + const auto tuned = [](const char* name, f32 fallback) { + const char* value = std::getenv(name); + return value ? static_cast(std::strtod(value, nullptr)) : fallback; + }; + static const f32 current_weight = tuned("EDEN_NX2_TEMPORAL_WEIGHT", 0.1f); + static const f32 kernel_scale = tuned("EDEN_NX2_TEMPORAL_KERNEL", 3.0f); + static const f32 clip_width = tuned("EDEN_NX2_TEMPORAL_CLIP", 1.5f); + static const f32 clip_motion = tuned("EDEN_NX2_TEMPORAL_CLIP_MOTION", 1.0f); + static const f32 kernel_motion = tuned("EDEN_NX2_TEMPORAL_KERNEL_MOTION", 1.0f); blit_image_helper.TemporalUpscale(dst_framebuffer, input_color, motion_vectors, - *temporal_history.view, dst_region, jitter, motion_scale, - reset ? 1.0f : current_weight); + *temporal_history.views[read_index], + *temporal_history.views[write_index], dst_region, jitter, + motion_scale, reset ? 1.0f : current_weight, kernel_scale, + clip_width, clip_motion, kernel_motion); + // RCAS on the output, set by the FSR sharpness slider (stops as FSR takes them, 0% = stored 200 + // turns it off). It reads the unsharpened result just stored, which stays the history. + // EDEN_NX2_TEMPORAL_SHARPEN=|off overrides the slider. + static const char* const sharpen_override = std::getenv("EDEN_NX2_TEMPORAL_SHARPEN"); + const int slider = Settings::values.fsr_sharpening_slider.GetValue(); + bool sharpen = slider < 200; + f32 sharpness = static_cast(slider) / 100.0f; + if (sharpen_override) { + sharpen = std::string_view{sharpen_override} != "off"; + sharpness = static_cast(std::strtod(sharpen_override, nullptr)); + } + const VkImage output = dst.ImageHandle(); + const u32 output_level = static_cast(dst.range.base.level); + const u32 output_layer = static_cast(dst.range.base.layer); scheduler.RequestOutsideRenderPassOperationContext(); - scheduler.Record([output, history, extent, output_layers, barrier](vk::CommandBuffer cmdbuf) { - constexpr VkAccessFlags written = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT; - const std::array before{ - barrier(output, written, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL, - output_layers), - barrier(history, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, - VK_IMAGE_LAYOUT_GENERAL, history_layers), - }; - cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | - VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, - VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); - const VkImageCopy copy{ - .srcSubresource = output_layers, - .srcOffset = {0, 0, 0}, - .dstSubresource = history_layers, - .dstOffset = {0, 0, 0}, - .extent = {extent.width, extent.height, 1}, - }; - cmdbuf.CopyImage(output, VK_IMAGE_LAYOUT_GENERAL, history, VK_IMAGE_LAYOUT_GENERAL, copy); - const std::array after{ - barrier(output, VK_ACCESS_TRANSFER_READ_BIT, - VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | - VK_ACCESS_TRANSFER_READ_BIT, - VK_IMAGE_LAYOUT_GENERAL, output_layers), - barrier(history, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, - VK_IMAGE_LAYOUT_GENERAL, history_layers), + scheduler.Record([history, next_history, barrier, output, output_level, output_layer, + sharpen](vk::CommandBuffer cmdbuf) { + // The next frame reads what was stored now and stores over what was read now; the + // sharpening pass reads what was stored now and draws over the output again. + const std::array barriers{ + barrier(next_history, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL), + barrier(history, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT, + VK_IMAGE_LAYOUT_GENERAL), + VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_GENERAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = output, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = output_level, + .levelCount = 1, + .baseArrayLayer = output_layer, + .layerCount = 1, + }, + }, }; - cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, - {}, {}, after); + constexpr VkPipelineStageFlags stages = + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT | VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT; + cmdbuf.PipelineBarrier(stages, stages, 0, {}, {}, + vk::Span(barriers.data(), + sharpen ? barriers.size() : 2)); }); + if (sharpen) { + blit_image_helper.TemporalSharpen(dst_framebuffer, *temporal_history.views[write_index], + dst_region, sharpness); + } + temporal_history.current = write_index; return true; } diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 564b2cbf9e..29687fda9c 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -188,10 +188,12 @@ public: BlitImageHelper& blit_image_helper; RenderPassCache& render_pass_cache; - /// TemporalUpscale's history: its previous result, copied right after drawing it. + /// TemporalUpscale's history: its previous result (images[current]) and the image the next + /// result is stored to. struct TemporalHistory { - vk::Image image; - vk::ImageView view; + std::array images; + std::array views; + size_t current{}; VkFormat format{}; VkExtent2D extent{}; } temporal_history; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 3f3ae9cafd..365137e7d6 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1131,11 +1131,27 @@ bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds PrepareImage(motion_id, false, false); PrepareImage(dst_id, true, false); for (const ImageId id : {src_id, motion_id, dst_id}) { - if (True(slot_images[id].flags & ImageFlagBits::Rescaled) || - slot_images[id].info.num_samples != 1) { + if (slot_images[id].info.num_samples != 1) { return false; } } + // With resolution scaling, bring the images that can follow to the same scale, as BlitImage + // does; the stand-in reads each image at its own size, so any left behind still work. + const auto rescaled = [&](ImageId id) { + return True(slot_images[id].flags & ImageFlagBits::Rescaled); + }; + if (rescaled(src_id) || rescaled(motion_id) || rescaled(dst_id)) { + for (const ImageId id : {src_id, motion_id, dst_id}) { + if (!rescaled(id) && ImageCanRescale(slot_images[id])) { + ScaleUp(slot_images[id]); + } + } + } + // The jitter is in the input's guest pixels. + if (rescaled(src_id)) { + const f32 factor = Settings::values.resolution_info.up_factor; + jitter = {jitter[0] * factor, jitter[1] * factor}; + } const auto view = [&](ImageId id, const Tegra::Engines::Fermi2D::Surface& surface, PixelFormat format) -> std::optional> { const std::optional base = slot_images[id].TryFindBase(surface.Address()); -- cgit v1.2.3 From 11cebf56014a5015db28179a8acc81cad0caf641 Mon Sep 17 00:00:00 2001 From: Duck912 <350528965+Duck912@users.noreply.github.com> Date: Mon, 2 Nov 2026 16:12:17 -0500 Subject: video_core: replace Fortune's Weave's DLSS with AMD FSR 3.1 Fire Emblem: Fortune's Weave's DLSS network can't run here, and the temporal stand-in is only an approximation of it. FSR 3.1 takes the same inputs DLSS does: the jittered colour, motion vectors, the jitter and the depth, which the game's first DLSS pass reads through cbuf0+0x298 (Z32, reversed, the colour's size). Kepler compute keeps it next to the motion vectors. Fsr3Upscaler ports the host side of the SDK's ffx_fsr3upscaler.cpp: the eight passes (prepare inputs, luma and shading-change pyramids, shading change, reactivity, luma instability, accumulate, RCAS), the internal images and the constants, recorded through the scheduler with the shaders from externals/FidelityFX-SDK. It takes HDR colour, render-size motion vectors and reversed depth, computes its own exposure, and uses the no-wave SPD variants on devices without compute quad operations. The SDK's GLSL uses separate textures and samplers, so DescriptorBankInfo gains sampled image and sampler counts (default 0). FSR's RCAS follows the RCAS Sharpness slider. The result goes to FSR's own RGBA16F image and is drawn into the game's output, which keeps its format. TextureCacheRuntime::TemporalUpscale runs FSR 3.1 first. Without the depth, at mismatched scales, on a device lacking FSR's formats or features, or if its pipelines fail to build, the stand-in runs instead, and the bilinear blit stays last. EDEN_NX2_DLSS_REPLACEMENT=standin forces the stand-in. A gap of more than 250 ms between evaluations restarts the history. The BlitColor overload that takes a raw image view transitions its source to SHADER_READ_ONLY_OPTIMAL but described it as GENERAL in the descriptor (a validation error); it now describes the layout it uses. Assisted-by: Claude Opus 5.5 (debugging, implementation and drafting) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com> --- src/qt_common/config/shared_translation.cpp | 6 +- src/video_core/CMakeLists.txt | 2 + src/video_core/engines/kepler_compute.cpp | 18 +- src/video_core/engines/kepler_compute.h | 7 +- src/video_core/host_shaders/CMakeLists.txt | 65 ++ src/video_core/rasterizer_interface.h | 9 +- src/video_core/renderer_vulkan/blit_image.cpp | 9 +- .../renderer_vulkan/vk_descriptor_pool.cpp | 7 +- .../renderer_vulkan/vk_descriptor_pool.h | 2 + .../renderer_vulkan/vk_fsr3_upscaler.cpp | 693 +++++++++++++++++++++ src/video_core/renderer_vulkan/vk_fsr3_upscaler.h | 119 ++++ src/video_core/renderer_vulkan/vk_rasterizer.cpp | 6 +- src/video_core/renderer_vulkan/vk_rasterizer.h | 1 + .../renderer_vulkan/vk_texture_cache.cpp | 126 +++- src/video_core/renderer_vulkan/vk_texture_cache.h | 26 +- src/video_core/texture_cache/texture_cache.h | 49 +- src/video_core/texture_cache/texture_cache_base.h | 3 +- src/video_core/vulkan_common/vulkan_device.h | 5 + 18 files changed, 1119 insertions(+), 34 deletions(-) create mode 100644 src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp create mode 100644 src/video_core/renderer_vulkan/vk_fsr3_upscaler.h (limited to 'src') diff --git a/src/qt_common/config/shared_translation.cpp b/src/qt_common/config/shared_translation.cpp index fda319cc6d..c5907e1b65 100644 --- a/src/qt_common/config/shared_translation.cpp +++ b/src/qt_common/config/shared_translation.cpp @@ -146,9 +146,9 @@ std::unique_ptr InitializeTranslations(QObject* parent) { "Options lower than 1X can cause artifacts.")); INSERT(Settings, scaling_filter, tr("Window Adapting Filter:"), QString()); INSERT(Settings, fsr_sharpening_slider, tr("RCAS Sharpness:"), - tr("Strength of RCAS (AMD's contrast-adaptive sharpening) on the temporal upscaler used " - "in place of a game's DLSS; 0% turns it off.\nAlso sets the sharpening of the AMD FSR 1 " - "and SGSR window adapting filters.")); + tr("Strength of RCAS (AMD's contrast-adaptive sharpening) on the upscaler used in place " + "of a game's DLSS (AMD FSR 3.1); 0% turns it off.\nAlso sets the sharpening " + "of the AMD FSR 1 and SGSR window adapting filters.")); INSERT(Settings, anti_aliasing, tr("Anti-Aliasing Method:"), tr("The anti-aliasing method to use.\nSMAA offers the best quality.\nFXAA " "can produce a more stable picture in lower resolutions.")); diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt index b3ef88c5f1..d0e84c324c 100644 --- a/src/video_core/CMakeLists.txt +++ b/src/video_core/CMakeLists.txt @@ -169,6 +169,8 @@ add_library(video_core STATIC renderer_vulkan/vk_command_pool.h renderer_vulkan/vk_compute_pass.cpp renderer_vulkan/vk_compute_pass.h + renderer_vulkan/vk_fsr3_upscaler.cpp + renderer_vulkan/vk_fsr3_upscaler.h renderer_vulkan/vk_compute_pipeline.cpp renderer_vulkan/vk_compute_pipeline.h renderer_vulkan/vk_descriptor_buffer.cpp diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index 4732167369..2d35b603ae 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -591,8 +591,9 @@ void KeplerCompute::DescribeNativeResources(const NativeQmdScheduler::Descriptor std::atomic g_nx2_trace_draws_after_upscale{}; // Fire Emblem: Fortune's Weave's first DLSS pass (rejected, before the network) has the input colour -// at cbuf0+0x278, its motion vectors (R16G16 float, the input's size) at +0x290 and this frame's -// camera jitter (x, y; input pixels, Halton bases 2 and 3 less 0.5) at +0x1b0. +// at cbuf0+0x278, its motion vectors (R16G16 float, the input's size) at +0x290, its depth (Z32, the +// input's size) at +0x298 and this frame's camera jitter (x, y; input pixels, Halton bases 2 and 3 +// less 0.5) at +0x1b0. void KeplerCompute::RememberDlssMotionVectors() { const auto& cbuf = launch_description.const_buffer_config[0]; if (cbuf.size.Value() < 0x294) return; @@ -610,6 +611,14 @@ void KeplerCompute::RememberDlssMotionVectors() { return; dlss_motion_vectors = *motion; dlss_motion_color = color->Address(); + const auto depth = cbuf.size.Value() >= 0x29c ? image(0x298) : std::nullopt; + if (depth && depth->format == Texture::TextureFormat::Z32 && depth->Address() != 0 && + depth->Width() == color->Width() && depth->Height() == color->Height() && + depth->IsBlockLinear()) { + dlss_depth = *depth; + } else { + dlss_depth.reset(); + } dlss_jitter = {std::bit_cast(memory_manager.Read(cbuf_address + 0x1b0)), std::bit_cast(memory_manager.Read(cbuf_address + 0x1b4))}; } @@ -770,8 +779,9 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { return false; Fermi2D::Surface motion = surface(*dlss_motion_vectors); motion.format = RTFormat::R16G16_FLOAT; - return rasterizer->AccelerateTemporalUpscale(source, surface(*output), motion, dlss_jitter, - {1.0f, 1.0f}); + return rasterizer->AccelerateTemporalUpscale(source, surface(*output), motion, + dlss_depth ? &*dlss_depth : nullptr, + dlss_jitter, {1.0f, 1.0f}); }; const bool done = upscale_temporally() || rasterizer->AccelerateSurfaceCopy(source, surface(*output), config); diff --git a/src/video_core/engines/kepler_compute.h b/src/video_core/engines/kepler_compute.h index b5aee3706a..6a06390294 100644 --- a/src/video_core/engines/kepler_compute.h +++ b/src/video_core/engines/kepler_compute.h @@ -269,10 +269,11 @@ private: void RememberDlssMotionVectors(); bool UpscaleInsteadOfNativeProgram(); - /// Motion vectors of Fire Emblem: Fortune's Weave's DLSS evaluation, from its first pass (run - /// earlier in the frame than the network pass the stand-in replaces), with the input colour - /// image and camera jitter they go with. + /// Motion vectors and depth of Fire Emblem: Fortune's Weave's DLSS evaluation, from its first + /// pass (run earlier in the frame than the network pass the stand-in replaces), with the input + /// colour image and camera jitter they go with. std::optional dlss_motion_vectors; + std::optional dlss_depth; GPUVAddr dlss_motion_color{}; std::array dlss_jitter{}; bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call); diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index 1678395679..6628d4cce3 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -212,6 +212,71 @@ foreach(VARIANT IN ITEMS ${SHADER_TYPE_VARIANTS}) set(SHADER_HEADERS ${SHADER_HEADERS} ${VARIANT_HEADER_FILE}) endforeach() +# AMD FidelityFX Super Resolution 3.1 upscaler (externals/FidelityFX-SDK, unmodified): the SDK's +# Vulkan pass files, compiled with one set of options (render-size motion vectors, reversed depth, +# HDR colour, reference Lanczos), plus the sharpening and the no-wave-operation SPD variants. +set(FSR3_GPU_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-SDK/include/FidelityFX/gpu) +set(FSR3_PASS_DIR ${CMAKE_SOURCE_DIR}/externals/FidelityFX-SDK/shaders/fsr3upscaler) +file(GLOB FSR3_GPU_FILES ${FSR3_GPU_DIR}/*.h ${FSR3_GPU_DIR}/fsr1/*.h ${FSR3_GPU_DIR}/spd/*.h + ${FSR3_GPU_DIR}/fsr3upscaler/*.h) +set(FSR3_FLAGS + -DFFX_GPU=1 + -DFFX_GLSL=1 + -DFFX_FSR3UPSCALER_OPTION_UPSAMPLE_SAMPLERS_USE_DATA_HALF=0 + -DFFX_FSR3UPSCALER_OPTION_ACCUMULATE_SAMPLERS_USE_DATA_HALF=0 + -DFFX_FSR3UPSCALER_OPTION_REPROJECT_SAMPLERS_USE_DATA_HALF=1 + -DFFX_FSR3UPSCALER_OPTION_POSTPROCESSLOCKSTATUS_SAMPLERS_USE_DATA_HALF=0 + -DFFX_FSR3UPSCALER_OPTION_UPSAMPLE_USE_LANCZOS_TYPE=2 + -DFFX_FSR3UPSCALER_OPTION_REPROJECT_USE_LANCZOS_TYPE=0 + -DFFX_FSR3UPSCALER_OPTION_HDR_COLOR_INPUT=1 + -DFFX_FSR3UPSCALER_OPTION_LOW_RESOLUTION_MOTION_VECTORS=1 + -DFFX_FSR3UPSCALER_OPTION_JITTERED_MOTION_VECTORS=0 + -DFFX_FSR3UPSCALER_OPTION_INVERTED_DEPTH=1 +) +set(FSR3_VARIANTS + "prepare_inputs|prepare_inputs|none" + "luma_pyramid|luma_pyramid|none" + "luma_pyramid_nowave|luma_pyramid|-DFFX_SPD_NO_WAVE_OPERATIONS=1" + "shading_change_pyramid|shading_change_pyramid|none" + "shading_change_pyramid_nowave|shading_change_pyramid|-DFFX_SPD_NO_WAVE_OPERATIONS=1" + "shading_change|shading_change|none" + "prepare_reactivity|prepare_reactivity|none" + "luma_instability|luma_instability|none" + "accumulate|accumulate|none" + "accumulate_sharpen|accumulate|-DFFX_FSR3UPSCALER_OPTION_APPLY_SHARPENING=1" + "rcas|rcas|none" +) + +foreach(VARIANT IN ITEMS ${FSR3_VARIANTS}) + string(REPLACE "|" ";" VARIANT_PARTS "${VARIANT}") + list(GET VARIANT_PARTS 0 FSR3_NAME) + list(GET VARIANT_PARTS 1 FSR3_PASS) + list(GET VARIANT_PARTS 2 FSR3_DEFINE) + if (FSR3_DEFINE STREQUAL "none") + set(FSR3_DEFINE "") + endif() + if (NOT FSR3_DEFINE MATCHES "APPLY_SHARPENING") + list(APPEND FSR3_DEFINE -DFFX_FSR3UPSCALER_OPTION_APPLY_SHARPENING=0) + endif() + set(FSR3_SOURCE ${FSR3_PASS_DIR}/ffx_fsr3upscaler_${FSR3_PASS}_pass.glsl) + string(TOUPPER fsr3_upscaler_${FSR3_NAME}_comp_spv FSR3_VARIABLE_NAME) + set(FSR3_HEADER_FILE ${SHADER_DIR}/fsr3_upscaler_${FSR3_NAME}_comp_spv.h) + add_custom_command( + OUTPUT + ${FSR3_HEADER_FILE} + COMMAND + ${GLSLANGVALIDATOR} -V ${QUIET_FLAG} -S comp -I"${FSR3_GPU_DIR}" + -I"${FSR3_GPU_DIR}/fsr3upscaler" ${FSR3_FLAGS} ${FSR3_DEFINE} + --variable-name ${FSR3_VARIABLE_NAME} -o ${FSR3_HEADER_FILE} ${FSR3_SOURCE} + --target-env ${SPIR_V_VERSION} + MAIN_DEPENDENCY + ${FSR3_SOURCE} + DEPENDS + ${FSR3_GPU_FILES} + ) + set(SHADER_HEADERS ${SHADER_HEADERS} ${FSR3_HEADER_FILE}) +endforeach() + foreach(FILEPATH IN ITEMS ${FIDELITYFX_FILES}) get_filename_component(FILENAME ${FILEPATH} NAME) string(REPLACE "." "_" HEADER_NAME ${FILENAME}) diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h index e9eaa2b3c2..363bb2d875 100644 --- a/src/video_core/rasterizer_interface.h +++ b/src/video_core/rasterizer_interface.h @@ -28,6 +28,9 @@ class AccelerateDMAInterface; namespace Control { struct ChannelState; } +namespace Texture { +struct TICEntry; +} } // namespace Tegra namespace VideoCore { @@ -179,10 +182,12 @@ public: /// Stand-in for a DLSS evaluation: temporal upscale of src into dst along motion_vectors /// (jitter in src pixels; motion_scale turns a motion vector into a UV offset to the previous - /// frame). False when unsupported; the caller then falls back to AccelerateSurfaceCopy. + /// frame), with the frame's depth when known. False when unsupported; the caller then falls + /// back to AccelerateSurfaceCopy. [[nodiscard]] virtual bool AccelerateTemporalUpscale( const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& dst, - const Tegra::Engines::Fermi2D::Surface& motion_vectors, std::array jitter, + const Tegra::Engines::Fermi2D::Surface& motion_vectors, + const Tegra::Texture::TICEntry* depth, std::array jitter, std::array motion_scale) { return false; } diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp index b2ff7d9bf2..2afc16b379 100644 --- a/src/video_core/renderer_vulkan/blit_image.cpp +++ b/src/video_core/renderer_vulkan/blit_image.cpp @@ -387,11 +387,12 @@ constexpr std::array MakeStages( } void UpdateOneTextureDescriptorSet(const Device& device, VkDescriptorSet descriptor_set, - VkSampler sampler, VkImageView image_view) { + VkSampler sampler, VkImageView image_view, + VkImageLayout image_layout = VK_IMAGE_LAYOUT_GENERAL) { const VkDescriptorImageInfo image_info{ .sampler = sampler, .imageView = image_view, - .imageLayout = VK_IMAGE_LAYOUT_GENERAL, + .imageLayout = image_layout, }; const VkWriteDescriptorSet write_descriptor_set{ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, @@ -787,7 +788,9 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView TransitionImageLayout(cmdbuf, src_image, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); BeginRenderPass(cmdbuf, dst_framebuffer); const VkDescriptorSet descriptor_set = one_texture_descriptor_allocator.Commit(); - UpdateOneTextureDescriptorSet(device, descriptor_set, src_sampler, src_image_view); + // The source was just transitioned: describe it in that layout. + UpdateOneTextureDescriptorSet(device, descriptor_set, src_sampler, src_image_view, + VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, descriptor_set, nullptr); diff --git a/src/video_core/renderer_vulkan/vk_descriptor_pool.cpp b/src/video_core/renderer_vulkan/vk_descriptor_pool.cpp index b020d99b93..e19dabac46 100644 --- a/src/video_core/renderer_vulkan/vk_descriptor_pool.cpp +++ b/src/video_core/renderer_vulkan/vk_descriptor_pool.cpp @@ -31,7 +31,8 @@ struct DescriptorBank { bool DescriptorBankInfo::IsSuperset(const DescriptorBankInfo& subset) const noexcept { return uniform_buffers >= subset.uniform_buffers && storage_buffers >= subset.storage_buffers && texture_buffers >= subset.texture_buffers && image_buffers >= subset.image_buffers && - textures >= subset.textures && images >= subset.images; + textures >= subset.textures && images >= subset.images && + sampled_images >= subset.sampled_images && samplers >= subset.samplers; } template @@ -59,7 +60,7 @@ static DescriptorBankInfo MakeBankInfo(std::span infos) { } static void AllocatePool(const Device& device, DescriptorBank& bank) { - std::array pool_sizes; + std::array pool_sizes; size_t pool_cursor{}; const u32 sets_per_pool = device.GetSetsPerPool(); const auto add = [&](VkDescriptorType type, u32 count) { @@ -77,6 +78,8 @@ static void AllocatePool(const Device& device, DescriptorBank& bank) { add(VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, info.image_buffers); add(VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, info.textures); add(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, info.images); + add(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, info.sampled_images); + add(VK_DESCRIPTOR_TYPE_SAMPLER, info.samplers); bank.pools.push_back(device.GetLogical().CreateDescriptorPool({ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO, .pNext = nullptr, diff --git a/src/video_core/renderer_vulkan/vk_descriptor_pool.h b/src/video_core/renderer_vulkan/vk_descriptor_pool.h index 4efeb60373..8213649090 100644 --- a/src/video_core/renderer_vulkan/vk_descriptor_pool.h +++ b/src/video_core/renderer_vulkan/vk_descriptor_pool.h @@ -30,6 +30,8 @@ struct DescriptorBankInfo { u32 image_buffers{}; ///< Number of image buffer descriptors u32 textures{}; ///< Number of texture descriptors u32 images{}; ///< Number of image descriptors + u32 sampled_images{}; ///< Number of sampled image descriptors (without a sampler) + u32 samplers{}; ///< Number of sampler descriptors s32 score{}; ///< Number of descriptors in total }; diff --git a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp new file mode 100644 index 0000000000..99275800d1 --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp @@ -0,0 +1,693 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include +#include +#include +#include +#include + +#include "common/alignment.h" +#include "common/assert.h" +#include "common/div_ceil.h" +#include "common/logging.h" +#include "video_core/fsr.h" +#include "video_core/host_shaders/fsr3_upscaler_accumulate_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_accumulate_sharpen_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_luma_instability_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_luma_pyramid_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_luma_pyramid_nowave_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_prepare_inputs_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_prepare_reactivity_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_rcas_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_shading_change_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_shading_change_pyramid_comp_spv.h" +#include "video_core/host_shaders/fsr3_upscaler_shading_change_pyramid_nowave_comp_spv.h" +#include "video_core/renderer_vulkan/vk_compute_pass.h" +#include "video_core/renderer_vulkan/vk_descriptor_pool.h" +#include "video_core/renderer_vulkan/vk_fsr3_upscaler.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/renderer_vulkan/vk_staging_buffer_pool.h" +#include "video_core/vulkan_common/vulkan_device.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" + +namespace Vulkan { +namespace { + +// cbFSR3Upscaler in ffx_fsr3upscaler_callbacks_glsl.h (std140; the SDK's Fsr3UpscalerConstants). +struct Constants { + std::array render_size; + std::array previous_render_size; + std::array upscale_size; + std::array previous_upscale_size; + std::array max_render_size; + std::array max_upscale_size; + std::array device_to_view_depth; + std::array jitter; + std::array previous_jitter; + std::array motion_vector_scale; + std::array downscale_factor; + std::array motion_vector_jitter_cancellation; + f32 tan_half_fov; + f32 jitter_phase_count; + f32 delta_time; + f32 delta_pre_exposure; + f32 view_space_to_meters; + f32 frame_index; + f32 velocity_factor; + f32 reactiveness_scale; + f32 shading_change_scale; + f32 accumulation_added_per_frame; + f32 min_disocclusion_accumulation; +}; +static_assert(sizeof(Constants) == 148); + +// cbSPD and cbRCAS. +struct SpdConstants { + u32 mips; + u32 num_work_groups; + std::array work_group_offset; + std::array render_size; +}; + +struct RcasConstants { + std::array config; +}; + +// Descriptor kinds in binding order, as the pass files number them (FSR3UPSCALER_BIND_*). Every +// pass also has the two samplers at bindings 1000 (point clamp) and 1001 (linear clamp). +enum class Kind : u8 { Texture, Image, Uniform }; +constexpr Kind T = Kind::Texture; +constexpr Kind I = Kind::Image; +constexpr Kind U = Kind::Uniform; + +constexpr std::array PREPARE_INPUTS{T, T, T, I, I, I, I, I, U}; +constexpr std::array LUMA_PYRAMID{T, T, I, I, I, I, I, I, I, I, I, U, U}; +constexpr std::array SHADING_CHANGE_PYRAMID{T, T, T, T, I, I, I, I, I, I, I, U, U}; +constexpr std::array SHADING_CHANGE{T, I, U}; +constexpr std::array PREPARE_REACTIVITY{T, T, T, T, T, T, T, T, T, I, I, I, U}; +constexpr std::array LUMA_INSTABILITY{T, T, T, T, T, T, T, I, I, U}; +constexpr std::array ACCUMULATE{T, T, T, T, T, T, T, T, T, I, I, I, U}; +constexpr std::array RCAS{T, T, I, U, U}; + +enum PassId : size_t { + PassPrepareInputs, + PassLumaPyramid, + PassShadingChangePyramid, + PassShadingChange, + PassPrepareReactivity, + PassLumaInstability, + PassAccumulate, + PassAccumulateSharpen, + PassRcas, + PassCount, +}; + +constexpr u32 SPD_STORED_MIPS = 6; + +/// One descriptor of a dispatch: an image view, or a uniform buffer range. +struct Resource { + VkImageView view{}; + VkBuffer buffer{}; + VkDeviceSize offset{}; + VkDeviceSize size{}; +}; + +constexpr VkMemoryBarrier MemoryBarrier(VkAccessFlags src, VkAccessFlags dst) { + return VkMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src, + .dstAccessMask = dst, + }; +} + +constexpr VkImageSubresourceRange ALL_LEVELS{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = VK_REMAINING_MIP_LEVELS, + .baseArrayLayer = 0, + .layerCount = 1, +}; + +// ffxFsr3UpscalerGetJitterPhaseCount. +s32 JitterPhaseCount(u32 render_width, u32 display_width) { + const f32 ratio = static_cast(display_width) / static_cast(render_width); + return static_cast(8.0f * ratio * ratio); +} + +// DLSS takes no camera, so FSR's depth-to-view-space conversion (which only scales its +// disocclusion thresholds) uses a typical near plane and vertical field of view. +constexpr f32 CAMERA_NEAR = 0.1f; +constexpr f32 CAMERA_FOV = 60.0f * std::numbers::pi_v / 180.0f; + +} // Anonymous namespace + +class Fsr3Upscaler::Pass final : public ComputePass { +public: + explicit Pass(const Device& device_, Scheduler& scheduler, DescriptorPool& descriptor_pool, + std::span kinds, std::span code, + const std::array& samplers) + : Pass(device_, scheduler, descriptor_pool, MakeLayout(kinds, samplers), code) {} + + void Dispatch(Scheduler& scheduler, ComputePassDescriptorQueue& queue, + std::span resources, u32 groups_x, u32 groups_y) { + queue.Acquire(scheduler, resources.size()); + for (const Resource& resource : resources) { + if (resource.buffer) { + queue.AddBuffer(resource.buffer, resource.offset, resource.size); + } else { + queue.AddImage(resource.view); + } + } + const void* const descriptor_data{queue.UpdateData()}; + 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); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {}); + cmdbuf.Dispatch(groups_x, groups_y, 1); + // The next pass reads what this one wrote (everything stays in GENERAL). + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, + MemoryBarrier(VK_ACCESS_SHADER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT | + VK_ACCESS_SHADER_WRITE_BIT)); + }); + } + +private: + struct Layout { + std::vector bindings; + std::vector templates; + DescriptorBankInfo bank; + }; + + explicit Pass(const Device& device_, Scheduler& scheduler, DescriptorPool& descriptor_pool, + const Layout& pass_layout, std::span code) + : ComputePass(device_, scheduler, descriptor_pool, pass_layout.bindings, + pass_layout.templates, pass_layout.bank, {}, code) {} + + static Layout MakeLayout(std::span kinds, + const std::array& samplers) { + Layout result; + for (u32 binding = 0; binding < kinds.size(); ++binding) { + VkDescriptorType type{}; + switch (kinds[binding]) { + case Kind::Texture: + type = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE; + ++result.bank.sampled_images; + break; + case Kind::Image: + type = VK_DESCRIPTOR_TYPE_STORAGE_IMAGE; + ++result.bank.images; + break; + case Kind::Uniform: + type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; + ++result.bank.uniform_buffers; + break; + } + result.bindings.push_back({ + .binding = binding, + .descriptorType = type, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = nullptr, + }); + result.templates.push_back({ + .dstBinding = binding, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = type, + .offset = binding * sizeof(DescriptorUpdateEntry), + .stride = sizeof(DescriptorUpdateEntry), + }); + } + for (u32 index = 0; index < samplers.size(); ++index) { + result.bindings.push_back({ + .binding = 1000 + index, + .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER, + .descriptorCount = 1, + .stageFlags = VK_SHADER_STAGE_COMPUTE_BIT, + .pImmutableSamplers = &samplers[index], + }); + ++result.bank.samplers; + } + result.bank.score = static_cast(result.bindings.size()); + return result; + } +}; + +Fsr3Upscaler::Fsr3Upscaler(const Device& device_, Scheduler& scheduler_, + MemoryAllocator& memory_allocator_, + StagingBufferPool& staging_buffer_pool_, + DescriptorPool& descriptor_pool, + ComputePassDescriptorQueue& descriptor_queue_) + : device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_}, + staging_buffer_pool{staging_buffer_pool_}, descriptor_queue{descriptor_queue_} { + const auto sampler = [&](VkFilter filter, VkSamplerMipmapMode mipmap) { + return device.GetLogical().CreateSampler(VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = filter, + .minFilter = filter, + .mipmapMode = mipmap, + .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, + }); + }; + point_sampler = sampler(VK_FILTER_NEAREST, VK_SAMPLER_MIPMAP_MODE_NEAREST); + linear_sampler = sampler(VK_FILTER_LINEAR, VK_SAMPLER_MIPMAP_MODE_LINEAR); + const std::array samplers{*point_sampler, *linear_sampler}; + + // SPD reduces with quad shuffles where the device has them in compute shaders. + const bool quads = device.IsSubgroupFeatureSupported(VK_SUBGROUP_FEATURE_QUAD_BIT) && + (device.GetSubgroupSupportedStages() & VK_SHADER_STAGE_COMPUTE_BIT) != 0; + const auto pass = [&](std::span kinds, std::span code) { + return std::make_unique(device, scheduler, descriptor_pool, kinds, code, samplers); + }; + passes.resize(PassCount); + passes[PassPrepareInputs] = pass(PREPARE_INPUTS, FSR3_UPSCALER_PREPARE_INPUTS_COMP_SPV); + passes[PassLumaPyramid] = + pass(LUMA_PYRAMID, quads ? std::span(FSR3_UPSCALER_LUMA_PYRAMID_COMP_SPV) + : std::span(FSR3_UPSCALER_LUMA_PYRAMID_NOWAVE_COMP_SPV)); + passes[PassShadingChangePyramid] = pass( + SHADING_CHANGE_PYRAMID, + quads ? std::span(FSR3_UPSCALER_SHADING_CHANGE_PYRAMID_COMP_SPV) + : std::span(FSR3_UPSCALER_SHADING_CHANGE_PYRAMID_NOWAVE_COMP_SPV)); + passes[PassShadingChange] = pass(SHADING_CHANGE, FSR3_UPSCALER_SHADING_CHANGE_COMP_SPV); + passes[PassPrepareReactivity] = + pass(PREPARE_REACTIVITY, FSR3_UPSCALER_PREPARE_REACTIVITY_COMP_SPV); + passes[PassLumaInstability] = pass(LUMA_INSTABILITY, FSR3_UPSCALER_LUMA_INSTABILITY_COMP_SPV); + passes[PassAccumulate] = pass(ACCUMULATE, FSR3_UPSCALER_ACCUMULATE_COMP_SPV); + passes[PassAccumulateSharpen] = pass(ACCUMULATE, FSR3_UPSCALER_ACCUMULATE_SHARPEN_COMP_SPV); + passes[PassRcas] = pass(RCAS, FSR3_UPSCALER_RCAS_COMP_SPV); +} + +Fsr3Upscaler::~Fsr3Upscaler() = default; + +bool Fsr3Upscaler::IsSupported(const Device& device) { + if (!device.IsStorageImageExtendedFormatsSupported()) { + return false; + } + constexpr VkFormatFeatureFlags storage = + VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT | VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT; + for (const VkFormat format : + {VK_FORMAT_R8_UNORM, VK_FORMAT_R16_SFLOAT, VK_FORMAT_R16G16_SFLOAT, VK_FORMAT_R32_SFLOAT, + VK_FORMAT_R8G8B8A8_UNORM, VK_FORMAT_R16G16B16A16_SFLOAT}) { + if (!device.IsFormatSupported(format, storage, FormatType::Optimal)) { + return false; + } + } + return device.IsFormatSupported(VK_FORMAT_R32_UINT, VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT, + FormatType::Optimal) && + device.IsFormatSupported(VK_FORMAT_R32G32B32A32_SFLOAT, + VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT, FormatType::Optimal) && + device.IsFormatSupported(VK_FORMAT_R16_SNORM, + VK_FORMAT_FEATURE_SAMPLED_IMAGE_BIT | + VK_FORMAT_FEATURE_SAMPLED_IMAGE_FILTER_LINEAR_BIT, + FormatType::Optimal); +} + +VkImage Fsr3Upscaler::OutputImage() const noexcept { + return *output.image; +} + +VkImageView Fsr3Upscaler::OutputView() const noexcept { + return *output.view; +} + +Fsr3Upscaler::Texture Fsr3Upscaler::CreateTexture(VkFormat format, u32 width, u32 height, + u32 levels) { + Texture texture; + texture.image = memory_allocator.CreateImage(VkImageCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {(std::max)(width, 1U), (std::max)(height, 1U), 1}, + .mipLevels = levels, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = static_cast( + VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | + (format == VK_FORMAT_R16_SNORM ? 0 : VK_IMAGE_USAGE_STORAGE_BIT)), + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }); + const auto view = [&](u32 base_level, u32 level_count) { + return device.GetLogical().CreateImageView(VkImageViewCreateInfo{ + .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .image = *texture.image, + .viewType = VK_IMAGE_VIEW_TYPE_2D, + .format = format, + .components{}, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = base_level, + .levelCount = level_count, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + }; + texture.view = view(0, levels); + if (levels > 1) { + for (u32 level = 0; level < levels; ++level) { + texture.levels.push_back(view(level, 1)); + } + } + return texture; +} + +void Fsr3Upscaler::CreateResources(VkExtent2D render_size, VkExtent2D upscale_size) { + const u32 rw = render_size.width; + const u32 rh = render_size.height; + const u32 uw = upscale_size.width; + const u32 uh = upscale_size.height; + for (size_t i = 0; i < 2; ++i) { + accumulation[i] = CreateTexture(VK_FORMAT_R8_UNORM, rw, rh); + luma[i] = CreateTexture(VK_FORMAT_R16_SFLOAT, rw, rh); + // The pass files declare the luma history as rgba8 (the SDK allocates RGBA16F, which a + // storage format qualifier does not match); allocate what the shaders store. + luma_history[i] = CreateTexture(VK_FORMAT_R8G8B8A8_UNORM, rw, rh); + internal_upscaled[i] = CreateTexture(VK_FORMAT_R16G16B16A16_SFLOAT, uw, uh); + } + farthest_depth = CreateTexture(VK_FORMAT_R16_SFLOAT, rw, rh); + shading_change = CreateTexture(VK_FORMAT_R8_UNORM, rw / 2, rh / 2); + new_locks = CreateTexture(VK_FORMAT_R8_UNORM, uw, uh); + const u32 spd_levels = + static_cast(std::floor(std::log2(static_cast((std::max)(rw / 2, rh / 2))))) + 1; + spd_mips = CreateTexture(VK_FORMAT_R16G16_SFLOAT, rw / 2, rh / 2, + (std::max)(spd_levels, SPD_STORED_MIPS)); + farthest_depth_mip1 = CreateTexture(VK_FORMAT_R16_SFLOAT, rw / 2, rh / 2); + dilated_motion_vectors = CreateTexture(VK_FORMAT_R16G16_SFLOAT, rw, rh); + dilated_depth = CreateTexture(VK_FORMAT_R32_SFLOAT, rw, rh); + previous_nearest_depth = CreateTexture(VK_FORMAT_R32_UINT, rw, rh); + dilated_reactive_masks = CreateTexture(VK_FORMAT_R8G8B8A8_UNORM, rw, rh); + frame_info = CreateTexture(VK_FORMAT_R32G32B32A32_SFLOAT, 1, 1); + spd_atomic = CreateTexture(VK_FORMAT_R32_UINT, 1, 1); + lanczos_lut = CreateTexture(VK_FORMAT_R16_SNORM, 128, 1); + default_reactive = CreateTexture(VK_FORMAT_R8_UNORM, 1, 1); + output = CreateTexture(VK_FORMAT_R16G16B16A16_SFLOAT, uw, uh); + render_extent = render_size; + upscale_extent = upscale_size; + first_execution = true; + frame_parity = 0; + LOG_INFO(Render_Vulkan, "FSR 3.1 upscaler: {}x{} -> {}x{}", rw, rh, uw, uh); +} + +void Fsr3Upscaler::Upscale(const Inputs& in) { + 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) { + if (output.image) { + scheduler.Finish(); // the old images may still be in use + } + CreateResources(in.render_size, in.upscale_size); + } + const bool first = first_execution; + const bool reset = in.reset || first; + first_execution = false; + + const s32 rw = static_cast(render_extent.width); + const s32 rh = static_cast(render_extent.height); + const s32 uw = static_cast(upscale_extent.width); + const s32 uh = static_cast(upscale_extent.height); + + // Constants, as fsr3upscalerDispatch computes them. + Constants constants{}; + constants.render_size = {rw, rh}; + constants.previous_render_size = first ? constants.render_size : previous_render_size; + constants.upscale_size = {uw, uh}; + constants.previous_upscale_size = first ? constants.upscale_size : previous_upscale_size; + constants.max_render_size = constants.render_size; + constants.max_upscale_size = constants.upscale_size; + { + // Reversed infinite depth (setupDeviceDepthToViewSpaceDepthParams with near = FLT_MAX). + const f32 near_plane = CAMERA_NEAR; + const f32 fov = CAMERA_FOV; + const f32 aspect = static_cast(rw) / static_cast(rh); + const f32 cot_half_fov = std::cos(0.5f * fov) / std::sin(0.5f * fov); + constants.device_to_view_depth = {-1.0f * (0.0f + FLT_EPSILON), near_plane, + 1.0f / (cot_half_fov / aspect), 1.0f / cot_half_fov}; + constants.tan_half_fov = std::tan(std::atan(std::tan(fov * 0.5f) * aspect)); + } + constants.jitter = in.jitter; + constants.previous_jitter = first ? in.jitter : previous_jitter; + constants.motion_vector_scale = {in.motion_vector_scale[0] / static_cast(rw), + in.motion_vector_scale[1] / static_cast(rh)}; + constants.downscale_factor = {static_cast(rw) / static_cast(uw), + static_cast(rh) / static_cast(uh)}; + constants.motion_vector_jitter_cancellation = {0.0f, 0.0f}; + const s32 phase_count = JitterPhaseCount(render_extent.width, upscale_extent.width); + if (reset || jitter_phase_count == 0.0f) { + jitter_phase_count = static_cast(phase_count); + } else if (static_cast(phase_count) > jitter_phase_count) { + jitter_phase_count += 1.0f; + } else if (static_cast(phase_count) < jitter_phase_count) { + jitter_phase_count -= 1.0f; + } + constants.jitter_phase_count = jitter_phase_count; + constants.delta_time = std::clamp(in.frame_time_ms / 1000.0f, 0.0f, 1.0f); + constants.delta_pre_exposure = 1.0f; + constants.view_space_to_meters = 1.0f; + frame_index = reset ? 0.0f : frame_index + 1.0f; + constants.frame_index = frame_index; + // The SDK's defaults (FFX_FSR3UPSCALER_CONFIGURE_UPSCALE_KEY_*). + constants.velocity_factor = 1.0f; + constants.reactiveness_scale = 1.0f; + constants.shading_change_scale = 1.0f; + constants.accumulation_added_per_frame = 1.0f / 3.0f; + constants.min_disocclusion_accumulation = -1.0f / 3.0f; + previous_render_size = constants.render_size; + previous_upscale_size = constants.upscale_size; + previous_jitter = in.jitter; + + // ffxSpdSetup over the whole render area. + const u32 spd_groups_x = (render_extent.width - 1) / 64 + 1; + const u32 spd_groups_y = (render_extent.height - 1) / 64 + 1; + const SpdConstants spd{ + .mips = static_cast((std::min)( + std::floor(std::log2(static_cast( + (std::max)(render_extent.width, render_extent.height)))), + 12.0f)), + .num_work_groups = spd_groups_x * spd_groups_y, + .work_group_offset = {0, 0}, + .render_size = {render_extent.width, render_extent.height}, + }; + RcasConstants rcas{}; + FSR::FsrRcasCon(rcas.config.data(), 2.0f - 2.0f * std::clamp(in.sharpness, 0.0f, 1.0f)); + + const VkDeviceSize alignment = (std::max)(device.GetUniformBufferAlignment(), 16); + const VkDeviceSize slot = Common::AlignUp(VkDeviceSize{sizeof(Constants)}, alignment); + const VkDeviceSize spd_slot = Common::AlignUp(VkDeviceSize{sizeof(SpdConstants)}, alignment); + const VkDeviceSize rcas_slot = Common::AlignUp(VkDeviceSize{sizeof(RcasConstants)}, alignment); + const StagingBufferRef uniforms = + staging_buffer_pool.Request(slot + spd_slot + rcas_slot, MemoryUsage::Upload); + std::memcpy(uniforms.mapped_span.data(), &constants, sizeof(constants)); + std::memcpy(uniforms.mapped_span.data() + slot, &spd, sizeof(spd)); + std::memcpy(uniforms.mapped_span.data() + slot + spd_slot, &rcas, sizeof(rcas)); + const Resource cb_fsr{.buffer = uniforms.buffer, .offset = uniforms.offset, .size = slot}; + const Resource cb_spd{ + .buffer = uniforms.buffer, .offset = uniforms.offset + slot, .size = spd_slot}; + const Resource cb_rcas{ + .buffer = uniforms.buffer, .offset = uniforms.offset + slot + spd_slot, .size = rcas_slot}; + + // Clears: everything when the images are new, the history on a reset, and the per-frame + // scratch (previous nearest depth, the SPD counter and mips) always. + const size_t p = frame_parity; + const size_t q = p ^ 1; + std::vector clear_zero{*previous_nearest_depth.image, *spd_atomic.image, + *spd_mips.image}; + std::vector clear_frame_info; + if (reset) { + clear_zero.push_back(*accumulation[p].image); + clear_frame_info.push_back(*frame_info.image); + } + std::vector initialize; + if (first) { + for (const auto* textures : {&accumulation, &luma, &luma_history, &internal_upscaled}) { + for (const Texture& texture : *textures) { + initialize.push_back(*texture.image); + } + } + for (const Texture* texture : + {&farthest_depth, &shading_change, &new_locks, &spd_mips, &farthest_depth_mip1, + &dilated_motion_vectors, &dilated_depth, &previous_nearest_depth, + &dilated_reactive_masks, &frame_info, &spd_atomic, &lanczos_lut, &default_reactive, + &output}) { + initialize.push_back(*texture->image); + } + clear_zero = initialize; + std::erase(clear_zero, *lanczos_lut.image); + std::erase(clear_zero, *frame_info.image); + clear_frame_info = {*frame_info.image}; + } + std::optional lut_upload; + if (first) { + // FSR's Lanczos-2 table (fsr3upscalerCreate). + std::array weights{}; + for (size_t i = 0; i < weights.size(); ++i) { + const f32 x = 2.0f * static_cast(i) / static_cast(weights.size() - 1); + const f32 pi_x = std::numbers::pi_v * x; + const f32 y = std::abs(x) < FLT_EPSILON + ? 1.0f + : (std::sin(pi_x) / pi_x) * (std::sin(0.5f * pi_x) / (0.5f * pi_x)); + weights[i] = static_cast(std::round(y * 32767.0f)); + } + lut_upload = staging_buffer_pool.Request(sizeof(weights), MemoryUsage::Upload); + std::memcpy(lut_upload->mapped_span.data(), weights.data(), sizeof(weights)); + } + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([initialize, clear_zero, clear_frame_info, lut = *lanczos_lut.image, + lut_buffer = lut_upload ? lut_upload->buffer : VkBuffer{}, + lut_offset = lut_upload ? lut_upload->offset : 0](vk::CommandBuffer cmdbuf) { + // The inputs come from the game's draws and dispatches; our images from the last frame. + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT | VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, + MemoryBarrier(VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | + VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT | + VK_ACCESS_SHADER_WRITE_BIT | + VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT | + VK_ACCESS_TRANSFER_WRITE_BIT)); + if (!initialize.empty()) { + std::vector barriers; + for (const VkImage image : initialize) { + barriers.push_back(VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_TRANSFER_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 = ALL_LEVELS, + }); + } + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, {}, {}, barriers); + } + if (lut_buffer) { + const VkBufferImageCopy copy{ + .bufferOffset = lut_offset, + .bufferRowLength = 0, + .bufferImageHeight = 0, + .imageSubresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .imageOffset{0, 0, 0}, + .imageExtent{128, 1, 1}, + }; + cmdbuf.CopyBufferToImage(lut_buffer, lut, VK_IMAGE_LAYOUT_GENERAL, copy); + } + for (const VkImage image : clear_zero) { + cmdbuf.ClearColorImage(image, VK_IMAGE_LAYOUT_GENERAL, VkClearColorValue{}, ALL_LEVELS); + } + for (const VkImage image : clear_frame_info) { + VkClearColorValue value{}; + value.float32[0] = -1.0f; + value.float32[1] = 1.0f; + cmdbuf.ClearColorImage(image, VK_IMAGE_LAYOUT_GENERAL, value, ALL_LEVELS); + } + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, + MemoryBarrier(VK_ACCESS_TRANSFER_WRITE_BIT, + VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT)); + }); + + const auto tex = [](const Texture& texture) { return Resource{.view = *texture.view}; }; + const auto mip = [&](u32 level) { + return Resource{.view = *spd_mips.levels[(std::min)(level, static_cast( + spd_mips.levels.size() - 1))]}; + }; + const Resource color{.view = in.color}; + const Resource depth{.view = in.depth}; + const Resource motion{.view = in.motion_vectors}; + const Resource exposure = tex(frame_info); // FSR's own exposure (auto exposure) + const Resource current_luma = tex(luma[p]); + const auto groups = [](s32 size, u32 tile) { + return Common::DivCeil(static_cast(size), tile); + }; + + passes[PassPrepareInputs]->Dispatch( + scheduler, descriptor_queue, + std::array{motion, depth, color, tex(dilated_motion_vectors), tex(dilated_depth), + tex(previous_nearest_depth), tex(farthest_depth), current_luma, cb_fsr}, + groups(rw, 8), groups(rh, 8)); + passes[PassLumaPyramid]->Dispatch( + scheduler, descriptor_queue, + std::array{current_luma, tex(farthest_depth), tex(spd_atomic), tex(frame_info), mip(0), + mip(1), mip(2), mip(3), mip(4), mip(5), tex(farthest_depth_mip1), cb_fsr, + cb_spd}, + spd_groups_x, spd_groups_y); + passes[PassShadingChangePyramid]->Dispatch( + scheduler, descriptor_queue, + std::array{current_luma, tex(luma[q]), tex(dilated_motion_vectors), exposure, + tex(spd_atomic), mip(0), mip(1), mip(2), mip(3), mip(4), mip(5), cb_fsr, cb_spd}, + spd_groups_x, spd_groups_y); + passes[PassShadingChange]->Dispatch( + scheduler, descriptor_queue, std::array{tex(spd_mips), tex(shading_change), cb_fsr}, + groups(rw / 2, 8), groups(rh / 2, 8)); + passes[PassPrepareReactivity]->Dispatch( + scheduler, descriptor_queue, + std::array{tex(previous_nearest_depth), tex(dilated_motion_vectors), tex(dilated_depth), + tex(default_reactive), tex(default_reactive), tex(accumulation[p]), + tex(shading_change), current_luma, exposure, tex(dilated_reactive_masks), + tex(new_locks), tex(accumulation[q]), cb_fsr}, + groups(rw, 8), groups(rh, 8)); + passes[PassLumaInstability]->Dispatch( + scheduler, descriptor_queue, + std::array{exposure, tex(dilated_reactive_masks), tex(dilated_motion_vectors), + tex(frame_info), tex(luma_history[p]), tex(farthest_depth_mip1), current_luma, + tex(luma_history[q]), tex(farthest_depth), cb_fsr}, + groups(rw, 8), groups(rh, 8)); + passes[in.sharpen ? PassAccumulateSharpen : PassAccumulate]->Dispatch( + scheduler, descriptor_queue, + std::array{exposure, tex(dilated_reactive_masks), tex(dilated_motion_vectors), + tex(internal_upscaled[p]), tex(lanczos_lut), tex(farthest_depth_mip1), + current_luma, tex(farthest_depth), color, tex(internal_upscaled[q]), + tex(output), tex(new_locks), cb_fsr}, + groups(uw, 8), groups(uh, 8)); + if (in.sharpen) { + passes[PassRcas]->Dispatch( + scheduler, descriptor_queue, + std::array{exposure, tex(internal_upscaled[q]), tex(output), cb_fsr, cb_rcas}, + groups(uw, 16), groups(uh, 16)); + } + scheduler.Record([](vk::CommandBuffer cmdbuf) { + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, + MemoryBarrier(VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT)); + }); + frame_parity = q; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h new file mode 100644 index 0000000000..e62b5fdeff --- /dev/null +++ b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/vk_update_descriptor.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class DescriptorPool; +class Device; +class MemoryAllocator; +class Scheduler; +class StagingBufferPool; + +/// AMD FidelityFX Super Resolution 3.1 (the FSR 3.1.4 upscaler from FidelityFX SDK v1.1.4, shaders +/// in externals/FidelityFX-SDK) in place of a game's DLSS evaluation. The host side follows the +/// SDK's ffx_fsr3upscaler.cpp: the same passes, internal images and constants, recorded through the +/// scheduler. Inputs are taken as reversed depth, render-size motion vectors and HDR colour, and +/// the exposure is computed by FSR itself. +class Fsr3Upscaler { +public: + struct Inputs { + VkImageView color; ///< Jittered linear colour at the render size. + VkImageView depth; ///< Device depth (reversed) at the render size. + VkImageView 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: input texel k sampled k + 0.5 - jitter. + std::array jitter; + /// Turns a motion vector into render pixels (the render size for UV offsets). + std::array motion_vector_scale; + f32 frame_time_ms; + bool reset; + bool sharpen; + f32 sharpness; ///< 0 to 1 as FSR takes it: 2 stops of RCAS at 0, none at 1. + }; + + explicit Fsr3Upscaler(const Device& device, Scheduler& scheduler, + MemoryAllocator& memory_allocator, StagingBufferPool& staging_buffer_pool, + DescriptorPool& descriptor_pool, + ComputePassDescriptorQueue& descriptor_queue); + ~Fsr3Upscaler(); + + /// Whether the device has what the passes need (formats, shader features). + [[nodiscard]] static bool IsSupported(const Device& device); + + /// Records one evaluation into Output(): RGBA16F at the upscale size, GENERAL layout, ready to + /// be sampled by a fragment shader. + void Upscale(const Inputs& inputs); + + [[nodiscard]] VkImage OutputImage() const noexcept; + [[nodiscard]] VkImageView OutputView() const noexcept; + + /// Nearest-neighbour clamp sampler (normalized coordinates), for reading Output(). + [[nodiscard]] VkSampler PointSampler() const noexcept { + return *point_sampler; + } + +private: + class Pass; + + struct Texture { + vk::Image image; + vk::ImageView view; ///< Every level. + std::vector levels; ///< One view per level, for images with mips. + }; + + void CreateResources(VkExtent2D render_size, VkExtent2D upscale_size); + Texture CreateTexture(VkFormat format, u32 width, u32 height, u32 levels = 1); + + const Device& device; + Scheduler& scheduler; + MemoryAllocator& memory_allocator; + StagingBufferPool& staging_buffer_pool; + ComputePassDescriptorQueue& descriptor_queue; + + vk::Sampler point_sampler; + vk::Sampler linear_sampler; + std::vector> passes; + + // Ping-pong pairs, indexed by frame parity like the SDK's resource tables. + std::array accumulation; + std::array luma; + std::array luma_history; + std::array internal_upscaled; + Texture farthest_depth; ///< Also the luma instability (the SDK aliases them). + Texture shading_change; + Texture new_locks; + Texture spd_mips; + Texture farthest_depth_mip1; + Texture dilated_motion_vectors; + Texture dilated_depth; + Texture previous_nearest_depth; + Texture dilated_reactive_masks; + Texture frame_info; + Texture spd_atomic; + Texture lanczos_lut; + Texture default_reactive; + Texture output; + + VkExtent2D render_extent{}; + VkExtent2D upscale_extent{}; + std::array previous_render_size{}; + std::array previous_upscale_size{}; + std::array previous_jitter{}; + f32 jitter_phase_count{}; + f32 frame_index{}; + u32 frame_parity{}; + bool first_execution{true}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index 978f8be547..6b22c01c43 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1307,10 +1307,10 @@ bool RasterizerVulkan::AccelerateSurfaceCopy(const Tegra::Engines::Fermi2D::Surf bool RasterizerVulkan::AccelerateTemporalUpscale( const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& dst, - const Tegra::Engines::Fermi2D::Surface& motion_vectors, std::array jitter, - std::array motion_scale) { + const Tegra::Engines::Fermi2D::Surface& motion_vectors, const Tegra::Texture::TICEntry* depth, + std::array jitter, std::array motion_scale) { std::scoped_lock lock{texture_cache.mutex}; - return texture_cache.TemporalUpscale(dst, src, motion_vectors, jitter, motion_scale); + return texture_cache.TemporalUpscale(dst, src, motion_vectors, depth, jitter, motion_scale); } Tegra::Engines::AccelerateDMAInterface& RasterizerVulkan::AccessAccelerateDMA() { diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h index dbb6ebdca8..7fa536f53f 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -133,6 +133,7 @@ public: bool AccelerateTemporalUpscale(const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& dst, const Tegra::Engines::Fermi2D::Surface& motion_vectors, + const Tegra::Texture::TICEntry* depth, std::array jitter, std::array motion_scale) override; Tegra::Engines::AccelerateDMAInterface& AccessAccelerateDMA() override; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index ceee8b5dc5..d0edda81c9 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -1203,11 +1203,13 @@ TextureCacheRuntime::TextureCacheRuntime(const Device& device_, Scheduler& sched StagingBufferPool& staging_buffer_pool_, BlitImageHelper& blit_image_helper_, RenderPassCache& render_pass_cache_, - DescriptorPool& descriptor_pool, - ComputePassDescriptorQueue& compute_pass_descriptor_queue) + DescriptorPool& descriptor_pool_, + ComputePassDescriptorQueue& compute_pass_descriptor_queue_) : device{device_}, scheduler{scheduler_}, memory_allocator{memory_allocator_}, staging_buffer_pool{staging_buffer_pool_}, blit_image_helper{blit_image_helper_}, - render_pass_cache{render_pass_cache_}, resolution{Settings::values.resolution_info} { + render_pass_cache{render_pass_cache_}, descriptor_pool{descriptor_pool_}, + compute_pass_descriptor_queue{compute_pass_descriptor_queue_}, + resolution{Settings::values.resolution_info} { // Before anything below reads astc_recompression (passes, view formats). UseBc3ForSwitch2OnSmallCards(device); // Huge ASTC arrays always decode on the GPU (IsHugeAstcArray), whatever the decode setting. @@ -1535,13 +1537,131 @@ void TextureCacheRuntime::ReinterpretImage(Image& dst, Image& src, }); } +bool TextureCacheRuntime::WantsTemporalUpscaleDepth() const { + // FSR 3.1 replaces DLSS; the temporal stand-in only runs when FSR cannot (no depth, a device + // without the formats, a failed start). EDEN_NX2_DLSS_REPLACEMENT=standin forces + // the stand-in for A/B runs. + static const bool force_standin = [] { + const char* const value = std::getenv("EDEN_NX2_DLSS_REPLACEMENT"); + return value && std::string_view{value} == "standin"; + }(); + return !force_standin && !fsr3_unsupported; +} + +bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageView& input_color, + ImageView& motion_vectors, ImageView& depth, + std::array render_size, + std::array jitter, + std::array motion_scale) { + if (!WantsTemporalUpscaleDepth()) { + return false; + } + if (!fsr3_upscaler) { + if (!Fsr3Upscaler::IsSupported(device)) { + LOG_WARNING(Render_Vulkan, "FSR 3.1 needs formats or features this device lacks; " + "using the temporal stand-in"); + fsr3_unsupported = true; + return false; + } + try { + fsr3_upscaler = std::make_unique(device, scheduler, memory_allocator, + staging_buffer_pool, descriptor_pool, + compute_pass_descriptor_queue); + } catch (const vk::Exception& exception) { + LOG_ERROR(Render_Vulkan, "FSR 3.1 failed to start ({}); using the temporal stand-in", + exception.what()); + fsr3_unsupported = true; + 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(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=|off overrides the slider. + static const char* const sharpen_override = std::getenv("EDEN_NX2_TEMPORAL_SHARPEN"); + const int slider = Settings::values.fsr_sharpening_slider.GetValue(); + bool sharpen = slider < 200; + f32 stops = static_cast(slider) / 100.0f; + if (sharpen_override) { + sharpen = std::string_view{sharpen_override} != "off"; + stops = static_cast(std::strtod(sharpen_override, nullptr)); + } + fsr3_upscaler->Upscale({ + .color = input_color.Handle(Shader::TextureType::Color2D), + .depth = depth.DepthView(), + .motion_vectors = motion_vectors.Handle(Shader::TextureType::Color2D), + .render_size = {render_size[0], render_size[1]}, + .upscale_size = extent, + .jitter = jitter, + .motion_vector_scale = {motion_scale[0] * static_cast(render_size[0]), + motion_scale[1] * static_cast(render_size[1])}, + .frame_time_ms = frame_time_ms, + .reset = reset, + .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. + const Region2D region{ + .start = {0, 0}, + .end = {static_cast(extent.width), static_cast(extent.height)}, + }; + const VkImage output = fsr3_upscaler->OutputImage(); + blit_image_helper.BlitColor(dst_framebuffer, fsr3_upscaler->OutputView(), output, + fsr3_upscaler->PointSampler(), region, region, + {extent.width, extent.height, 1}); + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([output](vk::CommandBuffer cmdbuf) { + const VkImageMemoryBarrier barrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = VK_ACCESS_SHADER_READ_BIT, + .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = output, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, barrier); + }); + 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; +} + bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& input_color, ImageView& motion_vectors, + ImageView* depth, std::array render_size, std::array jitter, std::array motion_scale) { if (dst.Samples() != VK_SAMPLE_COUNT_1_BIT || ImageAspectMask(dst.format) != VK_IMAGE_ASPECT_COLOR_BIT) { return false; } + if (depth && UpscaleWithFsr3(dst_framebuffer, input_color, motion_vectors, *depth, render_size, + jitter, motion_scale)) { + return true; + } const VkFormat format = MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, dst.format).format; // The framebuffer's area is the output's size on the host (scaled with resolution scaling). diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 29687fda9c..e7dbb97628 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -6,13 +6,17 @@ #pragma once +#include #include +#include +#include #include #include "video_core/texture_cache/texture_cache_base.h" #include "shader_recompiler/shader_info.h" #include "video_core/renderer_vulkan/vk_compute_pass.h" +#include "video_core/renderer_vulkan/vk_fsr3_upscaler.h" #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" @@ -99,12 +103,17 @@ public: Tegra::Engines::Fermi2D::Filter filter, Tegra::Engines::Fermi2D::Operation operation); - /// Stands in for a DLSS evaluation: temporal upscale of input_color into dst, accumulated with - /// a copy of dst's previous contents (the last output). False when dst cannot take it. + /// Stands in for a DLSS evaluation: temporal upscale of input_color (render_size on the host) + /// into dst, with FSR 3.1 when given the depth, else with the temporal stand-in. False when dst + /// cannot take it. bool TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& input_color, - ImageView& motion_vectors, std::array jitter, + ImageView& motion_vectors, ImageView* depth, + std::array render_size, std::array jitter, std::array motion_scale); + /// True when TemporalUpscale would use the depth (FSR 3.1 not forced off and not failed). + [[nodiscard]] bool WantsTemporalUpscaleDepth() const; + void CopyImage(Image& dst, Image& src, std::span copies); void CopyImageMSAA(Image& dst, Image& src, std::span copies); @@ -187,6 +196,17 @@ public: StagingBufferPool& staging_buffer_pool; BlitImageHelper& blit_image_helper; RenderPassCache& render_pass_cache; + DescriptorPool& descriptor_pool; + ComputePassDescriptorQueue& compute_pass_descriptor_queue; + + /// FSR 3.1 in place of a DLSS evaluation (TemporalUpscale), created on first use. + bool UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageView& input_color, + ImageView& motion_vectors, ImageView& depth, + std::array render_size, std::array jitter, + std::array motion_scale); + std::unique_ptr fsr3_upscaler; + bool fsr3_unsupported{}; + std::optional fsr3_last_evaluation; /// TemporalUpscale's history: its previous result (images[current]) and the image the next /// result is stored to. diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 365137e7d6..ecd8429fa2 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1097,6 +1097,7 @@ template bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& dst, const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& motion_vectors, + const Tegra::Texture::TICEntry* depth, std::array jitter, std::array motion_scale) { if constexpr (FRAMEBUFFER_BLITS) { return false; @@ -1122,6 +1123,12 @@ bool TextureCache

::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. + ImageId depth_id{}; + if (depth && motion_id && runtime.WantsTemporalUpscaleDepth()) { + depth_id = FindOrInsertImage(ImageInfo(*depth), depth->Address(), RelaxedOptions{}); + } if (has_deleted_images || !motion_id) { return false; // the colour and output ids may be stale; the caller blits instead } @@ -1130,6 +1137,13 @@ bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds PrepareImage(src_id, false, false); PrepareImage(motion_id, false, false); PrepareImage(dst_id, true, false); + if (depth_id) { + PrepareImage(depth_id, false, false); + if (slot_images[depth_id].info.num_samples != 1 || + slot_images[depth_id].info.format != PixelFormat::D32_FLOAT) { + depth_id = {}; + } + } for (const ImageId id : {src_id, motion_id, dst_id}) { if (slot_images[id].info.num_samples != 1) { return false; @@ -1140,13 +1154,25 @@ bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds const auto rescaled = [&](ImageId id) { return True(slot_images[id].flags & ImageFlagBits::Rescaled); }; - if (rescaled(src_id) || rescaled(motion_id) || rescaled(dst_id)) { - for (const ImageId id : {src_id, motion_id, dst_id}) { - if (!rescaled(id) && ImageCanRescale(slot_images[id])) { + if (rescaled(src_id) || rescaled(motion_id) || rescaled(dst_id) || + (depth_id && rescaled(depth_id))) { + for (const ImageId id : {src_id, motion_id, dst_id, depth_id}) { + if (id && !rescaled(id) && ImageCanRescale(slot_images[id])) { ScaleUp(slot_images[id]); } } } + // FSR 3.1 reads colour, motion and depth texel for texel: all at the colour's scale. + if (depth_id && (rescaled(depth_id) != rescaled(src_id) || + rescaled(motion_id) != rescaled(src_id))) { + depth_id = {}; + } + const Extent3D src_size = slot_images[src_id].info.size; + const std::array render_size = + rescaled(src_id) + ? std::array{Settings::values.resolution_info.ScaleUp(src_size.width), + Settings::values.resolution_info.ScaleUp(src_size.height)} + : std::array{src_size.width, src_size.height}; // The jitter is in the input's guest pixels. if (rescaled(src_id)) { const f32 factor = Settings::values.resolution_info.up_factor; @@ -1167,12 +1193,21 @@ bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds if (!src_view || !motion_view || !dst_view) { return false; } + ImageViewId depth_view{}; + if (depth_id) { + if (const std::optional base = slot_images[depth_id].TryFindBase(depth->Address())) { + const SubresourceRange range{.base = *base, .extent = {1, 1}}; + depth_view = FindOrEmplaceImageView( + depth_id, ImageViewInfo(ImageViewType::e2D, PixelFormat::D32_FLOAT, range)); + } + } // After the views: creating them may reallocate the framebuffer slots. Framebuffer* const dst_framebuffer = &slot_framebuffers[dst_view->first]; - return runtime.TemporalUpscale(dst_framebuffer, slot_image_views[dst_view->second], - slot_image_views[src_view->second], - slot_image_views[motion_view->second], jitter, - motion_scale); + return runtime.TemporalUpscale( + dst_framebuffer, slot_image_views[dst_view->second], slot_image_views[src_view->second], + slot_image_views[motion_view->second], + depth_view ? &slot_image_views[depth_view] : nullptr, render_size, jitter, + motion_scale); } } diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h index 09deb8da02..5847cae7ba 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -288,7 +288,8 @@ public: bool TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& dst, const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& motion_vectors, - std::array jitter, std::array motion_scale); + const Tegra::Texture::TICEntry* depth, std::array jitter, + std::array motion_scale); /// Try to find a cached image view in the given CPU address [[nodiscard]] std::pair TryFindFramebufferImageView( diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h index 622d792210..7175f5dd91 100644 --- a/src/video_core/vulkan_common/vulkan_device.h +++ b/src/video_core/vulkan_common/vulkan_device.h @@ -561,6 +561,11 @@ FN_MAX_LIMIT_LIST return features.features.shaderStorageImageReadWithoutFormat; } + /// Returns true if storage images of the extended formats (e.g. R16F, RG16F, R8) are supported. + bool IsStorageImageExtendedFormatsSupported() const { + return features.features.shaderStorageImageExtendedFormats; + } + /// Returns true if shader int64 is supported. bool IsShaderInt64Supported() const { return features.features.shaderInt64; -- cgit v1.2.3