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