From 52be634d6c7a94cbd5a03bd4f844e19a343d4285 Mon Sep 17 00:00:00 2001 From: vibecodekun <328927837+vibecodekun@users.noreply.github.com> Date: Wed, 4 Nov 2026 16:41:46 -0500 Subject: [PATCH 1/3] renderer_vulkan: reset dynamic state after BlitColor's view blit The BlitColor overload that takes a raw image view, which draws the FSR 3.1 and NVIDIA DLSS outputs, never invalidated the scheduler's state the way BlitImpl does. The game's next draws reused stale dynamic state and wrote nothing: with FXAA on, Fast Fusion kept showing its title screen while its menu ran underneath. Assisted-by: Claude Opus 5.5 (debugging, implementation, message draft) --- src/video_core/renderer_vulkan/blit_image.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp index 4a274b2915..3f8b58f0b1 100644 --- a/src/video_core/renderer_vulkan/blit_image.cpp +++ b/src/video_core/renderer_vulkan/blit_image.cpp @@ -804,6 +804,9 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView cmdbuf.Draw(3, 1, 0, 0); cmdbuf.EndRenderPass(); }); + // Like BlitImpl: the game's draws that follow set their dynamic state again (Fast Fusion's + // menu composited nothing after an FSR 3.1 output was drawn here). + scheduler.InvalidateState(); } void BlitImageHelper::TemporalUpscale(const Framebuffer* dst_framebuffer, From 0d102b645f0fc235e71a3bdc3f2923165a6c8188 Mon Sep 17 00:00:00 2001 From: vibecodekun <328927837+vibecodekun@users.noreply.github.com> Date: Wed, 4 Nov 2026 16:41:46 -0500 Subject: [PATCH 2/3] video_core: draw Fast Fusion's menu vehicles with FSR 3.1 Fast Fusion's menus (main menu, Fusion Shop, vehicle select) draw the vehicles through a DLSS evaluation at the same size (DLAA), in Pure mode too, and blend its output over the menu by alpha. The network's passes can't run, so nothing was written and no vehicle showed. Recognise Fast Fusion's inputs next to Fortune's Weave's: RGBA16F colour, motion vectors in NDC, standard D24S8 depth and an output the size of the input. FSR 3.1 now takes standard depth, reversed into an image of its own first, and carries the colour's alpha to its output, accumulated along its motion vectors. NVIDIA DLSS is told whether the depth is inverted. Fast Fusion's network is not translated: compiling it froze the menu for about 13 seconds and its output then failed the checks. Assisted-by: Claude Opus 5.5 (debugging, implementation, message draft) --- src/video_core/engines/kepler_compute.cpp | 109 ++++++++++++------ src/video_core/engines/kepler_compute.h | 12 +- src/video_core/host_shaders/CMakeLists.txt | 2 + .../host_shaders/fsr3_upscaler_alpha.comp | 87 ++++++++++++++ .../fsr3_upscaler_reverse_depth.comp | 21 ++++ src/video_core/rasterizer_interface.h | 6 +- .../renderer_vulkan/vk_fsr3_upscaler.cpp | 31 ++++- .../renderer_vulkan/vk_fsr3_upscaler.h | 12 +- .../renderer_vulkan/vk_rasterizer.cpp | 5 +- .../renderer_vulkan/vk_rasterizer.h | 2 +- .../renderer_vulkan/vk_streamline_dlss.cpp | 2 +- .../renderer_vulkan/vk_streamline_dlss.h | 3 +- .../renderer_vulkan/vk_texture_cache.cpp | 18 +-- .../renderer_vulkan/vk_texture_cache.h | 10 +- src/video_core/texture_cache/texture_cache.h | 15 ++- .../texture_cache/texture_cache_base.h | 4 +- 16 files changed, 270 insertions(+), 69 deletions(-) create mode 100644 src/video_core/host_shaders/fsr3_upscaler_alpha.comp create mode 100644 src/video_core/host_shaders/fsr3_upscaler_reverse_depth.comp diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp index 23ffbad790..f32843a000 100644 --- a/src/video_core/engines/kepler_compute.cpp +++ b/src/video_core/engines/kepler_compute.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -642,7 +643,7 @@ VideoCore::GameDlssReason KeplerCompute::GameDlssBlocker() const { bool KeplerCompute::EmulatesNativeTensorPrograms() const { if (dlss_chain.active) { - return dlss_chain.failure == VideoCore::GameDlssReason::None && + return dlss_network_translated && dlss_chain.failure == VideoCore::GameDlssReason::None && GameDlssBlocker() == VideoCore::GameDlssReason::None; } // Donkey Kong Bananza's programs are emulated: rejected, they write nothing and the screen is @@ -807,8 +808,9 @@ bool KeplerCompute::FinishDlssEvaluation() { return UpscaleInsteadOfNativeProgram(); } -// Fortune's Weave's last DLSS network pass: it reads the input colour (the first pass's) through -// cbuf0+0x328 and writes the upscaled image through +0x348. +// The last DLSS network pass reads the input colour (the first pass's) through cbuf0+0x328 and +// writes the output through +0x348: larger in Fortune's Weave, the same size in Fast Fusion's menus +// (DLAA). bool KeplerCompute::IsDlssOutputPass() { const auto& cbuf = launch_description.const_buffer_config[0]; if (cbuf.size.Value() < 0x34c) { @@ -824,7 +826,8 @@ bool KeplerCompute::IsDlssOutputPass() { }; const auto input = image(0x328), output = image(0x348); return input && output && input->Address() == dlss_motion_color && output->Address() != 0 && - input->format == output->format && output->Width() > input->Width(); + output->Address() != input->Address() && input->format == output->format && + output->Width() >= input->Width() && output->Height() >= input->Height(); } // Logs the images a rejected program's handles (constant bank 0, bytes 0x80-0x11c) point to, so a @@ -853,10 +856,14 @@ 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, 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. +// The first pass of a DLSS evaluation reads its inputs through cbuf0, with this frame's camera +// jitter (x, y; input pixels, Halton bases 2 and 3 less 0.5) at +0x1b0. Fire Emblem: Fortune's +// Weave's (rejected, before the network) has the input colour (B10G11R11) at +0x278, its motion +// vectors (R16G16 float, UV offsets) at +0x290 and its depth (Z32, reversed) at +0x298. Fast +// Fusion's (its menus draw the vehicles with DLAA) has the colour (R16G16B16A16 float) at +0x278, +// the motion vectors (R16G16 float) at +0x284 with their scale to input pixels at +0x198 (half the +// input's size: they are in NDC), the depth (D24S8) at +0x288 and whether it is reversed at +// +0x218. The motion vectors and depth are the colour's size. bool KeplerCompute::RememberDlssMotionVectors() { const auto& cbuf = launch_description.const_buffer_config[0]; if (cbuf.size.Value() < 0x294) return false; @@ -866,24 +873,52 @@ bool KeplerCompute::RememberDlssMotionVectors() { if (tic_index > regs.tic.limit) return std::nullopt; return GetTICEntry(tic_index); }; - const auto color = image(0x278), motion = image(0x290); - if (!color || !motion || color->format != Texture::TextureFormat::B10G11R11 || - motion->format != Texture::TextureFormat::R16G16 || motion->Address() == 0 || - motion->Width() != color->Width() || motion->Height() != color->Height() || - !motion->IsBlockLinear()) + const auto value = [&](u32 offset) { + return std::bit_cast(memory_manager.Read(cbuf_address + offset)); + }; + const auto color = image(0x278); + if (!color) return false; + const auto fits = [&](const std::optional& tic) { + return tic && tic->Address() != 0 && tic->Width() == color->Width() && + tic->Height() == color->Height() && tic->IsBlockLinear(); + }; + using Texture::TextureFormat; + std::optional motion, depth; + std::array motion_scale{1.0f, 1.0f}; + bool depth_reversed = true; + if (color->format == TextureFormat::B10G11R11) { + motion = image(0x290); + depth = cbuf.size.Value() >= 0x29c ? image(0x298) : std::nullopt; + if (!fits(depth) || depth->format != TextureFormat::Z32) { + depth.reset(); + } + } else if (color->format == TextureFormat::R16G16B16A16) { + motion = image(0x284); + depth = image(0x288); + if (!fits(depth) || (depth->format != TextureFormat::S8Z24 && + depth->format != TextureFormat::Z24S8 && + depth->format != TextureFormat::X8Z24 && + depth->format != TextureFormat::Z32)) { + return false; + } + motion_scale = {value(0x198) / static_cast(color->Width()), + value(0x19c) / static_cast(color->Height())}; + if (!std::isfinite(motion_scale[0]) || !std::isfinite(motion_scale[1]) || + motion_scale[0] == 0.0f || motion_scale[1] == 0.0f) { + return false; + } + depth_reversed = memory_manager.Read(cbuf_address + 0x218) != 0; + } else { return false; + } + if (!fits(motion) || motion->format != TextureFormat::R16G16) return false; 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))}; + dlss_motion_scale = motion_scale; + dlss_depth = depth; + dlss_depth_reversed = depth_reversed; + dlss_network_translated = color->format == TextureFormat::B10G11R11; + dlss_jitter = {value(0x1b0), value(0x1b4)}; return true; } @@ -894,7 +929,8 @@ bool KeplerCompute::RememberDlssMotionVectors() { // the input colour through cbuf0+0x328 and writes the DLSS output through cbuf0+0x348 (B10G11R11; // 720p to 1440p docked). Blit one to the other with bilinear filtering when both are plain 2D // images of the same format and the output is larger: title 4's slots first, then Fortune's -// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair. +// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair. A DLSS +// evaluation's output may also be the input's size (Fast Fusion's menus). bool KeplerCompute::UpscaleInsteadOfNativeProgram() { const auto& cbuf = launch_description.const_buffer_config[0]; const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low; @@ -910,13 +946,19 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { to->IsBlockLinear(); }; std::optional input = image(0x90), output = image(0xa0); - bool fortunes_weave = false; + bool dlss_evaluation = false; if (!upscale_pair(input, output)) { input = image(0x328); output = image(0x348); - fortunes_weave = upscale_pair(input, output); - } - if (!upscale_pair(input, output)) { + dlss_evaluation = + upscale_pair(input, output) || + (input && output && dlss_motion_vectors && input->Address() == dlss_motion_color && + input->format == output->format && output->Address() != 0 && + output->Address() != input->Address() && output->Width() == input->Width() && + output->Height() == input->Height() && input->IsBlockLinear() && + output->IsBlockLinear()); + } + if (!dlss_evaluation && !upscale_pair(input, output)) { input.reset(); output.reset(); for (u32 first = 0x80; first < 0x120 && !input; first += 4) { @@ -1054,22 +1096,23 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() { source.format = static_cast(std::strtoul(value, nullptr, 16)); } } - // Fortune's Weave: a temporal upscale along its motion vectors that undoes this frame's camera - // jitter (both from the frame's first DLSS pass) and accumulates the frames as DLSS would. - // EDEN_NX2_TEMPORAL_UPSCALE=0 blits instead. + // A DLSS evaluation: a temporal upscale along its motion vectors that undoes this frame's + // camera jitter (both from the frame's first DLSS pass) and accumulates the frames as DLSS + // would. EDEN_NX2_TEMPORAL_UPSCALE=0 blits instead. const auto upscale_temporally = [&] { static const bool enabled = [] { const char* value = std::getenv("EDEN_NX2_TEMPORAL_UPSCALE"); return value == nullptr || std::string_view{value} != "0"; }(); - if (!fortunes_weave || !enabled || !dlss_motion_vectors || + if (!dlss_evaluation || !enabled || !dlss_motion_vectors || dlss_motion_color != input->Address()) return false; Fermi2D::Surface motion = surface(*dlss_motion_vectors); motion.format = RTFormat::R16G16_FLOAT; return rasterizer->AccelerateTemporalUpscale(source, surface(*output), motion, dlss_depth ? &*dlss_depth : nullptr, - dlss_jitter, {1.0f, 1.0f}); + dlss_depth_reversed, dlss_jitter, + dlss_motion_scale); }; 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 bda4898bc2..af737dbe3a 100644 --- a/src/video_core/engines/kepler_compute.h +++ b/src/video_core/engines/kepler_compute.h @@ -306,13 +306,19 @@ class KeplerCompute final : public EngineInterface { VideoCore::GameDlssStatus status{}; } dlss_chain; - /// 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. + /// Motion vectors and depth of the frame'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, what turns the motion vectors into UV offsets, and whether the + /// depth is reversed. std::optional dlss_motion_vectors; std::optional dlss_depth; GPUVAddr dlss_motion_color{}; std::array dlss_jitter{}; + std::array dlss_motion_scale{1.0f, 1.0f}; + bool dlss_depth_reversed{true}; + /// Whether the evaluation's network is translated when it may draw: Fortune's Weave's. Fast + /// Fusion's took the host driver 13 s to compile, freezing its menu. + bool dlss_network_translated{true}; bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call); NativeQmdScheduler native_qmd; std::array* native_data_ram{}; diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt index bc3d3db54c..627b82a208 100644 --- a/src/video_core/host_shaders/CMakeLists.txt +++ b/src/video_core/host_shaders/CMakeLists.txt @@ -98,6 +98,8 @@ set(SHADER_FILES ${CMAKE_CURRENT_SOURCE_DIR}/sgsr1_shader_mobile_edge_direction.frag ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_optical_flow.comp ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_frame_interpolation.comp + ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_upscaler_alpha.comp + ${CMAKE_CURRENT_SOURCE_DIR}/fsr3_upscaler_reverse_depth.comp ${CMAKE_CURRENT_SOURCE_DIR}/dlss_network_probe.comp ) diff --git a/src/video_core/host_shaders/fsr3_upscaler_alpha.comp b/src/video_core/host_shaders/fsr3_upscaler_alpha.comp new file mode 100644 index 0000000000..e9e365b1d6 --- /dev/null +++ b/src/video_core/host_shaders/fsr3_upscaler_alpha.comp @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#version 460 core +#extension GL_EXT_samplerless_texture_functions : require + +// DLSS can upscale the colour's alpha (Fast Fusion blends its menu vehicles over the menu with it); +// FSR 3.1 writes 1. Carry the input's alpha into FSR's output: sampled where the output pixel lies +// in the unjittered frame, then blended with the previous result along FSR's dilated motion vectors +// and kept within the alpha around the sample, so edges settle on their coverage instead of +// following the camera jitter. + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(binding = 0) uniform texture2D input_color; +layout(binding = 1) uniform texture2D dilated_motion_vectors; // UV offsets to the previous frame +layout(binding = 2) uniform texture2D history; +layout(binding = 3, r16f) uniform writeonly image2D next_history; +layout(binding = 4, rgba16f) uniform image2D upscaled_output; + +// The start of cbFSR3Upscaler (ffx_fsr3upscaler_callbacks_glsl.h). +layout(binding = 5, std140) uniform Constants { + ivec2 render_size; + ivec2 previous_render_size; + ivec2 upscale_size; + ivec2 previous_upscale_size; + ivec2 max_render_size; + ivec2 max_upscale_size; + vec4 device_to_view_depth; + vec2 jitter; // input texel k shows the frame at k + 0.5 - jitter + vec2 previous_jitter; + vec2 motion_vector_scale; + vec2 downscale_factor; + vec2 motion_vector_jitter_cancellation; + float tan_half_fov; + float jitter_phase_count; + float delta_time; + float delta_pre_exposure; + float view_space_to_meters; + float frame_index; // 0 when the history starts over +}; + +layout(binding = 1001) uniform sampler linear_clamp; + +// The share of this frame's alpha in the result, as a temporal anti-aliasing resolve takes it. +const float CURRENT_WEIGHT = 0.1; + +// Alpha as blending takes it: 0 to 1, and 0 for what the game left undefined (NaN), which would +// otherwise stay in the history. +float Coverage(float alpha) { + return isnan(alpha) ? 0.0 : clamp(alpha, 0.0, 1.0); +} + +void main() { + const ivec2 position = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(position, upscale_size))) { + return; + } + const vec2 uv = (vec2(position) + 0.5) / vec2(upscale_size); + const vec2 sample_uv = uv + jitter / vec2(render_size); + const ivec2 center = ivec2(sample_uv * vec2(render_size)); + float lowest = 1.0; + float highest = 0.0; + for (int y = -1; y <= 1; ++y) { + for (int x = -1; x <= 1; ++x) { + const ivec2 texel = clamp(center + ivec2(x, y), ivec2(0), render_size - 1); + const float alpha = Coverage(texelFetch(input_color, texel, 0).a); + lowest = min(lowest, alpha); + highest = max(highest, alpha); + } + } + const float current = clamp( + Coverage(textureLod(sampler2D(input_color, linear_clamp), sample_uv, 0.0).a), lowest, + highest); + + float alpha = current; + const ivec2 motion_texel = clamp(ivec2(uv * vec2(render_size)), ivec2(0), render_size - 1); + const vec2 previous = uv + texelFetch(dilated_motion_vectors, motion_texel, 0).xy; + if (frame_index > 0.0 && all(greaterThanEqual(previous, vec2(0.0))) && + all(lessThanEqual(previous, vec2(1.0)))) { + const float remembered = + Coverage(textureLod(sampler2D(history, linear_clamp), previous, 0.0).r); + alpha = mix(clamp(remembered, lowest, highest), current, CURRENT_WEIGHT); + } + imageStore(next_history, position, vec4(alpha)); + imageStore(upscaled_output, position, vec4(imageLoad(upscaled_output, position).rgb, alpha)); +} diff --git a/src/video_core/host_shaders/fsr3_upscaler_reverse_depth.comp b/src/video_core/host_shaders/fsr3_upscaler_reverse_depth.comp new file mode 100644 index 0000000000..c8e6ae22b9 --- /dev/null +++ b/src/video_core/host_shaders/fsr3_upscaler_reverse_depth.comp @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#version 460 core +#extension GL_EXT_samplerless_texture_functions : require + +// FSR 3.1's passes are built for reversed depth (FFX_FSR3UPSCALER_OPTION_INVERTED_DEPTH): reverse +// a game's standard depth (0 near, 1 far) for them. + +layout(local_size_x = 8, local_size_y = 8, local_size_z = 1) in; + +layout(binding = 0) uniform texture2D depth; +layout(binding = 1, r32f) uniform writeonly image2D reversed_depth; + +void main() { + const ivec2 position = ivec2(gl_GlobalInvocationID.xy); + if (any(greaterThanEqual(position, imageSize(reversed_depth)))) { + return; + } + imageStore(reversed_depth, position, vec4(1.0 - texelFetch(depth, position, 0).r)); +} diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h index 133a4448b4..9b7a0cd19b 100644 --- a/src/video_core/rasterizer_interface.h +++ b/src/video_core/rasterizer_interface.h @@ -183,12 +183,12 @@ class RasterizerInterface { /// 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), with the frame's depth when known. False when unsupported; the caller then falls - /// back to AccelerateSurfaceCopy. + /// frame), with the frame's depth (reversed or not) 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, - const Tegra::Texture::TICEntry* depth, std::array jitter, + const Tegra::Texture::TICEntry* depth, bool depth_reversed, std::array jitter, std::array motion_scale) { return false; } diff --git a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp index b199005b33..6909774751 100644 --- a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp +++ b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.cpp @@ -15,12 +15,14 @@ #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_alpha_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_reverse_depth_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" @@ -90,8 +92,13 @@ 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}; +// Not FSR's: fsr3_upscaler_reverse_depth.comp and fsr3_upscaler_alpha.comp. +constexpr std::array REVERSE_DEPTH{T, I}; +constexpr std::array UPSCALE_ALPHA{T, T, T, I, I, U}; enum PassId : size_t { + PassReverseDepth, + PassUpscaleAlpha, PassPrepareInputs, PassLumaPyramid, PassShadingChangePyramid, @@ -279,6 +286,8 @@ Fsr3Upscaler::Fsr3Upscaler(const Device& device_, Scheduler& scheduler_, return std::make_unique(device, scheduler, descriptor_pool, kinds, code, samplers); }; passes.resize(PassCount); + passes[PassReverseDepth] = pass(REVERSE_DEPTH, FSR3_UPSCALER_REVERSE_DEPTH_COMP_SPV); + passes[PassUpscaleAlpha] = pass(UPSCALE_ALPHA, FSR3_UPSCALER_ALPHA_COMP_SPV); 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) @@ -391,6 +400,7 @@ void Fsr3Upscaler::CreateResources(VkExtent2D render_size, VkExtent2D upscale_si // 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); + alpha_history[i] = CreateTexture(VK_FORMAT_R16_SFLOAT, uw, uh); } farthest_depth = CreateTexture(VK_FORMAT_R16_SFLOAT, rw, rh); shading_change = CreateTexture(VK_FORMAT_R8_UNORM, rw / 2, rh / 2); @@ -408,6 +418,7 @@ void Fsr3Upscaler::CreateResources(VkExtent2D render_size, VkExtent2D upscale_si 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); + reversed_depth = CreateTexture(VK_FORMAT_R32_SFLOAT, rw, rh); output = CreateTexture(VK_FORMAT_R16G16B16A16_SFLOAT, uw, uh); render_extent = render_size; upscale_extent = upscale_size; @@ -527,7 +538,8 @@ void Fsr3Upscaler::Upscale(const Inputs& in) { } std::vector initialize; if (first) { - for (const auto* textures : {&accumulation, &luma, &luma_history, &internal_upscaled}) { + for (const auto* textures : + {&accumulation, &luma, &luma_history, &internal_upscaled, &alpha_history}) { for (const Texture& texture : *textures) { initialize.push_back(*texture.image); } @@ -536,7 +548,7 @@ void Fsr3Upscaler::Upscale(const Inputs& in) { {&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}) { + &reversed_depth, &output}) { initialize.push_back(*texture->image); } clear_zero = initialize; @@ -629,13 +641,19 @@ void Fsr3Upscaler::Upscale(const Inputs& in) { 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); }; + if (!in.depth_reversed) { + passes[PassReverseDepth]->Dispatch(scheduler, descriptor_queue, + std::array{Resource{.view = in.depth}, + tex(reversed_depth)}, + groups(rw, 8), groups(rh, 8)); + } + const Resource depth{.view = in.depth_reversed ? in.depth : *reversed_depth.view}; passes[PassPrepareInputs]->Dispatch( scheduler, descriptor_queue, @@ -682,6 +700,13 @@ void Fsr3Upscaler::Upscale(const Inputs& in) { std::array{exposure, tex(internal_upscaled[q]), tex(output), cb_fsr, cb_rcas}, groups(uw, 16), groups(uh, 16)); } + if (in.upscale_alpha) { + passes[PassUpscaleAlpha]->Dispatch( + scheduler, descriptor_queue, + std::array{color, tex(dilated_motion_vectors), tex(alpha_history[p]), + tex(alpha_history[q]), tex(output), cb_fsr}, + groups(uw, 8), groups(uh, 8)); + } scheduler.Record([](vk::CommandBuffer cmdbuf) { cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT, 0, diff --git a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h index e62b5fdeff..ad05b250f5 100644 --- a/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h +++ b/src/video_core/renderer_vulkan/vk_fsr3_upscaler.h @@ -22,13 +22,17 @@ 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. +/// scheduler. Inputs are taken as render-size motion vectors and HDR colour, and the exposure is +/// computed by FSR itself. The passes are built for reversed depth; a standard one is reversed +/// into an image of its own first. FSR has no alpha; the colour's can be carried to the output by +/// a pass of our own (fsr3_upscaler_alpha.comp). class Fsr3Upscaler { public: struct Inputs { VkImageView color; ///< Jittered linear colour at the render size. - VkImageView depth; ///< Device depth (reversed) at the render size. + bool upscale_alpha; ///< Carry the colour's alpha to the output (else it is 1). + VkImageView depth; ///< Device depth at the render size. + bool depth_reversed; ///< Whether the depth is reversed (1 near, 0 far). VkImageView motion_vectors; ///< Motion to the previous frame at the render size. VkExtent2D render_size; VkExtent2D upscale_size; @@ -90,6 +94,7 @@ class Fsr3Upscaler { std::array luma; std::array luma_history; std::array internal_upscaled; + std::array alpha_history; ///< The output's alpha (upscale_alpha) Texture farthest_depth; ///< Also the luma instability (the SDK aliases them). Texture shading_change; Texture new_locks; @@ -103,6 +108,7 @@ class Fsr3Upscaler { Texture spd_atomic; Texture lanczos_lut; Texture default_reactive; + Texture reversed_depth; ///< A standard depth input, reversed. Texture output; VkExtent2D render_extent{}; diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp index bb8c7f57a3..845c8ce447 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp +++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp @@ -1439,9 +1439,10 @@ std::optional RasterizerVulkan::ProbeGameDlssOutput( bool RasterizerVulkan::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) { + bool depth_reversed, std::array jitter, std::array motion_scale) { std::scoped_lock lock{texture_cache.mutex}; - return texture_cache.TemporalUpscale(dst, src, motion_vectors, depth, jitter, motion_scale); + return texture_cache.TemporalUpscale(dst, src, motion_vectors, depth, depth_reversed, 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 1e550a27ca..aff20f4d9c 100644 --- a/src/video_core/renderer_vulkan/vk_rasterizer.h +++ b/src/video_core/renderer_vulkan/vk_rasterizer.h @@ -141,7 +141,7 @@ class RasterizerVulkan final : public VideoCore::RasterizerInterface, 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, + const Tegra::Texture::TICEntry* depth, bool depth_reversed, std::array jitter, std::array motion_scale) override; Tegra::Engines::AccelerateDMAInterface& AccessAccelerateDMA() override; diff --git a/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp index 4fa9e240c4..c669fd40c6 100644 --- a/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp +++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.cpp @@ -355,7 +355,7 @@ bool StreamlineDlssUpscaler::Upscale(const Inputs& in) { constants.cameraFar = 1000.0f; constants.cameraFOV = 1.0f; constants.cameraAspectRatio = rw / rh; - constants.depthInverted = sl::eTrue; + constants.depthInverted = in.depth_reversed ? sl::eTrue : sl::eFalse; constants.cameraMotionIncluded = sl::eTrue; constants.motionVectors3D = sl::eFalse; constants.reset = reset ? sl::eTrue : sl::eFalse; diff --git a/src/video_core/renderer_vulkan/vk_streamline_dlss.h b/src/video_core/renderer_vulkan/vk_streamline_dlss.h index 9f8704fe15..4f040280c4 100644 --- a/src/video_core/renderer_vulkan/vk_streamline_dlss.h +++ b/src/video_core/renderer_vulkan/vk_streamline_dlss.h @@ -30,7 +30,8 @@ class StreamlineDlssUpscaler { struct Inputs { Image color; ///< Jittered linear colour at the render size. - Image depth; ///< Device depth (reversed) at the render size. + Image depth; ///< Device depth at the render size. + bool depth_reversed; ///< Whether the depth is reversed (1 near, 0 far). Image motion_vectors; ///< Motion to the previous frame at the render size. VkExtent2D render_size; VkExtent2D upscale_size; diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp index 6501a2c15b..e1ea3924b4 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp @@ -1575,7 +1575,7 @@ std::optional TextureCacheRuntime::ProbeGameDlssOutput bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageView& input_color, ImageView& motion_vectors, ImageView& depth, - std::array render_size, + bool depth_reversed, std::array render_size, std::array jitter, std::array motion_scale) { if (!WantsTemporalUpscaleDepth()) { @@ -1591,8 +1591,8 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie const bool reset = !fsr3_last_evaluation || frame_time_ms > 250.0f; fsr3_last_evaluation = now; #ifdef EGLANTINE_STREAMLINE - if (UpscaleWithDlss(dst_framebuffer, input_color, motion_vectors, depth, render_size, jitter, - motion_scale, reset)) { + if (UpscaleWithDlss(dst_framebuffer, input_color, motion_vectors, depth, depth_reversed, + render_size, jitter, motion_scale, reset)) { return true; } #endif @@ -1627,7 +1627,9 @@ bool TextureCacheRuntime::UpscaleWithFsr3(Framebuffer* dst_framebuffer, ImageVie } fsr3_upscaler->Upscale({ .color = input_color.Handle(Shader::TextureType::Color2D), + .upscale_alpha = VideoCore::Surface::HasAlpha(input_color.format), .depth = depth.DepthView(), + .depth_reversed = depth_reversed, .motion_vectors = motion_vectors.Handle(Shader::TextureType::Color2D), .render_size = {render_size[0], render_size[1]}, .upscale_size = extent, @@ -1690,7 +1692,7 @@ void TextureCacheRuntime::DrawUpscaledOutput(Framebuffer* dst_framebuffer, VkIma #ifdef EGLANTINE_STREAMLINE bool TextureCacheRuntime::UpscaleWithDlss(Framebuffer* dst_framebuffer, ImageView& input_color, ImageView& motion_vectors, ImageView& depth, - std::array render_size, + bool depth_reversed, std::array render_size, std::array jitter, std::array motion_scale, bool reset) { // EDEN_NX2_DLSS_REPLACEMENT=fsr keeps FSR 3.1 for A/B runs. @@ -1738,6 +1740,7 @@ bool TextureCacheRuntime::UpscaleWithDlss(Framebuffer* dst_framebuffer, ImageVie .color = image(input_color, input_color.Handle(Shader::TextureType::Color2D), VK_IMAGE_ASPECT_COLOR_BIT), .depth = image(depth, depth.DepthView(), VK_IMAGE_ASPECT_DEPTH_BIT), + .depth_reversed = depth_reversed, .motion_vectors = image(motion_vectors, motion_vectors.Handle(Shader::TextureType::Color2D), VK_IMAGE_ASPECT_COLOR_BIT), @@ -1766,14 +1769,15 @@ bool TextureCacheRuntime::UpscaleWithDlss(Framebuffer* dst_framebuffer, ImageVie bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& input_color, ImageView& motion_vectors, - ImageView* depth, std::array render_size, + ImageView* depth, bool depth_reversed, + 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)) { + if (depth && UpscaleWithFsr3(dst_framebuffer, input_color, motion_vectors, *depth, + depth_reversed, render_size, jitter, motion_scale)) { return true; } const VkFormat format = diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h index 26081a4c72..ee8f5524e9 100644 --- a/src/video_core/renderer_vulkan/vk_texture_cache.h +++ b/src/video_core/renderer_vulkan/vk_texture_cache.h @@ -108,10 +108,10 @@ class TextureCacheRuntime { Tegra::Engines::Fermi2D::Operation operation); /// 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. + /// into dst, with FSR 3.1 when given the depth (reversed or not), 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, ImageView* depth, + ImageView& motion_vectors, ImageView* depth, bool depth_reversed, std::array render_size, std::array jitter, std::array motion_scale); @@ -213,7 +213,7 @@ class TextureCacheRuntime { /// 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, + ImageView& motion_vectors, ImageView& depth, bool depth_reversed, std::array render_size, std::array jitter, std::array motion_scale); std::unique_ptr fsr3_upscaler; @@ -229,7 +229,7 @@ class TextureCacheRuntime { /// NVIDIA's DLSS (through NVIDIA Streamline) in place of the game's, /// before FSR 3.1, when the setting is on. Returns false to fall back to FSR 3.1. bool UpscaleWithDlss(Framebuffer* dst_framebuffer, ImageView& input_color, - ImageView& motion_vectors, ImageView& depth, + ImageView& motion_vectors, ImageView& depth, bool depth_reversed, std::array render_size, std::array jitter, std::array motion_scale, bool reset); std::unique_ptr dlss_upscaler; diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h index 25a313dd22..53e30e290c 100644 --- a/src/video_core/texture_cache/texture_cache.h +++ b/src/video_core/texture_cache/texture_cache.h @@ -1188,7 +1188,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, + const Tegra::Texture::TICEntry* depth, bool depth_reversed, std::array jitter, std::array motion_scale) { if constexpr (FRAMEBUFFER_BLITS) { return false; @@ -1230,8 +1230,12 @@ bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds PrepareImage(dst_id, true, false); if (depth_id) { PrepareImage(depth_id, false, false); + const PixelFormat depth_format = slot_images[depth_id].info.format; if (slot_images[depth_id].info.num_samples != 1 || - slot_images[depth_id].info.format != PixelFormat::D32_FLOAT) { + (depth_format != PixelFormat::D32_FLOAT && + depth_format != PixelFormat::D24_UNORM_S8_UINT && + depth_format != PixelFormat::S8_UINT_D24_UNORM && + depth_format != PixelFormat::X8_D24_UNORM)) { depth_id = {}; } } @@ -1289,7 +1293,8 @@ bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds 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)); + depth_id, ImageViewInfo(ImageViewType::e2D, slot_images[depth_id].info.format, + range)); } } // After the views: creating them may reallocate the framebuffer slots. @@ -1297,8 +1302,8 @@ bool TextureCache

::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& ds 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); + depth_view ? &slot_image_views[depth_view] : nullptr, depth_reversed, 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 06fcc0de77..78a22a1840 100644 --- a/src/video_core/texture_cache/texture_cache_base.h +++ b/src/video_core/texture_cache/texture_cache_base.h @@ -291,8 +291,8 @@ class TextureCache : public VideoCommon::ChannelSetupCaches jitter, - std::array motion_scale); + const Tegra::Texture::TICEntry* depth, bool depth_reversed, + std::array jitter, std::array motion_scale); /// Health check of a game's DLSS network output (dst) against its input (src); see the /// runtime's ProbeGameDlssOutput. Returns an earlier check's result when one is ready. From 385fe4fa0c7578bb20004af052248adf7d846a93 Mon Sep 17 00:00:00 2001 From: vibecodekun <328927837+vibecodekun@users.noreply.github.com> Date: Wed, 4 Nov 2026 16:41:46 -0500 Subject: [PATCH 3/3] renderer_vulkan: keep one-time draws while pipelines build With asynchronous shaders, a draw whose pipeline isn't built yet is skipped when depth is on, on the assumption that the game draws it again next frame. Fast Fusion renders each track from above once, when a race loads, to make its minimap, and draws its vehicle thumbnails once too, so before their pipelines were built the minimap outline and the thumbnails stayed empty for good. On Switch 2 titles, skip a draw only when the game has been drawing on that render target (address, size and format) for 60 frames with no gap over 15, and otherwise wait for the pipeline. The wait is recorded on the scheduler worker: on a cold shader cache, Fast Fusion hitched as often as before. Assisted-by: Claude Opus 5.5 (debugging, implementation, message draft) --- .../renderer_vulkan/vk_pipeline_cache.cpp | 50 ++++++++++++++++++- .../renderer_vulkan/vk_pipeline_cache.h | 16 +++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index 18aa74b29e..36be1a863c 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -993,6 +993,16 @@ void ReportNewPipelineKey(const GraphicsPipelineCacheKey& key) { diffs.empty() ? std::string{" nothing in the key prefix"} : diffs); } } + +// A render target counts as redrawn once the game has drawn on it for this many frames, with no +// gap longer than TargetMaxGapFrames. Fast Fusion's reflection probes take turns, one every 7. +constexpr u64 TargetRedrawnAfterFrames = 60; +constexpr u64 TargetMaxGapFrames = 15; + +// Games reuse memory: the same address can be a different target from one screen to the next. +constexpr u64 TargetKey(u64 address, u32 width, u32 height, u32 format) { + return address ^ (u64{width} << 40) ^ (u64{height} << 52) ^ (u64{format} << 58); +} } // Anonymous namespace GraphicsPipeline* PipelineCache::CurrentGraphicsPipelineSlowPath() { @@ -1025,6 +1035,9 @@ void PipelineCache::TickFrame() { constexpr u64 EvictCheckFrames = 600; if (frame_count % EvictCheckFrames == 0) { EvictUnusedPipelines(); + std::erase_if(target_uses, [this](const auto& entry) { + return frame_count - entry.second.last_frame > TargetMaxGapFrames; + }); } } @@ -1070,13 +1083,48 @@ void PipelineCache::EvictUnusedPipelines() { EvictAfterFrames, graphics_cache.size()); } -GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const noexcept { +bool PipelineCache::IsTargetRedrawn() { + const auto& regs = maxwell3d->regs; + const auto& color = regs.rt[0]; + const bool has_color = + regs.rt_control.count != 0 && color.format != Tegra::RenderTargetFormat::NONE; + const u64 target = has_color ? TargetKey(color.Address(), color.width, color.height, + static_cast(color.format)) + : TargetKey(regs.zeta.Address(), regs.zeta_size.width, + regs.zeta_size.height, + static_cast(regs.zeta.format)); + if (target == last_target && frame_count == last_target_frame) { + return last_target_redrawn; + } + TargetUse& use = + target_uses.try_emplace(target, TargetUse{frame_count, frame_count}).first->second; + if (frame_count - use.last_frame > TargetMaxGapFrames) { + use.first_frame = frame_count; + } + use.last_frame = frame_count; + last_target = target; + last_target_frame = frame_count; + last_target_redrawn = frame_count - use.first_frame >= TargetRedrawnAfterFrames; + return last_target_redrawn; +} + +GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) { + const bool target_redrawn = !use_asynchronous_shaders || + !maxwell3d->GetNativeMacroEngine().native_turing || + IsTargetRedrawn(); if (pipeline->IsBuilt()) { return pipeline; } if (!use_asynchronous_shaders) { return pipeline; } + // Skipping a draw only costs a frame if the game draws on that target again soon. On a target + // it has not been drawing on, the draw may be one that nothing repeats: Fast Fusion renders + // each track from above once when a race loads (with depth) to make its minimap, and its + // vehicle select thumbnails are drawn once too. + if (!target_redrawn) { + return pipeline; + } // If something is using depth, we can assume that games are not rendering anything which // will be used one time. if (maxwell3d->regs.zeta_enable) { diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h index 3c0719683d..9a85df4e13 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h @@ -128,7 +128,11 @@ class PipelineCache : public VideoCommon::ShaderCache { private: [[nodiscard]] GraphicsPipeline* CurrentGraphicsPipelineSlowPath(); - [[nodiscard]] GraphicsPipeline* BuiltPipeline(GraphicsPipeline* pipeline) const noexcept; + [[nodiscard]] GraphicsPipeline* BuiltPipeline(GraphicsPipeline* pipeline); + + /// Whether the game has been drawing on the current render target for a while, so that a draw + /// skipped now will be redrawn soon. Records the target's use, so it runs for every draw. + [[nodiscard]] bool IsTargetRedrawn(); std::unique_ptr CreateGraphicsPipeline(); @@ -172,6 +176,16 @@ class PipelineCache : public VideoCommon::ShaderCache { /// Destroys graphics pipelines unused for a while, once the GPU is done with them. void EvictUnusedPipelines(); u64 frame_count{}; + + struct TargetUse { + u64 first_frame; ///< First frame of the current run of frames drawing on the target + u64 last_frame; + }; + std::unordered_map target_uses; ///< By TargetKey + u64 last_target{}; + u64 last_target_frame{~u64{0}}; + bool last_target_redrawn{}; + std::deque>> evicted_pipelines; ///< (tick, pipeline) std::unordered_set used_and_evicted; ///< Key hashes of pipelines used this session, evicted since