11. 手机 GPU 为什么长这样
11. 手机 GPU 为什么长这样
本篇位置:模块二 · 第 11 篇 | 前置:帧缓冲与后处理 | 预计阅读:30 分钟
你已经在用它了
SurfaceFlinger 每一帧都要为每个图层做一个决定(CompositionEngine/src/OutputLayer.cpp):
void OutputLayer::writeCompositionTypeToHWC(HWC2::Layer* hwcLayer,
Composition requestedCompositionType,
bool isPeekingThrough, bool skipLayer) {
auto& outputDependentState = editState();
if (isClientCompositionForced(isPeekingThrough)) {
// If we are forcing client composition, we need to tell the HWC
requestedCompositionType = Composition::CLIENT;
}
// ...
}Composition::DEVICE 是交给 HWC 硬件合成,Composition::CLIENT 是回落 GPU。这套决策逻辑你太熟了:能给 HWC 就给 HWC,给不了才回落。
三个问题:
- "能给 HWC 就给 HWC"——省下的到底是什么?为什么值得厂商专门做一整块硬件来干这件事?
- 手机 GPU 和桌面 GPU 的架构为什么完全不同?它到底为什么要"分块"?
- 前面几篇留了好几个尾巴:MSAA 在移动端反而划算、depth pre-pass 反而可能亏、G-buffer 直接判死刑、后处理 pass 不能太多。这些结论的共同根据是什么?
第 3 个问题是这一篇存在的理由。它们全部指向同一件事。
学完你会什么
- 说清手机上真正稀缺的资源是什么,为什么不是算力。
- 说清 TBDR 是什么、它把哪部分数据关在了哪里、为什么这样能省。
- 说清前面几篇那些"移动端例外"背后的同一条原因。
- 说清 HWC 合成相对 GPU 合成到底省了多少,能自己算这笔账。
- 分清带宽和填充率不是一回事。
1. 手机上真正稀缺的不是算力,是能量
先建立最底层的事实。
一颗手机 SoC 的峰值算力其实相当可观,但它有一个桌面 GPU 没有的硬约束:没有风扇,靠一块金属背板散热,还得塞在几毫米厚的机身里。
这意味着功耗预算(power budget)才是真正的天花板——不是"算得快不快",是"算这么多会不会烫到降频"。
于是问题变成:同样干一件事,哪种做法更费电?
图:几种基本操作的能量代价(对数刻度)。一次 32 位浮点乘加约 4 pJ,一次片上 SRAM 读约 10 pJ,而一次系统内存(DRAM)读约 1300 pJ——是一次乘加的 325 倍,是片上读的 130 倍。
这组数字来自 Mark Horowitz 在 ISSCC 2014 的主题演讲(45nm 工艺),用来说明数量级关系,不代表任何具体 SoC 的实测值。工艺演进会改变绝对值,但"访存远贵于计算、片外远贵于片上"这个数量级差距一直成立,而且随着工艺推进还在拉大。
这条事实推出了移动图形学几乎所有的设计取舍:
算力相对便宜,搬数据非常贵。宁可多算几次,也别多搬一趟。
问题 1 的答案已经在这里了:HWC 省的不是算力(把几张图叠起来这点计算量微不足道),省的是内存往返。第 5 节会把这笔账算出来。
2. TBDR:把中间数据关在片上
现在回答问题 2。
回想第 5 篇和第 6 篇:一个片段要写进帧缓冲,中间会发生什么?
- 读深度缓冲,比一次;通过了就写深度;
- 混合的话还要读已有颜色;
- 最后写颜色。
每个片段,好几次读写。一帧几百万个片段,还有 overdraw——如果这些读写全都走系统内存,按上一节那个 1300 pJ,电池撑不了多久。
桌面 GPU 就是这么干的(叫立即模式,Immediate Mode Rendering),因为它有足够的显存带宽和散热能力。
移动 GPU 换了个思路:把屏幕切成一个个小方块(tile,典型 16×16 或 32×32),一次只处理一块。
图上:立即模式,每个片段的深度和颜色读写都要走系统内存。图下:TBDR,一个 tile 的所有片段着色、深度测试、混合全部在片上 tile 内存里完成,整块算完之后才把最终颜色写出去一次。
关键在于一个 tile 足够小,它的颜色和深度缓冲可以整个放进片上 SRAM。于是:
| 立即模式 | TBDR | |
|---|---|---|
| 深度缓冲读写 | 走系统内存 | 全在片上 |
| 混合时读背景色 | 走系统内存 | 全在片上 |
| 系统内存写入次数 | 每个片段可能好几次 | 每个像素一次(tile 算完时) |
这就是"分块"的全部动机:把最频繁的那部分读写关进片上,只让最终结果走一次系统内存。
TBDR 里的 D 是 Deferred(延迟)——它还会先把一个 tile 里所有几何收集齐、做完可见性判断,再开始着色。这带来一个额外好处:硬件层面的消隐(hidden surface removal),被完全遮挡的片段可以根本不着色,效果类似一个免费的、逐像素的 Early-Z。
3. 前面那些"移动端例外",根都在这里
现在回答问题 3。把前面几篇留下的尾巴一次收掉——它们全部是同一条原因的不同表现。
① MSAA 在移动端反而划算(第 9 篇)
MSAA 要为每个像素存 N 份深度和颜色。在立即模式下这意味着 N 倍的系统内存带宽,非常贵。
在 TBDR 上,这 N 份数据全程待在片上 tile 内存里,resolve(把 N 个采样点平均成一个像素)也在片上完成,只有平均后的结果才写出去。多出来的带宽开销大部分被片上吃掉了,所以 4x MSAA 在移动 GPU 上的代价远低于直觉。
② depth pre-pass 可能是净亏(第 5 篇)
depth pre-pass 的收益是"让被遮挡的片段不进片段着色器"。但 TBDR 的硬件消隐已经在做类似的事了,而且是免费的、逐像素的。
再跑一趟几何意味着整个场景的顶点数据要再读一遍系统内存——那是实打实的带宽。收益被硬件抢走了,代价还在,于是很多移动端场景下它是净亏。
③ 延迟渲染的 G-buffer 直接判死刑(第 10 篇)
G-buffer 要为每个像素存位置、法线、albedo、粗糙度……好几张全屏纹理。这些数据必须写出系统内存、下一趟再读回来,因为第二趟是一个独立的全屏 pass。
一块 1080×2400 的 4 通道纹理就接近 10 MB,四五张 G-buffer 一写一读就是上百 MB 的往返,每帧。移动端带宽预算直接爆掉。
④ 后处理 pass 不能太多(第 10 篇)
每一趟后处理都是"把 tile 内存刷回系统内存 → 下一趟再读回来"的完整往返。pass 的数量几乎直接等于带宽消耗的倍数。
这也是为什么移动端会拼命把多个效果合并进同一个着色器,哪怕代码难看——省下的一趟往返,比省下的那点计算值钱得多。
看出规律了吗?这四条没有一条是关于"算得快不快"的,全部是关于"数据搬了几趟"。
在移动 GPU 上做性能判断,第一个该问的永远是:这个改动让数据多走了几趟系统内存?
4. 算一笔合成的账
回到问题 1,把 HWC 省下的东西量化。
图:1080×2400 的屏幕、每像素 4 字节,一层就是 9.89 MB。5 个图层的情况下:
| 读 | 写 | 合计 | |
|---|---|---|---|
| HWC 直接合成 | 5 层各扫描一次 = 49.4 MB | 0(直接送显,不落地) | 49.4 MB |
| 回落 GPU 合成 | 5 层各读一次 + 合成结果被送显再读一次 = 59.3 MB | 合成结果写一次 = 9.9 MB | 69.2 MB |
回落 GPU 每帧多 19.8 MB 往返(+40%)。按 60fps 折算,多出约 1.16 GB/s 的内存带宽。
(这是个简化模型——真实情况还有 AFBC 压缩、部分更新、HWC 自身的读取效率等因素,但数量级和方向是对的。)
这就是问题 1 的答案:HWC 省的不是算力,是那多出来的一次写和一次读。而按第 1 节的账,内存往返正是最贵的那部分。
厂商愿意为此做一整块专用硬件,是因为合成这件事每一帧都要做、永远不会停。手机绝大多数时间在做的就是"把几个图层叠起来送显"——为一件永远在发生、且瓶颈明确在带宽的事情做专用电路,是笔非常划算的买卖。
你每天在 OutputLayer 里看到的那些"能不能给 HWC"的判断条件(图层数量上限、格式是否支持、有没有旋转、有没有 blur……),本质上都是在问同一句话:这一层能不能不走那趟内存往返。
⚠️ 别搞混
带宽和填充率你都熟,但它们不是一回事:
| 带宽(bandwidth) | 填充率(fill rate) | |
|---|---|---|
| 量的是什么 | 每秒搬多少字节 | 每秒能写多少像素 |
| 瓶颈在哪 | 内存总线和功耗 | ROP 单元数量 |
| 单位 | GB/s | Gpixel/s |
| overdraw 影响它吗 | 间接——多出来的像素要读写 | 直接——overdraw 就是重复填充 |
| 提高分辨率的影响 | 线性增长 | 线性增长 |
| 用小一点的格式有用吗 | 有(RGB565 比 RGBA8888 省一半) | 基本没有 |
填充率是"能写多快",带宽是"搬得动多少"。移动端通常先撞上的是带宽。
一个实用的判断法:如果换个更小的像素格式(或开 AFBC 压缩)就变快了,那是带宽瓶颈;如果减少 overdraw 才变快,那是填充率或着色瓶颈。
回到源码
问题 1:HWC 省的是什么?
省的是内存往返,不是算力。
按图里那笔账,1080×2400、5 个图层的情况下,回落 GPU 每帧多出 19.8 MB 的读写往返(+40%),60fps 下约 1.16 GB/s。而第 1 节说过,一次 DRAM 访问的能量代价是一次浮点乘加的 325 倍——"把几张图叠起来"那点计算量根本不是重点。
厂商做专用硬件是因为这件事每帧都做、永不停歇,而且瓶颈明确在带宽。
问题 2:为什么手机 GPU 要分块?
因为深度和混合的中间读写太频繁,走系统内存扛不住功耗预算。
把屏幕切成小块,让一个 tile 的颜色和深度缓冲整个装进片上 SRAM,所有中间读写就都在片上完成,只有最终颜色走一次系统内存。
问题 3:那些"移动端例外"的共同根据?
全部是"数据搬了几趟系统内存"。
MSAA 的多份数据留在片上 → 便宜;depth pre-pass 要重读一遍几何、收益又被硬件消隐抢走 → 可能净亏;G-buffer 必须跨 pass 落地 → 上百 MB 往返,判死刑;后处理每多一趟就多一次完整往返 → 能合并就合并。
这条判据比记住任何具体结论都有用:在移动 GPU 上评估一个改动,先问"它让数据多走了几趟系统内存"。答案通常就是结论。
自测
1. 为什么说移动 GPU 上「算力便宜、带宽贵」?
因为能量代价差了两个数量级。
一次 32 位浮点乘加约 4 pJ,一次片上 SRAM 读约 10 pJ,而一次系统内存读约 1300 pJ——是乘加的 325 倍、片上读的 130 倍。
而手机没有风扇,功耗预算才是真正的天花板。所以设计取舍永远是:宁可多算几次,也别多搬一趟。
(数字取自 Horowitz ISSCC 2014,45nm,说明数量级用。绝对值随工艺变,但差距一直在拉大。)
2. TBDR 里的 tile 为什么必须"小"?
因为它要整个装进片上 SRAM。
一个 tile 的颜色缓冲加深度缓冲必须能放进有限的片上内存,深度测试、混合这些高频读写才能全部在片上完成,不碰系统内存。
典型的 tile 是 16×16 或 32×32——大了装不下,小了分块本身的开销(每个 tile 都要重新遍历几何列表)又太高。
3. 为什么 4x MSAA 在移动端的代价比想象中低?
因为多出来的那 4 份深度和颜色数据全程待在片上 tile 内存里,resolve(4 个采样点平均成 1 个像素)也在片上完成,只有平均后的最终像素才写出系统内存。
在立即模式 GPU 上,这 4 份数据要走系统内存,就是实打实的 4 倍带宽。
同一个特性在两种架构上代价完全不同——这正是"先问数据搬了几趟"这条判据的价值。
4. 为什么延迟渲染在移动端基本不可行?
因为 G-buffer 必须跨 pass 落地系统内存。
延迟渲染第一趟把位置、法线、albedo、粗糙度写进几张全屏纹理,第二趟用一个独立的全屏 pass 读回来算光照。这个"写出去再读回来"绕不过 tile 内存——pass 之间必须经由系统内存。
1080×2400 的 4 通道纹理接近 10 MB,四五张 G-buffer 一写一读就是上百 MB 每帧的往返。移动端带宽预算直接爆掉。
(有一些针对 tile 架构的变通方案,比如利用 subpass / framebuffer fetch 让第二趟直接读片上数据,但适用面受限。)
5. 一个效果改用 RGB565 之后明显变快了,说明瓶颈在哪?
带宽。
换更小的像素格式减少的是每像素搬运的字节数,不改变像素数量,也不减少着色计算。它能带来加速,说明瓶颈在"搬得动多少字节"上。
对照:如果是减少 overdraw 才变快,那是填充率或片段着色瓶颈;如果是简化 shader 才变快,那是 ALU 瓶颈。
换格式 / 开 AFBC 是最快的带宽瓶颈探针。
小结
| 概念 | 一句话 | 要点 |
|---|---|---|
| 真正的约束 | 功耗预算,不是算力 | 没风扇,烫了就降频 |
| 能量差距 | 一次 DRAM 读 ≈ 325 次乘加 | 算力便宜,搬数据贵 |
| 核心取舍 | 宁可多算,别多搬 | 移动图形学几乎所有结论的根 |
| TBDR | 屏幕切块,一次算一块 | tile 的颜色+深度装进片上 SRAM |
| 省在哪 | 深度/混合的读写全在片上 | 只有最终颜色走一次系统内存 |
| 硬件消隐 | 先收集几何再着色 | 类似免费的逐像素 Early-Z |
| MSAA 反而便宜 | 多份数据留在片上 | 同一特性在两种架构上代价不同 |
| depth pre-pass 可能亏 | 收益被硬件消隐抢走,代价还在 | 多读一遍几何是真带宽 |
| G-buffer 判死刑 | 必须跨 pass 落地 | 上百 MB 每帧往返 |
| HWC vs GPU | 5 层时每帧省约 19.8 MB | 省的是往返,不是算力 |
| 带宽 vs 填充率 | 搬多少字节 vs 写多少像素 | 换格式变快 = 带宽瓶颈 |
延伸阅读
- Mark Horowitz《Computing's Energy Problem》ISSCC 2014 主题演讲(本篇能量数据来源):https://ieeexplore.ieee.org/document/6757323
- ARM Developer《Mali GPU Best Practices》(TBDR 架构下的实践准则):https://developer.arm.com/documentation/101897/latest/
- Imagination《A look at the PowerVR graphics architecture: Tile-based rendering》:https://blog.imaginationtech.com/a-look-at-the-powervr-graphics-architecture-tile-based-rendering/
- Qualcomm《Adreno GPU 开发者指南》:https://developer.qualcomm.com/software/adreno-gpu-sdk/gpu
- AOSP 源码:
frameworks/native/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp