From ab7c8501212f282f9b63de6e7feebb4121952834 Mon Sep 17 00:00:00 2001 From: Caleb Burke Date: Wed, 30 Sep 2026 15:51:33 -0700 Subject: [PATCH] Move barriers and frame submit to synchronization2 Image barriers now carry their own stage masks and are recorded with pipelineBarrier2, so barriers flushed together no longer share the union of their stages. AccessState uses the 64-bit stage and access flags, and the frame submit uses submit2. Co-Authored-By: Claude Opus 5.5 --- engine/src/renderer/render_context.cpp | 30 ++++++---- engine/src/renderer/render_graph.cpp | 9 +-- engine/src/renderer/render_graph.hpp | 4 +- engine/src/vulkan/access.cpp | 79 ++++++++++++++++---------- engine/src/vulkan/access.hpp | 24 +++++--- engine/src/vulkan/image.cpp | 5 +- 6 files changed, 87 insertions(+), 64 deletions(-) diff --git a/engine/src/renderer/render_context.cpp b/engine/src/renderer/render_context.cpp index 67a293e..73d7b51 100644 --- a/engine/src/renderer/render_context.cpp +++ b/engine/src/renderer/render_context.cpp @@ -86,22 +86,28 @@ void RenderContext::endFrame() { FrameData& frame = this->frames[this->currentFrameIndex]; frame.commandBuffer.end(); - vk::Semaphore waitSemaphore = frame.imageAvailable.get(); vk::Semaphore signalSemaphore = this->renderFinishedSemaphores.at(this->currentImageIndex).get(); - vk::PipelineStageFlags waitStage = SWAPCHAIN_ACQUIRE_STAGE; - vk::CommandBuffer commandBuffer = frame.commandBuffer; - vk::SubmitInfo submitInfo{}; - submitInfo.waitSemaphoreCount = 1; - submitInfo.pWaitSemaphores = &waitSemaphore; - submitInfo.pWaitDstStageMask = &waitStage; - submitInfo.commandBufferCount = 1; - submitInfo.pCommandBuffers = &commandBuffer; - submitInfo.signalSemaphoreCount = 1; - submitInfo.pSignalSemaphores = &signalSemaphore; + vk::SemaphoreSubmitInfo waitInfo{}; + waitInfo.semaphore = frame.imageAvailable.get(); + waitInfo.stageMask = SWAPCHAIN_ACQUIRE_STAGE; - this->device.getGraphicsQueue().submit(submitInfo, frame.inFlight.get()); + // Present has no pipeline stage to wait on, so signal once everything in + // the submission is done. + vk::SemaphoreSubmitInfo signalInfo{}; + signalInfo.semaphore = signalSemaphore; + signalInfo.stageMask = vk::PipelineStageFlagBits2::eAllCommands; + + vk::CommandBufferSubmitInfo commandBufferInfo{}; + commandBufferInfo.commandBuffer = frame.commandBuffer; + + vk::SubmitInfo2 submitInfo{}; + submitInfo.setWaitSemaphoreInfos(waitInfo); + submitInfo.setCommandBufferInfos(commandBufferInfo); + submitInfo.setSignalSemaphoreInfos(signalInfo); + + this->device.getGraphicsQueue().submit2(submitInfo, frame.inFlight.get()); vk::Result result = this->swapchain->present( this->device.getPresentQueue(), this->currentImageIndex, signalSemaphore); diff --git a/engine/src/renderer/render_graph.cpp b/engine/src/renderer/render_graph.cpp index 880dd5b..af5b978 100644 --- a/engine/src/renderer/render_graph.cpp +++ b/engine/src/renderer/render_graph.cpp @@ -55,8 +55,6 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& next, pending.barriers.push_back(makeImageBarrier( imported.image->get(), imported.image->getSubresourceRange(), imported.state, next)); - pending.srcStages |= imported.state.stage; - pending.dstStages |= next.stage; imported.state = next; } else { // Same-layout read after read. Nothing to record, but a later write has @@ -68,12 +66,7 @@ void RenderGraph::transitionTo(ImageRef ref, const AccessState& next, void RenderGraph::flush(vk::CommandBuffer commandBuffer, PendingBarriers& pending) { - if (pending.barriers.empty()) { - return; - } - - commandBuffer.pipelineBarrier(pending.srcStages, pending.dstStages, {}, - nullptr, nullptr, pending.barriers); + recordImageBarriers(commandBuffer, pending.barriers); pending = PendingBarriers{}; } diff --git a/engine/src/renderer/render_graph.hpp b/engine/src/renderer/render_graph.hpp index ff2cd32..be2e5ef 100644 --- a/engine/src/renderer/render_graph.hpp +++ b/engine/src/renderer/render_graph.hpp @@ -104,9 +104,7 @@ class RenderGraph { }; struct PendingBarriers { - std::vector barriers; - vk::PipelineStageFlags srcStages; - vk::PipelineStageFlags dstStages; + std::vector barriers; }; void transitionTo(ImageRef ref, const AccessState& next, diff --git a/engine/src/vulkan/access.cpp b/engine/src/vulkan/access.cpp index 640e369..027d9dc 100644 --- a/engine/src/vulkan/access.cpp +++ b/engine/src/vulkan/access.cpp @@ -14,10 +14,11 @@ constexpr std::array ALL_ACCESSES = { Access::Present, }; -constexpr vk::AccessFlags WRITE_ACCESSES = - vk::AccessFlagBits::eColorAttachmentWrite | - vk::AccessFlagBits::eDepthStencilAttachmentWrite | - vk::AccessFlagBits::eShaderWrite | vk::AccessFlagBits::eTransferWrite; +constexpr vk::AccessFlags2 WRITE_ACCESSES = + vk::AccessFlagBits2::eColorAttachmentWrite | + vk::AccessFlagBits2::eDepthStencilAttachmentWrite | + vk::AccessFlagBits2::eShaderStorageWrite | + vk::AccessFlagBits2::eTransferWrite; } // namespace @@ -25,34 +26,37 @@ AccessState getAccessState(Access access) { switch (access) { case Access::ColorWrite: return {vk::ImageLayout::eColorAttachmentOptimal, - vk::PipelineStageFlagBits::eColorAttachmentOutput, - vk::AccessFlagBits::eColorAttachmentRead | - vk::AccessFlagBits::eColorAttachmentWrite}; + vk::PipelineStageFlagBits2::eColorAttachmentOutput, + vk::AccessFlagBits2::eColorAttachmentRead | + vk::AccessFlagBits2::eColorAttachmentWrite}; case Access::DepthWrite: return {vk::ImageLayout::eDepthStencilAttachmentOptimal, - vk::PipelineStageFlagBits::eEarlyFragmentTests | - vk::PipelineStageFlagBits::eLateFragmentTests, - vk::AccessFlagBits::eDepthStencilAttachmentRead | - vk::AccessFlagBits::eDepthStencilAttachmentWrite}; + vk::PipelineStageFlagBits2::eEarlyFragmentTests | + vk::PipelineStageFlagBits2::eLateFragmentTests, + vk::AccessFlagBits2::eDepthStencilAttachmentRead | + vk::AccessFlagBits2::eDepthStencilAttachmentWrite}; case Access::SampledRead: return {vk::ImageLayout::eShaderReadOnlyOptimal, - vk::PipelineStageFlagBits::eFragmentShader, - vk::AccessFlagBits::eShaderRead}; + vk::PipelineStageFlagBits2::eFragmentShader, + vk::AccessFlagBits2::eShaderSampledRead}; case Access::StorageReadWrite: - return { - vk::ImageLayout::eGeneral, vk::PipelineStageFlagBits::eComputeShader, - vk::AccessFlagBits::eShaderRead | vk::AccessFlagBits::eShaderWrite}; + return {vk::ImageLayout::eGeneral, + vk::PipelineStageFlagBits2::eComputeShader, + vk::AccessFlagBits2::eShaderStorageRead | + vk::AccessFlagBits2::eShaderStorageWrite}; case Access::TransferSrc: return {vk::ImageLayout::eTransferSrcOptimal, - vk::PipelineStageFlagBits::eTransfer, - vk::AccessFlagBits::eTransferRead}; + vk::PipelineStageFlagBits2::eTransfer, + vk::AccessFlagBits2::eTransferRead}; case Access::TransferDst: return {vk::ImageLayout::eTransferDstOptimal, - vk::PipelineStageFlagBits::eTransfer, - vk::AccessFlagBits::eTransferWrite}; + vk::PipelineStageFlagBits2::eTransfer, + vk::AccessFlagBits2::eTransferWrite}; case Access::Present: + // Presentation isn't a pipeline stage; the present waits on a semaphore + // instead, so the barrier into this state has nothing to block. return {vk::ImageLayout::ePresentSrcKHR, - vk::PipelineStageFlagBits::eBottomOfPipe, vk::AccessFlags{}}; + vk::PipelineStageFlagBits2::eNone, vk::AccessFlags2{}}; } log::fatal("Unknown Access value."); @@ -83,22 +87,37 @@ bool needsBarrier(const AccessState& from, const AccessState& to) { return from.layout != to.layout || writesImage(from) || writesImage(to); } -vk::ImageMemoryBarrier makeImageBarrier(vk::Image image, - const vk::ImageSubresourceRange& range, - const AccessState& from, - const AccessState& to) { - vk::ImageMemoryBarrier barrier{}; +vk::ImageMemoryBarrier2 makeImageBarrier(vk::Image image, + const vk::ImageSubresourceRange& range, + const AccessState& from, + const AccessState& to) { + vk::ImageMemoryBarrier2 barrier{}; + barrier.srcStageMask = from.stage; + barrier.dstStageMask = to.stage; + // Only writes need to be made available; reads just need the execution + // dependency the stage masks already give. + barrier.srcAccessMask = from.access & WRITE_ACCESSES; + barrier.dstAccessMask = to.access; barrier.oldLayout = from.layout; barrier.newLayout = to.layout; barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; barrier.image = image; barrier.subresourceRange = range; - // Only writes need to be made available; reads just need the execution - // dependency the stage masks already give. - barrier.srcAccessMask = from.access & WRITE_ACCESSES; - barrier.dstAccessMask = to.access; return barrier; } +void recordImageBarriers( + vk::CommandBuffer commandBuffer, + vk::ArrayProxy barriers) { + if (barriers.empty()) { + return; + } + + vk::DependencyInfo dependency{}; + dependency.imageMemoryBarrierCount = barriers.size(); + dependency.pImageMemoryBarriers = barriers.data(); + commandBuffer.pipelineBarrier2(dependency); +} + } // namespace sophia diff --git a/engine/src/vulkan/access.hpp b/engine/src/vulkan/access.hpp index 67c35f4..3f78e67 100644 --- a/engine/src/vulkan/access.hpp +++ b/engine/src/vulkan/access.hpp @@ -25,8 +25,8 @@ enum class Access { */ struct AccessState { vk::ImageLayout layout = vk::ImageLayout::eUndefined; - vk::PipelineStageFlags stage = vk::PipelineStageFlagBits::eTopOfPipe; - vk::AccessFlags access; + vk::PipelineStageFlags2 stage = vk::PipelineStageFlagBits2::eNone; + vk::AccessFlags2 access; }; /** @@ -35,8 +35,8 @@ struct AccessState { * just-acquired state has to use this as its source stage, otherwise the * layout transition isn't ordered after the acquire. */ -inline constexpr vk::PipelineStageFlags SWAPCHAIN_ACQUIRE_STAGE = - vk::PipelineStageFlagBits::eColorAttachmentOutput; +inline constexpr vk::PipelineStageFlags2 SWAPCHAIN_ACQUIRE_STAGE = + vk::PipelineStageFlagBits2::eColorAttachmentOutput; AccessState getAccessState(Access access); @@ -55,9 +55,17 @@ bool writesImage(const AccessState& state); */ bool needsBarrier(const AccessState& from, const AccessState& to); -vk::ImageMemoryBarrier makeImageBarrier(vk::Image image, - const vk::ImageSubresourceRange& range, - const AccessState& from, - const AccessState& to); +/** + * A barrier carrying its own stage masks, for recording with + * pipelineBarrier2. Barriers flushed together don't share stages. + */ +vk::ImageMemoryBarrier2 makeImageBarrier(vk::Image image, + const vk::ImageSubresourceRange& range, + const AccessState& from, + const AccessState& to); + +void recordImageBarriers( + vk::CommandBuffer commandBuffer, + vk::ArrayProxy barriers); } // namespace sophia diff --git a/engine/src/vulkan/image.cpp b/engine/src/vulkan/image.cpp index 8d8b93c..d1cd9a5 100644 --- a/engine/src/vulkan/image.cpp +++ b/engine/src/vulkan/image.cpp @@ -56,10 +56,9 @@ void Image::transitionLayout(vk::CommandBuffer commandBuffer, AccessState to = getStateForLayout(newLayout); if (needsBarrier(from, to)) { - vk::ImageMemoryBarrier barrier = + vk::ImageMemoryBarrier2 barrier = makeImageBarrier(this->get(), this->getSubresourceRange(), from, to); - commandBuffer.pipelineBarrier(from.stage, to.stage, {}, nullptr, nullptr, - barrier); + recordImageBarriers(commandBuffer, barrier); } this->layout = newLayout;