Link

我们将需要在 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 管线