C++ 在Vulkan中,每个交换链映像、每个帧或每个命令池是否需要专用围栏/信号灯?

C++ 在Vulkan中,每个交换链映像、每个帧或每个命令池是否需要专用围栏/信号灯?,c++,graphics,3d,vulkan,C++,Graphics,3d,Vulkan,我读过几篇关于CPU-GPU(使用fences)和GPU-GPU(使用信号量)同步机制的文章,但仍然难以理解如何实现简单的渲染循环 请查看下面的简单render()函数。如果我没弄错的话,最起码的要求是我们通过一组可用的信号量图像和渲染完成来确保vkAcquireNextImageKHR、vkQueueSubmit和vkQueuePresentKHR之间的GPU-GPU同步 然而,这真的节省了吗?所有操作都是异步的。那么,在后续调用render()时再次“重用”可用的image\u信号量是否真的

我读过几篇关于CPU-GPU(使用fences)和GPU-GPU(使用信号量)同步机制的文章,但仍然难以理解如何实现简单的渲染循环

请查看下面的简单
render()
函数。如果我没弄错的话,最起码的要求是我们通过一组可用的信号量
图像
渲染完成
来确保
vkAcquireNextImageKHR
vkQueueSubmit
vkQueuePresentKHR
之间的GPU-GPU同步

然而,这真的节省了吗?所有操作都是异步的。那么,在后续调用
render()
时再次“重用”可用的
image\u
信号量是否真的安全,即使之前调用的信号请求尚未触发?我认为不是这样,但另一方面,我们使用的是相同的队列(不知道图形和表示队列实际上在哪里相同是否重要),队列中的操作应该按顺序使用。。。但如果我做对了,它们可能不会“作为一个整体”被消费,并且可以重新排序

第二件事是(同样,除非我遗漏了什么),我显然应该为每个交换链映像使用一个栅栏,以确保对与调用
render()
image\u索引
对应的映像的操作已经完成。但这是否意味着我必须做一个

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]);
编辑:如下面的答案所示,假设我们有
k
“飞行中的帧”,因此上面代码中使用的信号量和栅栏的
k
实例,我将用
m_image_available[I]
m_rendering_finished[I]
m_fence[I]
表示
I=0。。。,k-1
。让
i
表示飞行中帧的当前索引,在每次调用
render()
后,该索引增加
1
,并且
j
表示从
j=0开始的
render()
调用次数

现在,假设交换链包含三个图像

  • 如果
    j=0
    ,则
    i=0
    ,飞行中的第一帧使用交换链图像
    0
  • 同样,如果
    j=a
    ,则
    i=a
    ,飞行中的第
    a
    帧使用交换链图像
    a
    ,用于
    a=2,3
  • 现在,如果
    j=3
    ,则
    i=3
    ,但由于交换链映像只有三个映像,飞行中的第四帧再次使用交换链映像
    0
    。我不知道这是否有问题。我想不是,因为在调用
    render()
    时,在调用
    vkAcquireNextImageKHR
    vkQueueSubmit
    vkQueuePresentKHR
    时使用的等待/信号量
    m\u image\u可用[3]
    m\u渲染完成[3]
    ,专用于飞行中的特定帧
  • 如果我们到达
    j=k
    ,然后再次到达
    i=0
    ,因为飞行中只有
    k
    帧。现在,如果从
    render()
    的第一次调用(
    i=0
    )到
    vkQueuePresentKHR
    的调用尚未发出
    m_fence[0]
    的信号,我们可能会在
    render()的开始处等待

所以,除了上面第三个要点中描述的我的怀疑之外,剩下的唯一问题是为什么我不应该把
k
取得尽可能大?理论上我可以想象的是,如果我们以比GPU能够使用的更快的方式向GPU提交工作,使用的队列可能会不断增长并最终溢出(是否存在某种“队列中的最大命令数”限制?)。

首先,正如您正确提到的,信号量严格用于GPU-GPU同步,例如确保一批命令(一次提交)在另一批命令开始之前完成。这在这里用于将渲染命令与present命令同步,以便呈现引擎知道何时呈现渲染图像

围栏是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]);
if(vkWaitForFences(device(),1,&fence[上次调用的图像索引]、VK\u FALSE、std::numeric\u limits::max())!=VK\u成功)
抛出std::runtime_错误(“vkWaitForFences”);
vkResetFences(device()、1和fences[image_index_of_last_call]);
在我打电话给vkAcquireNextImageKHR之前

是的,您的代码中肯定需要这个,否则您的信号量将不安全,您可能会得到验证错误

通常,如果希望CPU等待GPU完成前一帧的渲染,则只有一个围栏和一对信号灯。您还可以使用队列或设备的waitIdle命令替换围栏。 但是,实际上,您不希望暂停CPU,同时记录下一帧的命令。这是通过飞行中的帧完成的。这仅仅意味着,对于飞行中的每一帧(即,可以与GPU上的执行并行记录的帧数),您有一个围栏和一对信号灯,用于同步该特定帧

因此,本质上,为了使渲染循环正常工作,飞行中每帧需要一对信号灯+围栏,与交换链图像的数量无关。但是,请注意,当前帧索引(飞行中的帧)和图像索引(swapchain)将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]);