把桌面端的渲染管线原样搬到手机上,几乎必然翻车。原因不是手机 GPU「算力弱」,而是它的架构模型根本不同:手机 GPU 普遍采用 TBDR(Tile-Based Deferred Rendering,分块延迟渲染),把屏幕切成小 tile,在片上高速内存里完成所有像素操作,最后一次性写回主存。这个设计大幅节省了带宽——但代价是「带宽」成了最稀缺的资源,任何导致 tile 内存被刷出的操作都会带来惩罚。
雪上加霜的是,手机还受制于散热与电池:持续运行几分钟后,SoC 会因温度上升而降频(热节流),帧率随之下降。因此移动端渲染优化有两条主线:省带宽与控功耗。本文围绕这两条主线,拆解从架构到落地清单的完整方法。
一、移动 GPU 的架构约束
一句话:移动 GPU 用 TBDR 把像素处理搬进片上内存,省下大量带宽,但任何「中途读回」都会打断 tile 流水,代价极高。
1.1 TBDR 与 IMR 的区别
| 维度 | 桌面 IMR(Immediate Mode) | 移动 TBDR |
|---|---|---|
| 处理方式 | 逐图元立即光栅化 | 按屏幕 tile 分批处理 |
| 像素存储 | 主存中的帧缓冲 | 片上 tile 内存 |
| 带宽特征 | 每像素多次读写主存 | 每像素仅最终写回一次 |
| 依赖/混合 | 直接读写主存 | 需 tile 内解决 |
| 中断 tile 的代价 | 无 | 极高(刷出并重载) |
TBDR 的省带宽原理:一个 tile(如 32×32 像素)的所有绘制都在片上完成,最终才把结果写回主存。如果这一帧里没有中断,主存读写只有「写一次」——理论上比 IMR 省好几倍带宽。
1.2 片上 tile 内存
tile 内存容量有限(几十到几百 KB)。这意味着:
- 渲染目标格式越大,能同时容纳的 tile 越少,甚至无法在片上完成;
- 高精度 HDR 缓冲(RGBA16F)会吃掉大量 tile 内存;
- MSAA 在 TBDR 上相对便宜(采样在片上完成),但采样数越高 tile 内存越紧张。
1.3 为什么移动端带宽是命门
桌面 GPU 有几百 GB/s 带宽,移动 SoC 往往只有 20~60 GB/s,且带宽与 CPU、显示控制器共享。一次 1080p 的 RGBA8 全屏读写就是 8 MB,若后处理链有 10 个 Pass,每个都读写全屏,就是 160 MB/帧——60 FPS 下需要 9.6 GB/s,已占去移动带宽的相当比例。这就是为什么移动端优化的一切都围绕「减少主存流量」。
1.4 主流移动 GPU 概览
| 厂商 | 架构 | 代表 | 特点 |
|---|---|---|---|
| Arm | Mali | 中端 Android 广泛 | TBDR,对带宽极敏感 |
| Qualcomm | Adreno | 高端 Android | TBDR,有独特扩展 |
| Imagination | PowerVR | 早期 iPhone、部分 IoT | TBDR 鼻祖,支持 TBDR+ |
| Apple | Apple GPU | iPhone/iPad | TBDR,Metal 专属 |
四家都是 TBDR(或变体),但细节差异大:Mali 的 tile 大小与 Adreno 不同,能容纳的 RT 数量也不同。这意味着同一份优化在不同机型上收益不同,必须做真机矩阵测试。
二、带宽是第一瓶颈
一句话:把每一字节的主存流量都当成花钱,能省则省——纹理压缩、合并 Pass、Load/Store Op 优化是三大手段。
2.1 带宽来源拆解
主存带宽消耗来源:
1. 纹理采样未命中缓存的部分(最大头)
2. Render Target 的读与写
3. 顶点/索引缓冲读取
4. Uniform / 常量读取
5. tile 内存溢出时的刷出与重载
诊断顺序:先用厂商工具(Arm Streamline / Snapdragon Profiler)确认带宽是否接近上限,再逐项排查。
2.2 纹理压缩
移动端应优先使用硬件支持的压缩格式:
| 格式 | 平台 | 压缩比 | 特点 |
|---|---|---|---|
| ETC2 / EAC | 全平台(GLES 3.0 必备) | 4:1~8:1 | 兼容性最好 |
| ASTC | 现代 Android / iOS | 4:1~8:1 | 灵活块尺寸,质量最佳 |
| PVRTC | 老款 iOS | 4:1 | 已被 ASTC 取代 |
| BC | 桌面 | 4:1~8:1 | 移动端不支持 |
ASTC 支持 4×4 到 12×12 的块尺寸,可在质量与体积间灵活取舍:UI 用 4×4 保质量,远景用 8×8 省显存。若运行时需要回退,可参考 Godot 运行时纹理压缩回退 的降级策略。纹理格式的完整讨论见 纹理采样与 GPU 内存优化 。
2.3 减少 Render Target 切换
每次切换 Render Target 都可能触发 tile 刷出与重载。策略:
- 合并 Pass:把能一次画完的东西合并,减少 RT 切换;
- 避免中间缓冲:能用 tile 内存解决的中间结果,不要落到主存纹理;
- 谨慎使用延迟渲染:G-Buffer 的多个 RT 在 TBDR 上代价高,除非带宽预算充足。
这也是移动端更偏好前向渲染(以及 延迟渲染与 Tiled/Clustered 光照剔除 中的 Forward+)的原因。
2.4 用 Load/Store Op 省带宽
这是 TBDR 上最容易被忽视的优化:
// 只渲染不读回:用 DONT_CARE,避免加载旧内容
VkAttachmentDescription color{};
color.loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR; // 或 DONT_CARE
color.storeOp = VK_ATTACHMENT_STORE_OP_DONT_CARE; // 不需要结果就丢弃
// 中间 Pass 的 RT 若不再使用,storeOp = DONT_CARE 能省一次写回
loadOp = DONT_CARE 告诉驱动「不需要加载旧像素」,storeOp = DONT_CARE 告诉它「结果不用写回主存」。在 TBDR 上,这两个设置能显著减少 tile 内存的刷出与重载。
2.5 顶点与索引优化
顶点带宽在移动端也不可忽视:
- 用索引绘制(
vkCmdDrawIndexed)复用顶点,减少顶点着色器调用; - 顶点格式紧凑:位置用
half、法线用 8/16 位编码、UV 用half,能把一个顶点从 32 字节压到 16 字节以下; - 合理的索引宽度:顶点数 < 65536 时用
uint16索引,省一半索引带宽。
// 顶点属性用紧凑格式(示例:位置 half4、法线 8 位归一化)
// layout(location=0) in vec4 aPos; // half 精度
// layout(location=1) in vec4 aNormal; // 8 位有符号归一化
三、Overdraw 与 Early-Z
一句话:Overdraw 指同一像素被多次着色,在填充率受限的移动 GPU 上是纯粹的浪费,靠排序与 Early-Z 消除。
3.1 Overdraw 的来源
- 不透明物体未排序:远处物体先画、近处后画,被覆盖的像素白画了;
- 透明物体:无法避免重叠,但可通过排序减少;
- 全屏特效:粒子、光晕、UI 叠加层层覆盖。
3.2 Early-Z 与深度预排序
Early-Z(早期深度测试)在片段着色器执行前就丢弃被遮挡的像素。要让它生效:
- 不透明物体从近到远排序(Front-to-Back),让近处像素先写深度,后续远处像素被早退;
- 避免在着色器里写
gl_FragDepth,那会禁用 Early-Z; - 避免
discard,它也会打断 Early-Z(除非用 Conservative Depth)。
3.3 前向排序与不透明/透明分离
标准流程:
1. 渲染不透明物体:按距离从近到远排序 → Early-Z 最大化
2. 渲染透明物体:按距离从远到近排序 → 混合正确
3. 渲染 UI / 全屏特效:最后叠加
把不透明与透明严格分离,是不干扰 Early-Z 的前提。
3.4 遮挡剔除在移动端的价值
移动端屏幕小、视锥窄,遮挡剔除的收益往往比桌面更大——大量物体被近处物体挡住却仍被提交。CPU 侧做视锥剔除 + 简单的遮挡剔除(如 HZB)能显著减少 Draw Call 与 Overdraw,只是移动端更要控制剔除自身的 CPU 开销。
四、分辨率与可变速率着色
一句话:像素数是填充率成本最直接的乘数——降低分辨率或降低着色率,是最立竿见影的优化。
4.1 动态分辨率
动态分辨率(Dynamic Resolution Scaling, DRS)根据实时 GPU 负载调整渲染分辨率:
if (gpuTime > budget * 1.1) scale *= 0.95; // 超预算则降
if (gpuTime < budget * 0.9) scale *= 1.05; // 有余量则升
scale = clamp(scale, 0.5, 1.0);
关键是用中位数 + 迟滞避免振荡,并保持 UI 与文本始终按原生分辨率渲染(否则会糊)。
4.2 可变速率着色(VRS)
VRS(Variable Rate Shading)允许逐区域改变着色率:屏幕边缘、运动模糊区域可以用 1/2 或 1/4 速率着色,中心保持全速。它比 DRS 更精细,因为它能保持几何分辨率不变。
着色率图(Shading Rate Image):
[1x1][1x1][1x1][1x1] 中心:全速
[1x2][1x2][1x2][1x2] 边缘:半速
[2x2][2x2][2x2][2x2] 角落:四分之一速
VRS 对片元着色密集的场景(如复杂 PBR、后处理)收益明显,对几何密集场景无效。
4.3 渲染比例与超分
更低的分辨率意味着更少的像素。除了直接降分辨率,还可以:
- 时间超分(Temporal Upscaling):渲染 2/3 分辨率,用历史帧信息重建到全分辨率;
- 空间超分:用边缘感知滤波放大。
超分与抗锯齿共享时序信息,可参考 抗锯齿全解析 中的 TAA/时序超分部分。
4.4 后处理的移动端策略
桌面常见的后处理链在移动端要精简:
- Bloom:只做 2~3 级金字塔,别做 6 级;
- 色调映射:用廉价的曲线(如 Reinhard),ACES 在低端机可能偏贵;
- SSAO/SSR:尽量关闭或降分辨率,屏幕空间效果的带宽代价高;
- 抗锯齿:优先 FXAA(一次全屏 Pass)或轻量 TAA,MSAA 在 TBDR 上可行但要留意 tile 内存。
原则是:每个全屏 Pass 都要用带宽与功耗数据证明其价值,不能因为桌面有就照搬。
五、功耗与热节流治理
一句话:手机的性能上限不是「跑多快」,而是「能持续跑多久」——功耗与温度决定了可持续性能(Sustained Performance)。
5.1 功耗的来源
移动 SoC 的功耗大头:
| 部件 | 功耗占比 | 优化手段 |
|---|---|---|
| GPU | 30~50% | 降分辨率、降着色率、减少带宽 |
| CPU | 20~40% | 减少绘制调用、优化脚本 |
| 内存/带宽 | 10~25% | 压缩纹理、减少流量 |
| 显示 | 固定 | 降刷新率(60→30) |
GPU 与内存的功耗高度相关——省带宽往往同时省功耗。
5.2 热节流与帧率墙
持续高负载几分钟后,SoC 温度上升触发降频:
前 2 分钟:GPU 满频,帧率 60
2~5 分钟:温度上升,开始降频,帧率 50~55
5 分钟后:稳定在某个「热平衡点」,帧率 40~45
更糟的是帧率骤降(从 60 突然掉到 30)比稳定低帧率体验更差。因此策略是主动限帧:与其让系统降频,不如自己把帧率锁在可持续的水平。
5.3 帧率与功耗的权衡
一个常见的错误是「能跑 60 就跑 60」。对于 UI 类应用、策略游戏,锁 30 FPS 能显著降低功耗与发热,延长续航。推荐做法:
- 菜单/静态界面:锁 30 FPS 甚至更低;
- 游戏内:提供 30/60 选项,或动态在两者间切换;
- 战斗/关键场景:短暂放开到 60,事后回落。
5.4 电池模式与画质降级
系统级信号(低电量模式、温度告警)应驱动画质降级。与 客户端移动端热与电池 中的分级策略一致:
| 状态 | 动作 |
|---|---|
| 正常 | 全画质、目标帧率 |
| 温度偏高 | 降分辨率比例、关闭高成本后处理 |
| 低电量模式 | 锁 30 FPS、关闭阴影/反射 |
| 严重过热 | 最低画质、停止后台渲染 |
这套分级机制需要引擎提供运行时切换画质档位的能力,且切换要平滑(避免一帧内突变)。
5.5 功耗的测量方法
功耗优化不能靠猜,要能测:
- 硬件功耗计:如 Monsoon,最准确,但成本高;
- 系统 API:Android 的
BatteryManager、iOS 的ProcessInfo.thermalState提供粗略信号; - 厂商工具:Snapdragon Profiler / Arm Streamline 可给出功耗估算;
- 间接指标:GPU 频率、温度、电池电流(
/sys/class/power_supply/)。
# Android:读取电池电流(微安)
adb shell cat /sys/class/power_supply/battery/current_now
# 读取热区温度
adb shell cat /sys/class/thermal/thermal_zone*/temp
把功耗与帧率、分辨率一起记录,才能看清「画质—性能—功耗」的三方权衡。
六、移动端渲染优化清单
一句话:把上面的原则落成一份可逐项打钩的清单,是团队协作与回归检查的基础。
6.1 逐项检查表
带宽
[ ] 所有纹理使用 ASTC/ETC2 压缩
[ ] 中间 RT 尽量用 loadOp=DONT_CARE / storeOp=DONT_CARE
[ ] 减少 RT 切换与全屏 Pass 数量
[ ] 避免不必要的读回(readback、getPixels)
填充率
[ ] 不透明物体近到远排序,启用 Early-Z
[ ] 着色器不写 gl_FragDepth、不无谓 discard
[ ] 透明物体远到近排序
[ ] 粒子/特效限制重叠层数
分辨率
[ ] 启用动态分辨率,UI 保持原生
[ ] 评估 VRS 在边缘区域的收益
[ ] 后处理链按半分辨率处理
功耗
[ ] 提供 30/60 帧率选项
[ ] 菜单与静态界面主动限帧
[ ] 响应系统低电量/温度信号做画质降级
[ ] 减少不必要的每帧唤醒(后台停渲染)
6.2 常见陷阱
- 照搬桌面管线:延迟渲染、复杂后处理链在移动端代价过高;
- 忽略 tile 溢出:高精度多 RT 导致 tile 内存不够,被迫刷出,带宽暴涨;
- 动态分辨率振荡:缺少迟滞,画面比例忽高忽低;
- 热节流后仍跑满:不主动降档,体验从「流畅」直接跌到「卡顿」;
- 只测首分钟:冷机测得的帧率不代表持续性能,必须跑 5~10 分钟看热平衡点。
6.3 建立性能预算
移动端开发应从一开始就建立性能预算(Performance Budget),而非事后优化:
目标机型:中端 Android(如 Snapdragon 7 系)
帧预算:33.3 ms(30 FPS)
CPU:≤ 10 ms
GPU:≤ 20 ms
余量:3 ms
显存预算:≤ 512 MB
纹理预算:≤ 256 MB
Draw Call 预算:≤ 300
每个新功能上线前,对照预算评估其开销;超预算就砍画质或降优先级。有了预算,性能问题就从「上线前救火」变成「开发中管控」。
小结
移动端渲染优化的核心可以浓缩成一句话:省带宽、控功耗、稳帧率。
- 架构认知:TBDR 把像素处理搬进片上内存,带宽是最稀缺资源,任何 tile 中断都要付出代价;
- 带宽手段:纹理压缩、合并 Pass、Load/Store Op 优化、减少读回;
- 填充率手段:排序 + Early-Z、动态分辨率、VRS;
- 功耗手段:主动限帧、响应系统信号分级降质、关注持续性能而非峰值。
把这些原则内化成团队的检查清单后,移动端渲染就能从「玄学调优」变成工程化的性能预算管理。若需要跨 API 的选型视角,可进一步了解现代图形 API 在移动端的取舍。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。