diff options
Diffstat (limited to 'src/video_core/renderer_vulkan/present')
| -rw-r--r-- | src/video_core/renderer_vulkan/present/layer.cpp | 50 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/present/layer.h | 12 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/present/post_process.cpp | 899 | ||||
| -rw-r--r-- | src/video_core/renderer_vulkan/present/post_process.h | 140 |
4 files changed, 1101 insertions, 0 deletions
diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp index 14935f92bb..1868142f51 100644 --- a/src/video_core/renderer_vulkan/present/layer.cpp +++ b/src/video_core/renderer_vulkan/present/layer.cpp @@ -18,6 +18,10 @@ #include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" #include "video_core/renderer_vulkan/present/layer.h" +#ifdef HAS_RESHADE +#include "video_core/post_processing/fx_chain.h" +#include "video_core/renderer_vulkan/present/post_process.h" +#endif #include "video_core/renderer_vulkan/present/present_push_constants.h" #include "video_core/renderer_vulkan/present/smaa.h" #include "video_core/renderer_vulkan/present/util.h" @@ -93,6 +97,9 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c RefreshResources(device, framebuffer); SetAntiAliasPass(device); +#ifdef HAS_RESHADE + SetPostProcessPass(device); +#endif // Finish any pending renderpass scheduler.RequestOutsideRenderPassOperationContext(); @@ -115,6 +122,12 @@ void Layer::ConfigureDraw(const Device& device, PresentPushConstants* out_push_c smaa->Draw(device, scheduler, image_index, &source_image, &source_image_view); } +#ifdef HAS_RESHADE + if (post_process.has_value()) { + post_process->Draw(device, scheduler, image_index, &source_image, &source_image_view); + } +#endif + auto crop_rect = Tegra::NormalizeCrop(framebuffer, texture_width, texture_height); const VkExtent2D render_extent{ .width = scaled_width, @@ -214,6 +227,43 @@ void Layer::SetAntiAliasPass(const Device& device) { } } +#ifdef HAS_RESHADE +void Layer::SetPostProcessPass(const Device& device) { + const VkExtent2D render_area{ + .width = Settings::values.resolution_info.ScaleUp(raw_width), + .height = Settings::values.resolution_info.ScaleUp(raw_height), + }; + + const u64 generation = VideoCore::FxChain::Instance().Snapshot().generation; + const bool enabled = Settings::values.post_shader_enabled.GetValue(); + + if (post_process_generation == generation && post_process_enabled == enabled && + post_process_extent.width == render_area.width && + post_process_extent.height == render_area.height) { + return; + } + + for (const u64 tick : resource_ticks) { + scheduler.Wait(tick); + } + + post_process_generation = generation; + post_process_enabled = enabled; + post_process_extent = render_area; + post_process.reset(); + + if (!enabled || VideoCore::FxChain::Instance().Size() == 0) { + return; + } + + post_process.emplace(device, memory_allocator, scheduler, image_count, render_area); + + if (post_process->Empty()) { + post_process.reset(); + } +} +#endif + void Layer::ReleaseRawImages() { for (const u64 tick : resource_ticks) { scheduler.Wait(tick); diff --git a/src/video_core/renderer_vulkan/present/layer.h b/src/video_core/renderer_vulkan/present/layer.h index 4d75e86ba6..f290ba5bb4 100644 --- a/src/video_core/renderer_vulkan/present/layer.h +++ b/src/video_core/renderer_vulkan/present/layer.h @@ -15,6 +15,9 @@ #include "video_core/renderer_vulkan/present/fsr.h" #include "video_core/renderer_vulkan/present/sgsr.h" #include "video_core/renderer_vulkan/present/fxaa.h" +#ifdef HAS_RESHADE +#include "video_core/renderer_vulkan/present/post_process.h" +#endif #include "video_core/renderer_vulkan/present/smaa.h" namespace Layout { @@ -66,6 +69,9 @@ private: void RefreshResources(const Device& device, const Tegra::FramebufferConfig& framebuffer); void SetAntiAliasPass(const Device& device); +#ifdef HAS_RESHADE + void SetPostProcessPass(const Device& device); +#endif void ReleaseRawImages(); u64 CalculateBufferSize(const Tegra::FramebufferConfig& framebuffer) const; @@ -95,6 +101,12 @@ private: Settings::AntiAliasing anti_alias_setting{}; std::variant<std::monostate, FXAA, SMAA> anti_alias{}; std::variant<std::monostate, SGSR, FSR> sr_filter{}; +#ifdef HAS_RESHADE + std::optional<PostProcessChain> post_process{}; + u64 post_process_generation{}; + bool post_process_enabled{}; + VkExtent2D post_process_extent{}; +#endif std::vector<u64> resource_ticks{}; }; diff --git a/src/video_core/renderer_vulkan/present/post_process.cpp b/src/video_core/renderer_vulkan/present/post_process.cpp new file mode 100644 index 0000000000..07d75b7025 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/post_process.cpp @@ -0,0 +1,899 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <cstring> +#include <random> + +#include "common/fs/fs.h" +#include "common/fs/fs_util.h" +#include "common/logging.h" +#include "video_core/post_processing/fx_chain.h" +#include "video_core/post_processing/fx_compile.h" +#include "video_core/post_processing/fx_effect.h" +#include "video_core/renderer_vulkan/present/post_process.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr VkFormat BACKBUFFER_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT; +constexpr size_t NO_TEXTURE = ~size_t{0}; + +VkFormat ToVkFormat(reshadefx::texture_format format) { + switch (format) { + case reshadefx::texture_format::r8: + return VK_FORMAT_R8_UNORM; + case reshadefx::texture_format::r16f: + return VK_FORMAT_R16_SFLOAT; + case reshadefx::texture_format::r32f: + return VK_FORMAT_R32_SFLOAT; + case reshadefx::texture_format::rg8: + return VK_FORMAT_R8G8_UNORM; + case reshadefx::texture_format::rg16: + return VK_FORMAT_R16G16_UNORM; + case reshadefx::texture_format::rg16f: + return VK_FORMAT_R16G16_SFLOAT; + case reshadefx::texture_format::rg32f: + return VK_FORMAT_R32G32_SFLOAT; + case reshadefx::texture_format::rgba8: + return VK_FORMAT_R8G8B8A8_UNORM; + case reshadefx::texture_format::rgba16: + return VK_FORMAT_R16G16B16A16_UNORM; + case reshadefx::texture_format::rgba16f: + return VK_FORMAT_R16G16B16A16_SFLOAT; + case reshadefx::texture_format::rgba32f: + return VK_FORMAT_R32G32B32A32_SFLOAT; + case reshadefx::texture_format::rgb10a2: + return VK_FORMAT_A2B10G10R10_UNORM_PACK32; + default: + return VK_FORMAT_R8G8B8A8_UNORM; + } +} + +VkSamplerAddressMode ToAddressMode(reshadefx::texture_address_mode mode) { + switch (mode) { + case reshadefx::texture_address_mode::wrap: + return VK_SAMPLER_ADDRESS_MODE_REPEAT; + case reshadefx::texture_address_mode::mirror: + return VK_SAMPLER_ADDRESS_MODE_MIRRORED_REPEAT; + case reshadefx::texture_address_mode::border: + return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER; + case reshadefx::texture_address_mode::clamp: + default: + return VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + } +} + +VkBlendFactor ToBlendFactor(reshadefx::blend_factor func) { + switch (func) { + case reshadefx::blend_factor::zero: + return VK_BLEND_FACTOR_ZERO; + case reshadefx::blend_factor::source_color: + return VK_BLEND_FACTOR_SRC_COLOR; + case reshadefx::blend_factor::source_alpha: + return VK_BLEND_FACTOR_SRC_ALPHA; + case reshadefx::blend_factor::one_minus_source_color: + return VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR; + case reshadefx::blend_factor::one_minus_source_alpha: + return VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + case reshadefx::blend_factor::dest_color: + return VK_BLEND_FACTOR_DST_COLOR; + case reshadefx::blend_factor::dest_alpha: + return VK_BLEND_FACTOR_DST_ALPHA; + case reshadefx::blend_factor::one_minus_dest_color: + return VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR; + case reshadefx::blend_factor::one_minus_dest_alpha: + return VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA; + case reshadefx::blend_factor::one: + default: + return VK_BLEND_FACTOR_ONE; + } +} + +VkBlendOp ToBlendOp(reshadefx::blend_op op) { + switch (op) { + case reshadefx::blend_op::subtract: + return VK_BLEND_OP_SUBTRACT; + case reshadefx::blend_op::reverse_subtract: + return VK_BLEND_OP_REVERSE_SUBTRACT; + case reshadefx::blend_op::min: + return VK_BLEND_OP_MIN; + case reshadefx::blend_op::max: + return VK_BLEND_OP_MAX; + case reshadefx::blend_op::add: + default: + return VK_BLEND_OP_ADD; + } +} + +VkPrimitiveTopology ToTopology(reshadefx::primitive_topology topology) { + switch (topology) { + case reshadefx::primitive_topology::point_list: + return VK_PRIMITIVE_TOPOLOGY_POINT_LIST; + case reshadefx::primitive_topology::line_list: + return VK_PRIMITIVE_TOPOLOGY_LINE_LIST; + case reshadefx::primitive_topology::line_strip: + return VK_PRIMITIVE_TOPOLOGY_LINE_STRIP; + case reshadefx::primitive_topology::triangle_strip: + return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_STRIP; + case reshadefx::primitive_topology::triangle_list: + default: + return VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + } +} + +vk::RenderPass CreateFxRenderPass(const Device& device, VkFormat format, bool clear) { + VkAttachmentLoadOp load_op = VK_ATTACHMENT_LOAD_OP_LOAD; + VkImageLayout initial_layout = VK_IMAGE_LAYOUT_GENERAL; + if (clear) { + load_op = VK_ATTACHMENT_LOAD_OP_CLEAR; + initial_layout = VK_IMAGE_LAYOUT_UNDEFINED; + } + + const VkAttachmentDescription attachment{ + .flags = 0, + .format = format, + .samples = VK_SAMPLE_COUNT_1_BIT, + .loadOp = load_op, + .storeOp = VK_ATTACHMENT_STORE_OP_STORE, + .stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE, + .stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE, + .initialLayout = initial_layout, + .finalLayout = VK_IMAGE_LAYOUT_GENERAL, + }; + + const VkAttachmentReference reference{ + .attachment = 0, + .layout = VK_IMAGE_LAYOUT_GENERAL, + }; + + const VkSubpassDescription subpass{ + .flags = 0, + .pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS, + .inputAttachmentCount = 0, + .pInputAttachments = nullptr, + .colorAttachmentCount = 1, + .pColorAttachments = &reference, + .pResolveAttachments = nullptr, + .pDepthStencilAttachment = nullptr, + .preserveAttachmentCount = 0, + .pPreserveAttachments = nullptr, + }; + + return device.GetLogical().CreateRenderPass(VkRenderPassCreateInfo{ + .sType = VK_STRUCTURE_TYPE_RENDER_PASS_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .attachmentCount = 1, + .pAttachments = &attachment, + .subpassCount = 1, + .pSubpasses = &subpass, + .dependencyCount = 0, + .pDependencies = nullptr, + }); +} + +vk::Pipeline CreateFxPipeline(const Device& device, vk::RenderPass& renderpass, + vk::PipelineLayout& layout, VkShaderModule vertex_shader, + VkShaderModule fragment_shader, + const reshadefx::pass& pass) { + const std::array<VkPipelineShaderStageCreateInfo, 2> stages{{ + { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_VERTEX_BIT, + .module = vertex_shader, + .pName = pass.vs_entry_point.c_str(), + .pSpecializationInfo = nullptr, + }, + { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_FRAGMENT_BIT, + .module = fragment_shader, + .pName = pass.ps_entry_point.c_str(), + .pSpecializationInfo = nullptr, + }, + }}; + + constexpr VkPipelineVertexInputStateCreateInfo vertex_input{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .vertexBindingDescriptionCount = 0, + .pVertexBindingDescriptions = nullptr, + .vertexAttributeDescriptionCount = 0, + .pVertexAttributeDescriptions = nullptr, + }; + + const VkPipelineInputAssemblyStateCreateInfo input_assembly{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .topology = ToTopology(pass.topology), + .primitiveRestartEnable = VK_FALSE, + }; + + constexpr VkPipelineViewportStateCreateInfo viewport_state{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .viewportCount = 1, + .pViewports = nullptr, + .scissorCount = 1, + .pScissors = nullptr, + }; + + constexpr VkPipelineRasterizationStateCreateInfo rasterization{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .depthClampEnable = VK_FALSE, + .rasterizerDiscardEnable = VK_FALSE, + .polygonMode = VK_POLYGON_MODE_FILL, + .cullMode = VK_CULL_MODE_NONE, + .frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE, + .depthBiasEnable = VK_FALSE, + .depthBiasConstantFactor = 0.0f, + .depthBiasClamp = 0.0f, + .depthBiasSlopeFactor = 0.0f, + .lineWidth = 1.0f, + }; + + constexpr VkPipelineMultisampleStateCreateInfo multisampling{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .rasterizationSamples = VK_SAMPLE_COUNT_1_BIT, + .sampleShadingEnable = VK_FALSE, + .minSampleShading = 0.0f, + .pSampleMask = nullptr, + .alphaToCoverageEnable = VK_FALSE, + .alphaToOneEnable = VK_FALSE, + }; + + VkBool32 blend_enable = VK_FALSE; + if (pass.blend_enable[0]) { + blend_enable = VK_TRUE; + } + + const VkPipelineColorBlendAttachmentState blending{ + .blendEnable = blend_enable, + .srcColorBlendFactor = ToBlendFactor(pass.source_color_blend_factor[0]), + .dstColorBlendFactor = ToBlendFactor(pass.dest_color_blend_factor[0]), + .colorBlendOp = ToBlendOp(pass.color_blend_op[0]), + .srcAlphaBlendFactor = ToBlendFactor(pass.source_alpha_blend_factor[0]), + .dstAlphaBlendFactor = ToBlendFactor(pass.dest_alpha_blend_factor[0]), + .alphaBlendOp = ToBlendOp(pass.alpha_blend_op[0]), + .colorWriteMask = static_cast<VkColorComponentFlags>(pass.render_target_write_mask[0] & 0xF), + }; + + const VkPipelineColorBlendStateCreateInfo color_blend{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .logicOpEnable = VK_FALSE, + .logicOp = VK_LOGIC_OP_COPY, + .attachmentCount = 1, + .pAttachments = &blending, + .blendConstants = {0.0f, 0.0f, 0.0f, 0.0f}, + }; + + constexpr std::array dynamic_states{ + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR, + }; + + const VkPipelineDynamicStateCreateInfo dynamic_state{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .dynamicStateCount = static_cast<u32>(dynamic_states.size()), + .pDynamicStates = dynamic_states.data(), + }; + + return device.GetLogical().CreateGraphicsPipeline(VkGraphicsPipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stageCount = static_cast<u32>(stages.size()), + .pStages = stages.data(), + .pVertexInputState = &vertex_input, + .pInputAssemblyState = &input_assembly, + .pTessellationState = nullptr, + .pViewportState = &viewport_state, + .pRasterizationState = &rasterization, + .pMultisampleState = &multisampling, + .pDepthStencilState = nullptr, + .pColorBlendState = &color_blend, + .pDynamicState = &dynamic_state, + .layout = *layout, + .renderPass = *renderpass, + .subpass = 0, + .basePipelineHandle = nullptr, + .basePipelineIndex = 0, + }); +} + +} // Anonymous namespace + +PostProcessChain::PostProcessChain(const Device& device, MemoryAllocator& allocator, + Scheduler& scheduler, size_t image_count, VkExtent2D extent) + : m_extent(extent) + , m_image_count(u32(image_count)) +{ + m_start = std::chrono::steady_clock::now(); + m_previous = m_start; + + CreatePingPongImages(device, allocator); + + m_fallback_sampler = CreateWrappedSampler(device); + m_fallback_image = CreateWrappedImage(allocator, VkExtent2D{1, 1}, VK_FORMAT_R8G8B8A8_UNORM); + m_fallback_view = CreateWrappedImageView(device, m_fallback_image, VK_FORMAT_R8G8B8A8_UNORM); + + if (!BuildEffects(device, allocator, scheduler)) { + m_effects.clear(); + } +} + +PostProcessChain::~PostProcessChain() = default; + +bool PostProcessChain::Empty() const { + return m_effects.empty(); +} + +void PostProcessChain::CreatePingPongImages(const Device& device, MemoryAllocator& allocator) { + m_frames.resize(m_image_count); + for (auto& frame : m_frames) { + for (size_t i = 0; i < frame.images.size(); ++i) { + frame.images[i] = CreateWrappedImage(allocator, m_extent, BACKBUFFER_FORMAT); + frame.views[i] = CreateWrappedImageView(device, frame.images[i], BACKBUFFER_FORMAT); + } + } +} + +bool PostProcessChain::BuildEffects(const Device& device, MemoryAllocator& allocator, + Scheduler& scheduler) { + const auto snapshot = VideoCore::FxChain::Instance().Snapshot(); + if (snapshot.entries.empty()) { + return true; + } + + const auto root = VideoCore::GetFxRootDirectory(); + + for (size_t entry_index = 0; entry_index < snapshot.entries.size(); ++entry_index) { + const auto& entry = snapshot.entries[entry_index]; + const auto path = root / entry.file; + + const auto compiled = VideoCore::CompileFxEffect(path, m_extent.width, m_extent.height, 8); + if (!compiled.Succeeded()) { + LOG_ERROR(Render_Vulkan, "Post-processing effect '{}' failed to compile:\n{}", + entry.file, compiled.error); + continue; + } + + const auto& module = compiled.module; + + const auto technique = std::find_if( + module.techniques.begin(), module.techniques.end(), + [&](const reshadefx::technique& t) { return t.name == entry.technique; }); + if (technique == module.techniques.end()) { + LOG_ERROR(Render_Vulkan, "Effect '{}' has no technique '{}'", entry.file, + entry.technique); + continue; + } + + Effect effect; + effect.entry_index = entry_index; + effect.file = entry.file; + effect.uniform_size = module.total_uniform_size; + for (const auto& [name, words] : compiled.entry_points) { + effect.shaders.emplace(name, CreateWrappedShaderModule(device, words)); + } + + for (const auto& texture : module.textures) { + Texture out; + out.name = texture.unique_name; + out.extent = VkExtent2D{texture.width, texture.height}; + out.format = ToVkFormat(texture.format); + + if (texture.semantic == "COLOR") { + out.is_backbuffer = true; + effect.textures.push_back(std::move(out)); + continue; + } + if (texture.semantic == "DEPTH") { + effect.textures.push_back(std::move(out)); + continue; + } + + out.image = CreateWrappedImage(allocator, out.extent, out.format); + out.view = CreateWrappedImageView(device, out.image, out.format); + effect.textures.push_back(std::move(out)); + } + + for (const auto& sampler : module.samplers) { + Sampler out; + out.texture_index = NO_TEXTURE; + for (size_t i = 0; i < effect.textures.size(); ++i) { + if (effect.textures[i].name == sampler.texture_name) { + out.texture_index = i; + break; + } + } + + VkFilter mag_filter = VK_FILTER_LINEAR; + VkFilter min_filter = VK_FILTER_LINEAR; + VkSamplerMipmapMode mip_mode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + const u32 filter = static_cast<u32>(sampler.filter); + if ((filter & 0x10) == 0) { + min_filter = VK_FILTER_NEAREST; + } + if ((filter & 0x04) == 0) { + mag_filter = VK_FILTER_NEAREST; + } + if ((filter & 0x01) == 0) { + mip_mode = VK_SAMPLER_MIPMAP_MODE_NEAREST; + } + + out.sampler = device.GetLogical().CreateSampler(VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = mag_filter, + .minFilter = min_filter, + .mipmapMode = mip_mode, + .addressModeU = ToAddressMode(sampler.address_u), + .addressModeV = ToAddressMode(sampler.address_v), + .addressModeW = ToAddressMode(sampler.address_w), + .mipLodBias = sampler.lod_bias, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 1.0f, + .compareEnable = VK_FALSE, + .compareOp = VK_COMPARE_OP_NEVER, + .minLod = sampler.min_lod, + .maxLod = sampler.max_lod, + .borderColor = VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, + .unnormalizedCoordinates = VK_FALSE, + }); + + effect.samplers.push_back(std::move(out)); + } + + for (const auto& uniform : module.uniforms) { + UniformWrite write; + write.name = uniform.name; + write.offset = uniform.offset; + write.components = std::min<u32>(uniform.type.components(), 4); + write.kind = UniformKind::Floating; + if (uniform.type.is_boolean()) { + write.kind = UniformKind::Boolean; + } else if (uniform.type.is_integral()) { + write.kind = UniformKind::Integer; + } + + for (const auto& annotation : uniform.annotations) { + if (annotation.name != "source") { + continue; + } + const std::string& source = annotation.value.string_data; + if (source == "frametime") { + write.source = UniformSource::FrameTime; + } else if (source == "framecount") { + write.source = UniformSource::FrameCount; + } else if (source == "timer") { + write.source = UniformSource::Timer; + } else if (source == "random") { + write.source = UniformSource::Random; + } else if (source == "pingpong") { + write.source = UniformSource::PingPong; + } + } + + if (uniform.has_initializer_value) { + for (u32 i = 0; i < write.components; ++i) { + if (write.kind == UniformKind::Floating) { + write.fallback[i] = uniform.initializer_value.as_float[i]; + } else { + write.fallback[i] = static_cast<f32>(uniform.initializer_value.as_int[i]); + } + } + } + + write.args = {0.0f, 1.0f, 1.0f, 0.0f}; + for (const auto& annotation : uniform.annotations) { + if (annotation.name == "min" && annotation.type.is_floating_point()) { + write.args[0] = annotation.value.as_float[0]; + } + if (annotation.name == "max" && annotation.type.is_floating_point()) { + write.args[1] = annotation.value.as_float[0]; + } + if (annotation.name == "step" && annotation.type.is_floating_point()) { + write.args[2] = annotation.value.as_float[0]; + } + } + write.state = write.args[0]; + + effect.uniforms.push_back(std::move(write)); + } + + effect.uniform_layout = CreateWrappedDescriptorSetLayout( + device, std::array{VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + + for (const auto& pass : technique->passes) { + Pass out; + out.num_vertices = pass.num_vertices; + out.clear = pass.clear_render_targets != 0; + out.target_texture = NO_TEXTURE; + out.extent = m_extent; + + const std::string& target = pass.render_target_names[0]; + if (target.empty()) { + out.writes_backbuffer = true; + } else { + for (size_t i = 0; i < effect.textures.size(); ++i) { + if (effect.textures[i].name == target) { + out.target_texture = i; + out.extent = effect.textures[i].extent; + break; + } + } + if (out.target_texture == NO_TEXTURE) { + LOG_WARNING(Render_Vulkan, "Effect '{}' pass targets unknown texture '{}'", + entry.file, target); + out.writes_backbuffer = true; + } + } + + if (pass.viewport_width != 0 && pass.viewport_height != 0) { + out.extent = VkExtent2D{pass.viewport_width, pass.viewport_height}; + } + + VkFormat target_format = BACKBUFFER_FORMAT; + if (!out.writes_backbuffer) { + target_format = effect.textures[out.target_texture].format; + } + + u32 binding_count = 0; + for (const auto& binding : pass.sampler_bindings) { + SamplerBinding entry_binding; + entry_binding.binding = binding.entry_point_binding; + entry_binding.sampler_index = binding.index; + out.sampler_bindings.push_back(entry_binding); + binding_count = std::max<u32>(binding_count, binding.entry_point_binding + 1); + } + + const std::vector<VkDescriptorType> sampler_types( + std::max<size_t>(binding_count, 1), VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER); + out.sampler_layout = CreateWrappedDescriptorSetLayout( + device, sampler_types, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); + + const std::array set_layouts{*effect.uniform_layout, *out.sampler_layout}; + out.pipeline_layout = + device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ + .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .setLayoutCount = static_cast<u32>(set_layouts.size()), + .pSetLayouts = set_layouts.data(), + .pushConstantRangeCount = 0, + .pPushConstantRanges = nullptr, + }); + + const auto vertex_shader = effect.shaders.find(pass.vs_entry_point); + const auto fragment_shader = effect.shaders.find(pass.ps_entry_point); + if (vertex_shader == effect.shaders.end() || + fragment_shader == effect.shaders.end()) { + LOG_WARNING(Render_Vulkan, "Effect '{}' pass references a missing entry point", + entry.file); + continue; + } + + out.renderpass = CreateFxRenderPass(device, target_format, out.clear); + out.pipeline = CreateFxPipeline(device, out.renderpass, out.pipeline_layout, + *vertex_shader->second, *fragment_shader->second, pass); + + if (out.writes_backbuffer) { + out.backbuffer_slot = static_cast<u32>(effect.backbuffer_pass_count % 2); + ++effect.backbuffer_pass_count; + for (u32 image = 0; image < m_image_count; ++image) { + for (size_t slot = 0; slot < 2; ++slot) { + out.framebuffers.push_back(CreateWrappedFramebuffer( + device, out.renderpass, m_frames[image].views[slot], out.extent)); + } + } + } else { + out.framebuffers.push_back( + CreateWrappedFramebuffer(device, out.renderpass, + effect.textures[out.target_texture].view, out.extent)); + } + + effect.passes.push_back(std::move(out)); + } + + if (effect.passes.empty()) { + LOG_WARNING(Render_Vulkan, "Effect '{}' technique '{}' has no passes", entry.file, + entry.technique); + continue; + } + + const u32 buffer_size = std::max<u32>(effect.uniform_size, 4); + for (u32 i = 0; i < m_image_count; ++i) { + effect.uniform_buffers.push_back( + CreateWrappedBuffer(allocator, buffer_size, MemoryUsage::Upload)); + } + + size_t sampler_descriptor_count = 0; + size_t sampler_set_count = 0; + for (const auto& pass : effect.passes) { + sampler_descriptor_count += + m_image_count * std::max<size_t>(pass.sampler_bindings.size(), 1); + sampler_set_count += m_image_count; + } + + effect.descriptor_pool = CreateWrappedDescriptorPool( + device, m_image_count + sampler_descriptor_count, m_image_count + sampler_set_count, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); + + const std::vector<VkDescriptorSetLayout> uniform_layouts(m_image_count, + *effect.uniform_layout); + effect.uniform_sets = CreateWrappedDescriptorSets(effect.descriptor_pool, uniform_layouts); + + for (auto& pass : effect.passes) { + const std::vector<VkDescriptorSetLayout> layouts(m_image_count, *pass.sampler_layout); + pass.sampler_sets = CreateWrappedDescriptorSets(effect.descriptor_pool, layouts); + } + + m_effects.push_back(std::move(effect)); + } + + return true; +} + +void PostProcessChain::PrepareImages(const Device& device, Scheduler& scheduler) { + if (m_images_ready) { + return; + } + + scheduler.Record([this](vk::CommandBuffer cmdbuf) { + ClearColorImage(cmdbuf, *m_fallback_image); + for (auto& frame : m_frames) { + for (auto& image : frame.images) { + ClearColorImage(cmdbuf, *image); + } + } + for (auto& effect : m_effects) { + for (auto& texture : effect.textures) { + if (texture.image) { + ClearColorImage(cmdbuf, *texture.image); + } + } + } + }); + scheduler.Finish(); + + m_images_ready = true; +} + +void PostProcessChain::UpdateUniforms(Effect& effect, size_t image_index, f32 delta_seconds) { + if (effect.uniform_size == 0) { + return; + } + + static thread_local std::mt19937 rng{std::random_device{}()}; + + std::vector<u8> staging(effect.uniform_size, 0); + const f32 elapsed = + std::chrono::duration<f32>(std::chrono::steady_clock::now() - m_start).count(); + const auto overrides = VideoCore::FxChain::Instance().EntryValues(effect.entry_index); + + for (auto& uniform : effect.uniforms) { + std::array<f32, 4> value = uniform.fallback; + + const auto override = overrides.find(uniform.name); + if (override != overrides.end()) { + value = override->second; + } + + switch (uniform.source) { + case UniformSource::FrameTime: + value[0] = delta_seconds * 1000.0f; + break; + case UniformSource::FrameCount: + value[0] = static_cast<f32>(m_frame_count); + break; + case UniformSource::Timer: + value[0] = elapsed * 1000.0f; + break; + case UniformSource::Random: { + const int low = static_cast<int>(uniform.args[0]); + int high = static_cast<int>(uniform.args[1]); + if (high <= low) { + high = low + 1; + } + std::uniform_int_distribution<int> dist(low, high); + value[0] = static_cast<f32>(dist(rng)); + break; + } + case UniformSource::PingPong: { + const f32 min_value = uniform.args[0]; + f32 max_value = uniform.args[1]; + if (max_value <= min_value) { + max_value = min_value + 1.0f; + } + f32 step = uniform.args[2]; + if (step == 0.0f) { + step = 1.0f; + } + uniform.state += uniform.direction * step * delta_seconds; + if (uniform.state >= max_value) { + uniform.state = max_value; + uniform.direction = -1.0f; + } + if (uniform.state <= min_value) { + uniform.state = min_value; + uniform.direction = 1.0f; + } + value[0] = uniform.state; + value[1] = uniform.direction; + break; + } + case UniformSource::Value: + default: + break; + } + + for (u32 i = 0; i < uniform.components; ++i) { + const size_t offset = uniform.offset + i * sizeof(u32); + if (offset + sizeof(u32) > staging.size()) { + break; + } + if (uniform.kind == UniformKind::Floating) { + const f32 element = value[i]; + std::memcpy(staging.data() + offset, &element, sizeof(f32)); + } else { + const s32 element = static_cast<s32>(value[i]); + std::memcpy(staging.data() + offset, &element, sizeof(s32)); + } + } + } + + const std::span<u8> mapped = effect.uniform_buffers[image_index].Mapped(); + if (mapped.size() >= staging.size()) { + std::memcpy(mapped.data(), staging.data(), staging.size()); + effect.uniform_buffers[image_index].Flush(); + } +} + +void PostProcessChain::UpdateDescriptors(const Device& device, Effect& effect, Pass& pass, + size_t image_index, VkImageView backbuffer_view) { + std::vector<VkDescriptorImageInfo> image_infos; + std::vector<VkWriteDescriptorSet> writes; + image_infos.reserve(pass.sampler_bindings.size() + 1); + + const VkDescriptorSet sampler_set = pass.sampler_sets[image_index]; + + for (const auto& binding : pass.sampler_bindings) { + VkImageView view = *m_fallback_view; + VkSampler handle = *m_fallback_sampler; + + if (binding.sampler_index < effect.samplers.size()) { + const Sampler& sampler = effect.samplers[binding.sampler_index]; + if (sampler.sampler) { + handle = *sampler.sampler; + } + if (sampler.texture_index != NO_TEXTURE) { + const Texture& texture = effect.textures[sampler.texture_index]; + if (texture.is_backbuffer) { + view = backbuffer_view; + } else if (texture.view) { + view = *texture.view; + } + } + } + + writes.push_back( + CreateWriteDescriptorSet(image_infos, handle, view, sampler_set, binding.binding)); + } + + const VkDescriptorBufferInfo buffer_info{ + .buffer = *effect.uniform_buffers[image_index], + .offset = 0, + .range = VK_WHOLE_SIZE, + }; + + writes.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = effect.uniform_sets[image_index], + .dstBinding = 0, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .pImageInfo = nullptr, + .pBufferInfo = &buffer_info, + .pTexelBufferView = nullptr, + }); + + device.GetLogical().UpdateDescriptorSets(writes, {}); +} + +void PostProcessChain::Draw(const Device& device, Scheduler& scheduler, size_t image_index, + VkImage* inout_image, VkImageView* inout_image_view) { + if (m_effects.empty()) { + return; + } + + PrepareImages(device, scheduler); + + const auto now = std::chrono::steady_clock::now(); + const f32 delta_seconds = std::chrono::duration<f32>(now - m_previous).count(); + m_previous = now; + ++m_frame_count; + + FrameImages& frame = m_frames[image_index]; + VkImage current_image = *inout_image; + VkImageView current_view = *inout_image_view; + u32 slot = 0; + + for (auto& effect : m_effects) { + UpdateUniforms(effect, image_index, delta_seconds); + + for (size_t pass_index = 0; pass_index < effect.passes.size(); ++pass_index) { + Pass& pass = effect.passes[pass_index]; + + UpdateDescriptors(device, effect, pass, image_index, current_view); + + VkFramebuffer framebuffer{}; + VkImage target_image{}; + if (pass.writes_backbuffer) { + const u32 target_slot = (slot + 1) % 2; + framebuffer = *pass.framebuffers[image_index * 2 + target_slot]; + target_image = *frame.images[target_slot]; + } else { + framebuffer = *pass.framebuffers[0]; + target_image = *effect.textures[pass.target_texture].image; + } + + const VkImage source_image = current_image; + const VkRenderPass renderpass = *pass.renderpass; + const VkPipeline pipeline = *pass.pipeline; + const VkPipelineLayout layout = *pass.pipeline_layout; + const VkDescriptorSet uniform_set = effect.uniform_sets[image_index]; + const VkDescriptorSet sampler_set = pass.sampler_sets[image_index]; + const VkExtent2D extent = pass.extent; + const u32 vertices = pass.num_vertices; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([=](vk::CommandBuffer cmdbuf) { + TransitionImageLayout(cmdbuf, source_image, VK_IMAGE_LAYOUT_GENERAL); + TransitionImageLayout(cmdbuf, target_image, VK_IMAGE_LAYOUT_GENERAL); + BeginRenderPass(cmdbuf, renderpass, framebuffer, extent); + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline); + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, + std::array{uniform_set, sampler_set}, {}); + cmdbuf.Draw(vertices, 1, 0, 0); + cmdbuf.EndRenderPass(); + TransitionImageLayout(cmdbuf, target_image, VK_IMAGE_LAYOUT_GENERAL); + }); + + if (pass.writes_backbuffer) { + slot = (slot + 1) % 2; + current_image = *frame.images[slot]; + current_view = *frame.views[slot]; + } + } + } + + *inout_image = current_image; + *inout_image_view = current_view; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/post_process.h b/src/video_core/renderer_vulkan/present/post_process.h new file mode 100644 index 0000000000..9d42bd4751 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/post_process.h @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <chrono> +#include <map> +#include <string> +#include <vector> + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class Scheduler; + +class PostProcessChain { +public: + explicit PostProcessChain(const Device& device, MemoryAllocator& allocator, Scheduler& scheduler, + size_t image_count, VkExtent2D extent); + ~PostProcessChain(); + + void Draw(const Device& device, Scheduler& scheduler, size_t image_index, VkImage* inout_image, + VkImageView* inout_image_view); + + bool Empty() const; + +private: + enum class UniformKind : u32 { + Boolean, + Integer, + Floating, + }; + + enum class UniformSource : u32 { + Value, + FrameTime, + FrameCount, + Timer, + Random, + PingPong, + }; + + struct UniformWrite { + std::string name; + u32 offset{}; + u32 components{}; + UniformKind kind{UniformKind::Floating}; + UniformSource source{UniformSource::Value}; + std::array<f32, 4> fallback{}; + std::array<f32, 4> args{}; + f32 state{}; + f32 direction{1.0f}; + }; + + struct Texture { + std::string name; + vk::Image image{}; + vk::ImageView view{}; + VkExtent2D extent{}; + VkFormat format{}; + bool is_backbuffer{}; + }; + + struct Sampler { + vk::Sampler sampler{}; + size_t texture_index{}; + }; + + struct SamplerBinding { + u32 binding{}; + size_t sampler_index{}; + }; + + struct Pass { + vk::RenderPass renderpass{}; + vk::Pipeline pipeline{}; + vk::DescriptorSetLayout sampler_layout{}; + vk::PipelineLayout pipeline_layout{}; + vk::DescriptorSets sampler_sets{}; + std::vector<SamplerBinding> sampler_bindings{}; + std::vector<vk::Framebuffer> framebuffers{}; + size_t target_texture{}; + VkExtent2D extent{}; + u32 num_vertices{3}; + bool clear{}; + bool writes_backbuffer{}; + u32 backbuffer_slot{}; + }; + + struct Effect { + size_t entry_index{}; + std::string file{}; + std::map<std::string, vk::ShaderModule> shaders{}; + std::vector<Texture> textures{}; + std::vector<Sampler> samplers{}; + std::vector<Pass> passes{}; + std::vector<UniformWrite> uniforms{}; + u32 uniform_size{}; + std::vector<vk::Buffer> uniform_buffers{}; + vk::DescriptorSetLayout uniform_layout{}; + vk::DescriptorPool descriptor_pool{}; + vk::DescriptorSets uniform_sets{}; + size_t backbuffer_pass_count{}; + u32 backbuffer_slots{1}; + }; + + struct FrameImages { + std::array<vk::Image, 2> images{}; + std::array<vk::ImageView, 2> views{}; + }; + + bool BuildEffects(const Device& device, MemoryAllocator& allocator, Scheduler& scheduler); + void CreatePingPongImages(const Device& device, MemoryAllocator& allocator); + void PrepareImages(const Device& device, Scheduler& scheduler); + void UpdateUniforms(Effect& effect, size_t image_index, f32 delta_seconds); + void UpdateDescriptors(const Device& device, Effect& effect, Pass& pass, size_t image_index, + VkImageView backbuffer_view); + + const VkExtent2D m_extent; + const u32 m_image_count; + + std::vector<Effect> m_effects{}; + std::vector<FrameImages> m_frames{}; + + vk::Sampler m_fallback_sampler{}; + vk::Image m_fallback_image{}; + vk::ImageView m_fallback_view{}; + + std::chrono::steady_clock::time_point m_start{}; + std::chrono::steady_clock::time_point m_previous{}; + u64 m_frame_count{}; + bool m_images_ready{}; +}; + +} // namespace Vulkan |
