Vulkan 同步机制

大家好,接下来将为大家介绍Vulkan 同步机制。

在Vulkan中,对资源读写所需要的同步操作是应用程序开发者的职责,Vulkan本身只提供了很少的隐式同步机制,其余的都需要在程序中显式地使用Vulkan中的同步机制来实现。

一、提交顺序

提交顺序是Vulkan中的一个非常基本的概念,它本身并不具有任何同步的意义,但是不管是Vulkan提供的隐式同步,还是用户要自己实现的显式同步,都要以这个精确的概念为前提。
在Vulkan中,用户需要将命令写入CommandBuffer中,然后把一个或多个CommandBuffer写入到一个或多个VkSubmitInfo中,再把一个或多个VkSubmitInfo传给vkQueueSubmit,让Queue开始执行向GPU传入的命令,由此,从高往低,提交顺序为:
1.在CPU上通过多次vkQueueSubmit提交了一系列命令,这些命令的提交顺序为调用vkQueueSubmit从前往后的顺序,即先通过vkQueueSubmit提交的命令一定在后通过vkQueueSubmit提交的命令之前。

2.在同一次vkQueueSubmit中,传入了一个或多个VkSubmitInfo,这些VkSubmitInfo中的命令,按照VkSubmitInfo的下标顺序排列,即在pSubmits所指向的VkSubmitInfo数组中,下标靠前的VkSubmitInfo中所记录的所有命令都在下标靠后的VkSubmitInfo中所记录的所有命令之前。

3.在同一个VkSubmitInfo中,填入了一个或多个CommandBuffer,这些CommandBuffer中的命令的提交顺序为按照这些CommandBuffer的下标顺序,类似2中的顺序。

4.在同一个CommandBuffer中,所记录的命令分为两种:
一是不在RenderPass中的命令,即除去所有在vkCmdBeginRenderPass和vkCmdEndRenderPass之间的命令,这些命令的提交顺序为按照在CPU上写入CommandBuffer时的顺序。
二是在RenderPass中的命令,在RenderPass中的命令,只定义在同一SubPass中的其他命令的提交顺序,这些命令的提交顺序也是按照在CPU上写入CommandBuffer时的顺序。注意,如果几个命令在vkCmdBeginRenderPass和vkCmdEndRenderPass之间,但是它们不在同一SubPass中,那么它们之间是不存在任何提交顺序的。

二、Vulkan中的各个同步

1、Fence

Fence用于同步渲染队列和CPU之间的同步,它有两种状态——signaled和unsignaled。
在创建Fence时可以指定它的初始状态,如unsignaled;
在调用vkQueueSubmit时,可以传入一个Fence,这样当Queue中的所有命令都被完成以后,Fence就会被设置成signaled的状态
通过调用vKResetFences可以让一个Fence恢复成unsignaled的状态;

vkWaitForFences会让CPU在当前位置被阻塞掉,然后一直等待到它接受的Fence变为signaled的状态,这样就可以实现在某个渲染队列内的所有任务被完成后,CPU再执行某些操作的同步情景。

举一个具体的例子:假如现在SwapChain中一共有3个Image,然后创建了3个CommandBuffer分别代表在渲染到相应Image时所需要执行的所有命令。在每一帧渲染时,我们需要获取当前需要渲染到的Image的编号,然后使用对应的CommandBuffer,传入渲染队列中,执行渲染命令。那么现在就有一个问题,一个CommandBuffer,如果它还没有被执行完全,那么它是不能够再次被开始执行的。也就是说上面所说的那个获取CommandBuffer后,把它传入渲染队列执行的这样一个CPU上的操作一定要在这个CommandBuffer在上一次被执行完全以后才可以执行。所以这里就遇到了一个渲染队列和CPU之间的一个同步情景,此时可以对每个CommandBuffer分别设置一个Fence来实现这样的一种同步,大体的实现如下(这里用到了Semaphore,不过可以先只关注Fence):
 

	void draw()
	{
		// Get next image in the swap chain (back/front buffer)
		VK_CHECK_RESULT(swapChain.acquireNextImage(presentCompleteSemaphore, &currentBuffer));

		// Use a fence to wait until the command buffer has finished execution before using it again
		VK_CHECK_RESULT(vkWaitForFences(device, 1, &waitFences[currentBuffer], VK_TRUE, UINT64_MAX));
		VK_CHECK_RESULT(vkResetFences(device, 1, &waitFences[currentBuffer]));

		// Pipeline stage at which the queue submission will wait (via pWaitSemaphores)
		VkPipelineStageFlags waitStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
		// The submit info structure specifices a command buffer queue submission batch
		VkSubmitInfo submitInfo = {};
		submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
		submitInfo.pWaitDstStageMask = &waitStageMask;									// Pointer to the list of pipeline stages that the semaphore waits will occur at
		submitInfo.pWaitSemaphores = &presentCompleteSemaphore;							// Semaphore(s) to wait upon before the submitted command buffer starts executing
		submitInfo.waitSemaphoreCount = 1;												// One wait semaphore																				
		submitInfo.pSignalSemaphores = &renderCompleteSemaphore;						// Semaphore(s) to be signaled when command buffers have completed
		submitInfo.signalSemaphoreCount = 1;											// One signal semaphore
		submitInfo.pCommandBuffers = &drawCmdBuffers[currentBuffer];					// Command buffers(s) to execute in this batch (submission)
		submitInfo.commandBufferCount = 1;												// One command buffer

		// Submit to the graphics queue passing a wait fence
		VK_CHECK_RESULT(vkQueueSubmit(queue, 1, &submitInfo, waitFences[currentBuffer]));
		
		// Present the current buffer to the swap chain
		// Pass the semaphore signaled by the command buffer submission from the submit info as the wait semaphore for swap chain presentation
		// This ensures that the image is not presented to the windowing system until all commands have been submitted
		VK_CHECK_RESULT(swapChain.queuePresent(queue, currentBuffer, renderCompleteSemaphore));
	}

由此可见Fence是一种比较粗粒度的同步原语,另一个需要关心的问题是:上面只提到了它在操作执行方面的同步,而Vulkan中非常重要的另一种——内存数据的同步,Fence能不能做到呢?

事实上,Fence也具备这种内存数据同步的功能,但是并不需要手动地指定,在使用Fence时,如果它一旦被signaled,那么使用这个Fence的Queue中的所有的命令如果涉及到了对内存的修改,那么这些Memory Access就一定会再signaled之前在Device上变得available(注意只是在Device上有这个效果,如果在CPU上读相关的内存数据,并不能保证读到的是最新的值,所以如果确保CPU也能够获取最新的值的话,就需要再用上其他的同步原语)。

2、Semaphore

Semaphore用于渲染队列每次提交的一批命令(batch)之间的同步,和Fence一样,它也有两种状态:signaled和unsignaled。
调用vkQueueSubmit提交命令时,会填充VkSubmitInfo结构,而这个结构体中需要填入pWaitSemaphores、pSignalSemaphores、pWaitDstStageMask,表示此次提交的所有命令在执行到pWaitDstStageMask时,要停下,必须要等待pWaitSemaphores所指向的所有Semaphore的状态变成signaled时才可以继续执行,此次提交的所有命令结束以后,pSignalSemaphores所指向的所有Semaphore的状态都会被设置成signaled。

可以看到Fence和Semaphore都会在vkQueueSubmit时作为参数传入,不同之处是,Fence用于阻塞CPU直到Queue中的命令执行结束(GPU、CPU之间的同步),而Semaphore用于不同的命令提交之间的同步(GPU、GPU之间的同步)。

在Fence中给出的那段代码中,使用了两个Semaphore,用于控制queue提交的present命令(注意swapChain.queuePresent()的实现也是通过queue提交了一个执行present的命令)和render命令之间的同步:在渲染时,需要将渲染的结果写入到ColorAttachment中,我们必须要等待上一次把这个ColorAttachment给present到屏幕上的命令结束以后,才可以完成这个写入操作;
并且,将当前帧渲染结果显示到屏幕上的这个present命令,必须要等到当前帧的render命令完全执行结束以后,才可以开始执行。

和Fence一样,Semaphore也是一种粗粒度的同步,它本身也提供了隐含的内存数据的同步:
1.当让一个semaphore变成signaled时:semaphore之前的所有命令涉及到的内存写操作,都会在semaphore变成signaled之前,达到available的状态
2.当等待一个semaphore变成signaled时:在semaphore变成signaled之后,所有暂停的命令被重新唤醒继续执行之前,所有此后相关的Memory Access,都会达到visible的状态。
也就是说在使用Fence和Semaphore时,一般是不需要对GPU上有关的任何Memory Access做同步处理,这些都会被自动完成。但是,这些隐含的同步只是针对GPU的,CPU上所需要的内存数据同步操作必须由应用程序显式完成,比如:当CPU需要读一个经由GPU修改过的内存数据,就需要加一个MemoryBarrier来确保CPU读到的是最新的数据。

还有一点值得注意的是,在讨论Fence和Semaphore时,都提到了vkQueueSubmit函数,这个函数本身也是隐含了一个内存数据的同步的:就是CPU上所有的内存修改操作,都会在GPU读写之前,对GPU而言变成available的,并且对于所有之后GPU上的MemoryAccess,它们都是visible的。

3、Event

Event用于同步提交到同一队列的不同命令,或者同步CPU和队列。它同样也具有两种状态——signaled和unsignaled,与Fence不同的是,它的状态改变既可以在CPU上完成,也可以在GPU上完成,并且它是一种细粒度的同步机制。注意:Event不能用于不同队列的命令之间的同步。

在CPU上,可以调用vkSetEvent来使一个Event变成Signaled的状态;可以调用vkResetEvent来使一个Event变成Unsignaled的状态;可以调用vkGetEventStatus来获取一个Event的当前状态,可以利用这个当前状态来对CPU进行阻塞。

而在GPU上:
1.可以通过vkCmdSetEvent命令来使得一个Event变成Signaled状态,此时该命令附带了一个操作执行同步:根据提交顺序,所有在该命令之前的所有命令都必须在此次把Event设置Signaled状态之前完成。
2.可以通过vkCmdResetEvent命令来使得一个Event变成Unsignaled状态,此时该命令附带了一个操作执行同步:根据提交顺序,所有在该命令之前的所有命令都必须在此次把Event设置Unsignaled状态之前完成。
这里有一点非常需要注意:vkCmdSetEvent和vkCmdResetEvent不能够在一个RenderPass内被执行。

发布了58 篇原创文章 · 获赞 67 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/u010281924/article/details/105379542