[英]Do I need dedicated fences/semaphores per swap chain image, per frame or per command pool in Vulkan?
我已经阅读了几篇关于 CPU-GPU(使用围栏)和 GPU-GPU(使用信号量)同步机制的文章,但仍然无法理解我应该如何实现一个简单的渲染循环。
请看下面的简单render()
function。 如果我做对了,最低要求是我们通过一组信号量image_available
和rendering_finished
确保vkAcquireNextImageKHR
、 vkQueueSubmit
和vkQueuePresentKHR
之间的 GPU-GPU 同步,正如我在下面的示例代码中所做的那样。
然而,这真的安全吗? 所有操作都是异步的。 那么,即使先前调用的信号请求尚未触发,在随后的render()
调用中再次“重用” image_available
信号量真的安全吗? 我认为不是,但是另一方面,我们使用的是相同的队列(不知道图形和表示队列实际上是否相同是否重要),并且队列内的操作应该按顺序使用.. . 但是,如果我做对了,它们可能不会“作为一个整体”被消耗,并且可以重新排序......
第二件事是(同样,除非我遗漏了什么)我显然应该为每个交换链图像使用一个栅栏,以确保与调用render()
的image_index
对应的图像上的操作已经完成。 但这是否意味着我一定需要做一个
if (vkWaitForFences(device(), 1, &fence[image_index_of_last_call], VK_FALSE, std::numeric_limits<std::uint64_t>::max()) != VK_SUCCESS)
throw std::runtime_error("vkWaitForFences");
vkResetFences(device(), 1, &fence[image_index_of_last_call]);
在我打电话给vkAcquireNextImageKHR
之前? 然后我是否需要每个交换链图像专用image_available
和rendering_finished
信号量? 或者也许每帧? 或者也许每个命令缓冲区/池? 我真的很困惑...
void render()
{
std::uint32_t image_index;
switch (vkAcquireNextImageKHR(device(), swap_chain().handle(),
std::numeric_limits<std::uint64_t>::max(), m_image_available, VK_NULL_HANDLE, &image_index))
{
case VK_SUBOPTIMAL_KHR:
case VK_SUCCESS:
break;
case VK_ERROR_OUT_OF_DATE_KHR:
on_resized();
return;
default:
throw std::runtime_error("vkAcquireNextImageKHR");
}
static VkPipelineStageFlags constexpr wait_destination_stage_mask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submit_info{};
submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info.waitSemaphoreCount = 1;
submit_info.pWaitSemaphores = &m_image_available;
submit_info.signalSemaphoreCount = 1;
submit_info.pSignalSemaphores = &m_rendering_finished;
submit_info.pWaitDstStageMask = &wait_destination_stage_mask;
if (vkQueueSubmit(graphics_queue().handle, 1, &submit_info, VK_NULL_HANDLE) != VK_SUCCESS)
throw std::runtime_error("vkQueueSubmit");
VkPresentInfoKHR present_info{};
present_info.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present_info.waitSemaphoreCount = 1;
present_info.pWaitSemaphores = &m_rendering_finished;
present_info.swapchainCount = 1;
present_info.pSwapchains = &swap_chain().handle();
present_info.pImageIndices = &image_index;
switch (vkQueuePresentKHR(presentation_queue().handle, &present_info))
{
case VK_SUCCESS:
break;
case VK_ERROR_OUT_OF_DATE_KHR:
case VK_SUBOPTIMAL_KHR:
on_resized();
return;
default:
throw std::runtime_error("vkQueuePresentKHR");
}
}
编辑:正如下面的答案所建议的,假设我们有k
个“飞行中的帧”,因此有k
个信号量实例和上面代码中使用的栅栏,我将用m_image_available[i]
、 m_rendering_finished[i]
和m_fence[i]
对于i = 0, ..., k - 1
。 令i
表示飞行中帧的当前索引,在每次调用render()
后增加1
, j
表示调用render()
的次数,从j = 0
开始。
现在,假设交换链包含三个图像。
j = 0
,则i = 0
并且飞行中的第一帧使用交换链图像0
j = a
,则i = a
并且飞行中的第a
帧正在使用交换链图像a
,对于a= 2, 3
j = 3
,则i = 3
,但由于交换链图像只有三个图像,所以飞行中的第四帧再次使用交换链图像0
。 我想知道这是否有问题。 我猜不是这样,因为在调用render()
时调用vkAcquireNextImageKHR
、 vkQueueSubmit
和vkQueuePresentKHR
中使用的等待/信号量m_image_available[3]
/ m_rendering_finished[3]
专用于飞行中的这个特定帧。j = k
,那么i = 0
再次,因为只有k
帧在飞行。 现在我们可能会在render()
的开头等待,如果从第一次调用( i = 0
)的render()
调用vkQueuePresentKHR
还没有发出m_fence[0]
的信号。 所以,除了我在上面第三个要点中描述的怀疑之外,唯一剩下的问题是为什么我不应该尽可能大地取k
? 我理论上可以想象的是,如果我们以比 GPU 能够消耗的速度更快的方式向 GPU 提交工作,则使用的队列可能会不断增长并最终溢出(队列中是否存在某种“最大命令“ 限制?)。
如果我做对了,最低要求是我们通过一组信号量 image_available 和 rendering_finished 确保 vkAcquireNextImageKHR、vkQueueSubmit 和 vkQueuePresentKHR 之间的 GPU-GPU 同步,正如我在下面的示例代码中所做的那样。
是的,你没看错。 您通过vkAcquireNextImageKHR
提交获取要渲染的新图像的愿望。 一旦要渲染的图像可用,表示引擎就会发出m_image_available
信号量的信号。 但是您已经提交了指令。
接下来,您通过submit_info
向图形队列提交一些命令。 即它们也已经提交给 GPU 并在那里等待,直到m_image_available
信号量接收到它的信号。
此外,将表示指令提交给表示引擎,该指令表示它需要等待直到submit_info
命令通过等待m_rendering_finished
信号量完成的依赖关系。
即一切都已提交。 如果尚未发出任何信号,则所有内容都位于某些 GPU 缓冲区中并等待信号。
现在,如果您的代码直接循环回到render()
function 并重新使用相同的m_image_available
和m_rendering_finished
信号量,它只会在您非常幸运的情况下工作,即如果所有信号量在您再次使用它们之前已经发出信号。
vkAcquireNextImageKHR
的规格说明如下:
如果信号量不是 VK_NULL_HANDLE 它不能有任何未完成的信号或等待操作挂起
等待二进制信号量的行为也会取消该信号量的信号。
即确实,您需要在 CPU上等待,直到您确定之前使用相同vkAcquireNextImageKHR
信号量的m_image_available
已完成。
是的,您已经做对了:您需要为传递给vkQueueSubmit
的内容使用栅栏。 如果您不在 CPU 上进行同步,您将在 GPU 上进行更多工作(这是一个问题),并且您正在重复使用的信号量可能无法及时正确地取消信号(这是一个问题)。
经常做的是将信号量和栅栏相乘,例如每个成3个,并按顺序使用这些同步对象集,以便在GPU上并行处理更多工作。 Vulkan 教程在其渲染和演示一章中很好地描述了这一点。 7:59开始的本次讲座中还使用 animation 进行了解释。
因此,首先,正如您正确提到的,信号量严格用于 GPU-GPU 同步,例如,确保一批命令(一个提交)在另一批命令开始之前完成。 这在这里用于将渲染命令与呈现命令同步,以便呈现引擎知道何时呈现呈现的图像。
Fences 是 CPU-GPU 同步的主要工具。 您在队列提交中放置一个栅栏,然后在 CPU 端等待它,然后再继续。 这通常在这里完成,这样我们就不会在前一帧尚未完成时排队任何新的渲染/呈现命令。
但这是否意味着我一定需要做一个
if (vkWaitForFences(device(), 1, &fence[image_index_of_last_call], VK_FALSE, std::numeric_limits<std::uint64_t>::max()) != VK_SUCCESS)
throw std::runtime_error("vkWaitForFences");
vkResetFences(device(), 1, &fence[image_index_of_last_call]);
在我打电话给 vkAcquireNextImageKHR 之前?
是的,您的代码中肯定需要这个,否则您的信号量将不安全,并且您可能会遇到验证错误。
一般来说,如果你想让你的 CPU 等到你的 GPU 完成前一帧的渲染,你将只有一个栅栏和一对信号量。 您还可以通过队列或设备的 waitIdle 命令替换栅栏。 但是,在实践中,您不希望停止 CPU 并同时记录下一帧的命令。 这是通过飞行中的帧完成的。 这仅仅意味着对于飞行中的每一帧(即可以与 GPU 上的执行并行记录的帧数),您有一个栅栏和一对同步该特定帧的信号量。
因此,从本质上讲,为了让您的渲染循环正常工作,您需要在每帧飞行中使用一对信号量 + 栅栏,与交换链图像的数量无关。 但是,请注意,当前帧索引(飞行中的帧)和图像索引(交换链)通常不会相同,除非您使用与飞行中的帧相同数量的交换链图像。 这是因为呈现引擎可能会根据您的呈现模式为您提供乱序的交换链图像。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.