游戏引擎-Vulkan 多线程渲染:并行录制与 UE4 架构改造

文章目录

游戏引擎-Vulkan 多线程渲染:并行录制与 UE4 架构改造


💡 TL;DR / 文章摘要:

Vulkan 最大的优势之一是其内建了极度自由的多线程支持能力,允许 Command Buffer 在多个物理设备和线程上并行处理。本文详细剖析了 Vulkan API 下多线程渲染的理论基础,探讨了 Async Pass (Pass级别)Async Drawcall (Drawcall级别) 两种并行粒度设计。最后,结合虚幻引擎 (UE4),阐述了如何在实际的 3D 引擎架构中落地这套并发机制以突破移动端 Drawcall 瓶颈。


1. Vulkan 视角下的多线程渲染核心框架

要深入理解多线程渲染,首先需要从 Vulkan API 的顶层框架入手。与 OpenGL ES (GLES) 将 Command Buffer 隐藏在单一链条后的设计不同,Vulkan 最大的特点是显式化与高度并行化

Vulkan 允许在多个设备多个线程上同时处理多种类型的 Command Buffer。其层级结构如下:

  • Physical Device (物理设备):Vulkan 支持同一设备组中的多个物理设备联合创建一个逻辑设备 (Device)。
  • Queue Families (队列族):每个物理设备拥有多个 Queue,归属于不同的 Queue Family(如 Graphics, Compute, Transfer 等)。
  • Command Pool & Command Buffer (命令池与命令缓冲区):每个 Queue Family 可以创建对应的 Command Pool,进而分配出 Command Buffer。这些 Buffer 是 CPU 与 GPU 之间通信的桥梁。

CPU 将渲染指令记录到 Command Buffer 中,随后将其提交至 Queue 交由 GPU 执行。Vulkan 内部还允许使用 Secondary Command Buffer 被主 Command Buffer (Primary Command Buffer) 嵌套调用,这种设计为极细粒度的并行创造了条件。


2. 渲染管线中哪些阶段可以被并行?

在 CPU 一侧,主要存在以下几个可以被并行处理的阶段:

  1. Command Buffer 的录制 (Record):所有 vkCmd*** 类型的 API 调用本质上都是在进行指令录制。我们可以拆解出多个独立的 Command Buffer,由不同的 RHI 线程进行并行调用。(这是解决游戏 Drawcall 数量瓶颈的核心优化点)
  2. Command Buffer 的提交 (Submit):不同的 Command Buffer 可以被并行提交到独立的 Queue 中。(但在实际工程中,Submit 耗时通常不足 1ms,且大多是一帧提交一次,此优化性价比不高)。
  3. 录制与提交的跨帧并行:在当前帧录制 Command Buffer,在下一帧提交。(引入了一帧的延迟,性价比一般)。
  4. 多重管线异步 (Async Compute & Transfer):为 Transfer(资源准备)、Graphics(图形绘制)、Compute(计算着色器)建立各自独立的 Queue 和 Command Buffer。从而让 Drawcall、CS Dispatch 与资源上传在不同线程上独立运行,大幅减少图形流水线被阻塞的概率。

🔍 Jungleman Pro 洞察:突破瓶颈的关键 在上述策略中,“并行录制 Command Buffer” 是现代引擎(如 UE5 / 自研引擎)解决海量 Drawcall 的最核心手段。本文的重点也聚焦于这一环节的架构设计。


3. 深入探讨:并行粒度策略

在讨论并行拆分前,必须了解 Vulkan 的一个刚性限制:一个 Renderpass 必须被完整包含在同一个 Command Buffer 中(无法跨越)。为了在减少带宽消耗(控制 Renderpass 数量)与并行处理海量 Drawcall 之间取得平衡,我们设计了两种粒度的并行方案。

策略一:Pass 级别的并行 (Async Pass)

这是最基础也是最易实现的并行模式。每个 Command Buffer 内部封装一个或多个完整的 Renderpass。

  • 运行机制:不同的工作线程分配独立的 Command Buffer。各个 Renderpass 在各自的线程中并行地调用渲染 API。
  • 适用场景:当各个 Renderpass 之间相对独立,且单个 Pass 内的 Drawcall 数量处于合理区间时使用。
  • 优势:实现极其简单,不存在复杂的线程同步与状态管理,各个线程正常开启、结束各自的 Pass 即可。

策略二:Drawcall 级别的并行 (Async Drawcall)

当单个 Renderpass 中的 Drawcall 数量极其庞大(例如上千个物件的 G-Buffer 渲染)时,Pass 级并行将失去意义,必须引入更为复杂的 Drawcall 级别并行

由于 API 限制 Renderpass 不能跨越 Command Buffer,我们必须使用 Secondary Command Buffer (次级命令缓冲区)

  • Secondary Command Buffer 内部不能执行 Renderpass 开始/结束的操作,只能记录 Drawcall 相关的纯渲染 API。
  • 它无法直接 Submit 给 Queue,只能通过在 Primary Command Buffer 中调用 vkExecuteCommands 的方式被“内嵌”执行。
  • 主 Buffer 在结束录制(vkEndCommandBuffer)之前,必须完成对次级 Buffer 的 Execute。

架构落地拆解

假设 主线程(Thread 1) 的 Renderpass 包含 300 个 Drawcall,我们需要将其等分为 3 份交由子线程处理:

  1. 启动 3 个子线程(Thread 1.1 - 1.3),各自持有一个 Secondary Command Buffer,并行填充 Drawcall。
  2. 主线程(Thread 1)的 Primary Command Buffer 负责 Execute 这些次级 Buffer。
  3. 避免主线程被阻塞的技巧:主线程不需要干等子线程结束。主线程可以在需要内嵌 Secondary Command Buffer 时,直接开辟一个新的 Primary Command Buffer (n+1) 去记录后续的指令。旧的 Primary Command Buffer (n) 挂起等待其关联的次级线程完成即可。最终在 Submit 时,只需保证 Buffer(n) 位于 Buffer(n+1) 之前,即可完美保证渲染的先后时序。

基于这套机制,理论上所有的 Drawcall 都可以被无限拆分并行,从而彻底吃满多核 CPU 的性能。


4. 实战应用:UE4 现有多线程渲染框架与改造

4.1 现存架构与瓶颈

原生的虚幻 4 (UE4) 引擎已经实现了基本的多线程管线:逻辑更新 (Game Thread)渲染指令生成 (Render Thread)API 调用 (RHI Thread) 三大任务被分离在不同线程。项目引擎甚至引入了 Auxiliary RHI 机制,将资源生成任务放到单独的 RHI 线程(可跑在小核上),防止资源准备阻塞 Drawcall 的提交。

4.2 Vulkan 极致优化改造目标

为了在 Vulkan 下将性能推向极限,新的架构改造目标如下:

  • 保留主 RHI 线程 (Main RHI Thread):专门处理那些不适合拆分到子线程的杂项 API 调用,以及负责最终帧的 Submit 和 Present。
  • 异步 Render Pass:对相互独立的 Pass 进行 Async Pass 分流。
  • 异步 Draw Call:对体量庞大(如 Base Pass)的节点,拆分提取进行 Async Drawcall 处理。
  • Render Thread 级联并行:为了能够异步地填充 RHI Command,原本单一的 Render Thread 也被相应拆开,实现与多个 RHI Thread 一对一绑定的全面并行流。

4.3 移动端多线程的特殊考量 (Performance Caveats)

对于移动端平台,多线程并非“银弹”。我们常说不要在移动端肆意开启过多的线程。因为目前顶级的 Android 设备通常也只有 4 个性能大核(甚至更少),盲目开辟十几个渲染线程不仅无法提升效率,反而会因为频繁的上下文切换 (Context Switch) 和核心间的缓存失效引发严重的性能发热与降频。因此,在移动端落地时,线程池的规模必须根据 CPU 拓扑结构 (Big.LITTLE 架构) 进行动态调度与精细化管理。


本文作者:Berg Zha

本文链接:https://junglemanpro.com/posts/52bc0277/

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

关于

我是谁
一个热爱底层渲染的人
与我交流
bergzha@gmail.com
ESC 关闭 | 导航 | Enter 打开
输入关键词开始搜索