Vulkan:如何在单独的线程中记录命令缓冲区?

Vulkan: How to record command buffers in separate thread?

我不太了解如何在 Vulkan 中并行处理不同线程上的工作。

为了开始发出 vkCmd*s,您需要开始渲染过程。调用 begin render pass 需要引用帧缓冲区。但是,vkAcquireNextImageKHR() 不保证 return 以循环方式对图像进行索引。因此,在三缓冲设置中,如果当前图像索引为 0,我不能只绑定帧缓冲区 1 并开始为下一帧发出绘制调用,因为下一次调用 vkAcquireNextImageKHR() 可能 return image索引 2.

无需提前指定要使用的帧缓冲区即可记录命令的正确方法是什么?

您可以使用多个线程为使用辅助命令缓冲区的同一个渲染过程生成绘制命令。您可以在同一帧中并行生成不同渲染通道的工作——只有最后一个通道(通常是后处理通道)取决于特定的交换链图像、所有阴影通道、gbuffer/shading/lighting 个通道,以及除最后的后处理过程没有。这不是必需的,但在您已经生成许多先前的通道之后,甚至在您准备好开始生成最终的渲染通道之前,甚至不要调用 vkAcquireNextImageKHR 通常是个好主意。

首先要明确:

In order to begin issuing vkCmd*s, you need to begin a render pass.

这不一定是真的。在命令缓冲区中 您可以记录多个不同的命令,所有这些命令都以 vkCmd 开头。这些命令中只有一部分需要记录在渲染过程中——那些与绘图相关的命令。有一些命令不能在渲染过程中调用(例如调度计算着色器)。但这只是解决问题的旁注。

接下来 - 提到了三重缓冲。在 Vulkan 中,图像的显示方式取决于支持的呈现模式。不同的硬件供应商,甚至不同的驱动程序版本,可能会提供不同的呈现模式,因此在一个硬件上你可能会得到与三重缓冲(MAILBOX)最相似的呈现模式,但在其他硬件上你可能得不到。呈现模式影响呈现引擎允许您从交换链获取图像,然后将它们显示在屏幕上的方式。但正如您所指出的,您不能依赖于返回图像的顺序,因此您不应将您的应用程序设计为好像您在所有平台上始终具有相同的行为。

但要回答你的问题 - 最简单、天真的方法是在帧的开头调用 vkAcquireNextImageKHR(),记录使用它返回的图像的命令缓冲区,提交这些命令缓冲区并呈现图片。您可以按需创建帧缓冲区,就在您需要在命令缓冲区内使用它之前:您创建一个使用适当图像的帧缓冲区(与 vkAcquireNextImageKHR() 函数返回的索引相关联的图像)以及提交命令缓冲区之后当他们停止使用它时,您就将其销毁。 Vulkan Cookbook 中介绍了此类行为:here and here.

更合适的方法是为所有可用的交换链图像准备帧缓冲区,并在帧期间采用适当的帧缓冲区。但是您需要记住在重新创建交换链时重新创建它们。

更高级的场景会推迟获取交换链,直到真正需要它为止。 vkAcquireNextImageKHR() 函数调用可能会阻塞您的应用程序(等到图像可用),因此在您准备帧时应尽可能晚地调用它。这就是为什么您应该首先记录不需要引用交换链图像的命令缓冲区(例如那些在延迟着色算法中将几何图形渲染到 G 缓冲区中的命令缓冲区)。之后当你想在屏幕上显示图像时(例如一些后处理技术)你只需采用上述方法:获取图像,准备适当的命令缓冲区并呈现图像。

您还可以预先记录引用特定交换链图像的命令缓冲区。如果您知道图像的来源将始终相同(如提到的 G 缓冲区),您可以拥有一组命令缓冲区,这些缓冲区总是从该数据到所有交换链图像执行一些 postprocess/copy-like 操作 - 一个每个交换链图像的命令缓冲区。然后,在帧期间,如果设置了所有数据,则获取图像,检查哪个预先记录的命令缓冲区是合适的,并提交与获取的图像相关联的命令缓冲区。

有多种方法可以实现你想要的,所有这些都取决于许多因素 - 性能、平台、你想要实现的特定目标、你在你的应用程序中执行的操作类型、你实现的同步机制等等事物。你需要弄清楚什么最适合你。但最后 - 如果您想在屏幕上显示图像,您需要在命令缓冲区中引用交换链图像。我建议首先从最简单的选项开始,然后,当您习惯它时,您可以改进您的实现以获得更高的性能、灵活性、更容易的代码维护等。

您可以在任何线程中调用vkAcquireNextImageKHR。只要您确保对您传递给它的交换链、信号量和栅栏的访问是同步的。

没有其他限制您在任何线程中调用它,包括录制线程。

您也可以一次获取多张图像。假设你已经创造了足够多的东西。换句话说,在展示当前图像之前获取下一张图像是允许的。

您有一个或多个要每帧执行的渲染过程。每一个都有一个或多个子通道,你想在其中投入工作。因此,您的主渲染线程将为这些子通道生成一个或多个辅助命令缓冲区,并将该辅助 CB 序列传递给提交线程。

提交线程将创建要呈现的主要 CB。它 begins/ends 渲染通道,并进入每个子通道,它执行在渲染线程上为该特定子通道创建的辅助 CB。

所以每个线程都在创建自己的命令缓冲区。提交线程是处理 VkFramebuffer 对象的线程,因为它开始渲染过程。它也是获取交换链图像等的那个。渲染线程是使辅助 CB 完成所有实际工作的线程。

是的,您仍将在提交线程上进行一些 CB 构建,但总体上应该非常简约。这也用于从渲染线程中抽象出渲染目标的细节,以便处理交换链的代码可以本地化到提交线程。这为您提供了更大的灵活性。

例如,如果您想要三重缓冲区,而交换链实际上不允许这样做,那么您的提交线程可以创建自己的额外图像,然后从其内部图像复制到真正的交换链中。渲染线程的代码根本不需要被打扰就可以做到这一点。