本文主要是介绍Vulkan Synchronization 同步,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
Vulkan的同步有两个作用,一是控制Command的执行顺序,二是控制缓存的刷新。
Vulkan会保证在同一个Command Queue中的Command按顺序开始执行,但并不会保证谁先执行完成,而且会多个Command同时执行。例如Command1 开执行后会,立即开始执行Command2,两个命令会同时执行,且不保证谁先执行完。
GPU是incoherent caches(缓存不一致),Vulkan也不会保证缓存一致性,所以需要控制缓存的刷新,保证后面执行的Command读取到前面Command的执行结果。
Execution Barriers
VKAPI_ATTR void VKAPI_CALL 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);
Execution Barriers是用来控制命令的执行顺序的。函数vkCmdPipelineBarrier设置前3个参数(commandBuffer、srcStageMask、dstStageMask),其他参数设为null或0,就是设置了一个Execution Barriers。
设置Execution Barriers后会将Queue中的所有Command分成两部分,即Execution Barriers之前的Command和Execution Barriers之后的Command。这里的Command不限于当前commanBuffer中的Command,还包括当前commanBuffer提交之前提交的commandBuffer中的Command和提交之后的,也就是说Execution Barriers在queue中是全局。Execution Barriers的作用是:之后的Command执行dstStageMask阶段,需要等待之前Command执行完srcStageMask阶段。
例如以下代码:
vkCmdPipelineBarrier(commandBuffer, TRANSFER_BIT, FRAGMENT_SHADER_BIT);
代码之后的命令执行到FRAGMENT_SHADER_BIT时,需要之前所有命令都执行完了TRANSFER_BIT。
Memory Available and Visible
GPU缓存如下图:
Vulkan并不会保证缓存一致性,所以单纯的控制执行顺序并不能保证后面执行的Command读取到前面Command的执行结果。因为Command执行后其执行结果会存在L1缓存,并不会立即刷新到L2缓存,所以后面在不同的SM上执行的Command就不能读取到最新的结果。为了保证后面执行的Command读取到前面Command的执行结果,还需要控制缓存的刷新。
Memory Available
当L2缓存中包含了最新的数据时,则当前的Memory是Avilable状态。如果一个Command执行时修改了数据,则对应的Memory是Undefined状态,此时其他命令读取Memory会读到错误的数据。为了让Memory在Command执行完后变成Available状态,需要在Comand后面的vkCmdPipelineBarrier中MemoryBarrier的srcAccessMask设置一个VK_ACCESS_*_WRITE_BIT。这样Command执行完srcStageMask阶段后,会将执行结果更新到L2缓存。
Memory Visible
当前L1缓存中包含了最新数据,并设置了访问权限时,则当前Memory是Visible状态,也就是当前SM单元可以读取或修改数据。为了让Memory在Command执行完后变成Visible状态,首先需要保证数据为Avilable状态,然后在vkCmdPipelineBarrier中MemoryBarrier的dstAccessMask设置一个VK_ACCESS_*_WRITE_BIT/VK_ACCESS_*_READ_BIT权限。这样Command执行完dstStageMask阶段时,L1会重新刷新,从L2中读取到最新数据。
其他
- srcAccessMask不需要设置*READ_BIT标志,srcAccessMask是用来控制L1缓存中的结果写入到L2缓存的,*READ_BIT标志并不会有任何作用。
- TOP_OF_PIPE/BOTTOM_OF_PIPE阶段并执行任何Command,这两个阶段srcAccessMask/dstAccessMask只能设置为0。
- “making memory available” is all about flushing caches,将最新数据写入L2缓存
- “making memory visible” is invalidating caches。无效化缓存,加载最新的数据。
Image Memory Barrier and Layout
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;
Image对象使用前需要设置Layout,因为不同的Layout有不同的用途。例如:纹理用作Frame Buffer时Layout需要是COLOR_ATTACHMENT_OPTIMAL,纹理用作着色器采样时Layout需要是SHADER_READ_ONLY_OPTIMAL。
VkImageMemoryBarrier可以用来转换纹理的Layout,这个转换过程可以看作一次读写,转换时会改变Image Memory的字节格式,会先读取Image然后再写入成新的格式。转换前需要保证Image Memory状态为Available,转换后数据会写入L2缓缓,Image Memory状态自动设置为Available,但不是Visible。
新创建的ImageMemory状态默认是Available和Visible,所以可以直接转换Layout。
Image Alias Memory
使用别名会同时将多个Image绑定到同一块Memory中,所以使用时可能其他Image已修改过Memory导致Memory状态是Undefined,不是Available。所以使用前需要增加一个VkImageMemoryBarrier保证Memory为Available。
例如Image1和Image2都绑定到同块Memory,并且都用于RenderPass渲染,使用时如下:
- vkCmdPipelineBarrier(image = image1, oldLayout = UNDEFINED, newLayout = COLOR_ATTACHMENT_OPTIMAL, srcStageMask = COLOR_ATTACHMENT_OUTPUT, srcAccessMask = COLOR_ATTACHMENT_WRITE, dstStageMask = COLOR_ATTACHMENT_OUTPUT, dstAccessMask = COLOR_ATTACHMENT_WRITE|READ) // 1
- vkCmdBeginRenderPass/EndRenderPass
- vkCmdPipelineBarrier(image = image2, …) // 与1相同
- vkCmdBeginRenderPass/EndRenderPass
注:因为RenderPass中可以包含vkCmdPipelineBarrier,所以实际使用时可以不需要写上面两个vkCmdPipelineBarrier,这个两个vkCmdPipelineBarrier可以包含到RenderPass中
Semaphores and Fences
Semaphores和Fences可以用在vkQueueSubmit中进行同步,用于Command Queue,Host,Present中进行同步。
当前Semaphores或Fences激活(signaled)时所有的Memory状态会被设为Available。相当于添加了一个MemoryBarrier,srcStageMask = ALL_COMMANDS_BIT,srcAccessMask = MEMORY_WRITE_BIT。
当前获得(wait)Semaphores或Fences,所有的Memory状态会被设为Visible。
Host Memory
更新数据
当更新了UniformBuffer后,并不需要手动添加Barrier同步更新后的数据,当vkQueueSubmit时会自动同步,会将对应的Memory设置Avaiable和Visible。
如果更新数据在vkQueueSubmit后面则需要手动设置Barrier同步,设置如下:
- srcStageMask = HOST
- srcAccessMask = HOST_WRITE_BIT
- dstStageMask = TRANSFER
- dstAccessMask = TRANSFER_READ
读取数据
当CPU端需要读取GPU数据时,通常需要使用Fences同步,当获得(wait)Fences时会自动将Memory状态设为Visible,但这个仅限于GPU内部。所以还需要手动添加Barrier用于刷新缓存,设置dstStageMask = PIPELINE_STAGE_HOST和dstAccessMask = ACCESS_HOST_READ_BIT。这样Command执行后会将缓存刷新到CPU内存,CPU就可以读取到最新数据。
总结
Vulkan同步会将Command分成前面的和后面的两个部分,同步主要是用于控制后面的Command执行顺序,以及后面的Command读取到最新的数据。
不同的同步方式:
参考:
Yet another blog explaining Vulkan synchronization – Maister's Graphics Adventures
Vulkan Synchronization
这篇关于Vulkan Synchronization 同步的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!