NOTE系列导航:00 总览 → 01 线程与 Scene Proxy → 02 可见性与 Mesh Drawing → 03 RDG → 04 Deferred Renderer 逐 Pass。版本基线为 UE 5.8。
先给结论
Render Dependency Graph(RDG)让 Renderer 先声明“要做什么、读写什么”,再决定“怎样分配资源、插入同步并执行”。它的主要价值不是缩短 AddPass 代码,而是让引擎看见完整依赖后能够:
- 裁掉结果无人消费的 Pass;
- 延后物理资源分配并计算精确生命周期;
- 让生命周期不重叠的瞬态资源复用显存;
- 自动生成大部分 RHI Resource Transition、UAV Barrier 与跨队列同步;
- 安排 Graphics 与 Async Compute 的合法重叠;
- 统一 GPU Event、统计、验证和调试信息。
RDG 不负责以下事情:
- 它不是 D3D12、Vulkan 或 Metal 的替代品;
- 它不会自动让一个昂贵 Shader 变快;
- 标记
AsyncCompute不保证硬件上出现重叠; - 参数表里没有声明的隐藏资源访问,RDG 无法可靠推导;
FRDGTextureRef不是能跨帧保存的纹理所有权。
RDG 在 RHI 上面做什么
一帧中可能有多个 Graph Builder 或嵌套在不同渲染工作流里的 Graph,不能把 RDG 理解成全引擎唯一的“全局帧图”。但对于一个 Builder,所有图内资源和 Pass 都必须遵守同一个构建—执行生命周期。
三个阶段:Setup、Compile、Execute
1. Setup:声明逻辑资源与 Pass
调用 CreateTexture 或 CreateBuffer 时,通常只是创建一个带 Descriptor 的逻辑资源节点;它不等于立即向 RHI 分配完整物理显存。
调用 AddPass 时:
- Pass Parameters 声明 SRV、UAV、Render Target、Depth Stencil 等访问;
- Pass Flags 声明 Raster、Compute、Async Compute、Copy 等队列语义;
- Execute Lambda 保存真正录制 RHI 工作的代码;
- GPU Event Name 为 Capture 和统计提供稳定标识。
Setup 代码立即在 CPU 上运行,但 Pass Lambda 通常稍后在 Graph Execute 阶段运行。
2. Compile:从依赖得到执行计划
Builder 掌握完整图后,能计算:
- 哪些 Pass 从最终输出向前可达;
- 每个资源第一次和最后一次使用;
- 哪些资源可进入 Transient Allocator;
- Pass 之间需要什么 Transition / Barrier;
- Graphics / Async Compute 之间在哪里 Signal / Wait;
- 哪些 Pass 有机会合并、并行准备或跳过。
3. Execute:获取资源并录制实际命令
执行时才按计划获取物理资源、运行未被裁剪的 Pass Lambda、录制 RHI 命令并在最后释放或提取资源。Immediate Mode 会改变这个过程以方便调试,不能用它的性能代表正常 RDG。
最小 Compute Pass:依赖藏在参数表里
BEGIN_SHADER_PARAMETER_STRUCT(FResearchPassParameters, ) SHADER_PARAMETER_RDG_TEXTURE_SRV(Texture2D, InputTexture) SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D<float4>, OutputTexture)END_SHADER_PARAMETER_STRUCT()
FRDGTextureDesc OutputDesc = FRDGTextureDesc::Create2D( Input->Desc.Extent, PF_FloatRGBA, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV);
FRDGTextureRef Output = GraphBuilder.CreateTexture( OutputDesc, TEXT("Research.Output"));
FResearchPassParameters* Parameters = GraphBuilder.AllocParameters<FResearchPassParameters>();
Parameters->InputTexture = GraphBuilder.CreateSRV(Input);Parameters->OutputTexture = GraphBuilder.CreateUAV(Output);
TShaderMapRef<FResearchCS> ComputeShader(View.ShaderMap);
FComputeShaderUtils::AddPass( GraphBuilder, RDG_EVENT_NAME("Research %dx%d", OutputDesc.Extent.X, OutputDesc.Extent.Y), ComputeShader, Parameters, FComputeShaderUtils::GetGroupCount(OutputDesc.Extent, FIntPoint(8, 8)));从这段参数表,RDG 可以知道:
Input:本 Pass 读取Output:本 Pass 写入→ Input 的生产者必须先完成→ Output 的消费者必须后开始→ 两者在执行时需要正确 Resource State示例省略了 Shader 类定义、Permutation 和平台支持检查。真正实现应复用当前 Renderer 中同类 Pass 的 Shader 参数与工具函数。
Pass Parameters 是正确性的契约
常用声明类型:
| 声明 | RDG 看到的语义 |
|---|---|
SHADER_PARAMETER_RDG_TEXTURE | Shader 读取 Texture 对象 |
SHADER_PARAMETER_RDG_TEXTURE_SRV | 通过 SRV 读取 Texture |
SHADER_PARAMETER_RDG_TEXTURE_UAV | 通过 UAV 读写 Texture |
| Buffer 对应宏 | 读取或读写 Buffer |
RENDER_TARGET_BINDING_SLOTS() | Raster Render Target / Depth Stencil 访问 |
RDG_PARAMETER_STRUCT_INCLUDE 等组合 | 把公共参数的资源访问纳入本 Pass |
| Uniform Buffer 参数 | 通过结构化参数访问共享 Shader 数据 |
如果 Lambda 从全局变量、单例或捕获的裸 RHI 指针访问另一份资源,而参数表没有声明,可能造成:
- 生产者和消费者被错误重排;
- Pass 被错误裁剪;
- 缺少 Transition 或 UAV Barrier;
- 资源在使用前已被 Transient Aliasing 覆盖;
- Async Compute 与 Graphics 产生数据竞争。
“关闭 RDG Validation 后不报错”不能证明访问合法,只是移除了检查。
Pass Culling:为什么我的 Lambda 没有运行
考虑这张图:
如果 Unused 没有消费者,Pass C 没有声明外部副作用,它可以被整体裁掉。Graph 通常从以下根节点向生产者回溯:
- 被提取或转为 External 的输出;
- 最终要 Present / Copy 到外部的输出;
- 明确不可裁剪且确实有外部副作用的 Pass;
- Builder / RHI 约定的其他 Root。
NeverCull 不是“让调试 Pass 出现”的默认修复
ERDGPassFlags::NeverCull 适合 RDG 无法从资源图看见、但确实必须发生的副作用,例如某些 Query、Readback 或外部 API 交互。若普通纹理计算 Pass 被裁掉,应先检查:
- 输出是否真的连接到下游;
- 下游是否也被裁掉;
- 是否需要 Queue Extraction;
- 资源是否遗漏在 Pass Parameters 之外。
滥用 NeverCull 会保留无用工作并掩盖依赖缺陷。
逻辑资源、物理资源与 Transient Aliasing
假设两个 4K RGBA16F 临时纹理分别只在帧的前半和后半使用:
时间 ─────────────────────────────────────────→
TempA: [创建][写入][读取][最后使用]TempB: [创建][写入][读取][最后使用]
物理显存区间 X: [──────── TempA ────────][──────── TempB ────────]只要生命周期不重叠、Descriptor 与平台约束允许,Transient Allocator 可以让两个逻辑资源复用同一物理区域。这是“显存别名”,不是两张纹理同时指向同一有效内容。
会延长生命周期并减少 Aliasing 机会的情况:
- 资源被很早创建、很晚才消费;
- Queue Extraction 让资源活到 Graph 外;
- Async Compute Pass 跨过较长的 Graphics 区间;
- 隐藏访问迫使开发者禁用优化;
- 不必要地把中间结果注册为 External;
- Capture / Debug 模式改变了 Transient 策略。
分析 RDG 显存时,资源像素尺寸之和不是实际峰值;要看同时存活集合和平台分配粒度。
External Resource:把 Graph 外资源带进来
Swap Chain、Scene 历史、持久阴影缓存、跨帧 Temporal History 或其他系统拥有的 Pooled RHI 资源,不由当前 Graph 创建。它们需要注册成 External Resource:
FRDGTextureRef History = GraphBuilder.RegisterExternalTexture( PooledHistory, TEXT("Research.History"));注册后,当前 Graph 可以跟踪它在本图内的访问与 Transition,但底层资源所有权仍在外部。还要明确:
- 进入 Graph 时的已知 Access / State;
- Graph 结束后外部消费者需要的最终 Access;
- 外部所有者保证资源覆盖整个 Graph 执行期;
- 多个 Graph 或队列之间的同步责任。
External 不等于“RDG 不再管理状态”,而是“生命周期所有权不属于本 Graph”。
Extraction:让图内结果活到图外
如果当前 Graph 创建的结果要在 Execute() 之后使用,可排队提取:
TRefCountPtr<IPooledRenderTarget> ExtractedOutput;GraphBuilder.QueueTextureExtraction(Output, &ExtractedOutput);
GraphBuilder.Execute();
// Execute 之后 ExtractedOutput 才由外部持有。Extraction 会把资源变成 Graph 的可观察输出,因此:
- 生产它的 Pass 不再因无人消费而被裁掉;
- 物理资源不能在 Graph 内最后一次逻辑读取后立刻复用;
- 外部必须负责持有和后续状态 / 生命周期;
- 每帧无条件提取大纹理可能增加峰值显存。
若只是为了在同一个 Graph 的后续 Pass 使用,直接连接 FRDGTextureRef,不要提取再注册回来。
Barrier:RDG 自动化了什么
GPU Resource 在不同使用方式之间通常需要状态转换与同步,例如:
UAV Write → Shader Resource ReadRender Target Write → Copy SourceCopy Dest → Shader Resource ReadGraphics Write → Async Compute ReadAsync Compute Write → Graphics ReadRDG 根据参数声明和 Pass Flags 生成或合并:
- Texture / Buffer Transition;
- UAV Hazard Barrier;
- Graphics 与 Async Compute Queue Fence;
- Pass Prologue / Epilogue Barrier;
- 需要时的 Subresource 级状态跟踪。
自动化不表示 Barrier 免费。以下图形会产生同步压力:
- 资源在 UAV 与 SRV / RT 间高频来回切换;
- Graphics 和 Async Compute 交替读写同一资源;
- 很小的 Pass 被切得过碎;
- 全资源 Transition 代替可用的 Subresource 访问;
- Readback 或外部访问迫使 GPU / CPU 等待。
优化目标不是“Barrier 数量归零”,而是在正确性约束下减少不必要的状态抖动和队列等待。
Async Compute:有依赖不等于有重叠
一个适合异步的拓扑可能是:
如果硬件有独立队列能力,C0 可能与 G1 重叠。但只给 Pass 加 ERDGPassFlags::AsyncCompute 仍不保证收益:
- G1 与 C0 可能竞争同一批 GPU Execution Units、Cache 或带宽;
- C0 输入太晚可用、输出又太早被 Join 消费,重叠窗口很窄;
- 跨队列 Transition 和 Fence 成本超过重叠收益;
- C0 本身太小,调度成本占主导;
- 平台将 Async Compute 串行化或不支持目标路径;
- Async 生命周期延长,增加 Transient Memory 峰值。
是否成功必须看 PIX、RenderDoc、RGP、Nsight 等平台时间线中的实际队列并发,不能只看 RDG Event 标注。
Pass Flags 速查
| Flag | 典型用途 | 注意点 |
|---|---|---|
Raster | Render Target / Depth Stencil 光栅 Pass | 参数中应声明 RT Binding |
Compute | Graphics Queue 上的 Compute Dispatch | 不代表异步队列 |
AsyncCompute | 允许调度到 Async Compute | 必须测量真实重叠与同步 |
Copy | Copy / Resolve 类工作 | 资源需声明正确 Copy Access |
NeverCull | 有不可见外部副作用 | 少用,不能代替依赖连接 |
SkipRenderPass 等特殊 Flag | Pass 自己管理特殊 RHI 行为 | 只沿用引擎现有模式,确认平台限制 |
Flag 可以组合,合法组合与精确语义以 UE 5.8 ERDGPassFlags 注释为准。
Lambda 捕获与分配生命周期
下面这些代码在 Setup 阶段结束时,Pass 还可能没有执行:
FLocalData Local;
GraphBuilder.AddPass( RDG_EVENT_NAME("BadCapture"), Parameters, ERDGPassFlags::Compute, [&Local](FRHIComputeCommandList& RHICmdList) { // 若 Execute 发生在 Local 生命周期之后,这个引用已悬空。 });安全原则:
- 小型不可变 POD 值捕获;
- Shader Parameters 用
GraphBuilder.AllocParameters; - 需要与 Graph 同寿命的数组 / 对象使用 Builder 提供的分配工具;
- 不捕获临时栈对象引用;
- 不在成员变量中保存
FRDGTextureRef跨 Graph 使用; - Pass 可能被裁掉,不把必须发生的 CPU 副作用塞进 Lambda。
GraphBuilder.Alloc* 的内存服务于 Graph 生命周期,不应在 Execute() 之后交给外部长期持有。
Raster Pass 的资源声明
Raster Pass 常在参数结构里使用:
BEGIN_SHADER_PARAMETER_STRUCT(FResearchRasterParameters, ) SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View) SHADER_PARAMETER_RDG_TEXTURE(Texture2D, InputTexture) RENDER_TARGET_BINDING_SLOTS()END_SHADER_PARAMETER_STRUCT()
Parameters->RenderTargets[0] = FRenderTargetBinding( Output, ERenderTargetLoadAction::ENoAction);Load Action 是依赖的一部分:
ELoad表示保留并读取已有 Attachment 内容;EClear表示以声明的 Clear Value 初始化;ENoAction表示旧内容不需要保留。
错误地使用 ELoad 会引入不必要带宽与依赖;错误地使用 ENoAction 却在 Shader 中依赖旧内容,会产生未定义结果。
调试工具
给 Pass 和资源起可搜索的名字
RDG_EVENT_SCOPE(GraphBuilder, "ResearchPipeline");RDG_GPU_STAT_SCOPE(GraphBuilder, ResearchPipeline);
FRDGTextureRef Output = GraphBuilder.CreateTexture( Desc, TEXT("Research.Output"));稳定命名能把源码、ProfileGPU、RenderDoc / PIX Event 与资源关联起来。事件名中可以放分辨率或质量级别,但不要放每帧随机地址导致 Capture 无法比较。
常见 RDG 调试变量
| 变量 / 模式 | 目的 | 代价 |
|---|---|---|
r.RDG.Debug | 增加 Validation / Warning 与调试检查 | CPU 开销上升,Development 使用 |
r.RDG.ImmediateMode | Pass 添加后尽快执行,获得更直接调用栈 | 关闭正常编译优化,不能测性能 |
r.RDG.ClobberResources | 用特殊值覆盖资源,暴露未初始化读取 | 明显变慢并改变内容 |
| Graph / Transition Dump | 输出拓扑、资源和 Transition | 日志量大,名称随版本变化 |
变量与取值会随 UE 分支变化。使用 DumpConsoleCommands RDG 或当前 Console Variables Reference 核对,不把旧版本博客命令当作事实。
六个可证伪实验
H1:无人消费的 Pass 会被裁掉
- 建立 A → B 链,但不把 B 输出连接到外部或后续 Pass。
- 在两个 Pass 加 GPU Event 和 CPU 计数。
- 正常模式执行,再对 B 输出 Queue Extraction。
预期:未提取时链可能整体被裁掉;提取后生产链出现。若未提取仍执行,检查 NeverCull、外部副作用或隐含 Root。
H2:非重叠临时资源可以复用显存
- 创建两个相同 Descriptor 的大纹理 A、B。
- 保证 B 的首次使用晚于 A 的最后使用。
- 记录 RDG Resource Lifetime 与 Transient Memory。
- 再添加一个 Pass 同时读取 A、B,强制生命周期重叠。
预期:非重叠版本峰值更低或显示 Aliasing;强制重叠后不能共用同一物理区间。平台分配粒度可能使数字不恰好相差一张纹理。
H3:Extraction 会延长资源生命周期
- 对一个中间大纹理分别测试图内消费与 Queue Extraction。
- 保持 Shader 工作相同。
- 比较 Transient 峰值、资源释放点和后续资源 Aliasing。
预期:提取版资源活到 Graph 末尾并由外部持有,峰值可能上升。
H4:隐藏资源访问破坏依赖推导
- 在 Development 构建启用 RDG Debug / Validation。
- 故意让测试 Pass 通过参数表之外的路径访问资源。
- 观察 Validation、Transition 或输出异常。
- 把访问移动到正确 Parameters 声明后复测。
实验只在隔离测试分支进行;不要把未声明访问保留在主代码。
H5:Async Compute 只有实际队列重叠才有收益
- 让一个 Compute Pass 依赖早期资源、输出只被较晚 Graphics Pass 消费。
- 分别使用
Compute与AsyncCompute。 - 在目标 GPU 的队列时间线比较总帧、重叠区间、Fence 和 Busy 时间。
- 增加共享带宽压力,再观察重叠是否消失或变慢。
预期:拓扑提供机会,但收益与硬件资源竞争相关;标记异步却没有重叠时,不能宣称优化成功。
H6:Immediate Mode 适合查调用栈,不适合测性能
- 对同一场景捕获正常 RDG 与 Immediate Mode。
- 验证输出内容和 Pass 调用栈。
- 比较 Pass Culling、Aliasing、Barrier 与总帧时间。
预期:Immediate Mode 更容易定位添加 Pass 的调用路径,但执行计划和性能不代表正常模式。
源码阅读地图
| 文件 / 符号 | 研究问题 |
|---|---|
RenderGraphBuilder.h / FRDGBuilder | 公共构建 API 与生命周期契约是什么? |
RenderGraphBuilder.cpp | Compile / Execute、Pass Culling 和 Barrier 调度怎样串联? |
RenderGraphResources.* | Texture / Buffer / View 怎样表示逻辑与物理资源? |
RenderGraphPass.* | Pass Parameters、Flags 与依赖怎样存储? |
RenderGraphValidation.* | 哪些非法访问可在 Development 被发现? |
RenderGraphTrace.* / Event 相关文件 | Graph、资源和 GPU Event 怎样进入调试工具? |
RenderGraphUtils.* | Copy、Clear、Fullscreen、Readback 等标准 Pass 怎样复用? |
TransientResourceAllocator 相关实现 | Heap / Page / Aliasing 与平台后端怎样连接? |
FRDGBuilder::AddPass... | 一个 Pass 从 Setup 到内部节点发生什么? |
FRDGBuilder::Execute | Graph 的 Compile、资源获取、执行和清理边界在哪里? |
跟一个真实 Pass 时按以下顺序:
调用方创建资源 → Parameters 声明读写 → AddPass / Helper → 下游消费者 → Graph Compile 中的依赖与 Lifetime → Pass Execute Lambda → RHI Command → GPU Capture Event 与资源状态故障定位表
| 症状 | 最可能先查 |
|---|---|
| Pass Event 完全不存在 | 输出未消费、整链被裁掉、Quality 分支没进入 |
| Pass 存在但输出全黑 | Load Action、Clear、Shader 参数、Extent / View Rect、未初始化读取 |
| RDG Validation Assert | 参数外访问、非法生命周期、错误 SRV/UAV/RT 声明 |
| 只在 Shipping / 高负载闪烁 | 隐藏依赖、跨线程捕获、历史资源所有权、缺 Barrier |
| 开 Async Compute 反而慢 | 重叠窗口、队列 Fence、带宽/Occupancy 竞争、生命周期延长 |
| 显存峰值异常 | Extraction、External 资源、重叠生命周期、Capture 模式、超大 Descriptor |
| RenderDoc 中资源名难追 | 缺少稳定 RDG Event / Resource Name |
加 NeverCull 后“修好” | 依赖没连上或真正外部副作用未建模 |
本篇建立的心智模型
Renderer 声明 Pass Parameters → RDG 得到资源 Read / Write 边 → 从外部输出回溯并裁剪 → 计算 Logical Resource Lifetime → 分配 / Alias Transient Memory → 安排 Transition、UAV Barrier、Queue Fence → 执行 Pass Lambda → RHI Command List → Graphics / Compute / Copy Queue下一篇把 RDG 放回完整 Deferred Frame:逐项跟踪 Depth、GBuffer、Velocity、Shadows、Lighting、Translucency、TSR 与 Tonemap 的输入输出,建立一份能与 GPU Capture 对照的 Pass 地图。
官方资料
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






