constexpr int MAX_FRAMES_IN_FLIGHT = 2;
目前我们的渲染循环有一个明显的缺陷。 我们需要等待上一帧完成后才能开始渲染下一帧,这导致了主机不必要的空闲。
解决这个问题的方法是允许多帧同时 在飞行中(in-flight),也就是说,允许一帧的渲染不干扰下一帧的记录。 我们该怎么做呢? 任何在渲染期间被访问和修改的资源都必须被复制。 因此,我们需要多个命令缓冲、信号量和围栏。 在后续章节中,我们还会添加其他资源的多个实例,因此我们会看到这个概念再次出现。
首先在程序顶部添加一个常量,定义应该并发处理的帧数:
constexpr int MAX_FRAMES_IN_FLIGHT = 2;
我们选择数字 2,因为我们不希望 CPU 比 GPU *过于*提前。 当有两帧在飞行中时,CPU 和 GPU 可以同时处理各自的任务。 如果 CPU 提前完成,它会等到 GPU 完成渲染后再提交更多工作。 如果有三帧或更多帧在飞行中,CPU 可能会领先于 GPU,增加帧延迟。 通常,额外的延迟是不希望的。 但是让应用程序控制飞行中的帧数是 Vulkan 明确性的另一个例子。
每一帧都应该有自己的命令缓冲、一组信号量和围栏。
重命名并将它们改为对象的 std::vector:
std::vector<vk::raii::CommandBuffer> commandBuffers;
...
std::vector<vk::raii::Semaphore> presentCompleteSemaphores;
std::vector<vk::raii::Semaphore> renderFinishedSemaphores;
std::vector<vk::raii::Fence> inFlightFences;
然后我们需要创建多个命令缓冲。
将 createCommandBuffer 重命名为 createCommandBuffers。
接下来,我们需要将命令缓冲向量的大小调整为 MAX_FRAMES_IN_FLIGHT,修改 vk::CommandBufferAllocateInfo 以包含那么多命令缓冲,然后将目标更改为我们的命令缓冲向量:
void createCommandBuffers()
{
vk::CommandBufferAllocateInfo allocInfo{.commandPool = commandPool, .level = vk::CommandBufferLevel::ePrimary, .commandBufferCount = MAX_FRAMES_IN_FLIGHT};
commandBuffers = vk::raii::CommandBuffers( device, allocInfo );
}
createSyncObjects 函数应该被修改为创建所有对象:
void createSyncObjects()
{
assert(presentCompleteSemaphores.empty() && renderFinishedSemaphores.empty() && inFlightFences.empty());
for (size_t i = 0; i < swapChainImages.size(); i++)
{
renderFinishedSemaphores.emplace_back(device, vk::SemaphoreCreateInfo());
}
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++)
{
presentCompleteSemaphores.emplace_back(device, vk::SemaphoreCreateInfo());
inFlightFences.emplace_back(device, vk::FenceCreateInfo{.flags = vk::FenceCreateFlagBits::eSignaled});
}
}
为了在每帧使用正确的对象,我们需要跟踪当前帧。 我们将为此使用一个帧索引:
uint32_t frameIndex = 0;
现在可以修改 drawFrame 函数以使用正确的对象:
void drawFrame() {
auto fenceResult = device.waitForFences(*inFlightFences[frameIndex], vk::True, UINT64_MAX);
if (fenceResult != vk::Result::eSuccess)
{
throw std::runtime_error("failed to wait for fence!");
}
device.resetFences(*inFlightFences[frameIndex]);
auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphores[frameIndex], nullptr);
commandBuffers[frameIndex].reset();
recordCommandBuffer(imageIndex);
vk::PipelineStageFlags waitDestinationStageMask(vk::PipelineStageFlagBits::eColorAttachmentOutput);
const vk::SubmitInfo submitInfo{.waitSemaphoreCount = 1,
.pWaitSemaphores = &*presentCompleteSemaphores[frameIndex],
.pWaitDstStageMask = &waitDestinationStageMask,
.commandBufferCount = 1,
.pCommandBuffers = &*commandBuffers[frameIndex],
.signalSemaphoreCount = 1,
.pSignalSemaphores = &*renderFinishedSemaphores[imageIndex]};
queue.submit(submitInfo, *inFlightFences[frameIndex]);
}
当然,我们不应该忘记每次前进到下一帧:
void drawFrame() {
...
frameIndex = (frameIndex + 1) % MAX_FRAMES_IN_FLIGHT;
}
通过使用取模(%)运算符,我们确保帧索引在每 MAX_FRAMES_IN_FLIGHT 帧入队后循环。
我们现在已经实现了所有必要的同步,以确保入队的工作帧不超过 MAX_FRAMES_IN_FLIGHT 帧,并且这些帧不会相互干扰。
请注意,代码的其他部分(如最终清理)依赖于更粗略的同步(如 vk::raii::Device::waitIdle)也是可以的。
你应该根据性能要求决定使用哪种方法。
此外,我们可以使用时间线信号量来代替这里介绍的二值信号量。要查看如何使用时间线信号量的示例,请参阅计算着色器章节。请注意,时间线信号量在该示例中对于处理计算队列和图形队列尤其有用。这种简单的二值信号量方法可以被认为是更传统的同步方法。
要了解更多通过示例进行的同步,请查看 Khronos 提供的这份详尽概述。
在下一章中,我们将处理一个行为规范的 Vulkan 程序所需的一件小事。
C++ 代码 / Slang 着色器 / GLSL 顶点着色器 / GLSL 片段着色器
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。