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 (GT) Render Thread (RT) RHI Thread ───────────────── ────────────────── ────────── UObjects, Blueprints → FSceneRenderer::Render() → RHICreateTexture() UWorld::Tick() FRDGBuilder setup RHIDrawIndexedPrimitive() Actor/Component API RDG compile + execute Platform API (D3D12/Vulkan) ↕ ENQUEUE_RENDER_COMMAND ↕ RHICmdList.Flush() Frame N | Frame N+1 | Frame N+2

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
}
Thread check macros: Always guard code with the correct thread assertion: 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说明
FRHICommandListImmediateRender Thread (primary)渲染线程(主)Commands execute immediately if RHI thread disabled; otherwise enqueued禁用 RHI 线程时立即执行;否则入队
FRHICommandListParallel recording并行录制Secondary command lists for parallel render passes并行渲染通道的辅助命令列表
FRHIComputeCommandListCompute 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 允许领先的程度:

Frame N: GT runs world tick ──────────────────────────────→ RT renders frame N ─────────────────────→ Frame N+1: GT runs world tick (starts before RT finishes N!) ──→ RT renders frame N+1 ──→ ↑ This is the "1 frame latency"
Input latency implication: Because GT runs ahead of RT, there is inherently 1 frame of render latency on PC. On some platforms UE can reduce this to zero with r.OneFrameThreadLag=0, but this eliminates the pipelining benefit and may cause hitches. 输入延迟影响:由于 GT 领先 RT,PC 上固有 1 帧渲染延迟。在某些平台上,UE 可以通过 r.OneFrameThreadLag=0 将其减少到零,但这会消除流水线收益并可能导致卡顿。

Common Pitfalls常见陷阱

Source Navigation源码导航

Unreal Engine 5.6 · Engine/Source/Runtime/RenderCore/
Source analysis · For learning purposes