
手机发热的排查很多人第一反应是“Shader 写重了”“粒子放太多了”但拉出性能数据一看真正吃掉带宽的大头往往不在计算的复杂度上而在两个最不起眼的地方纹理读取和后处理全屏 Pass。这个系列写到第 4 篇前面聊的执行层优化大多是教你怎么把“算”的代价降下来这篇开始话题要转向“搬”的代价——从内存把纹理数据搬进 GPU、把后处理中间结果搬进搬出帧缓冲每一步都是发热的隐性来源。理解了这两条搬运链路再回头看发热问题很多优化方向会自动浮出来。1. 发烫的本质不是算得多而是搬得勤1.1 为什么一次内存访问比一次计算贵这么多我最早对“搬运量”这个概念有体感是在用平台分析工具盯总线带宽曲线的时候。GPU 的浮点算力这些年涨得飞快但内存带宽的增速远远跟不上。一个移动 GPU 核心做一次浮点乘加的能量开销可能只有几次比特寄存器操作的量级可一旦发生显存或 DRAM 访问单次开销直接高出一到两个数量级。你写一千行 ALU 指令可能还比不上一个纹理缓存未命中、回 DRAM 取数据烧的电多。这句话听起来反直觉但它就是移动端发热优化的底层逻辑。发热本质上来自功耗功耗主要由动态翻转产生而数据搬运意味着总线、缓存、存储控制器、DRAM 一堆硬件同时在高频工作。所以一个片段的发热程度不该只看它有多少计算量更该看它每帧从 DRAM 读了多少 GB 的数据、往 DRAM 写了多少 GB 的数据。1.2 为什么偏偏是纹理和后处理“涉事最多”游戏画面里的数据搬运大致分三类几何数据、纹理数据、像素数据。几何数据每一帧的量很有限一个场景几百万三角形顶点属性加起来也就在几十 MB 量级纹理数据就完全不一样了一张 2048×2048 的 RGBA8888 贴图原样读一遍就是 16MB而一个场景里往往挂着几十上百张贴图像素数据则主要被后处理放大——每做一次全屏特效整张目标纹理都要被读一遍、写一遍。纹理负责“搬进来”后处理负责“搬来搬去”。这两个东西叠加在一起轻轻松松就能吃光 SoC 带宽预算的 40% 以上尤其是在高分辨率手机上。所以我说它俩是“搬运量最大的两个惯犯”一点不冤枉。1.3 一个常见的误判GPU 占用率高不等于着色器复杂很多人看到 GPU 占用率 90% 多第一反应是“Shader 太复杂了砍指令”。但有一种情况很迷惑你把 shader 的 ALU 压得极低GPU 占用率还是纹丝不动。这时候要警惕瓶颈根本不在运算单元而在纹理/带宽单元——GPU 有一大堆运算单元在空转等数据回来。判断方法很土但很有效在分析工具里把时钟频率固定分别压低 ALU 频率和总线/纹理频率观察帧率跟随哪边变化。跟谁走瓶颈就在哪。大部分发热项目我测下来都是后者占多数尤其是那些贴图没有压缩、后处理 Pass 数量又多的项目。2. 纹理的搬运账本格式、mipmap 与采样器2.1 纹理格式决定了每像素的“搬运单价”纹理带宽的公式很简单采样次数 × 每像素占用 bit 数 × 像素面积。公式里最容易动手脚的就是每像素 bit 数。一张贴图是存成 32bpp 的 RGBA8888还是压成 4bpp 的 ASTC 8×8同样一次采样带宽差了整整 8 倍。移动端纹理格式的选择优先级我一般这么排纹理格式每像素 bit压缩类型适用场景RGBA888832无压缩UI 小图、需要极致清晰度的资源RGB565 / RGBA444416无压缩老平台兜底勉强能看ETC24~8有损压缩Android 平台的兼容兜底ASTC 4×48有损压缩高质量压缩需求ASTC 6×6约 3.56有损压缩平衡画质与带宽ASTC 8×82有损压缩大尺寸、低细节纹理我接手的大多数发热项目贴图还清一色是 RGBA8888。导出资源时把“Texture Format”设置成 ASTC 或 ETC2这一步做完项目的总线带宽常常直接掉一半。注意ASTC 在高通和 Mali 平台都支持iOS 也原生支持打包时按平台分开选格式不要一套纹理想通吃。2.2 mipmap 不只是防闪烁更是带宽救星很多美术对 mipmap 的理解是“让远处的东西不闪烁”确实是这样但更大的意义在带宽。一张没有 mipmap 的贴图在屏幕上缩得很小的时候采样器依然要去读原始尺寸的纹理数据缓存命中率极低有 mipmap 之后硬件会直接挑一个合适的小层级来采样纹理量瞬间掉几个数量级。实测下来一个地形场景把主贴图全部开启 mipmap 后纹理相关带宽可以降到原来的三分之一左右。远处山体本来要读大尺寸原图生成 mipmap 后实际读的可能是一张 64×64 的小图这个差距对发热非常友好。代价是内存占用增加大约三分之一。但对于运行时发热用多 1/3 的存储换 2/3 的带宽节省这笔账对绝大多数项目都划算。唯一要注意的是 UI 图不要开 mipmap——UI 通常按原始分辨率 1:1 绘制开了 mipmap 只会浪费内存还可能让文字发虚。材质上的法线贴图也建议开近景质量不受影响。2.3 采样器设置里的隐藏开销同样的贴图采样器的过滤方式不同带宽也会明显浮动。Bilinear 一次采样取 4 个 texeltrilinear 在 mip 过渡区要取 8 个各向异性过滤更夸张8× 的各向异性在某些 GPU 上等效于把采样次数抬高到接近 16 次。这不是说不能开而是要分清对象。大面积地形、地面这类贴图各向异性开 4×8× 画质提升明显带宽也可控但如果你整个场景几百个物体全部盲目开 8×那就是在给总线加戏。另外还有一个容易被忽略的点Wrap 模式。Repeat 模式在采样时对纹理边界需要额外的取模运算和 cache 行为Clamp 在某些平台上更友好。如果纹理本身不需要无缝平铺尽量用 Clamp能省一点是一点。这些细节单个看都是几 MB 的量但架不住贴图数量多。2.4 纹理上传和动态纹理是隐形搬运工除了 GPU 主动采样还有一类搬运容易被忽略CPU 往 GPU 上传纹理数据。特别是 UI 图集动态重建、视频纹理更新、RenderTexture 回读每一次 CPU→GPU 的数据拷贝都在消耗带宽。Graphics API 里的纹理上传接口容易写出“每帧整图上送”的代码实际上一次 UI 图集重建可能就要传 10MB 以上的数据。优化思路是先看更新区域很多引擎支持区域更新只传脏矩形部分再考虑降频更新比如 UI 动画帧率限制到 30Hz最后再看纹理格式动态纹理也尽量用硬件压缩格式避免 CPU 侧软解。3. 后处理的搬运账本每一次全屏 Pass 都在烧钱3.1 算一笔账五段后处理链路的带宽账单后处理之所以贵是因为它短短几毫秒内把整张 RT 来回读写好几遍。以 1080p 为例一张 RGBA16F 的 HDR RT 是 16.6MB读一次写一次就是 33MB。一次 Bloom 效果从提取亮部、降采样若干级、再逐级上采样到最终合成加起来十几次全屏操作这就是 500MB 级别的搬运量。跑 60fps光一个 Bloom 就能吃掉 30GB/s 带宽而主流移动 SoC 的总带宽预算也就 2060GB/s 这个量级。这也是为什么很多游戏一开后处理就发热降频。不是不该开而是不该用“桌面端思路”在手机上一层层铺 Pass。移动端的后处理第一原则是尽量减少整屏读写次数第二原则是能低分辨率就低分辨率。3.2 RT 切换为什么比预想更伤在移动 GPU 的 Tile-Based Rendering 架构下每一次 Render Target 切换都可能导致当前 Tile 数据被冲刷回内存。这意味着“切 RT”这个动作不仅是把绑定的纹理换一下还可能把之前渲染的半成品数据 flush 出去下次再切回来还得重新加载。所以优化后处理的隐藏手段之一是减少中间 RT 的来回切换。把同一 RT 上要画的东西集中在一起画完再切到下一个 RT后处理链路上别一会儿用 A 一会儿用 B能 ping-pong 的就 ping-pong别开第三个 RT。具体到操作层面别为每一个小功能单独建一张 RT能用一张 RT 复用多种目的就绝不复用一张。3.3 合并 Pass 的艺术合并 Pass 是我在优化项目里最常做的事。以手机端常见的 Color Grading Tone Mapping Vignette 三个效果为例桌面端标准实现是三个 Pass先查 LUT 调色再 tone map再画暗角。但移动端完全可以在一个 Fragment Shader 里串起来——先读取 LUT 颜色再执行 tone mapping 函数最后叠加 vignette 的透明度一次全屏采样全搞定。同理Bloom 的降采样和上采样阶段也能合并优化。例如从 1/2 分辨率降到 1/4 时不必单独出一次 Pass可以在一个 shader 里同时做两个级别的降采样输出到两张不同的 RT上采样和最终的 Bloom 合成也可以写进同一个 shader这样至少省下 46 次全屏 Pass。类似这样合并后后处理链路的带宽往往能降低一半以上。代价是单个 shader 变复杂但只要没有动态分支移动 GPU 对这种线性逻辑还是比较友好的。3.4 分辨率取舍半分辨率不是洪水猛兽模糊、泛光这类效果对分辨率的要求远没有最终场景那么高。Bloom 的模糊部分放到 1/4 分辨率做面积极小带宽变成全分辨率的 1/16肉眼几乎察觉不到差异。景深模糊同理。我自己的习惯是分清效果类型依赖像素级细节、可能包含文字或边缘的比如描边、边缘检测保持全分辨率低频大区域效果Bloom、DOF、Glow一律半分辨率甚至更低。Vignette、色差这类效果本来就是在最终输出上叠加颜色偏移全分辨率做一下也不贵但能合并到 Tone Mapping 那一个 Pass 里就绝不单独拉一个 Pass。3.5 HDR 颜色格式选择后处理中间结果的格式也直接影响带宽。RGBA16F 一张 RT 16.6MBR11G11B10F 只要 4MB带宽差距 4 倍。对 Pixel 精度要求不高的效果尽量选紧凑格式。很多项目的 Bloom 提取其实不需要 Alpha 通道RGB 半浮点就足够只有做后期抗锯齿或深度相关效果时才需要额外通道。把 RT 格式从 RGBA16F 换成 R11G11B10发热改善非常明显肉眼几乎无损。4. 实测链路从 Frame Debugger 到平台 Counter4.1 抓“搬运惯犯”的标准流程我排查发热时的第一站永远是先看整体 Pass 顺序和 RT 创建情况。Unity 的 Frame Debugger 或 UE 的 RenderDoc 都可以快速列出每一帧做了多少次全屏 Pass每张 RT 的尺寸和格式以及有没有反复切换同一个 RT。具体步骤可以照抄抓一帧列出所有 RT看数量、尺寸、格式。超过 5 张中间 RT 的项目后处理必然有压缩空间。在 RenderDoc 的 Texture Viewer 里看每个 Pass 实际绑定了哪些纹理特别关注那些动不动就来一次全屏采样的 Pass。再做一次“排除法”关掉后处理效果逐项打开每开一个效果就记录一次温度和帧率变化。哪一项变化最明显它就是主要嫌疑人。再用平台级工具看带宽Adreno 平台用 Snapdragon ProfilerMali 平台用 Arm Mobile StudioiOS 用 Xcode 的 Metal System Trace。这些工具能直接给出总线和纹理单元的真实负载。4.2 一个典型的发热项目优化前后对比之前接手过一个 AR 展示项目30fps 跑五分钟机身发烫严重用户反馈非常集中。拉数据一看很典型的“两个惯犯”同时作案纹理方面所有贴图都是 RGBA8888个别贴图没开 mipmap地面贴图开了 16× 各向异性。后处理方面后处理链有 7 个 PassBloom 全分辨率跑中间结果全是 RGBA16F。第一次优化只做了三件事把所有贴图转成 ASTC 6×6地面各向异性降到 4×Bloom 降到 1/4 分辨率并合并了其中 3 个 Pass。效果非常直接指标优化前优化后总线带宽占用约 14GB/s约 5GB/s帧率稳定性掉帧频繁基本满帧持续运行温度48℃41℃峰值功耗4.8W3.5W看起来改动不小其实全是低垂果实。后来我又做了一轮更细的把 Bloom 的 upsample 和最终合成并进同一个 shader把 ToneMapping 和 ColorGrading 合并又把 6 个 RT 减到 3 个。这轮改动带宽只又降了 1GB/s但 RT 数量下降对引擎的 GC 压力和内存占用都有帮助。4.3 关键指标怎么看才不被误导只看 FPS 是最容易误判的。一个 60fps 的项目可能带宽已经接近饱和只是画面简单尚未爆发。我一般强调测试项目里记录这几项带宽占用率看总线/内存控制器负载这是发热的核心指标。GPU idle / stall 比例如果超标说明在处理单元等数据带宽瓶颈实锤。温度曲线而不是瞬时温度5 分钟持续运行后是否稳定比峰值更能说明问题。功耗后台开功耗计看单位帧功耗这是最终体检报告。平台工具生成的带宽数字单位都是 GB/s记得换算成“每帧搬运量”更直观。比如 60fps 下总带宽 10GB/s意味着每帧搬运了 166MB 数据换算一下就知道哪些 Pass 该砍了。5. 优化落地时的取舍与避坑5.1 纹理压缩并不等于画质毁灭很多团队不敢压缩贴图是被早期 ETC1 的糊和色偏吓坏了。ASTC 的画质控制要好得多尤其是 6×6 及以上配合合适的压缩质量参数绝大多数纹理在手机屏幕上看不出明显区别。但有几个特殊情况要单独处理带 UI 文字的贴图压缩后文字边缘容易出“脏”的感觉这部分可以保持无压缩或做特殊二值化处理。法线贴图直接用 ASTC 压容易让法线方向偏掉要用带法线重构的压缩通道或者选择专门的法线压缩格式。渐变色天空盒低 bit 压缩后容易出现色带可以配合 dithering 或提高压缩块大小来缓解。比较稳妥的流程是先用压缩工具批量预览拉几个典型场景截图对比没人能一眼看出来再全量切换。5.2 后处理合并的副作用要提前防合并不是“把所有效果塞进一个 shader”就万事大吉。太长的 shader 会导致寄存器压力某些 GPU 上反而让帧率恶化不同效果的中间结果如果都要在高精度下保留为了合并而合并反而浪费带宽。我实践中的合并原则是三条没有依赖关系的效果才能串行合到一个 pass合并后的 shader 里不要出现动态分支一经出现就拆开中间不需要的高精度变量就换半精度。合并后务必在低端机上回归测试因为寄存器溢出这类问题往往低端机先爆。5.3 设备分级比一刀切更可靠纹理压缩方案和后处理强度都应当按设备分级。高端机上全开 Bloom 加景深用户体验很好中端机开到半分辨率低端机干脆关掉后处理用简单的颜色调整代替。这种分级不是“阉割”是保证设备不烫手、不长续航焦虑的必要手段。我这里有个简单的分级参考设备档位纹理处理后处理高端机ASTC 6×6各向异性 8×全分辨率 tone map 1/2 分辨率 Bloom中端机ASTC 8×8各向异性 4×半分辨率后处理合并情绪类效果低端机ETC2/ASTC 极致压缩关 mipmap 的高级特性只保留最简 tone mapping5.4 别忘了驱动和引擎版本的差异最后提醒一个很现实的问题同一份 ASTC 纹理、同一条后处理链在不同 GPU 驱动和引擎版本上的表现差异可能非常大。有的驱动对 ASTC 解码效率更高有的引擎在 RT 切换上有额外开销。所以优化做完之后不能只在一台真机上验证至少要在两个不同 GPU 品牌的机器上跑一遍带宽数据才能确认收益是普适的。我自己踩过一次坑在 Adreno 上优化效果很好结果跑到 Mali 设备上发热反而更严重反复排查发现是 ASTC 解码单元在 Mali 上负载更高换回部分纹理 ETC2 才平衡。这也说明带宽优化永远是平台相关的工作离开真机数据谈优化方案就是纸上谈兵。回到开头那句话——发热问题多数情况下不是“算不过来”而是“搬不过来”。纹理和后处理就是搬运量最大的两个环节把它们的账本理清项目功耗就已经赢了大半。下一次再收到“手机发烫”的反馈别急着砍画质先去看看谁在搬货。