Vulkan Cookbook 第四章 3 设置缓冲区内存屏障

设置缓冲区内存屏障

译者注:示例代码点击此处

缓冲区可用于各种目的。对于每个缓冲区,我们可以上传数据或从中复制数据通过描述符集将缓冲区绑定到管线。并在着色器中将其用作数据源,或者可以在着色器中将数据储存在缓冲区中。

我们不仅在缓冲区创建期间,而且在预期使用之前必须向驱动程序通知使用目的。当我们已经使用了一个缓冲区并且从现在开始希望以不同的方式使用它时,必须告诉驱动程序缓冲区使用目的的变化。这是通过缓冲内存屏障完成的。在命令缓冲区记录期间,它们被设置为管线屏障的一部分(请参阅第三章,命令缓冲区和同步中的开始命令缓冲区记录操作)。

译者注:恩。。稍微有点乱,捋一捋。屏障(barriers)是用于指定缓冲区内的起始位(offset)和尺寸(size)区域在特定管线下的用途,在特定管线阶段才可以使用这一缓存区域。注意!!!缓冲区(butter)是VkBuffer类型而命令缓冲区(command_buffer)是VkCommandBuffer不是一回事!!缓冲区是用于存放数据的,而命令缓冲区是存储命令的!命令缓冲区见第三章创建命令池分配命令缓冲区等相关章节

做好准备...

出于本节内容的目的,我们将使用名为BufferTransition的自定义结果类型:其定义如下:

struct BufferTransition { 
  VkBuffer      Buffer;
  VkAccessFlags CurrentAccess;
  VkAccessFlags NewAccess;
  uint32_t      CurrentQueueFamily;
  uint32_t      NewQueueFamily;
};

通过这种结构,我们将定义想要用于缓存内存屏障的参数。在CurrentAccess和NewAccess中,我们分别存储有关缓冲区当前如何使用以及之后如何使用的信息(被定义为给定缓冲区的内存操作类型)。当我们想要将所有权从一个队列族的转译到另一个族时,将使用CurrentQueueFamily和NewQueueFamily成员。但只是当缓冲区创建期间指定了独占共享模式时才需要这样做。

译者注:看来Vulkan驱动并没有帮我存储当前的CurrentAccess和CurrentQueueFamily状态,这大概是出于通用性和执行效率的考虑,我们需要自己存下来指定的缓冲区当前是什么状态并在切换目的的时候告诉他当前状态和新状态。当然当设置新状态后新状态就变成当前状态了,我们需要存下这个状态以备下次切换使用。

怎么做...

1.为每个缓冲区准备参数,将它们储存在名为buffer_transitions的std::vector<BufferTransition>类型变量里。对于每个缓冲区,存储以下参数:
    1.对Buffer字段设置缓冲区的句柄。
    2.CurrentAccess设置到目前为止缓冲区的内存操作类型
    3.NewAccess设置从现在开始(在屏障之后)将在缓冲区上执行的内存操作类型。
    4.CurrentQueueFamily设置到目前为止一直引用缓冲区的队列族索引(如果不想转移队列所有权,则为VK_QUEUE_FAMILY_IGNORED值)。
    5.NewQueueFamily成员中从现在起将引用缓冲区的队列族索引(如果不想转移队列所有权,则为VK_QUEUE_FAMILY_IGNORED值)。
2.创建一个名称为buffer_memory_barriers的std::vector<VkBufferMemoryBarrier>类型变量。
3.将buffer_transitions每个元素添加到buffer_memory_barriers中。对新元素的成员使用以下值:
    ·sType为VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER
    ·pNext为nullptr
    ·srcAccessMask为CurrentAccess
    ·dstAccessMask为NewAccess
    ·srcQueueFamilyIndex为CurrentQueueFamily
    ·dstQueueFamilyIndex为NewQueueFamily
    ·buffer为缓冲区的句柄
    ·offset为0
    ·size为VK_WHOLE_SIZE
4.获取命令缓冲区的句柄并将其储存在名为command_buffer的VkCommandBuffer变量中。
5.确保command_buffer句柄所代表的命令缓冲区处于记录状态(为命令缓冲区启用了记录操纵)。
6.创建名为generating_stages的VkPipelineStageFlags位字段类型变量。在此变量中,存储表示到目前为止一直使用缓冲区的管线阶段的值。
7.创建名为consume_stages的位字段类型VkPipelineStageFlags的变量。在此变量中,存储表示管线阶段的值,缓冲区将在屏障之后使用。
8.调用vkCmdPipelineBarrier(command_buffer, generating_stages, consuming_stages, 0, 0, nullptr, 
 static_cast<uint32_t>(buffer_memory_barriers.size()), &buffer_memory_barriers[0], 0, nullptr )。在第一个参数中提供命令缓冲区的句柄,并且分别在第二个和第三个参数中提供generating_stages和consume_stages变量。buffer_memory_barriers向量的元素个数应该在第七个参数中提供,第八个参数应该指向第一个buffer_memory_barriers向量的元素。

这个怎么运作...

在Vulkan中,提交到队列的操作按顺序执行,但它们是独立的,有时某些操作可能会在之前的操作完成之前启动。这种并行执行是当前图形硬件最重要的性能因素之一,但有时候,一些操作应该等待早期操作的结果是至关重要的:这就是内存屏障派上用场的时候。

译者注:提交到队列的操作按顺序执行,但它们是独立的只有当命令缓冲区提交给队列的时候,命令缓冲区才会执行操作,因为命令是按照顺序记录的,例如这个记录顺序:记录屏障1 -> 记录要执行的操作1 -> 记录屏障2 -> 记录要执行的操作2。如此当我们将命令缓冲区提交给队列开始执行,它执行是按特定的记录顺序执行的,但当队列执行到“记录屏障1 -> 记录要执行的操作1时”队列将这个操作直接丢给相关的处理者(例如硬件)然后马上执行下一句“记录屏障2 -> 记录要执行的操作2”也将它丢给相关处理者。这样他们实际处理的过程,未必1就比2先完成。因为队列不是实际的处理者它只是任务的分发者,具体在何时处理相关的操作是要看记录屏障的管线阶段如何设置的。注意!屏障只能够在同一队列中的命令之间或同一子通道中的命令之间插入依赖关系。相关文献 https://developer.samsung.com/game/usage 页面中搜索关键字Barriers。但我目前有个疑问我想应该每一个屏障都只依赖它上一个屏障如下图所示:

译者注:上图第一个命令是生产者将数据写入到特定的缓存内然后第二个命令是消费者等待前一个命令的特定管线阶段完成以此类推。我现在并没有找到相关解释,本书上面写它们是独立的,但并没有说屏障的依赖关系,我想它不应该是每个都互相依赖,而是只依赖它的前一个,否则会形成循环依赖。。那样就谁都别动了。

提示:内存屏障用于定义命令缓冲区执行中的时刻,后续命令应该等待先前的命令完成其工作。它们还会导致这些操作的结果对其他操作可见。

译者注:上面作者的提示并不全面,内存屏障是规定了缓冲区的范围和这个范围的目的以及这个范围在管线中可用的时刻。

通过内存屏障(memory barriers),我们指定缓冲区的使用方式以及哪些管线阶段一直运行到我们设置的屏障的位置。接下来需要定义在屏障之后的使用方式以及哪些管线阶段将使用它。有了这些信息,驱动程序可以暂停需要等待早期操作结果变为可用的操作。

译者注:我们指定缓冲区的使用方式以及哪些管线阶段一直运行到我们设置的屏障的位置这里的意思是我们需要设置两个阶段,分别是源阶段和目标阶段,因为GPU是高度流水线化设备,他会按照顺序一个个执行管线阶段,我们可以将源阶段和目标阶段看做是生产者和消费者阶段,例如假设一共10个阶段从0到9,如果我们设原阶段9目标阶段0,则此屏障要等待前一个命令执行完管线9阶段,才能运行。假设源阶段为6目标阶段为1,那么很可能我们的目标阶段的管线会先运行到1阶段,然后等待前一个命令运行到6阶段再运行。参https://gpuopen.com/vulkan-barriers-explained/

缓冲区的用法只能在创建期间定义。每个用法对应于可以通过其访问缓冲区内容的内存操作的类型。以下是支持的内存访问类型列表:
     ·当缓冲区的内容是间接绘制的数据源时使用VK_ACCESS_INDIRECT_COMMAND_READ_BIT
     ·当缓冲区的内容在绘制操作期间用于索引时使用VK_ACCESS_INDEX_READ_BIT
     ·当缓冲区是在绘制期间读取的顶点属性索引源时VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT
     ·当缓冲区作为统一缓冲区通过着色器访问时使用VK_ACCESS_UNIFORM_READ_BIT
     ·当可以在着色器内读取缓冲区时(但不能作为统一缓冲区)使用VK_ACCESS_SHADER_READ_BIT
     ·当着色器将数据写入缓冲区时使用VK_ACCESS_SHADER_WRITE_BIT
     ·当我们想要从缓冲区赋值数据时使用VK_ACCESS_TRANSFER_READ_BIT
     ·当我们想要将数据复制到缓冲区时使用VK_ACCESS_TRANSFER_WRITE_BIT
     ·当应用程序将读取缓冲区的内容时(通过内存映射)使用VK_ACCESS_HOST_READ_BIT
     ·当应用程序将数据写入缓冲区时(通过内存映射)使用VK_ACCESS_HOST_WRITE_BIT
     ·当以上未指定的任何其他方式读取缓冲区的内存时使用VK_ACCESS_MEMORY_READ_BIT
     ·当以上未指定的任何其他方式写入缓冲区的内存时使用VK_ACCESS_MEMORY_WRITE_BIT

内存操作需要屏障才能在之后的命令中可见。没有它们,读取缓冲区内容的命令可能会在内容被前面的操作正确写入之前开始读取它们。但是命令缓冲区执行中的这种中断会导致图形硬件处理管线中的停顿。不幸的是,这可能会影响我们应用程序的性能:

提示:我们应该在尽可能少的屏障中尽可能多的聚合缓冲区和所有权转换操作

译者注:上面这张图的意思是命令缓冲区的命令实际执行是并行的。

要为缓冲区设置内存屏障,我们需要准备类型为VkBufferMemoryBarrier的变量。如果可能,我们应该在一个内存屏障中聚合多个缓冲区的数据。这就是为什么VkBufferMemoryBarrier类型元素的向量看起来非常有用,可以像这样填充:

std::vector<VkBufferMemoryBarrier> buffer_memory_barriers;

for( auto & buffer_transition : buffer_transitions ) {   
  buffer_memory_barriers.push_back( {
    VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER, 
    nullptr, 
    buffer_transition.CurrentAccess, 
    buffer_transition.NewAccess,
    buffer_transition.CurrentQueueFamily, 
    buffer_transition.NewQueueFamily, 
    buffer_transition.Buffer,
    0,
    VK_WHOLE_SIZE 
  } );
}

接下来,我们在命令缓冲区中设置内存屏障。这是在命令缓冲区的记录操作期间完成的:

if( buffer_memory_barriers.size() > 0 ) {
  vkCmdPipelineBarrier( command_buffer, generating_stages, consuming_stages, 0, 0, nullptr, static_cast<uint32_t>(buffer_memory_barriers.size()), buffer_memory_barriers.data(), 0, nullptr );
}

在屏障中,我们指定在屏障之后执行的命令的哪个管线阶段应该等待屏障之前的执行的命令的哪个管线阶段的结果。

记住,只有在使用改变时,我们才需要设置一个屏障。如果屏障区多次用于同一目的,我们不需要这么做。假设想要从两个不同的资源将数据复制两次到缓冲区。首先需要设置一个屏障,通知驱动程序我们将执行涉及VK_ACCESS_TRANSFER_WRITE_BIT类型的内存访问操作。之后,我们可以按照希望的方式将数据复制到缓冲区。接下来如果我们想要使用缓冲区,例如作为顶点缓冲区(在渲染期间顶点属性的来源),需要设置另一个屏障,指示我们将从缓冲区中读取顶点属性的数据,这是由VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT表示。当完成绘制并且缓冲区将用于另一个目的时,及时我们想要再次将数据复制到缓冲区,还需要使用适当的参数设置内存屏障。

还有更多…

我们不需要为整个缓冲区设置屏障,只能为缓冲区的部分内存做这件事。为此,我们需要为给定缓冲区定义VkBufferMemoryBarrier类型变量的offset和size成员指定适当的值。通过这些成员定义了内存开始的内容,以及我们想要定义屏障的内存大小。这些值以计算机单位(字节)指定。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值