Thread Concurrency — Game, Render & RHI Threads线程并发 — Game、Render 与 RHI 线程
The Three-Thread Architecture三线程架构
UE runs rendering work across three threads, each with a distinct role and a strict rule: never touch another thread's objects from the wrong thread.
UE 将渲染工作分布在三个线程上,每个线程有明确的职责和严格规则:绝不从错误的线程访问另一个线程的对象。
- Game Thread: Runs UObject logic, Blueprint, physics, AI. Issues render commands via
ENQUEUE_RENDER_COMMAND. Never calls RHI directly. - Game Thread:运行 UObject 逻辑、蓝图、物理、AI。通过
ENQUEUE_RENDER_COMMAND发出渲染命令。永远不直接调用 RHI。 - Render Thread: Executes render commands from the GT queue. Builds and executes the RDG. Creates logical RHI commands (
FRHICommandList). Does not submit to GPU directly. - Render Thread:执行来自 GT 队列的渲染命令。构建并执行 RDG。创建逻辑 RHI 命令(
FRHICommandList)。不直接向 GPU 提交。 - RHI Thread: Translates
FRHICommandListto actual platform API calls (D3D12, Vulkan, Metal). Handles GPU submission, fence management, and resource destruction. - RHI Thread:将
FRHICommandList翻译为实际平台 API 调用(D3D12、Vulkan、Metal)。处理 GPU 提交、fence 管理和资源销毁。
ENQUEUE_RENDER_COMMAND — The Primary Sync MechanismENQUEUE_RENDER_COMMAND — 主要同步机制
The canonical way to send work from the Game Thread to the Render Thread. Under the hood, EnqueueUniqueRenderCommand() dispatches a task to the render thread's task graph.
从 Game Thread 向 Render Thread 发送工作的规范方式。在底层,EnqueueUniqueRenderCommand() 将任务分发到渲染线程的任务图。
// Typical usage: pass data from game thread to render thread // IMPORTANT: capture data by VALUE — the game thread may move on before the lambda executes float MyValue = SomeActor->GetValue(); // game thread reads UObject FMyRenderResource* RenderResource = MyRenderComponent->GetRenderResource(); // proxy pointer ENQUEUE_RENDER_COMMAND(FMyUpdateCommand)( [MyValue, RenderResource](FRHICommandListImmediate& RHICmdList) { // This lambda runs on the RENDER THREAD // Safe to access FRenderResource, FSceneProxy, RHI resources // NOT safe to access UObjects, AActor, etc. RenderResource->UpdateUniformBuffer(RHICmdList, MyValue); }); // Implementation (RenderingThread.h:378): template<typename RenderCommandTag, typename LambdaType> FORCEINLINE_DEBUGGABLE void EnqueueUniqueRenderCommand(LambdaType&& Lambda) { if (IsInRenderingThread()) Lambda(GetImmediateCommandList_ForRenderCommand()); // already on RT: run immediately else if (ShouldExecuteOnRenderThread()) FRenderThreadCommandPipe::Enqueue<RenderCommandTag>(MoveTemp(Lambda)); // enqueue to RT else Lambda(GetImmediateCommandList_ForRenderCommand()); // single-threaded: run now }
check(IsInGameThread()), check(IsInRenderingThread()), or check(IsInRHIThread()). These are compiled out in Shipping builds but are invaluable for catching threading bugs.
线程检查宏:始终用正确的线程断言保护代码:check(IsInGameThread())、check(IsInRenderingThread()) 或 check(IsInRHIThread())。这些在 Shipping 版本中被编译掉,但对于发现线程错误非常有价值。
FRenderCommandFence — Waiting for the Render ThreadFRenderCommandFence — 等待渲染线程
FRenderCommandFence is the standard mechanism for the Game Thread to wait for a specific render command to complete — for example, when destroying a component whose render proxy is still in use.
FRenderCommandFence 是 Game Thread 等待特定渲染命令完成的标准机制——例如,当销毁一个渲染代理仍在使用中的组件时。
// In a component's destructor: class UMyRenderComponent : public UPrimitiveComponent { FRenderCommandFence RenderFence; virtual void BeginDestroy() override { Super::BeginDestroy(); // Enqueue cleanup command to render thread, then insert a fence DetachFromScene(); RenderFence.BeginFence(); // signals RT to insert fence after processing current commands } virtual bool IsReadyForFinishDestroy() override { // Called repeatedly by the GC — return true only when RT has passed the fence return RenderFence.IsFenceComplete(); } };
FlushRenderingCommands — Full RT SyncFlushRenderingCommands — 完全 RT 同步
FlushRenderingCommands() blocks the calling thread (always the Game Thread) until the Render Thread has processed all pending commands. This is expensive — it stalls both threads and should only be used during level loads, editor operations, or one-off maintenance tasks, never per-frame.
FlushRenderingCommands() 阻塞调用线程(始终是 Game Thread),直到 Render Thread 处理完所有待处理命令。这很昂贵——它会停滞两个线程,应该只在关卡加载、编辑器操作或一次性维护任务中使用,绝不用于每帧。
// Correct usage: during hot-reload or asset reimport FlushRenderingCommands(); // blocks until RT is empty // Now safe to modify resources that the RT was using MyMaterial->Modify(); // Internally dispatches a sentinel render command and waits on it: // ENQUEUE_RENDER_COMMAND → FRenderCommandFence::BeginFence → wait
RenderCommandPipe — Parallel Scene UpdateRenderCommandPipe — 并行场景更新
UE 5.x introduced FRenderCommandPipe — a system where the Game Thread can record batches of render commands into named pipes, which are then replayed on the Render Thread. This allows parallel recording of render commands from multiple game thread tasks without immediate RT dispatch. Commands within a pipe are ordered; commands across pipes may be concurrent.
UE 5.x 引入了 FRenderCommandPipe——Game Thread 可以将一批渲染命令记录到命名管道,然后在 Render Thread 上重放。这允许多个 Game Thread 任务并行记录渲染命令,而无需立即分发到 RT。同一管道内的命令有序;不同管道间的命令可能并发。
// Define a named pipe (static, registered at startup) DEFINE_RENDER_COMMAND_PIPE(FMyComponentPipe, "MyComponent"); // Record commands into the pipe from any thread: FRenderCommandPipe::Enqueue<FMyComponentPipe>( [Data = MyData](FRHICommandList& RHICmdList) { // Executes on RT, in-order relative to other FMyComponentPipe commands }); // Stop recording and sync all pipes to RT (called at end of GT tick) UE::RenderCommandPipe::StopRecording();
RHI Thread & Command ListsRHI 线程与命令列表
The Render Thread does not submit commands to the GPU — it records them into FRHICommandList objects. The RHI Thread then drains these lists, translating each FRHICommand into platform API calls. This separation allows the RT to stay ahead of GPU submission, buffering commands for the driver to pipeline.
Render Thread 不直接向 GPU 提交命令——它将命令记录到 FRHICommandList 对象中。RHI Thread 随后排空这些列表,将每个 FRHICommand 转换为平台 API 调用。这种分离使 RT 能够领先于 GPU 提交,为驱动程序流水线化命令提供缓冲。
| Type类型 | Used by使用方 | Notes说明 |
|---|---|---|
| FRHICommandListImmediate | Render Thread (primary)渲染线程(主) | Commands execute immediately if RHI thread disabled; otherwise enqueued禁用 RHI 线程时立即执行;否则入队 |
| FRHICommandList | Parallel recording并行录制 | Secondary command lists for parallel render passes并行渲染通道的辅助命令列表 |
| FRHIComputeCommandList | Compute passes计算通道 | Subset of FRHICommandList — only compute commandsFRHICommandList 的子集——仅计算命令 |
Frame Pipelining — GT Leads RT by 1+ Frames帧流水线 — GT 领先 RT 1+ 帧
Under normal operation, the Game Thread runs ahead of the Render Thread by approximately one frame. This is controlled by FFrameEndSync::Sync(), called at the end of each GT tick. The sync policy determines how far the GT is allowed to lead:
正常运行下,Game Thread 领先 Render Thread 约一帧。这由每次 GT Tick 结束时调用的 FFrameEndSync::Sync() 控制。同步策略决定 GT 允许领先的程度:
r.OneFrameThreadLag=0, but this eliminates the pipelining benefit and may cause hitches.
输入延迟影响:由于 GT 领先 RT,PC 上固有 1 帧渲染延迟。在某些平台上,UE 可以通过 r.OneFrameThreadLag=0 将其减少到零,但这会消除流水线收益并可能导致卡顿。
Common Pitfalls常见陷阱
- Capturing UObject pointers in render commands: UObjects can be garbage collected at any time on the GT. If a render command captures a raw
UObject*, it may be invalid by the time it executes. Always capture proxy data by value, or useTWeakObjectPtrand check validity inside the lambda. - 在渲染命令中捕获 UObject 指针:UObject 可能在 GT 上随时被垃圾回收。如果渲染命令捕获了裸
UObject*,执行时可能已无效。始终按值捕获代理数据,或使用TWeakObjectPtr并在 lambda 内检查有效性。 - Calling FlushRenderingCommands() per-frame: This is a frame-rate killer. Each call stalls the GT until the RT drains its queue, breaking the pipeline. Profile with Unreal Insights to find per-frame flushes.
- 每帧调用 FlushRenderingCommands():这会严重影响帧率。每次调用都会停滞 GT,直到 RT 排空队列,破坏流水线。使用 Unreal Insights 查找每帧的 flush。
- Modifying RenderProxy data on the GT after enqueue: Data captured in a render command lambda must not be modified after enqueue. Once the lambda is in the queue, the RT may read it at any moment.
- 入队后在 GT 修改 RenderProxy 数据:在渲染命令 lambda 中捕获的数据在入队后不得修改。一旦 lambda 进入队列,RT 随时可能读取它。
Source Navigation源码导航
- RenderCore/Public/RenderingThread.h:378EnqueueUniqueRenderCommand() — full implementation with thread dispatch logicEnqueueUniqueRenderCommand() — 含线程分发逻辑的完整实现
- RenderCore/Public/RenderingThread.h:404FRenderCommandPipe — parallel render command pipe systemFRenderCommandPipe — 并行渲染命令管道系统
- RenderCore/Private/RenderingThread.cppRT startup, FlushRenderingCommands(), FFrameEndSyncRT 启动、FlushRenderingCommands()、FFrameEndSync
- RHI/Public/RHICommandList.hFRHICommandList, FRHICommandListImmediate — command recording API命令录制 API
- Engine/Source/Runtime/Engine/Public/PrimitiveSceneProxy.hRT-side scene object — the render thread's view of UPrimitiveComponentRT 侧场景对象——渲染线程对 UPrimitiveComponent 的视图