Barrier用于同一个queue中的commands,或者同一个subpass中的commands所明确指定的依赖关系。我们可以想象一下有一大串的command乱序执行(实际上是顺序开始,乱序结束),barrier就是在中间树立一道栅栏,要求栅栏前后保持一定的顺序,但是前后的内部之间的顺序它是不关心的。
commands顺序开始,乱序结束有什么问题么?如果不涉及到对资源的访问,那么就没什么问题。如果不同的command涉及到了对同一资源的访问以及修改,那么就有问题了,也正是因此我们看Barrier的设置函数中主要是对资源进行设置。
在khronos的官方的blog里面由一句话室非常重要的,它明了的说明了Barrier是什么:
1 | Pipeline barriers specify what data or which stages of the rendering pipeline to wait for and which stages to block until other specified stages in previous commands are completed. |
1 | Pipeline barriers确定了哪些data或者哪些指定的stages需要阻塞等待, 直到previous commands中的specified stages完成。 |
vulkan中barrier提供了一个API来设置,如下所示:
1 | VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier( |
虽然只有一个API,但是这个API的参数确实变化多端,相当不易理解,下面我们就分析下。
commandBuffer指的是barrier将要插入的commandBuffer。srcStageMask表示哪个阶段的管线最后写入资源。dstStageMask表示哪个阶段的管线接下来从资源读取数据。dependencyFlags描述屏障表示的依赖关系如何影响屏障引用的资源。
下面的3组参数分别表示设置的3组屏障的类别
pMemoryBarriers与memoryBarrierCount用于全局的内存屏障bufferMemoryBarrierCount与pBufferMemoryBarriers用于缓冲区的内存屏障imageMemoryBarrierCount与pImageMemoryBarriers用于图像的内存屏障
针对上面的3组类型的屏障参数,我们可以有下面的4种场景。
Execution BarrierMemory BarrierBuffer Memory BarrierImage Memory Barrier
Stage Mask
在介绍具体的barrier类型前,我们首先需要理解一下VkPipelineStageFlags这个类型。在vkCmdPipelineBarrier调用中,需要设置srcStageMask和dstStageMask,这两个分别表示源和目的的管线阶段,下面我们具体理解一下。
GPU是高度流水线化的设备,vulkan中的commands会出现在顶部VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,然后按照顺序执行各个阶段,执行完毕后,命令会在VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT结束。
vulkan一共定义了如下的管线阶段:
TOP_OF_PIPE_BITDRAW_INDIRECT_BITVERTEX_INPUT_BITVERTEX_SHADER_BITTESSELLATION_CONTROL_SHADER_BITTESSELLATION_EVALUATION_SHADER_BITGEOMETRY_SHADER_BITFRAGMENT_SHADER_BITEARLY_FRAGMENT_TESTS_BITLATE_FRAGMENT_TESTS_BITCOLOR_ATTACHMENT_OUTPUT_BITTRANSFER_BITCOMPUTE_SHADER_BITBOTTOM_OF_PIPE_BIT
注意:上面的枚举阶段并不一定是命令的执行顺序,某些阶段可以合并,某些阶段可能会丢失。
我们看下面的例子:
1 | vkCmdPipelineBarrier( |
上面的Barrier设置表示需要等所有的命令都执行完,然后再执行转换,并且在完全转换之前,任何命令都不能启动。这个Barrier将等待所有内容完成,并阻止任何工作开始。通常来讲这样设置效率比较差,会引起一些不必要的pipeline bubble。
我们再看一个例子:

假设有一个顶点着色器,这个vertex shader会将数据通过imageStore存储,然后跟着一个想要使用它的计算着色器。在这种情况下,我们不希望等待后续片段着色器完成,因为这可能需要很长时间才能完成。我们希望计算着色器在顶点着色器完成后立即开始。因此我们可以做如下的设置:
1 | vkCmdPipelineBarrier( |
Execution Barrier
如果上面的3种Barrier都没有设置,那么这个Barrier就是一个Execution Barrier。这也就意味着这个barrier不对资源做任何的限制,而只对command的执行做限制,如下所示:
1 | 1. vkCmdDispatch |
这意味着1,2,3的command执行完毕后,5,6,7的command才开始执行,而1,2,3之间的执行是没有顺序的。
这么看Execution Barrier很好啊,能解决资源的并发问题啊,即使他们对同一资源进行读写,那么也是在1,2,3读写完毕后,才会执行4,5,6的读写,那么他们有什么问题呢?
逻辑上没有什么问题,问题在于具体的实现上,由于现代GPU同样采取了复杂的缓存控制机制,这个理想的模型是不存在的。一种可能的结果是,第一个dispatch执行完毕后, resource最新的内容被缓存到了某一级cache中。不幸的是,第二个dispatch开始执行的时候,这个cache对第二个dispatch不可见(例如,两个dispatch被分派到了不同的执行单元中)。尽管从顺序上,这的确保证了第一个dispatch先执行,然后才是第二个dispatch,但是我们仍然无法保证第二个dispatch能够看到第一个dispatch更新后的结果。
因此Execution Barrier只能保证执行的先后顺序,而不能保证对资源的最终读写顺序,因此需要更多参数来控制(也就是上面的3组类型的屏障参数)。
Memory Barrier
全局内存屏障,它的存在就是为了解决上面Execution Barrier没法解决的问题的,既然我们只是限定执行顺序没法保证资源的读写同步,那么我们就再加两个变量,用来限定对资源的访问,如下所示:
1 | typedef struct VkMemoryBarrier { |
这里新增了srcAccessMask:表示内存最后如何写入。
dstAccessMask :表示内存接下来如何被读取。
它们的取值范围如下:
1 | typedef enum VkAccessFlagBits { |
全局memory barrier只有srcAccessMask和dstAccessMask ,因此作用于当前所有的resource。
一般场景下我们也很少用到这种粗粒度的全局内存屏障,大多数场景是需要具体操纵某个resource的时候,根据resource的类型,分别使用buffer或者image的memory barrier.
Buffer Memory Barrier
当我们操作具体的buffer时,就要用到下面的VkBufferMemoryBarrier了。srcAccessMask和dstAccessMask 是和上面一样的定义和意义。
1 | typedef struct VkBufferMemoryBarrier { |
这里新增加了srcQueueFamilyIndex和dstQueueFamilyIndex,这是因为BufferMemory的所有权是存在从一个queue到另一个queue的转移的,所以需要设置,如果没有所有权的转移,则两者均设置为VK_QUEUE_FAMILY_IGNORED
buffer,offset,size这三个参数用来执行具体的内存缓冲区。
Image Memory Barrier
Image Memory Barrier和Buffer Memory Barrier一样,只不过这里是用来处理Image的。
1 | typedef struct VkImageMemoryBarrier { |
oldLayout和newLayout
我们看这里新增了oldLayout和newLayout这两个参数,这里使用来做图像布局的转换的。我们知道在vulkan中图像是有布局的,在不同的场景下,需要设置为不同的布局的。布局的转换就需要用到Barrier。
image指的是相应的图像subresourceRange是指屏障影响到的图像的区域
1 | typedef struct VkImageSubresourceRange { |
VkImageAspectFlags是指图像是什么类型的,有如下的类型:
1 | typedef enum VkImageAspectFlagBits { |
常用的也就前几个。
- 用于mipmap图像,mipmap的一个子集也可以包含进barrier
baseMipLevel 指定最小数字(最高分辨率)的mipmap层级。
levelCount 指定包含进barrier的mipmap的层级数量
如果图像不包含mipmap,则baseMipLevel = 0 和 levelCount = 1
- 对于阵列图像的图像层子集也可以包含进barrier
baseArrayLayer 设置的第一个层的索引
layerCount 所要包含的层数
Reference
Yet another blog explaining Vulkan synchronization – Maister’s Graphics Adventures (themaister.net)
Vulkan® Barriers Explained - GPUOpen
vulkan中的同步和缓存控制之二,barrier和event - 知乎 (zhihu.com)
Synchronization Examples (Legacy synchronization APIs) · KhronosGroup/Vulkan-Docs Wiki · GitHub)
【译】拆解D3D12和Vulkan中的Barrier(1) - 知乎 (zhihu.com)
Understanding Vulkan Synchronization - The Khronos Group Inc