UE5 移动端全 OnePass 渲染管线

概述

在 UE5 移动端渲染管线中实现全 OnePass:主视图的全部光栅阶段(Base/GBuffer → Decals → Lighting → Fog → Translucency → Tonemap)在一次物理 RenderPass 内完成,主 SceneColor/GBuffer 全程保持在 GPU tile memory(Memoryless),永不 Store/Reload。

  • 适用平台: Android Vulkan(5/6-subpass 拓扑)+ Android OpenGL ES(FBF barrier 序列 + inline tonemap);Metal 保持现有 framebuffer fetch 路径
  • 预期收益: 消除 2~4 次 SceneColor/GBuffer 的 Store/Reload 带宽;TBDR GPU(Adreno/Mali)收益最大
  • 核心机制:
    • Vulkan: 新增 ESubpassHint 复合拓扑 —— Forward 5-subpass / Deferred 6-subpass
    • GLES: 无 subpass API,同一渲染函数 + NextSubpass() = non-coherent FBF barrier 序列 + gl_LastFragColor inline tonemap
  • 改动范围: 21 个文件(Vulkan 版 16 个 + GLES 版 5 个),构建验证中(编译通过,链接等待编辑器关闭)

一、原本管线的痛点

1. MultiPass 下 SceneColor/GBuffer 反复 Store/Reload

移动端默认 Forward 路径在部分平台强制 MultiPass(RequiresMultiPass() 判定):

1
2
3
RenderPass 1:  BasePass(写 SceneColor)       → Store SceneColor 到主内存
RenderPass 2: Decals + Fog + Translucency → Load SceneColor ← 又写回去
RenderPass 3: PostProcessing / Tonemap → Load SceneColor

Deferred 路径更严重:GBuffer(4~6 个附件)写完 Store、Decals/Lighting 再 Load,Lighting 后 SceneColor 再 Store/Reload 一次。

在 TBDR(Tile-Based Deferred Rendering)GPU 上的代价:每个 Store/Load 都是一次完整的主内存带宽往返。以 1080p RGBA16F SceneColor 为例,一次 Store+Load ≈ 16MB×2 = 32MB 带宽;多 pass 下 SceneColor + GBuffer 合计可达 4~8 次往返,近百 MB 带宽浪费。而 TBDR GPU 的 tile memory 本可以完全避免这些流量 —— 这是移动端最贵的浪费之一。

2. UE 已有 SinglePass 的局限

UE 已经通过 Vulkan SubPass / Metal framebuffer fetch 把 Base+Decals+Fog+Translucency 合并进一个 RenderPass(RenderForwardSinglePass / RenderDeferredSinglePass),但仍有三个结构性缺口:

缺口 后果
ESubpassHint 是固定模板 DepthReadSubpass=2 个、DeferredShadingSubpass=3 个、CustomResolveSubpass=3 个 subpass,无法细分出独立的 Fog / Translucency subpass,也无法在 Deferred 里追加 Tonemap
Tonemap 是独立后处理 pass 主 pass 结束后 SceneColor 必须 Store 到主内存,再 Load 回来做 Tonemap/FXAA —— 主 pass 的 Memoryless 收益被最后一步打回原形
Deferred 默认强制 MultiPass r.Mobile.AllowFramebufferFetch=0(ReadOnly,默认关闭)时 Deferred 直接走 MultiPass,GBuffer 全部落内存

3. 已有尝试的缺陷

我们最初的实现(两个 Render*OnePass 包装函数)试图通过多调几次 RHICmdList.NextSubpass() 伪造更多 subpass —— 这是错误用法

  • Vulkan 的 subpass 数量由 RenderPass 创建时的拓扑决定,NextSubpass() 只是前进游标;
  • 结果 Forward 最多 3 次、Deferred 最多 4 次调用全部越界(Vulkan validation 直接报错);
  • 还遗留了一个多余的 } 导致整个 Renderer 模块编译失败。

结论:想真正做全 OnePass,必须从 RHI 层扩展 subpass 拓扑,而不是在渲染器层堆调用


二、我们的修改解决了什么

1. 新增复合 SubPass 拓扑(RHI 层)

ESubpassHint 中新增两个枚举,让 Vulkan RenderPass Builder 能构建完整管线拓扑:

Deferred 6-subpassDeferredFullOnePassSubpass):

1
2
SP0 BasePass+GBuffer → SP1 Decals → SP2 Deferred Lighting
→ SP3 Fog → SP4 Translucency → SP5 Tonemap CustomResolve → FinalOutput

Forward 5-subpassForwardFullOnePassSubpass):

1
2
SP0 BasePass+Sky → SP1 Decals+ModulatedShadows → SP2 Fog
→ SP3 Translucency → SP4 Tonemap CustomResolve → FinalOutput

2. 关键收益

收益 说明
SceneColor/GBuffer 全程 Memoryless 从 SP0 写入到 SP5 读取,全程 tile memory,零主内存往返
Tonemap inline CustomResolve subpass 读 SceneColor 输入附件直接输出 FinalOutput,后处理链(Bloom/DOF/FXAA)整条跳过
每阶段独立 subpass Decals/Fog/Translucency 分开,TBDR tile 内显式 layout/dependency,便于驱动调优
安全回退 能力不足时输出一次性 warning 并回退到原有路径,默认 r.Mobile.OnePass=0 零回归

三、如何修改(分层方案)

第 1 层:RHI 枚举(RHI/Public/RHIResources.h

1
2
3
4
5
6
7
8
9
enum class ESubpassHint : uint8
{
None,
DepthReadSubpass,
DeferredShadingSubpass,
CustomResolveSubpass,
ForwardFullOnePassSubpass, // 新增:Forward 5-subpass
DeferredFullOnePassSubpass, // 新增:Deferred 6-subpass
};

第 2 层:Vulkan RenderPass Builder(VulkanRenderpass.h

核心改造点:

  1. 动态 subpass 数量BuildCreateInfo() 按 hint 生成 1~6 个 subpass,依赖链(srcSubpass/dstSubpass)改用动态索引而非硬编码;
  2. last-attachment resolve:CustomResolve subpass 的输出从硬编码 ColorAttachmentReferences[1] 改为 ColorAttachmentReferences[NumColorAttachments](最后一个附件)—— 原实现假定只有 SceneColor+FinalOutput 两个附件,Deferred 布局下会写错到 GBufferA;
  3. attachment 预算:主/Decal subpass 用 NumColorAttachments(排除 final output),Lighting 的 GBuffer 输入附件排除 final output;
  4. 强制 classic RenderPass:新 hint 绕过 Dynamic Rendering 路径(KHR_dynamic_rendering_local_read 未实现,dynamic rendering 下 NextSubpass 只有一条不完整的 memory barrier,无法承载 subpass 语义)。

第 3 层:渲染器函数(MobileShadingRenderer.cpp

  • RenderForwardFullOnePass:4 次 NextSubpass(),严格匹配 5-subpass 拓扑;
  • RenderDeferredFullOnePass:5 次 NextSubpass(),并把 ViewFamilyTexture 追加为最后一个单采样附件;
  • bFullOnePass 判定:
1
2
bOnePass     = (CVarMobileOnePass != 0) && !bRequiresMultiPass;   // 能力不足 → warning + 回退
bFullOnePass = bOnePass && bTonemapSubpassInline && IsVulkanPlatform;

第 4 层:PSO 预缓存一致性

关键陷阱:PSO 预缓存(GetSubpassHint())和运行时 render pass 必须使用同一组判定条件,否则 PSO 按错误 subpass index 创建,Vulkan 直接崩。因此:

  • MeshPassProcessor.cpp::GetSubpassHint() 镜像 bFullOnePass 判定(CVar + Vulkan + inline tonemap 可用);
  • MobileBasePass.cpp Translucency subpass index:Forward=3 / Deferred=4;
  • MobileDeferredShadingPass.cpp Lighting hint 跟随 CVar;
  • 同时放开 SceneUtils.cpp 对 Deferred 使用 inline tonemap 的硬排除(OnePass 开启时)。

为什么不做”每帧一个 Pass”

阴影图(光源视角 atlas)、HZB/AO/体积雾(compute/半尺寸)、DBuffer(需在 Base 前就绪)、Opaque FX(GPU sort)在架构上无法进入同一个屏幕空间光栅 RenderPass。TBDR 下”一帧一个 Pass”既不存在也不需要 —— 把最贵的 SceneColor 链压进单 RenderPass 即收益最大化,其余阶段用 Memoryless/前置图节点保证不触碰主 SceneColor。


四、改动清单

RHI 层

# 文件 改动
1 RHI/Public/RHIResources.h 新增 2 个 ESubpassHint 枚举值
2 VulkanRHI/Private/VulkanRenderpass.h 5/6-subpass builder、动态依赖索引、last-attachment resolve
3 VulkanRHI/Private/VulkanRenderTargetLayout.cpp 新 hint 最后附件识别为单采样 final target
4 VulkanRHI/Private/VulkanPipeline.cpp 各 subpass RT 数量 + resolve subpass RasterizationSamples=1
5 VulkanRHI/Private/VulkanRenderTarget.cpp 新 hint 强制 classic RenderPass
6 VulkanRHI/Private/VulkanContext.cpp NumSamples 校验覆盖新 hint
7 RHI/Private/RHI.cpp depth input-attachment 校验覆盖新 hint

渲染器层

# 文件 改动
8 MobileShadingRenderer.cpp 两个 FullOnePass 函数 + bFullOnePass + dispatch 三分支
9 SceneRendering.h 成员 + 函数声明
10 Engine/Private/SceneUtils.cpp OnePass 时允许 Deferred inline tonemap
11 MeshPassProcessor.cpp GetSubpassHint() 镜像判定
12 MobileBasePass.cpp Translucency subpass index 映射
13 MobileDeferredShadingPass.cpp Lighting PSO hint 跟随 CVar

配置与文档

# 文件 改动
14 AndroidScalability.ini / IOSScalability.ini 移除无效 r.Mobile.OnePass=1(SystemSettings 优先级更高会压住 Scalability)
15 README.md 全 OnePass 章节

五、参考与借鉴

来源 借鉴点
Epic: Performance and Optimization for Mobile TBDR 带宽优化原则、Memoryless 标记策略
Epic: Mobile Rendering Features / Shading Modes Mobile Deferred HDR、Forward/Deferred 选择
UE 源码现有实现 CustomResolveSubpass(Vulkan inline tonemap)、DeferredShadingSubpass(GBuffer input attachment)的 attachment/dependency 模式 —— 新拓扑是它们的复合扩展
Vulkan Spec: Render Pass / Subpass / Input Attachment subpass 依赖链(VK_DEPENDENCY_BY_REGION_BIT)、last-attachment 语义
UF2025 Shanghai: 移动端渲染管线优化实战 —《洛克王国:世界》 SubPass 合并、Deferred HDR 单 Pass 的工程实践方向
ARM: Mali and UE5 Nanite / MegaLights on mobile Mali GPU tile memory 行为、带宽瓶颈分析

说明:具体实现(枚举扩展、builder 改造、PSO 一致性)是本仓库的原创工程,参考了上述文档与 UE 源码中已有 subpass 模式的语义,并非某篇公开文章的复刻。


六、适合什么样的项目

✅ 适合

项目特征 原因
Android Vulkan 为目标平台 原生 5/6-subpass 拓扑,收益最完整
Android GLES + FBF 设备(Adreno 6xx+ / Mali G-series+) 同一渲染函数 + barrier 序列 + FBF inline tonemap(MSAA≤1)
TBDR GPU Store/Load 带宽是主要瓶颈,OnePass 收益最大
Deferred HDR 高端画质 原本 GBuffer 落内存的浪费最大,收益最明显
对功耗/发热敏感 减少主内存带宽 = 直接降低功耗
不需要完整后处理链 OnePass 模式跳过 Bloom/DOF/TAA,配合 MSAA 抗锯齿即可

❌ 不适合

项目特征 原因
重度依赖后处理链(Bloom/DOF/TAA/FXAA 都要) OnePass 模式只保留 inline tonemap
使用 Distortion / SceneColorCopy 折射材质 / SingleLayerWater 折射 需要偏移/邻域采样主 SceneColor,tile-local input attachment 无法提供
GLES 兼容范围广(低端安卓机群) 无 FBF/PLS 设备只能 MultiPass;PLS 限 128-bit GBuffer
大量 PostOpaque Extensions / 第三方渲染扩展 外部扩展可读写 SceneColor/GBuffer,无 subpass 合约
PC / 主机为主 桌面端无 FramebufferFetch,MobileOnePass 不生效

七、验证方法

构建验证(已完成)

1
2
Build.bat UnrealEditor Win64 Development -Module=Engine -Module=VulkanRHI -Module=Renderer
# Result: Succeeded(0 error)

RenderDoc 抓帧(待真机)

关键字 含义
SceneColorRendering_FullOnePass 主 OnePass RenderPass
MobileTonemapSubpass 最终 Tonemap CustomResolve subpass
vkCmdNextSubpass 确认 subpass 数量(Forward=4 次 / Deferred=5 次)
attachment 面板 确认 SceneColor/GBuffer 为 DONT_CARE store(Memoryless)

真机

  1. Android Vulkan 打包(Adreno 6xx+ / Mali G-series+)
  2. stat gpu 对比 OnePass 0/1 的 RenderPass 数量与带宽
  3. RenderDoc 无线抓帧验证 subpass 拓扑

八、GLES 全 OnePass(Android OpenGL ES)

GLES 与 Vulkan 的差异

GLES 没有 subpass API —— 全 OnePass 在 GLES 上的形态:

概念 GLES 等效
一次物理 RenderPass 一次 framebuffer 绑定 + 连续绘制(TBDR 上即一次 tile pass)
5/6 阶段序列 与 Vulkan 同一渲染函数RenderForwardFullOnePass / RenderDeferredFullOnePass),每次 NextSubpass() = non-coherent FBF barrier
input attachment(读当前像素) GL_EXT_shader_framebuffer_fetchgl_LastFragColor)/ PLS
Memoryless GBuffer GL_EXT_shader_pixel_local_storage(PLS,128-bit)

GLES 改动清单(5 个文件)

# 文件 改动
1 Engine/Private/SceneUtils.cpp IsMobileTonemapSubpassEnabledInline 放开 GLES(FBF 设备);MSAA 收窄为 ≤1(GLES 无 shader MSAA resolve,gl_LastFragColor 只能读当前 sample)
2 Renderer/Private/MobileShadingRenderer.cpp bFullOnePass 判定加入 IsAndroidOpenGLESPlatform && GSupportsShaderFramebufferFetch → GLES 上 dispatch 进入 FullOnePass 函数
3 Engine/Shaders/Private/PostProcessTonemap.usf FetchAndResolveSceneColor 新增 COMPILER_GLSL_ES3_1 分支:FramebufferFetchES2()gl_LastFragColor)读 SceneColor
4 OpenGLDrv/Private/OpenGLRenderTarget.cpp RHINextSubpass 的 FBF barrier 覆盖 3 个新 hint(每阶段边界);PLS glEnable/glDisable 覆盖 DeferredFullOnePassSubpass
5 Renderer/Private/MobileDeferredShadingPass.cpp 修复 CVar-only bug:PSO 预缓存 bFullOnePass 补上能力门槛,与运行时一致

GLES 行为矩阵

场景 行为
GLES + FBF + MSAA≤1 + OnePass=1 ✅ 完整全 OnePass:单 framebuffer、5/6 段 barrier 序列、inline tonemap(FBF fetch)
GLES + PLS + MSAA≤1 + OnePass=1(Deferred) ✅ 完整全 OnePass:PLS GBuffer + CopyBuffer + inline tonemap
GLES + FBF + MSAA>1 + OnePass=1 主链全 OnePass(单 pass),tonemap 段回退独立 AddMobileCustomResolvePass(硬件限制:无 shader MSAA resolve)
GLES 无 FBF/PLS RequiresMultiPass=true → 回退 MultiPass

九、平台兼容性

平台 全 OnePass 说明
Android Vulkan ✅(新 5/6-subpass 拓扑) maxColorAttachments>=5AllowFramebufferFetch=1
Android GLES + FBF/PLS ✅(同一渲染函数 + barrier 序列 + FBF inline tonemap) MSAA≤1 才 inline;PLS 限 128-bit GBuffer;non-coherent fetch 需 barrier
Android GLES 无 FBF ❌ 回退 MultiPass
iOS Metal ❌ 保持现有 framebuffer fetch encoder Metal 无真实 subpass;inline tonemap 未移植
Desktop PC ❌ 无效果 MobileAllowFramebufferFetch 返回 false

十、已知问题 & 注意事项

  1. 真机验证未完成:subpass 拓扑/依赖链/GLES FBF barrier 只通过编译器验证,需 RenderDoc + validation layer 真机确认(当前 0 GPU 执行记录)。
  2. GLES MSAA 限制gl_LastFragColor 无法做多采样平均(无 Vulkan SubpassInputMS 等效)→ MSAA>1 时 tonemap 自动回退独立 resolve pass(主链单 pass 收益保留)。
  3. Deferred shadowed point light:仍走 MultiPass 的 per-light mask 流程(SinglePass 路径本就不支持,非新增回归)。
  4. Dynamic Rendering(Vulkan):新 hint 强制 classic RenderPass;KHR_dynamic_rendering_local_read 实现后才能解锁。
  5. 功能降级(与现有 Forward inline-tonemap 路径一致):Distortion / SceneColorCopy 折射材质 / SingleLayerWater 折射不采样主 SceneColor;FXAA 用 MSAA 替代;PostOpaque Extensions 不调用。
  6. PSO 一致性GetSubpassHint()(预缓存)与 bFullOnePass(运行时)必须保持同一组判定条件;本次已修复 MobileDeferredShadingPass 只判 CVar 导致 GLES 预缓存错误 PSO 的 bug。