RDG 渲染图

自动关联目录:RDG 渲染图

官方依据:Render Dependency Graph(UE 5.8 Documentation)。本页覆盖该页全部 36 个条目,并按本库标准重排为"模型 → 写法 → 参数 → 代价 → 排查"。

1. 定位

RDG(Render Dependency Graph / Render Graph)是一个立即模式 API:把渲染命令记录进一个图数据结构,之后再编译并执行。

一句话定位:RDG 把"手写渲染管线"里最容易出错的那几件事自动化了——资源生命周期、内存复用、屏障、并行录制、无用 pass 剔除。官方明确要求:所有高层渲染代码都应该用 RDG 写,延迟渲染器与移动端渲染器(及关联插件)都已转换。

它自动化的七件事:

能力说明
异步计算栅栏调度按图依赖自动插 fence
瞬态资源分配最优生命周期 + 内存别名(aliasing)
子资源状态转换用 split-barrier 隐藏延迟、提升 GPU 重叠
并行命令列表录制pass 可并行录制
剔除未被使用的资源与 pass 从图里剔除
校验API 用法与资源依赖的合法性
可视化RDG Insights

2. 核心模型:Setup / Execute 两条时间线

Setup 时间线(构建图)
  创建资源描述符、做管线分支、AddPass 登记 lambda
        │
        │  RDG 编译:剔除、排生命周期、插屏障与 fence
        ▼
Execute 时间线(执行)
  跨多个线程调用 pass lambda,把命令录进 RHI 命令列表

因此有一条铁律:pass lambda 里不能有副作用,只应往命令列表里记录命令——因为执行可能被并行化。

阶段能做什么
Setup创建资源(只分配描述符,不分配 GPU 内存)、分支配置
lambda 内只能通过 GetRHI() 访问真正的 RHI 资源,且必须是声明了该资源的 pass

GetRHI() 在任何其它地方调用都会触发校验层断言。

另一个硬约束:Graph Builder 的 API 是单线程的,同一时刻只能有一个实例——这排除了层级式或并列的图。延迟与移动端渲染器都是每次场景渲染用一个 builder。

3. 写法

参数结构体

BEGIN_SHADER_PARAMETER_STRUCT(FParameters, )
    SHADER_PARAMETER(FVector2D, HalfTexelSize)
    SHADER_PARAMETER(float, Level)
    SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, MipInSRV)
    SHADER_PARAMETER_SAMPLER(SamplerState, MipSampler)
    RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
宏族用途
SHADER_PARAMETER / _ARRAY / _TEXTURE / _SAMPLER / _SRV / _UAV普通着色器参数(RDG 会忽略)
SHADER_PARAMETER_RDG_TEXTURE / _SRV / _UAVRDG 资源,RDG 用它推导依赖与生命周期
RDG_TEXTURE_ACCESS(Name, ERHIAccess::X)无着色器语义的访问(CopySrc/CopyDest 等)
RENDER_TARGET_BINDING_SLOTS()光栅 pass 的 RT/深度绑定槽
SHADER_PARAMETER_STRUCT_REF把 uniform buffer 作为引用纳入
SHADER_PARAMETER_RDG_UNIFORM_BUFFERRDG uniform buffer(必须的声明方式,否则 lambda 里不能解引用)

赋 null 是允许的,RDG 会忽略——这是减少图复杂度的手段。

一个 pass

FMyCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FMyCS::FParameters>();
PassParameters->MipInSRV   = GraphBuilder.CreateSRV(FRDGTextureSRVDesc::CreateForMipLevel(Tex, Level-1));
PassParameters->MipOutUAV  = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(Tex, Level));

GraphBuilder.AddPass(
    RDG_EVENT_NAME("GenerateMips DestMipLevel=%d", Level),
    PassParameters,
    ERDGPassFlags::Compute,
    [PassParameters, ComputeShader, GroupCount](FRHIComputeCommandList& RHICmdList)
    {
        FComputeShaderUtils::Dispatch(RHICmdList, ComputeShader, *PassParameters, GroupCount);
    });
Pass 标志用途
ERDGPassFlags::Compute只发计算命令
ERDGPassFlags::Raster光栅
ERDGPassFlags::AsyncCompute异步计算(lambda 参数用 FRHIComputeCommandList)
ERDGPassFlags::Copy拷贝类

命令列表类型的选择有性能含义:

  • 计算 pass 用 FRHIComputeCommandList(异步与图形计算的共用接口)
  • 光栅 pass 用 FRHICommandList
  • 除非绝对必要,不要用 FRHICommandListImmediate——它会让该 pass 失去并行执行资格

光栅 pass 的 Load Action

动作含义
Load保留纹理现有内容
Clear清成优化过的清除值
NoAction不保证保留内容;在部分硬件上更快(前提是所有有效像素都会被写入)

Tile-based 硬件(移动端)上准确的 load action 直接决定性能。深度与模板各自单独指定,且需要 FExclusiveDepthStencil 控制读写权限。

内存生命周期(最常见的崩溃来源)

// 错!栈上对象被引用捕获,Execute 时已失效
FMyObject Object;
GraphBuilder.AddPass(..., [&Object](FRHICommandList&){ /* 悬垂 */ });

// 对:用 RDG 自带的线性分配器
FMyObject* Object = GraphBuilder.AllocObject<FMyObject>();   // 会调析构
FMyObject* Obj2   = GraphBuilder.AllocPOD<FMyObject>();      // POD,不调析构
API用途
GraphBuilder.AllocParameters<T>()pass 参数(额外带追踪)
GraphBuilder.AllocObject<T>()C++ 对象,会调析构
GraphBuilder.AllocPOD<T>()POD,不调析构
GraphBuilder.Alloc(Size, Align)原始内存

分配的内存存活到 builder 销毁,线性分配器,很快。

资源:Transient vs External

类型生命周期说明
Transient限制在图内可与生命周期不重叠的其它瞬态资源共享内存
External延伸到图外注册已有 RHI 资源,或执行后提取出来
// 提取:Execute 之后指针才被填充
TRefCountPtr<IPooledRenderTarget> ExtractedTexture;
GraphBuilder.QueueTextureExtraction(Texture, &ExtractedTexture);
GraphBuilder.Execute();
check(ExtractedTexture);

// 注册:下一个图里用
FRDGTexture* Tex2 = GraphBuilder.RegisterExternalTexture(ExtractedTexture);
  • 注册 → 生命周期延伸到图的开头(因为分配发生在 setup 阶段)
  • 提取 → 延伸到末尾
  • ConvertToExternal 是替代方案:立即分配并返回,但它意味着该资源无法与帧内任何其它资源共享底层分配

RDG 资源指针归 Graph Builder 所有,builder 销毁后失效;执行结束后应把这些指针置空。

4. 参数与控制台变量

CVar / 参数默认作用
r.RDG.AsyncCompute开RDG 是否启用异步计算。profile 时设 0 可得可信计时(见 虚拟阴影贴图)
r.RDG.TransientAllocator开瞬态分配器;排查 alias 问题时关掉对比
r.RDG.CullPasses开关掉可排除"pass 被剔除"这个可能
r.RDG.MergeRenderPasses开关掉则每个 RDG 光栅 pass 一个独立 render pass
r.RDG.ParallelExecute开关掉则所有 pass 在渲染线程串行
r.RDG.ImmediateMode / -rdgimmediate关立即模式:跳过图编译,在 AddPass 调用处直接执行
r.RHICmdBypass关禁用并行渲染与软件命令列表;与立即模式配合得到单一调用栈
r.RDG.ClobberResources—强制把所有资源初始化成已知值,用来暴露"读取前未初始化"
r.RDG.Debug.ExtendResourceLifetimes—禁用图内所有 aliasing
r.RDG.Debug.DisableTransientResources—让资源不走瞬态分配器
r.RDG.Debug.ResourceFilter—按资源名过滤调试输出
r.RDG.Debug.PassFilter—按 pass 名过滤
r.rdg.transitionlog X / -rdgtransitionlog—记录 X 帧内 RDG 的所有资源转换
-rhivalidation -rhivalidationlog=Name—RHI 侧的资源转换日志
-onethread / -norhithread -forcerhibypass—让 RDG 日志(渲染线程)与 RHI 日志(RHI 线程)对齐
-trace=rdg,defaults—抓 RDG Insights 轨迹

5. 性能剖析与可视化

宏给谁用
RDG_EVENT_SCOPEGPU profile scope,被 RenderDoc 与 RDG Insights 消费
RDG_GPU_STAT_SCOPE给 stat gpu 加 scope
RDG_CSV_STAT_EXCLUSIVE_SCOPECSV profiler

它们接受 builder 作为输入,能正确处理 setup / execute 两条时间线的归属。

RDG Insights 插件(Edit > Plugins > Insights)能看:资源生命周期、pass 关联、资源池分配重叠、异步计算 fence 与重叠、图剔除与 render pass 合并、并行执行区间、瞬态内存布局。

典型用法:回答"为什么异步计算没有和图形 pass 重叠""某个资源分配和谁重叠了""哪些 pass 被剔除了"。

注意:RDG 轨迹数据量很大。

命名约定(影响可查性)

  • 资源命名空间用点分隔:如 TSR.History.ScreenPercentage——这样在 RDG Insights 里过滤很方便
  • builder 实例命名 GraphBuilder
  • 着色器参数内联命名 FParameters
  • 用 RDG_EVENT_SCOPE 作为 pass 的命名空间
  • 优先用 RenderGraphUtils.h / ScreenPass.h 里的工具函数(FComputeShaderUtils::AddPass、FPixelShaderUtils::AddFullscreenPass)

6. 代价与权衡

设计收益代价什么时候不该用
延迟编译(图)自动剔除、别名、屏障、并行出错时调用栈离 setup 代码很远调试时开立即模式
瞬态分配器显著降低 GPU 内存水位线先前内容是垃圾——资源池时代被掩盖的问题会暴露依赖"资源上次内容"的代码会直接坏
并行执行CPU 侧省时间lambda 必须无副作用必须在渲染线程创建/锁定 RHI 资源的 pass 只能用 immediate
异步计算 flag与图形 pipe 重叠依赖平台支持;不支持则回退图形 pipe平台不支持时等于白标
单 builder 实例简单、可全局优化不能做层级或并列的图需要嵌套图的场景不成立
RDG uniform buffer可含 RDG 资源、可剔除不能 null 掉资源参数、着色器无法反射剔除未用参数 → 必须按参数组合手动建多份 UB参数组合爆炸时要权衡

7. 踩坑与排查

坑现象怎么验证
lambda 里捕获栈对象引用执行时悬垂崩溃用 AllocObject / AllocPOD
在 pass 外调 GetRHI()校验层断言移进声明了该资源的 pass lambda
用了 FRHICommandListImmediatepass 无法并行,CPU 变慢检查是否真的必须在渲染线程锁资源
忘了把资源放进参数结构体依赖没建立、生命周期错所有用到的 RDG 资源都要出现在参数里
依赖资源的"上次内容"开瞬态分配器后画面乱r.RDG.ClobberResources 复现;瞬态资源不保证初值
参数没被置 null图变复杂、生命周期被无谓延长ClearUnusedGraphResources(及多着色器版本)
pass 被剔除了效果没出现r.RDG.CullPasses=0 对比
崩溃栈看不出 setup 位置调试困难r.RDG.ImmediateMode=1 + r.RHICmdBypass
资源转换错误RHI 校验报错但栈都一样-rdgtransitionlog + -rhivalidation -rhivalidationlog=Name + -onethread 对齐
移动端慢Load Action 不精确能写满就用 NoAction
手动 lock/unlock 做上传引入同步点、破坏并行用 QueueBufferUpload(会自动标记为非瞬态)
上传数据活不到 Execute读到垃圾用 ERDGInitialDataFlags::NoCopy 只在数据生命周期足够时

排查顺序

1. 崩溃在 lambda 里 → r.RDG.ImmediateMode=1 + r.RHICmdBypass 拿单一调用栈
2. 画面错误 → 先 r.RDG.TransientAllocator=0 对比(alias / 未初始化)
             再 r.RDG.ClobberResources 验证"读取前未初始化"
3. pass 没跑 → r.RDG.CullPasses=0 对比
4. 性能 → RDG Insights 看并行区间与异步计算重叠(profile 时 r.RDG.AsyncCompute=0)
5. 资源状态问题 → 两条 transition log 对齐后比对
6. 看具体资源 → vis 命令(Development 构建下 RDG 会发布所有 UAV/RTV 写入)

参考