Moving Data
Moving Data
通常来说,图形操作和计算操作数据密集型操作。Vulkan提供了几种对象来用来存储和操作数据,所以将数据移动到这类对象内部或从对线中移出就很重要。vulkan提供了一些这样方法:数据复制,填充buffer或image对象。此外,资源任何时候都可能处于某种状态,并且vulkan的pipeline许多地方可能需要访问他们。
Managing Resource State
程序运行的任何时候,每个资源都可能可处于不同状态。比如,如果图形管线正在向图像绘制或将图像当作纹理进行读取,又或者vulkan正在从host内存将数据复制进image,这些使用场景都导致状态的不同。在某些vulkan实现中,这些状态可能没有真正的区别,而对另一些实现,准确了解资源在某一特定时间点的状态可以让程序使是否正常工作或者正在渲染无用功之间产生区别。
因为command buffers中的命令大多数时候负责资源的访问,同时又因为command buffer构建的顺序和提交的顺序可能不同,所以尝试追踪资源的状态并且确保资源状态正确对于vulkan实现来说是不实际的事情。尤其资源还可能因为命令的执行在状态之间切换。虽然驱动程序可以在command buffer使用资源时追踪资源的状态,但command buffer提交后在进行追踪将需要大量的工作。所以,一般这个责任由应用程序提供。资源状态可能对image来说尤其重要,因为他们复杂并且具有结构。
一个image的状态由两部分组成:image的layout,用来决定image如何在内存中布局以及最后写入image的记录,用来影响设备上的缓存一致性。一个image的初始布局在创建时指定,并且有可能在image的生命周期中发生改变,要么是显式使用屏障 或隐式被renderpass改变。屏障还可以整理vulkan管线不同部分中访问资源的地方,在某些情况下,将资源从一个布局过渡到另一个布局可以在其他的中间管道的同步中由屏障完成。
Pipeline Barriers
屏障是一种用来给内存访问管理提供同步机制,以及在vulkan管线中实现资源状态切换。接口为vkCmdPiplineBarier。
执行该屏障的commandbuffer由参数commandBuffer传递。接下来的两个参数,srcStageMask和dstStageMask分别指定了最后向资源写入的管线阶段和接下来哪一阶段会读取资源。即,他们制定了屏障代表的数据流的来源和目的地。二者都属于VkPipelineStageFlagBits成员:
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT:只要设备开始处理命令,就被认为到达了的管线的顶端。VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT:当管线执行一个间接 命令时,它会从内存中获得一些命令参数。该阶段就是获取那些参数的阶段。VK_PIPELINE_STAGE_VERTEX_INPUT_BIT:该阶段会从vertex buffer中获取vertex属性。之后,顶点缓冲区的内容可以被覆盖,即使vertex shader还没有完全执行完毕。VK_PIPELINE_STAGE_{VERTEX,TESSELLATION_CONTROL,TESSELLATION_EVALUATION,GEOMETRY,FRAGMENT}_SHADER_BIT:当由绘图命令产生的所有对应shader阶段完成后,对应阶段完成。VK_PIPELINE_STAGE_EARLY_FRAGMENT_TESTS_BIT:所有在fragment shader启动前进行片元测试都完成,该阶段完成。VK_PIPELINE_STAGE_LATE_FRAGMENT_TESTS_BIT:所有fragment shader完成后进行的片元测试都完成。注意,输出到深度-模板附件发生在该测试中,所以该阶段和early fragment test阶段都包括深度-模板输出。VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT:由该管线产生的片元写入到颜色附件中。VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT:作为调度结果的所有compute shader都完成。VK_PIPELINE_STAGE_TRANSFER_BIT:任何由于接口调用触发的移动操作,比如vkCmdCopyImage和vkCmdCopyBuffer,都已经完成。VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT:所有认为是图形管线中的操作都已经完成。VK_PIPELINE_STAGE_HOST_BIT:该管线阶段与访问host有关。VK_PIPELINE_STAGE_ALL_GRAPHICS_BIT:当该位当作目的地使用时,意为任何管线阶段都可能访问内存。如果作为源头,它等同于VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT‘。VK_PIPELINE_STAGE_ALL_COMMANDS_BIT:这是个万能钥匙,如果你不知道接下来要发生什么,就用它。它会使一切都同步。不过请理智使用。
因为由srcStageMask和dstStageMask指定的东西会表明事情发生的时间,所以vulkan可以接收移动他们或以各种方式解释他们。srcStageMask指定了源阶段完成资源的读写的时间。因此,将该阶段在管线中的有效位置移动到后面并不会改变这些访问已经完成的事实,只是可能意味着实现会等待更长的时间。
同样,dstStageMask指定了管线需要在继续执行之前的等待点。如果将该点向前移动,同样是可以的。
dependencyFlags参数指出了屏障引用的资源的关联性。唯一的可能是VK_DEPENDENCY_BY_REGION_BIT,代表了屏障之影响源阶段编辑的那部分数据,这部分数据也会被目的阶段使用。
Global Memory Barriers
由vkCmdPipelineBarrier触发的内存屏障数量由memoryBarrierCount指出,pMemoryBarriers指向由VkMemoryBarrier组成的数组。
结构体包含两个字段srcAccessMask和dstAccessMask。他们都是VkAccessFlagBits一员。
内存屏障提供两个重要的功能,一是避免冲突,而是确保数据一致性。
冲突是因为读写操作按照程序员不期望的顺序执行,他们很难检查出来。有三种冲突类型:
- read-after-write:错误发生在程序员希望读取一个最近写过的数据。如果读操作结束在写操作完成之前,那么读操作会读取旧数据。
- write-after-read:错误发生在程序员希望写一个刚刚读取过的数据。如果写操作发生在读操作完成之前,那么读操作会看到新数据。
- write-after-write:程序员希望多次覆写内存中的同一位置,并且只有最后一次写入结果可见。如果写入操作发生重排,那么最后结果就会发生错误。
举个例子,为了确保所有纹理数据读取发生在图像赋值操作之前,我们需要在srcAccessMask指定VK_ACCESS_SHADER_READ_BIT,在dstAccessMask指定VK_ACCESS_TRANSFER_WRITE_BIT。这会告诉vulkan第一个阶段让shader从image中读取数据,然后第二阶段可能会覆写该image。所以我们不要重新排序复制操作在任何shader读取操作之前。
注意到VkAccessFlagBits和VkPipelineStageFlagBits有点区别。VkAccessFlagBits指定什么操作会发生,VkPipelineStageFlagBits指出管线的哪一阶段发生。
内存屏障提供的第二个功能是管线不同部分的内存视图一致性。比如一个应用程序会从shader中向buffer写入数据,然后再利用这个buffer从中通过映射底层内存对象再读取,我们应该指定srcAccessMask为VK_ACCESS_SHADER_WRITE_BIT,dstAccessMask为VK_ACCESS_HOST_READ_BIT。如果在设备中buffer写入是通过缓存实现的,我们还需要将缓存刷新回去让host看见写入结果才行。
Buffer Memory Barriers
当buffer的所有权从一个队列转到另一个队列上,而且二者隶属于不同的队列家族,我们需要在srcQueueFamilyIndex和dstQueueFamilyIndex分别指定哪一队列家族。如果没有所有权转移,那么二者可以设置为VK_QUEUE_FAMILY_IGNORED。该情况下,唯一的所有权被假定为构建的command buffer的家族队列。
命令相关的buffer由buffer指定,指定同步访问范围由offset和size指定,以字节为单位。
如果buffer将被多个队列家族的队列在执行工作的时候访问,你需要执行额外的工作。因为单个设备暴露出来的多个队列家族可能由多个物理组件构成,这些组件拥有各自的缓存,调度架构,内存控制器等,vulkan需要知道资源什么时候会从一个队列转移到另一个队列。该情况下,需要指定srcQueueFamilyIndex和dstQueueFamilyIndex。
Image Memory Barriers
oldLayout和newLayout字段指定了在屏障前后的图像布局。
Clearing and Filling Buffers
为了让buffer可用,你需要在buffer中填上数据。简单的方式是用特定的值填充。这允许你初始化一个buffer,以便将来使用它。调用接口vkCmdFillBuffer
注意,该命令中区域的划分以字节为单位且二者必须是4的倍数,但是填充单位则是按照一个uint32_t为单位填充的,它会将buffer按照uint32_t为大小进行划分,如果你想向其写入一个32位浮点数,将浮点数重新翻译为uint32_t,比如*(const uint32_t*)&value
有时,用固定值填写buffer是不够的,我们还需要更明显的在buffer对象中放置数据的方式。如果大量的数据需要在buffer中转移,合适的方法有很多,比如从host处映射buffer然后写入,或者使用接口vkCmdCopyBuffer从另一个buffer中转移。如果需要小范围的更新,那么使用vkCmdUpdateBuffer。
命令直接从host内存复制到buffer中。当接口返回时,我们就可以释放host内存或覆写其内容。不过注意,必须保证命令在被提交到设备执行之前都没有改变。offset和size必须是4的倍数。能够更新的最大值为65536字节,而且VK_WHOLE_SIZE不可用。
我们需要保证我们的数据必须是size字节长。
最后,使用vkCmdFillBuffer需要小心谨慎,它的目的是为了小的,快速的buffer更新。
Clearing and Filling Images
Images更大且复杂,他们是不透明的数据结构,所以offset和data数据对于应用程序是不可见的。
将image清理为固定值,使用vkCmdClearColorImage。命令中,可用的layout为VK_IMAGE_LAYOUT_GENERAL和VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL。为了清理image,你需要将image的layout使用屏障转移到二者之一。
清理用的值为VkClearColorValue。你可以将一个image的不同区域清理为同一颜色,所有区域由一个VkImageSubresourceRange数组指定。
结构体中,aspectMask必须是VK_IMAGE_ASPECT_COLOR_BIT。
清理一个深度-模板图像与其类似,使用一个VkClearDepthStencilValue结构体,接口为vkCmdClearDepthStencilImage。如果你将subresource的aspectMask设置为depth,那么之后只有值中的depth部分会使用,如果设置为stencil,只有值中的stencil部分会使用。通常我们同时将aspectMask设置为二者,这样更有效率。
Copying Image Data
你可能需要更新纹理数据到image对象或者复制在image之间复制数据。vulkan提供从buffer中复制数据到image,image之间的数据复制以及从image中复制到buffer。
将buffer中数据复制到image中的多个区域中,调用vkCmCopyBufferToImage。
用来执行该命令的command buffer由参数commandBuffer指定,源buffer由srcBuffer指定,目的image由dstImage指定。如同清理image操作一样,目的image的layout为VK_IMAGE_LAYOUT_GENERAL以及VK_IMAGE_LAYOU_TRANSFER_DST_OPTIMAL二者之一,layout由参数dstImageLayout指定。需要更新的区域数量由regionCount表示,pRegions指向一个VkBufferImageCopy数据,每个元素定义一个复制区域。
该结构体中,bufferOffset字段指定buffer中数据的偏移字节位置。数据在buffer中排布式从左到右,从上到下的,就像下图这样。

bufferRowLength字段指定了原图像中一行纹素的数量,而bufferImageHeight指定了图像中数据的行数。如果bufferRowLength为0,那么假定图像被buffer紧紧包围,即等于字段imageExtent.width。如果bufferImageHeight为0,那么假定图像数据行数与imageExtent.height相同。
将数据复制到image中的子资源部分由VkImageSubresourceLayers指定。结构体中aspectMask指定哪一部分会受到复制操作。如果目标image是颜色图像,那么应该设为VK_IMAGE_ASPECT_COLOR_BIT。如果是深度图像,则为VK_IMAGE_ASPECT_DEPTH_BIT。模板图像则为VK_IMAGE_ASPECT_STENCIL_BIT。深度模板图像则为二者的组合。
目标的mipmap等级由mipLevel指定。你可将所有pRegions的数据复制到单一的mipmap级中,也可以指向不同的等级。
如果目标图像是array image,那么你应该指定起始层和层数。否则设为0和1。
每个目标区域可以是整个mipmap level或者是某个mipmap级的一部分,每个部分的偏移量由imageOffset指定,大小由imageExtent指定。如果想要覆写整个mipmap,将imageOffset.x和imageOffset.y设为0,imageExtent设为和mipmap level同样的大小,vulkan不会帮你计算这些。
从image复制到buffer也是可能的,调用vkCmdCopyImageToBuffer,源图像的layout为VK_IMAGE_LAYOUT_GENERAL和VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL二者之一。同样也有一组区域来指定复制的范围,同样为VkBufferImageCopy结构。
在两个image之间复制使用vkCmdCopyImage
这回复制的区域由VkImageCopy指定。
每个VkImageCopy实例指定了复制的字资源信息和双方在图像中的偏移量。vkCmdCopyImage不能重设image数据的答案小,所以无论是源图像还是目的图像,他们每一个复制的区域大小都是相同的,由extent字段指定。
Stretching Images
为了支持格式转换或者对复制的数据重设大小,使用vkCmdBlitImage命令,该命令可以使用不同格式的图像,然后拉伸用于复制的区域将其写进目标图像中。blit 术语是block image transfer 的缩写,指的是不仅是复制图像,还潜在的在复制时进行了处理。
vkCmdBlitImage的原型为
void vkCmdBlitImage(
VkCommandBuffer commandBuffer,
VkImage srcImage,
VkImageLayout srcImageLayout,
VkImage dstImage,
VkImageLayout dstImageLayout,
uint32_t regionCount,
const VkImageBlit* pRegions,
VkFilter filter);
VkImageBlit结构体中,srcSubresource和dstSubresource指定了各自的子资源。在VkImageCopy中,一块被复制的区域由一个VkOffset3D和VkExtent3D指出,而VkImageBlit中,一块区域由一对VkOffset3D指出。第一个指出区域的一角,第二个指出另一角。如果两个区域发现是颠倒的,那么复制的区域将被垂直翻转。同样,如果一个区域相对另一个区域左右颠倒,那么图像将被水平翻转。如果两个条件都满足,那么复制区域将相对于原始区域旋转180。
如果区域大小不同,那么图像数据会放大或缩小。该情况下,由filter参数指出的过滤模式会起作用。filter一定是VK_FILTER_NEAREST或VK_FILTER_LINEAR之一,分别代表着临近采样和线性采样。
源图像的格式一定要支持VK_FORMAT_FEATURE_BLIT_SRC_BIT。在大多数的实现中,几乎所有格式都支持该特性。目的图像的格式也一定支持VK_FORMAT_FEATURE_BLIT_DST_BIT。一般来说,这是可以让shader用来写入或渲染的任何格式。任何vulkan设备都不太可能会让压缩图像格式支持blitting。

浙公网安备 33010602011771号