我们有了渲染通道,我们也有了命令缓冲区,并且我们已经初始化了 Vulkan 本身。现在是时候真正开始编写渲染循环本身了。
同步
Vulkan 提供了显式同步结构,以允许 CPU 与 GPU 同步命令的执行。并且还控制 GPU 中执行的顺序。所有执行的 Vulkan 命令都将进入队列并将“不停地”执行,并且顺序未定义。
有时,您明确希望确保某些操作在执行新操作之前已完成。虽然对给定 VkQueue 执行的操作是线性的,但如果您有多个队列,则顺序无法保证。为此,以及为了与 CPU 通信,我们有两个结构
VkFence
这用于 GPU -> CPU 通信。许多 Vulkan 操作(例如 vkQueueSubmit)允许一个可选的 fence 参数。如果设置了此参数,我们可以从 CPU 知道 GPU 是否已完成这些操作。我们将使用它来同步 CPU 中的主循环和 GPU。
伪代码示例
//we have a fence object created from somewhere
VkFence myFence;
//start some operation on the GPU
VkSomeOperation(whatever, myFence);
// block the CPU until the GPU operation finishes
VkWaitForFences(myFence);
//fences always have to be reset before they can be used again
VkResetFences(myFence);
VkSemaphore
这用于 GPU 到 GPU 同步。信号量允许定义 GPU 命令的操作顺序,并使它们一个接一个地运行。一些 Vulkan 操作(如 VkQueueSubmit)支持发出信号或等待信号量。
如果将其设置为发出信号量,则意味着该操作将在执行时立即“锁定”所述信号量,并在执行完成后解锁。
如果将其设置为等待信号量,则意味着该操作将等待直到该信号量被解锁才开始执行。
伪代码示例
VkSemaphore Task1Semaphore;
VkSemaphore Task2Semaphore;
VkOperationInfo OpAlphaInfo;
// Operation Alpha will signal the semaphore 1
OpAlphaInfo.signalSemaphore = Task1Semaphore;
VkDoSomething(OpAlphaInfo);
VkOperationInfo OpBetaInfo;
// Operation Beta signals semaphore 2, and waits on semaphore 1
OpBetaInfo.signalSemaphore = Task2Semaphore;
OpBetaInfo.waitSemaphore = Task1Semaphore;
VkDoSomething(OpBetaInfo);
VkOperationInfo OpGammaInfo;
//Operation gamma waits on semaphore 2
OpGammaInfo.waitSemaphore = Task2Semaphore;
VkDoSomething(OpGammaInfo);
此代码将在 GPU 中按严格顺序执行 3 个 DoSomething。GPU 端命令的执行顺序将为 Alpha->Beta->Gamma。在 Alpha 完全完成执行之前,操作 Beta 不会开始。
如果您在这种情况下不使用信号量,则 3 个操作的命令可能会并行执行,彼此交错。
渲染循环
在渲染循环中,我们将使用单个 fence 来等待直到 GPU 完成执行渲染工作。目前我们不打算做任何高级操作,我们只会等待发送到 GPU 的工作执行完毕,然后开始准备下一帧。在本教程的第 4 章中,我们将更改此设置,以允许 CPU 在 GPU 忙于提交时继续准备下一帧。
在执行渲染循环时,我们需要从交换链请求图像。从交换链请求图像将阻塞 CPU 线程,直到图像可用。使用 vsync-d 模式将完全阻塞 CPU,而其他模式(如 Mailbox)将几乎立即返回。
我们将通过请求图像开始渲染循环,这将为我们提供一个整数图像索引。然后我们将此索引与我们制作的帧缓冲区数组一起使用。
然后我们重置命令缓冲区,并开始渲染命令。渲染命令完成后,我们将其提交到图形队列,然后通过调用 VkQueuePresent()
将我们刚刚渲染的图像呈现到窗口。
让我们开始编写渲染循环的代码。
下一步:编程渲染循环