Vulkan编程指南翻译 第四章 队列和命令 第1节 管理资源的状态

2017-02-28

在本章,你将学到:

  • 如何管理资源被Vulkan使用时的状态
  • 如何在资源间复制数据,用已知数据填充缓冲区和图像
  • 如何进行位块操作以拉伸或缩放图像数据

 

图形和计算操作总体上是数据密集型的。Vulkan引入了几个对象,可以提供存储和操纵数据的途径。经常需要把数据移入或者转出这些对象,有几个命令可以用来做这个工作:复制数据,填充缓冲区和图像对象。进一步,在任何时刻,一个资源可能出狱多个状态,Vulkan管线的多个部分可能需要访问他们。本章将讲解数据移动命令—可以用来复制数据与填充内存—当它们被应用程序访问的时候,命令需要用来管理资源的状态。

第三章,展示了命令被放在了一个命令缓冲区中并提交到设备的某一个队列以被执行。这非常重要,因为这表示命令并不是你的应用程序调用它们的时候就被执行的,却是当你把它们提交到队列,队列进入设备的时候被执行的。之前介绍给你的第一个相关函数,vkCmdCopyBuffer(),在两个buffer之间或者同一buffer不同的区域之间复制数据。这是众多能够改变buffer,图像,其他的Vulkan对象的命令之一。本章将讲解填充、复制、清除buffer与图像的相关命令。

 

4.1 管理资源状态

在一个程序执行中在任何给定的时刻,每一个资源都可以处于一个或者多个不同的状态。例如,如果图形管线正在绘制一个图像或者使用它作为一个贴图,或者Vulkan正在从CPU端复制数据到一个图像,这些场景都是不同的。对于一些Vulkan实现,上述的一些状态之间也许没有任何差别,对于其他的,准确的知道一个时间点资源所处的状态会决定你的应用程序正常的工作或者是做垃圾工作。

因为命令缓冲区内的命令负责访问资源,且多个命令缓冲区被创建的顺序有可能不同于他们被提交执行的顺序,故由Vulkan跟踪资源的状态并且保证在每一场景都正确的工作是不现实的。特别是,一个资源会因命令缓冲区的执行从一个状态转为另一个状态。驱动无法跟踪资源的状态,当命令缓冲区被提交执行的时候,在命令缓冲区之间跟踪状态有显著的损耗。因此,这个责任落到了应用程序身上。资源的状态,对图像来说可能非常重要,因为他们是复制、结构化的资源。

一个图像的状态可以粗略的分为两种正交的集合:布局,记录。布局决定了数据在内存中的存放布局,在本书前面讲过。记录,谁最后一次写入信息,将影响cache和数据的一致性。图像初始的布局是在创建时被指定的,然后在其生命周期内发生改变,要么显式的使用barriers或者隐式的使用renderpass。Barriers掌控这Vulkan渲染管线的不同部分对资源的访问,在某些情况下,在另一个midpipeline同步工作中,barriers可以一个资源从一个布局转变到另一布局。

每一种布局的准确使用将在本书后面详细讲到。然而,转移数据从一个状态到另一状态的基础操作就是barrier,且,在应用程序中准确的理解并高效率的使用它,是极其重要的。

 

4.1.1 管线屏障

屏障是同步机制的一种,被用来管理内存访问和资源的在Vulkan管线的一个stage内状态迁移。资源访问同步和状态迁移主要的命令是vkCmdPipelineBarrier(),原型如下:

 

 

 

void 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);

需要执行屏障的命令缓冲区是通过commandBuffer指定的。接下来的两个参数,srcStageMask 和 dstStageMask,指定了哪个阶段的管线最后向资源写入和哪个阶段接下来要从资源读数据。亦即,屏障通过它们指定了数据流通的源和目的地。每一个值都是VkPipelineStageFlagBits枚举类型的值构造的。

• VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT: The top of pipe is considered to be hit as soon

as the device starts processing the command.

• VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT: When the pipeline executes an

indirect command, it fetches some of the parameters for the command from memory. This

is the stage that fetches those parameters.

• VK_PIPELINE_STAGE_VERTEX_INPUT_BIT: This is the stage where vertex attributes are

fetched from their respective buffers. After this, content of vertex buffers can be overwritten,

even if the resulting vertex shaders have not yet completed execution.

• VK_PIPELINE_STAGE_VERTEX_SHADER_BIT: This stage is passed when all vertex

shader work resulting from a drawing command is completed.

• VK_PIPELINE_STAGE_TESSELLATION_CONTROL_SHADER_BIT: This stage is passed

when all tessellation control shader invocations produced as the result of a drawing command

have completed execution.

• VK_PIPELINE_STAGE_TESSELLATION_EVALUATION_SHADER_BIT: This stage is

passed when all tessellation evaluation shader invocations produced as the result of a drawing

command have completed execution.

• VK_PIPELINE_STAGE_GEOMETRY_SHADER_BIT: This stage is passed when all geometry

shader invocations produced as the result of a drawing command have completed execution.

• VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT: This stage is passed when all fragment

shader invocations produced as the result of a drawing command have completed execution.

Note that there is no way to know that a primitive has been completely rasterized while the

resulting fragment shaders have not yet completed. However, rasterization does not access

memory, so no information is lost here.

• VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT: All per-fragment tests that

might occur before the fragment shader is launched have completed.

• VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT: All per-fragment tests that might

occur after the fragment shader is executed have completed. Note that outputs to the depth and

stencil attachments happen as part of the test, so this stage and the early fragment test stage

include the depth and stencil outputs.

• VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT: Fragments produced by the

pipeline have been written to the color attachments.

• VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT: Compute shader invocations produced as

the result of a dispatch have completed.

• VK_PIPELINE_STAGE_TRANSFER_BIT: Any pending transfers triggered as a result of calls

to vkCmdCopyImage() or vkCmdCopyBuffer(), for example, have completed.

• VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT: All operations considered to be part of

the graphics pipeline have completed.

• VK_PIPELINE_STAGE_HOST_BIT: This pipeline stage corresponds to access from the host.

• VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT: When used as a destination, this special flag

means that any pipeline stage may access memory. As a source, it’s effectively equivalent to

VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT.

• VK_PIPELINE_STAGE_ALL_COMMANDS_BIT: This stage is the big hammer. Whenever

you just don’t know what’s going on, use this; it will synchronize everything with everything.

Just use it wisely

因为srcStageMask 和 dstStageMask内的标志位用来指示事情什么时候发生,Vulkan实现把它们放在一边或者多种方式解读它们。srcStageMask指定了什么时候源阶段已经完成了向资源读或写数据。结果,在管线中稍后移动这个阶段有效的位置并不改变访问已经完成的事实。

这意味着实现等待的时间比它需要完成的时间还久。

同样的,dstStageMask指定了管线在处理之前等着的时间点。如果一个Vulkan实现把等待的时间点提前了,也是能够工作的。在逻辑上后面的管线部分开始执行前,被等待的事件也最终会被完成。这种实现仅仅是失去了当它在等待时却能够工作的机会。

dependencyFlags参数指定了标志位的一个集合,描述了由屏障表示的依赖关系如何影响到屏障引用的资源。唯一被定义的标志类型是VK_DEPENDENCY_BY_REGION_BIT,它表示屏障只影响被source stage改变的区域,此区域被目标阶段所消耗。

vkCmdPipelineBarrier()调用可用来触发多个屏障操作。有三种类型的屏障操作:全局内存屏障,buffer屏障,图像屏障。全局内存屏障影响到诸如映射内存被CPU、GPU同步访问之类的工作。Buffer和image屏障主要影响设备对buffer和images资源的访问。、

 

4.1.2  全局内存屏障

vkCmdPipelineBarrier()可触发的全局内存屏障是通过memoryBarrierCount参数指定的。如果这是一个非零值,那么pMemoryBarriers指向一个大小为memoryBarrierCount的VkMemoryBarrier类型的数组,每一个都代表一个内存屏障。VkMemoryBarrier定义如下:

typedef struct VkMemoryBarrier {

VkStructureType  sType;

const void*  pNext;

VkAccessFlags  srcAccessMask;

VkAccessFlags  dstAccessMask;

} VkMemoryBarrier;VkMemoryBarrier的sType域应置为VK_STRUCTURE_TYPE_MEMORY_BARRIER, pNext应置为nullptr。此数据类型中其他两个域,srcAccessMask 和 dstAccessMask,分别表示源和目标访问掩码。源访问掩码指定了内存最后如何写入,目标访问掩码知道了内存下一次如何被读取。可用的标志位列举如下:

  • VK_ACCESS_INDIRECT_COMMAND_READ_BIT:内存将会是间接绘制命令和诸如vkCmdDrawIndirect()或vkCmdDispatchIndirect()这样的转发命令的来源。
  • VK_ACCESS_INDEX_READ_BIT: 内存将是诸如vkCmdDrawIndexed() 或vkCmdDrawIndexedIndirect()这样的绘制命令索引数据的来源
  • VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT: 内存将是Vulkan固定管线顶点组装阶段顶点数据的来源
  • VK_ACCESS_UNIFORM_READ_BIT: 内存将是被shader访问的uniform块的来源
  • VK_ACCESS_INPUT_ATTACHMENT_READ_BIT: 内存将被用来存储图像作为一个输入附件
  • VK_ACCESS_SHADER_READ_BIT: 内存将被用作存储shader内图像加载或纹理读取的图像对象
  • VK_ACCESS_SHADER_WRITE_BIT: 内存将被用作存储shader内写入数据的图像对象
  • VK_ACCESS_COLOR_ATTACHMENT_READ_BIT: 内存将被用作存储支持读操作的颜色附件的图像,也许是因为开启了混合。注意这和输入附件显式的被fragment着色器读取数据的情况不同。
  • VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT: The memory referenced is used to back

an image used as a color attachment that will be written to.

• VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_READ_BIT: The memory referenced is

used to back an image used as a depth or stencil attachment that will be read from because the

relevant test is enabled.

• VK_ACCESS_DEPTH_STENCIL_ATTACHMENT_WRITE_BIT: The memory referenced is

used to back an image used as a depth or stencil attachment that will be written to because the

relevant write mask is enabled.

• VK_ACCESS_TRANSFER_READ_BIT: The memory referenced is used as the source of data

in a transfer operation such as vkCmdCopyImage(), vkCmdCopyBuffer(), or

vkCmdCopyBufferToImage().

• VK_ACCESS_TRANSFER_WRITE_BIT: The memory referenced is used as the destination of

a transfer operation.

• VK_ACCESS_HOST_READ_BIT: The memory referenced is mapped and will be read from by

the host.

• VK_ACCESS_HOST_WRITE_BIT: The memory referenced is mapped and will be written to

by the host.

• VK_ACCESS_MEMORY_READ_BIT: All other memory reads not explicitly covered by the

preceding cases should specify this bit.

• VK_ACCESS_MEMORY_WRITE_BIT: All other memory writes not explicitly covered by the

preceding cases should specify this bit.

 

内存屏障提供两个重要的功能。第一,它们可以帮助避免危险的事,第二,可确保数据一致性。

当读写操作的被重新排序并且和程序员期待的执行顺序不同时,就有可能发生危险的事情。这可能很难排查原因,因为它们常常是平台或者时间无关的。有三种危险的事情:

  • 写后读,或者RaW,此危险发生在 程序员希望对一片内存的读操作发生在最近的写操作之后,能够看到写入的结果。如果读操作被重新安排,并在写操作完成之前执行了,那么读操作将会看到老的数据。
  • 读后写,或WaR,此危险发生在程序员期待在程序的另一部分的读操作完成之后再进行写操作。如果写操作发生在读操作之前,那么毒操作就会看到最新的数据,这并不是希望的结果。
  • 写后写,或WaW,此危险发生在程序员期待对同一片内存多次进行写操作,并且只有最后一次写操作的数据应该被后续的读操作看到。如果写操作被重新安排顺序,那么只能是碰巧最后被执行的写操作的结果能被读操作看到。

 

并没有所谓的“读后读”危险,因为没有数据被修改过。在内存屏障内,源并不需要是数据的生产者,但是,第一个操作是被该屏障所保护的。为了避免RaW危险,读操作是真正的源。

比如,为了保证所有的复制操作中纹理读取在写入图像之前完成,我们需要在srcAccessMask域中指定VK_ACCESS_SHADER_READ_BIT标志位,在dstAccessMask中指定VK_ACCESS_TRANSFER_WRITE_BIT标志位。这就会告诉Vulkan第一阶段是在shader里从图像读数据,第二阶段可以对该图像写入,所以我们不应该把写入图像操作放在任何shader可能读取它之前。

注意,在VkAccessFlagBits和VkPipelineStageFlagBits中标志位有一些耦合。VkAccessFlagBits标志位指定执行什么操作,VkPipelineStageFlagBits描述在管线的哪一部分执行操作。

内存屏障的第二个功能是保证在管线不同部分中数据的视图的一致性。比如,如果应用程序的一个shader向一个缓冲区写入数据,然后需要通过绑定内存对象来读取数据回到缓冲区,就需要指定srcAccessMask含有VK_ACCESS_SHADER_WRITE_BIT,dstAccessMask含有VK_ACCESS_HOST_READ_BIT标志位。如果设备有缓存,在shader写入buffer时使用到,那么这些cache需要刷新,以便于主机端(CPU)能够看到写操作的结果。

 

4.1.3  缓冲区内存屏障

缓冲区内存屏障提供对用于缓冲区对象的内存精细力度的控制。vkCmdPipelineBarrier()函数调用执行的缓冲区内存屏障的个数通过bufferMemoryBarrierCount参数指定,函数的pBufferMemoryBarriers参数是一个指向VkBufferMemoryBarrier数组的指针,每一个元素都定义了一个缓冲区内存屏障。VkBufferMemoryBarrier定义如下:

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;

VkBufferMemoryBarrier的sType域应置为VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER,pNext应置为nullptr。srcAccessMask 和 dstAccessMask 域和VkMemoryBarrier中的是一样的含义。显然,一些标志位仅对图像有用,颜色、深度附件等几乎和缓冲区内存没有什么关系。

当缓冲区的控制权从一个队列转移到另一个队列,且这些队列属于不同的族时,源和目的队列的族的索引必须通过srcQueueFamilyIndex 和 dstQueueFamilyIndex域指定。如果没有控制权的转移,那么srcQueueFamilyIndex 和 dstQueueFamilyIndex都可被置为VK_QUEUE_FAMILY_IGNORED。在这种情况下,控制权默认是创建命令缓冲区的队列组。

屏障控制访问权的缓冲区通过buffer域指定。为了让缓冲区的一部分区域的访问保持同步,使用offset和size域来指定这个区域,单位是byte。为了控制整个缓冲区,把offset设置为0,size为VK_WHOLE_SIZE即可。

如果缓冲区被一个或多个队列上的工作访问,且这些队列属于不同的族,应用程序必须采取一些附加措施。因为单一一个设备实际上需要使用多个物理组件才能暴露多个队列族,而且因为这些组件也许拥有自己的cache,调度架构,内存控制器等等。Vulkan需要知道一个资源何时从一个队列转移到另一个。如果是这种情况,通过srcQueueFamilyIndex指定源队列的族索引,和dstQueueFamilyIndex来指定目标队列所属的族索引。

和图像内存屏障相似,如果资源不在不属同族的队列之间转移,就可以把srcQueueFamilyIndex 和dstQueueFamilyIndex设置为VK_QUEUE_FAMILY_IGNORED。

 

4.1.4  图像内存屏障

和缓冲区一样,需要对图像付出特别的注意,图像内存屏障被用来控制对图像的访问。vkCmdPipelineBarrier()操作的图像内存屏障个数通过imageMemoryBarrierCount参数指定,pImageMemoryBarriers是指向VkImageMemoryBarrier数组的指针,数组的每个元素都描述了一个屏障。VkImageMemoryBarrier的定义如下:

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;

每一个VkImageMemoryBarrier类型的数据的sType域都应该被设置为VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,pNext应被置为nullptr。如同其他的内存屏障,srcAccessMask 和dstAccessMask域指定了源和目标访问类型。再有,只有某些访问类型能被应用到图像。同样,跨多个队列控制访问权,srcQueueFamilyIndex和dstQueueFamilyIndex域应该设置为执行工作的源和目标队列所属族的索引。

oldLayout 和newLayout 域指定了屏障前和后的图像所使用的布局。这个和创建图像使用的域是相同的。屏障影响到的图像是通过image指定的,屏障影响到图像的哪些部分是由subresourceRange指定的,它是VkImageSubresourceRange类型的数据,定义如下:

typedef struct VkImageSubresourceRange {

VkImageAspectFlags  aspectMask;

uint32_t  baseMipLevel;

uint32_t  levelCount;

uint32_t  baseArrayLayer;

uint32_t  layerCount;

} VkImageSubresourceRange;

图像aspect作为图像的一部分,被包含在屏障里。大多数图像格式和类型只有一个aspect。一个常见的的例外就是depth-stencil图像,它的深度和stencil成员都有自己的aspect。比如,可以使用aspect标志,来舍弃stencil数据的同时保持深度数据以供后续步骤采样。

对于有mipmaps的图像,mipmap的一部分可以通过baseMipLevel域指定最小数(最高分辨率)mipmap层和levelCount域指定的层被包含进屏障。如果图像不包含完整的mipmap链,baseMipLevel应当被置为0,levelCount应当被置为1.

同理,对于数组图像,图像层的一部分可以通过设置baseArrayLayer为第一层的索引、layerCount为需包含的层数来被包含进屏障里。同样,即使图像不是数组图像,你应设置baseArrayLayer为0,layerCount为1.简单来说,把所有图像想象成它们有mipmaps(即使只有一层),或即使他们都是数组(甚至只有一层)。

Listing 4.1 展示了一个如何操作图像内存屏障的例子

Listing 4.1: Image Memory Barrier

const VkImageMemoryBarrier imageMemoryBarriers =

{

VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,  // sType

nullptr, // pNext

VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT,  // srcAccessMask

VK_ACCESS_SHADER_READ_BIT,  // dstAccessMask

VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL, // oldLayout

VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,  // newLayout

VK_QUEUE_FAMILY_IGNORED,  // srcQueueFamilyIndex

VK_QUEUE_FAMILY_IGNORED,  // dstQueueFamilyIndex

image,  // image

{  // subresourceRange

VK_IMAGE_ASPECT_COLOR_BIT,  // aspectMask

0,  // baseMipLevel

VK_REMAINING_MIP_LEVELS,  // levelCount

0,  // baseArrayLayer

VK_REMAINING_ARRAY_LAYERS  // layerCount

}

};

 

 

 

 

 

 

vkCmdPipelineBarrier(m_currentCommandBuffer,

VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT,

VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,

0,

0,  nullptr,

0,  nullptr,

1,  &imageMemoryBarrier);

在 Listing 4.1中展示的图像内存屏障接受一个之前处于VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL布局且之后转移到VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL布局的图像。数据的来源是管线的颜色输出,经VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT指定的,被shader采样的数据的目的地,由VK_ACCESS_SHADER_READ_BIT指定。

队列间没有所属权的转移,所以,srcQueueFamilyIndex 和 dstQueueFamilyIndex应该置为VK_QUEUE_FAMILY_IGNORED。同样,我们在图像的所有mipmap层和array layers上执行屏障,所以subresourceRange的成员 levelCount and layerCount应各置为VK_REMAINING_MIP_LEVELS和VK_REMAINING_ARRAY_LAYERS。这个屏障接受一个之前被当作管线可写入的颜色附件,后进入可被shader读取的一个状态的图像。

 

 

 

 

如果有任何意见,欢迎留言讨论。


[ 主页 ]
COMMENTS
POST A COMMENT

(optional)



(optional)