You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Vulkan中如何在多次Render Pass间保留Attachment内容?

解决方案:多次Render Pass间保留Attachment数据

你的核心痛点是多次启动Render Pass时重复清除同一个交换链图像,因为初始设置了VK_ATTACHMENT_LOAD_OP_CLEAR,但只需要第一次清除。结合你无法使用子通道(渲染器数量未知)的限制,有几种可行的思路:

1. 动态调整Render Pass的Load Op(两种实现方式)

方式A:创建两个Render Pass,区分首次和后续渲染

Vulkan中,Render Pass的Attachment描述是固定的,所以你需要预先创建两个Render Pass:

  • 第一个Render Pass的颜色附件loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR,用于首次清除图像
  • 第二个Render Pass的颜色附件loadOp = VK_ATTACHMENT_LOAD_OP_LOAD,用于后续读取已有图像内容,不清除

然后在循环中判断是否是首次渲染,切换使用对应的Render Pass:

VkCommandBuffer cb{...}; // 获取主命令缓冲

// 先将交换链图像从Present布局转为颜色附件布局(必做步骤)
VkImageMemoryBarrier layoutBarrier{};
layoutBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
layoutBarrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
layoutBarrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
layoutBarrier.image = yourSwapchainImage;
layoutBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
layoutBarrier.srcAccessMask = 0;
layoutBarrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, 
                     VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 0,
                     0, nullptr, 0, nullptr, 1, &layoutBarrier);

bool isFirstRender = true;
for(auto& renderer : renderers) {
    VkRenderPassBeginInfo rpBeginInfo = get_render_pass_begin_info(...);
    // 切换Render Pass
    rpBeginInfo.renderPass = isFirstRender ? rpWithClear : rpWithLoad;
    if (!isFirstRender) {
        rpBeginInfo.pClearValues = nullptr; // 后续渲染不需要清除值
    }

    vkCmdBeginRenderPass(cb, &rpBeginInfo, VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS);
    auto [rpCmd, postRpCmd] = renderer->getCommandBuffers(); // 假设返回两个次级缓冲
    vkCmdExecuteCommands(cb, 1, &rpCmd);
    vkCmdEndRenderPass(cb);
    vkCmdExecuteCommands(cb, 1, &postRpCmd);

    isFirstRender = false;
}

// 渲染完成后将图像转回Present布局
VkImageMemoryBarrier presentBarrier{};
presentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
presentBarrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
presentBarrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
presentBarrier.image = yourSwapchainImage;
presentBarrier.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1};
presentBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
presentBarrier.dstAccessMask = 0;
vkCmdPipelineBarrier(cb, VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, 
                     VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0,
                     0, nullptr, 0, nullptr, 1, &presentBarrier);

方式B:使用动态渲染扩展(VK_KHR_dynamic_rendering)

如果你的Vulkan版本支持(或可以启用)VK_KHR_dynamic_rendering扩展,就不需要预先创建Render Pass,而是在每次启动渲染时动态指定附件的Load Op,灵活性更高:

VkCommandBuffer cb{...};

// 同样先做布局转换(代码同上)

bool isFirstRender = true;
for(auto& renderer : renderers) {
    // 动态配置颜色附件
    VkRenderingAttachmentInfoKHR colorAttach{};
    colorAttach.sType = VK_STRUCTURE_TYPE_RENDERING_ATTACHMENT_INFO_KHR;
    colorAttach.imageView = yourSwapchainImageView;
    colorAttach.imageLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
    colorAttach.loadOp = isFirstRender ? VK_ATTACHMENT_LOAD_OP_CLEAR : VK_ATTACHMENT_LOAD_OP_LOAD;
    colorAttach.storeOp = VK_ATTACHMENT_STORE_OP_STORE;
    if (isFirstRender) {
        colorAttach.clearValue = {.color = {0.0f, 0.0f, 0.0f, 1.0f}}; // 自定义初始清除颜色
        isFirstRender = false;
    }

    // 启动动态渲染
    VkRenderingInfoKHR renderingInfo{};
    renderingInfo.sType = VK_STRUCTURE_TYPE_RENDERING_INFO_KHR;
    renderingInfo.renderArea = {.offset = {0,0}, .extent = yourSwapchainExtent};
    renderingInfo.layerCount = 1;
    renderingInfo.colorAttachmentCount = 1;
    renderingInfo.pColorAttachments = &colorAttach;

    vkCmdBeginRenderingKHR(cb, &renderingInfo);
    auto [rpCmd, postRpCmd] = renderer->getCommandBuffers();
    vkCmdExecuteCommands(cb, 1, &rpCmd);
    vkCmdEndRenderingKHR(cb);
    vkCmdExecuteCommands(cb, 1, &postRpCmd);
}

// 转回Present布局(代码同上)

2. 你的初始思路:合并Render Pass,收集外部命令

你提到的把vkCmdBeginRenderPassvkCmdEndRenderPass移到循环外,收集所有需要在Render Pass外执行的次级缓冲,这个方案其实更高效——因为减少了多次启动/结束Render Pass的开销,适合所有渲染器的Render Pass配置完全一致的场景:

VkCommandBuffer cb{...};

// 布局转换(代码同上)

// 只启动一次Render Pass
VkRenderPassBeginInfo rpBeginInfo = get_render_pass_begin_info(...);
vkCmdBeginRenderPass(cb, &rpBeginInfo, VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS);

std::vector<VkCommandBuffer> postRenderCmds;
for(auto& renderer : renderers) {
    auto [rpCmd, postRpCmd] = renderer->getCommandBuffers();
    // 在Render Pass内执行第一个缓冲
    vkCmdExecuteCommands(cb, 1, &rpCmd);
    // 收集需要在外部执行的缓冲
    postRenderCmds.push_back(postRpCmd);
}

vkCmdEndRenderPass(cb);

// 统一执行所有外部命令
for(auto cmd : postRenderCmds) {
    vkCmdExecuteCommands(cb, 1, &cmd);
}

// 转回Present布局(代码同上)

方案选择建议

  • 如果你的渲染器可能需要不同的Render Pass配置(比如有的需要深度缓冲,有的不需要),优先选动态渲染或双Render Pass方案
  • 如果所有渲染器的Render Pass配置完全一致,合并Render Pass+收集外部命令的方案性能更好,代码也更简洁

内容的提问来源于stack exchange,提问作者nikitablack

火山引擎 最新活动