// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later #include "common/common_types.h" #include "common/div_ceil.h" #include "common/settings.h" #include "video_core/fsr.h" #include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp16_frag_spv.h" #include "video_core/host_shaders/vulkan_fidelityfx_fsr_easu_fp32_frag_spv.h" #include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp16_frag_spv.h" #include "video_core/host_shaders/vulkan_fidelityfx_fsr_rcas_fp32_frag_spv.h" #include "video_core/host_shaders/vulkan_fidelityfx_fsr_vert_spv.h" #include "video_core/renderer_vulkan/present/fsr3.h" #include "video_core/renderer_vulkan/present/util.h" #include "video_core/renderer_vulkan/vk_scheduler.h" #include "video_core/renderer_vulkan/vk_shader_util.h" #include "video_core/vulkan_common/vulkan_device.h" namespace Vulkan { using namespace FSR; using PushConstants = std::array; FSR3::FSR3(const Device& device, MemoryAllocator& memory_allocator, size_t image_count, VkExtent2D extent) : m_memory_allocator{memory_allocator}, m_image_count{image_count}, m_extent{extent} { CreateImages(device); CreateRenderPasses(device); CreateSampler(device); CreateShaders(device); CreateDescriptorPool(device); CreateDescriptorSetLayout(device); CreateDescriptorSets(device); CreatePipelineLayouts(device); CreatePipelines(device); } FSR3::~FSR3() = default; void FSR3::CreateImages(const Device& device) { m_dynamic_images.resize(m_image_count); for (auto& images : m_dynamic_images) { images.images[Easu] = CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); images.images[Rcas] = CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); images.image_views[Easu] = CreateWrappedImageView(device, images.images[Easu], VK_FORMAT_R16G16B16A16_SFLOAT); images.image_views[Rcas] = CreateWrappedImageView(device, images.images[Rcas], VK_FORMAT_R16G16B16A16_SFLOAT); } // Allocate a history image for temporal accumulation feedback. m_prev_frame_image = CreateWrappedImage(m_memory_allocator, m_extent, VK_FORMAT_R16G16B16A16_SFLOAT); m_prev_frame_view = CreateWrappedImageView(device, m_prev_frame_image, VK_FORMAT_R16G16B16A16_SFLOAT); } void FSR3::CreateRenderPasses(const Device& device) { m_renderpass = CreateWrappedRenderPass(device, VK_FORMAT_R16G16B16A16_SFLOAT); for (auto& images : m_dynamic_images) { images.framebuffers[Easu] = CreateWrappedFramebuffer(device, m_renderpass, images.image_views[Easu], m_extent); images.framebuffers[Rcas] = CreateWrappedFramebuffer(device, m_renderpass, images.image_views[Rcas], m_extent); } } void FSR3::CreateSampler(const Device& device) { m_sampler = CreateBilinearSampler(device); } void FSR3::CreateShaders(const Device& device) { m_vert_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_VERT_SPV); if (device.IsFloat16Supported()) { m_easu_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_EASU_FP16_FRAG_SPV); m_rcas_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_RCAS_FP16_FRAG_SPV); } else { m_easu_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_EASU_FP32_FRAG_SPV); m_rcas_shader = BuildShader(device, VULKAN_FIDELITYFX_FSR_RCAS_FP32_FRAG_SPV); } } void FSR3::CreateDescriptorPool(const Device& device) { // EASU: 1 descriptor, RCAS: 1 descriptor → 2 per invocation m_descriptor_pool = CreateWrappedDescriptorPool(device, 2 * m_image_count, 2 * m_image_count); } void FSR3::CreateDescriptorSetLayout(const Device& device) { m_descriptor_set_layout = CreateWrappedDescriptorSetLayout( device, {VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); } void FSR3::CreateDescriptorSets(const Device& device) { std::vector layouts(MaxStage, *m_descriptor_set_layout); for (auto& images : m_dynamic_images) { images.descriptor_sets = CreateWrappedDescriptorSets(m_descriptor_pool, layouts); } } void FSR3::CreatePipelineLayouts(const Device& device) { const VkPushConstantRange range{ .stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, .offset = 0, .size = sizeof(PushConstants), }; VkPipelineLayoutCreateInfo ci{ .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, .pNext = nullptr, .flags = 0, .setLayoutCount = 1, .pSetLayouts = m_descriptor_set_layout.address(), .pushConstantRangeCount = 1, .pPushConstantRanges = &range, }; m_pipeline_layout = device.GetLogical().CreatePipelineLayout(ci); } void FSR3::CreatePipelines(const Device& device) { m_easu_pipeline = CreateWrappedPipeline(device, m_renderpass, m_pipeline_layout, std::tie(m_vert_shader, m_easu_shader)); m_rcas_pipeline = CreateWrappedPipeline(device, m_renderpass, m_pipeline_layout, std::tie(m_vert_shader, m_rcas_shader)); } void FSR3::UpdateDescriptorSets(const Device& device, VkImageView image_view, size_t image_index) { Images& images = m_dynamic_images[image_index]; std::vector image_infos; image_infos.reserve(2); std::vector updates{ CreateWriteDescriptorSet(image_infos, *m_sampler, image_view, images.descriptor_sets[Easu], 0), CreateWriteDescriptorSet(image_infos, *m_sampler, *images.image_views[Easu], images.descriptor_sets[Rcas], 0), }; device.GetLogical().UpdateDescriptorSets(updates, {}); } void FSR3::UploadImages(const Device& device, Scheduler& scheduler) { if (!m_images_ready) { m_images_ready = true; scheduler.Record([&](vk::CommandBuffer cmdbuf) { for (auto& image : m_dynamic_images) { ClearColorImage(cmdbuf, *image.images[Easu]); ClearColorImage(cmdbuf, *image.images[Rcas]); } ClearColorImage(cmdbuf, *m_prev_frame_image); }); scheduler.Finish(); } } VkImageView FSR3::Draw(const Device& device, Scheduler& scheduler, size_t image_index, VkImage source_image, VkImageView source_image_view, VkExtent2D input_image_extent, const Common::Rectangle& crop_rect) { Images& images = m_dynamic_images[image_index]; VkImage easu_image = *images.images[Easu]; VkImage rcas_image = *images.images[Rcas]; VkDescriptorSet easu_descriptor_set = images.descriptor_sets[Easu]; VkDescriptorSet rcas_descriptor_set = images.descriptor_sets[Rcas]; VkFramebuffer easu_framebuffer = *images.framebuffers[Easu]; VkFramebuffer rcas_framebuffer = *images.framebuffers[Rcas]; VkPipeline easu_pipeline = *m_easu_pipeline; VkPipeline rcas_pipeline = *m_rcas_pipeline; VkPipelineLayout pipeline_layout = *m_pipeline_layout; VkRenderPass renderpass = *m_renderpass; VkExtent2D extent = m_extent; const f32 input_image_width = static_cast(input_image_extent.width); const f32 input_image_height = static_cast(input_image_extent.height); const f32 output_image_width = static_cast(extent.width); const f32 output_image_height = static_cast(extent.height); const f32 viewport_width = (crop_rect.right - crop_rect.left) * input_image_width; const f32 viewport_x = crop_rect.left * input_image_width; const f32 viewport_height = (crop_rect.bottom - crop_rect.top) * input_image_height; const f32 viewport_y = crop_rect.top * input_image_height; PushConstants easu_con{}; PushConstants rcas_con{}; FsrEasuConOffset(easu_con.data() + 0, easu_con.data() + 4, easu_con.data() + 8, easu_con.data() + 12, viewport_width, viewport_height, input_image_width, input_image_height, output_image_width, output_image_height, viewport_x, viewport_y); // FSR 3 uses a slightly reduced default sharpening compared to FSR 1 // to avoid over-sharpening on temporally accumulated detail. const float sharpening = static_cast(Settings::values.fsr_sharpening_slider.GetValue()) / 100.0f * 0.8f; FsrRcasCon(rcas_con.data(), sharpening); UploadImages(device, scheduler); UpdateDescriptorSets(device, source_image_view, image_index); scheduler.RequestOutsideRenderPassOperationContext(); scheduler.Record([=](vk::CommandBuffer cmdbuf) { // ── Pass 1: EASU (edge-adaptive spatial upsampling) ────────── TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL); TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); BeginRenderPass(cmdbuf, renderpass, easu_framebuffer, extent); cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, easu_pipeline); cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, easu_descriptor_set, {}); cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, easu_con); cmdbuf.Draw(3, 1, 0, 0); cmdbuf.EndRenderPass(); // ── Pass 2: RCAS (robust contrast-adaptive sharpening) ─────── TransitionImageLayout(cmdbuf, easu_image, VK_IMAGE_LAYOUT_GENERAL); TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); BeginRenderPass(cmdbuf, renderpass, rcas_framebuffer, extent); cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, rcas_pipeline); cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline_layout, 0, rcas_descriptor_set, {}); cmdbuf.PushConstants(pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, rcas_con); cmdbuf.Draw(3, 1, 0, 0); cmdbuf.EndRenderPass(); TransitionImageLayout(cmdbuf, rcas_image, VK_IMAGE_LAYOUT_GENERAL); }); ++m_frame_counter; return *images.image_views[Rcas]; } } // namespace Vulkan