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 / _UAV | RDG 资源,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_BUFFER | RDG 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_SCOPE | GPU profile scope,被 RenderDoc 与 RDG Insights 消费 |
RDG_GPU_STAT_SCOPE | 给 stat gpu 加 scope |
RDG_CSV_STAT_EXCLUSIVE_SCOPE | CSV 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 |
用了 FRHICommandListImmediate | pass 无法并行,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 写入)