Vulkan Cookbook 第四章 7 设置图像内存屏障

设置图像内存屏障

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

创建图像用于各种目的,它们用作纹理,通过描述符集将它们绑定到管线,作为渲染目标,或者作为交换链中的可呈现图像。我们可以将数据复制到图像或从图像复制数据(这些也是在图像创建期间定义的独特用法)。

在我们开始将图像用作任何目的之前,每次想要更改给定图像的当前使用情况时,都需要知道驱动程序有关此操作的信息。我们通过使用在命令缓冲区记录期间设置的图像内存屏障来完成此操作。

做好准备

出于本节内容的目的,引入了自定义结构类型ImageTransition。它有以下定义:

struct ImageTransition { 
  VkImage             Image;
  VkAccessFlags       CurrentAccess; 
  VkAccessFlags       NewAccess; 
  VkImageLayout       CurrentLayout; 
  VkImageLayout       NewLayout; 
  uint32_t            CurrentQueueFamily; 
  uint32_t            NewQueueFamily;
  VkImageAspectFlags  Aspect;
};

CurrentAccess和NewAccess成员定义了给定图像在屏障之前的内存操作类型以及屏障之后的类型。

在Vulkan中用于不同目的图像可能具有不同的内存组织。换句话说,内存可以针对不同图像使用特定布局。当我们想要以不同的方式开始使用布局时,需要改变这个内存布局。这是通过CurrentLayout和NewLayout完成的。

如果图像以独占共享模式创建,则内存屏障还准许我们转移队列族所有权。在CurrentQueueFamily成员中我们定义了一个族的索引,在NewQueueFamily中我们需要为将在屏障之后使用的图像的队列定义一个族的索引。当我们不想转移所有权时,还可以为两者使用VK_QUEUE_FAMILY_IGNORED特殊值。

Aspect(方面)成员定了图像的使用“上下文”。我们可以从颜色,深度或模板方面进行选择。

怎么做...

1.需要为每个要设置屏障的图像准备参数。将它们储存在名为image_transitions的std::vector<ImageTransition> 类型向量中。
    ·Image为图像句柄
    ·CurrentAccess为到目前为止图像的内存操作类型
    ·NewAccess为在屏障之后对图像执行的内存操作类型
    ·CurrentLayout为当前图像的内存布局
    ·NewLayout为屏障之后改变的图像内存布局
    ·CurrentQueueFamily为到目前为止引用图像的队列族的索引(如果我们不想传输队列所有权,则为VK_QUEUE_FAMILY_IGNORED值)。
    ·NewQueueFamily为从现在起将引用的图像的队列族索引(如果不想转移队列所有权,则为VK_QUEUE_FAMILY_IGNORED值)。译者注:这里的队列族我想也是从屏障之后发生改变,但是原文这句并没有写屏障(barrier)之后,太不严谨了!
    ·Aspect为图像方面(颜色,深度或模板)。
2.创建一个名为image_memory_barriers的std::vector<VkImageMemoryBarrier>类型向量变量。
3.将image_transitions变量的每个元素添加到image_memory_barriers向量。对新新元素的成员使用如下值:
    ·sType为VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER
    ·pNext为nullptr
    ·srcAccessMask为CurrentAccess
    ·dstAccessMask为NewAccess
    ·oldLayout为CurrentLayout
    ·newLayout为NewLayout
    ·srcQueueFamilyIndex为CurrentQueueFamily
    ·dstQueueFamilyIndex为NewQueueFamily
    ·image为图像句柄
    ·对subresourceRange的成员设置如下值:
        ·aspectMask为Aspect
        ·baseMipLevel为0
        ·levelCount为VK_REMAINING_MIP_LEVELS
        ·baseArrayLayer为0
        ·layerCount为VK_REMAINING_ARRAY_LAYERS
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, 0, nullptr, static_cast<uint32_t>(image_memory_barriers.size()), &image_memory_barriers[0] )并在第一个参数中提供命令缓冲区的句柄。第二和第三个参数中提供generating_stages和consume_stages变量。应该在倒数第二个参数中提供image_memory_barriers向量的元素数量,并且最后一个参数应该指向image_memory_barriers向量的第一个元素。

这个怎么运作...

在Vulkan中,操作在管线中处理。既使操作的处理需要按照提交的顺序开始,管线的某些部分仍然可以同时执行,但有时,我们可能需要同步这些操作并告诉驱动程序我们希望其中一些操作等待其他操作的结果。

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

内存操作需要屏障才能在以后的命令中可见。如果操作系统将数据写入图像并进一步操作将从中读取,我们需要使用图像内存屏障。反之亦然,覆盖图像数据的操作应该等待早期操作停止从中读取数据。在这两种情况下,如果不这样做,将使图像的内容无效。但是这种情况尽可能少,否则我们的应用可能会遭到性能损失。这是因为命令缓冲区执行中的这种暂停导致图形硬件处理管线的停顿,因此浪费了时间:

图像内存屏障还用于定义图像使用方式的变化。这种使用变化通常还要求我们同步提交的操作,这就是为什么需要通过内存屏障来完成。为了更改图像的用法,我们需要定义在屏障之前和之后对图像执行的内存操作类型(内存访问)。我们还指定了屏障之前的内存布局以及屏障之后应该如何布局内存。这是因为图像在用于不同目的时可能具有不同的内存组织。例如,从着色器内的图像采集数据可能需要对它们进行高速缓存,使得相邻纹素在内存中是邻居,但是,当内存线性布局时,可以更快地将数据写入图像。这就是为什么在Vulkan中引入图像布局的原因。每个图像使用都有自己的指定布局。有一个通用布局,可用于所有目的。但是,简易不要使用常规布局,因为这可能会影响某些硬件平台的性能。

提示:为获得最佳性能,建议使用指定的图像内存布局以用于特定用途,但如果过于频繁执行布局转换则必须小心。

定义用法更改的参数是通过VkImageMemoryBarrier类型的变量指定的,如下所示:

std::vector<VkImageMemoryBarrier> image_memory_barriers;

for( auto & image_transition : image_transitions ) {   
  image_memory_barriers.push_back( {
    VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, 
    nullptr, 
    image_transition.CurrentAccess, 
    image_transition.NewAccess, 
    image_transition.CurrentLayout, 
    image_transition.NewLayout, 
    image_transition.CurrentQueueFamily, 
    image_transition.NewQueueFamily, 
    image_transition.Image,
    {
      image_transition.Aspect, 
      0, 
      VK_REMAINING_MIP_LEVELS, 
      0, 
      VK_REMAINING_ARRAY_LAYERS
    } 
  } );
}

但是为了使屏障正常工作,我们需要定义到目前为止使用图像的管道阶段,以及从现在开始使用的图像:

在上图中,我们可以看到两个管线屏障的例子。在左侧,颜色由片段着色器生成,并且在此进行所有片段测试(深度测试,混合)之后颜色数据被写入图像。然后在连续命令的顶点着色器中使用该图像,这样的设置很可能在管线中产生停顿。

右侧的示例显示了图像命令中的另一个依赖项。这里数据被写入顶点着色器中的资源。然后下一个命令的片段着色器使用这些数据。这一次,顶点着色器的所有实例很可能在下一个命令的片段着色器开始执行之前完成其作业。这就是为什么重要的是降低管道屏障的数量,并且如果需要正确设置绘制命令并选择屏障的管道阶段,屏障的参分为生成阶段和消费阶段,调用如下函数为我们的图像集设置屏障。

译者注:上面这个例子作者写的不明不白的,看到后面明白了再回来补充解释。

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

如果图像以相同的方式多次使用,并且在两者之前没有用于其他目的,则在实际使用图像之前我们不需要设置屏障,我们将它们设置为使用更改的信号,而不是使用自身

译者注:我们将它们设置为使用更改的信号,而不是使用自身原文We set it to signal the usage change, not the usage itself。暂时没看懂是什么鸟意思。好像只有之前信号量里提到过signal,但是好像和这里没有关系。先不管了向后看

猜你喜欢

转载自blog.csdn.net/qq_19473837/article/details/84913676