
接手发热优化这个活儿之后我养成了一个惯例不管项目画质调得怎么样先打开 profiler 看 bandwidth 曲线。原因很简单在移动端大部分“烫手”问题最后都能归到数据搬运量上而搬运量最大的两个惯犯就是纹理和后处理。这个系列写到第 4 篇前面几篇已经把 CPU 侧的调用优化和渲染参数调整讲得差不多了这篇专门盯这两块 GPU 侧的带宽大户。可能有人要问为什么是这两个你要是也跑过一轮帧率与温升测试会发现其他环节的优化都像小打小闹唯独把纹理压缩换掉、把后处理链路重排一遍温度会肉眼可见地降下来。这篇文章就把这两个环节的原理、算账方法和实战步骤一次性讲透适合在 Unity、UE 上做移动端开发的朋友参考也适合刚接手项目性能优化、不知道从哪下手的同学。1. 为什么说纹理和后处理是发热的“惯犯”1.1 移动端发热的本质带宽就是热量芯片发热这件事拆到底就是一句话有电流通过的地方就有功耗。GPU 内部的 ALU 在运算时耗电数据从 DRAM 搬到缓存再搬进寄存器也耗电而后者比前者贵得多。说得直白一点一个浮点运算消耗的能量可能只有从片外内存读取一个数据消耗能量的几十分之一所以一款移动 SoC 在满载跑图形任务时大量功耗都花在了数据搬运上。用生活里的事来类比你家楼下的快递站就是 DRAM你的桌子就是 GPU 缓存而快递员就是带宽。快递员每跑一趟都要烧汽油哪怕只是送一张纸。渲染一个画面如果到处乱取数据等效于快递员在小区里来回空跑油钱自然高得离谱。发热优化说穿了就是想办法让快递员每趟多捎点东西、少跑几趟。这个认知决定了优化的优先级。很多新手一上来就追 shader 里那几条复杂的 sin、pow 指令想把 ALU 占用打下来结果温度曲线纹丝不动。其实大多数情况下真正把 SoC 顶到高温的是内存带宽被打满而带宽大户就在纹理采样和后处理这两块。这俩为什么“惯犯”属性这么强因为它们天生就是高搬运量的结构。1.2 纹理一次采样背后是一条完整的数据搬运链纹理贴图是渲染里最基础的数据源。一个 PBR 材质模型上一个像素的最终颜色往往要读基础色贴图、法线贴图、粗糙度贴图、AO 贴图甚至还有阴影图、光照图。每读一张硬件都会从 DRAM 把数据搬进 cache再从 cache 搬进纹理单元。场景里同时可见几十张贴图的时候这个画面一帧的数据搬运量就是几百 MB 起步。我经常跟团队里的同学说看纹理优化不要只盯“内存占用”那是静态指标真正惹祸的是“采样带宽”。比如一张 2048×2048 的 RGBA32 贴图内存里是 16.8MB如果一帧里被 alpha 测试、主渲染、阴影渲染轮流采了 10 遍理论搬运量就是 168MB。换到 ASTC 6x6 之后单张 2MB同样采 10 遍只有 20MB 上下。这就是为什么换压缩格式往往比砍几个 draw call 效果来得猛因为它是从根源上压缩了每一次搬运的体量。再叠加一个现实因素移动端 GPU 的 cache 比 PC 小得多纹素命中率低时搬运量会成倍放大。很多开发者用 PC 上跑得好好的逻辑直接扔到手机上结果发热到无法握持其实就是 PC 无限大的 cache 把采样的低效掩盖了手机上一裸奔就露馅。1.3 后处理全屏读写来来回回搬运自然翻倍后处理的全名是 post-processing也就是在场景渲染完成后对整张画面再做的一次二次加工。当相机输出完主场景引擎会得到一张整屏的 render target然后叠加各种效果泛光、景深、环境光遮蔽、抗锯齿、色调映射……每一个效果基本都是把这一整张画面读进来处理完再写回另一张 RT。这里有一个很容易被忽视的点一次全屏 pass 的数据搬运量不是屏幕分辨率乘以每像素字节数而是还要乘以 2——因为要先读一遍再写一遍。如果后处理链里叠了 6 个独立效果等于整张画面被来回搬了 6 趟。屏幕越大、RT 精度越高这个数字就越吓人。而且“后处理”不是游戏领域独有的概念。做目标检测的同学都知道YOLO 模型跑完前向推理还得出框、做 NMS 后处理这同样是一堆数据在内存里排序、筛选做三维重建的同学用 OpenMVS 这类开源算法生成纹理贴图时也要做大量后处理校正甚至数控加工领域的 CAM 软件里Hypermill 五轴后处理要处理坐标转换、插补格式、进给率校验这些环节。可以发现凡是叫“后处理”的环节都处在“主流程已经算完、需要对结果做精加工”的位置天然带着大量数据在走。处理得好是画龙点睛处理不好就是发热元凶。2. 纹理优化把每张贴图的“体积税”降下来2.1 压缩格式选型ASTC 不是玄学是算术移动端纹理优化第一步永远是选对压缩格式。现在主流安卓和 iOS 设备基本都支持 ASTC它比老的 ETC2 灵活得多可以从 4x4 到 12x12 选块大小对应的码率从 8BPPbits per pixel一直降到 0.89BPP。块越大压缩率越高但画质损失也越明显。我把常用档位整理成了下面这张表方便直接抄作业ASTC 块大小码率2048×2048 体积常见用途4x48.0 BPP4 MB需要细节的高频贴图、UI 大图5x55.12 BPP2.6 MB高质量角色 Diffuse6x64.0 BPP2 MB常规场景贴图的首选8x82.0 BPP1 MB粗糙度、AO 这类低频贴图10x101.28 BPP0.64 MB大范围地形、墙面12x120.89 BPP0.45 MB纯色块、渐变极少的贴图我的习惯是 Diffuse 用 6x6 起步法线贴图用 6x6 甚至 5x5因为法线信息对压缩噪声非常敏感块太大边缘会出现肉眼可见的“撕裂感”和错误的镜面高光。粗糙度、AO、金属度这类低频图可以放心用 8x8因为它们本来就没什么高频细节。顶替不了的时候还可以给一张贴图指定不同的 mip 级别用不同块大小不过这个动作一般发生在极致优化阶段普通项目先统一设好就行。为什么要这么在意码率因为码率直接决定每帧搬运量。还是用上面的数字2048 的 RGBA32 是 16.8MBASTC 6x6 是 2MB一次采样省掉 14.8MB 的搬运。如果一帧里这个贴图被采样 20 次那就是省了近 300MB。一秒钟 60 帧省掉的带宽就是 17GB/s 级别。这个数字放在哪一台手机上都是可以改变温度档位的存在。2.2 别省 mipmap内存省了带宽会加倍还回来我接手过不少项目美术为了省内存把贴图的 mipmap 勾选去掉了。这种做法在 PC 上问题不大但在移动端等于按下了发热的加速键。原因在于没有 mipmap 的贴图在远处物体和倾斜表面采样时纹素复用率极低GPU 只能被迫把高分辨率的原始贴图一遍遍从 DRAM 往 cache 里搬cache miss 率一路飙升。加了 mipmap 之后硬件可以根据当前像素与纹理坐标的距离自动选择合适的分辨率级别。远处的山用 64×64 的 mip 就够不用去读 2048 的原图近处的细节用高分辨率 mip保证清晰度。这样一来每次采样搬运的数据量大幅下降带宽压力也直线走低。我见过一个场景只是把所有贴图补上 mipmapGPU 带宽就降了 20% 以上温度降了 2 到 3 度。顺带提一句各向异性过滤AF也会增加采样次数移动端一般开到 4x 或 8x 就够了没必要像 PC 那样拉满 16x。如果你发现某个场景突然变热可以先查一下 AF 参数是不是被谁偷偷调到了 16x这种小事经常被忽略。2.3 源贴图处理OpenMVS 这类工具导出的贴图别直接塞引擎很多项目里的贴图资源不是美术手工做的而是通过程序化生成或者三维重建算法得到的。比如 OpenMVS 这类开源多视图重建算法会把几百张照片投射成一张或几张巨大的纹理贴图输出通常是 PNG 甚至 TIFF分辨率动辄 8192 甚至更高。这种贴图直接导入游戏引擎用是灾难性的。首先PNG/TIFF 没有 GPU 硬件直接支持的压缩格式运行时会被解压成 RGBA32内存和带宽都爆炸。其次重建出来的 UV 往往非常分散纹理走样严重没有生成 mipmap 的话采样远处时画面全是噪点。正确做法是拿到这种贴图之后先做光照烘焙或 AO 效果合入再统一转成 ASTC、生成 mipmap、关掉 sRGB 的异常标记最后再进引擎。这里面最容易踩的坑是“看起来还行就直接用”。OpenMVS 出的图在建模软件里放大看很细腻但那是软件视图在帮你做降采样真正丢到引擎里GPU 是拿原始分辨率去采样再加上没有压缩带宽和发热都压不住。别问我怎么知道的都是前人踩出来的坑。3. 后处理优化每一趟全屏搬运都得算清楚3.1 后处理开销公式一次全屏 pass 要搬多少数据要优化后处理先把账算明白。一次全屏 pass 的数据搬运量公式很简单单 pass 搬运字节数 屏幕宽度 × 屏幕高度 × 每像素字节数 × 2为什么乘以 2因为要读 RT 一遍再写回目标 RT 一遍。假设游戏跑在 1920×1080 分辨率使用 RGBA16F 的 HDR 缓冲区每像素 8 字节那一次全屏 pass 的搬运量是拦截式子1920 × 1080 × 8 × 2 ≈ 33.2 MB如果你有 6 个后处理效果每个都是独立的全屏 pass那总搬运量约 200MB。这还只是一帧。60 帧每秒就是 12GB/s 的带宽光后处理一项就把带宽占掉一大截。更别说有的效果还要做降采样、多次模糊迭代实际数字能翻两三倍。所以后处理优化的核心其实是一个字省。省 pass 数量、省每次 pass 处理的数据量、省不必要的 RT 精度。方向对了温度自然会降。3.2 半分辨率与合并 Pass两个最实用的降载手段第一个手段是降分辨率。人类视觉对亮度、色彩的空间频率敏感度有限很多后处理效果根本不需要全分辨率去做。泛光、环境光遮蔽、景深的散景模糊这些低频效果都可以在 1/2 甚至 1/4 分辨率的 RT 上完成最后再双线性插值升采样回全屏。这就是“半分辨率后处理”的基本思路。实际操作中我会把后处理链拆成两段必须全分辨率的部分色调映射、抗锯齿边缘修复、色彩校正留在最后其余能降采样的效果全部扔进低分辨率链。像 Bloom 这类需要多级降采样再升采样的效果本身就是在低分辨率上迭代的。这么一改同样的效果堆叠带宽占用大概能砍掉 40% 到 50%。第二个手段是合并 Pass。很多后处理效果可以写进同一个 shader一次遍历同时算完。比如颜色校正和色调映射可以合成一步泛光的上采样和最终叠加可以合并有些抗锯齿算法的边缘检测和混合也可以一步完成。减少 pass 数量本质就是减少“读一次、写一次”的过程。现在引擎里都有 Render Graph 之类的机制能自动帮你合并和裁剪后处理 pass但我还是建议在立项期就想清楚别等后期优化时再靠工具帮忙擦屁股。工具能省掉的是冗余的 pass省不掉的是设计时就过重的效果栈。3.3 后处理取舍别让“电影感”烧掉电量后处理是最容易“加着加着就过量”的东西。美术同学看到 Bloom 好看再调高一点策划觉得景深有电影感要求全局常开TA 又想加个 SSAO 增强立体感。结果就是一条后处理链上挂了十几个 pass在高端旗舰机上勉强能跑到了中端机上温度直接起飞。做取舍的时候我习惯用“收益/带宽”来给每个效果打分。泛光对画面表现影响很大但可以用半分辨率加约束迭代次数来控制成本SSAO 属于收益中等、开销很高的效果移动端尽量别全局开只给特定场景或者关键角色附近开小范围版本景深如果只是 UI 上的按钮提示直接用美术静态图替代千万不要拿真正的景深后处理去跑。这个思路在别的领域同样适用。做 YOLO 推理时NMS 这种后处理如果不去裁剪候选框的数量、不去调整置信度阈值端侧芯片的耗电也会明显上升做 CAM 的人聊 Hypermill 五轴后处理时也会反复强调要精简不必要的辅助运动、优化坐标变换的先后顺序因为每多一条冗余指令在真实机床上就是多出来的时间和磨损。后处理在任何领域都是精细活做加法容易做减法才是本事。4. 发热问题的排查工具与实战记录4.1 定位带宽大户先用工具量化再动手优化发热优化最忌讳的是凭感觉改代码。今天觉得这个 shader 长了明天觉得那个贴图大了改来改去温度一点没降还把自己搞得很累。正确顺序一定是先量化再动手。我自己常用的工具组合如下Android 平台用 Android GPU Inspector 查看 DRAM 带宽、GPU 频率、CPU 侧的提交耗时。Mali GPU 项目还可以用 Mali Offline Compiler 静态分析每个 shader 的带宽和寄存器压力。iOS 平台用 Xcode 自带的 Metal System Trace 和 GPU Frame Capture能直接把一帧里每个 pass 的带宽占用和 cache miss 比例导出来。引擎层面Unity Profiler 的 GPU 时间段、Frame Debugger 的 RT 创建和 pass 记录UE 里则用 RenderDoc 抓帧后看每个 pass 的读写了多少数据。工具输出的数字比任何感觉都管用。我见过一个案例团队一直以为是角色 shader 太复杂导致发热结果一抓帧发现真正吃带宽的是半透明 UGUI 界面一套 UI 图集没压缩每个界面来回重叠draw call 不多但搬运量巨大。这种问题靠肉眼盯着画面看是永远看不出来的。4.2 一次“温度下降 8°C”的优化演练拿一个我参与过的 MOBA 类项目场景举例。当时的目标是把温升从 48°C 左右压到 42°C 以下机型是一台主流骁龙旗舰。测试条件固定亮度 50%开启飞行模式不充电同一局战斗跑 15 分钟记录机身最高温度。第一轮优化前整场景贴图大量使用 RGBA32后处理链条是 Bloom 景深 SSAO TAA 色调映射全分辨率执行。实测 GPU 平均带宽大约 42GB/s15 分钟温度 48.6°C。第一轮只做贴图压缩全部 Diffuse 转 ASTC 6x6法线转 ASTC 6x6UI 大图转 ASTC 5x5。带宽从 42GB/s 降到约 31GB/s温度降到 45.8°C。这个数据告诉我们贴图压缩是最划算的第一步。第二轮做后处理重构Bloom 和 SSAO 改到 1/4 分辨率景深降采样到 1/2再把色调映射与非锐化掩膜合并成一个最终 pass。带宽从 31GB/s 降到 23GB/s温度降到 43.2°C。第三轮做产品层面的取舍把全局 SSAO 改成只在英雄附近小范围启用远处的环境光遮蔽直接烘焙进光照贴图。带宽降到 20GB/s 左右温度稳定在 42.5°C 上下。整个过程整理成一张变化表优化阶段主要动作GPU 带宽15 分钟峰值温度基线RGBA 贴图 全分辨率后处理链42 GB/s48.6°C第一轮贴图转 ASTCUI 大图压缩31 GB/s45.8°C第二轮后处理半分辨率 合并 pass23 GB/s43.2°C第三轮限制 SSAO 范围光照烘焙兜底20 GB/s42.5°C你看没有哪个动作是玄学全是在带宽计算表上做减法。数据摆在那里团队里的美术和策划也更容易接受因为他们能看到每次改动到底换来了几度降温。4.3 容易忽略的隐藏陷阱与低垂果实优化做多了有些坑是每次都会碰到的单列出来分享给各位。第一UI 的 Overdraw 和半透明叠加。UGUI 的每个 Image 都是一次全屏矩形采样如果 UI 图集没压缩、层级又多一块界面的带宽消耗可能比一个 3D 场景还高。检查的时候别只看 draw call要跑一轮 overdraw 可视化。第二粒子系统贴图常常是漏网之鱼。美术做特效时喜欢直接拖一张 1024×1024 的透明通道 TGA记得统一转成 ASTC 并开启 mipmap能省非常多。第三法线贴图压缩噪声导致高光异样。有时候美术会抱怨“压缩之后画面脏了”一急就退回 RGBA32。这种时候不要硬刚换 ASTC 5x5或者单独把法线的蓝色通道放到更高精度通常能同时保住画质和带宽。第四后处理 RT 格式选错了。RGBA16F 是 8BPPRGBA8 是 4BPP如果你在一个低频效果上用了全 HDR 的 RT等于浪费一半带宽。能上 RGBA8 的地方绝不用 RGBA16F。最后再说一个测试方法的坑真机对比温度时一定要锁亮度、关自动亮度、开飞行模式、拔掉充电线。手机亮度和充电状态对机身温度的影响极大不控制变量的话你做了一堆优化测出来温度反而升高会严重误导后续判断。最后再分享一个小技巧发热优化这个活儿做久了我的体会是纠结某个效果该不该留、某个贴图该压多狠之前先老老实实把 profiler 里的带宽一栏看明白。纹理和后处理之所以是“惯犯”不是因为它们本身罪恶滔天而是它们离数据搬运的源头最近稍微松一点就把带宽撑爆。把它们的账算清楚了项目的温度基本不会太离谱。另外还想补一句很多团队优化到最后发现收益已经到头了那是因为只在 GPU 侧打转。纹理与后处理剪完下一步可以往骨骼权重、顶点数据流、阴影贴图更新频率这些方向扩展思路是完全一样的。这个系列如果后面有机会我会把阴影这块的带宽账也拉出来算一算。