如何在Vulkan API中仅在首个命令缓冲中清除屏幕?
问题背景
我尝试用Vulkan API实现多线程渲染,从其他线程创建并执行多个命令缓冲,但每个命令缓冲都执行屏幕清除操作,导致画面无法正常显示。
原代码片段:
VkCommandBufferBeginInfo beginInfo{}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; VkRenderPassBeginInfo renderPassInfo{}; renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; renderPassInfo.renderPass = m_pRenderPass; renderPassInfo.framebuffer = m_swapChainFrameBufferPtrs[m_nImageIndex]; renderPassInfo.renderArea.offset = { 0, 0 }; renderPassInfo.renderArea.extent = { m_nWidth, m_nHeight }; std::array<VkClearValue, 2> clearValues{}; clearValues[0].color = { {0.0f, 0.0f, 0.0f, 1.0f} }; clearValues[1].depthStencil = { 1.0f, 0 }; renderPassInfo.clearValueCount = static_cast<uint32_t>(clearValues.size()); renderPassInfo.pClearValues = clearValues.data(); VkViewport viewport{...}; VkRect2D scissor{...}; CFrameCommandBuffer& currentFrame = m_frames[m_nCurrentFrame]; for (std::uint32_t n = 0; n < currentFrame.m_commandBufferPtrs.size(); ++n) { auto& pCmdBuffer = currentFrame.m_commandBufferPtrs[n]; vkResetCommandBuffer(pCmdBuffer, 0); vkBeginCommandBuffer(pCmdBuffer, &beginInfo); vkCmdBeginRenderPass(pCmdBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdSetViewport(pCmdBuffer, 0, 1, &viewport); vkCmdSetScissor(pCmdBuffer, 0, 1, &scissor); }
尝试修改第二个及后续命令缓冲不执行清除时,设置:
renderPassInfo.clearValueCount = 0; renderPassInfo.pClearValues = nullptr;
触发错误:
In vkCmdBeginRenderPass the VkRenderPassBeginInfo struct has a clearValueCount of 0 but there must be at least 2 entries in pClearValues array to account for the highest index attachment in VkRenderPass 0x1ebe0f000000012a[] that uses VK_ATTACHMENT_LOAD_OP_CLEAR is 2. Note that the pClearValues array is indexed by attachment number so even if some pClearValues entries between 0 and 1 correspond to attachments that aren't cleared they will be ignored.
核心原因
Vulkan的RenderPass规则是:只要RenderPass中附件的加载操作设置为VK_ATTACHMENT_LOAD_OP_CLEAR,任何启动该RenderPass的命令缓冲都必须提供足够数量的ClearValue——因为清除操作是RenderPass启动阶段的全局行为,无法在单个命令缓冲中跳过。
正确实现方案
方案一:主命令缓冲启动RenderPass,二级命令缓冲执行绘制逻辑
通过主命令缓冲统一启动RenderPass并执行清除,其他子命令缓冲作为二级缓冲,仅录制RenderPass内的绘图逻辑,最后由主缓冲调用执行。
代码调整示例:
// 主命令缓冲(首个缓冲) auto& mainCmdBuffer = currentFrame.m_commandBufferPtrs[0]; vkResetCommandBuffer(mainCmdBuffer, 0); vkBeginCommandBuffer(mainCmdBuffer, &beginInfo); // 启动RenderPass并执行清除,指定使用二级命令缓冲 vkCmdBeginRenderPass(mainCmdBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS); // 录制二级命令缓冲 std::vector<VkCommandBuffer> secondaryBuffers; for (uint32_t n = 1; n < currentFrame.m_commandBufferPtrs.size(); ++n) { auto& subCmdBuffer = currentFrame.m_commandBufferPtrs[n]; vkResetCommandBuffer(subCmdBuffer, 0); // 二级缓冲必须指定继承信息,关联到目标RenderPass VkCommandBufferBeginInfo subBeginInfo{}; subBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; subBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT; VkCommandBufferInheritanceInfo inheritInfo{}; inheritInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_INHERITANCE_INFO; inheritInfo.renderPass = m_pRenderPass; inheritInfo.subpass = 0; subBeginInfo.pInheritanceInfo = &inheritInfo; vkBeginCommandBuffer(subCmdBuffer, &subBeginInfo); vkCmdSetViewport(subCmdBuffer, 0, 1, &viewport); vkCmdSetScissor(subCmdBuffer, 0, 1, &scissor); // 录制子缓冲的绘制逻辑... vkEndCommandBuffer(subCmdBuffer); secondaryBuffers.push_back(subCmdBuffer); } // 主缓冲中执行所有二级缓冲 vkCmdExecuteCommands(mainCmdBuffer, secondaryBuffers.size(), secondaryBuffers.data()); vkCmdEndRenderPass(mainCmdBuffer); vkEndCommandBuffer(mainCmdBuffer);
方案二:修改RenderPass加载模式,手动执行清除
将RenderPass的附件加载操作改为VK_ATTACHMENT_LOAD_OP_LOAD,然后在首个命令缓冲中手动调用清除指令,后续缓冲直接加载已有画面内容。
1. 重新创建RenderPass
// 颜色附件修改加载模式 VkAttachmentDescription colorAttachment{}; colorAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD; // 替换原VK_ATTACHMENT_LOAD_OP_CLEAR // 深度模板附件同理 VkAttachmentDescription depthAttachment{}; depthAttachment.loadOp = VK_ATTACHMENT_LOAD_OP_LOAD;
2. 命令缓冲录制逻辑
for (std::uint32_t n = 0; n < currentFrame.m_commandBufferPtrs.size(); ++n) { auto& pCmdBuffer = currentFrame.m_commandBufferPtrs[n]; vkResetCommandBuffer(pCmdBuffer, 0); vkBeginCommandBuffer(pCmdBuffer, &beginInfo); vkCmdBeginRenderPass(pCmdBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); vkCmdSetViewport(pCmdBuffer, 0, 1, &viewport); vkCmdSetScissor(pCmdBuffer, 0, 1, &scissor); // 仅首个缓冲执行手动清除 if (n == 0) { VkClearAttachment clearAttachments[2]; // 清除颜色附件 clearAttachments[0].attachment = 0; clearAttachments[0].aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; clearAttachments[0].clearValue = clearValues[0]; // 清除深度模板附件 clearAttachments[1].attachment = 1; clearAttachments[1].aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT | VK_IMAGE_ASPECT_STENCIL_BIT; clearAttachments[1].clearValue = clearValues[1]; VkClearRect clearRect{}; clearRect.rect = { {0,0}, {m_nWidth, m_nHeight} }; clearRect.baseArrayLayer = 0; clearRect.layerCount = 1; vkCmdClearAttachments(pCmdBuffer, 2, clearAttachments, 1, &clearRect); } // 录制绘制逻辑... vkCmdEndRenderPass(pCmdBuffer); vkEndCommandBuffer(pCmdBuffer); }
注意事项
- 方案一中,二级命令缓冲必须以
VK_COMMAND_BUFFER_LEVEL_SECONDARY的级别创建,否则无法被主缓冲调用。 - 方案二中,使用
VK_ATTACHMENT_LOAD_OP_LOAD时,需确保交换链图像的初始状态有效,或在RenderPass中正确处理图像布局转换。
内容的提问来源于stack exchange,提问作者Fobes Misterio

