移动端发热优化:纹理压缩与后处理Pass的带宽治理 📅 发布时间:2026/9/14 20:33:19 👁 浏览次数: 这系列文章写到第4篇前面聊过CPU侧的调度、GPU的负载均衡、资源的生命周期今天要聊的是我做了这几年发热优化之后最想按着头让所有人注意的两个环节纹理和后处理。在我的实测数据里这两个家伙常年霸占“单帧搬运量最大”的前两名而且往往是并行出现——美术资源没管好后处理又叠了一堆全屏特效整块SoC的内存带宽直接被吃穿手机不发烫才怪。先说一个容易被忽略的事实移动端GPU的发热大头往往不是“算力”而是“搬运”。芯片里ALU算一笔数用的能量比起从DRAM里把一个纹理像素搬进GPU高速缓存便宜得多。数据搬来搬去才是真正的能耗黑洞。纹理采样、后处理Pass读写RenderTarget本质上都是在做搬运工搬得越多发热越明显。所以这篇就是围绕“怎么少搬数据”来展开的干货为主数值都是我真机测过的量级可以直接套用。1. 为什么纹理和后处理是“搬运惯犯”1.1 带宽才是发热的核心内耗很多做Unity或者UE优化的同学第一反应是砍DrawCall、砍三角形数量、砍Shader复杂度这些当然有用但在移动端很多时候真正卡住性能的不是GPU执行单元算不过来而是内存带宽喂不进去。拿Mali或者Adreno的架构来说GPU从DDR内存读取纹理、读写帧缓冲都需要通过内存控制器而这个控制器的带宽是固定的访问延迟也比GPU内置缓存高一个数量级。每次纹理采样如果命中不了Cache就要从主存搬一块数据进来每跑一个全屏后处理Pass至少要读一次整张RenderTarget、写一次整张RenderTarget。这些搬运量累加起来就是SoC发热的主要来源。我拿一个1080p项目做过粗算一次RGBA16F的全屏Pass读写一次大概要搬1920×1080×8字节×2次操作约等于33MB。如果一个后处理栈有10个Pass那光后处理每帧就要搬330MB60帧每秒就是近20GB的搬运量。手机DDR的峰值带宽看着很高但实际能分给GPU的往往打个对折再对折这个数字已经很恐怖了。再叠加上纹理采样发热不失控才怪。1.2 纹理和后处理各自的“搬运账单”纹理这边的搬运量主要取决于三件事纹理格式、纹理分辨率、采样命中率。一张2048×2048的RGBA8888贴图一次完整采样如果全miss就是16MB数据从内存里走一遍。如果场景里有几百张贴图单帧切来切去搬运量分分钟破百MB。后处理那边更直接每一个全屏Pass都要把整张屏幕当成纹理读进来再输出到一张新的RT。Pass越多、RT分辨率越大、RT格式越“胖”搬运量就线性上涨。更隐蔽的是很多后处理还自带降采样、升采样、多级Blur每一级都是独立的全屏读写做一次Bloom搞五个Pass轻轻松松。所以这两个东西被叫做“惯犯”一点不冤一个是资源源头带来的持续搬运一个是管线末端叠加的倍增搬运。后面两章就分别讲它们怎么治。2. 纹理从“贴图”到“压缩包”把每次采样的体积压下来2.1 纹理压缩选型ETC2还是ASTC别再用RGBA8888裸奔我第一次接手发热项目进Scene一拉资源列表差点没坐稳一堆UI贴图、场景贴图全是RGBA8888连法线贴图也是未压缩的。美术说“PC上这效果没问题啊”问题是移动端的带宽和PC差了十倍不止。你这个贴图在PC上只是多吃一点显存在手机上每一次采样都可能是压垮带宽的稻草。移动端正确的做法是只保留两种主流压缩格式优先ASTC兼容性兜底用ETC2。ASTC是ARM推的但Adreno、Mali、PowerVR这些主流移动GPU都支持块大小可以从4×4到12×12任意选压缩比非常灵活。ETC2作为ASTC的兜底画质上不如ASTC精细但胜在兼容老设备。我自己的选型原则是这样的UI和需要清晰细节的贴图用ASTC 4×4或者6×6漫反射、法线这类次要看细节的用ASTC 8×8大块的地形、天空和背景用ASTC 10×10甚至12×12。这样做的理由很简单块越小每个像素分到的bit越多画质越好但带宽也越高块越大省得越狠但细节糊得也越明显。需要你根据贴图在屏幕上实际占据的大小去权衡而不是一刀切。这里顺带提一个很多团队会踩的坑法线贴图用ASTC压缩时部分设备上会出现明显的带状噪声尤其是光滑材质高光区域。我后来都是给法线贴图单独走ASTC 4×4或者6×6并且压缩完以后专门在真机上拉高光看一圈再放行。2.2 mipmap不设的代价采样发散导致Cache Miss和额外带宽很多朋友会觉得mipmap只是“防远处闪烁”用的开不开影响不大。实际上它对带宽的影响极其夸张。如果没有mipmap当摄像机拉远之后一个纹理像素在屏幕上缩到小于一个屏幕像素显卡为了算出一个点的颜色需要把周围一大圈texel都采样进来做过滤采样区域大Cache命中率就直线下降搬运量成倍往上翻。这也是为什么有些项目“运行久了越来越烫”其实和你视角拉远拉近有直接关系。开了mipmap之后GPU会直接选一块已经预过滤好的低分辨率纹理采样面积小、命中率高而且负责过滤的纹理单元也不会满载。代价是多占约33%的纹理内存但换来的带宽节省经常是数量级的这笔账怎么算都值。记得有一次做优化场景是开放大地图地面和山体贴图都开了压缩但没开mipmap中端机上跑起来功率直接飙到6W多。后来我写了个资源检查工具把所有不带mipmap的贴图都扫出来美术全部切到“Generate Mip Maps”同一段路径功耗降了将近1.5W。这就是搬运量的威力。2.3 纹理分辨率和布局别让“高清资源”成为默认习惯还有一项搬运量刺客是纹理分辨率虚高。很多项目资源规范写着“贴图最大2048”结果美术为了过审连一个不起眼的木桶都出2048甚至4096的贴图。场景里几十个这样的资源一叠加理论上限当场爆炸。我建议做一次资源资产审计把每张贴图在屏幕上占据的最大像素数拉出来看。如果一张贴图实际显示区间只有256×256那给它出2048×2048的贴图就是纯纯的浪费。移动端的通用策略是UI图标类控制在256或512以内道具和角色部件512到1024场景大件如建筑墙面最高1024地形用大块tileable贴图做平铺不要靠整张超大贴图。纹理集这块也要留意。很多项目为了减少DrawCall喜欢把散贴图打成一个大的图集图集本身没有错但注意别把各种半透明UI和无透明场景物体混在同一张图集里否则为了兼容就会被迫用RGBA格式带宽直接翻倍。2.4 常用纹理格式带宽与画质对比我整理了一份常用的移动端纹理格式对照表方便你在方案评审时直接拍板格式每像素带宽画质适用场景备注RGBA88884字节最高仅在UI关键区域、动态生成纹理移动端尽量少用ETC2 RGB0.5字节较好兼容性兜底老Android设备Alpha需要ETC2 RGBAASTC 4×41字节好UI、精细贴图、法线贴图兼容性和画质平衡ASTC 6×6约0.44字节良好场景漫反射、次要贴图我项目里主力格式ASTC 8×80.25字节一般地形、天空、大色块材质适合大面积低频细节ASTC 12×120.11字节较差极远背景、噪声类谨慎使用可能有色块从这张表可以直观看到ASTC 8×8相比RGBA8888带宽直接降到十六分之一画质在手机上肉眼差距非常有限但功耗差距天壤之别。所以我在项目里一直要求“默认压缩例外解压”解压要走审批流程。3. 后处理能少跑一趟就少跑一趟能跑一半范围就跑一半3.1 后处理的带宽账一次全屏Pass到底搬了多少数据后处理最迷惑人的地方在于每一个Pass看起来都很“轻”只是全屏画个三角形跑个Shader但架不住数量多。我之前算过一笔账1080p的RGBA16F RT一次Pass读写就是33MB左右的搬运量即便换成RGBA8也是16MB上下。一个项目后处理栈如果有八个十个Pass那加起来就是几百MB一帧这个量级已经比场景所有纹理采样的总和还高了。所以后处理优化的第一原则是减少全屏Pass数量而不是优化单个Pass的Shader。Shader里多几条ALU指令真不心疼少一次全屏读写才是实打实省带宽。做后处理方案评审时我只看一个数这一帧里所有后处理Pass累计读写RT多少次、每次RT多大、什么格式。三个数乘起来就是后处理总搬运预算超了就要改方案。3.2 半分辨率与降采样模糊、Bloom、SSAO这些效果的真需求后处理里有一类效果天然不需要全分辨率比如Bloom的模糊链、SSAO、DOF的CoC模糊、体积光、阴影软化等。它们属于“低频信息”类效果做在半分辨率甚至四分之一分辨率上肉眼几乎分辨不出差别但带宽直接降到1/4甚至1/16。我在项目里的固定做法是Bloom的降采样链全部跑在1/4分辨率上从全屏RT提取亮部之后后续的各级模糊和升采样都基于1/4分辨率进行直到最后一步合成时才升回全屏。SSAO直接用1/4分辨率采样半径和法线信息够用就行。DOF的模糊层也控制在1/2或1/4分辨率视景深范围而定。这里有个易错点降采样本身也是一次读写所以不能为了省带宽而无限加“降采样升采样”的次数。正确的做法是尽可能把多个需要低分辨率的后处理效果合并到同一条降采样链上共用同一张低分辨率RT而不是每个效果单独降一次、升一次。我自己优化过的一个项目就是Bloom和SSAO共用了一张1/4的HDR RT把两套独立链合并成一套后处理总带宽直接砍掉40%。3.3 合并与重组Pass合并、FrameBuffer Fetch/Subpass避免反复倒腾除了降分辨率第二个大杀器是Pass合并。Unity的Post Processing栈、URP自带的Volume很多时候一个“效果”是由好几个Pass拼出来的比如Bloom的亮部提取预模糊、颜色分级色调映射、抗锯齿最终输出。这些Pass如果一个个分开跑每多一个Pass就多一轮全屏读写。移动端还有一个更省带宽的玩意儿叫FrameBuffer FetchiOS的Apple A系列和部分Mali上叫SubpassOpenGL ES里可以用shader_framebuffer_fetch扩展。它的核心思想是在同一块RenderTarget上连续做多个效果第二个效果可以直接从当前像素的framebuffer里拿上一个效果的数据而不需要先把上一个结果读出来再写进去。这样可以把多个Pass合并成一个Pass带宽直接少好几轮。Unity里可以通过自定义RenderPassFeature或者直接写Graphics.Blit的替代方案来用效果非常明显。我举一个典型例子原来项目里的后处理顺序是“Bloom → 颜色分级 → 色调映射 → 抗锯齿(TAA/MSAA) → UI合成”每个都是独立Pass。优化后把颜色分级和色调映射用Subpass合并Bloom的亮部提取和第一级降采样合并抗锯齿和最终输出合并整个链从原来的12个全屏Pass压到7个总带宽降了接近一半发热数据立刻好看很多。3.4 后处理顺序和格式的细节HDR RT别随便开能LDR就LDR再聊一个经常被忽视的细节RT格式。很多后处理效果实际上不需要HDR或者只有某个阶段需要HDR。RGBA16F比RGBA8大一倍如果整个后处理链都用16F跑带宽直接多一倍。我的建议是把HDR的部分限制在Bloom提取亮部、色调映射之前的范围内一旦做完色调映射后续效果全部切到RGBA8的LDR RT上跑带宽立刻减半。后处理顺序也很关键顺序不同临时的RT格式要求就不同。比如Vignette、色差这种本质上就是查表加乘色放在LDR阶段做完全没问题而Bloom、SSAO这些必须要在色调映射之前因为它们需要的是线性HDR信息。所以规划后处理管线时要把“必须HDR的效果”和“可以LDR的效果”分成两拨中间用一次色调映射隔开。这样一来后处理链里只有前半段用胖格式后半段用瘦格式总搬运量会好看很多。另一个和格式相关的坑是MSAA。很多项目为了抗锯齿开了4x MSAA每帧要写四倍的framebuffer数据搬运量直接翻几倍。移动端我建议优先用TAA或者后处理AA替代实在要用MSAA就只在最关键的场景物体上开不要在UI和后处理阶段继续带MSAA RT。4. 怎么量化“搬运量”profiler 和算账公式4.1 算清楚每帧的纹理后处理带宽优化这件事最怕的就是凭感觉。我一般定优化方案之前会先手动估算一遍当前每帧的搬运量确保后面改完了有对比基准。算公式很简单就是“读写次数 × RT大小 × 格式字节数”。比如一个1080p的RGBA16F RT一张是1920×1080×8字节≈16.6MB一次Pass要读一次写一次就是33MB如果这个Pass还额外采样了几个噪声贴图或深度RT把这些采样的数据量也算进去。纹理侧的估算更粗犷一些把每帧内大概率会被采样到的纹理列个清单用“贴图分辨率 × 格式每像素字节数 × 每帧预估采样次数”大致算一下。这个数不一定准但能帮你发现有没有离谱的资源。比如一张1024×1024的RGBA8888纹理全tiling加载一次就是4MB要是场景里挂了20张这类纹理光一次全tiling就是80MB已经顶一个中型后处理链了。把这些数填到一张表里跟项目设定的目标带宽一对比该先优化谁、优化到什么程度一目了然。4.2 工具链Unity Profiler / Android GPU Inspector / RenderDoc 怎么抓带宽估算归估算真机数据才是最终裁判。我在项目里用得比较多的几个工具Android GPU InspectorAGI这是目前Android端最靠谱的GPU计数器工具能直接看到每个DrawCall读了多少纹理字节、写了多少RT字节。用它定位“哪张纹理或者哪个Pass搬运量最大”比看CPU端Profile直接多了。推荐先用它跑一轮把带宽Top5的Pass和纹理拉出来。Unity Profiler的GPU模块能看Pass数、RT切换次数虽然拿不到精确的DRAM带宽但能看到哪些Pass占用时间最长。配合Frame Debugger查看每一个Pass到底在干嘛。RenderDoc主要用来查纹理绑定和管线状态比如确认某张贴图实际绑到了哪个阶段、有没有被压缩、mipmap是否完整。它不适合测带宽但适合排查“为什么会搬这么多”。ARM的Streamline和Adreno Profiler分别针对Mali和Adreno设备可以读到DRAM带宽的实际占用比例用来做整机带宽水位监测。这个数据非常关键我经常拿它验证“优化后带宽是否真的降下来了”。4.3 预算分配给“搬运量”设上限之后发热就好控制了Heat优化做到后期本质上是给整个SoC做功耗预算管理。CPU有CPU的功耗预算GPU运算有运算的功耗预算内存搬运有内存搬运的功耗预算。我习惯在项目早期就把“单帧总搬运量”设一个上限比如中端机不超过600MB高端机不超过1GB超过就砍效果或者砍资源绝不拖到后期再处理。这个预算要拆到具体环节纹理总采样预算、后处理总Pass预算、网格顶点数据预算、UI和文字预算。每一个环节都设一个报警线超过报警线就在构建日志里直接标红提醒。我甚至写过一个小插件在打包时自动扫资源凡是纹理没压缩、没有mipmap、分辨率超标的直接输出警告清单发给美术和TA。有了预算和自动化检查发热问题基本不会拖到发版阶段才爆发。5. 实战排查记录三个“搬运量爆炸”的真实案例5.1 案例一大世界地形贴图没开压缩mipmap没生成有一次接手一个大世界项目进入场景后中端机迅速发烫即使站在一个点位不动功率也稳定在5.5W以上。我第一反应就是看纹理。用AGI一拉带宽发现地形和植被层采样贡献了超过60%的DRAM带宽。检查后发现地形使用了大量2048分辨率RGBA8888草和泥的过渡贴图而且贴图导入设置里Mipmap是关闭的。修复方案很直接所有地形贴图转成ASTC 8×8生成Mipmap部分超大贴图降到1024。改完之后同一个点位功率降到3.8W发热从“烫手”变成“温热”。这里的关键不是某一个动作而是“压缩mipmap尺寸合理”三件套一起上差距才会这么明显。5.2 案例二Bloom堆了五层全屏PassRGBA16F反复读写另一个项目是做二次元风格的画面要“通透发光”于是美术和后端在Bloom上疯狂加料提取高亮、预模糊、横向模糊、纵向模糊、升采样、合成前前后后六个全屏Pass而且全部跑在RGBA16F全分辨率上。一帧后处理总带宽接近200MB配合角色身上的动态辉光整机功耗直接起飞。我把Bloom改成了1/4分辨率链提取高亮和第一级降采样合并成同一个Pass后续模糊和升采样全部在1/4 RT上完成最后只有合成一步在全屏分辨率做。改完后Bloom的视觉效果我自己在真机上对比动态场景里基本看不出差别但后处理总带宽从200MB降到了70MB上下发热改善非常明显。后来我把这个配置做成了项目级后处理预设任何新场景直接套。5.3 案例三全屏后处理栈里插了一个SSAO直接把中端机拖垮这个案例特别典型后处理栈看着不多就Bloom加ColorGrading加ToneMapping加Vignette跑在iPhone上的时候都还好一换某款中端Android帧率直接掉到30。逐个Pass排查后才发现后处理链里不知道什么时候被插入了一个全分辨率的SSAO而且是独立执行输入深度、输出AO、再跟主画面合成白白多了两个全屏Pass。SSAO这种环境光遮蔽效果本身就属于低频信息放到1/4分辨率完全够用。优化后我把SSAO降到了1/4分辨率并且把它挪到了Bloom降采样链之后和Bloom共用一张低分辨率RT省掉了独立的AO Pass。最终中端机上帧率回到57帧温度也从接近44°C降到39°C。这让我深刻意识到后处理链里每多塞一个效果都要先问一句它真的需要全分辨率吗5.4 常见问题速查表问题典型原因快速解法远处纹理闪烁带宽飙升没开Mipmap资源导入设置打开Generate Mip Maps贴图体积大、发热高大量RGBA8888按场景换成ASTC 6×6或8×8后处理链Pass数量爆炸每个效果独立全屏Pass合并Pass、共用降采样链Bloom糊且发热全分辨率/多级模糊Bloom跑1/4分辨率链SSAO/DOF拉低帧率全分辨率计算降到1/4和Bloom共用RT后处理带宽翻倍RGBA16F跑全程色调映射后切RGBA8MSAA抗锯齿导致带宽翻倍4x MSAA全屏改成后处理AA或TAA最后再分享一个小技巧优化发热问题别只盯着GPU占用率或者帧率看把“带宽使用率”和“内存控制器负载”一起盯着这两个数据往往比帧率更早暴露问题。纹理和后处理这两个惯犯只要你在带宽维度上把它们按住手机温度基本就能控制住了。我自己这几年做优化的体会是动手之前先算清楚每帧要搬多少数据动手之后用profiler验证有没有真降下来只要这个闭环跑起来发热优化就不会是玄学。