概述

RDG(Rendering Dependency Graph),渲染依赖图,是UE4.22开始引进的全新的渲染子系统,是基于有向无环图(Directed Acyclic Graph,DAG)的调度系统,用于执行渲染管线的整帧优化。

传统的图形API(DirectX 11、OpenGL)要求驱动器调用复杂的启发法,以确定何时以及如何在GPU上执行关键的调度操作。例如清空缓存,管理和再使用内存,执行布局转换等等。由于接口存在即时模式特性,因此需要复杂的记录和状态跟踪才能处理各种极端情况。这些情况最终会对性能产生负面影响,阻碍并行。
现代的图形API(DirectX 12、Vulkan和Metal 2)与传统图形API不同,将低级GPU管理的负担转移到应用程序。这使得应用程序可以利用渲染管线的高级情境来驱动调度,从而提高性能并且简化渲染堆栈。

RDG的理念是,不在GPU上立即执行Pass,而是先收集所有需要渲染的Pass,并构建Pass之间的依赖关系,然后在编译中并对进行各类裁剪和优化,最后统一执行。

依赖性图表数据结构的整帧认知与现代图形API的能力相结合,使RDG能够在后台执行复杂的调度任务,RDG包括以下功能:

  • 安排异步计算栅栏的执行
  • 以最佳生命周期和内存别名分配临时资源
  • 使用拆分屏障转换子资源,在GPU上隐藏延迟并改善重叠
  • 命令列表并行记录
  • 剔除图中未使用的资源和通道
  • 验证API的使用和资源依赖关系
  • 在RDG Insights中实现图结构和内存生命周期的可视化

此外,RDG利用依赖性图表在通道设置期间提供丰富的验证,对影响功能和性能的问题进行自动捕捉,从而改进开发流程。

RDG机制

基本概念

RDG基础类型和接口主要集中于RenderGraphUtils.h和RenderGraphDefinitions.h之中,其中包括RDG Pass类型、Buffer标记、纹理标记、UAV标记、RDG对象分配器等等。

RDG资源

RDG资源并不是直接用RHI资源,而是包裹了RHI资源引用,然后针对不同类型的资源各自封装,且增加了额外的信息。
RDG系统对基本上所有的RHI资源进行了封装和包裹,以便进一步控制、管理RHI资源,精准控制它们的生命周期、引用关系及调试信息等,进一步优化、裁剪,提升渲染性能。

RDG Pass

Pass是RDG体系中最核心的类型之一,涉及了消费者、生产者、转换依赖、各类资源状态等等数据和处理。RDG的Pass有以下几种类型:

RDGPass

RDG Pass和渲染Pass并非一一对应关系,有可能多个合并成一个渲染Pass。

RDGBuilder

RDGBuilder是RDG体系的核心,负责收集渲染Pass和参数,编译Pass、数据,处理资源依赖,裁剪和优化各类数据,还有提供执行接口。作为RDG系统的驱动器,FRDGBuilder负责存储数据、处理状态转换、自动管理资源生命周期和屏障、裁剪无效资源,以及收集、编译、执行Pass,提取纹理或缓冲等等功能。

AddPass

AddPass会根据传入的参数构建一个RDG Pass的实例,然后设置该Pass的纹理和缓冲区数据,接着用内部设置Pass的依赖Pass等句柄,如果是立即模式,会重定向纹理和缓冲区的Merge状态成Pass状态,并且直接执行。

// Engine\Source\Runtime\RenderCore\Public\RenderGraphBuilder.inl

template <typename ParameterStructType, typename ExecuteLambdaType>
FRDGPassRef FRDGBuilder::AddPass(
    FRDGEventName&& Name,
    const ParameterStructType* ParameterStruct,
    ERDGPassFlags Flags,
    ExecuteLambdaType&& ExecuteLambda)
{
#if !USE_NULL_RHI
    return AddPassInternal(Forward<FRDGEventName>(Name), ParameterStructType::FTypeInfo::GetStructMetadata(), ParameterStruct, Flags, Forward<ExecuteLambdaType>(ExecuteLambda));
#else
    checkNoEntry();
    return nullptr;
#endif // !USE_NULL_RHI
}

template <typename ParameterStructType, typename ExecuteLambdaType>
FRDGPassRef FRDGBuilder::AddPassInternal(
    FRDGEventName&& Name,
    const FShaderParametersMetadata* ParametersMetadata,
    const ParameterStructType* ParameterStruct,
    ERDGPassFlags Flags,
    ExecuteLambdaType&& ExecuteLambda)
{
    using LambdaPassType = TRDGLambdaPass<ParameterStructType, ExecuteLambdaType>;
    (...)

    // 分配RDG Pass实例
    FRDGPass* Pass = Allocators.Root.AllocNoDestruct<LambdaPassType>(
        MoveTemp(Name),
        ParametersMetadata,
        ParameterStruct,
        OverridePassFlags(Name.GetTCHAR(), Flags),
        MoveTemp(ExecuteLambda));

    IF_RDG_ENABLE_DEBUG(ClobberPassOutputs(Pass));
    // 加入Pass Registry
    Passes.Insert(Pass);
    // 设置Pass
    SetupParameterPass(Pass);
    return Pass;
}

SetupParameterPass

// Engine\Source\Runtime\RenderCore\Public\RenderGraphBuilder.inl

FRDGPass* FRDGBuilder::SetupParameterPass(FRDGPass* Pass)
{
    (...)
    // 获取Pass数据,
    SetupPassInternals(Pass);

    // ----处理纹理状态、缓冲区状态----
    if (ParallelSetup.bEnabled)
    {
        MarkResourcesAsProduced(Pass);
        AsyncSetupQueue.Push(FAsyncSetupOp::SetupPassResources(Pass));
    }
    else
    {
        SetupPassResources(Pass);
    }

    // 如果为立即模式,则重定向纹理和缓冲区的Merge状态为Pass状态,并直接执行
    SetupAuxiliaryPasses(Pass);
    return Pass;
}

// Engine\Source\Runtime\RenderCore\Private\RenderGraphBuilder.cpp

void FRDGBuilder::SetupPassResources(FRDGPass* Pass)
{
    // 设置Pass的资源
    (...)

    if (ParallelSetup.bEnabled)
    {
        // 设置依赖关系
        SetupPassDependencies(Pass);

        for (FRDGPass::FExternalAccessOp Op : Pass->ExternalAccessOps)
        {
            Op.Resource->AccessModeState.ActiveMode = Op.Mode;
        }
    }
}

void FRDGBuilder::SetupPassDependencies(FRDGPass* Pass)
{
    bool bIsCullRootProducer = false;

    // 遍历当前Pass的所有贴图
    for (auto& PassState : Pass->TextureStates)
    {
        FRDGTextureRef Texture = PassState.Texture;
        auto& LastProducers = Texture->LastProducers;

        Texture->ReferenceCount += PassState.ReferenceCount;

        // 遍历当前贴图的所有Producer
        for (uint32 Index = 0, Count = LastProducers.Num(); Index < Count; ++Index)
        {
            const FRDGSubresourceState* SubresourceState = PassState.State[Index];

            if (!SubresourceState)
            {
                continue;
            }

            FRDGProducerState ProducerState;
            ProducerState.Pass = Pass;
            ProducerState.Access = SubresourceState->Access;
            ProducerState.NoUAVBarrierHandle = SubresourceState->NoUAVBarrierFilter.GetUniqueHandle();

            // 添加依赖
            bIsCullRootProducer |= AddCullingDependency(LastProducers[Index], ProducerState, Pass->Pipeline) && Texture->IsCullRoot();
        }
    }

    // 遍历当前Pass的所有BufferState
    for (auto& PassState : Pass->BufferStates)
    {
        FRDGBufferRef Buffer = PassState.Buffer;
        const FRDGSubresourceState& SubresourceState = PassState.State;

        Buffer->ReferenceCount += PassState.ReferenceCount;

        FRDGProducerState ProducerState;
        ProducerState.Pass = Pass;
        ProducerState.Access = SubresourceState.Access;
        ProducerState.NoUAVBarrierHandle = SubresourceState.NoUAVBarrierFilter.GetUniqueHandle();

        // 添加依赖
        bIsCullRootProducer |= AddCullingDependency(Buffer->LastProducer, ProducerState, Pass->Pipeline) && Buffer->IsCullRoot();
    }

    // 依赖剔除
    const bool bCullPasses = GRDGCullPasses > 0;
    Pass->bCulled = bCullPasses;

    if (bCullPasses && (bIsCullRootProducer || Pass->bHasExternalOutputs || EnumHasAnyFlags(Pass->Flags, ERDGPassFlags::NeverCull)))
    {
        CullPassStack.Emplace(Pass);

        FlushCullStack();
    }
}

Execute

// Engine\Source\Runtime\RenderCore\Private\RenderGraphBuilder.cpp

void FRDGBuilder::Execute()
{
    (...)

    // 在编译之前,在图的末尾创建epilogue pass
    SetupEmptyPass(EpiloguePass = Passes.Allocate<FRDGSentinelPass>(Allocators.Root, RDG_EVENT_NAME("Graph Epilogue")));

    const FRDGPassHandle ProloguePassHandle = GetProloguePassHandle();
    const FRDGPassHandle EpiloguePassHandle = GetEpiloguePassHandle();

    (...)

    // 非立即模式
    if (!IsImmediateMode())
    {
        SubmitParallelSetupTasks();
        BeginFlushResourcesRHI();
        WaitForParallelSetupTasks();

        (...)

        Compile();

        (...)
    }
    else
    {
        SubmitBufferUploads(RHICmdList);
        FinalizeResources();
    }

    SubmitParallelSetupTasks();
    EndFlushResourcesRHI();
    WaitForParallelSetupTasks();

    (...)


    // 执行Pass
    if (!IsImmediateMode())
    {
        SCOPED_NAMED_EVENT_TEXT("FRDGBuilder::ExecutePasses", FColor::Magenta);
        SCOPE_CYCLE_COUNTER(STAT_RDG_ExecuteTime);
        CSV_SCOPED_TIMING_STAT_EXCLUSIVE(RDG_Execute);

        for (FRDGPassHandle PassHandle = ProloguePassHandle; PassHandle <= EpiloguePassHandle; ++PassHandle)
        {
            (...)

            ExecutePass(Pass, RHICmdList);
        }
    }
    else
    {
        ExecutePass(EpiloguePass, RHICmdList);
    }

    RHICmdList.SetStaticUniformBuffers({});

    (...)

    // 执行纹理提取和缓冲区提取
    for (const FExtractedTexture& ExtractedTexture : ExtractedTextures)
    {
        check(ExtractedTexture.Texture->RenderTarget);
        *ExtractedTexture.PooledTexture = ExtractedTexture.Texture->RenderTarget;
    }
    for (const FExtractedBuffer& ExtractedBuffer : ExtractedBuffers)
    {
        check(ExtractedBuffer.Buffer->PooledBuffer);
        *ExtractedBuffer.PooledBuffer = ExtractedBuffer.Buffer->PooledBuffer;
    }

    (...)
}

void FRDGBuilder::ExecutePass(FRDGPass* Pass, FRHIComputeCommandList& RHICmdListPass)
{
    {
        SCOPED_GPU_MASK(RHICmdListPass, Pass->GPUMask);

        {
            FRHICommandListScopedPipeline Scope(RHICmdListPass, Pass->Pipeline);

#if 0 // Disabled by default to reduce memory usage in Insights.
            SCOPED_NAMED_EVENT_TCHAR(Pass->GetName(), FColor::Magenta);
#endif

#if RDG_CPU_SCOPES
            if (!Pass->bParallelExecute)
            {
                Pass->CPUScopeOps.Execute();
            }
#endif

            IF_RDG_ENABLE_DEBUG(ConditionalDebugBreak(RDG_BREAKPOINT_PASS_EXECUTE, BuilderName.GetTCHAR(), Pass->GetName()));

            Pass->GPUScopeOpsPrologue.Execute(RHICmdListPass);

            // 执行pass的顺序: 1.prologue -> 2.pass主体 -> 3.epilogue
            // 整个过程使用指定Pass上的命令列表执行
            ExecutePassPrologue(RHICmdListPass, Pass);

#if RDG_DUMP_RESOURCES_AT_EACH_DRAW
            BeginPassDump(Pass);
#endif

            Pass->Execute(RHICmdListPass);

#if RDG_DUMP_RESOURCES_AT_EACH_DRAW
            EndPassDump(Pass);
#endif

            ExecutePassEpilogue(RHICmdListPass, Pass);

            Pass->GPUScopeOpsEpilogue.Execute(RHICmdListPass);
        }
    }

    // 异步计算完成, 则立即派发
    if (!Pass->bParallelExecute && Pass->bDispatchAfterExecute)
    {
        if (Pass->Pipeline == ERHIPipeline::Graphics)
        {
            RHICmdList.ImmediateFlush(EImmediateFlushType::DispatchToRHIThread);
        }
    }

    // 如果是调试模式且非异步计算,则提交命令并刷新到GPU, 然后等待GPU处理完成
    if (GRDGDebugFlushGPU)
    {
        check(!GRDGAsyncCompute && !ParallelExecute.bEnabled);
        RHICmdList.SubmitCommandsAndFlushGPU();
        RHICmdList.BlockUntilGPUIdle();
    }
}

总结

RDG是在RHI之上的又一层封装,以实现RHI命令延迟执行。其将Pipeline分为设置阶段与执行阶段,在设置阶段收集Lambda、记录各个Pass的资源,并建立依赖关系,进行Pass剔除、合并具有相同RenderTarget的Pass等操作,最终将统一执行Lambda。

FRDGBuilder的执行周期可划分为4个阶段:收集Pass、编译Pass、执行Pass、清理

收集Pass阶段:主要是收集渲染模块的所有能够产生RHICommand的Pass(Lambda),非立即模式下收集之后不立即执行,将其延迟执行。AddPass的步骤是先创建FRDGPass的实例,并加入到Pass列表,随后执行SetupPass。SetupPass中主要处理纹理和缓冲区的状态、引用、依赖和标记等。

编译Pass阶段:主要包含构建生产者和消费者的依赖关系,确定Pass的裁剪等各类标记,调整资源的生命周期,裁剪Pass,处理Pass的资源转换和屏障,处理异步计算Pass的依赖和引用关系,查找并建立分叉和合并Pass节点,合并所有具体相同渲染目标的光栅化Pass等步骤。

执行Pass阶段:首先会执行编译,再根据编译结果执行所有符合条件的Pass。执行单个Pass时依次执行前序、主体和后续,相当于执行命令队列的BeginRenderPass、执行Pass主体(Lambda)渲染代码、EndRenderPass。执行Pass主体时过程简洁,就是调用该Pass的Lambda实例。

清理阶段:将清理或重置FRDGBuilder实例内的所有数据和内存。

Reference

Unreal Engine Documentation: Render Dependency Graph

剖析虚幻渲染体系(11)- RDG