diff options
| author | Duck912 <350528965+Duck912@users.noreply.github.com> | 2026-11-02 16:07:08 -0500 |
|---|---|---|
| committer | Duck912 <350528965+Duck912@users.noreply.github.com> | 2026-11-02 16:28:01 -0500 |
| commit | 960ebcc73d998c7c596c3377b0ec8a1d6eefccf1 (patch) | |
| tree | 3382749be8d24a053c5f74d28e2388a9dd704eb8 /src | |
| parent | bb8fdfac21434db0bfd62c56b0a409ce26070a38 (diff) | |
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=<stops>|off overrides the slider.
Assisted-by: Claude Opus 5.5 (debugging, implementation and drafting)
Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>
Diffstat (limited to 'src')
| -rw-r--r-- | src/qt_common/config/shared_translation.cpp | 6 | ||||
| -rw-r--r-- | src/video_core/host_shaders/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | src/video_core/host_shaders/temporal_sharpen.frag | 49 | ||||
| -rw-r--r-- | src/video_core/host_shaders/temporal_upscale.frag | 85 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/blit_image.cpp | 166 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/blit_image.h | 25 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_texture_cache.cpp | 227 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/vk_texture_cache.h | 8 | ||||
| -rw-r--r-- | src/video_core/texture_cache/texture_cache.h | 20 |
9 files changed, 440 insertions, 147 deletions
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<TranslationMap> 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 <algorithm> #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<float, 2> jitter; std::array<float, 2> 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<u32, 4> rcas; +}; + struct MSAACopyPushConstants { std::array<s32, 2> dst_offset; std::array<s32, 2> 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<u32>(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<u32>(THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()), - .pBindings = THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(), + .bindingCount = static_cast<u32>(TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()), + .pBindings = TEMPORAL_UPSCALE_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(), }; template <VkShaderStageFlags stageFlags, size_t size> 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<VkImageView, 3> image_views) { - std::array<VkDescriptorImageInfo, 3> image_infos{}; - std::array<VkWriteDescriptorSet, 3> write_descriptor_sets{}; - for (u32 binding = 0; binding < 3; ++binding) { +void UpdateTemporalUpscaleDescriptorSet(const Device& device, VkDescriptorSet descriptor_set, + VkSampler sampler, std::array<VkImageView, 4> image_views) { + std::array<VkDescriptorImageInfo, 4> image_infos{}; + std::array<VkWriteDescriptorSet, 4> 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<VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstants)>))), @@ -655,9 +686,14 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_, PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstants)>))), temporal_upscale_pipeline_layout( device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo( - three_textures_set_layout.address(), + temporal_upscale_set_layout.address(), PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(TemporalUpscalePushConstants)>))), + temporal_sharpen_pipeline_layout( + device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo( + one_texture_set_layout.address(), + PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + sizeof(TemporalSharpenPushConstants)>))), clear_color_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo( nullptr, PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(float) * 4>))), 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<f32, 2> jitter, std::array<f32, 2> 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<VkImageView, 3> views{input_color.Handle(Shader::TextureType::Color2D), + const std::array<VkImageView, 4> 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<u32>(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<f32, 2> jitter, - std::array<f32, 2> motion_scale, f32 current_weight); + VkImageView next_history, const Region2D& dst_region, + std::array<f32, 2> jitter, + std::array<f32, 2> 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<vk::Pipeline> blit_color_pipelines; std::vector<VkRenderPass> temporal_upscale_keys; std::vector<vk::Pipeline> temporal_upscale_pipelines; + std::vector<VkRenderPass> temporal_sharpen_keys; + std::vector<vk::Pipeline> temporal_sharpen_pipelines; std::vector<BlitImagePipelineKey> blit_depth_stencil_keys; std::vector<vk::Pipeline> blit_depth_stencil_pipelines; std::vector<BlitImagePipelineKey> 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<u32>(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<u32>(dst.range.base.level), - .baseArrayLayer = static_cast<u32>(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<s32>(extent.width), static_cast<s32>(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<f32>(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=<stops>|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<f32>(slider) / 100.0f; + if (sharpen_override) { + sharpen = std::string_view{sharpen_override} != "off"; + sharpness = static_cast<f32>(std::strtod(sharpen_override, nullptr)); + } + const VkImage output = dst.ImageHandle(); + const u32 output_level = static_cast<u32>(dst.range.base.level); + const u32 output_layer = static_cast<u32>(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<VkImageMemoryBarrier>(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<vk::Image, 2> images; + std::array<vk::ImageView, 2> 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<P>::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<std::pair<FramebufferId, ImageViewId>> { const std::optional base = slot_images[id].TryFindBase(surface.Address()); |
