我们有了交换链,也有了命令缓冲区,并且 Vulkan 本身也已初始化。现在是时候真正编写渲染循环了。在这里,我们将使用 VkCmdClearColorImage 来“绘制”我们的帧。这将给我们一个闪烁的颜色。为了实现这一点,我们需要能够与交换链交互,以便从交换链中获取图像,用颜色清除它,然后在屏幕上显示该图像。为此,我们将设置同步结构,使我们能够将来自交换链的 OS/GPU 操作与我们的清除命令同步。
同步
Vulkan 提供了显式的同步结构,允许 CPU 将命令的执行与 GPU 同步。还可以控制 GPU 中执行的顺序。默认情况下,一旦您通过队列或其他操作向 GPU 发送一些命令,这些命令将没有任何限制,驱动程序/gpu 将根据其认为合适的方式执行它们。如果我们想执行多个操作,并且希望它们按给定的顺序执行,我们需要使用同步系统。我们为此准备了 Fence 和 Semaphore。
VkFence
这用于 GPU -> CPU 通信。许多 Vulkan 操作(例如 vkQueueSubmit)允许使用*可选的* fence 参数。如果设置了此参数,我们可以从 CPU 知道 GPU 是否已完成这些操作。我们将使用它来同步 CPU 中的主循环和 GPU。Fence 将在作为命令的一部分提交后发出信号,然后我们可以使用 VkWaitForFences 让 CPU 停止,直到这些命令执行完毕。我们将有两个 fence,每个 FrameData 结构一个。
伪代码示例
//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)支持 Signal 或 Wait 信号量。
给定的信号量充当多个 gpu 队列操作之间的链接。一个操作必须发出信号量,另一个操作必须等待它。可以有多个 GPU 操作等待给定的信号量,但您只能从一个操作发出信号量。如果一个操作等待信号量,则意味着它在从其他操作完成时发出信号量之前不会开始执行。
线性化 3 个操作的伪代码示例
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 在执行后发出 Task1 信号,这将开始在 Beta 上执行,然后在 Beta 完成执行后,它将发出 Gamma 信号以供执行。
如果在这种情况下不使用信号量,则 3 个操作的命令可能会并行执行,彼此交错。也可以将信号量用于跨队列操作,例如,您可能希望这样做以在后台执行计算着色器,而主图形队列正忙于某些通道,或者将专用的 Present 队列(将图像放在屏幕上)与绘制图像的图形队列同步。
渲染循环
对于我们的渲染循环,我们使用双缓冲渲染结构。这样,当 gpu 忙于执行一帧的命令时,CPU 可以继续处理下一帧。但是,一旦计算出下一帧,我们需要停止 CPU,直到第一帧执行完毕,以便我们可以再次记录其命令。
对于我们的渲染工作,我们需要将其与交换链结构同步。如果我们正在进行无需与屏幕同步的无头绘制,则不需要这样做。但是我们正在绘制到窗口中,因此我们需要向操作系统请求要绘制的图像,然后在上面绘制,然后向操作系统发出信号,表明我们要将该图像显示在屏幕上。
图像布局
GPU 以不同的格式存储图像,以满足其内存中的不同需求。图像布局是 vulkan 对这些格式的抽象。只读图像的布局将与要写入的图像的布局不同。要更改图像的布局,vulkan 使用管线屏障。管线屏障是一种同步来自单个命令缓冲区的命令的方法,但它也可以执行诸如转换图像布局之类的操作。布局的实现方式因供应商而异,某些转换实际上在某些硬件上是空操作。为了正确进行转换,必须使用验证层,验证层将检查图像是否处于针对任何给定 GPU 操作的正确布局上。如果您不这样做,则代码在 NVidia 硬件上完全正常工作,但在 AMD 上或相反的情况下会导致故障,这种情况非常常见。
我们从交换链获得的图像将处于无效状态,因此如果我们想在其上使用 VkCmdDraw 或任何其他绘制操作,我们需要首先将图像转换为可写布局。一旦我们完成绘制命令,我们需要将其转换为交换链用于屏幕输出的布局。
在旧版本的 vulkan 中,这些布局转换将作为 RenderPass 的一部分完成。但是我们使用的是 vulkan 1.3 并且我们使用动态渲染,这意味着我们将手动执行这些转换,另一方面,我们节省了构建完整渲染通道的所有工作和复杂性。如果您想了解有关渲染通道的信息,旧版本的教程在此处进行了解释
让我们开始编写渲染循环的代码。
下一步: 主循环代码