Vulkan同步Barrier的理解

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
2
3
4
5
6
7
8
9
10
11
VKAPI_ATTR void VKAPI_CALL 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);

虽然只有一个API,但是这个API的参数确实变化多端,相当不易理解,下面我们就分析下。

  • commandBuffer 指的是barrier将要插入的commandBuffer
  • srcStageMask表示哪个阶段的管线最后写入资源。

  • dstStageMask表示哪个阶段的管线接下来从资源读取数据。

  • dependencyFlags描述屏障表示的依赖关系如何影响屏障引用的资源。

下面的3组参数分别表示设置的3组屏障的类别

  • pMemoryBarriersmemoryBarrierCount用于全局的内存屏障
  • bufferMemoryBarrierCountpBufferMemoryBarriers用于缓冲区的内存屏障
  • imageMemoryBarrierCountpImageMemoryBarriers用于图像的内存屏障

针对上面的3组类型的屏障参数,我们可以有下面的4种场景。

  • Execution Barrier
  • Memory Barrier
  • Buffer Memory Barrier
  • Image Memory Barrier

Stage Mask

在介绍具体的barrier类型前,我们首先需要理解一下VkPipelineStageFlags这个类型。在vkCmdPipelineBarrier调用中,需要设置srcStageMaskdstStageMask,这两个分别表示源和目的的管线阶段,下面我们具体理解一下。

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
2
3
4
5
vkCmdPipelineBarrier(
commandBuffer,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, // source stage
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, // destination stage
/* remaining parameters omitted */);

上面的Barrier设置表示需要等所有的命令都执行完,然后再执行转换,并且在完全转换之前,任何命令都不能启动。这个Barrier将等待所有内容完成,并阻止任何工作开始。通常来讲这样设置效率比较差,会引起一些不必要的pipeline bubble。

我们再看一个例子:

barrier1

假设有一个顶点着色器,这个vertex shader会将数据通过imageStore存储,然后跟着一个想要使用它的计算着色器。在这种情况下,我们不希望等待后续片段着色器完成,因为这可能需要很长时间才能完成。我们希望计算着色器在顶点着色器完成后立即开始。因此我们可以做如下的设置:

1
2
3
4
5
vkCmdPipelineBarrier(
commandBuffer,
VK_PIPELINE_VERTEX_SHADER_BIT, // source stage
VK_PIPELINE_COMPUTE_SHADER_BIT, // destination stage
/* remaining parameters omitted */);

Execution Barrier

如果上面的3种Barrier都没有设置,那么这个Barrier就是一个Execution Barrier。这也就意味着这个barrier不对资源做任何的限制,而只对command的执行做限制,如下所示:

1
2
3
4
5
6
7
1. vkCmdDispatch
2. vkCmdDispatch
3. vkCmdDispatch
4. vkCmdPipelineBarrier(srcStageMask = COMPUTE, dstStageMask = COMPUTE)
5. vkCmdDispatch
6. vkCmdDispatch
7. 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
2
3
4
5
6
typedef struct VkMemoryBarrier {
VkStructureType sType;
const void* pNext;
VkAccessFlags srcAccessMask;
VkAccessFlags dstAccessMask;
} VkMemoryBarrier;

这里新增了srcAccessMask:表示内存最后如何写入。

dstAccessMask :表示内存接下来如何被读取。

它们的取值范围如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
typedef enum VkAccessFlagBits {
VK_ACCESS_INDIRECT_COMMAND_READ_BIT = 0x00000001,
VK_ACCESS_INDEX_READ_BIT = 0x00000002,
VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT = 0x00000004,
VK_ACCESS_UNIFORM_READ_BIT = 0x00000008,
VK_ACCESS_INPUT_ATTACHMENT_READ_BIT = 0x00000010,
VK_ACCESS_SHADER_READ_BIT = 0x00000020,
VK_ACCESS_SHADER_WRITE_BIT = 0x00000040,
VK_ACCESS_COLOR_ATTACHMENT_READ_BIT = 0x00000080,
VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT = 0x00000100,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT = 0x00000200,
VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT = 0x00000400,
VK_ACCESS_TRANSFER_READ_BIT = 0x00000800,
VK_ACCESS_TRANSFER_WRITE_BIT = 0x00001000,
VK_ACCESS_HOST_READ_BIT = 0x00002000,
VK_ACCESS_HOST_WRITE_BIT = 0x00004000,
VK_ACCESS_MEMORY_READ_BIT = 0x00008000,
VK_ACCESS_MEMORY_WRITE_BIT = 0x00010000,
VK_ACCESS_TRANSFORM_FEEDBACK_WRITE_BIT_EXT = 0x02000000,
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_READ_BIT_EXT = 0x04000000,
VK_ACCESS_TRANSFORM_FEEDBACK_COUNTER_WRITE_BIT_EXT = 0x08000000,
VK_ACCESS_CONDITIONAL_RENDERING_READ_BIT_EXT = 0x00100000,
VK_ACCESS_COMMAND_PROCESS_READ_BIT_NVX = 0x00020000,
VK_ACCESS_COMMAND_PROCESS_WRITE_BIT_NVX = 0x00040000,
VK_ACCESS_COLOR_ATTACHMENT_READ_NONCOHERENT_BIT_EXT = 0x00080000,
VK_ACCESS_SHADING_RATE_IMAGE_READ_BIT_NV = 0x00800000,
VK_ACCESS_ACCELERATION_STRUCTURE_READ_BIT_NV = 0x00200000,
VK_ACCESS_ACCELERATION_STRUCTURE_WRITE_BIT_NV = 0x00400000,
VK_ACCESS_FRAGMENT_DENSITY_MAP_READ_BIT_EXT = 0x01000000,
VK_ACCESS_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkAccessFlagBits;
typedef VkFlags VkAccessFlags;

全局memory barrier只有srcAccessMaskdstAccessMask ,因此作用于当前所有的resource。

一般场景下我们也很少用到这种粗粒度的全局内存屏障,大多数场景是需要具体操纵某个resource的时候,根据resource的类型,分别使用buffer或者image的memory barrier.

Buffer Memory Barrier

当我们操作具体的buffer时,就要用到下面的VkBufferMemoryBarrier了。srcAccessMaskdstAccessMask 是和上面一样的定义和意义。

1
2
3
4
5
6
7
8
9
10
11
typedef struct VkBufferMemoryBarrier {
VkStructureType sType;
const void* pNext;
VkAccessFlags srcAccessMask;
VkAccessFlags dstAccessMask;
uint32_t srcQueueFamilyIndex;
uint32_t dstQueueFamilyIndex;
VkBuffer buffer;
VkDeviceSize offset;
VkDeviceSize size;
} VkBufferMemoryBarrier;

这里新增加了srcQueueFamilyIndexdstQueueFamilyIndex,这是因为BufferMemory的所有权是存在从一个queue到另一个queue的转移的,所以需要设置,如果没有所有权的转移,则两者均设置为VK_QUEUE_FAMILY_IGNORED

bufferoffsetsize这三个参数用来执行具体的内存缓冲区。

Image Memory Barrier

Image Memory Barrier和Buffer Memory Barrier一样,只不过这里是用来处理Image的。

1
2
3
4
5
6
7
8
9
10
11
12
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;
  • oldLayoutnewLayout

我们看这里新增了oldLayoutnewLayout这两个参数,这里使用来做图像布局的转换的。我们知道在vulkan中图像是有布局的,在不同的场景下,需要设置为不同的布局的。布局的转换就需要用到Barrier。

  • image指的是相应的图像

  • subresourceRange是指屏障影响到的图像的区域

1
2
3
4
5
6
7
typedef struct VkImageSubresourceRange {
VkImageAspectFlags aspectMask;
uint32_t baseMipLevel;
uint32_t levelCount;
uint32_t baseArrayLayer;
uint32_t layerCount;
} VkImageSubresourceRange;

VkImageAspectFlags是指图像是什么类型的,有如下的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef enum VkImageAspectFlagBits {
VK_IMAGE_ASPECT_COLOR_BIT = 0x00000001,
VK_IMAGE_ASPECT_DEPTH_BIT = 0x00000002,
VK_IMAGE_ASPECT_STENCIL_BIT = 0x00000004,
VK_IMAGE_ASPECT_METADATA_BIT = 0x00000008,
VK_IMAGE_ASPECT_PLANE_0_BIT = 0x00000010,
VK_IMAGE_ASPECT_PLANE_1_BIT = 0x00000020,
VK_IMAGE_ASPECT_PLANE_2_BIT = 0x00000040,
VK_IMAGE_ASPECT_MEMORY_PLANE_0_BIT_EXT = 0x00000080,
VK_IMAGE_ASPECT_MEMORY_PLANE_1_BIT_EXT = 0x00000100,
VK_IMAGE_ASPECT_MEMORY_PLANE_2_BIT_EXT = 0x00000200,
VK_IMAGE_ASPECT_MEMORY_PLANE_3_BIT_EXT = 0x00000400,
VK_IMAGE_ASPECT_PLANE_0_BIT_KHR = VK_IMAGE_ASPECT_PLANE_0_BIT,
VK_IMAGE_ASPECT_PLANE_1_BIT_KHR = VK_IMAGE_ASPECT_PLANE_1_BIT,
VK_IMAGE_ASPECT_PLANE_2_BIT_KHR = VK_IMAGE_ASPECT_PLANE_2_BIT,
VK_IMAGE_ASPECT_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkImageAspectFlagBits;
typedef VkFlags VkImageAspectFlags;

常用的也就前几个。

  • 用于mipmap图像,mipmap的一个子集也可以包含进barrier

baseMipLevel 指定最小数字(最高分辨率)的mipmap层级。

levelCount 指定包含进barrier的mipmap的层级数量

如果图像不包含mipmap,则baseMipLevel = 0levelCount = 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

显示 Gitment 评论