/* Copyright 2026 flyinghead This file is part of Flycast. Flycast is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. Flycast is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Flycast. If not, see . */ #include "motion_pass.h" #include "vulkan_context.h" #include "hw/pvr/ta_ctx.h" #include "rend/transform_matrix.h" #include void MotionVectorPass::Init(vk::Extent2D size, vk::Format depthFormat, vk::ImageLayout depthLayout, const std::vector& depthViews, vk::PipelineLayout pipelineLayout, vk::ShaderModule vertexShader, vk::ShaderModule fragmentShader, CommandPool *commandPool) { VulkanContext *context = VulkanContext::Instance(); vk::Device device = context->GetDevice(); if (size != this->size || depthLayout != this->depthLayout || attachments.size() != depthViews.size() || sampled) Term(commandPool); this->size = size; this->depthLayout = depthLayout; if (!renderPass) { vk::AttachmentDescription attachmentDescriptions[] = { vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), vk::Format::eR16G16Sfloat, vk::SampleCountFlagBits::e1, vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal), // (the depth as the frame left it, tested, not written, and left as it was) vk::AttachmentDescription(vk::AttachmentDescriptionFlags(), depthFormat, vk::SampleCountFlagBits::e1, vk::AttachmentLoadOp::eLoad, vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare, depthLayout, depthLayout), }; vk::AttachmentReference colorReference(0, vk::ImageLayout::eColorAttachmentOptimal); vk::AttachmentReference depthReference(1, depthLayout); vk::SubpassDescription subpass(vk::SubpassDescriptionFlags(), vk::PipelineBindPoint::eGraphics, 0, nullptr, 1, &colorReference, nullptr, &depthReference); vk::SubpassDependency dependencies[] = { // the frame's depth, drawn, then tested here vk::SubpassDependency(VK_SUBPASS_EXTERNAL, 0, vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests, vk::PipelineStageFlagBits::eEarlyFragmentTests | vk::PipelineStageFlagBits::eLateFragmentTests, vk::AccessFlagBits::eDepthStencilAttachmentWrite, vk::AccessFlagBits::eDepthStencilAttachmentRead), // the vectors, and the depth, read by the upscaler vk::SubpassDependency(0, VK_SUBPASS_EXTERNAL, vk::PipelineStageFlagBits::eColorAttachmentOutput | vk::PipelineStageFlagBits::eLateFragmentTests, vk::PipelineStageFlagBits::eFragmentShader | vk::PipelineStageFlagBits::eComputeShader, vk::AccessFlagBits::eColorAttachmentWrite | vk::AccessFlagBits::eDepthStencilAttachmentWrite, vk::AccessFlagBits::eShaderRead), }; renderPass = device.createRenderPassUnique(vk::RenderPassCreateInfo(vk::RenderPassCreateFlags(), ARRAY_SIZE(attachmentDescriptions), attachmentDescriptions, 1, &subpass, ARRAY_SIZE(dependencies), dependencies)); } while (attachments.size() < depthViews.size()) { // the vectors, two halves a pixel, sampled and copied from by the upscaler as it likes attachments.push_back(std::unique_ptr( new FramebufferAttachment(context->GetPhysicalDevice(), device))); attachments.back()->Init(size.width, size.height, vk::Format::eR16G16Sfloat, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc, false); const size_t i = framebuffers.size(); vk::ImageView views[] = { attachments.back()->GetImageView(), depthViews[i] }; framebuffers.push_back(device.createFramebufferUnique(vk::FramebufferCreateInfo(vk::FramebufferCreateFlags(), *renderPass, ARRAY_SIZE(views), views, size.width, size.height, 1))); } if (retro_atomic_load_relaxed_int(&pipelines[0].state) == PSO_EMPTY) MakePipelines(pipelineLayout, vertexShader, fragmentShader, true); } void MotionVectorPass::InitSampled(vk::Extent2D size, const std::vector& depthViews, const std::vector& uniformBuffers, vk::DeviceSize uniformBytes, vk::ShaderModule vertexShader, vk::ShaderModule fragmentShader, CommandPool *commandPool) { VulkanContext *context = VulkanContext::Instance(); vk::Device device = context->GetDevice(); if (size != this->size || !sampled || attachments.size() != depthViews.size()) Term(commandPool); this->size = size; this->depthLayout = vk::ImageLayout::eShaderReadOnlyOptimal; sampled = true; if (!renderPass) { // the vectors alone: the depth is sampled vk::AttachmentDescription attachmentDescription(vk::AttachmentDescriptionFlags(), vk::Format::eR16G16Sfloat, vk::SampleCountFlagBits::e1, vk::AttachmentLoadOp::eClear, vk::AttachmentStoreOp::eStore, vk::AttachmentLoadOp::eDontCare, vk::AttachmentStoreOp::eDontCare, vk::ImageLayout::eUndefined, vk::ImageLayout::eShaderReadOnlyOptimal); vk::AttachmentReference colorReference(0, vk::ImageLayout::eColorAttachmentOptimal); vk::SubpassDescription subpass(vk::SubpassDescriptionFlags(), vk::PipelineBindPoint::eGraphics, 0, nullptr, 1, &colorReference, nullptr, nullptr); vk::SubpassDependency dependencies[] = { // the frame's depth, stored by the kernel, then sampled here vk::SubpassDependency(VK_SUBPASS_EXTERNAL, 0, vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eFragmentShader, vk::AccessFlagBits::eShaderWrite, vk::AccessFlagBits::eShaderRead), // the vectors, read by the upscaler vk::SubpassDependency(0, VK_SUBPASS_EXTERNAL, vk::PipelineStageFlagBits::eColorAttachmentOutput, vk::PipelineStageFlagBits::eFragmentShader | vk::PipelineStageFlagBits::eComputeShader, vk::AccessFlagBits::eColorAttachmentWrite, vk::AccessFlagBits::eShaderRead), }; renderPass = device.createRenderPassUnique(vk::RenderPassCreateInfo(vk::RenderPassCreateFlags(), 1, &attachmentDescription, 1, &subpass, ARRAY_SIZE(dependencies), dependencies)); } if (!setLayout) { // the vertex uniforms as position.glsl has them, and the depth vk::DescriptorSetLayoutBinding bindings[] = { { 0, vk::DescriptorType::eUniformBuffer, 1, vk::ShaderStageFlagBits::eVertex }, { 1, vk::DescriptorType::eCombinedImageSampler, 1, vk::ShaderStageFlagBits::eFragment }, }; setLayout = device.createDescriptorSetLayoutUnique(vk::DescriptorSetLayoutCreateInfo(vk::DescriptorSetLayoutCreateFlags(), ARRAY_SIZE(bindings), bindings)); ownLayout = device.createPipelineLayoutUnique(vk::PipelineLayoutCreateInfo(vk::PipelineLayoutCreateFlags(), 1, &setLayout.get())); depthSampler = device.createSamplerUnique(vk::SamplerCreateInfo(vk::SamplerCreateFlags(), vk::Filter::eNearest, vk::Filter::eNearest, vk::SamplerMipmapMode::eNearest, vk::SamplerAddressMode::eClampToEdge, vk::SamplerAddressMode::eClampToEdge, vk::SamplerAddressMode::eClampToEdge)); } if (sets.size() < depthViews.size()) { const u32 count = (u32)depthViews.size(); const vk::DescriptorPoolSize sizes[] = { { vk::DescriptorType::eUniformBuffer, count }, { vk::DescriptorType::eCombinedImageSampler, count }, }; descriptorPool = device.createDescriptorPoolUnique(vk::DescriptorPoolCreateInfo(vk::DescriptorPoolCreateFlags(), count, ARRAY_SIZE(sizes), sizes)); std::vector layouts(count, *setLayout); sets = device.allocateDescriptorSets(vk::DescriptorSetAllocateInfo(*descriptorPool, count, layouts.data())); std::vector bufferInfos; std::vector imageInfos; std::vector writes; bufferInfos.reserve(count); imageInfos.reserve(count); for (u32 i = 0; i < count; i++) { bufferInfos.push_back(vk::DescriptorBufferInfo(uniformBuffers[i], 0, uniformBytes)); writes.push_back(vk::WriteDescriptorSet(sets[i], 0, 0, 1, vk::DescriptorType::eUniformBuffer, nullptr, &bufferInfos.back())); imageInfos.push_back(vk::DescriptorImageInfo(*depthSampler, depthViews[i], vk::ImageLayout::eShaderReadOnlyOptimal)); writes.push_back(vk::WriteDescriptorSet(sets[i], 1, 0, 1, vk::DescriptorType::eCombinedImageSampler, &imageInfos.back())); } device.updateDescriptorSets(writes, nullptr); } while (attachments.size() < depthViews.size()) { attachments.push_back(std::unique_ptr( new FramebufferAttachment(context->GetPhysicalDevice(), device))); attachments.back()->Init(size.width, size.height, vk::Format::eR16G16Sfloat, vk::ImageUsageFlagBits::eColorAttachment | vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc, false); vk::ImageView views[] = { attachments.back()->GetImageView() }; framebuffers.push_back(device.createFramebufferUnique(vk::FramebufferCreateInfo(vk::FramebufferCreateFlags(), *renderPass, 1, views, size.width, size.height, 1))); } if (retro_atomic_load_relaxed_int(&pipelines[0].state) == PSO_EMPTY) MakePipelines(*ownLayout, vertexShader, fragmentShader, false); } void MotionVectorPass::MakePipelines(vk::PipelineLayout pipelineLayout, vk::ShaderModule vertexShader, vk::ShaderModule fragmentShader, bool depthTest) { this->pipelineLayout = pipelineLayout; this->vertexShader = vertexShader; this->fragmentShader = fragmentShader; this->depthTest = depthTest; PsoSlot *all[] = { &pipelines[0], &pipelines[1], &pipelines[2] }; VkPsoWorker::Instance().BuildSoon(all, 3, VulkanContext::Instance()->GetPipelineCache()); } VkPipeline MotionVectorPass::Make(int cull, vk::PipelineCache cache, bool orThrow) const { vk::Device device = VulkanContext::Instance()->GetDevice(); // the vertex as the colour pass has it, and where it was: its own stream static const vk::VertexInputBindingDescription bindings[] = { { 0, sizeof(Vertex) }, { 1, sizeof(float) * 2 }, }; static const vk::VertexInputAttributeDescription vertexAttributes[] = { vk::VertexInputAttributeDescription(0, 0, vk::Format::eR32G32B32Sfloat, offsetof(Vertex, x)), vk::VertexInputAttributeDescription(1, 1, vk::Format::eR32G32Sfloat, 0), }; vk::PipelineVertexInputStateCreateInfo vertexInput(vk::PipelineVertexInputStateCreateFlags(), ARRAY_SIZE(bindings), bindings, ARRAY_SIZE(vertexAttributes), vertexAttributes); vk::PipelineInputAssemblyStateCreateInfo inputAssembly(vk::PipelineInputAssemblyStateCreateFlags(), vk::PrimitiveTopology::eTriangleStrip); vk::PipelineViewportStateCreateInfo viewportState(vk::PipelineViewportStateCreateFlags(), 1, nullptr, 1, nullptr); vk::PipelineMultisampleStateCreateInfo multisample; // the surface the colour pass left: its depth, equal or nearer (reversed); the sampled mode's shader does the test vk::PipelineDepthStencilStateCreateInfo depthStencil(vk::PipelineDepthStencilStateCreateFlags(), depthTest, false, vk::CompareOp::eGreaterOrEqual, false, false); vk::PipelineColorBlendAttachmentState blendAttachment(false, vk::BlendFactor::eOne, vk::BlendFactor::eZero, vk::BlendOp::eAdd, vk::BlendFactor::eOne, vk::BlendFactor::eZero, vk::BlendOp::eAdd, vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG); vk::PipelineColorBlendStateCreateInfo blend(vk::PipelineColorBlendStateCreateFlags(), false, vk::LogicOp::eNoOp, 1, &blendAttachment); vk::DynamicState dynamicStates[] = { vk::DynamicState::eViewport, vk::DynamicState::eScissor }; vk::PipelineDynamicStateCreateInfo dynamic(vk::PipelineDynamicStateCreateFlags(), ARRAY_SIZE(dynamicStates), dynamicStates); // (the vectors in pixels: the size, as the shader's constants) const float sizeConstants[] = { (float)size.width, (float)size.height }; const vk::SpecializationMapEntry entries[] = { vk::SpecializationMapEntry(10, 0, sizeof(float)), vk::SpecializationMapEntry(11, sizeof(float), sizeof(float)), }; vk::SpecializationInfo specialization(ARRAY_SIZE(entries), entries, sizeof(sizeConstants), sizeConstants); vk::PipelineShaderStageCreateInfo stages[] = { { vk::PipelineShaderStageCreateFlags(), vk::ShaderStageFlagBits::eVertex, vertexShader, "main", &specialization }, { vk::PipelineShaderStageCreateFlags(), vk::ShaderStageFlagBits::eFragment, fragmentShader, "main" }, }; vk::PipelineRasterizationStateCreateInfo rasterization(vk::PipelineRasterizationStateCreateFlags(), false, false, vk::PolygonMode::eFill, cull == 2 ? vk::CullModeFlagBits::eBack : cull == 1 ? vk::CullModeFlagBits::eFront : vk::CullModeFlagBits::eNone, vk::FrontFace::eCounterClockwise, false, 0.f, 0.f, 0.f, 1.f); vk::GraphicsPipelineCreateInfo createInfo(vk::PipelineCreateFlags(), ARRAY_SIZE(stages), stages, &vertexInput, &inputAssembly, nullptr, &viewportState, &rasterization, &multisample, depthTest ? &depthStencil : nullptr, &blend, &dynamic, pipelineLayout, *renderPass, 0); if (!orThrow) return PsoCreate(device, cache, createInfo); return (VkPipeline)device.createGraphicsPipelineUnique(cache, createInfo).release(); } // (one the pipeline thread could not make: made here, or thrown for as Init() did) vk::Pipeline MotionVectorPass::Pipeline(int cull) { const VkPipeline made = PsoGet(pipelines[cull]); if (made != VK_NULL_HANDLE) return vk::Pipeline(made); return vk::Pipeline(PsoOwnMade(pipelines[cull], Make(cull, VulkanContext::Instance()->GetPipelineCache(), true))); } void MotionVectorPass::Term(CommandPool *commandPool) { if (commandPool != nullptr) { for (auto& attachment : attachments) if (attachment) commandPool->DeferDelete(std::move(attachment)); for (auto& framebuffer : framebuffers) if (framebuffer) commandPool->DeferDelete(std::unique_ptr(new vk::UniqueFramebuffer(std::move(framebuffer)))); } attachments.clear(); framebuffers.clear(); // (the pipeline thread done with what it has of them before what they are made of goes) PsoOwnDrop(this, pipelines, 3, VulkanContext::Instance() != nullptr ? VulkanContext::Instance()->GetDevice() : vk::Device()); renderPass.reset(); prevXyBuffers.clear(); sets.clear(); descriptorPool.reset(); sampled = false; recorded = false; } bool MotionVectorPass::Begin(int image) { recorded = false; if (pvrrc.gpu || framebuffers.empty()) return false; // the frame's strips matched with the frame before's: where each vertex was const u32 bytes = (u32)std::max((size_t)pvrrc.verts.used() * 2 * sizeof(float), 1024); while (prevXyBuffers.size() <= (size_t)image) prevXyBuffers.emplace_back(); std::unique_ptr& buffer = prevXyBuffers[image]; if (!buffer || buffer->bufferSize < bytes) buffer = std::unique_ptr(new BufferData(bytes, vk::BufferUsageFlagBits::eVertexBuffer)); /* (written where the pass reads them: the buffer, mapped) */ upscaler_match(pvrrc, (float *)buffer->MapMemory()); recorded = true; return true; } void MotionVectorPass::Record(vk::CommandBuffer commandBuffer, int image, vk::Buffer vertexBuffer, vk::DeviceSize indexOffset, const vk::Rect2D& scissor, vk::DeviceSize vertexOffset) { const vk::ClearValue clearValues[] = { vk::ClearColorValue(std::array { 0.f, 0.f, 0.f, 0.f }), vk::ClearDepthStencilValue { 0.f, 0 } }; commandBuffer.beginRenderPass(vk::RenderPassBeginInfo(*renderPass, *framebuffers[image], vk::Rect2D( { 0, 0 }, size), sampled ? 1u : (u32)ARRAY_SIZE(clearValues), clearValues), vk::SubpassContents::eInline); commandBuffer.setViewport(0, vk::Viewport(0.0f, 0.0f, (float)size.width, (float)size.height, 0.0f, 1.0f)); commandBuffer.setScissor(0, scissor); if (sampled) commandBuffer.bindDescriptorSets(vk::PipelineBindPoint::eGraphics, *ownLayout, 0, 1, &sets[image], 0, nullptr); const vk::Buffer buffers[] = { vertexBuffer, *prevXyBuffers[image]->buffer }; const vk::DeviceSize offsets[] = { vertexOffset, 0 }; commandBuffer.bindVertexBuffers(0, ARRAY_SIZE(buffers), buffers, offsets); commandBuffer.bindIndexBuffer(vertexBuffer, indexOffset, vk::IndexType::eUint32); int bound = -1; const List *lists[] = { &pvrrc.global_param_op, &pvrrc.global_param_pt }; for (const List *list : lists) for (int i = 0; i < list->used(); i++) { const PolyParam& pp = list->head()[i]; if (pp.count < 3) continue; const int cull = pp.isp.CullMode == 3 ? 2 : pp.isp.CullMode == 2 ? 1 : 0; if (cull != bound) { commandBuffer.bindPipeline(vk::PipelineBindPoint::eGraphics, Pipeline(cull)); bound = cull; } commandBuffer.drawIndexed(pp.count, 1, pp.first, 0, 0); } commandBuffer.endRenderPass(); } void MotionVectorPass::Submit(int image, vk::Image depthImage, vk::ImageView depthView, vk::Format depthFormat) { struct retro_hw_upscaler_image motion, depth; motion.image = (uint64_t)(VkImage)attachments[image]->GetImage(); motion.view = (uint64_t)(VkImageView)attachments[image]->GetImageView(); motion.format = (unsigned)VK_FORMAT_R16G16_SFLOAT; motion.state = (unsigned)VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; motion.width = size.width; motion.height = size.height; depth.image = (uint64_t)(VkImage)depthImage; depth.view = (uint64_t)(VkImageView)depthView; depth.format = (unsigned)(VkFormat)depthFormat; depth.state = (unsigned)(VkImageLayout)depthLayout; depth.width = size.width; depth.height = size.height; // (the depth is 1/w: nearer is greater) upscaler_submit(motion, depth, RETRO_HW_UPSCALER_DEPTH_INVERTED); recorded = false; }