说明
本文主要根据UE5-MeshDrawingPipeline中分析的渲染过程,依次分析TaskGraph在渲染线程中的应用
关于TaskGraph的分析参考,请移步UE5-TaskGraph系统-上:基本类型和UE5-TaskGraph系统-下:机制分析
ENQUEUE_RENDER_COMMAND
ENQUEUE_RENDER_COMMAND 宏的作用是由游戏线程向渲染线程入队命令,将宏展开可以看见其在异步 Enqueue 情况下的调用过程:
FRenderCommandPipe::Enqueue --> FRenderCommandPipe::EnqueueUniqueRenderCommand
若开启了独立的渲染线程,则 RenderCommand 不会立即执行而需要先入队至渲染线程,此时需要使用 TaskGraph 系统创建并入队任务,以在合适的时机执行。
同步入队是每次使用 ENQUEUE_RENDER_COMMAND 宏都会为当前命令创建一个入队至渲染线程的任务,而异步入队则是先收集命令,并在一定时机创建任务将其统一入队,但总之都需要使用 TaskGraph 进行入队任务的分发。
对 ENQUEUE_RENDER_COMMAND 中任务创建与入队的分析详见UE5-渲染并行化
收集MeshBatch
StaticMeshBatch
FPrimitiveSceneInfo::AddStaticMeshes 中完成了静态 MeshBatch 的收集
其中使用 ParallelForTemplate 函数启动任务,异步地将收集到的 FStaticMeshBatch 添加到 SceneInfo->StaticMeshes 中
// Engine\Source\Runtime\Renderer\Private\PrimitiveSceneInfo.cpp
void FPrimitiveSceneInfo::AddStaticMeshes(FRHICommandListBase& RHICmdList, FScene* Scene, TArrayView<FPrimitiveSceneInfo*> SceneInfos, bool bCacheMeshDrawCommands)
{
LLM_SCOPE(ELLMTag::StaticMesh);
{
ParallelForTemplate(SceneInfos.Num(), [Scene, &SceneInfos](int32 Index)
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelRenderingThread);
SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_AddStaticMeshes_DrawStaticElements, FColor::Magenta);
FPrimitiveSceneInfo* SceneInfo = SceneInfos[Index];
// Cache the primitive's static mesh elements.
FBatchingSPDI BatchingSPDI(SceneInfo);
BatchingSPDI.SetHitProxy(SceneInfo->DefaultDynamicHitProxy);
SceneInfo->Proxy->DrawStaticElements(&BatchingSPDI);
SceneInfo->StaticMeshes.Shrink();
SceneInfo->StaticMeshRelevances.Shrink();
SceneInfo->bPendingAddStaticMeshes = false;
check(SceneInfo->StaticMeshRelevances.Num() == SceneInfo->StaticMeshes.Num());
});
}
(...)
if (bCacheMeshDrawCommands)
{
CacheMeshDrawCommands(Scene, SceneInfos);
CacheNaniteMaterialBins(Scene, SceneInfos);
#if RHI_RAYTRACING
CacheRayTracingPrimitives(Scene, SceneInfos);
#endif
}
}
// Engine\Source\Runtime\Core\Public\Async\ParallelFor.h
template<typename BodyType, typename PreWorkType, typename ContextType>
inline void ParallelForInternal(const TCHAR* DebugName, int32 Num, int32 MinBatchSize, BodyType Body, PreWorkType CurrentThreadWorkToDoBeforeHelping, EParallelForFlags Flags, const TArrayView<ContextType>& Contexts)
{
(...)
//launch all the worker tasks
FEventRef FinishedSignal { EEventMode::ManualReset };
FDataHandle Data = new FParallelForData(DebugName, Num, BatchSize, NumBatches, NumWorkers, Contexts, Body, FinishedSignal, Priority);
// Launch the first worker before we start doing prework
FParallelExecutor::LaunchAnotherWorkerIfNeeded(Data);
// do the prework
CurrentThreadWorkToDoBeforeHelping();
// help with the parallel-for to prevent deadlocks
FParallelExecutor LocalExecutor(MoveTemp(Data), NumWorkers);
const bool bFinishedLast = LocalExecutor(true);
if (!bFinishedLast)
{
const bool bPumpRenderingThread = (Flags & EParallelForFlags::PumpRenderingThread) != EParallelForFlags::None;
if (bPumpRenderingThread && IsInActualRenderingThread())
{
// FinishedSignal 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
while (!FinishedSignal->Wait(1))
{
FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GetRenderThread_Local());
}
}
else
{
// FinishedSignal 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
TRACE_CPUPROFILER_EVENT_SCOPE(ParallelFor.Wait);
FinishedSignal->Wait();
}
}
checkSlow(LocalExecutor.GetData()->BatchItem.load(std::memory_order_relaxed) * LocalExecutor.GetData()->BatchSize >= LocalExecutor.GetData()->Num);
}
DynamicMeshBatch
收集动态 MeshBatch 的过程有如下调用:
FVisibilityTaskData::ProcessRenderThreadTasks --> FVisibilityTaskData::GatherDynamicMeshElements
FVisibilityTaskData::GatherDynamicMeshElements 中为收集动态 MeshBatch 创建任务并分发入队
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp
void FVisibilityTaskData::GatherDynamicMeshElements(FDynamicPrimitiveIndexList&& DynamicPrimitiveIndexList)
{
FDynamicPrimitiveIndexList RenderThreadDynamicPrimitiveIndexList;
const int32 NumAsyncContexts = DynamicMeshElements.ContextContainer.GetNumAsyncContexts();
if (NumAsyncContexts > 0)
{
const auto FilterDynamicPrimitives = [] (TArrayView<FPrimitiveSceneProxy*> PrimitiveSceneProxies, FDynamicPrimitiveIndexList::FList& Primitives, FDynamicPrimitiveIndexList::FList& RenderThreadPrimitives)
{
for (int32 Index = 0; Index < Primitives.Num(); )
{
const FDynamicPrimitiveIndex PrimitiveIndex = Primitives[Index];
if (!PrimitiveSceneProxies[PrimitiveIndex.Index]->SupportsParallelGDME())
{
RenderThreadPrimitives.Emplace(PrimitiveIndex);
Primitives.RemoveAtSwap(Index, 1, EAllowShrinking::No);
}
else
{
Index++;
}
}
};
FilterDynamicPrimitives(Scene.PrimitiveSceneProxies, DynamicPrimitiveIndexList.Primitives, RenderThreadDynamicPrimitiveIndexList.Primitives);
#if WITH_EDITOR
FilterDynamicPrimitives(Scene.PrimitiveSceneProxies, DynamicPrimitiveIndexList.EditorPrimitives, RenderThreadDynamicPrimitiveIndexList.EditorPrimitives);
#endif
}
else
{
RenderThreadDynamicPrimitiveIndexList = MoveTemp(DynamicPrimitiveIndexList);
DynamicPrimitiveIndexList = {};
}
if (!RenderThreadDynamicPrimitiveIndexList.IsEmpty())
{
Tasks.DynamicMeshElementsRenderThread = DynamicMeshElements.ContextContainer.LaunchRenderThreadTask(MoveTemp(RenderThreadDynamicPrimitiveIndexList));
}
if (!DynamicPrimitiveIndexList.IsEmpty())
{
FDynamicPrimitiveIndexQueue* Queue = Allocator.Create<FDynamicPrimitiveIndexQueue>(MoveTemp(DynamicPrimitiveIndexList));
for (int32 Index = 0; Index < DynamicMeshElements.ContextContainer.GetNumAsyncContexts(); ++Index)
{
Tasks.DynamicMeshElements.AddPrerequisites(DynamicMeshElements.ContextContainer.LaunchAsyncTask(Queue, Index, TaskConfig.TaskPriority));
}
}
}
FVisibilityTaskData::GatherDynamicMeshElements 中首先对动态 Primitives 进行过滤,将支持异步处理和不支持异步处理的 Primitives 分开存储。对于不支持异步处理的 Primitives,在渲染线程上创建并分发收集 DynamicMeshElements 的任务;而对于支持异步处理的 Primitives,则根据AsyncContexts 数量启动相应的异步收集 DynamicMeshElements 的任务,同时将它们都作为当前 TaskEvent 事件的前序任务,以实现线程同步。
在渲染线程处理:
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp
FGraphEventRef FDynamicMeshElementContextContainer::LaunchRenderThreadTask(FDynamicPrimitiveIndexList&& PrimitiveIndexList)
{
return Contexts.Last()->LaunchRenderThreadTask(MoveTemp(PrimitiveIndexList));
}
FGraphEventRef FDynamicMeshElementContext::LaunchRenderThreadTask(FDynamicPrimitiveIndexList&& PrimitiveIndexList)
{
return FFunctionGraphTask::CreateAndDispatchWhenReady([this, PrimitiveIndexList = MoveTemp(PrimitiveIndexList)]
{
for (FDynamicPrimitiveIndex PrimitiveIndex : PrimitiveIndexList.Primitives)
{
GatherDynamicMeshElementsForPrimitive(Primitives[PrimitiveIndex.Index], PrimitiveIndex.ViewMask);
}
(...)
}, TStatId{}, nullptr, ENamedThreads::GetRenderThread_Local());
}
static FGraphEventRef FFunctionGraphTask::CreateAndDispatchWhenReady(TUniqueFunction<void()> InFunction, TStatId InStatId = TStatId{}, const FGraphEventArray* InPrerequisites = nullptr, ENamedThreads::Type InDesiredThread = ENamedThreads::AnyThread)
{
return TGraphTask<TFunctionGraphTaskImpl<void(), ESubsequentsMode::TrackSubsequents>>::CreateTask(InPrerequisites).ConstructAndDispatchWhenReady(MoveTemp(InFunction), InStatId, InDesiredThread);
}
异步处理:
// Engine\Source\Runtime\Renderer\Private\SceneVisibility.cpp
UE::Tasks::FTask FDynamicMeshElementContextContainer::LaunchAsyncTask(FDynamicPrimitiveIndexQueue* PrimitiveIndexQueue, int32 Index, UE::Tasks::ETaskPriority TaskPriority)
{
return Contexts[Index]->LaunchAsyncTask(PrimitiveIndexQueue, TaskPriority);
}
UE::Tasks::FTask FDynamicMeshElementContext::LaunchAsyncTask(FDynamicPrimitiveIndexQueue* PrimitiveIndexQueue, UE::Tasks::ETaskPriority TaskPriority)
{
return Pipe.Launch(UE_SOURCE_LOCATION, [this, PrimitiveIndexQueue]
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelRenderingThread);
FDynamicPrimitiveIndex PrimitiveIndex;
while (PrimitiveIndexQueue->Pop(PrimitiveIndex))
{
GatherDynamicMeshElementsForPrimitive(Primitives[PrimitiveIndex.Index], PrimitiveIndex.ViewMask);
}
(...)
}, TaskPriority);
}
// Engine\Source\Runtime\Core\Public\Tasks\Pipe.h
template<typename TaskBodyType>
TTask<TInvokeResult_T<TaskBodyType>> FPipe::Launch(
const TCHAR* InDebugName,
TaskBodyType&& TaskBody,
ETaskPriority Priority = ETaskPriority::Default,
EExtendedTaskPriority ExtendedPriority = EExtendedTaskPriority::None,
ETaskFlags Flags = ETaskFlags::None)
{
using FResult = TInvokeResult_T<TaskBodyType>;
using FExecutableTask = Private::TExecutableTask<std::decay_t<TaskBodyType>>;
FExecutableTask* Task = FExecutableTask::Create(InDebugName, Forward<TaskBodyType>(TaskBody), Priority, ExtendedPriority, Flags);
Task->SetPipe(*this);
Task->TryLaunch(sizeof(*Task));
return TTask<FResult>{ Task };
}
总结:FVisibilityTaskData::GatherDynamicMeshElements 函数首先检查是否有异步上下文可用。如果有,则过滤不支持异步处理的网格元素,并其放在单独的列表中,这些列表将被直接提交到渲染线程上创建任务处理,而剩余的网格元素将被异步处理。如果没有异步上下文,所有网格元素都将直接提交到渲染线程上处理。
收集MeshDrawCommand
StaticMeshDrawCommand
FPrimitiveSceneInfo::CacheMeshDrawCommands 函数中完成静态 MeshDrawCommand 的收集
在该函数中,首先定义 DoWorkLamda 用于线程回调,在其中定义了为各 MeshPass 创建 MeshPassProcessor 并分发 MeshDrawCommand 的操作
在函数最后判断是否开启多线程,若开启多线程,则使用 ParallelForTemplate 函数启动多线程任务并使用上述回调函数,异步地收集静态 MeshDrawCommand
// Engine\Source\Runtime\Renderer\Private\PrimitiveSceneInfo.cpp
void FPrimitiveSceneInfo::CacheMeshDrawCommands(FScene* Scene, TArrayView<FPrimitiveSceneInfo*> SceneInfos)
{
SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_CacheMeshDrawCommands, FColor::Emerald);
CSV_SCOPED_TIMING_STAT_EXCLUSIVE(FPrimitiveSceneInfo_CacheMeshDrawCommands);
QUICK_SCOPE_CYCLE_COUNTER(STAT_CacheMeshDrawCommands);
// 计数并行的线程数量
const int BATCH_SIZE = WITH_EDITOR ? 1 : GMeshDrawCommandsBatchSize;
const int NumBatches = (SceneInfos.Num() + BATCH_SIZE - 1) / BATCH_SIZE;
// 线程回调
auto DoWorkLambda = [Scene, SceneInfos, BATCH_SIZE](FCachedPassMeshDrawListContext& DrawListContext, int32 Index)
{
SCOPED_NAMED_EVENT(FPrimitiveSceneInfo_CacheMeshDrawCommand, FColor::Green);
struct FMeshInfoAndIndex
{
int32 InfoIndex;
int32 MeshIndex;
};
TArray<FMeshInfoAndIndex, SceneRenderingAllocator> MeshBatches;
MeshBatches.Reserve(3 * BATCH_SIZE);
// 遍历当前线程的范围,逐个处理PrimitiveSceneInfo
int LocalNum = FMath::Min((Index * BATCH_SIZE) + BATCH_SIZE, SceneInfos.Num());
for (int LocalIndex = (Index * BATCH_SIZE); LocalIndex < LocalNum; LocalIndex++)
{
FPrimitiveSceneInfo* SceneInfo = SceneInfos[LocalIndex];
check(SceneInfo->StaticMeshCommandInfos.Num() == 0);
SceneInfo->StaticMeshCommandInfos.AddDefaulted(EMeshPass::Num * SceneInfo->StaticMeshes.Num());
FPrimitiveSceneProxy* SceneProxy = SceneInfo->Proxy;
// 体积透明阴影需要每帧更新,不能缓存
if (!SceneProxy->CastsVolumetricTranslucentShadow())
{
// 将PrimitiveSceneInfo的所有静态网格添加到MeshBatch列表
for (int32 MeshIndex = 0; MeshIndex < SceneInfo->StaticMeshes.Num(); MeshIndex++)
{
FStaticMeshBatch& Mesh = SceneInfo->StaticMeshes[MeshIndex];
// 检测是否支持缓存MeshDrawCommand
if (SupportsCachingMeshDrawCommands(Mesh))
{
MeshBatches.Add(FMeshInfoAndIndex{ LocalIndex, MeshIndex });
}
}
}
}
// 遍历所有MeshPass,将每个静态元素生成的MeshDrawCommand添加到对应Pass的缓存列表中
for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
{
const EShadingPath ShadingPath = GetFeatureLevelShadingPath(Scene->GetFeatureLevel());
EMeshPass::Type PassType = (EMeshPass::Type)PassIndex;
if ((FPassProcessorManager::GetPassFlags(ShadingPath, PassType) & EMeshPassFlags::CachedMeshCommands) != EMeshPassFlags::None)
{
// 构建FCachedPassMeshDrawListContext
FCachedPassMeshDrawListContext::FMeshPassScope MeshPassScope(DrawListContext, PassType);
// 创建Pass的FMeshPassProcessor
FMeshPassProcessor* PassMeshProcessor = FPassProcessorManager::CreateMeshPassProcessor(ShadingPath, PassType, Scene->GetFeatureLevel(), Scene, nullptr, &DrawListContext);
if (PassMeshProcessor != nullptr)
{
for (const FMeshInfoAndIndex& MeshAndInfo : MeshBatches)
{
FPrimitiveSceneInfo* SceneInfo = SceneInfos[MeshAndInfo.InfoIndex];
FStaticMeshBatch& Mesh = SceneInfo->StaticMeshes[MeshAndInfo.MeshIndex];
FStaticMeshBatchRelevance& MeshRelevance = SceneInfo->StaticMeshRelevances[MeshAndInfo.MeshIndex];
check(!MeshRelevance.CommandInfosMask.Get(PassType));
uint64 BatchElementMask = ~0ull;
// 添加MeshBatch到PassMeshProcessor,内部会将FMeshBatch转换到FMeshDrawCommand
PassMeshProcessor->AddMeshBatch(Mesh, BatchElementMask, SceneInfo->Proxy);
FCachedMeshDrawCommandInfo CommandInfo = DrawListContext.GetCommandInfoAndReset();
if (CommandInfo.CommandIndex != -1 || CommandInfo.StateBucketId != -1)
{
static_assert(sizeof(MeshRelevance.CommandInfosMask) * 8 >= EMeshPass::Num, "CommandInfosMask is too small to contain all mesh passes.");
MeshRelevance.CommandInfosMask.Set(PassType);
MeshRelevance.CommandInfosBase++;
int CommandInfoIndex = MeshAndInfo.MeshIndex * EMeshPass::Num + PassType;
// 将CommandInfo缓存到PrimitiveSceneInfo中
FCachedMeshDrawCommandInfo& CurrentCommandInfo = SceneInfo->StaticMeshCommandInfos[CommandInfoIndex];
checkf(CurrentCommandInfo.MeshPass == EMeshPass::Num,
TEXT("SceneInfo->StaticMeshCommandInfos[%d] is not expected to be initialized yet. MeshPass is %d, but expected EMeshPass::Num (%d)."),
CommandInfoIndex, (int32)EMeshPass::Num, CurrentCommandInfo.MeshPass);
CurrentCommandInfo = CommandInfo;
}
}
// 销毁FMeshPassProcessor
delete PassMeshProcessor;
}
}
}
(...)
};
// 并行
bool bAnyLooseParameterBuffers = false;
if (GMeshDrawCommandsCacheMultithreaded && FApp::ShouldUseThreadingForPerformance())
{
(...)
ParallelForTemplate(
NumBatches,
[&DrawListContexts, &DoWorkLambda](int32 Index)
{
FOptionalTaskTagScope Scope(ETaskTag::EParallelRenderingThread);
DoWorkLambda(DrawListContexts[Index], Index);
},
EParallelForFlags::Unbalanced
);
(...)
}
// 单线程
else
{
(...)
}
(...)
}
DynamicMeshDrawCommand
在 FVisibilityTaskData::SetupMeshPasses 函数的最后调用 FSceneRenderer::SetupMeshPass,在其中为各个 MeshPass 创建相应的 MeshPassProcessor ,并调用 FParallelMeshDrawCommandPass::DispatchPassSetup 完成 MeshDrawCommand 的收集
// Engine\Source\Runtime\Renderer\Private\SceneRendering.cpp
void FSceneRenderer::SetupMeshPass(FViewInfo& View, FExclusiveDepthStencil::Type BasePassDepthStencilAccess, FViewCommands& ViewCommands, FInstanceCullingManager& InstanceCullingManager)
{
SCOPE_CYCLE_COUNTER(STAT_SetupMeshPass);
const EShadingPath ShadingPath = GetFeatureLevelShadingPath(Scene->GetFeatureLevel());
for (int32 PassIndex = 0; PassIndex < EMeshPass::Num; PassIndex++)
{
const EMeshPass::Type PassType = (EMeshPass::Type)PassIndex;
if ((FPassProcessorManager::GetPassFlags(ShadingPath, PassType) & EMeshPassFlags::MainView) != EMeshPassFlags::None)
{
(...)
FMeshPassProcessor* MeshPassProcessor = FPassProcessorManager::CreateMeshPassProcessor(ShadingPath, PassType, Scene->GetFeatureLevel(), Scene, &View, nullptr);
FParallelMeshDrawCommandPass& Pass = View.ParallelMeshDrawCommandPasses[PassIndex];
(...)
FName PassName(GetMeshPassName(PassType));
Pass.DispatchPassSetup(
Scene,
View,
FInstanceCullingContext(PassName, ShaderPlatform, &InstanceCullingManager, ViewIds, View.PrevViewInfo.HZB, InstanceCullingMode, CullingFlags),
PassType,
BasePassDepthStencilAccess,
MeshPassProcessor,
View.DynamicMeshElements,
&View.DynamicMeshElementsPassRelevance,
View.NumVisibleDynamicMeshElements[PassType],
ViewCommands.DynamicMeshCommandBuildRequests[PassType],
ViewCommands.DynamicMeshCommandBuildFlags[PassType],
ViewCommands.NumDynamicMeshCommandBuildRequestElements[PassType],
ViewCommands.MeshCommands[PassIndex]);
}
}
}
其中的多线程任务分发是在 FParallelMeshDrawCommandPass::DispatchPassSetup 函数中
若开启了多线程,则会创建 FMeshDrawCommandPassSetupTask 并加入队列,在其执行时完成 MeshDrawCommand 的收集
// Engine\Source\Runtime\Renderer\Private\MeshDrawCommands.cpp
void FParallelMeshDrawCommandPass::DispatchPassSetup(
FScene* Scene,
const FViewInfo& View,
FInstanceCullingContext&& InstanceCullingContext,
EMeshPass::Type PassType,
FExclusiveDepthStencil::Type BasePassDepthStencilAccess,
FMeshPassProcessor* MeshPassProcessor,
const TArray<FMeshBatchAndRelevance, SceneRenderingAllocator>& DynamicMeshElements,
const TArray<FMeshPassMask, SceneRenderingAllocator>* DynamicMeshElementsPassRelevance,
int32 NumDynamicMeshElements,
TArray<const FStaticMeshBatch*, SceneRenderingAllocator>& InOutDynamicMeshCommandBuildRequests,
TArray<EMeshDrawCommandCullingPayloadFlags, SceneRenderingAllocator> InOutDynamicMeshCommandBuildFlags,
int32 NumDynamicMeshCommandBuildRequestElements,
FMeshCommandOneFrameArray& InOutMeshDrawCommands,
FMeshPassProcessor* MobileBasePassCSMMeshPassProcessor,
FMeshCommandOneFrameArray* InOutMobileBasePassCSMMeshDrawCommands
)
{
(...)
if (MaxNumDraws > 0)
{
(...)
const bool bExecuteInParallel = FApp::ShouldUseThreadingForPerformance()
&& CVarMeshDrawCommandsParallelPassSetup.GetValueOnRenderThread() > 0
&& GIsThreadedRendering; // Rendering thread is required to safely use rendering resources in parallel.
if (bExecuteInParallel)
{
if (IsOnDemandShaderCreationEnabled())
{
TaskEventRef = TGraphTask<FMeshDrawCommandPassSetupTask>::CreateTask().ConstructAndDispatchWhenReady(TaskContext);
}
else
{
FGraphEventArray DependentGraphEvents;
DependentGraphEvents.Add(TGraphTask<FMeshDrawCommandPassSetupTask>::CreateTask().ConstructAndDispatchWhenReady(TaskContext));
TaskEventRef = TGraphTask<FMeshDrawCommandInitResourcesTask>::CreateTask(&DependentGraphEvents).ConstructAndDispatchWhenReady(TaskContext);
}
}
else
{
QUICK_SCOPE_CYCLE_COUNTER(STAT_MeshPassSetupImmediate);
FMeshDrawCommandPassSetupTask Task(TaskContext);
Task.AnyThreadTask();
if (!IsOnDemandShaderCreationEnabled())
{
FMeshDrawCommandInitResourcesTask DependentTask(TaskContext);
DependentTask.AnyThreadTask();
}
}
(...)
}
}
RHICommand转译
MeshDrawCommand 会先由 RDG 进行收集,在 RDG Render Pass 执行时将 MeshDrawCommand 转译成 RHICommand,而其中 CommandList 的转译以及转译所得 RHIPlatformCommandList 的提交可以进行异步处理。详见UE5-渲染并行化