同步
同步是 Vulkan 最强大但也最复杂的内容之一,在vulkan中,由应用开发人员负责使用各种 Vulkan 同步原语来管理同步。同步使用不当会导致难以发现的错误,而且可能会让GPU产生不必要地等待而影响性能。
Khronos 提供了一组示例和文章了解 Vulkan 同步,介绍了如何使用一些同步原语。此外,还有 Tobias Hector 在 Vulkan 演讲中的内容:[ppt第1部分](https://www.khronos.org/assets/uploads/developers/library/2017-vulkan-devu-vancouver/009 - Synchronization - Keeping Your Device Fed.pdf)(视频1)和[ppt第2部分](https://www.khronos.org/assets/uploads/developers/library/2018-vulkanised/06-Keeping Your Device Fed v4_Vulkanised2018.pdf)(视频2)。
下图是VkEvent
、 VkFence
和 VkSemaphore
之间差异的示例图:
校验
Khronos 校验层已经实现了一些同步验证,可以通过 Vulkan SDK 附带的 Vulkan Configurator来启用。这篇 Khronos 博客也讨论了同步验证白皮书的内容。
管线屏障
Pipeline Barriers用于在执行命令缓冲区时控制管线的哪些步骤等待前面的步骤:
Pipeline Barriers 一开始可能很难理解,下面一些 Khronos 演讲和文章对其进行了深入地讲解:
- [Vulkanised 2018 - 管道屏障的底层之谜](https://www.khronos.org/assets/uploads/developers/library/2018-vulkanised/05-The low-level mysteries of pipeline barriers_Vulkanised2018.pdf)(视频)
- [Vulkanised 2019 - Live Long and Optimize](https://www.khronos.org/assets/uploads/developers/library/2019-vulkanised/02_Live Long And Optimise-May19.pdf) (视频)从ppt12 开始
- 博文:Vulkan 屏障解释
- 博文:解释 Vulkan 同步
VK_KHR_synchronization2
VK_KHR_synchronization2
扩展对原始的核心同步 API 进行了修改,降低了应用程序开发的难度,并添加了一些附加功能。
VK_KHR_synchronization2
在 Vulkan 1.3 中提升为核心扩展
该扩展对管道屏障、事件、图像布局转换和队列提交进行了改进。下文介绍了 Vulkan 原始同步操作与VK_KHR_synchronization2
扩展功能的区别,还有一些示例说明了应用程序代码如何使用该扩展。
重新设计 Pipeline 阶段和 Access Flag
该扩展的一个主要变化是在 memory barrier 结构中加入了 pipeline stages 和 access flags,使得两者之间的联系更加明显。
增加的新型结构体VkDependencyInfoKHR
将所有屏障包装到一起:
添加设置事件的屏障
随着VkDependencyInfoKHR
的 引入,vkCmdSetEvent2KHR
与vkCmdSetEvent
相比,增加了添加屏障的能力,这使得VkEvent
更有用途。由于 synchronization2 实现的VkEvent
可能与 Vulkan 1.2 有很大差异,因此严禁将扩展和核心 API 调用的VkEvent
混合。例如,不能先调用vkCmdSetEvent2KHR()
,然后再调用vkCmdWaitEvents()
。
复用相同的管线阶段和访问标志
由于32位的VkAccessFlag
用完而创建了64位的VkAccessFlags2KHR
。为了防止VkPipelineStageFlags
出现同样的问题,还创建了64 位的VkPipelineStageFlags2KHR
。
并非所有的 C/C++ 编译器都提供了64位枚举类型,因此新字段使用了static const
值而不是枚举。所以没有VkPipelineStageFlagBits
和VkAccessFlagBits
的等效类型。一些函数例如vkCmdWriteTimestamp()
只能传入具体值,而不是多个位的掩码。这些函数需要转换为使用Flags
类型,所以应用代码需进行适当的调整,就像vkCmdWriteTimestamp2KHR()
。
新标志包含了与原始同步标志相同的位,具有相同的基名称和值。 旧标志可以直接在新 API 中使用,这 2 个示例显示了命名差异:
VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT
转化为VK_PIPELINE_STAGE_2_COMPUTE_SHADER_BIT_KHR
VK_ACCESS_SHADER_READ_BIT
转化为VK_ACCESS_2_SHADER_READ_BIT_KHR
VkSubpassDependency
VkSubpassDependency
的管线阶段和访问标志转换到时,需使用VkMemoryBarrier2KHR
传入VkSubpassDependency2
的pNext
。
例如:
// Without VK_KHR_synchronization2
VkSubpassDependency dependency = {
.srcSubpass = 0,
.dstSubpass = 1,
.srcStageMask = VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT |
VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT,
.dstStageMask = VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
.srcAccessMask = VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT,
.dstAccessMask = VK_ACCESS_INPUT_ATTACHMENT_READ_BIT,
.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT
};
转化为:
// With VK_KHR_synchronization2
VkMemoryBarrier2KHR memoryBarrier = {
.sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER_2_KHR,
.pNext = nullptr,
.srcStageMask = VK_PIPELINE_STAGE_2_EARLY_FRAGMENT_TESTS_BIT_KHR |
VK_PIPELINE_STAGE_2_LATE_FRAGMENT_TESTS_BIT_KHR,
.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT_KHR,
.srcAccessMask = VK_ACCESS_2_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT_KHR,
.dstAccessMask = VK_ACCESS_2_INPUT_ATTACHMENT_READ_BIT_KHR
};
// The 4 fields unset are ignored according to the spec
// When VkMemoryBarrier2KHR is passed into pNext
VkSubpassDependency2 dependency = {
.sType = VK_STRUCTURE_TYPE_SUBPASS_DEPENDENCY_2,
.pNext = &memoryBarrier,
.srcSubpass = 0,
.dstSubpass = 1,
.dependencyFlags = VK_DEPENDENCY_BY_REGION_BIT
};
拆分管线阶段和访问掩码
一些VkAccessFlags
和VkPipelineStageFlags
的值在硬件中目标不明确,新的VkAccessFlags2KHR
和VkPipelineStageFlags2KHR
将这些值的场景分开,同时保留旧值以兼容旧代码。
拆分 VK_PIPELINE_STAGE_VERTEX_INPUT_BIT
VK_PIPELINE_STAGE_VERTEX_INPUT_BIT
(现在是VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT_KHR
),以前索引输入和顶点输入组合在一个管线阶段,现在各自拆分为1个新阶段:
VK_PIPELINE_STAGE_2_INDEX_INPUT_BIT_KHR
VK_PIPELINE_STAGE_2_VERTEX_ATTRIBUTE_INPUT_BIT_KHR