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/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, 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_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 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.