diff options
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; |
