PAYATHON 2026

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 相关共享规则

备注:内容仅供参考。