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 Barrier
Memory Barrier
Buffer Memory Barrier
Image 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_BIT
DRAW_INDIRECT_BIT
VERTEX_INPUT_BIT
VERTEX_SHADER_BIT
TESSELLATION_CONTROL_SHADER_BIT
TESSELLATION_EVALUATION_SHADER_BIT
GEOMETRY_SHADER_BIT
FRAGMENT_SHADER_BIT
EARLY_FRAGMENT_TESTS_BIT
LATE_FRAGMENT_TESTS_BIT
COLOR_ATTACHMENT_OUTPUT_BIT
TRANSFER_BIT
COMPUTE_SHADER_BIT
BOTTOM_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