我们将需要在 VulkanEngine 类中添加几个成员,以保存主循环所需的同步结构(信号量和围栏)。 加上初始化它们的函数。
class VulkanEngine {
public:
//--- other code ---
VkSemaphore _presentSemaphore, _renderSemaphore;
VkFence _renderFence;
private:
//--- other code ---
void init_sync_structures();
}
//make sure to call the new init function from end of the main init
void VulkanEngine::init()
{
//--- other code ---
init_sync_structures();
//everything went fine
_isInitialized = true;
}
我们将需要 2 个信号量和主渲染围栏。 让我们开始创建它们。
void VulkanEngine::init_sync_structures()
{
//create synchronization structures
VkFenceCreateInfo fenceCreateInfo = {};
fenceCreateInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceCreateInfo.pNext = nullptr;
//we want to create the fence with the Create Signaled flag, so we can wait on it before using it on a GPU command (for the first frame)
fenceCreateInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
VK_CHECK(vkCreateFence(_device, &fenceCreateInfo, nullptr, &_renderFence));
//for the semaphores we don't need any flags
VkSemaphoreCreateInfo semaphoreCreateInfo = {};
semaphoreCreateInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
semaphoreCreateInfo.pNext = nullptr;
semaphoreCreateInfo.flags = 0;
VK_CHECK(vkCreateSemaphore(_device, &semaphoreCreateInfo, nullptr, &_presentSemaphore));
VK_CHECK(vkCreateSemaphore(_device, &semaphoreCreateInfo, nullptr, &_renderSemaphore));
}
创建围栏和信号量非常简单,因为它们是相对简单的结构。
绘制循环
让我们通过首先等待 GPU 完成其工作来开始绘制循环,使用围栏
void VulkanEngine::draw()
{
//wait until the GPU has finished rendering the last frame. Timeout of 1 second
VK_CHECK(vkWaitForFences(_device, 1, &_renderFence, true, 1000000000));
VK_CHECK(vkResetFences(_device, 1, &_renderFence));
}
我们使用 vkWaitForFences()
等待 GPU 完成其工作,之后我们重置围栏。 围栏必须在使用之间重置,您不能在不重置中间围栏的情况下在多个 GPU 命令上使用同一个围栏。
WaitFences 调用的超时时间为 1 秒。 它使用纳秒作为等待时间。 如果您使用 0 作为超时时间调用该函数,您可以使用它来了解 GPU 是否仍在执行命令。
接下来,我们将从交换链请求图像索引。
//request image from the swapchain, one second timeout
uint32_t swapchainImageIndex;
VK_CHECK(vkAcquireNextImageKHR(_device, _swapchain, 1000000000, _presentSemaphore, nullptr, &swapchainImageIndex));
vkAcquireNextImageKHR 将从交换链请求图像索引,如果交换链没有任何我们可以使用的图像,它将阻塞线程,最大超时时间为 1 秒。 这将是我们的 FPS 锁。
检查我们如何将 _presentSemaphore 发送给它。 这是为了确保我们可以将其他操作与交换链同步,使其具有可用于渲染的图像。
开始渲染命令的时候了
//now that we are sure that the commands finished executing, we can safely reset the command buffer to begin recording again.
VK_CHECK(vkResetCommandBuffer(_mainCommandBuffer, 0));
我们需要首先重置命令缓冲区,以清空它并开始排队新命令。 一旦命令缓冲区被重置,我们就可以开始它。
//naming it cmd for shorter writing
VkCommandBuffer cmd = _mainCommandBuffer;
//begin the command buffer recording. We will use this command buffer exactly once, so we want to let Vulkan know that
VkCommandBufferBeginInfo cmdBeginInfo = {};
cmdBeginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
cmdBeginInfo.pNext = nullptr;
cmdBeginInfo.pInheritanceInfo = nullptr;
cmdBeginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT;
VK_CHECK(vkBeginCommandBuffer(cmd, &cmdBeginInfo));
又一个 Vulkan 信息结构,非常典型的 sType 和 pNext 值。 命令缓冲区上的继承信息用于辅助命令缓冲区,但我们不打算使用它们,因此将其保留为 nullptr。 对于标志,我们希望让 Vulkan 知道此命令缓冲区将提交一次。 由于我们将每帧记录命令缓冲区,因此最好让 Vulkan 知道此命令只会执行一次,因为它可以允许驱动程序进行大量优化。
命令缓冲区记录开始后,让我们向其中添加命令。 我们将启动渲染通道,并使用随时间闪烁的清除颜色。
//make a clear-color from frame number. This will flash with a 120*pi frame period.
VkClearValue clearValue;
float flash = abs(sin(_frameNumber / 120.f));
clearValue.color = { { 0.0f, 0.0f, flash, 1.0f } };
//start the main renderpass.
//We will use the clear color from above, and the framebuffer of the index the swapchain gave us
VkRenderPassBeginInfo rpInfo = {};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpInfo.pNext = nullptr;
rpInfo.renderPass = _renderPass;
rpInfo.renderArea.offset.x = 0;
rpInfo.renderArea.offset.y = 0;
rpInfo.renderArea.extent = _windowExtent;
rpInfo.framebuffer = _framebuffers[swapchainImageIndex];
//connect clear values
rpInfo.clearValueCount = 1;
rpInfo.pClearValues = &clearValue;
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
开始渲染通道需要我们开始编写另一个信息结构,因为开始渲染通道需要很多参数。 像往常一样设置 sType 和 pNext,.renderPass
是我们要开始的任何渲染通道,.renderArea.offset
和 .renderArea.extent
是将要渲染的区域,以防我们想要将小渲染通道渲染到更大的图像中。 我们只需将偏移量设置为 0(无偏移量),并将范围设置为我们的主窗口大小。
.framebuffer
是我们将为此渲染通道渲染到的图像,因此我们将使用从交换链获得的图像索引索引到缓存的 _framebuffers 中。
最后,我们将创建一个闪烁的蓝色 VkClearValue,并将其连接到信息。 我们正在使用 _frameNumber
变量来获取渲染的帧数,并将其用于闪烁。 此变量来自起始代码中的引擎。
vkCmdBeginRenderPass()
函数将绑定帧缓冲区,清除图像,并将图像放入我们在创建渲染通道时指定的布局中。 我们现在可以开始渲染命令了……但我们还没有任何要渲染的东西。 这将在下一章介绍。
我们现在可以结束渲染通道,也可以结束命令缓冲区
//finalize the render pass
vkCmdEndRenderPass(cmd);
//finalize the command buffer (we can no longer add commands, but it can now be executed)
VK_CHECK(vkEndCommandBuffer(cmd));
调用 vkCmdEndRenderPass()
完成渲染,并将图像转换为我们指定的“准备好显示”状态。 由于工作现在已完成,我们可以 vkEndCommandBuffer()
来完成命令缓冲区。
缓冲区完成后,我们可以通过将其提交到 GPU 来执行它
//prepare the submission to the queue.
//we want to wait on the _presentSemaphore, as that semaphore is signaled when the swapchain is ready
//we will signal the _renderSemaphore, to signal that rendering has finished
VkSubmitInfo submit = {};
submit.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit.pNext = nullptr;
VkPipelineStageFlags waitStage = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
submit.pWaitDstStageMask = &waitStage;
submit.waitSemaphoreCount = 1;
submit.pWaitSemaphores = &_presentSemaphore;
submit.signalSemaphoreCount = 1;
submit.pSignalSemaphores = &_renderSemaphore;
submit.commandBufferCount = 1;
submit.pCommandBuffers = &cmd;
//submit command buffer to the queue and execute it.
// _renderFence will now block until the graphic commands finish execution
VK_CHECK(vkQueueSubmit(_graphicsQueue, 1, &submit, _renderFence));
要执行 vkQueueSubmit()
,我们需要设置信息结构。 我们将配置它以等待 _presentSemaphore
,并发出 _renderSemaphore
信号。 通过等待我们从 vkAcquireNextImageKHR
发出信号的 _presentSemaphore
,我们确保用于渲染的图像在 GPU 中完全准备就绪。
然后,我们还设置了我们要提交的命令缓冲区。
.pWaitDstStageMask
是一个复杂的参数。 在我们详细介绍同步之前,我们不会解释它。
提交命令后,我们现在将图像显示到屏幕上
// this will put the image we just rendered into the visible window.
// we want to wait on the _renderSemaphore for that,
// as it's necessary that drawing commands have finished before the image is displayed to the user
VkPresentInfoKHR presentInfo = {};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.pNext = nullptr;
presentInfo.pSwapchains = &_swapchain;
presentInfo.swapchainCount = 1;
presentInfo.pWaitSemaphores = &_renderSemaphore;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pImageIndices = &swapchainImageIndex;
VK_CHECK(vkQueuePresentKHR(_graphicsQueue, &presentInfo));
//increase the number of frames drawn
_frameNumber++;
vkQueuePresentKHR
函数将图像显示到屏幕上。 我们必须告诉它我们在此调用中使用的是哪个交换链,以及图像索引是什么。 我们还需要使用我们从主渲染的 VkQueueSubmit 发出信号的 _renderSemaphore
正确设置 WaitSemaphore。 这将告诉 GPU 仅在主渲染工作完成执行后才将图像显示到屏幕上。 由于我们的渲染帧现在已完成,我们可以递增 _frameNumber
变量以增加引擎时间。
我们终于有一些东西可以渲染了! 您应该看到一个闪烁的蓝色屏幕。
下一步: 第 2 章: Vulkan 管线