说明

本文主要根据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-渲染并行化

Reference

UE4之TaskGraph系统

剖析虚幻渲染体系(02)- 多线程渲染

剖析虚幻渲染体系(03)- 渲染机制