0xb*_*00d 5 c++ 3d graphics vulkan
我已经阅读了几篇关于 CPU-GPU(使用栅栏)和 GPU-GPU(使用信号量)同步机制的文章,但仍然无法理解我应该如何实现一个简单的渲染循环。
请看render()下面的简单函数。如果我这样做是正确的最低要求是,我们要确保的GPU-GPU同步vkAcquireNextImageKHR,vkQueueSubmit并且vkQueuePresentKHR由单一的信号量集image_available,并rendering_finished为我在下面的示例代码来完成。
然而,这真的有救吗?所有操作都是异步的。那么,即使来自前一次调用的信号请求尚未触发image_available,在随后的render()再次调用中“重用”信号量真的安全吗?我认为它不是,但是,另一方面,我们使用相同的队列(不知道图形和演示队列实际上是否相同是否重要)并且队列中的操作应该按顺序使用.. . 但如果我猜对了,它们可能不会“作为一个整体”被消费,并且可以重新排序......
第二件事是(同样,除非我遗漏了什么)我显然应该为每个交换链图像使用一个栅栏,以确保对对应于image_index调用的图像的操作render()已经完成。但这是否意味着我一定需要做一个
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]);
Run Code Online (Sandbox Code Playgroud)
在我打电话之前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");
}
}
Run Code Online (Sandbox Code Playgroud)
编辑:正如下面的答案中所建议的,假设我们有k“飞行中的帧”,因此k有上面代码中使用的信号量和围栏的实例,我将用m_image_available[i],m_rendering_finished[i]和m_fence[i]for表示i = 0, ..., k - 1。让i表示飞行中帧的当前索引,1在每次调用 后增加render(),并j表示render()从 开始的调用次数j = 0。
现在,假设交换链包含三个图像。
j = 0,则i = 0飞行中的第一帧正在使用交换链图像0j = a,则i = a和a飞行中的第 th 帧正在使用交换链图像a,对于a= 2, 3j = 3,则i = 3,但由于交换链图像只有三个图像,飞行中的第四帧0再次使用交换链图像。我想知道这是否有问题。我想它不是,因为在 的调用中使用的等待/信号信号量m_image_available[3]/ ,以及在 的调用中,专用于这个特定的飞行帧。m_rendering_finished[3]vkAcquireNextImageKHRvkQueueSubmitvkQueuePresentKHRrender()j = k,那么i = 0再次,因为只有k帧在飞行。现在我们可能在 的开头等待render(),如果vkQueuePresentKHR从第一次调用 ( i = 0) ofrender()开始的调用还没有发出信号m_fence[0]。所以,除了我在上面第三个要点中描述的怀疑之外,唯一剩下的问题是为什么我不应该k尽可能大?我理论上可以想象的是,如果我们以比 GPU 能够消耗的速度更快的方式向 GPU 提交工作,则使用的队列可能会不断增长并最终溢出(是否有某种“队列中的最大命令” “ 限制?)。
如果我做对了,最低要求是我们通过一组信号量 image_available 和 render_finished 确保 vkAcquireNextImageKHR、vkQueueSubmit 和 vkQueuePresentKHR 之间的 GPU-GPU 同步,就像我在下面的示例代码中所做的那样。
是的,你说得对。您提交了获取新图像以渲染到 via 的愿望vkAcquireNextImageKHR。m_image_available一旦要渲染的图像可用,呈现引擎就会向信号量发出信号。但您已经提交了指令。
接下来,您通过 向图形队列提交一些命令submit_info。即它们也已经提交给 GPU 并在那里等待,直到m_image_available信号量接收到它的信号。
此外,呈现指令被提交给呈现引擎,该呈现引擎表达了它需要等待直到submit_info通过等待信号量来完成命令的依赖性m_rendering_finished。
即一切都已提交。如果尚未发出任何信号,则所有内容都将位于某些 GPU 缓冲区中并等待信号。
现在,如果您的代码循环回到render()函数并重新使用相同的m_image_available信号m_rendering_finished量,它只会在您非常幸运的情况下它才会起作用,即如果所有信号量在您再次使用它们之前已经发出信号。
规格说明如下vkAcquireNextImageKHR:
如果信号量不是 VK_NULL_HANDLE,则它不能有任何未完成的信号或等待操作挂起
等待二进制信号量的行为也会取消该信号量的信号。
也就是说,确实,您需要等待CPU,直到您确定vkAcquireNextImageKHR使用相同 m_image_available信号量的前一个操作已完成。
是的,你已经猜对了:你需要对你传递到的地方使用栅栏vkQueueSubmit。如果您不在 CPU 上进行同步,您将把更多的工作转移到 GPU(这是一个问题),并且您正在重复使用的信号量可能无法及时正确地取消信号量(这是一个问题)。
通常所做的是将信号量和栅栏相乘,例如各乘以 3,并且按顺序使用这些同步对象集,以便可以在 GPU 上并行化更多工作。Vulkan教程在其渲染和演示章节中很好地描述了这一点。本次讲座从7点59分开始也用动画进行了讲解。
小智 3
首先,正如您正确提到的,信号量严格用于 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]);
Run Code Online (Sandbox Code Playgroud)
在我调用 vkAcquireNextImageKHR 之前?
是的,您的代码中肯定需要这个,否则您的信号量将不安全,并且您可能会收到验证错误。
一般来说,如果您希望 CPU 等待 GPU 完成前一帧的渲染,那么您将只有一个栅栏和一对信号量。您还可以用队列或设备的 waitIdle 命令替换栅栏。然而,实际上您不希望停止 CPU 并同时记录下一帧的命令。这是通过飞行中的帧完成的。这仅仅意味着对于飞行中的每一帧(即可以与 GPU 上的执行并行记录的帧数),您有一个栅栏和一对同步该特定帧的信号量。
因此,本质上,为了使渲染循环正常工作,飞行中的每帧需要一对信号量+栅栏,与交换链图像的数量无关。但是,请注意,当前帧索引(飞行中的帧)和图像索引(交换链)通常不会相同,除非您使用与飞行中的帧相同数量的交换链图像。这是因为呈现引擎可能会根据您的呈现模式为您提供无序的交换链图像。