现在我们的渲染循环有一个明显的缺陷。我们需要等待上一帧完成,然后才能开始渲染下一帧,这会导致主机不必要的空闲。
解决这一问题的方法是允许多帧同时运行,也就是说,允许一帧的渲染不干扰下一帧的录制。我们如何做到这一点?必须复制渲染期间访问和修改的任何资源。因此,我们需要多个命令缓冲区、信号灯和围栏。在后面的章节中,我们还将添加其他资源的多个实例,因此我们将看到这个概念再次出现。
首先在程序顶部添加一个常量,该常量定义应同时处理多少帧:
const int MAX_FRAMES_IN_FLIGHT = 2;
我们选择数字2是因为我们不希望CPU超过GPU太多。当有2帧在运行时,CPU和GPU可以同时执行各自的任务。如果CPU提前完成,它将等待GPU完成渲染后再提交更多工作。在3帧或更多帧的情况下,CPU可以领先GPU,增加延迟帧。通常,不需要额外的延迟。但让应用程序控制飞行中的帧数是Vulkan显式的另一个例子。
每个帧都应该有自己的命令缓冲区、信号量集和围栏。重命名,然后将其更改为对象的std::vectors:
std::vector<VkCommandBuffer> commandBuffers;
...
std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
std::vector<VkFence> inFlightFences;
然后我们需要创建多个命令缓冲区。将createCommandBuffer重命名为createCommandBuffers。接下来,我们需要将命令缓冲区向量调整为MAX_FRAMES_IN_FLIGHT的大小,更改VkCommandBufferAllocateInfo以包含这么多命令缓冲区,然后将目标更改为命令缓冲区的vector:
void createCommandBuffers() {
commandBuffers.resize(MAX_FRAMES_IN_FLIGHT);
...
allocInfo.commandBufferCount = (uint32_t) commandBuffers.size();
if (vkAllocateCommandBuffers(device, &allocInfo, commandBuffers.data()) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate command buffers!");
}
}
createSyncObjects函数应更改为创建所有对象:
void createSyncObjects() {
imageAvailableSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
renderFinishedSemaphores.resize(MAX_FRAMES_IN_FLIGHT);
inFlightFences.resize(MAX_FRAMES_IN_FLIGHT);
VkSemaphoreCreateInfo semaphoreInfo{};
semaphoreInfo.sType = VK_STRUCTURE_TYPE_SEMAPHORE_CREATE_INFO;
VkFenceCreateInfo fenceInfo{};
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
if (vkCreateSemaphore(device, &semaphoreInfo, nullptr, &imageAvailableSemaphores[i]) != VK_SUCCESS ||
vkCreateSemaphore(device, &semaphoreInfo, nullptr, &renderFinishedSemaphores[i]) != VK_SUCCESS ||
vkCreateFence(device, &fenceInfo, nullptr, &inFlightFences[i]) != VK_SUCCESS) {
throw std::runtime_error("failed to create synchronization objects for a frame!");
}
}
}
同样,它们也应该被清理:
void cleanup() {
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
vkDestroyFence(device, inFlightFences[i], nullptr);
}
...
}
请记住,因为当我们释放命令池时,命令缓冲区会为我们释放,所以命令缓冲区清理无需额外操作。
要在每帧使用正确的对象,我们需要跟踪当前帧。为此,我们将使用框架索引:
uint32_t currentFrame = 0;
现在可以修改drawFrame函数以使用正确的对象:
void drawFrame() {
vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
vkResetFences(device, 1, &inFlightFences[currentFrame]);
vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
...
vkResetCommandBuffer(commandBuffers[currentFrame], 0);
recordCommandBuffer(commandBuffers[currentFrame], imageIndex);
...
submitInfo.pCommandBuffers = &commandBuffers[currentFrame];
...
VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
...
VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
...
if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
}
当然,我们不应该每次都忘记进入下一帧:
void drawFrame() {
...
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}
通过使用模(%)运算符,我们确保帧索引在每个MAX_FRAMES_IN_FLIGHT排队帧之后循环。
我们现在已经实现了所有所需的同步,以确保排队的工作帧不超过MAX_FRAMES_IN_FLIGHT,并且这些帧不会彼此重叠。请注意,代码的其他部分(如最终清理)可以依赖更粗略的同步,如vkDeviceWaitIdle。您应该根据性能要求决定使用哪种方法。
要通过示例了解有关同步的更多信息,请查看Khronos的这篇广泛概述。
在下一章中,我们将处理一个行为良好的Vulkan程序所需的另一件小事。