GPU是高度并行化的设备,其不仅对处理的数据是并行化的,而且GPU的渲染流程中各个阶段的处理也都是并行化的。如果彼此间真的不存在关联,那么这种并行化就没有任何问题,但是当各个处理流程或者数据间是有关联的,那么这个并行就麻烦了,他们之间一定要有一定的先后顺序,否则就一定后导致混乱的发生。比如说对同一资源的读写访问等,一个读,一个写,如果没有相应的同步机制保障,则必然后导致读写混乱。
vulkan提供了一套完善而复杂的同步逻辑来保证GPU的并发执行,下面我们就来分析一下。
同步原语
Fence
fence用于CPU与GPU之间的同步.
比如说对于UniformBuffer,CPU负责修改其内容,GPU负责读取其内容,由于CPU与GPU是同时各自独立执行的,那么就必然会有同时读写的存在,fence就是为了解决这个问题而产生的。
首先创建fence,并将其设置为unsignaled的状态
1 | vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) |
然后再draw的时候,需要调用vkWaitForFences
,让CPU在当前位置被阻塞掉,然后一直等待到它接受的Fence变为signaled的状态,这样就可以实现在某个渲染队列内的所有任务被完成后,CPU再执行某些操作的同步情景,如UpdateUniformBuffer等操作
然后调用vkResetFences(device, 1, &inFlightFences[currentFrame])
函数将fence的状态切换为unsignaled
最后在调用vkQueueSubmit提交命令时,传入unsignaled的fence
1 | void drawFrame() { |
Semaphore
semaphore用于同步GPU不同的queue之间,或者同一个queue不同的submission之间的执行顺序。
比如说在渲染流程中一般至少存在两个并发任务,一个是渲染(render),一个是呈现(present)。他们都是GPU的任务,按照业务逻辑,需要首先完成渲染,然后再进行呈现。而同样的渲染和呈现是分开独立进行的,如果没有干预,会发生把正在渲染的半成品呈现到屏幕的问题,因此需要同步机制干预,vulkan中用Semaphore来解决这个问题。
一般来讲,对于上面例子的任务,会设置两个Semaphore:
1 | VkSemaphore imageAvailableSemaphore; |
将其设置到提交渲染命令(vkQueueSubmit)的信息中,如下所示:
1 | VkSubmitInfo submitInfo{}; |
也就是说在本次提交到Queue中的命令commandBuffers[imageIndex];
需要等待imageAvailableSemaphore
,然后再执行完毕后,设置renderFinishedSemaphore
。
在present任务中,需要等待renderFinishedSemaphore
。
1 | VkPresentInfoKHR presentInfo{}; |
这样就解决了上面提到的同步问题。
但是上面这样搞,原本的并行就变为了串行了啊,因此在具体的业务实现中,一般采用多buffer的机制,来实现并行处理。
通过上面的例子,我们可以看到Semaphore是解决每次提交的一批命令与另一批命令之间的同步关系的。那么同一批commands之中的command的同步要怎么解决呢?下面的Barrier就会解决这个问题
Event
Event是一个细粒度的同步原语,用于精确的界定管线里发生的操作。
Event用于同步提交到同一队列的不同命令,或者同步CPU和队列。Event不能用于不同队列的命令间的同步。
Event有两种状态signaled和unsignaled。
无论是设备还是主机,都可以直接操作event,不过他们是有区别的,主机可以使用vkGetEventStatus立即获取事件对象的状态,但是不能直接等待事件,而设备正好相反,不能直接获取事件对象的状态,但是能等待一个或者多个事件。
Barrier
上面的fence用于CPU与GPU之间的同步,semaphore用于GPU与GPU之间不同submission间的commandBuffers的同步问题,那么同一个commandBuffers中的不同的command之间的同步问题,该怎么办呢?vulkan提供了Barrier来解决。
Barrier实际上是屏障的意思,something material that blocks or is intended to block passage,就是阻挡通道的一些blocks。那么在queue的commands里面就是插入一个barrier,将所有的commands分成两部分,一部分执行完毕了,后面一部分才能执行。
不同的command执行的先后有什么问题么?如果不涉及到对资源的访问,那么就没什么问题。如果不同的command涉及到了对同一资源的访问以及修改,那么就有问题了,也正是因此我们看Barrier的设置函数中会专门对资源进行设置。
1 | VKAPI_ATTR void VKAPI_CALL vkCmdPipelineBarrier( |
看上面的API,
命令的提交顺序
对于vulkan而言,所有的操作最终都是需要变为command,然后提交到queue中,然后执行的。
然而GPU是并行的,后提交的命令有可能在先提交的命令之前完成,所以需要引入各种的同步机制来保证渲染或者计算业务的正确运行。
虽然执行的快慢不一定,但是命令的提交,或者说执行是有序的,一般而言先提交的命令会先执行,但是不保证先执行的任务先完成。
同时在录制命令的时候,也是有提交顺序的,如下所示:
CPU通过多次调用
vkQueueSubmit
提交的命令,一定是按照vkQueueSubmit
调用的先后顺序提交的,也就是说submitInfo
中的command一定比submitInfo1
中的任务先提交到Queue中。1
2vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]);
vkQueueSubmit(graphicsQueue, 1, &submitInfo1, inFlightFences[currentFrame]);在同一次vkQueueSubmit中,传入了一个或多个
VkSubmitInfo
,这些VkSubmitInfo
中的命令,按照VkSubmitInfo
的下标顺序排列,即在pSubmits
所指向的VkSubmitInfo
数组中,下标靠前的VkSubmitInfo
中所记录的所有命令都在下标靠后的VkSubmitInfo
中所记录的所有命令之前。也就是说submitInfo[0]
一定比submitInfo[1]
先提交到Queue中。1
2
3
4
5VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
VkQueue queue,
uint32_t submitCount,
const VkSubmitInfo* pSubmits,
VkFence fence);同一个
VkSubmitInfo
中,填入了多个commandBuffer
(pCommandBuffers
指向数组),这些commandBuffer
中的命令也是按照下标顺序提交,即commandBuffer[0]
一定比commandBuffer[1]
先提交到Queue中,和上面的逻辑一致。1
2
3
4
5
6
7
8
9
10
11typedef struct VkSubmitInfo {
VkStructureType sType;
const void* pNext;
uint32_t waitSemaphoreCount;
const VkSemaphore* pWaitSemaphores;
const VkPipelineStageFlags* pWaitDstStageMask;
uint32_t commandBufferCount;
const VkCommandBuffer* pCommandBuffers;
uint32_t signalSemaphoreCount;
const VkSemaphore* pSignalSemaphores;
} VkSubmitInfo;同一个commandBuffer中,有两种情况
- 不在
RenderPass
中的命令,即除去所有在vkCmdBeginRenderPass
和vkCmdEndRenderPass
之间的命令,这些命令的提交顺序为按照在CPU上写入CommandBuffer
时的顺序。 - 在
RenderPass
中的命令,只定义在同一SubPass
中的其他命令的提交顺序,这些命令的提交顺序也是按照在CPU上写入CommandBuffer
时的顺序。如果几个命令在vkCmdBeginRenderPass
和vkCmdEndRenderPass
之间,但是它们不在同一SubPass
中,那么它们之间是不存在任何提交顺序的
- 不在
vulkan还提供了一些隐式的同步机制,这些机制实际上都遵循了一个准则,那就是按照提交的先后顺序开始执行,但是不保证结束也是按照顺序完成
所有的Action类命令(Draw、Transfer、Clear、Copy等)以及显示地使用同步机制的命令(这个在之后会介绍),这些命令在执行
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT
时,会遵循提交顺序。即这些命令开始执行的顺序,是严格遵循提交顺序的。(但这并不意味着这些命令结束执行时的顺序会有什么约束,所有的这些命令,到底是哪个先结束,隐式同步并没有严格的规定,也就是说任何一个命令都有可能最先结束。)所有的设置状态类的命令(bind pipelines, descriptor sets, and buffers等),由于它们不需要在GPU上执行,它们只负责设置CPU上相应CommandBuffer的状态,所以它们的执行顺序,遵循它们在CPU上写入CommandBuffer时的顺序。
所有的Draw类命令在处理Primitive时,首先遵循提交顺序,即先提交的Draw中的Primitive会先被处理。而在一个Draw内所提交的Primitive,会按照顶点和索引的下标顺序执行。
ImageLayout的转移,是通过ImageMemoryBarrier实现的(也是一种显式的同步原语),它们遵循提交顺序,即先提交的先转移。
参考资料
【Vulkan学习记录-基础篇-4】Vulkan中的同步机制_syddf-CSDN博客
Understanding Vulkan Synchronization - The Khronos Group Inc