UE 渲染的多线程模型一句话版本:游戏线程产生意图,渲染线程消费场景,RHI 线程对 GPU 说话。这三者不是风格选择,是控制台世代(Xbox 360/PS3 之后)确立的流水线结构,为的是让 CPU 模拟和 GPU 提交重叠进行。源码:RenderCore/Public/RenderingThread.h、RenderCore/Private/RenderingThread.cpp(UE5.3)。
三条线程的分工
GameThread(GT):跑全部 gameplay 逻辑、物理、动画。它对渲染的贡献是改数据——移动 Actor、改材质参数、生成新 primitive。它从不直接调图形 API。
RenderThread(RT):把游戏线程改过的场景整理成一帧的渲染命令序列。它持有渲染侧的场景表示(FScene、FPrimitiveSceneInfo),执行视口遍历、剔除调度、RDG 构建。它调 RHI 层,但调的是"录命令",不是"提交给 GPU"。
RHIThread(RHI-T):真正把 RHI 命令翻译成平台 API(DX12 的 ID3D12GraphicsCommandList、Vulkan 的 VkCommandBuffer)并提交执行。它的存在让 RT 的录命令和 GPU 的提交进一步解耦:RT 可以录第 N+1 帧的命令,RHI 线程在提交第 N 帧。
开启状态看 r.RHIThread.Enable(DX12/Vulkan 默认开,部分平台强制)。关掉它 RHI 命令就在 RT 上直接提交,省一条线程上下文切换,但 RT 会被 GPU 驱动的同步调用拖住——实测在命令量大的场景 stat rendering 的 RT 耗时上涨就是这个原因。
ENQUEUE_RENDER_COMMAND 的真相
这个宏(RenderingThread.h)经常被误解为"把函数指针扔进队列等渲染线程执行"。实际它建一个 FRenderCommand 对象(捕获 lambda),塞进当前线程到渲染线程的命令管道:
// 典型用法
ENQUEUE_RENDER_COMMAND(MyUpdateCmd)(
[Scene = Scene](FRHICommandListImmediate& RHICmdList)
{
Scene->UpdateMyStuff(); // 在渲染线程上执行
});
两个必须知道的性质。其一,lambda 内访问的一切必须是渲染线程可安全触碰的:通常是 FScene 侧的镜像数据,不是游戏线程的 UObject。把 AActor 指针捕获进去就是跨线程裸奔,崩溃日志里表现为 !IsInRenderingThread() 断言或随机损坏。引擎给游戏侧对象的规矩是:UObject 改场景数据时发命令,由渲染侧代理(SceneProxy)在命令里改自己的副本。
其二,入队不保证执行时机。命令在 RT 处理到对应栅栏时才跑,晚一两帧是常态。所以"我在 ENQUEUE 之后立刻读 GPU 侧结果"是错的——要么继续传命令读,要么用 fence 等。
帧同步:双向栅栏
两条线程不是全异步的,帧边界用一对栅栏卡死:
- RenderingFence(帧末栅栏):RT 完成一帧的命令处理时更新。GT 在准备下一帧的渲染相关数据前等上一帧的栅栏(
FrameNumber对不上就阻塞),保证"游戏线程改场景数据时,渲染线程已经不用上一版快照"。 - RHI 提交栅栏:RHI 线程提交完第 N 帧后释放,RT 复用命令缓冲区内存靠它保证。
时序上形成了经典的二级流水:
帧 N: GT 改场景 → 发命令 ─┐
帧 N: RT 处理命令(用帧 N-1 快照不成立,用 GT 本帧已发的)
帧 N: RHI 提交帧 N-1 的命令
关键洞察:GT 改场景和 RT 渲场景用的是同一份 UObject 数据,靠栅栏保证错开。GT 在等 RT 栅栏时阻塞,这就是 stat game 里 GameThread 偶尔"莫名"等待的来源——不是游戏逻辑慢,是渲染追不上。反过来 RT 等 GT 则表现为渲染线程空闲(stat rendering 里大片 Waiting)。分析卡顿先看两边谁在等谁。
为什么不是更多线程
RT 只有一条,理由和物理线程类似:场景数据的组织(剔除结构、渲染项排序)在多线程下需要全量加锁,锁竞争会吃掉并行收益。真正的并行在更下层——RT 内的工作被拆给 TaskGraph 的任务(剔除、网格处理并行),RDG pass 间无依赖的也可并行录命令。所以 UE 的 CPU 渲染扩展性是"RT 主流程串行 + 任务并行",加渲染线程数不是旋钮。
排障速查
- 崩溃栈出现
RenderThread+ 访问 UObject:跨线程访问,查最近新加的 ENQUEUE 捕获。 stat unit里 Game 和 Draw 都高但 GT 逻辑不多:大概率 GT 在等 RT 栅栏,渲染侧是瓶颈。- RHI 线程 CPU 高:
stat rhi看提交量,draw call 太多的第一征兆。 - 想关 RHI 线程做对照实验:
r.RHIThread.Enable 0后重启生效,对比stat rendering差异。