单个Cmd Buffer中高效等待两个VkEvent的方法及代码疑问解答
Vulkan事件同步问题解答
问题背景
场景说明
- 动作1、2、3执行后生成结果OutcomeA,动作4、5、6执行后生成结果OutcomeB
- 动作7、8仅依赖OutcomeA,不依赖OutcomeB
- 动作9仅依赖OutcomeB,不依赖OutcomeA
现有代码
// Three dispatches that don’t have conflicting resource accesses vkCmdDispatch( 1 ); vkCmdDispatch( 2 ); vkCmdDispatch( 3 ); // 4, 5, and 6 don’t share resources with 1, 2, and 3 // No reason for them to be blocked, so set an event to wait for later vkCmdSetEvent( A, srcStageMask = COMPUTE ); vkCmdDispatch( 4 ); vkCmdDispatch( 5 ); vkCmdDispatch( 6 ); // 7 and 8 don’t use the same resources as 4, 5, and 6. So use an event vkCmdSetEvent( B, srcStageMask = COMPUTE ); // 7 and 8 need the results of 1, 2, and 3 // So we’ll wait for them by waiting on A vkCmdWaitEvents( A, dstStageMask = COMPUTE ); vkCmdDispatch( 7 ); vkCmdDispatch( 8 ); // 9 uses the same resources as 4, 5, and 6 so we wait. // Also assumed is that 9 needs nothing from 7 and 8 vkCmdWaitEvents( B, dstStageMask = COMPUTE ); vkCmdDispatch( 9 );
疑问
- 事件B的作用范围是什么?
- 动作9是否会同时等待OutcomeA(动作1、2、3)的结果?
- 若答案为“是”(非预期结果),正确的实现方式是什么?
问题解答
1. 事件B的作用范围
事件B的设计意图是标记OutcomeB(动作4、5、6的执行结果)已就绪,原本是给需要依赖OutcomeB的命令提供同步信号。但在当前代码的命令流中,vkCmdSetEvent(B)被放在动作4-6之后、动作7-8的等待命令之前,导致它的触发时机虽然是动作4-6完成后,但后续的命令执行顺序会让它的同步逻辑偏离预期。
2. 动作9是否会等待OutcomeA的结果?
是,这属于非预期结果。Vulkan的命令队列严格按照提交顺序执行,vkCmdWaitEvents(B)排在vkCmdWaitEvents(A)和动作7-8的vkCmdDispatch之后,所以动作9必须等前面所有命令(包括等待OutcomeA完成、执行动作7-8)全部结束后才会启动,相当于间接等待了OutcomeA的结果,完全违背了“动作9仅依赖OutcomeB”的需求。
3. 正确的实现方式
要实现动作7-8仅等OutcomeA、动作9仅等OutcomeB,且两者可以并行执行,有两种常用方案:
方案一:调整单命令缓冲区的命令顺序
把动作9的等待和执行逻辑移到动作7-8的等待命令之前,这样vkCmdWaitEvents(B)只会等待事件B(即动作4-6完成),不会被动作7-8的执行阻塞:
// 执行动作1-3,标记OutcomeA就绪 vkCmdDispatch( 1 ); vkCmdDispatch( 2 ); vkCmdDispatch( 3 ); vkCmdSetEvent( A, srcStageMask = COMPUTE ); // 执行动作4-6,标记OutcomeB就绪 vkCmdDispatch( 4 ); vkCmdDispatch( 5 ); vkCmdDispatch( 6 ); vkCmdSetEvent( B, srcStageMask = COMPUTE ); // 动作9仅依赖OutcomeB,等待B后执行 vkCmdWaitEvents( B, dstStageMask = COMPUTE ); vkCmdDispatch( 9 ); // 动作7-8仅依赖OutcomeA,等待A后执行 vkCmdWaitEvents( A, dstStageMask = COMPUTE ); vkCmdDispatch( 7 ); vkCmdDispatch( 8 );
这种方式下,动作9和动作7-8可以在硬件支持的情况下并行执行,各自只等待自己需要的结果。
方案二:使用多命令缓冲区并行提交
将不同依赖链的命令拆分到独立的命令缓冲区,然后一起提交到队列,实现真正的并行执行:
// 命令缓冲区1:完成动作1-3、4-6并设置事件 VkCommandBuffer cmdBuf1 = ...; // 初始化命令缓冲区 vkCmdBegin(cmdBuf1, ...); vkCmdDispatch(cmdBuf1, 1); vkCmdDispatch(cmdBuf1, 2); vkCmdDispatch(cmdBuf1, 3); vkCmdSetEvent(cmdBuf1, A, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); vkCmdDispatch(cmdBuf1, 4); vkCmdDispatch(cmdBuf1, 5); vkCmdDispatch(cmdBuf1, 6); vkCmdSetEvent(cmdBuf1, B, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT); vkCmdEnd(cmdBuf1); // 命令缓冲区2:等待OutcomeA,执行动作7-8 VkCommandBuffer cmdBuf2 = ...; vkCmdBegin(cmdBuf2, ...); VkEvent waitEventsA[] = {A}; vkCmdWaitEvents(cmdBuf2, 1, waitEventsA, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, nullptr, 0, nullptr); vkCmdDispatch(cmdBuf2, 7); vkCmdDispatch(cmdBuf2, 8); vkCmdEnd(cmdBuf2); // 命令缓冲区3:等待OutcomeB,执行动作9 VkCommandBuffer cmdBuf3 = ...; vkCmdBegin(cmdBuf3, ...); VkEvent waitEventsB[] = {B}; vkCmdWaitEvents(cmdBuf3, 1, waitEventsB, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, nullptr, 0, nullptr); vkCmdDispatch(cmdBuf3, 9); vkCmdEnd(cmdBuf3); // 提交三个命令缓冲区到队列 VkSubmitInfo submitInfos[3] = {}; submitInfos[0].sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfos[0].commandBufferCount = 1; submitInfos[0].pCommandBuffers = &cmdBuf1; submitInfos[1].sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfos[1].commandBufferCount = 1; submitInfos[1].pCommandBuffers = &cmdBuf2; submitInfos[2].sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfos[2].commandBufferCount = 1; submitInfos[2].pCommandBuffers = &cmdBuf3; vkQueueSubmit(queue, 3, submitInfos, nullptr);
这种方案更灵活,适合复杂的依赖场景,能最大化利用硬件并行能力。
内容的提问来源于stack exchange,提问作者alex
相关产品推荐
相关产品推荐

