UE4下Component数据更新机制(GameThread->RenderThread)
UWorld 维护两个数组:
-
TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate(可并行执行) -
TArray<UActorComponent*> ComponentsThatNeedEndOfFrameUpdate_OnGameThread(必须 GameThread 执行)
用途是收集那些需要在帧末把数据从游戏线程推送到渲染线程的组件(如 SkeletalMesh、StaticMesh 的 Transform 更新等)
将Component放到ComponentsThatNeedEndOfFrameUpdate 中,可提升游戏并发度,有利于提升游戏性能
两者的分流由 MarkActorComponentForNeededEndOfFrameUpdate 函数来决策
| 函数 | 行为 | 线程约束 |
|---|---|---|
MarkActorComponentForNeededEndOfFrameUpdate |
Add 到数组尾部 | 必须 GameThread |
ClearActorComponentEndOfFrameUpdate |
把对应槽位置 nullptr(不真正删除,避免 index 失效) |
必须 GameThread |
在ClearActorComponentEndOfFrameUpdate 函数中,将对应Component在数组所在Index的元素设置为nullptr,而不调用 RemoveAt进行删除
if (CurrentState == EComponentMarkedForEndOfFrameUpdateState::Marked) { const int32 ArrayIndex = FMarkComponentEndOfFrameUpdateState::GetArrayIndex(Component); check(ComponentsThatNeedEndOfFrameUpdate.IsValidIndex(ArrayIndex)); check(ComponentsThatNeedEndOfFrameUpdate[ArrayIndex] == Component); ComponentsThatNeedEndOfFrameUpdate[ArrayIndex] = nullptr; } else if (CurrentState == EComponentMarkedForEndOfFrameUpdateState::MarkedForGameThread) { const int32 ArrayIndex = FMarkComponentEndOfFrameUpdateState::GetArrayIndex(Component); check(ComponentsThatNeedEndOfFrameUpdate_OnGameThread.IsValidIndex(ArrayIndex)); check(ComponentsThatNeedEndOfFrameUpdate_OnGameThread[ArrayIndex] == Component); ComponentsThatNeedEndOfFrameUpdate_OnGameThread[ArrayIndex] = nullptr; } FMarkComponentEndOfFrameUpdateState::Set(Component, INDEX_NONE, EComponentMarkedForEndOfFrameUpdateState::Unmarked);
除了删除时引发数组性能问题外,还有一个原因是:
在 SendAllEndOfFrameUpdates 函数的 ParallelFor 期间,如果某个 Component 通过其它路径触发了 则后续并行任务持有的 Index 会全部错位,引发逻辑问题。ClearActorComponentEndOfFrameUpdate,
Component状态和Index缓存
每个 Component 内部存了 2 bit 状态(uint8 MarkedForEndOfFrameUpdateState:2) + 1个 ArrayIndex(int32 MarkedForEndOfFrameUpdateArrayIndex)
用来记录一个Component "在哪个数组中 + 在该数组的什么位置"
EComponentMarkedForEndOfFrameUpdateState三种状态:
namespace EComponentMarkedForEndOfFrameUpdateState { enum Type { Unmarked, // 不在任何数组中 Marked, // 在 ComponentsThatNeedEndOfFrameUpdate 数组中 MarkedForGameThread, // 在 ComponentsThatNeedEndOfFrameUpdate_OnGameThread 数组中 }; }
FMarkComponentEndOfFrameUpdateState::Set 是状态切换和设置ArrayIndex索引值的唯一入口
struct FMarkComponentEndOfFrameUpdateState { private: FORCEINLINE static void Set(UActorComponent* Component, int32 ArrayIndex, const EComponentMarkedForEndOfFrameUpdateState::Type UpdateState) { checkSlow(UpdateState < 4); // Only 2 bits are allocated to store this value Component->MarkedForEndOfFrameUpdateState = UpdateState; Component->MarkedForEndOfFrameUpdateArrayIndex = ArrayIndex; } }
状态机的所有 Set 操作都成对维护 (State, ArrayIndex):
| 状态 | ArrayIndex 含义 | 数组位置不变量 |
|---|---|---|
Unmarked |
INDEX_NONE |
不在任何数组中 |
Marked |
[0, ComponentsThatNeedEndOfFrameUpdate.Num()) |
Components[ArrayIndex] == Component |
MarkedForGameThread |
[0, ComponentsThatNeedEndOfFrameUpdate_OnGameThread.Num()) |
ComponentsThatNeedEndOfFrameUpdate_OnGameThread[ArrayIndex] == Component |
注:FMarkComponentEndOfFrameUpdateState是ActorComponent的友元类,可以操作其private变量
防止重复入队
在MarkActorComponentForNeededEndOfFrameUpdate会检查Component的EComponentMarkedForEndOfFrameUpdateState状态,防止重复入队
uint32 CurrentState = Component->GetMarkedForEndOfFrameUpdateState(); // ... if (CurrentState == EComponentMarkedForEndOfFrameUpdateState::Unmarked) { // 只有 Unmarked 才会真正 Add 到数组 }
注:一帧之内同一个 Component 可能被多次 MarkRenderTransformDirty / MarkRenderStateDirty,状态机保证它最多入队一次,避免数组膨胀和重复 DoDeferredRenderUpdates_Concurrent。
在数组中O(1)查找
配合 ArrayIndex 在ComponentsThatNeedEndOfFrameUpdate和ComponentsThatNeedEndOfFrameUpdate_OnGameThread 数组中实现 O(1) 查找
允许状态降级
如果一个 Component 已经 Marked(在并行数组),但后续又被请求 bForceGameThread=true,状态机会做"降级",将其放在Game数组中
if (CurrentState == EComponentMarkedForEndOfFrameUpdateState::Marked && bForceGameThread) { // 把原数组对应槽位置 nullptr,状态退回 Unmarked ComponentsThatNeedEndOfFrameUpdate[ArrayIndex] = nullptr; CurrentState = EComponentMarkedForEndOfFrameUpdateState::Unmarked; } // 然后下面的 Unmarked 分支会重新加到 _OnGameThread 数组
没有这个状态机,要么需要 TSet<UActorComponent*> 带来 hash 开销,要么每次 Clear 都要 O(N) 扫描,对于动辄数千个 Component 的场景是不可接受的
UWorld::SendAllEndOfFrameUpdates函数实现分析
该函数在每帧结束时被调用,负责将所有标记了"帧末更新"的组件的延迟渲染更新提交到渲染线程。整个流程分为以下阶段:
准备工作
将 ComponentsThatNeedEndOfFrameUpdate 拷贝到 静态局部变量 LocalComponentsThatNeedEndOfFrameUpdate,后续并行访问使用本地副本
定义两个工作 Lambda
ParallelWork(可并行执行)
auto ParallelWork = [](int32 Index) { UActorComponent* NextComponent = LocalComponentsThatNeedEndOfFrameUpdate[Index]; if (NextComponent) { if (NextComponent->IsRegistered() && !NextComponent->IsTemplate() && !NextComponent->IsPendingKill()) { NextComponent->DoDeferredRenderUpdates_Concurrent(); } FMarkComponentEndOfFrameUpdateState::Set(NextComponent, INDEX_NONE, EComponentMarkedForEndOfFrameUpdateState::Unmarked); } };
注:对每个组件调用 DoDeferredRenderUpdates_Concurrent()(线程安全的延迟渲染更新),然后清除标记。
GTWork(必须在游戏线程执行)
auto GTWork = [this]() { for (UActorComponent* Component : ComponentsThatNeedEndOfFrameUpdate_OnGameThread) { if (Component) { if (Component->IsRegistered() && !Component->IsTemplate() && !Component->IsPendingKill()) Component->DoDeferredRenderUpdates_Concurrent(); FMarkComponentEndOfFrameUpdateState::Set(Component, INDEX_NONE, EComponentMarkedForEndOfFrameUpdateState::Unmarked); } } ComponentsThatNeedEndOfFrameUpdate_OnGameThread.Reset(); ComponentsThatNeedEndOfFrameUpdate.Reset(); };
注:处理那些被标记为"必须在游戏线程"上更新的组件,并清空两个组件列表。
并行执行策略
if (CVarAllowAsyncRenderThreadUpdatesDuringGamethreadUpdates.GetValueOnGameThread() > 0) { ParallelForWithPreWork(LocalComponentsThatNeedEndOfFrameUpdate.Num(), ParallelWork, GTWork); } else { GTWork(); ParallelFor(LocalComponentsThatNeedEndOfFrameUpdate.Num(), ParallelWork); }
根据 CVar AllowAsyncRenderThreadUpdatesDuringGamethreadUpdates 分两种模式:
| 模式 | 行为 |
|---|---|
| 允许异步(CVar > 0) | 使用 ParallelForWithPreWork:先派发并行任务,主线程先执行 GTWork(游戏线程专属组件),然后主线程也参与 ParallelWork |
| 不允许异步(CVar = 0) | 先串行执行 GTWork,再用 ParallelFor 并行执行 ParallelWork,主线程也参与 ParallelWork |
注:前者效率更高,因为游戏线程专属工作GTWork和ParallelWork并行工作可以重叠执行,减少总耗时
ParallelForWithPreWork会调用ParallelForWithPreWorkInternal函数
// UnrealEngine\Engine\Source\Runtime\Core\Public\Async\ParallelFor.h template<typename FunctionType> inline void ParallelForWithPreWorkInternal(int32 Num, FunctionType Body, TFunctionRef<void()> CurrentThreadWorkToDoBeforeHelping, EParallelForFlags Flags = EParallelForFlags::None) { SCOPE_CYCLE_COUNTER(STAT_ParallelFor); // 单线程下情况 int32 AnyThreadTasks = 0; const bool bIsMultithread = FApp::ShouldUseThreadingForPerformance() || FForkProcessHelper::IsForkedMultithreadInstance(); if ((Flags & EParallelForFlags::ForceSingleThread) == EParallelForFlags::None && bIsMultithread) { AnyThreadTasks = FMath::Min<int32>(FTaskGraphInterface::Get().GetNumWorkerThreads(), Num); } if (!AnyThreadTasks) { // do the prework CurrentThreadWorkToDoBeforeHelping(); // no threads, just do it and return for (int32 Index = 0; Index < Num; Index++) { Body(Index); } return; } check(Num); const bool bBackgroundPriority = (Flags & EParallelForFlags::BackgroundPriority) != EParallelForFlags::None; const ENamedThreads::Type DesiredThread = bBackgroundPriority ? ENamedThreads::AnyBackgroundThreadNormalTask : ENamedThreads::AnyHiPriThreadHiPriTask; // 根据TaskGraph的work线程数,创建并发起并行任务(将Num个Component放到AnyThreadTasks个线程上跑) TParallelForData<FunctionType>* DataPtr = new TParallelForData<FunctionType>(Num, AnyThreadTasks, false, Body, Flags); TSharedRef<TParallelForData<FunctionType>, ESPMode::ThreadSafe> Data = MakeShareable(DataPtr); TGraphTask<TParallelForTask<FunctionType>>::CreateTask().ConstructAndDispatchWhenReady(Data, DesiredThread, AnyThreadTasks - 1); // 游戏线程执行prework任务 // do the prework CurrentThreadWorkToDoBeforeHelping(); // 游戏线程也帮忙执行并行任务(处理Num个Component) // this thread can help too and this is important to prevent deadlock on recursion if (!Data->Process(0, Data, DesiredThread, true)) { if (IsInRenderingThread() && (Flags & EParallelForFlags::PumpRenderingThread) != EParallelForFlags::None) { while (!Data->Event->Wait(1)) { FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GetRenderThread_Local()); } } else { Data->Event->Wait(); } check(Data->bTriggered); } else { check(!Data->bTriggered); } check(Data->NumCompleted.GetValue() == Data->Num); Data->bExited = true; // Data must live on until all of the tasks are cleared which might be long after this function exits }
ParallelFor会调用ParallelForInternal函数
// UnrealEngine\Engine\Source\Runtime\Core\Public\Async\ParallelFor.h template<typename FunctionType> inline void ParallelForInternal(int32 Num, FunctionType Body, EParallelForFlags Flags) { SCOPE_CYCLE_COUNTER(STAT_ParallelFor); check(Num >= 0); // 单线程下情况 int32 AnyThreadTasks = 0; const bool bIsMultithread = FApp::ShouldUseThreadingForPerformance() || FForkProcessHelper::IsForkedMultithreadInstance(); if (Num > 1 && (Flags & EParallelForFlags::ForceSingleThread) == EParallelForFlags::None && bIsMultithread) { AnyThreadTasks = FMath::Min<int32>(FTaskGraphInterface::Get().GetNumWorkerThreads(), Num - 1); } if (!AnyThreadTasks) { // no threads, just do it and return for (int32 Index = 0; Index < Num; Index++) { Body(Index); } return; } const bool bPumpRenderingThread = (Flags & EParallelForFlags::PumpRenderingThread) != EParallelForFlags::None; const bool bBackgroundPriority = (Flags & EParallelForFlags::BackgroundPriority) != EParallelForFlags::None; const ENamedThreads::Type DesiredThread = bBackgroundPriority ? ENamedThreads::AnyBackgroundThreadNormalTask : ENamedThreads::AnyHiPriThreadHiPriTask; // 根据TaskGraph的work线程数,创建并发起并行任务(将Num个Component放到AnyThreadTasks个线程上跑) TParallelForData<FunctionType>* DataPtr = new TParallelForData<FunctionType>(Num, AnyThreadTasks + 1, (Num > AnyThreadTasks + 1) && bPumpRenderingThread, Body, Flags); TSharedRef<TParallelForData<FunctionType>, ESPMode::ThreadSafe> Data = MakeShareable(DataPtr); TGraphTask<TParallelForTask<FunctionType>>::CreateTask().ConstructAndDispatchWhenReady(Data, DesiredThread, AnyThreadTasks - 1); // 游戏线程也帮忙执行并行任务(处理Num个Component) // this thread can help too and this is important to prevent deadlock on recursion if (!Data->Process(0, Data, DesiredThread, true)) { if (bPumpRenderingThread && IsInActualRenderingThread()) { while (!Data->Event->Wait(1)) { FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GetRenderThread_Local()); } } else { Data->Event->Wait(); } check(Data->bTriggered); } else { check(!Data->bTriggered); } check(Data->NumCompleted.GetValue() == Data->Num); Data->bExited = true; // DoneEvent waits here if some other thread finishes the last item // Data must live on until all of the tasks are cleared which might be long after this function exits }
浙公网安备 33010602011771号