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

译者注:提交到队列的操作按顺序执行,但它们是独立的原文In Vulkan, operations that are submitted to queues are executed in order, but they are independent.为何顺序执行它们还是独立的?队列操作是顺序执行操作,而操作是独立的看上去比较矛盾,难道在队列中的操作还能并行吗,但是前一句已经否定了并行。那看来这里说的并行是指的队列是并行的,所以操作是独立的。也可能我理解错了,要看看后面的章节再说。

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

译者注:上面的提示也许并不全面,好像内存屏障是规定了缓冲区的范围和这个范围的目的以及这个范围在命令缓冲区执行中可见的时刻,否则还要offset和size做什么?

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

译者注:原文In the case of buffers, through memory barriers, we specify how the buffer was used and which pipeline stages were using it up to the moment in which we placed a barrier. Next we need to define which pipeline stages will be using it and how, after the barrier. With this information, the driver can pause operations that need to wait for the results of earlier operations to become available, but execute operations that won't reference the buffer at all.我们指定缓冲区的使用方式以及哪些管线阶段一直运行到我们设置的屏障的位置。这里所说的的哪些管线表示的是generating_stages?,而一直运行到我们设置的屏障的位置,这个位置是哪里?目前看好像是consuming_stages的位置。那这句话的意思是我们指定缓冲区的使用方式以及generating_stages设置的管线阶段一直运行到consuming_stages管线的位置?还要定义在consuming_stages之后的内存使用目的。还有这个但执行根本不会引用缓冲区的操作是说的在内存不可能用的时间段操作不会引用特定缓冲区? 这部分作者写的真是有很大的歧义,generating_stages和consuming_stages还是位字段如果我指定了多个管线阶段会怎样?需要先看了后续章节再考虑清楚。如果你有什么想法请回复。法克!

缓冲区的用法只能在创建期间定义。每个用法对应于可以通过其访问缓冲区内容的内存操作的类型。以下是支持的内存访问类型列表:
     ·当缓冲区的内容是间接绘制的数据源时使用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成员指定适当的值。通过这些成员定义了内存开始的内容,以及我们想要定义屏障的内存大小。这些值以计算机单位(字节)指定。

猜你喜欢

转载自blog.csdn.net/qq_19473837/article/details/84782320
今日推荐