mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4024 字
11 分钟
UE5 渲染管线 03:RDG 资源生命周期、Barrier 与异步计算
2026-07-16
NOTE

系列导航:00 总览01 线程与 Scene Proxy02 可见性与 Mesh Drawing03 RDG04 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 上面做什么#

flowchart TD A["Renderer 高层功能<br/>Nanite / Lumen / Shadows / Post"] B["FRDGBuilder<br/>Pass + Resource + Dependency"] C["Graph Compile<br/>Cull / Lifetime / Barrier / Queue"] D["Graph Execute<br/>Pass Lambda / Parallel Work"] E[RHI Command List] F[D3D12 / Vulkan / Metal] G[GPU Queues] A --> B --> C --> D --> E --> F --> G

一帧中可能有多个 Graph Builder 或嵌套在不同渲染工作流里的 Graph,不能把 RDG 理解成全引擎唯一的“全局帧图”。但对于一个 Builder,所有图内资源和 Pass 都必须遵守同一个构建—执行生命周期。

三个阶段:Setup、Compile、Execute#

1. Setup:声明逻辑资源与 Pass#

调用 CreateTextureCreateBuffer 时,通常只是创建一个带 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_TEXTUREShader 读取 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 没有运行#

考虑这张图:

flowchart LR I[External Input] A["Pass A<br/>写 TempA"] B["Pass B<br/>读 TempA 写 TempB"] C["Pass C<br/>写 Unused"] D["Pass D<br/>读 TempB 写 SceneColor"] O[External / Extracted Output] I --> A --> B --> D --> O I --> C

如果 Unused 没有消费者,Pass C 没有声明外部副作用,它可以被整体裁掉。Graph 通常从以下根节点向生产者回溯:

  • 被提取或转为 External 的输出;
  • 最终要 Present / Copy 到外部的输出;
  • 明确不可裁剪且确实有外部副作用的 Pass;
  • Builder / RHI 约定的其他 Root。

NeverCull 不是“让调试 Pass 出现”的默认修复#

ERDGPassFlags::NeverCull 适合 RDG 无法从资源图看见、但确实必须发生的副作用,例如某些 Query、Readback 或外部 API 交互。若普通纹理计算 Pass 被裁掉,应先检查:

  1. 输出是否真的连接到下游;
  2. 下游是否也被裁掉;
  3. 是否需要 Queue Extraction;
  4. 资源是否遗漏在 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 Read
Render Target Write → Copy Source
Copy Dest → Shader Resource Read
Graphics Write → Async Compute Read
Async Compute Write → Graphics Read

RDG 根据参数声明和 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:有依赖不等于有重叠#

一个适合异步的拓扑可能是:

flowchart LR G0["Graphics G0<br/>产生 Depth"] G1["Graphics G1<br/>与 Compute 无资源冲突"] C0["Async Compute C0<br/>读取 Depth 写 AO"] J["Graphics Join<br/>读取 G1 与 AO"] G0 --> G1 --> J G0 --> C0 --> J

如果硬件有独立队列能力,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典型用途注意点
RasterRender Target / Depth Stencil 光栅 Pass参数中应声明 RT Binding
ComputeGraphics Queue 上的 Compute Dispatch不代表异步队列
AsyncCompute允许调度到 Async Compute必须测量真实重叠与同步
CopyCopy / Resolve 类工作资源需声明正确 Copy Access
NeverCull有不可见外部副作用少用,不能代替依赖连接
SkipRenderPass 等特殊 FlagPass 自己管理特殊 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.ImmediateModePass 添加后尽快执行,获得更直接调用栈关闭正常编译优化,不能测性能
r.RDG.ClobberResources用特殊值覆盖资源,暴露未初始化读取明显变慢并改变内容
Graph / Transition Dump输出拓扑、资源和 Transition日志量大,名称随版本变化

变量与取值会随 UE 分支变化。使用 DumpConsoleCommands RDG 或当前 Console Variables Reference 核对,不把旧版本博客命令当作事实。

六个可证伪实验#

H1:无人消费的 Pass 会被裁掉#

  1. 建立 A → B 链,但不把 B 输出连接到外部或后续 Pass。
  2. 在两个 Pass 加 GPU Event 和 CPU 计数。
  3. 正常模式执行,再对 B 输出 Queue Extraction。

预期:未提取时链可能整体被裁掉;提取后生产链出现。若未提取仍执行,检查 NeverCull、外部副作用或隐含 Root。

H2:非重叠临时资源可以复用显存#

  1. 创建两个相同 Descriptor 的大纹理 A、B。
  2. 保证 B 的首次使用晚于 A 的最后使用。
  3. 记录 RDG Resource Lifetime 与 Transient Memory。
  4. 再添加一个 Pass 同时读取 A、B,强制生命周期重叠。

预期:非重叠版本峰值更低或显示 Aliasing;强制重叠后不能共用同一物理区间。平台分配粒度可能使数字不恰好相差一张纹理。

H3:Extraction 会延长资源生命周期#

  1. 对一个中间大纹理分别测试图内消费与 Queue Extraction。
  2. 保持 Shader 工作相同。
  3. 比较 Transient 峰值、资源释放点和后续资源 Aliasing。

预期:提取版资源活到 Graph 末尾并由外部持有,峰值可能上升。

H4:隐藏资源访问破坏依赖推导#

  1. 在 Development 构建启用 RDG Debug / Validation。
  2. 故意让测试 Pass 通过参数表之外的路径访问资源。
  3. 观察 Validation、Transition 或输出异常。
  4. 把访问移动到正确 Parameters 声明后复测。

实验只在隔离测试分支进行;不要把未声明访问保留在主代码。

H5:Async Compute 只有实际队列重叠才有收益#

  1. 让一个 Compute Pass 依赖早期资源、输出只被较晚 Graphics Pass 消费。
  2. 分别使用 ComputeAsyncCompute
  3. 在目标 GPU 的队列时间线比较总帧、重叠区间、Fence 和 Busy 时间。
  4. 增加共享带宽压力,再观察重叠是否消失或变慢。

预期:拓扑提供机会,但收益与硬件资源竞争相关;标记异步却没有重叠时,不能宣称优化成功。

H6:Immediate Mode 适合查调用栈,不适合测性能#

  1. 对同一场景捕获正常 RDG 与 Immediate Mode。
  2. 验证输出内容和 Pass 调用栈。
  3. 比较 Pass Culling、Aliasing、Barrier 与总帧时间。

预期:Immediate Mode 更容易定位添加 Pass 的调用路径,但执行计划和性能不代表正常模式。

源码阅读地图#

文件 / 符号研究问题
RenderGraphBuilder.h / FRDGBuilder公共构建 API 与生命周期契约是什么?
RenderGraphBuilder.cppCompile / 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::ExecuteGraph 的 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 地图。

官方资料#

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

UE5 渲染管线 03:RDG 资源生命周期、Barrier 与异步计算
https://example.pages.dev/posts/ue5-rendering-pipeline/03-rdg/
作者
博主
发布于
2026-07-16
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录