使用异步计算来饱和 GPU

此示例的源代码可以在 Khronos Vulkan 示例 github 存储库中找到。

概述

大多数 Vulkan 实现都会公开多个 Vulkan 队列,应用程序可以随时使用这些队列。硬件公开多个队列的主要动机是,我们可以在比线程更高的级别上表达并行性。

计算一切 - 后期处理案例研究

在现代游戏引擎中,计算着色器越来越多地用于执行“所有事情”,除了主通道光栅化。此示例旨在演示一些我们可以用来在基于图块的渲染器(尤其是)上获得最佳行为的技术。正如我们稍后将讨论的那样,由于架构差异,即时模式渲染器的策略有所不同。

在基于图块的延迟渲染器 (TBDR) 上进行计算着色器后期处理的挑战

TBDR 架构将顶点着色和片元着色分为两部分。首先,对顶点进行着色和分箱,一旦完成所有这些操作,就会进行片元着色。一个关键的性能优势是,渲染通道 N + {1, 2, …​} 中的顶点着色可以与渲染通道 N 中的片元着色重叠。当针对这些 GPU 上的最佳性能时,我们必须确保永远不会阻塞片元着色。

由于这种渲染架构,这些 GPU 上应该至少有两个硬件队列。

至少在 Arm Mali GPU 上,计算工作负载与顶点着色和分箱在同一个队列中运行。这是直观的,因为顶点着色基本上与计算着色相同,只是添加了一些额外的固定功能魔术。

在这种帧中,计算着色器后期处理变得有问题

  • 光栅化通道

  • 片元 -> 计算 信号量

  • 计算通道

  • 计算 -> 片元 信号量 (性能悬崖! :<)

  • 渲染 UI + 后输出

  • 在图形中呈现

出于两个原因,在图形队列中结束帧是很自然的

  • 我们确实希望在渲染到交换链的同一渲染通道中渲染 UI。带宽非常重要,这样可以避免对可以相当大的本机分辨率 UI 图像进行回写和读取。

  • 理论上可以在一个计算线程中内联渲染 UI(VK_EXT_descriptor_indexing 肯定会有所帮助!),但这是一个极其复杂的事情。

真正的问题是这里的 计算 -> 片元 信号量。由于 片元 -> 计算 和 计算 -> 片元 屏障,我们有效地阻止了片元在 计算 运行时执行任何工作。如前所述,这是 TBDR 上的性能问题。

关于 TBDR 与即时模式 (IMR) 桌面 GPU 上计算后期处理的说明

这里更像桌面风格的方法是从异步计算呈现,即在计算中完成所有事情,并尝试使用这种方法

  • 光栅化通道

  • 片元 -> 计算 信号量

  • 计算后期处理

  • 在图形队列中渲染 UI(带宽命中,但我们并不真正在意)

  • 片元 -> 计算 信号量

  • 在计算着色器中合成最终结果

  • 在计算中呈现

从异步计算中呈现是一个不同的话题,本示例不涵盖,但这是需要记住的。

使用多个图形队列来消除瓶颈

一些 GPU 在 Vulkan 中公开多个图形队列。这非常方便,因为我们可以通过这种方式解决屏障问题。假设我们有两个支持所有功能的 VkQueues,我们可以像这样渲染帧:

  • 光栅化过程(队列 #1)

  • 片段(队列 #1)-> 计算(队列 #0)信号量

  • 计算过程(队列 #0)

  • 计算(队列 #0)-> 片段(队列 #0)信号量 (没有性能 OOF! :>)

  • 渲染 UI + 后期输出(队列 #0)

  • 在图形中呈现(队列 #0)

  • …​

  • 光栅化过程(队列 #1)不会被计算阻塞,实现重叠 :>

队列优先级

锦上添花的是调整队列优先级。队列优先级的行为与实现无关,但其目的是允许驱动程序优先处理一个队列中的工作而不是另一个。在我们的例子中,我们应该将队列 #0 设置为高优先级,将队列 #1 设置为低优先级,因为帧后期进行的工作比下一帧早期进行的工作更重要。从延迟的角度来看,如果队列 #0 可以中断队列 #1,那就最理想了。

手动重新排序过程?

本示例中方法的替代方案是推迟提交 UI + 呈现工作,并在阻塞计算工作之前开始提交下一帧的图形工作。这是有问题的,因为:

  • 它增加了处理多个正在进行的帧的复杂性。

  • 它增加了不必要的输入延迟。当我们在帧之间增加更多重叠时,我们也会降低响应速度,这对交互式内容非常重要。

示例

HDR sample

该示例实现了一个非常基本的渲染管线,演示了一个合理的渲染场景,包括:

  • 以 8K 分辨率渲染定向阴影贴图

  • 以 4K 分辨率渲染具有非常基本光照的 HDR 图像

  • 在异步计算中进行非常简单和粗糙的 HDR +(非常泛滥的)Bloom 管线

  • 在交换链过程中进行色调映射 + UI

这里的目标是利用阴影映射过程,该过程在固定功能光栅化性能方面受到极大的限制。如果我们可以并行进行有用的计算工作,我们应该可以在性能方面获得提升。

No async queue

这里我们看到片段周期远低于 GPU 周期。这意味着片段队列的工作量不足。这是由于上面提到的不良屏障造成的。顶点 + 片段周期仍然 > GPU 周期,这意味着存在一些重叠,但这只是顶点着色的重叠。后处理计算使 GPU 工作量不足。

With async queue

在这里我们可以看到一个不错的性能提升(21.8 毫秒 vs. 22.9 毫秒),并且片段周期现在非常接近 GPU 周期,这意味着没有发生工作量不足的情况。请注意,性能在这里并没有显着提升,我们也不应该期望它会这样。虽然顶点周期和片段周期都增加,但它们仍然在同一着色器核心上竞争资源。我们为获得良好重叠所做的工作意味着 GPU 在硬件队列耗尽工作时,总是有事情可做。

选项

  • 启用异步队列:使用多个队列以避免片段队列停滞。

  • 双缓冲 HDR:旨在利用更多的重叠机会。

  • 旋转阴影:禁用动画光,由于开启时性能会略有波动,因此很难研究性能差异。

最佳实践总结

这些技巧在某种程度上是 TBDR 特有的。

应该做

  • 如果有任何片段 -> 计算工作负载发生,请使用多个 Vulkan 队列。

  • 任何依赖于片段的计算工作都应该在不同的队列中完成,以避免片段停滞。

  • 在呈现最终图像的队列上使用更高的优先级。

不应该做

  • 除非您有计划如何避免不可避免的计算 -> 片段屏障,否则不要引入片段 -> 计算屏障。

调试

  • IHV 分析工具可以可视化不同硬件队列的饱和程度。