2026/9/18 2:14:49

GPU带宽成移动端发热元凶?纹理压缩与后处理优化全解析

GPU带宽成移动端发热元凶?纹理压缩与后处理优化全解析 发烫优化做了好几轮之后你会慢慢发现一个规律真正让手机变成暖手宝的往往不是那些看起来很复杂的shader也不是场景里的三角形数量而是那些藏在管线深处、每天都在疯狂搬运数据的环节。纹理采样算是其中一个后处理更是另一个。这两个东西在优化工单里出现的频率极高所以我直接把它们拎出来单独写一篇。先说个背景这是“发烫优化系列”的第4篇。前几篇聊了CPU侧的负载拆分和GPU侧的overdraw控制这一篇聚焦在GPU里两个最容易吃带宽、最容易让芯片温度失控的环节——纹理与后处理。如果你正在做移动端渲染优化、Unity/UE项目性能调优或者纯粹被真机发热搞得焦头烂额这篇应该能给你一些可以直接拿去用的思路。1. 为什么纹理和后处理是“搬运量”最大的惯犯1.1 GPU发热的本质功耗来自哪里很多人以为GPU发热是因为计算量大其实不太准确。移动端GPU大部分功耗消耗在“数据搬运”上而不是“数据计算”上。芯片内部有一堆总线、缓存、内存控制器数据在它们之间流动每一次流动都在给芯片加温。总线翻转要耗电缓存命中率低了要访问外部DRAM要耗电带宽吃满了整个内存子系统都在高压运转发热自然就上来了。纹理采样和后处理正好是数据搬运的两个极端典型。纹理采样要把纹理数据从显存或者内存搬到GPU的texture cache再经过采样器送到shader里做计算后处理则是把整张渲染结果从渲染目标RenderTarget里读出来经过一遍又一遍的全屏Pass处理再写回新的渲染目标。每一帧都在搬一秒钟60次全年无休。所以优化纹理和后处理本质上就是在给芯片“减负”把那些不必要的搬运路径砍掉。芯片不搬数据了功耗自然降下来温度也会跟着回落。1.2 带宽计算公式先算算你在搬多少数据要量化搬运量我一般会做一个简单估算。这里有一个很实用的带宽计算公式单帧带宽 渲染目标读写流量 纹理采样流量 顶点数据流量 其他杂项拿后处理举例一个1080p的分辨率RGBA8格式一张RT的大小大概是1920 × 1080 × 4 bytes ≈ 8.3 MB读写一次就是16.6MB如果你用了bloom高光提取水平模糊垂直模糊合成中间还有降采样随便就是四五次全屏RT读写那就是80MB左右的流量。这还没算纹理采样流量。如果再做一次MSAA4x MSAA的resolve过程还要把4倍于屏幕像素的样本数据读出来再合并带宽消耗直接翻好几倍。这些数据全部堆在内存总线上的时候芯片温度想压都压不住。1.3 纹理搬运路径从DRAM到Shader的长途运输纹理数据在GPU里的搬运路径大致是这样的显存/内存 — 纹理缓存texture cache— 采样器 — ALU。每一步都有成本但最贵的是第一跳——从外部DRAM拿数据。纹理缓存的大小有限如果纹理太大、mipmap缺失、采样方式低效缓存命中率就会下降GPU就不得不频繁访问外部内存这时候最费电。后处理也是一样的逻辑。它的问题在于每一个Pass都涉及一次“整张屏幕的读写循环”这是一个不折不扣的带宽黑洞。而且后处理Pass还有个特点越往后叠加消耗越非线性——多个后处理效果如果串在一起做每一层都要把前一层的完整图像搬一遍这个成本是线性叠加的。2. 纹理篇搬得少才能凉得快2.1 纹理压缩格式首选项从ASTC开始纹理优化的第一道关卡是压缩格式。很多人习惯了在PC上直接存RGBA/BGRA纹理到了移动端还是这么干这是个大坑。RGBA8每个像素占用32bit而ASTC 4x4可以做到每个像素占用8bitASTC 8x8只要4bit。同样一张1024×1024的图RGBA需要4MBASTC 8x8只需要1MB搬运量直接节约了75%。下面是几个常用格式的对比格式压缩比说明RGBA8/BGRA81:1不压缩移动端尽量避免ETC2 RGBA4:1老平台兼容性好但压缩质量一般ASTC 4x48:1平衡性最好质量损失小ASTC 6x6约10.7:1质量还可以带宽进一步降低ASTC 8x816:1带宽最低但会有明显压缩痕迹注意ASTC是OpenGL ES 3.0和Vulkan时代的主流选择苹果的A8芯片和绝大多数安卓中高端芯片都支持。如果你的项目还需要兼容比较老的设备ETC2是底线。别在移动端用RGBA8做UI大图尤其是全屏背景图一次采样就要搬好几MB数据。关于纹理压缩的块尺寸选择我的经验是UI和需要锐利边缘的贴图用ASTC 4x4或6x6普通漫反射纹理用6x6Normal Map这种对精度敏感的用4x4因为有channal packing需求大块地形和天空球可以退到8x8。项目里跑一轮真机测试同一张图分别压缩成不同块比大小画面对比一下很快就能定标准。2.2 mipmap不只是画质问题是带宽问题纹理的mipmap链经常被忽略但它直接影响缓存命中率。试想一下一个没有mipmap的地表纹理在近处采样时GPU要读大图在远处采样时还是读大图。远处的像素明明只需要几个texel的信息却被迫读入一大块纹理数据。这相当于你要从图书馆搬一本书到教室只为了看第3页的一句话——为什么不把目录撕下来带着走呢mipmap就是那本“精简目录”。它为纹理生成了从大到小的一系列缩小版本远处的像素采样时可以直接命中小的mip层数据量大幅下降。对于地形、墙体这些大纹理mipmap带来的带宽节省非常可观。开启mipmap很简单但真正要做的其实是合理性检查哪些纹理其实根本不需要mipmapUI纹理、图集中的小元素、需要保持像素精确排布的贴图都不需要。给不需要的纹理开mipmap反而会多占33%左右的额外显存还白白增加了生成时间。2.3 纹理尺寸上限1024大于2048纹理尺寸和发热的关系很直接——尺寸每翻一倍带宽需求翻四倍面积是平方关系。一个2048的纹理单次采样需要的纹理数据量是1024的4倍。所以控制纹理最大尺寸是一个性价比极高的策略。我在项目里常用的一个原则凡是屏幕占比不超过1/4的物体纹理尺寸压到512基本够用全屏或接近全屏的背景、主角特写才用到1024或2048。这个原则直接能砍掉相当一部分内存和带宽压力。透贴和法线贴图尤其要注意尺寸控制这两类纹理在shader采样的开销比普通漫反射更高。如果你做的是Unity项目纹理的Max Size可以在导入设置里改如果做的是引擎侧的底层优化建议在资源导入管线里加一条自动校验超过指定尺寸的纹理直接告警或者自动压缩。让策划和美术在上传资源时就被拦住比事后排查要高效得多。2.4 图集和纹理数组减少“换纹理”这个隐性开销每次切换纹理GPU的纹理缓存都可能要重新加载数据。如果你的模型是这种场景一个角色身上用了6张64×64的小贴图皮肤、衣服、裤子、鞋子、配饰……每切换一次贴图缓存就会被冲一次。对比之下把所有小贴图打成一个1024×1024的图集角色渲染时只需要绑定一次纹理采样的数据量大减缓存命中率也高得多。图集在小尺寸纹理多的项目里效果尤其明显。UI、场景物件、角色服装贴图都适合做图集合并。纹理数组Texture Array是另一个方向它比图集更好的一点是避免了图集相邻区域间的采样漏边问题适合用在地形和多物件合批的场景里。当然图集也不是完全没有代价。图集越大单次纹理绑定时进入传输通道的数据就越大所以图集大小也要控制。一般来说移动端单张图集不要超过2048×20481024×1024是更稳妥的选择。2.5 纹理流送大世界项目的救命稻草如果你的项目是大地图、开放世界类型纹理流送基本是必选项。场景里同时存在的纹理总量远超显存容量不可能一次性都加载进来就必须按需加载、按区域卸载。这里面最核心的就是优先级管理——靠近摄像机的纹理高优先级刚进入视野的纹理高优先级远离视野且长时间不出现的纹理就可以卸载或降精度。纹理流送的难点在于避免“突然加载”造成的卡顿。我的做法是在视锥体的范围基础上外扩一段距离提前做纹理加载同时把每个纹理拆分成多个mip层级依次加载先加载低精度版本保证画面不至于空洞再渐进式加载高精度版本。这和地图的LOD思路本质上是相通的。3. 后处理篇每一趟全屏Pass都在烧电3.1 后处理开销的度量方式别只看帧率后处理优化的第一个问题是度量。很多团队只看帧率但其实帧率对于后处理瓶颈的敏感度不够——因为后处理通常发生在渲染管线的后半段它只影响GPU一小段时间帧率掉得不一定明显发热却会很真实。我更建议用GPU Profiler的Render Pass数据来度量。Unity的Frame Debugger能帮你看到每个后处理Pass的执行时间高通平台的Snapdragon Profiler能给出各Pass在GPU上的耗时和带宽占用。用工具把Pass逐一列出来你会很直观地看到哪一步是“带宽刺客”。有了Pass级别的数据优化优先级就清楚了先砍那些耗时高、效果不明显的Pass再合并小Pass最后再考虑降分辨率。别一上来就整体砍后处理那等于把整个画面质量一刀切了。3.2 动刀顺序砍效果先于降分辨率后处理优化里有两条路线一条是减少Pass数量砍特效、合并效果另一条是降低每个Pass的渲染面积降分辨率。两条路线各有侧重但实际项目中我建议先走砍效果这条路。为什么因为很多后处理特效是可以放在一个Pass里合并计算的。比如Color Grading和Tonemapping如果分开做每个都要读写一次全屏RT合并成一个Pass只需要一次读写带宽直接减半。Bloom和Glow这类效果如果美术上够了也不一定需要bloom和色差的多次迭代采样。先做Pass合并和效果取舍再考虑降分辨率这样才能保住画质底线。3.3 半分辨率后处理管线省一半还看不出来半分辨率后处理是一个性价比极高的方案。做法是在主渲染结束后先把最终场景RT降到一半分辨率比如1080p降到540p然后在半分辨率上跑那些对精度不敏感的后处理Bloom、Color Grading等最后再升采样回全分辨率做合成。这个过程里每一个全屏Pass的带宽消耗都直接减半。比如一个原分辨率Pass要读写16.6MB×2半分辨率就只要4.15MB×2。如果管线里有四五个后处理Pass叠加省下来的数字非常可观。有人担心半分辨率后处理会导致画质劣化严重其实还好。像Bloom这种效果本身模糊的低分辨率跑反而有一种柔化的美感——前提是升采样时用双线性滤波不能用最近邻不然会有明显的锯齿。Color Grading这种也是对分辨率不敏感的直接在半分辨率上跑完全没问题。但像景深DOF这种对边缘敏感的半分辨率处理可能会让边缘闪烁要不要降需要具体测试。3.4 Pass合并与RT复用少切换就是省电后处理Pass多了渲染目标之间的切换也会带来额外开销。每切换一次RTGPU都要做一次完整的flushpipeline state也要重新设置一遍这些开销虽然单次不大但叠加起来在移动端是不可忽视的。Pass合并的原则很简单能够在一个Pass里算完的效果就不要拆成两个。举个例子如果同时要做高斯模糊和锐化完全可以在一个pass里先做横向模糊再做纵向模糊甚至多做几次采样合并输出而不是拆成blur_pass1、blur_pass2、sharpen_pass三个独立Pass。RT复用是另一个容易被忽略的点。如果后处理管线里有多个临时RT尽量让它们复用同一块内存避免频繁分配和释放。Unity里可以通过RenderTexture的临时分配接口RenderTexture.GetTemporary来复用临时RT切记用完要释放。3.5 抗锯齿选型MSAA在移动端是重度发烫源抗锯齿在后处理优化里总能碰到而且这里有一个容易踩坑的点。很多人从PC端过来习惯性开MSAA 4x或8x。在PC上这么做没问题但移动端MSAA的代价极高——因为它需要在内存里存储4倍或8倍于屏幕像素的样本数据resolve过程也很耗带宽。移动端我更推荐FXAA或TAA。FXAA是一个后处理Pass开销极低虽然画质相比MSAA稍差但对于移动端的视觉影响完全可以接受。TAA画质更好但需要处理历史帧数据内存和带宽开销比FXAA高一些。移动端如果要做TAA建议配合TSR这类超分辨率技术一起做效果会有意外惊喜。我这里给出一个简单的抗锯齿方案对比方案GPU成本发热影响画质适用场景MSAA 4x高明显发烫最好桌面端 / 低分辨率移动端FXAA低几乎无感一般移动端性价比之选TAA中中等好移动端画质追求型项目TAATSR中高中等很好需要高帧率的移动端大作实操中我还会做一个额外判断如果项目帧率目标不高30帧以内MSAA 4x其实还可以用但如果目标是60帧或120帧MSAA直接pass选FXAA或者TAA。发烫和帧率目标永远是强相关的。3.6 动态分辨率与自适应性能让发烫降下来除了砍Pass、降分辨率还有一个“运行时”的思路——动态分辨率。这个方案的核心逻辑是GPU负载高、温度升高时自动降低渲染分辨率GPU空闲、温度下降后再恢复。相当于给画面质量加了一个自适应阀门用动态画质换取温热稳定。动态分辨率的实现并不复杂。在Unity中可以通过调节渲染纹理的分辨率实现也可以用RenderScale来实现。关键是要有一个可靠的性能信号来判断负载情况比如用GPU帧时间FrameTime来做反馈控制。设定一个目标帧时间比如16.7ms如果长期超过目标值就逐步降低分辨率如果回到目标值以下就逐步恢复。这个方法尤其适合那种温度天花板的场景玩家玩久了手机会烫烫了以后降帧越降越难受。用了动态分辨率之后温度被压在可控范围内帧率能相对保持稳定虽然画质会稍微波动但体验比“掉帧降频”好太多了。4. 实操记录一次真实项目的纹理后处理削减4.1 第一步用Profiler给Shader和Pass记账这里分享一个我最近做过的移动端项目优化过程你可以在自己项目里复现这套思路。项目特征是角色扮演类实时战斗目标机型是老款安卓芯片帧率目标30帧。刚接手时真机稳定输出大概在22~25帧机身温度在连续运行15分钟后接近烫手边缘。第一步就是用Snapdragon Profiler和Unity Profiler拉了一整帧的带宽和Pass数据。结果很典型后处理管线上一次完整链路是 bloom_extract → bloom_h → bloom_v → color_grading → tonemapping → fxaa整整6个Pass全部在1080p分辨率下执行。纹理方面场景里有大量2048×2048的角色高精度贴图和1024×1024的UI贴图很多压缩格式还是RGBA。4.2 第二步具体改动清单和实测数据列一下这次优化动过的地方和前后对比纹理压缩格式角色和场景贴图从RGBA/BGRA统一改为ASTC 6x6重要贴图4x4UI贴图改为ASTC 4x4全场压缩比至少降了一半以上。场景普通贴图尺寸上限从2048压到1024UI大图从1024压到512个别特写物体保留1024。Bloom链路重构原来的高光提取、水平模糊、垂直模糊、合成四步改用半分辨率渲染合并成一个bloom pass 最后升采样合成Pass数从4个变2个分辨率减半。Color Grading和Tonemapping合并为同一个全屏Pass不再单独跑。MSAA 4x 切换为FXAA。加入动态分辨率以GPU帧时间为信号在16.7ms和22ms之间做两档自动切换。优化结果单帧GPU时间从22ms降到13ms左右帧率从22~25帧稳定到29~30帧。温度表现也更明显同一台老机型连续运行30分钟机身从接近烫手的状态缓解到温热。带宽数据上单帧RT读写带宽大概从200MB/s级别降到了80MB/s级别。这个过程中我个人最大的体会是优化前后画质肉眼差距非常小——除非你用放大镜对比单帧截图否则正常游玩的注意力根本不会注意到那些压缩痕迹。但手感差距是直观的帧率稳了、温度降了、续航也长了。5. 常见问题速查与排查技巧5.1 纹理优化常见问题问题可能原因排查思路画面出现块状压缩噪点ASTC块尺寸太大或纹理源质量差降低块尺寸8x8改4x4或纹理在压缩前先做降噪处理远处物体纹理闪烁mipmap未生成或生成错误检查纹理导入设置确认mipmap链完整图集区域之间出现渗色图集元素间未加padding或filter模式不对增加2~4像素的padding区域或改用Texture Array内存占用不降反升纹理流送优先级设置不当预加载过多纹理调低预加载范围按视野距离动态调整加载顺序5.2 后处理优化常见问题问题可能原因排查思路半分辨率后处理画面有锯齿升采样滤波方式不对改用双线性过滤必要时做一次轻量锐化TAA产生重影历史帧数据没有做运动矢量校正检查TAA的motion vector输出确保场景物体有正确速度信息动态分辨率频繁切换导致画面闪烁反馈阈值设置太窄增加切换滞回区间hysteresis分辨率档位之间留出缓冲后处理Pass合并后效果错乱RT格式或UV坐标处理不一致逐Pass对比Frame Debugger输出检查每个Pass的输入输出是否匹配5.3 几个值得养成的排查习惯排查纹理后处理发热问题有几个习惯我觉得特别值得养成。第一个习惯是用“排除法”定位热点关掉全部后处理跑一遍帧率再逐个打开后处理效果哪个打开之后帧率跌得最狠哪个就是优化重点。纹理层面同理可以用一个纯色/低精度纹理替换场景贴图测试帧率变化如果帧率变化不大说明纹理不是瓶颈如果明显掉了说明纹理采样和带宽问题值得处理。第二个习惯是“数据对比优先于感觉”发热问题不是肉眼能看出来的必须依赖性能分析工具。拉出GPU帧时间、Pass耗时、带宽占用、温度曲线四个维度用数据说话。不要凭感觉判断哪个效果好哪个效果差。第三个习惯是保持对老机型的敬畏。你在开发机上看着流畅的画面在老芯片上可能是灾难。合理的做法是项目里设一个“年度最低档机型”的测试标准所有优化都以这台机器为主验证目标。这样能避免很多上了线之后才暴露的发热问题。写在最后发烫优化的本质是给数据“减负”这套纹理加后处理的优化思路我自己在多个项目里反复实践过。我的直观感受是很多人一听说发热优化就想调分辨率、关阴影、砍特效结果画质面目全非。但其实在纹理和后处理这两个数据搬运大头上做文章性价比远远高于粗暴地砍画质。特别想强调最后一点纹理压缩格式和mipmap这两个东西你只要做正确了基本就是白捡的性能提升。它们不牺牲什么画面表现力只是在资源的组织方式上动了手脚带宽和发热却能明显改善。后处理降分辨率和Pass合并也是同理效果上几乎不可察觉但温度和功耗的差异是实实在在的。如果你手头正好有一个发烫严重的项目我建议你先从本文第2节和第3节的方案里挑两个最容易落地的做起来。先拉一次Profiler数据再动手改一个纹理压缩格式合并两个后处理Pass实测一下帧率和温度的变化。这个流程走通之后你就有了一套自己的判断标准知道后续优化的方向应该往哪里走了。