diff --git a/CMakeLists.txt b/CMakeLists.txt index 766c00b2..091d5f30 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -213,6 +213,7 @@ set(SOURCE_FILES MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp MobileGL/MG_Backend/DirectVulkan/Renderer/SwapchainObject.cpp MobileGL/MG_Backend/DirectVulkan/Renderer/FrameContext.cpp + MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateBuilder.cpp MobileGL/MG_Backend/DirectVulkan/Renderer/VertexInputStateFactory.cpp diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp new file mode 100644 index 00000000..8ef44a3f --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp @@ -0,0 +1,115 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "PipelineFactory.h" + +namespace MobileGL::MG_Backend::DirectVulkan { + PipelineFactory::~PipelineFactory() { + DestroyAll(); + } + + PipelineFactory::HashType PipelineFactory::ComputeHash(const PipelineCreatePayload& payload) const { + XXHASH_VERIFY(XXH64_reset(m_hashState, m_config.CacheVersion)); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.programHash, sizeof(payload.programHash))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.vertexInputHash, sizeof(payload.vertexInputHash))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.pipelineLayout, sizeof(payload.pipelineLayout))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.renderPass, sizeof(payload.renderPass))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.subpass, sizeof(payload.subpass))); + XXHASH_VERIFY(XXH64_update(m_hashState, &payload.topology, sizeof(payload.topology))); + return XXH64_digest(m_hashState); + } + + VkPipeline PipelineFactory::GetOrCreatePipeline(const PipelineCreatePayload& payload) { + const HashType hash = ComputeHash(payload); + auto it = m_cache.find(hash); + if (it != m_cache.end()) { + return it->second; + } + + VkPipeline pipeline = CreatePipeline(payload); + m_cache.emplace(hash, pipeline); + return pipeline; + } + + void PipelineFactory::DestroyAll() { + for (auto& pair : m_cache) { + if (pair.second != VK_NULL_HANDLE) { + vkDestroyPipeline(m_device, pair.second, nullptr); + } + } + m_cache.clear(); + } + + VkPipeline PipelineFactory::CreatePipeline(const PipelineCreatePayload& payload) const { + MOBILEGL_ASSERT(payload.stages != nullptr && !payload.stages->empty(), "PipelineFactory: stages are empty"); + MOBILEGL_ASSERT(payload.vertexInputState != nullptr, "PipelineFactory: vertexInputState is null"); + MOBILEGL_ASSERT(payload.pipelineLayout != VK_NULL_HANDLE, "PipelineFactory: pipelineLayout is null"); + MOBILEGL_ASSERT(payload.renderPass != VK_NULL_HANDLE, "PipelineFactory: renderPass is null"); + + static constexpr VkDynamicState kDynamicStates[] = { + VK_DYNAMIC_STATE_VIEWPORT, + VK_DYNAMIC_STATE_SCISSOR + }; + + VkPipelineDynamicStateCreateInfo dynamicState{}; + dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; + dynamicState.dynamicStateCount = static_cast(std::size(kDynamicStates)); + dynamicState.pDynamicStates = kDynamicStates; + + VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; + ia.topology = payload.topology; + + VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; + vpci.viewportCount = 1; + vpci.scissorCount = 1; + + VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; + raster.polygonMode = VK_POLYGON_MODE_FILL; + raster.cullMode = VK_CULL_MODE_NONE; + raster.frontFace = VK_FRONT_FACE_CLOCKWISE; + raster.lineWidth = 1.0f; + + VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; + ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; + + VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; + depthStencil.depthTestEnable = VK_FALSE; + depthStencil.depthWriteEnable = VK_FALSE; + depthStencil.depthCompareOp = VK_COMPARE_OP_ALWAYS; + depthStencil.depthBoundsTestEnable = VK_FALSE; + depthStencil.stencilTestEnable = VK_FALSE; + + VkPipelineColorBlendAttachmentState colorAttach{}; + colorAttach.colorWriteMask = + VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; + colorAttach.blendEnable = VK_FALSE; + VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; + blend.attachmentCount = 1; + blend.pAttachments = &colorAttach; + + VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; + gpi.stageCount = static_cast(payload.stages->size()); + gpi.pStages = payload.stages->data(); + gpi.pVertexInputState = payload.vertexInputState; + gpi.pInputAssemblyState = &ia; + gpi.pViewportState = &vpci; + gpi.pRasterizationState = &raster; + gpi.pMultisampleState = &ms; + gpi.pDepthStencilState = &depthStencil; + gpi.pColorBlendState = &blend; + gpi.pDynamicState = &dynamicState; + gpi.layout = payload.pipelineLayout; + gpi.renderPass = payload.renderPass; + gpi.subpass = payload.subpass; + + VkPipeline pipeline = VK_NULL_HANDLE; + VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpi, nullptr, &pipeline), + "vkCreateGraphicsPipelines"); + return pipeline; + } +} // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h new file mode 100644 index 00000000..7e83062c --- /dev/null +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h @@ -0,0 +1,48 @@ +// MobileGL - MobileGL/MG_Backend/DirectVulkan/Renderer/PipelineFactory.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once + +#include "Config.h" +#include "../VkIncludes.h" +#include + +namespace MobileGL::MG_Backend::DirectVulkan { + class PipelineFactory { + public: + using HashType = Uint64; + + struct PipelineCreatePayload { + HashType programHash = 0; + HashType vertexInputHash = 0; + VkPipelineLayout pipelineLayout = VK_NULL_HANDLE; + VkRenderPass renderPass = VK_NULL_HANDLE; + Uint32 subpass = 0; + VkPrimitiveTopology topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + const Vector* stages = nullptr; + const VkPipelineVertexInputStateCreateInfo* vertexInputState = nullptr; + }; + + explicit PipelineFactory(VkDevice device, const VulkanRendererConfig& config): + m_device(device), m_config(config) {} + ~PipelineFactory(); + PipelineFactory(const PipelineFactory&) = delete; + + HashType ComputeHash(const PipelineCreatePayload& payload) const; + VkPipeline GetOrCreatePipeline(const PipelineCreatePayload& payload); + void DestroyAll(); + + private: + VkPipeline CreatePipeline(const PipelineCreatePayload& payload) const; + + VkDevice m_device = VK_NULL_HANDLE; + const VulkanRendererConfig& m_config; + UnorderedMap m_cache; + static inline XXH64_state_t* m_hashState = XXH64_createState(); + }; +} // namespace MobileGL::MG_Backend::DirectVulkan diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index f809d2bb..6efbc261 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -76,96 +76,21 @@ namespace MobileGL::MG_Backend::DirectVulkan { } )"; - VkPipeline VulkanRenderer::CreateGraphicsPipeline(const VkPipelineVertexInputStateCreateInfo& vertexInputState) const { - MOBILEGL_ASSERT(!m_demoPipelineStages.empty(), "CreateGraphicsPipeline requires shader stages"); + VkPipeline VulkanRenderer::GetOrCreatePipeline( + Uint64 programHash, Uint64 vertexInputHash, const VkPipelineVertexInputStateCreateInfo& vertexInputState) { + MOBILEGL_ASSERT(m_pipelineFactory != nullptr, "PipelineFactory is not initialized"); + MOBILEGL_ASSERT(!m_demoPipelineStages.empty(), "GetOrCreatePipeline requires shader stages"); - VkPipelineDynamicStateCreateInfo dynamicState{}; - dynamicState.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO; - dynamicState.dynamicStateCount = static_cast(std::size(s_dynamicStates)); - dynamicState.pDynamicStates = s_dynamicStates; - - VkPipelineInputAssemblyStateCreateInfo ia{VK_STRUCTURE_TYPE_PIPELINE_INPUT_ASSEMBLY_STATE_CREATE_INFO}; - ia.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; - - VkViewport vp{}; - vp.x = 0; - vp.y = 0; - const auto swapchainExtent = m_swapchainObject.GetExtent(); - vp.width = (float)swapchainExtent.width; - vp.height = (float)swapchainExtent.height; - vp.minDepth = 0; - vp.maxDepth = 1; - VkRect2D scissor{{0, 0}, swapchainExtent}; - VkPipelineViewportStateCreateInfo vpci{VK_STRUCTURE_TYPE_PIPELINE_VIEWPORT_STATE_CREATE_INFO}; - vpci.viewportCount = 1; - vpci.pViewports = &vp; - vpci.scissorCount = 1; - vpci.pScissors = &scissor; - - VkPipelineRasterizationStateCreateInfo raster{VK_STRUCTURE_TYPE_PIPELINE_RASTERIZATION_STATE_CREATE_INFO}; - raster.polygonMode = VK_POLYGON_MODE_FILL; - raster.cullMode = VK_CULL_MODE_NONE; - raster.frontFace = VK_FRONT_FACE_CLOCKWISE; - raster.lineWidth = 1.0f; - - VkPipelineMultisampleStateCreateInfo ms{VK_STRUCTURE_TYPE_PIPELINE_MULTISAMPLE_STATE_CREATE_INFO}; - ms.rasterizationSamples = VK_SAMPLE_COUNT_1_BIT; - - VkPipelineDepthStencilStateCreateInfo depthStencil{VK_STRUCTURE_TYPE_PIPELINE_DEPTH_STENCIL_STATE_CREATE_INFO}; - depthStencil.depthTestEnable = VK_FALSE; - depthStencil.depthWriteEnable = VK_FALSE; - depthStencil.depthCompareOp = VK_COMPARE_OP_ALWAYS; - depthStencil.depthBoundsTestEnable = VK_FALSE; - depthStencil.stencilTestEnable = VK_FALSE; - - VkPipelineColorBlendAttachmentState colorAttach{}; - colorAttach.colorWriteMask = - VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT; - colorAttach.blendEnable = VK_FALSE; - VkPipelineColorBlendStateCreateInfo blend{VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO}; - blend.attachmentCount = 1; - blend.pAttachments = &colorAttach; - - VkGraphicsPipelineCreateInfo gpi{VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO}; - gpi.stageCount = static_cast(m_demoPipelineStages.size()); - gpi.pStages = m_demoPipelineStages.data(); - gpi.pVertexInputState = &vertexInputState; - gpi.pInputAssemblyState = &ia; - gpi.pViewportState = &vpci; - gpi.pRasterizationState = &raster; - gpi.pMultisampleState = &ms; - gpi.pDepthStencilState = &depthStencil; - gpi.pColorBlendState = &blend; - gpi.pDynamicState = &dynamicState; - gpi.layout = m_pipelineLayout; - gpi.renderPass = m_renderPassLoad; - gpi.subpass = 0; - - VkPipeline pipeline = VK_NULL_HANDLE; - VK_VERIFY(vkCreateGraphicsPipelines(m_device, VK_NULL_HANDLE, 1, &gpi, nullptr, &pipeline), - "vkCreateGraphicsPipelines"); - return pipeline; - } - - VkPipeline VulkanRenderer::GetOrCreatePipelineVariant( - Uint64 hash, const VkPipelineVertexInputStateCreateInfo& vertexInputState) { - const auto it = m_pipelineVariants.find(hash); - if (it != m_pipelineVariants.end()) { - return it->second; - } - - VkPipeline pipeline = CreateGraphicsPipeline(vertexInputState); - m_pipelineVariants.emplace(hash, pipeline); - return pipeline; - } - - void VulkanRenderer::DestroyPipelineVariants() { - for (auto& pair : m_pipelineVariants) { - if (pair.second != VK_NULL_HANDLE) { - vkDestroyPipeline(m_device, pair.second, nullptr); - } - } - m_pipelineVariants.clear(); + PipelineFactory::PipelineCreatePayload payload{}; + payload.programHash = programHash; + payload.vertexInputHash = vertexInputHash; + payload.pipelineLayout = m_pipelineLayout; + payload.renderPass = m_renderPassLoad; + payload.subpass = 0; + payload.topology = VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST; + payload.stages = &m_demoPipelineStages; + payload.vertexInputState = &vertexInputState; + return m_pipelineFactory->GetOrCreatePipeline(payload); } void VulkanRenderer::PrepareDemoPipeline() { @@ -196,6 +121,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { } auto& stages = m_programFactory->GetOrCreatePipelineShaderStages(programObject, ProgramFactory::CompileOptionBit::None); + m_demoProgramHash = m_programFactory->ComputeHash(programObject, ProgramFactory::CompileOptionBit::None); m_demoPipelineStages = stages; VkPipelineLayoutCreateInfo plci{VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO}; @@ -203,7 +129,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VertexInputStateBuilder vertexInputBuilder; const auto& vertexInput = vertexInputBuilder.Build(); - m_pipeline = CreateGraphicsPipeline(vertexInput); + m_pipeline = GetOrCreatePipeline(m_demoProgramHash, 0, vertexInput); // vkDestroyShaderModule(m_device, vs, nullptr); // vkDestroyShaderModule(m_device, fs, nullptr); @@ -228,6 +154,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { RecreateSwapchain(); + m_pipelineFactory = MakeUnique(m_device, m_config); m_programFactory = MakeUnique(m_device, m_config); m_vertexInputStateFactory = MakeUnique(m_config); @@ -244,6 +171,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { void VulkanRenderer::Shutdown() { VK_VERIFY(vkDeviceWaitIdle(m_device)); + m_pipelineFactory.reset(); m_programFactory.reset(); m_vertexInputStateFactory.reset(); m_vertexBuffer.Destroy(); @@ -251,11 +179,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_frameContext.Destroy(m_device, m_commandPool); - if (m_pipeline != VK_NULL_HANDLE) { - vkDestroyPipeline(m_device, m_pipeline, nullptr); - m_pipeline = VK_NULL_HANDLE; - } - DestroyPipelineVariants(); + m_pipeline = VK_NULL_HANDLE; if (m_pipelineLayout != VK_NULL_HANDLE) { vkDestroyPipelineLayout(m_device, m_pipelineLayout, nullptr); @@ -500,7 +424,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipeline pipelineToBind = m_pipeline; if (vertexInputState) { - pipelineToBind = GetOrCreatePipelineVariant(vertexInputState->hash, vertexInputState->state); + pipelineToBind = GetOrCreatePipeline(m_demoProgramHash, vertexInputState->hash, vertexInputState->state); if (pipelineToBind == VK_NULL_HANDLE) { MGLOG_W("DrawArrays skipped: failed to create/get pipeline variant"); return; @@ -604,7 +528,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipeline pipelineToBind = m_pipeline; if (vertexInputState) { - pipelineToBind = GetOrCreatePipelineVariant(vertexInputState->hash, vertexInputState->state); + pipelineToBind = GetOrCreatePipeline(m_demoProgramHash, vertexInputState->hash, vertexInputState->state); if (pipelineToBind == VK_NULL_HANDLE) { MGLOG_W("DrawElements skipped: failed to create/get pipeline variant"); return; @@ -1465,15 +1389,14 @@ namespace MobileGL::MG_Backend::DirectVulkan { CreateDepthStencilResources(); CreateDefaultRenderPass(); CreateDefaultFramebuffers(); - DestroyPipelineVariants(); - if (m_pipeline != VK_NULL_HANDLE) { - vkDestroyPipeline(m_device, m_pipeline, nullptr); - m_pipeline = VK_NULL_HANDLE; + if (m_pipelineFactory) { + m_pipelineFactory->DestroyAll(); } - if (!m_demoPipelineStages.empty() && m_pipelineLayout != VK_NULL_HANDLE) { + m_pipeline = VK_NULL_HANDLE; + if (!m_demoPipelineStages.empty() && m_pipelineLayout != VK_NULL_HANDLE && m_pipelineFactory) { VertexInputStateBuilder vertexInputBuilder; const auto& vertexInput = vertexInputBuilder.Build(); - m_pipeline = CreateGraphicsPipeline(vertexInput); + m_pipeline = GetOrCreatePipeline(m_demoProgramHash, 0, vertexInput); } if (m_frameContext.GetFrameCount() > 0) { m_frameContext.GetCurrent().isCommandRecording = false; diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h index aebc79c5..a20978e3 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.h @@ -9,6 +9,7 @@ #pragma once #include "Config.h" #include "FrameContext.h" +#include "PipelineFactory.h" #include "ProgramFactory.h" #include "SwapchainObject.h" #include "VkBufferObject.h" @@ -117,7 +118,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkPipelineLayout m_pipelineLayout = VK_NULL_HANDLE; VkPipeline m_pipeline = VK_NULL_HANDLE; - UnorderedMap m_pipelineVariants; + Uint64 m_demoProgramHash = 0; Vector m_demoPipelineStages; VkBufferObject m_vertexBuffer; VkBufferObject m_indexBuffer; @@ -130,6 +131,7 @@ namespace MobileGL::MG_Backend::DirectVulkan { Uint32 m_pendingClearStencil = 0; Bool m_isMainRenderPassActive = false; + UniquePtr m_pipelineFactory; UniquePtr m_programFactory; UniquePtr m_vertexInputStateFactory; @@ -151,9 +153,8 @@ namespace MobileGL::MG_Backend::DirectVulkan { VkRenderPass CreateDefaultRenderPass(VkAttachmentLoadOp loadOp); void CreateDefaultFramebuffers(); void PrepareDemoPipeline(); - VkPipeline CreateGraphicsPipeline(const VkPipelineVertexInputStateCreateInfo& vertexInputState) const; - VkPipeline GetOrCreatePipelineVariant(Uint64 hash, const VkPipelineVertexInputStateCreateInfo& vertexInputState); - void DestroyPipelineVariants(); + VkPipeline GetOrCreatePipeline(Uint64 programHash, Uint64 vertexInputHash, + const VkPipelineVertexInputStateCreateInfo& vertexInputState); void TransitionSwapchainImageToColorAttachment(VkCommandBuffer commandBuffer, Uint32 imageIndex); void TransitionDepthStencilImageToAttachment(VkCommandBuffer commandBuffer, Uint32 imageIndex); void RecordColorClear(VkCommandBuffer commandBuffer, const VkClearColorValue& clearColor);