Vulkan 如何管理多个 queue family 使用的资源?
结论
VK_SHARING_MODE_CONCURRENT 可以省去指定 queue family 之间的显式所有权转移,但不能保证所有设备都没有性能代价,也不能省去必要的同步。
对于已经执行的 release,如果需要保留资源内容,应完成与之匹配的 acquire;如果允许丢弃旧内容,可以跳过 acquire,重新取得所有权并初始化资源。等待几帧不会自动转移所有权。
以下讨论普通设备内部的资源访问,采用未启用 maintenance9 时的所有权规则。启用该特性后的例外见末节。
1. CONCURRENT 是否真的没有额外开销?
问题中引用的说法可以在 NVIDIA 的 Vulkan Dos and Don’ts 中找到,应按该厂商的实现建议理解。Vulkan 规范明确允许 VK_SHARING_MODE_CONCURRENT 的资源访问性能低于 VK_SHARING_MODE_EXCLUSIVE,因此不能保证它在所有厂商和设备上都没有开销。VkSharingMode 参考
两种模式的区别是:
EXCLUSIVE:同一 buffer range 或 image subresource range 在同一时刻由一个 queue family 拥有。跨族访问若要保留内容,通常需要转移所有权。CONCURRENT:创建资源时,通过queueFamilyIndexCount和pQueueFamilyIndices指定可访问它的 queue family,这些族之间无需显式所有权转移。
使用 CONCURRENT 后,仍需处理读写依赖、跨队列执行顺序和 image layout transition。例如,A 写入后由 B 读取,仍需通过 semaphore 等机制建立正确的依赖。仅在两个队列中各放一个普通 pipeline barrier,无法建立跨队列同步。Vulkan 队列指南
实现初版 render graph 时,可以为确实需要跨族访问的资源选择 CONCURRENT,以简化所有权管理,再在目标设备上测量性能。没有实测依据,不应将它视为免费优化。
2. render graph 重编译后,孤立的 release 怎么处理?
如何处理取决于 release 是否已提交,以及后续是否需要旧内容。
release 尚未提交
如果它只存在于旧图的计划或未提交的 command buffer 中,可以放弃旧计划并重新生成 barrier。记录命令本身不会改变设备上的资源状态。
已经提交的命令仍须纳入状态跟踪。即使新图不再包含这些命令,也不能视为已取消;处于 pending 状态的 command buffer 也不能直接重置。vkQueueSubmit2 参考
release 已执行,后续需要保留内容
应在原定目标 queue family 上执行匹配的 acquire,并确保 release happens-before acquire。
例如,旧计划已经执行了 A→B 的 release,而新图决定继续由 A 读取旧数据,可以先完成 B 上的 acquire,再执行 B→A 的 release/acquire。如果改由 C 使用,同样可以先完成 A→B 的转移,再转移 B→C。
配对时,buffer 或 image、范围以及源和目标 queue family 必须匹配。两个 barrier 的方向都填写 A→B,acquire 端不能反写成 B→A。
release 已执行,但允许丢弃内容
规范允许跳过匹配的 acquire,之后在任意队列上重新使用资源,包括原来的释放队列,但必须满足以下条件:
- 旧内容视为未定义,读取前必须初始化实际会读取的区域。
- image 必须从
VK_IMAGE_LAYOUT_UNDEFINED转换到后续使用所需的 layout。 - 仍需正确同步此前在途工作,避免新的写入或 layout transition 与旧操作重叠。
- 一旦选择重新初始化,就不能再执行旧 release 对应的 acquire;后续 acquire 需要新的匹配 release。
这条路径适用于可以丢弃的临时数据,不能用于保留历史帧结果。所有权转移规范
3. 所有权是否会跨帧保留?
会。EXCLUSIVE 资源的所有权按 queue family 跟踪,直到被后续所有权操作改变。帧号、CPU 等待时间和 GPU 空闲都不会自动将所有权交回 A。
还应先检查 Queue A 和 Queue B 的 family index:
- 如果属于同一个 queue family,无需队列族所有权转移,但不同队列之间仍可能需要同步。
- 如果属于不同 queue family,要保留资源内容,通常需要显式转移。
新创建的 EXCLUSIVE 资源最初没有所属 queue family,首次使用会隐式取得所有权。资源共享模式规范
4. release/acquire barrier 如何配对?
下面以 buffer 为例,演示它从 A 的 transfer 写入转为 B 的 compute shader 读取。假设:
- A、B 属于不同 queue family,分别支持对应操作。
- buffer 使用
VK_SHARING_MODE_EXCLUSIVE,且具备 transfer destination 和 storage buffer 用途。 - 已启用
synchronization2,示例使用 Vulkan 1.3 的 API 名称。 - 此次需要显式所有权转移,且要保留写入内容。
这里只展示 barrier 录制过程,变量代表已创建的 Vulkan 对象:
// cmdA: after the transfer write.
VkBufferMemoryBarrier2 release{};
release.sType = VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2;
release.srcStageMask = VK_PIPELINE_STAGE_2_TRANSFER_BIT;
release.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT;
release.dstStageMask = VK_PIPELINE_STAGE_2_NONE;
release.dstAccessMask = VK_ACCESS_2_NONE;
release.srcQueueFamilyIndex = familyA;
release.dstQueueFamilyIndex = familyB;
release.buffer = buffer;
release.offset = 0;
release.size = VK_WHOLE_SIZE;
VkDependencyInfo releaseInfo{};
releaseInfo.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
releaseInfo.bufferMemoryBarrierCount = 1;
releaseInfo.pBufferMemoryBarriers = &release;
vkCmdPipelineBarrier2(cmdA, &releaseInfo);
// cmdB: before the compute shader reads the buffer.
VkBufferMemoryBarrier2 acquire = release;
acquire.srcStageMask = VK_PIPELINE_STAGE_2_NONE;
acquire.srcAccessMask = VK_ACCESS_2_NONE;
acquire.dstStageMask = VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT;
acquire.dstAccessMask = VK_ACCESS_2_SHADER_READ_BIT;
VkDependencyInfo acquireInfo{};
acquireInfo.sType = VK_STRUCTURE_TYPE_DEPENDENCY_INFO;
acquireInfo.bufferMemoryBarrierCount = 1;
acquireInfo.pBufferMemoryBarriers = &acquire;
vkCmdPipelineBarrier2(cmdB, &acquireInfo);
release 端填写此前写入对应的源作用域,acquire 端填写后续读取对应的目标作用域。VkBufferMemoryBarrier2 参考
提交时还需要建立如下依赖:
Queue A: transfer write -> release -> signal semaphore
Queue B: wait semaphore -> acquire -> compute read
对于这里采用的默认所有权转移规则,使用 vkQueueSubmit2 时,semaphore signal 和 wait 的 VkSemaphoreSubmitInfo::stageMask 都可设为 VK_PIPELINE_STAGE_2_ALL_COMMANDS_BIT,以覆盖 release/acquire。不能只根据 B 后续的 shader 阶段随意缩窄等待范围。
转移 image 时,如果同时改变 layout,release 和 acquire 两侧应填写相同的 oldLayout、newLayout 和 subresourceRange。例如两侧都写 TRANSFER_DST_OPTIMAL→SHADER_READ_ONLY_OPTIMAL;该转换在转移过程中只执行一次。VkImageMemoryBarrier2 参考
5. render graph 应该记录哪些状态?
建议将资源的运行时状态独立于单次图编译结果保存,并按 buffer range 或 image subresource range 跟踪:
- 当前 owner queue family。
- 最近访问的 stage/access,以及 image 的 layout。
- 是否存在已提交的 release,其源族、目标族和范围。
- 已提交工作对应的完成条件,例如 timeline semaphore value。
- 内容是否需要保留,是否允许重新初始化。
对于图重编译,新图应从已提交工作的预期结果继续推导 barrier,不能因为重新编译就把持久资源恢复成“首次使用”状态。也可以等到下一个消费者确定后再生成跨族 release,减少帧末提前释放后又改变计划的情况。
还需留意 maintenance9:启用该特性后,同一逻辑设备内的 buffer 和 linear image 在跨族隐式取得所有权时可以保留内容;部分 optimal image 也适用,但受 usage 和 queue family 属性限制。该特性不会取消数据同步要求,使用前应查询特性及适用条件。maintenance9 相关共享规则
备注:内容仅供参考。
