帧缓冲队列
帧缓冲队列
目前,我们的渲染循环有一个明显的缺陷。我们需要等待前一帧完成才能开始渲染下一帧,这导致主机不必要的空闲。
解决这个问题的方法是允许多个帧处于飞行中,也就是说,允许渲染一帧不干扰下一帧的录制。我们该怎么做呢?任何在渲染期间被访问和修改的资源都必须被复制。因此,我们需要多个命令缓冲区、信号量和栅栏。在后面的章节中,我们还将添加其他资源的多个实例,因此我们将看到这个概念再次出现。
首先在程序顶部添加一个常量,用于定义应同时处理多少帧
const int MAX_FRAMES_IN_FLIGHT = 2;
我们选择数字 2 是因为我们不希望 CPU 太超前于 GPU。通过 2 帧缓冲队列,CPU 和 GPU 可以同时处理各自的任务。如果 CPU 提前完成,它将等待 GPU 完成渲染,然后再提交更多工作。使用 3 个或更多帧缓冲队列,CPU 可能会超前于 GPU,从而增加帧延迟。通常,额外的延迟是不希望的。但是,让应用程序控制帧缓冲队列的数量是 Vulkan 显式性的另一个例子。
每一帧都应该有自己的命令缓冲区、信号量和栅栏。重命名它们,然后将它们更改为对象的 std::vector
std::vector<VkCommandBuffer> commandBuffers;
...
std::vector<VkSemaphore> imageAvailableSemaphores;
std::vector<VkSemaphore> renderFinishedSemaphores;
std::vector<VkFence> inFlightFences;
然后我们需要创建多个命令缓冲区。将 createCommandBuffer
重命名为 createCommandBuffers
。接下来,我们需要将命令缓冲区向量调整为 MAX_FRAMES_IN_FLIGHT
的大小,修改 VkCommandBufferAllocateInfo
以包含那么多命令缓冲区,然后将目标更改为我们的命令缓冲区向量
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 程序良好运行所需的另一件小事。