// Copyright (C) 2024 Jérôme "SirLynix" Leclercq (lynix680@gmail.com) // This file is part of the "Nazara Engine - Graphics module" // For conditions of distribution and use, see copyright notice in Config.hpp // This class was written with a lot of help from themaister articles and Granite source code, check them out! // https://themaister.net/blog/2017/08/15/render-graphs-and-vulkan-a-deep-dive/ #include #include #include #include #include #include #include #include namespace Nz { namespace { template void UniquePushBack(std::vector& vec, const T& value) { auto it = std::find(vec.begin(), vec.end(), value); if (it == vec.end()) vec.push_back(value); } } BakedFrameGraph FrameGraph::Bake() { if (m_graphOutputs.empty()) throw std::runtime_error("no graph output has been set"); m_pending.attachmentReadList.clear(); m_pending.attachmentToTextures.clear(); m_pending.attachmentWriteList.clear(); m_pending.barrierList.clear(); m_pending.passIdToPhysicalPassIndex.clear(); m_pending.passList.clear(); m_pending.physicalPasses.clear(); m_pending.renderPasses.clear(); m_pending.textures.clear(); m_pending.texture2DPool.clear(); m_pending.textureCubePool.clear(); BuildReadWriteList(); for (std::size_t output : m_graphOutputs) { auto it = m_pending.attachmentWriteList.find(output); if (it == m_pending.attachmentWriteList.end()) throw std::runtime_error("no pass writes to backbuffer"); const std::vector& backbufferPasses = it->second; for (std::size_t passIndex : backbufferPasses) TraverseGraph(passIndex); } std::reverse(m_pending.passList.begin(), m_pending.passList.end()); RemoveDuplicatePasses(); ReorderPasses(); AssignPhysicalTextures(); AssignPhysicalPasses(); BuildPhysicalPasses(); BuildBarriers(); BuildPhysicalBarriers(); std::vector bakedPasses; bakedPasses.reserve(m_pending.physicalPasses.size()); std::size_t renderPassIndex = 0; for (auto& physicalPass : m_pending.physicalPasses) { auto& bakedPass = bakedPasses.emplace_back(); bakedPass.name = std::move(physicalPass.name); bakedPass.renderPass = std::move(m_pending.renderPasses[renderPassIndex++]); bakedPass.invalidationBarriers = std::move(physicalPass.textureBarrier); for (auto& subpass : physicalPass.passes) { const FramePass& framePass = m_framePasses[subpass.passIndex]; bakedPass.executionCallback = framePass.GetExecutionCallback(); //< FIXME auto& bakedSubpass = bakedPass.subpasses.emplace_back(); bakedSubpass.commandCallback = framePass.GetCommandCallback(); const auto& colorOutputs = framePass.GetOutputs(); for (std::size_t i = 0; i < colorOutputs.size(); ++i) { const auto& output = colorOutputs[i]; if (std::size_t textureIndex = Retrieve(m_pending.attachmentToTextures, output.attachmentId); textureIndex != InvalidTextureIndex) bakedPass.outputTextureIndices.push_back(textureIndex); if (output.clearColor) { bakedPass.outputClearValues.resize(i + 1); bakedPass.outputClearValues[i].color = *output.clearColor; } } // Add depth-stencil clear values if (const auto& depthStencilClear = framePass.GetDepthStencilClear()) { std::size_t depthClearIndex = colorOutputs.size(); bakedPass.outputClearValues.resize(depthClearIndex + 1); auto& dsClearValues = bakedPass.outputClearValues[depthClearIndex]; dsClearValues.depth = depthStencilClear->depth; dsClearValues.stencil = depthStencilClear->stencil; } std::size_t attachmentId; if (attachmentId = framePass.GetDepthStencilOutput(); attachmentId != FramePass::InvalidAttachmentId) { if (std::size_t textureIndex = Retrieve(m_pending.attachmentToTextures, attachmentId); textureIndex != InvalidTextureIndex) bakedPass.outputTextureIndices.push_back(textureIndex); } else if (attachmentId = framePass.GetDepthStencilInput(); attachmentId != FramePass::InvalidAttachmentId) { if (std::size_t textureIndex = Retrieve(m_pending.attachmentToTextures, attachmentId); textureIndex != InvalidTextureIndex) bakedPass.outputTextureIndices.push_back(textureIndex); } } } std::vector bakedTextures; bakedTextures.reserve(m_pending.textures.size()); for (auto& texture : m_pending.textures) { auto& bakedTexture = bakedTextures.emplace_back(); static_cast(bakedTexture) = std::move(texture); bakedTexture.texture = bakedTexture.externalTexture; } return BakedFrameGraph(std::move(bakedPasses), std::move(bakedTextures), std::move(m_pending.attachmentToTextures), std::move(m_pending.passIdToPhysicalPassIndex)); } void FrameGraph::AssignPhysicalPasses() { auto ShouldMerge = [&](const FramePass& prevPass, const FramePass& nextPass) { //TODO return false; }; for (std::size_t passIndex = 0; passIndex < m_pending.passList.size();) { std::size_t mergeEnd = passIndex + 1; for (; mergeEnd < m_pending.passList.size(); ++mergeEnd) { bool merge = true; for (std::size_t mergeStart = passIndex; mergeStart < mergeEnd; ++mergeStart) { if (!ShouldMerge(m_framePasses[m_pending.passList[mergeStart]], m_framePasses[m_pending.passList[mergeEnd]])) { merge = false; break; } } if (!merge) break; } std::size_t physPassIndex = m_pending.physicalPasses.size(); PhysicalPassData& currentPass = m_pending.physicalPasses.emplace_back(); auto it = m_pending.passList.begin() + passIndex; auto end = m_pending.passList.begin() + mergeEnd; for (; it < end; ++it) { const FramePass& pass = m_framePasses[*it]; if (currentPass.name.empty()) currentPass.name = pass.GetName(); else currentPass.name += " / " + pass.GetName(); auto& subpass = currentPass.passes.emplace_back(); subpass.passIndex = *it; m_pending.passIdToPhysicalPassIndex.emplace(subpass.passIndex, physPassIndex); } passIndex = mergeEnd; } } void FrameGraph::AssignPhysicalTextures() { // Assign last use pass index for every attachment for (std::size_t passIndex : m_pending.passList) { const FramePass& framePass = m_framePasses[passIndex]; framePass.ForEachAttachment([&](std::size_t attachmentId) { attachmentId = ResolveAttachmentIndex(attachmentId); m_pending.attachmentLastUse[attachmentId] = passIndex; }); } for (std::size_t passIndex : m_pending.passList) { const FramePass& framePass = m_framePasses[passIndex]; for (const auto& input : framePass.GetInputs()) { std::size_t textureId = RegisterTexture(input.attachmentId); if (textureId != InvalidTextureIndex) { FrameGraphTextureData& attachmentData = m_pending.textures[textureId]; attachmentData.usage |= input.textureUsageFlags.value_or(TextureUsage::ShaderSampling); } } for (const auto& output : framePass.GetOutputs()) { std::size_t textureId = RegisterTexture(output.attachmentId); if (textureId != InvalidTextureIndex) { FrameGraphTextureData& attachmentData = m_pending.textures[textureId]; attachmentData.usage |= TextureUsage::ColorAttachment; } } if (std::size_t depthStencilInput = framePass.GetDepthStencilInput(); depthStencilInput != FramePass::InvalidAttachmentId) { std::size_t textureId = RegisterTexture(depthStencilInput); if (textureId != InvalidTextureIndex) { FrameGraphTextureData& attachmentData = m_pending.textures[textureId]; attachmentData.usage |= TextureUsage::DepthStencilAttachment; } if (std::size_t depthStencilOutput = framePass.GetDepthStencilOutput(); depthStencilOutput != FramePass::InvalidAttachmentId) { if (auto it = m_pending.attachmentToTextures.find(depthStencilOutput); it == m_pending.attachmentToTextures.end()) { // Special case where multiples attachments point simultaneously to the same texture m_pending.attachmentToTextures.emplace(depthStencilOutput, textureId); auto inputIt = m_pending.attachmentLastUse.find(depthStencilInput); auto outputIt = m_pending.attachmentLastUse.find(depthStencilInput); if (inputIt != m_pending.attachmentLastUse.end() && outputIt != m_pending.attachmentLastUse.end()) { if (inputIt->second > outputIt->second) m_pending.attachmentLastUse.erase(outputIt); else m_pending.attachmentLastUse.erase(inputIt); } } else if (it->second != textureId) throw std::runtime_error("depth-stencil output already assigned"); } } if (std::size_t depthStencilOutput = framePass.GetDepthStencilOutput(); depthStencilOutput != FramePass::InvalidAttachmentId) { std::size_t textureId = RegisterTexture(depthStencilOutput); FrameGraphTextureData& attachmentData = m_pending.textures[textureId]; attachmentData.usage |= TextureUsage::DepthStencilAttachment; } framePass.ForEachAttachment([&](std::size_t attachmentId) { attachmentId = ResolveAttachmentIndex(attachmentId); auto it = m_pending.attachmentLastUse.find(attachmentId); // If this pass is the last one where this attachment is used, push the texture to the reuse pool if (it != m_pending.attachmentLastUse.end() && passIndex == it->second) { const auto& attachmentData = m_attachments[attachmentId]; if (std::holds_alternative(attachmentData)) { std::size_t textureId = Retrieve(m_pending.attachmentToTextures, attachmentId); if (m_pending.textures[textureId].canReuse) { assert(std::find(m_pending.texture2DPool.begin(), m_pending.texture2DPool.end(), textureId) == m_pending.texture2DPool.end()); m_pending.texture2DPool.push_back(textureId); } } else if (std::holds_alternative(attachmentData)) { std::size_t textureId = Retrieve(m_pending.attachmentToTextures, attachmentId); if (m_pending.textures[textureId].canReuse) { assert(std::find(m_pending.textureCubePool.begin(), m_pending.textureCubePool.end(), textureId) == m_pending.textureCubePool.end()); m_pending.texture2DArrayPool.push_back(textureId); } } else if (std::holds_alternative(attachmentData)) { std::size_t textureId = Retrieve(m_pending.attachmentToTextures, attachmentId); if (m_pending.textures[textureId].canReuse) { assert(std::find(m_pending.textureCubePool.begin(), m_pending.textureCubePool.end(), textureId) == m_pending.textureCubePool.end()); m_pending.textureCubePool.push_back(textureId); } } } }); } // Add TextureUsage::ShaderSampling and TextureUsage::TransferSource to final outputs for (std::size_t output : m_graphOutputs) { auto it = m_pending.attachmentToTextures.find(output); assert(it != m_pending.attachmentToTextures.end()); if (std::size_t textureIndex = it->second; textureIndex != InvalidTextureIndex) { auto& finalTexture = m_pending.textures[textureIndex]; finalTexture.usage |= TextureUsage::ShaderSampling | TextureUsage::TransferSource; } } // Apply texture view usage to their parents for (auto& textureData : m_pending.textures) { if (textureData.viewData) { auto& parentTextureData = m_pending.textures[textureData.viewData->parentTextureId]; parentTextureData.usage |= textureData.usage; } } } void FrameGraph::BuildBarriers() { assert(m_pending.barrierList.empty()); m_pending.barrierList.reserve(m_pending.passList.size()); auto GetBarrier = [&](std::vector& barriers, std::size_t attachmentId) -> Barrier* { std::size_t textureId = Retrieve(m_pending.attachmentToTextures, ResolveAttachmentIndex(attachmentId)); if (textureId == InvalidTextureIndex) return nullptr; auto it = std::find_if(barriers.begin(), barriers.end(), [&](const Barrier& barrier) { return barrier.textureId == textureId; }); if (it != barriers.end()) return &*it; else { // Insert a new barrier auto& barrier = barriers.emplace_back(); barrier.textureId = textureId; barrier.layout = TextureLayout::Undefined; return &barrier; } }; for (std::size_t passId : m_pending.passList) { const FramePass& framePass = m_framePasses[passId]; auto& barriers = m_pending.barrierList.emplace_back(); auto GetInvalidationBarrier = [&](std::size_t attachmentId) -> Barrier* { return GetBarrier(barriers.invalidationBarriers, attachmentId); }; auto GetFlushBarrier = [&](std::size_t attachmentId) -> Barrier* { return GetBarrier(barriers.flushBarriers, attachmentId); }; for (const auto& input : framePass.GetInputs()) { Barrier* barrier = GetInvalidationBarrier(input.attachmentId); if (!barrier) continue; if (barrier->layout != TextureLayout::Undefined) throw std::runtime_error("layout mismatch"); barrier->access |= input.accessFlags; barrier->stages |= input.stageFlags; barrier->layout = input.layout; } for (const auto& output : framePass.GetOutputs()) { Barrier* barrier = GetFlushBarrier(output.attachmentId); if (!barrier) continue; if (barrier->layout != TextureLayout::Undefined) throw std::runtime_error("layout mismatch"); barrier->access |= MemoryAccess::ColorWrite; barrier->stages |= PipelineStage::ColorOutput; barrier->layout = TextureLayout::ColorOutput; } std::size_t dsInputAttachment = framePass.GetDepthStencilInput(); std::size_t dsOutputAttachement = framePass.GetDepthStencilOutput(); if (dsInputAttachment != FramePass::InvalidAttachmentId && dsOutputAttachement != FramePass::InvalidAttachmentId) { // DS input/output if (Barrier* invalidationBarrier = GetInvalidationBarrier(dsInputAttachment)) { if (invalidationBarrier->layout != TextureLayout::Undefined) throw std::runtime_error("layout mismatch"); invalidationBarrier->layout = TextureLayout::DepthStencilReadWrite; invalidationBarrier->access = MemoryAccess::DepthStencilRead | MemoryAccess::DepthStencilWrite; invalidationBarrier->stages = PipelineStage::FragmentTestsEarly | PipelineStage::FragmentTestsLate; } if (Barrier* flushBarrier = GetFlushBarrier(dsOutputAttachement)) { flushBarrier->layout = TextureLayout::DepthStencilReadWrite; flushBarrier->access = MemoryAccess::DepthStencilWrite; flushBarrier->stages = PipelineStage::FragmentTestsLate; } } else if (dsInputAttachment != FramePass::InvalidAttachmentId) { // DS input-only if (Barrier* invalidationBarrier = GetInvalidationBarrier(dsInputAttachment)) { if (invalidationBarrier->layout != TextureLayout::Undefined) throw std::runtime_error("layout mismatch"); invalidationBarrier->layout = TextureLayout::DepthStencilReadWrite; invalidationBarrier->access = MemoryAccess::DepthStencilRead; invalidationBarrier->stages = PipelineStage::FragmentTestsEarly | PipelineStage::FragmentTestsLate; } } else if (dsOutputAttachement != FramePass::InvalidAttachmentId) { // DS output-only if (Barrier* flushBarrier = GetFlushBarrier(dsOutputAttachement)) { if (flushBarrier->layout != TextureLayout::Undefined) throw std::runtime_error("layout mismatch"); flushBarrier->layout = TextureLayout::DepthStencilReadWrite; flushBarrier->access = MemoryAccess::DepthStencilWrite; flushBarrier->stages = PipelineStage::FragmentTestsLate; } } } } void FrameGraph::BuildPhysicalBarriers() { struct PassTextureStates { MemoryAccessFlags invalidatedAccesses; MemoryAccessFlags flushedAccesses; PipelineStageFlags invalidatedStages; PipelineStageFlags flushedStages; TextureLayout initialLayout = TextureLayout::Undefined; TextureLayout finalLayout = TextureLayout::Undefined; }; struct TextureStates { MemoryAccessFlags flushedAccesses; PipelineStageFlags flushedStages; TextureLayout currentLayout = TextureLayout::Undefined; }; std::vector textureStates(m_pending.textures.size()); std::vector passTextureStates; auto barriersIt = m_pending.barrierList.begin(); for (auto& physicalPass : m_pending.physicalPasses) { passTextureStates.clear(); passTextureStates.resize(m_pending.textures.size()); for (auto& subpass : physicalPass.passes) { auto& barriers = *barriersIt++; for (auto& invalidation : barriers.invalidationBarriers) { auto& states = passTextureStates[invalidation.textureId]; if (states.initialLayout == TextureLayout::Undefined) { // First use in this pass states.invalidatedAccesses |= invalidation.access; states.invalidatedStages |= invalidation.stages; states.initialLayout = invalidation.layout; } states.finalLayout = invalidation.layout; states.flushedAccesses = 0; states.flushedStages = 0; } for (auto& flush : barriers.flushBarriers) { auto& states = passTextureStates[flush.textureId]; states.flushedAccesses |= flush.access; states.flushedStages |= flush.stages; states.finalLayout = flush.layout; if (states.initialLayout == TextureLayout::Undefined) { // First flush in a render pass needs a matching invalidation states.initialLayout = flush.layout; states.invalidatedAccesses = flush.access; states.invalidatedStages = flush.stages; textureStates[flush.textureId].currentLayout = flush.layout; if (states.invalidatedStages & PipelineStage::FragmentTestsLate) states.invalidatedStages |= PipelineStage::FragmentTestsEarly; if (states.invalidatedAccesses & MemoryAccess::ColorWrite) states.invalidatedAccesses |= MemoryAccess::ColorRead; if (states.invalidatedAccesses & MemoryAccess::DepthStencilWrite) states.invalidatedAccesses |= MemoryAccess::DepthStencilRead; if (states.invalidatedAccesses & MemoryAccess::ShaderWrite) states.invalidatedAccesses |= MemoryAccess::ShaderRead; // TODO: Discard resource } } } for (std::size_t textureId = 0; textureId < passTextureStates.size(); ++textureId) { const auto& state = passTextureStates[textureId]; if (state.initialLayout == TextureLayout::Undefined && state.finalLayout == TextureLayout::Undefined) continue; //< Texture wasn't touched in this pass assert(state.finalLayout != TextureLayout::Undefined); if (textureStates[textureId].flushedAccesses != 0) { auto& invalidationBarrier = physicalPass.textureBarrier.emplace_back(); invalidationBarrier.textureId = textureId; invalidationBarrier.srcAccessMask = textureStates[textureId].flushedAccesses; invalidationBarrier.srcStageMask = textureStates[textureId].flushedStages; invalidationBarrier.dstAccessMask = state.invalidatedAccesses; invalidationBarrier.dstStageMask = state.invalidatedStages; invalidationBarrier.oldLayout = textureStates[textureId].currentLayout; invalidationBarrier.newLayout = state.initialLayout; textureStates[textureId].flushedAccesses = 0; textureStates[textureId].flushedStages = 0; } textureStates[textureId].currentLayout = state.finalLayout; textureStates[textureId].flushedAccesses |= state.flushedAccesses; textureStates[textureId].flushedStages |= state.flushedStages; } } } void FrameGraph::BuildPhysicalPassDependencies(std::size_t colorAttachmentCount, bool hasDepthStencilAttachment, std::vector& renderPassAttachments, std::vector& subpasses, std::vector& dependencies) { if (hasDepthStencilAttachment) { auto& depthStencilAttachment = renderPassAttachments[colorAttachmentCount]; if (PixelFormatInfo::GetContent(depthStencilAttachment.format) == PixelFormatContent::DepthStencil) { depthStencilAttachment.stencilLoadOp = depthStencilAttachment.loadOp; depthStencilAttachment.stencilStoreOp = depthStencilAttachment.storeOp; } else { depthStencilAttachment.stencilLoadOp = AttachmentLoadOp::Discard; depthStencilAttachment.stencilStoreOp = AttachmentStoreOp::Discard; } } struct SubpassInfo { bool hasColorWrite = false; bool hasDepthStencilRead = false; bool hasDepthStencilWrite = false; bool externalColorSynchronization = false; bool externalDepthSynchronization = false; }; StackArray subpassInfo = NazaraStackArray(SubpassInfo, subpasses.size()); for (std::size_t attachmentIndex = 0; attachmentIndex < renderPassAttachments.size(); ++attachmentIndex) { bool used = false; //< has the attachment already been used in a previous subpass TextureLayout currentLayout = renderPassAttachments[attachmentIndex].initialLayout; auto FindColor = [&](std::size_t subpassIndex) -> RenderPass::AttachmentReference* { auto& subpassDesc = subpasses[subpassIndex]; for (auto& colorReference : subpassDesc.colorAttachment) { if (colorReference.attachmentIndex == attachmentIndex) return &colorReference; } return nullptr; }; auto FindDepthStencil = [&](std::size_t subpassIndex) -> RenderPass::AttachmentReference* { auto& subpassDesc = subpasses[subpassIndex]; if (subpassDesc.depthStencilAttachment && subpassDesc.depthStencilAttachment->attachmentIndex == attachmentIndex) return &subpassDesc.depthStencilAttachment.value(); return nullptr; }; for (std::size_t subpassIndex = 0; subpassIndex < subpasses.size(); ++subpassIndex) { RenderPass::SubpassDescription& subpassDesc = subpasses[subpassIndex]; RenderPass::AttachmentReference* colorAttachment = FindColor(subpassIndex); RenderPass::AttachmentReference* depthStencilAttachment = FindDepthStencil(subpassIndex); if (!colorAttachment && !depthStencilAttachment) { if (used) subpassDesc.preserveAttachments.push_back(attachmentIndex); continue; } if (colorAttachment) { subpassInfo[subpassIndex].hasColorWrite = true; currentLayout = colorAttachment->attachmentLayout; // If this subpass performs a layout change, color must be synchronized with external if (!used && renderPassAttachments[attachmentIndex].initialLayout != currentLayout) subpassInfo[subpassIndex].externalColorSynchronization = true; } else if (depthStencilAttachment) { if (depthStencilAttachment->attachmentLayout == TextureLayout::DepthStencilReadWrite) subpassInfo[subpassIndex].hasDepthStencilWrite = true; subpassInfo[subpassIndex].hasDepthStencilRead = true; currentLayout = depthStencilAttachment->attachmentLayout; // If this subpass performs a layout change, depth must be synchronized with external if (!used && renderPassAttachments[attachmentIndex].initialLayout != currentLayout) subpassInfo[subpassIndex].externalDepthSynchronization = true; } used = true; } } // Handle external subpass dependencies for (std::size_t subpassIndex = 0; subpassIndex < subpasses.size(); ++subpassIndex) { const auto& sync = subpassInfo[subpassIndex]; if (!sync.externalColorSynchronization && !sync.externalDepthSynchronization) continue; auto& subpassDependency = dependencies.emplace_back(); subpassDependency.fromSubpassIndex = RenderPass::ExternalSubpassIndex; subpassDependency.toSubpassIndex = subpassIndex; subpassDependency.tilable = true; // TODO: Handle bottom of pipe? if (sync.externalColorSynchronization) { subpassDependency.fromStages |= PipelineStage::ColorOutput; subpassDependency.fromAccessFlags |= MemoryAccess::ColorWrite; subpassDependency.toStages |= PipelineStage::ColorOutput; subpassDependency.toAccessFlags |= MemoryAccess::ColorRead | MemoryAccess::ColorWrite; } if (sync.externalDepthSynchronization) { subpassDependency.fromStages |= PipelineStage::FragmentTestsLate; subpassDependency.fromAccessFlags |= MemoryAccess::DepthStencilWrite; subpassDependency.toStages |= PipelineStage::FragmentTestsEarly | PipelineStage::FragmentTestsLate; subpassDependency.toAccessFlags |= MemoryAccess::DepthStencilRead | MemoryAccess::DepthStencilWrite; } } // TODO: Handle self-dependencies // Handle pass to pass dependencies for (std::size_t subpassIndex = 1; subpassIndex < subpasses.size(); ++subpassIndex) { auto& subpassDependency = dependencies.emplace_back(); subpassDependency.fromSubpassIndex = subpassIndex - 1; subpassDependency.toSubpassIndex = subpassIndex; subpassDependency.tilable = true; const auto& prevSync = subpassInfo[subpassDependency.fromSubpassIndex]; const auto& sync = subpassInfo[subpassDependency.toSubpassIndex]; // Previous pass flags if (prevSync.hasColorWrite) { subpassDependency.fromAccessFlags = MemoryAccess::ColorWrite; subpassDependency.fromStages = PipelineStage::ColorOutput; } if (prevSync.hasDepthStencilRead) { subpassDependency.fromStages |= PipelineStage::FragmentTestsEarly | PipelineStage::FragmentTestsLate; subpassDependency.fromAccessFlags |= MemoryAccess::DepthStencilRead; } if (prevSync.hasDepthStencilWrite) { subpassDependency.fromStages |= PipelineStage::FragmentTestsEarly | PipelineStage::FragmentTestsLate; subpassDependency.fromAccessFlags |= MemoryAccess::DepthStencilWrite; } // Current pass flags if (sync.hasColorWrite) { subpassDependency.toStages = PipelineStage::ColorOutput; subpassDependency.toAccessFlags = MemoryAccess::ColorRead | MemoryAccess::ColorWrite; } if (sync.hasDepthStencilRead) { subpassDependency.toStages |= PipelineStage::FragmentTestsEarly | PipelineStage::FragmentTestsLate; subpassDependency.toAccessFlags |= MemoryAccess::DepthStencilRead; } if (sync.hasDepthStencilWrite) { subpassDependency.toStages |= PipelineStage::FragmentTestsEarly | PipelineStage::FragmentTestsLate; subpassDependency.toAccessFlags |= MemoryAccess::DepthStencilRead | MemoryAccess::DepthStencilWrite; } // TODO: Handle InputAttachment } } void FrameGraph::BuildPhysicalPasses() { const RenderPassCache& renderPassCache = Graphics::Instance()->GetRenderPassCache(); std::vector textureLayouts(m_pending.textures.size(), TextureLayout::Undefined); // Per-pass data (reuse memory) std::optional depthStencilAttachmentIndex; std::size_t depthStencilAttachmentId; std::unordered_map usedTextureAttachments; std::vector renderPassAttachments; std::vector subpassesDesc; std::vector subpassesDeps; auto RegisterColorInputRead = [&](const FramePass::Input& input) { std::size_t textureId = Retrieve(m_pending.attachmentToTextures, input.attachmentId); if (textureId == InvalidTextureIndex) return; TextureLayout& textureLayout = textureLayouts[textureId]; if (!input.assumedLayout) { assert(textureLayouts[textureId] != TextureLayout::Undefined); textureLayout = input.layout; } else textureLayout = *input.assumedLayout; }; auto RegisterColorOutput = [&](const FramePass::Output& output, bool shouldLoad) { std::size_t textureId = Retrieve(m_pending.attachmentToTextures, output.attachmentId); if (textureId == InvalidTextureIndex) return InvalidAttachmentIndex; TextureLayout initialLayout = textureLayouts[textureId]; textureLayouts[textureId] = TextureLayout::ColorOutput; auto it = usedTextureAttachments.find(textureId); if (it != usedTextureAttachments.end()) return it->second; std::size_t attachmentIndex = renderPassAttachments.size(); auto& attachment = renderPassAttachments.emplace_back(); attachment.format = m_pending.textures[textureId].format; attachment.initialLayout = initialLayout; attachment.storeOp = AttachmentStoreOp::Store; attachment.stencilLoadOp = AttachmentLoadOp::Discard; attachment.stencilStoreOp = AttachmentStoreOp::Discard; if (output.clearColor) attachment.loadOp = AttachmentLoadOp::Clear; else if (shouldLoad) attachment.loadOp = AttachmentLoadOp::Load; else attachment.loadOp = AttachmentLoadOp::Discard; usedTextureAttachments.emplace(textureId, attachmentIndex); return attachmentIndex; }; auto RegisterDepthStencil = [&](std::size_t attachmentId, TextureLayout textureLayout, bool* first) -> RenderPass::Attachment* { if (depthStencilAttachmentIndex) { assert(depthStencilAttachmentId == attachmentId); *first = false; return &renderPassAttachments[depthStencilAttachmentIndex.value()]; } *first = true; std::size_t textureId = Retrieve(m_pending.attachmentToTextures, attachmentId); if (textureId == InvalidTextureIndex) return nullptr; TextureLayout initialLayout = textureLayouts[textureId]; textureLayouts[textureId] = textureLayout; depthStencilAttachmentId = attachmentId; depthStencilAttachmentIndex = renderPassAttachments.size(); usedTextureAttachments.emplace(textureId, *depthStencilAttachmentIndex); auto& depthStencilAttachment = renderPassAttachments.emplace_back(); depthStencilAttachment.format = m_pending.textures[textureId].format; depthStencilAttachment.initialLayout = initialLayout; return &depthStencilAttachment; }; std::size_t physicalPassIndex = 0; for (auto& physicalPass : m_pending.physicalPasses) { depthStencilAttachmentIndex = std::nullopt; usedTextureAttachments.clear(); renderPassAttachments.clear(); subpassesDesc.clear(); subpassesDeps.clear(); std::size_t subpassIndex = 0; std::vector colorAttachments; for (auto& subpass : physicalPass.passes) { const FramePass& framePass = m_framePasses[subpass.passIndex]; const auto& subpassInputs = framePass.GetInputs(); const auto& subpassOutputs = framePass.GetOutputs(); colorAttachments.reserve(subpassOutputs.size()); for (const auto& input : subpassInputs) { if (input.doesRead) RegisterColorInputRead(input); } for (const auto& output : subpassOutputs) { bool shouldLoad = false; // load content if read-write if (HasAttachment(subpassInputs, output.attachmentId)) shouldLoad = true; std::size_t attachmentIndex = RegisterColorOutput(output, shouldLoad); if (attachmentIndex != InvalidAttachmentIndex) { colorAttachments.push_back({ attachmentIndex, TextureLayout::ColorOutput }); } } } std::size_t colorAttachmentCount = renderPassAttachments.size(); std::optional depthStencilAttachment; for (auto& subpass : physicalPass.passes) { const FramePass& framePass = m_framePasses[subpass.passIndex]; std::size_t dsInputAttachment = framePass.GetDepthStencilInput(); std::size_t dsOutputAttachement = framePass.GetDepthStencilOutput(); if (dsInputAttachment != FramePass::InvalidAttachmentId && dsOutputAttachement != FramePass::InvalidAttachmentId) { // DS input/output bool first; RenderPass::Attachment* dsAttachment = RegisterDepthStencil(dsInputAttachment, TextureLayout::DepthStencilReadWrite, &first); if (dsAttachment) { if (first) { dsAttachment->loadOp = AttachmentLoadOp::Load; dsAttachment->storeOp = AttachmentStoreOp::Store; } depthStencilAttachment = RenderPass::AttachmentReference{ depthStencilAttachmentIndex.value(), TextureLayout::DepthStencilReadWrite }; } } else if (dsInputAttachment != FramePass::InvalidAttachmentId) { // DS input-only bool first; RenderPass::Attachment* dsAttachment = RegisterDepthStencil(dsInputAttachment, TextureLayout::DepthStencilReadOnly, &first); if (dsAttachment) { if (first) { bool canDiscard = true; // Check if a future pass reads from the DS buffer or if we can discard it after this pass if (auto readIt = m_pending.attachmentReadList.find(dsInputAttachment); readIt != m_pending.attachmentReadList.end()) { for (std::size_t passIndex : readIt->second) { auto it = m_pending.passIdToPhysicalPassIndex.find(passIndex); if (it == m_pending.passIdToPhysicalPassIndex.end()) continue; //< pass may have been discarded std::size_t readPhysicalPassIndex = it->second; if (readPhysicalPassIndex > physicalPassIndex) //< Read in a future pass? { // Yes, store it canDiscard = false; break; } } } dsAttachment->storeOp = (canDiscard) ? AttachmentStoreOp::Discard : AttachmentStoreOp::Store; } depthStencilAttachment = RenderPass::AttachmentReference{ depthStencilAttachmentIndex.value(), TextureLayout::DepthStencilReadOnly }; } } else if (dsOutputAttachement != FramePass::InvalidAttachmentId) { // DS output-only bool first; RenderPass::Attachment* dsAttachment = RegisterDepthStencil(dsOutputAttachement, TextureLayout::DepthStencilReadWrite, &first); if (dsAttachment) { if (first) { dsAttachment->initialLayout = TextureLayout::Undefined; //< Don't care about initial layout dsAttachment->loadOp = (framePass.GetDepthStencilClear()) ? AttachmentLoadOp::Clear : AttachmentLoadOp::Discard; dsAttachment->storeOp = AttachmentStoreOp::Store; } depthStencilAttachment = RenderPass::AttachmentReference{ depthStencilAttachmentIndex.value(), TextureLayout::DepthStencilReadWrite }; } } subpassesDesc.push_back({ std::move(colorAttachments), {}, {}, std::move(depthStencilAttachment) }); subpassIndex++; } // Assign final layout (TODO: Use this to perform layouts useful for future passes?) for (const auto& [textureId, attachmentIndex] : usedTextureAttachments) { auto& attachment = renderPassAttachments[attachmentIndex]; attachment.finalLayout = textureLayouts[textureId]; } BuildPhysicalPassDependencies(colorAttachmentCount, depthStencilAttachmentIndex.has_value(), renderPassAttachments, subpassesDesc, subpassesDeps); if (!renderPassAttachments.empty()) m_pending.renderPasses.push_back(renderPassCache.Get(renderPassAttachments, subpassesDesc, subpassesDeps)); else m_pending.renderPasses.push_back(nullptr); physicalPassIndex++; } } void FrameGraph::BuildReadWriteList() { for (std::size_t passIndex = 0; passIndex < m_framePasses.size(); ++passIndex) { const FramePass& framePass = m_framePasses[passIndex]; for (const auto& input : framePass.GetInputs()) UniquePushBack(m_pending.attachmentReadList[input.attachmentId], passIndex); if (std::size_t depthStencilId = framePass.GetDepthStencilInput(); depthStencilId != FramePass::InvalidAttachmentId) UniquePushBack(m_pending.attachmentReadList[depthStencilId], passIndex); for (const auto& output : framePass.GetOutputs()) UniquePushBack(m_pending.attachmentWriteList[output.attachmentId], passIndex); if (std::size_t depthStencilId = framePass.GetDepthStencilOutput(); depthStencilId != FramePass::InvalidAttachmentId) UniquePushBack(m_pending.attachmentWriteList[depthStencilId], passIndex); } } bool FrameGraph::HasAttachment(const std::vector& inputs, std::size_t attachmentIndex) const { attachmentIndex = ResolveAttachmentIndex(attachmentIndex); for (const auto& input : inputs) { if (ResolveAttachmentIndex(input.attachmentId) == attachmentIndex) return true; } return false; } void FrameGraph::RegisterPassInput(std::size_t passIndex, std::size_t attachmentIndex) { auto it = m_pending.attachmentWriteList.find(attachmentIndex); if (it != m_pending.attachmentWriteList.end()) { const PassList& dependencyPassList = it->second; for (std::size_t dependencyPass : dependencyPassList) { if (dependencyPass != passIndex) TraverseGraph(dependencyPass); } } } std::size_t FrameGraph::RegisterTexture(std::size_t attachmentIndex) { if (auto it = m_pending.attachmentToTextures.find(attachmentIndex); it != m_pending.attachmentToTextures.end()) return it->second; auto InsertTexture = [this](ImageType imageType, std::size_t attachmentIndex, const FramePassAttachment& attachmentData, std::size_t& textureId) -> FrameGraphTextureData& { textureId = m_pending.textures.size(); m_pending.attachmentToTextures.emplace(attachmentIndex, textureId); FrameGraphTextureData& data = m_pending.textures.emplace_back(); data.type = imageType; data.name = attachmentData.name; data.format = attachmentData.format; data.width = attachmentData.width; data.height = attachmentData.height; data.size = attachmentData.size; data.layerCount = 1; data.usage = attachmentData.additionalUsages; data.viewerIndex = attachmentData.viewerIndex; data.canReuse = true; return data; }; auto CheckExternalTexture = [this](std::size_t attachmentIndex, FrameGraphTextureData& data) { // Check if texture if (auto externalIt = m_externalTextures.find(attachmentIndex); externalIt != m_externalTextures.end()) { if (data.viewData) throw std::runtime_error("texture views cannot be bound to external textures"); data.externalTexture = externalIt->second; data.canReuse = false; data.size = FramePassAttachmentSize::Fixed; const TextureInfo& textureInfo = data.externalTexture->GetTextureInfo(); data.width = textureInfo.width; data.height = textureInfo.height; // Check that texture settings match if (textureInfo.type != data.type) throw std::runtime_error("external texture type doesn't match attachment type"); if (textureInfo.layerCount != data.layerCount) throw std::runtime_error("external texture layer count doesn't match attachment type"); if (textureInfo.pixelFormat != data.format) throw std::runtime_error("external texture format doesn't match attachment type"); } }; return std::visit([&](auto&& arg) -> std::size_t { using T = std::decay_t; if constexpr (std::is_same_v) { const FramePassAttachment& attachmentData = arg; // Fetch from reuse pool if possible for (auto it = m_pending.texture2DPool.begin(); it != m_pending.texture2DPool.end(); ++it) { std::size_t textureId = *it; FrameGraphTextureData& data = m_pending.textures[textureId]; assert(data.type == ImageType::E2D); if (data.format != attachmentData.format || data.width != attachmentData.width || data.height != attachmentData.height || data.size != attachmentData.size) continue; if (data.size == FramePassAttachmentSize::ViewerTargetFactor && data.viewerIndex != attachmentData.viewerIndex) continue; m_pending.texture2DPool.erase(it); m_pending.attachmentToTextures.emplace(attachmentIndex, textureId); if (!attachmentData.name.empty() && data.name != attachmentData.name) data.name += " / " + attachmentData.name; return textureId; } std::size_t textureId; FrameGraphTextureData& data = InsertTexture(ImageType::E2D, attachmentIndex, attachmentData, textureId); data.layerCount = 1; CheckExternalTexture(attachmentIndex, data); // Final outputs cannot be reused if (std::find(m_graphOutputs.begin(), m_graphOutputs.end(), attachmentIndex) != m_graphOutputs.end()) data.canReuse = false; return textureId; } else if constexpr (std::is_same_v) { const AttachmentArray& attachmentData = arg; // Fetch from reuse pool if possible for (auto it = m_pending.texture2DArrayPool.begin(); it != m_pending.texture2DArrayPool.end(); ++it) { std::size_t textureId = *it; FrameGraphTextureData& data = m_pending.textures[textureId]; assert(data.type == ImageType::E2D_Array); if (data.format != attachmentData.format || data.width != attachmentData.width || data.height != attachmentData.height || data.size != attachmentData.size || data.layerCount != attachmentData.layerCount) continue; if (data.size == FramePassAttachmentSize::ViewerTargetFactor && data.viewerIndex != attachmentData.viewerIndex) continue; m_pending.texture2DArrayPool.erase(it); m_pending.attachmentToTextures.emplace(attachmentIndex, textureId); if (!attachmentData.name.empty() && data.name != attachmentData.name) data.name += " / " + attachmentData.name; return textureId; } std::size_t textureId; FrameGraphTextureData& data = InsertTexture(ImageType::E2D_Array, attachmentIndex, attachmentData, textureId); data.layerCount = attachmentData.layerCount; CheckExternalTexture(attachmentIndex, data); // Final outputs cannot be reused if (std::find(m_graphOutputs.begin(), m_graphOutputs.end(), attachmentIndex) != m_graphOutputs.end()) data.canReuse = false; return textureId; } else if constexpr (std::is_same_v) { const AttachmentCube& attachmentData = arg; // Fetch from reuse pool if possible for (auto it = m_pending.textureCubePool.begin(); it != m_pending.textureCubePool.end(); ++it) { std::size_t textureId = *it; FrameGraphTextureData& data = m_pending.textures[textureId]; assert(data.type == ImageType::Cubemap); if (data.format != attachmentData.format || data.width != attachmentData.width || data.height != attachmentData.height || data.size != attachmentData.size) continue; if (data.size == FramePassAttachmentSize::ViewerTargetFactor && data.viewerIndex != attachmentData.viewerIndex) continue; m_pending.textureCubePool.erase(it); m_pending.attachmentToTextures.emplace(attachmentIndex, textureId); if (!attachmentData.name.empty() && data.name != attachmentData.name) data.name += " / " + attachmentData.name; return textureId; } std::size_t textureId; FrameGraphTextureData& data = InsertTexture(ImageType::Cubemap, attachmentIndex, attachmentData, textureId); data.layerCount = 1; CheckExternalTexture(attachmentIndex, data); // Final outputs cannot be reused if (std::find(m_graphOutputs.begin(), m_graphOutputs.end(), attachmentIndex) != m_graphOutputs.end()) data.canReuse = false; return textureId; } else if constexpr (std::is_same_v) { const AttachmentLayer& texLayer = arg; // TODO: Reuse texture views from pool? std::size_t parentTextureId = RegisterTexture(texLayer.attachmentId); std::size_t textureId = m_pending.textures.size(); m_pending.attachmentToTextures.emplace(attachmentIndex, textureId); FrameGraphTextureData& data = m_pending.textures.emplace_back(); const FrameGraphTextureData& parentTexture = m_pending.textures[parentTextureId]; data.type = ImageType::E2D; data.format = parentTexture.format; data.width = parentTexture.width; data.height = parentTexture.height; data.size = parentTexture.size; data.viewData = { parentTextureId, texLayer.layerIndex }; data.viewerIndex = parentTexture.viewerIndex; CheckExternalTexture(attachmentIndex, data); return textureId; } else if constexpr (std::is_same_v) { const AttachmentProxy& proxy = arg; std::size_t textureId = RegisterTexture(proxy.attachmentId); m_pending.attachmentToTextures.emplace(attachmentIndex, textureId); if (m_externalTextures.contains(proxy.attachmentId)) throw std::runtime_error("proxy attachments cannot be bound to external textures"); if (std::find(m_graphOutputs.begin(), m_graphOutputs.end(), attachmentIndex) != m_graphOutputs.end()) m_pending.textures[textureId].canReuse = false; return textureId; } else if constexpr (std::is_same_v) { m_pending.attachmentToTextures.emplace(attachmentIndex, InvalidTextureIndex); return InvalidTextureIndex; } else static_assert(AlwaysFalse::value, "non-exhaustive visitor"); }, m_attachments[attachmentIndex]); } void FrameGraph::RemoveDuplicatePasses() { // A way to remove duplicates from a std::vector without sorting it Bitset<> seen(m_framePasses.size(), false); auto itRead = m_pending.passList.begin(); auto itWrite = m_pending.passList.begin(); while (itRead != m_pending.passList.end()) { std::size_t passIndex = *itRead; if (!seen[passIndex]) { seen[passIndex] = true; if (itRead != itWrite) *itWrite++ = passIndex; else ++itWrite; } ++itRead; } m_pending.passList.erase(itWrite, m_pending.passList.end()); } std::size_t FrameGraph::ResolveAttachmentIndex(std::size_t attachmentIndex) const { assert(attachmentIndex < m_attachments.size()); while (const AttachmentProxy* proxy = std::get_if(&m_attachments[attachmentIndex])) attachmentIndex = proxy->attachmentId; return attachmentIndex; } void FrameGraph::ReorderPasses() { /* TODO */ } void FrameGraph::TraverseGraph(std::size_t passIndex) { m_pending.passList.push_back(passIndex); const FramePass& framePass = m_framePasses[passIndex]; for (const auto& input : framePass.GetInputs()) RegisterPassInput(passIndex, input.attachmentId); if (std::size_t dsInput = framePass.GetDepthStencilInput(); dsInput != FramePass::InvalidAttachmentId) RegisterPassInput(passIndex, dsInput); } }