飞行中的帧
飞行中的帧"Frames in flight",指 同时处于渲染流水线不同阶段的多个帧。
飞行中的帧
我们刚才完成的渲染循环其实有个问题:必须等到一帧渲染呈现完成,才能录制命令并开始新的一帧。显然这将导致GPU和CPU的资源浪费。
解决这个问题的方法是允许多个"飞行中的帧",使得一帧的渲染不影响下一帧的录制。 为达成此目的,我们需要复制所有在渲染期间可能被访问或修改的资源。 所以我们需要多个命令缓冲、信号量和围栏。后面的章节中我们还会添加其他新资源。
首先定义一个常量,用于指定应该同时处理多少个帧。
constexpr int MAX_FRAMES_IN_FLIGHT = 2;
我们将最大值设为 2 可以避免 CPU 太过领先于 GPU 。 现在,CPU 和 GPU 将同时处理自己的任务。如果 CPU 完成的更快,那么它在提交更多任务之前必须先等待 GPU 完成。 使用三个或更多时,CPU 可能领先 GPU ,导致画面延迟增加。
我们可以让程序指定具体使用几个飞行中的帧,这也是Vulkan显式控制的特点。
1. 修改成员变量
每个飞行中的帧都需要自己的命令缓冲、信号量和围栏,所以我们需要修改变量:
std::vector<vk::raii::CommandBuffer> m_commandBuffers;
std::vector<vk::raii::Semaphore> m_imageAvailableSemaphores;
std::vector<vk::raii::Semaphore> m_renderFinishedSemaphores;
std::vector<vk::raii::Fence> m_inFlightFences;
注意我们还修改了变量名,在末尾加上了 s 。
2. 修改成员变量的创建
现在将函数 createCommandBuffer 改名成 createCommandBuffers ,并修改参数,从而获取指定数量的命令缓冲。
void createCommandBuffers() {
vk::CommandBufferAllocateInfo allocInfo;
allocInfo.commandPool = m_commandPool;
allocInfo.level = vk::CommandBufferLevel::ePrimary;
allocInfo.commandBufferCount = MAX_FRAMES_IN_FLIGHT;
m_commandBuffers = m_device.allocateCommandBuffers(allocInfo);
}
同样,还需要修改 createSyncObjects 函数:
void createSyncObjects() {
constexpr vk::SemaphoreCreateInfo semaphoreInfo;
constexpr vk::FenceCreateInfo fenceInfo(
vk::FenceCreateFlagBits::eSignaled // flags
);
for(size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i){
m_imageAvailableSemaphores.emplace_back( m_device, semaphoreInfo );
m_renderFinishedSemaphores.emplace_back( m_device, semaphoreInfo );
m_inFlightFences.emplace_back( m_device , fenceInfo );
}
}
注意到这里创建对象时,调用的是信号量和围栏的构造函数,而不是m_device的成员函数。
你也可以使用成员函数然后移动构造:
m_imageAvailableSemaphores.emplace_back( m_device.createSemaphore(semaphoreInfo) );
m_renderFinishedSemaphores.emplace_back( m_device.createSemaphore(semaphoreInfo) );
m_inFlightFences.emplace_back( m_device.createFence(fenceInfo) );
3. 修改帧的绘制
我们需要定义一个成员变量,来追踪当前程序处理的是哪个帧:
uint32_t m_currentFrame = 0;
然后我们就可以修改 drawFrame 函数,只需要将每个信号量/围栏/命令缓冲都设置上m_currentFrame:
void drawFrame() {
if(const auto res = m_device.waitForFences( *m_inFlightFences[m_currentFrame], true, std::numeric_limits<uint64_t>::max() );
res != vk::Result::eSuccess
) throw std::runtime_error{ "waitForFences in drawFrame was failed" };
m_device.resetFences( *m_inFlightFences[m_currentFrame] );
auto [nxtRes, imageIndex] = m_swapChain.acquireNextImage(std::numeric_limits<uint64_t>::max(), m_imageAvailableSemaphores[m_currentFrame]);
m_commandBuffers[m_currentFrame].reset();
recordCommandBuffer(m_commandBuffers[m_currentFrame], imageIndex);
vk::SubmitInfo submitInfo;
submitInfo.setWaitSemaphores( *m_imageAvailableSemaphores[m_currentFrame] );
std::array<vk::PipelineStageFlags,1> waitStages = { vk::PipelineStageFlagBits::eColorAttachmentOutput };
submitInfo.setWaitDstStageMask( waitStages );
submitInfo.setCommandBuffers( *m_commandBuffers[m_currentFrame] );
submitInfo.setSignalSemaphores( *m_renderFinishedSemaphores[m_currentFrame] );
m_graphicsQueue.submit(submitInfo, m_inFlightFences[m_currentFrame]);
vk::PresentInfoKHR presentInfo;
presentInfo.setWaitSemaphores( *m_renderFinishedSemaphores[m_currentFrame] );
presentInfo.setSwapchains( *m_swapChain );
presentInfo.pImageIndices = &imageIndex;
if (const auto res = m_presentQueue.presentKHR( presentInfo );
res != vk::Result::eSuccess
) throw std::runtime_error{ "presentKHR in drawFrame was failed" };
}
记得在函数末尾更新我们的 m_currentFrame 。
void drawFrame() {
// ...
m_currentFrame = (m_currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
}
验证层警告
现在运行程序,你的验证层很可能还会提示“信号量非预期重用”的警告,和上一节末尾相同。
原因
此警告原自下面这一机制:
如果图像被提交至呈现队列时设置了信号量,那么 Vulkan 验证层会追踪此图像和信号量的状态, 在图像被再次获取之前,信号量不能被再次使用(不能被激活)。
由于呈现本身不是 GPU 的工作,无法设置让呈现任务在完成时激活信号量。 验证层的想法很好理解,只有图像被再次获取时才能确定呈现完成了,此时才能确定此信号量可以被别人使用。
而我们的方案,由于交换链中存在多张图像,有可能图像A还在呈现,然后图像B就被获取并渲染,渲染完成就激活了不该激活的信号量。
解决方法
此问题的解决思路也很简单,只要将图像和呈现信号量一一对应,这样每个呈现信号量都只会被自身绑定的图像激活。
现在修改信号量的创建函数,保证控制呈现的信号量与交换链图像数一致:
void createSyncObjects() {
// ...
for(size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; ++i){
m_imageAvailableSemaphores.emplace_back( m_device, semaphoreInfo );
m_inFlightFences.emplace_back( m_device , fenceInfo );
}
for(size_t i = 0; i < m_swapChainImages.size(); ++i) {
m_renderFinishedSemaphores.emplace_back( m_device, semaphoreInfo );
}
}
然后修改 drawFrame ,把 m_renderFinishedSemaphores 的索引都改成 imageIndex。
imageIndex 是图像的索引,实际是图像在交换链数组中的索引,从而将图像与此信号量一一对应。
void drawFrame() {
// ....
m_renderFinishedSemaphores[imageIndex]
// ....
}
再次运行程序,应该可以看到警告消失了。
最后
我们已经实现了所有必要的同步工作以确保入队的工作帧不超过 MAX_FRAMES_IN_FLIGHT 个,且这些帧不会互相覆盖。
请注意,对于代码的其他部分(如最终清理),可以依赖更粗略的同步,例如 m_device.waitIdle()。
要通过示例了解有关同步的更多信息,请查看 Khronos 提供的 这份全面的概述。
在下一章中,我们将处理使 Vulkan 程序良好运行所需的另一件小事。