提醒:本文不含任何图,读起来可能有些枯燥,如果对Vulkan中的同步还没有任何了解的,建议先参考后面给出的链接 [1][2][3]
在Vulkan中,对资源读写所需要做的同步是应用程序的职责,Vulkan本身只提供了很少的隐式同步机制,其余的都需要在程序中显式地使用Vulkan中的同步机制来实现。
提交顺序
提交顺序是Vulkan中的一个非常基本的概念,它本身并不具有任何同步的意义,但是不管是Vulkan提供的隐式同步,还是用户要自己实现的显式同步,都要以这个精确的概念为前提。
在Vulkan中,用户需要将命令写入CommandBuffer中,然后把一个或多个CommandBuffer写入到一个或多个VkSubmitInfo中,再把一个或多个VkSubmitInfo传给vkQueueSubmit,让Queue开始执行传入的命令,由此,从高往低,提交顺序为:
1.在CPU上通过多次vkQueueSubmit提交了一系列命令,这些命令的提交顺序为调用vkQueueSubmit从前往后的顺序,即先通过vkQueueSubmit提交的命令一定在后通过vkQueueSubmit提交的命令之前。
2.在同一次vkQueueSubmit中,传入了一个或多个VkSubmitInfo,这些VkSubmitInfo中的命令,按照VkSubmitInfo的下标顺序排列,即在pSubmits所指向的VkSubmitInfo数组中,下标靠前的VkSubmitInfo中所记录的所有命令都在下标靠后的VkSubmitInfo中所记录的所有命令之前。
3.在同一个VkSubmitInfo中,填入了一个或多个CommandBuffer,这些CommandBuffer中的命令的提交顺序为按照这些CommandBuffer的下标顺序,类似2中的顺序。
4.在同一个CommandBuffer中,所记录的命令分为两种:
一是不在RenderPass中的命令,即除去所有在vkCmdBeginRenderPass和vkCmdEndRenderPass之间的命令,这些命令的提交顺序为按照在CPU上写入CommandBuffer时的顺序。
二是在RenderPass中的命令,在RenderPass中的命令,只定义在同一SubPass中的其他命令的提交顺序,这些命令的提交顺序也是按照在CPU上写入CommandBuffer时的顺序。注意,如果几个命令在vkCmdBeginRenderPass和vkCmdEndRenderPass之间,但是它们不在同一SubPass中,那么它们之间是不存在任何提交顺序的。
Vulkan提供的隐式同步
有了提交顺序的概念,就可以定义一些隐式的同步机制,即不需要用户自己去实现,一定会默认遵循的同步。
Spec中提到的隐式同步有:
1.所有的Action类命令(Draw、Transfer、Clear、Copy等)以及显示地使用同步机制的命令(这个在之后会介绍),这些命令在执行VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT时,会遵循提交顺序。即这些命令开始执行的顺序,是严格遵循提交顺序的。(但这并不意味着这些命令结束执行时的顺序会有什么约束,所有的这些命令,到底是哪个先结束,隐式同步并没有严格的规定,也就是说任何一个命令都有可能最先结束。)
2.所有的设置状态类的命令(bind pipelines, descriptor sets, and buffers等),由于它们不需要在GPU上执行,它们只负责设置CPU上相应CommandBuffer的状态,所以它们的执行顺序,遵循它们在CPU上写入CommandBuffer时的顺序。
3.所有的Draw类命令在处理Primitive时,首先遵循提交顺序,即先提交的Draw中的Primitive会先被处理。而在一个Draw内所提交的Primitive,会按照顶点和索引的下标顺序执行。
4.ImageLayout的转移,是通过ImageMemoryBarrier实现的(也是一种显式的同步原语),它们遵循提交顺序,即先提交的先转移。
以上差不多就是Vulkan中所提供的隐式同步了,还有一些细节可以参考Spec6.2中相关内容。
其他的所有需要用到同步的情景(比如写后写问题,写后读问题,读后写问题),都需要手动地通过显式的同步机制来实现。
Vulkan同步的基本概念
在使用同步类命令时,往往会填一些让人不太容易理解的参数,比如看pipeline barrier以及image memory barrier 的参数:
void vkCmdPipelineBarrier(
VkCommandBuffer commandBuffer,
VkPipelineStageFlags srcStageMask,
VkPipelineStageFlags dstStageMask,
VkDependencyFlags dependencyFlags,
uint32_t memoryBarrierCount,
const VkMemoryBarrier* pMemoryBarriers,
uint32_t bufferMemoryBarrierCount,
const VkBufferMemoryBarrier* pBufferMemoryBarriers,
uint32_t imageMemoryBarrierCount,
const VkImageMemoryBarrier* pImageMemoryBarriers);
typedef struct VkImageMemoryBarrier {
VkStructureType sType;
const void* pNext;
VkAccessFlags srcAccessMask;
VkAccessFlags dstAccessMask;
VkImageLayout oldLayout;
VkImageLayout newLayout;
uint32_t srcQueueFamilyIndex;
uint32_t dstQueueFamilyIndex;
VkImage image;
VkImageSubresourceRange subresourceRange;
} VkImageMemoryBarrier;
可以看到这里面有很多莫名其妙的参数,比如VkPipelineStageFlags、VkAccessFlags、VkImageLayout等,初看时,可能会认为这些东西和同步根本没有任何关系,所谓同步,是想让一个命令的某个过程,一定要等待另一个命令的某个过程完成后才能开始执行,所以这些参数有什么意义呢?
VkPipelineStageFlags
Vulkan中并不提供任何命令级别的同步,即明确地指定某两个命令之间需要满足什么同步。所有需要同步的命令,都会将其执行过程划分为若干个阶段,所有的命令都会在流水线上执行,只是不同类型的命令,它们的阶段划分是不同的。当我们在Vulkan中使用同步机制时,都是以流水线阶段为单位,即某个流水线阶段上执行的所有命令,会在当前阶段暂停,等待另一个流水线阶段上的所有命令在相应的阶段执行完全后,再开始执行。VkPipelineStageFlags就代表流水线阶段,在Spec 6.1.2节中给出了它的所有可能取值,以及对于不同类型命令的流水线划分规则。
VkAccessFlags
Vulkan中的同步不仅控制操作执行的顺序,还要控制缓存的写回,即内存数据的同步。什么意思呢?不管是CPU还是GPU,存储系统都是按级划分的,比如主存、L2 Cache、L1 Cache等等ÿ