移动端GPU发热优化:纹理采样与后处理的带宽治理实践 📅 发布时间:2026/9/15 8:10:18 👁 浏览次数: 这个系列写到第四篇我越来越确认一件事手机发烫很多时候真不是 GPU 算力不够用而是数据在路上堵了、烧了。你调出 GPU 的带宽计数器看一眼就会发现真正把热量拉起来的几乎总是纹理采样和后处理这两个环节——它们才是搬运量最大的两个惯犯。这里的纹理指的是每帧采样用的各种贴图、光照图、遮蔽图后处理则是 Bloom、TAA、色差、锐化这类全屏特效。它们的共同点是都要从显存里把大块数据搬进 GPU算完再搬回去。而移动端 GPU 最大的功耗黑洞恰恰就是这套搬运。这篇我来把这两个惯犯的作案手法、优化手段和实测工具串一遍希望能帮你在发热优化这条路上少走几段弯路。1. 为什么偏偏是这两个惯犯1.1 发热的根源其实不在计算在搬运很多人一谈发热优化第一反应就是“把 shader 里的复杂计算减一减”。这个方向没错但经常抓不到大头。移动端 GPU 和桌面 GPU 不太一样它的并行计算单元其实已经很强了真正卡住功耗的往往是数据搬运。手机的内存带宽受限于 LPDDR 的位宽和频率而且 DRAM 读写一次要付出远高于寄存器访问的成本。数据从 DRAM 搬到 GPU 内部的缓存和寄存器这个动作本身就要烧掉不少电。打个比方超市货架上的商品很便宜算力就是货架但把货物从仓库一件件运到货架这个物流成本才是大头。GPU 里的纹理采样、渲染目标读写本质上都是物流。纹理采样要把纹素从 DRAM 搬到纹理缓存再送到 shader 里后处理要把整张渲染目标读进来、算完再写出去。这两个动作的搬运量往往比所有顶点处理和像素计算加起来还要大几倍。实际开发里我也踩过这个坑。做一个半透明草的渲染shader 里写了不少光照代码结果手机照样烫。后来抓了带宽数据才发现草叶纹理没有压缩、没有 mipmap每次采样都在拉高分辨率的纹理带宽直接爆了。计算量反而小得可怜。从那以后我做优化都先看带宽再谈计算。1.2 纹理是怎么把带宽偷走的纹理采样的带宽消耗取决于三件事纹理格式、采样次数、缓存命中率。先说格式。一张 1024x1024 的 RGBA8 纹理不压缩的情况下一个纹素占 4 字节整张图就是 4MB。如果场景里有 20 张这样的贴图光存在那里就 80MB。渲染时 shader 每采样一次就要从 DRAM 里取回一坨纹素具体取多少取决于过滤方式。更麻烦的是没有 mipmap 的情况。一个远处的小物体可能只占画面几十个像素但采样时仍然要读最高分辨率的纹素。GPU 的纹理缓存容量是有限的这种“大纹理小采样”的用法缓存命中率极低基本每次采样都要回 DRAM 搬数据。一旦纹理分辨率上到 2048、4096搬运量就会成倍增长。我见过一个项目角色身上的主贴图和法线贴图都是 4096 的 RGBA8四个人同屏加上环境贴图和阴影贴图一帧光纹理读取就吃掉了几百 MB 带宽。后来把贴图压成 ASTC 6x6、加上 mipmap带宽瞬间降了一档手机背面从烫手变成温热。纹理优化对发热的帮助往往就是来得这么直接。1.3 后处理为什么也这么能吃带宽后处理是另一个大胃王。它的问题在于全屏 pass 要对整张渲染目标做一遍读写。1080p 的分辨率大约是 207 万像素如果用 RGBA16F 格式每个像素 8 字节那么读一遍就是 16MB写一遍又是 16MB一次全屏 pass 就是 32MB 的搬运量。看起来还能接受但后处理很少只有一个 pass。以常见的 Bloom 为例亮度提取、降采样、横向模糊、纵向模糊、升采样、合并加起来十个 pass 是常事。十次全屏读写那就是 300MB 以上的搬运量在 60fps 下每秒就是 18GB/s 的带宽。手机 DRAM 的带宽顶多几十 GB/s一个 Bloom 就能吃掉一大半发热简直板上钉钉。更别提现在的游戏动不动就堆效果Bloom、TAA、色差、景深、锐化、暗角每个效果都至少两三个 pass。各个效果之间还需要中间缓冲GPU 不断在这些缓冲之间搬运数据。所以后处理优化几乎总能带来明显的发热改善因为它砍掉的都是纯粹的搬运量。2. 纹理优化把物流成本压到最低2.1 压缩格式选型ASTC 和 ETC2 怎么选纹理压缩是降低带宽和内存最直接的手段。移动端绕不开的两个格式是 ETC2 和 ASTC。ETC2 是 OpenGL ES 3.0 之后的基础格式所有现代手机都有硬件解码支持压缩比固定为 4:1也就是每纹素 4bit还支持带 alpha 的 ETC2EAC。如果你的目标设备很老、或者需要兼容面很广ETC2 是保底选择。ASTC 则要灵活得多它用 block 压缩每块可以是 4x4 到 12x12 的纹素阵列对应的压缩比从约 8bit/texel 到不到 1bit/texel。iOS 从 A7 之后、Android 上 Mali 和 Adreno 的主流 GPU 也都支持兼容性在近些年已经不是问题。ASTC 的优势是同样压缩比下画质通常比 ETC2 好尤其是颜色渐变和法线贴图。我自己在项目里的选型经验是这样的纹理类型推荐格式压缩比说明UI 图标、文字贴图ASTC 4x4 或 ETC2EAC4:1 ~ 8:1UI 细节多压缩太重会糊角色 DiffuseASTC 6x6约 5.3:1兼顾画质和带宽场景法线贴图ASTC 6x6 或 8x85.3:1 ~ 9:1法线对颜色精度没 Diffuse 敏感大型地形、天空盒ASTC 8x8 或 10x109:1 ~ 15:1细节靠程序化扰动补足选格式的时候我有一个额外提醒同一张纹理不要在 Android 和 iOS 上各搞一套不同格式做资源管线的成本很高。现在两家都支持 ASTC统一用 ASTC 是省心的方案。真遇到完全不支持 ASTC 的机器走 ETC2 兼容层画质差一点也比带宽爆炸强。2.2 Mipmap 是白送的带宽优化器Mipmap 经常被忽略因为它看起来只是“增加了内存占用”很多人不愿意开。但 mipmap 其实是一个带宽优化器它的价值远超那点内存开销。当物体离相机远时GPU 会自动选择更小的 mip level采样时读进来的纹素数据量大幅下降。比如一个 1024 的纹理在远处可能只采样 64x64 的 mip带宽直接降到十六分之一。没有 mipmap 的话远距离采样高分辨率纹理不仅带宽浪费还会因为采样频率不足产生闪烁和摩尔纹。画质和性能两头吃亏。所以我的默认规则是任何用于 3D 场景的纹理都开 mipmap只有纯 UI 平面贴图可以不开因为 UI 不会做透视缩小。各向异性过滤也要提一嘴。它能大幅改善斜视角度的纹理清晰度但会增加采样次数。移动端没必要拉到 16x我用 4x 或 8x 比较多画质和带宽能同时兼顾。另外如果发现某个纹理在近看时还算清楚远处却闪得厉害别急着加各向异性先检查是不是 mipmap 没开完整、或者 mip 偏置设置过大。2.3 图集、流向和其他省流量姿势纹理图集是减少切换和缓存 miss 的手段。把多个小贴图合并到一张大图集里可以避免 GPU 频繁在不同纹理对象之间切换缓存命中率会明显提升。不过图集也有坑压缩格式一个 block 覆盖一片纹素如果两个小图隔得太近压缩时会互相污染边缘。做图集时一定要给每个子图预留 padding至少 4~8 个像素。纹理流送streaming是一个更进阶的手段。在开放世界或者大地图项目里根据相机距离动态加载和卸载纹理可以显著降内存和带宽。但流送对加载管线、内存管理、异步 IO 的要求比较高小型项目不一定划算。我自己做过一次流送踩了不少坑最后发现收益确实有但是需求如果不强烈优先把压缩格式和 mipmap 做好性价比更高。另外一个小技巧不要随便在运行时把纹理从 RGBA 转成压缩格式那会在加载阶段烧一次极大量的 CPU/GPU 搬运有时候比省下的带宽还夸张。压缩应该在离线资源管线里做运行时直接加载压缩好的纹理数据。3. 后处理优化全屏特效省流的几个硬招3.1 先给 Bloom 算笔账空谈无用我们直接把 Bloom 的带宽算出来。假设 1080p、RGBA16F每像素 8 字节一个标准的金字塔降采样 BloomPass操作读取写入合计Brightness亮度提取1080p: 16.6MB1080p: 16.6MB33.2MBDown 1/2降采样到 540p16.6MB4.1MB20.7MBDown 1/4降采样到 270p4.1MB1.0MB5.1MBDown 1/8降采样到 135p1.0MB0.26MB1.3MBBlur 135p横纵模糊0.52MB x20.26MB x21.6MBUp 1/4升采样到 270p0.26MB1.0MB1.3MBUp 1/2升采样到 540p1.0MB4.1MB5.1MBUp 1/1升采样到 1080p4.1MB16.6MB20.7MBCombine合并回主画面33.2MB 左右16.6MB49.8MB把这张表加起来一个还算克制的 Bloom 链也要搬接近 150MB 的数据。在 60fps 下每秒就是 9GB/s。如果项目里再用上 RGBA16F 做多个中间缓冲后处理链一起奔着 20GB/s 去手机不烫才怪。所以优化 Bloom 的每一块拼图都等于直接给发热做减法。3.2 降低分辨率、合并 Pass、换用 Compute降低后处理分辨率是最立竿见影的手段。把全屏链从 1080p 降到 540p带宽直接降到四分之一画质损失却往往不明显尤其是模糊类效果。你可以把 Bloom、景深这类低频效果整个放在半分辨率下做最后再升采样回全分辨率。大多数情况下观众根本看不出来区别。合并 Pass 是第二招。很多后处理链有大量可以合并的步骤比如亮度提取和第一次降采样可以合成一个 pass在降采样的同时算出高亮区域。多个模糊步骤也可以合成到同一个 pass 的代码里用更复杂的 UV 偏移一次完成横纵两个方向的采样。尽量做到每个像素只被读一次、写一次而不是读十次、写十次。第三招是换用 Compute Shader。传统的全屏 pass 每道都要绑定渲染目标读写都在 DRAM 上。Compute Shader 则可以把降采样、模糊、合并的中间结果放在共享内存LDS里完成减少对全局内存的读写次数。我实测过一个 Bloom从全屏 pass 改成 Compute Shader 后带宽减了大概 40%发热也能感觉到明显改善。不过 Compute Shader 对开发能力和驱动兼容性要求更高小团队量力而行。3.3 格式、Stencil、Bayer 那些容易被忽略的钱后处理缓冲的格式选择也有很多门道。RGBA16F 是最常用的但并不是所有数据都需要 16 位浮点。比如 Bloom 中间结果直接耗在 R11G11B10F 上每像素只有 4 字节带宽直接减半画质损失微乎其微。TAA 的历史缓冲就更讲究用 R11G11B10 配合抖动采样能省下不少带宽。带 alpha 的 RT 要看清 alpha 通道到底用不用用了就没办法省但这部分数据量也是可以计算的。Stencil 优化在移动端很实用。很多后处理只需要作用在局部区域比如水面倒影、角色周围的特殊光效。用模板缓冲把需要处理的区域标记出来fragment shader 里直接 early-z/stencil 丢弃避免在全屏像素上做无效读写。虽然驱动不见得能完全省下 DRAM 流量但至少能省掉 shader 执行。Bayer 抖动是一个比较取巧的手段。用 4x4 Bayer 矩阵把后处理的效果随机打散采样时只有部分像素参与计算视觉上因为加入了高频噪声反而看不出明显的分辨率下降。有些手游的 Bloom 就用这种方案盯着看会觉得星星点点但动态画面下观感不错。它并不是银弹但在带宽极度吃紧的机型上是值得尝试的降载手段。4. 把“搬运量”量化工具链与实测方法4.1 常用调试工具与带宽读数空口说某个东西吃带宽不如直接看数据。移动端我常用的工具大致这么几类Unity 自带 Frame Debugger、RenderDoc、Xcode GPU Frame Capture、高通 Adreno GPU Profiler、Arm 的 Mali Offline Compiler。Frame Debugger 能看到每个 draw call 和 passRenderDoc 能抓取渲染资源灌入分析Xcode 的 GPU counter 能给出 buffer 读写量。要看纹理带宽注意区分“纹理读取”和“渲染目标读写”这两个计数器。很多工具默认只展示渲染目标的数据量容易漏掉纹理采样那部分。在 Adreno Profiler 里要看 Texture Fetch 相关的指标在 Xcode 里找 Texture Read Bandwidth。如果你的平台有厂商扩展权限甚至能直接看到 DRAM 总带宽占用那是最准的。另一个容易被忽略的点工具的 Buffer 或 Render Pass 读取数据通常是指 GPU 向缓存与 DRAM 之间的移动量但具体的缓存命中率也会影响最终 DRAM 带宽。同一份数据在缓存里命中率和 miss 率不同DRAM 流量差好几倍。所以不要只盯着单个 pass 的理论带宽要结合实际运行时的计数器来看。4.2 一个可复现的对比实验我自己的优化流程里一定会跑一个“开关对比实验”。具体来说把一份特定场景导出两个分支A 分支保持原始逻辑B 分支关闭后处理、或者把纹理全部压成 ASTC 高档位。然后在同一台测试机上分别跑完整场景抓两种数据一是 GPU 带宽计数值二是表面温度曲线。有条件的话再抓一下瞬时功耗。这个实验不需要很精密的仪器手机自带的状态页、Perfetto 或者 Snapdragon Profiler 都行。关键是保证两次测试的相机路径、负载时长、亮度设置完全一致。我一般固定跑三分钟取稳定后的平均温度和平均带宽。实测下来压一次纹理格式往往就能让带宽降掉三成后处理从全分辨率改半分辨率温度能掉 3~5 度。这个幅度见过好几次了比在 shader 里抠几条指令明显得多。对比实验的另一个作用是定位“最大搬运点”。把后处理链分步开关每关一步看一次带宽很快就能找出哪一步是真正的大头。可能是某个焦距的深度模糊也可能是某个多层的 Bloom。知道最大搬运点在哪优化就不会乱打。5. 常见问题与排查技巧实录5.1 纹理压缩后边缘发脏、闪烁纹理压缩最常见的问题是边缘脏和半透明区域发虚。ASTC 和 ETC2 都是块压缩块与块之间交界处会出现颜色涂抹和 Alpha 失控。常见原因是把带有细微 Alpha 的贴图塞进了 RGB 压缩格式或者图集里子图之间的 padding 不足。解决办法有几个第一如果是 UI 图集padding 至少给到 8 像素这是最稳的第二压缩时尽量保留 Alpha 通道使用 ASTC RGBA 或 ETC2EAC第三如果只是边缘脏可以在 UV 上做内缩让贴图边缘不完全贴近模型轮廓。闪烁问题则多半是 mipmap 未开全、或者 mip 偏置过大把 Aniso 过滤降到 2x 或 4x再调节 bias一般能解决。5.2 后处理颜色断层和漏光颜色断层banding几乎在每个后处理项目里都会出现。最常见原因是渲染目标精度不够RGBA8 在暗部渐变的地方很容易出现一圈一圈的色阶。解决办法一是把中间缓冲换成 R11G11B10F 或者 RGBA16F二是加噪声抖动在 8bit 目标上叠加一个微小的 Bayer 抖动视觉上能骗过眼睛。不要指望只用高精度 RT 就能一劳永逸高精度 RT 本身也会增加带宽能省则省。Bloom 漏光也是经典问题。降采样时采样半径过大或者升采样 UV 坐标算错会把高光涂抹到不该出现的方向形成横竖两条亮线。排查时先在低分辨率下看每个中间缓冲确认降采样图像是否符合预期再检查升采样阶段的 UV 是否加上了当前的纹素偏移。有些引擎的 UV 坐标在 NDX 和纹理坐标之间差半像素稍不留神就漏光。5.3 多 Pass 全屏链如何收缩到一碗水多 Pass 是后处理的老毛病。要收缩全屏链最实用的思路是先画一张“全屏调用图”把每个 pass 往纸上列出来标清输入输出缓冲。然后问自己哪些 pass 可以合并哪些可以降分辨率哪些中间缓冲根本不需要保存到下一帧我做一个阴影模糊时发现引擎里竟然有四个全屏模糊 pass每个都读写一次全屏缩放目标合并成两个之后带宽直接降了四成。还有一个值得养的毛病不要在同一个后处理效果里采样过多的纹理特别是采样多个大图。这样会把原本只需一次读写的缓存行忙得不可开交。如果确实需要考虑把不需要的颜色分量压缩或合并进单张纹理。结尾最后说一点私活。做了这么多发烫优化我发现特别管用的是先量化、再优化。别急着猜哪个卡先把带宽计数器拉出来看看纹理采样和后处理到底搬了多少数据。抓到这两个惯犯往往改一下压缩格式、降一下后处理分辨率发热就立刻好转一大截。很多时候你花两天抠 shader 指令效果还比不上一次实打实的带宽削减。希望这篇能帮你少走这几段弯路。