ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

移动端GPU发热优化:纹理与后处理的带宽治理实战

移动端GPU发热优化:纹理与后处理的带宽治理实战 聊发烫优化聊到第4篇前面的文章分别盯过CPU侧的调用频率、GPU侧的draw call以及Shader复杂度。总结下来就一句话发热的本质是功耗功耗的本质是数据搬运。手机GPU再怎么标称多少TFLOPs只要数据在内存和计算单元之间来回倒腾电流就在烧芯片就在热。而整个渲染管线里搬运量最离谱的两个“惯犯”就是纹理和后处理。这篇就把这两个大件彻底拆开讲清楚它们为什么会发热、怎么量化它们的搬运成本、以及从资源规范和渲染架构两个方向怎么治。文章会用到真实的带宽计算、格式参数和排查方法适合正在做移动端性能优化的图形开发者、Unity/UE手游引擎使用者以及被线上发热和降频问题折磨的客户端老兵参考。1. 先算一笔账数据搬运为什么是发热大户1.1 功耗公式决定的事移动端GPU的功耗可以粗略拆成两部分动态功耗来自晶体管翻转而这其中有相当大比例花在内存访问上。业内经常引用一个大概的能耗比经验值一次32bit内存访问消耗的能量大约是做一次32bit浮点运算的几十倍。换句话说在手机这颗SoC上跑算术往往比搬数据更“便宜”。所以优化GPU负载表面上看是降帧、降特效本质上是在优化“每帧该从显存里搬多少数据”。CPU帮不上忙Texture Unit和ROP不会平白无故少干活真正能改的只有两件事一个是让需要搬运的数据总量变小另一个是让同一条数据被反复利用的次数变多。纹理和后处理恰好在这两个维度上都管理得非常差。1GB/s的带宽在PC上不值一提但手机上的内存带宽通常只有20~50GB/s还要被CPU、GPU、显示控制器、ISP这些模块共享。一个现代手游画面光是后处理链就可能吃掉每秒几十GB的读写这还没算纹理采样。所以常看到一种现象GPU占用率看着不高芯片温度却降不下来很可能就是带宽在偷偷燃烧。1.2 前几篇聊了什么这篇要解决什么系列前几篇把CPU侧的Overdraw、Draw Call和Shader指令数都处理过了这次终于要动最伤筋动骨的两个模块。我的处理思路是先让读者对“一条数据是怎么被GPU搬来搬去”有体感再分别对纹理和后处理做详细的带宽审计最后给出可复现的排查流程。和Shader优化不一样的是纹理和后处理往往不是“关掉一个开关”就能解决的它们和画面主观品质强相关。所以这篇不会说“把所有后处理都删了”这种废话而是给出分级策略低端机跑什么配置、中端机跑什么配置、如何用格式和分辨率去交换视觉差一点点但功耗掉一大截的效果。2. 纹理GPU里最沉默的搬运工2.1 PBR时代一张模型的纹理到底有多大游戏美术进入PBR流程后纹理数量早就不是一张Diffuse走天下了。一个标准角色BaseColor、Normal、Roughness、Metallic、AO最少五张如果带Emissive、Height或者Lightmap还要更多。场景物件一个比一个精细2K甚至4K纹理普遍存在。问题在于纹理不是存在显存里就完事了渲染时GPU需要按照Mip等级把它们从内存拖到纹理单元、经过解压缩、再做过滤。举个直观的例子一张1024×1024的RGBA32位未压缩纹理原始大小是4MB。如果场景里同时可见两百个物件平均每个物件被采样到多个LOD层级那每一帧从显存读进纹理单元的量就是以GB为单位计算的。这还没算mipmap自动生成的额外1/3内存以及各向异性过滤成倍增加的采样次数。所以移动端项目里纹理侧优化的第一原则不是“把贴图画小”而是“让GPU以最小的代价拿到合适的像素”。这里面纹理压缩格式的选择比纹理本身分辨率更值得花时间。2.2 纹理压缩格式怎么选ASTC、ETC2、BC系列很多团队直到卡顿才想起来纹理压缩但具体用ASTC还是ETC2往往拍脑袋决定。我直接说结论新项目无脑用ASTC存量项目尽快往ASTC迁移。ASTCAdaptive Scalable Texture Compression是ARM主导的压缩标准优点在于它有极其灵活的Block Size选择。从4×4到12×12每个Block占128bit所以码率可以从8bpp一路降到0.89bpp。这就意味着你可以根据纹理类型单独控制精度Normal贴图用5×55.12bpp保细节BaseColor用6×63.56bpp大面积的粗糙度/金属度图可以用8×82bpp来压。ETC2则只有一个RGB模式即4bpp虽然Android官方要求所有设备从API 17开始支持但它对Normal贴图这类需要高精度的数据不太友好。至于PC上常见的BC7虽然质量好但并非所有移动GPU都支持兼容性不如ASTC。我处理过的最典型案例项目美术资源从PSD导出时统一用的TGA RGBA32场景量起来之后光是纹理原始数据就吃掉了2GB内存GPU带宽被压得抬不起头。后来批量转ASTC 6×6内存降到600MB左右帧率稳定了发热也明显好转。这里唯一的注意点是ASTC解压是硬件做的压缩时应该用astcenc这类工具预计算一次不要指望运行时压缩。注意Normal贴图不建议压到8×8或12×12会损失法线细节引起高光闪烁。实测5×5是移动端画质和性能比较平衡的选择。2.3 mipmap和各向异性过滤开了不一定浪费关了不一定省很多开发者有个误区觉得mipmap是“多余的内存开销”。实际上mipmap是GPU批量采样时的降带宽利器。远处物体一直在缩小GPU需要的是低分辨率mip这时若没有mipmap硬件只能硬着头皮从大图上做降采样过滤读取范围反而更大Cache命中率更低。举个例子一个4K纹理如果被全屏远处的墙面大面积采样没有mipmap时GPU可能被迫读取大量纹素来做缩小过滤带宽消耗可能是带mipmap纹理的十倍以上。而且mipmap还能提升Cache友好度同一个瓦片内采样到的纹素集中在低分辨率mip里命中率极高。这就是为什么所有引擎默认生成mipmap是有道理的。各向异性过滤Anisotropic Filtering刚好相反它是采样次数放大器对远处的斜视表面效果好但在移动端成本非常昂贵。iOS和Android上各向异性过滤常见支持到4x或8x但开启后纹素的采样数量成倍增加。如果项目发热严重我建议先把各向异性过滤从8x降到4x或者关闭画面差异通常没那么大带宽能省将近一半。顺带提一个很容易被忽略的参数MipBias。在引擎中给纹理设置负的MipBias等于让GPU倾向使用更大的mip画质好一点但带宽爆炸反之设成正的MipBias可以在不明显影响观感的前提下大幅降带宽。遇到发热问题可以全局加一档正向MipBias试一刀肉眼很难分辨。2.4 纹理图集和UV打包的细节移动端最怕的纹理浪费是每张模型都用一整张1024×1024但实际只填充了中间一块。UV布局过于分散会让GPU没法高效做纹理Cache命中。把多个小物件做Atlas合批可以让同一个Draw Call使用同一张纹理减少纹理切换同时提升局部性。但Atlas也有自己的细节坑如果物体之间的UV岛离得太近采样时纹理过滤会把邻居的内容混进来出现“出血”bleeding。常见解法是在打包时加入2~4像素的内边距Padding或者为UV岛生成半透明扩张边界。这个环节做不好美术会立刻发现画面边缘出现奇怪的颜色渗漏。另一个细节是纹理内存对齐。移动GPU喜欢纹理宽高是4的倍数某些压缩格式还要求是4×4块的整数倍。如果原图尺寸是1000×1000转ASTC时会额外padding到1004或1008实际占用的内存大于预期。美术导出时统一成2的幂次512、1024、2048可以避免这类浪费。2.5 顺带聊聊openmvs这类3D重建纹理的优化说到纹理最近总有人拿三维重建生成的纹理贴图来问怎么优化。像OpenMVS这类开源算法跑出来的纹理图集特点很鲜明分辨率往往巨大甚至一张图集可以到8192×8192以上UV利用率参差不齐有些区域被密集覆盖有些区域大面积留白而且因为重建算法的缘故接缝处经常出现颜色断裂。这类纹理放到移动端展示时如果直接拖进引擎性能灾难几乎是注定的。正确的做法是先重打包图集把离散的UV岛排布得更紧凑然后整体转成ASTC最后在引擎里把纹理的MipStreaming打开只加载当前视角需要的mip等级。这样做过之后一个博物馆扫描模型的内存占用能从500MB压到150MB加载速度也快很多。3. 后处理一条链子上面全是全屏读写3.1 一个全屏pass到底要搬多少数据讲后处理之前这个成本一定算明白。移动端常见的后处理链是这样的场景渲染进RT → Bloom多级模糊 → 色调映射 → 最终输出。每一个环节都是全屏Pass每渲染一个Pass就要读一张或几张全屏纹理再往另一张全屏纹理里写入。以1080p为例一张RGBA16F的全屏RT大小是1920×1080×8字节约16.6MB。在一个Pass里读一次、写一次就是33MB的搬运量。如果后处理链有8个Pass那一帧仅后处理就搬运约264MB。如果是60FPS每秒就是15.8GB。中端手机的内存带宽大概在25GB/s左右一个后处理链就占去六成这还只是后处理其他场景渲染、UI、粒子全都没算。这个计算一出来就明白为什么很多游戏在高端机上特效全开毫无压力一到中端机就烫手降频——后处理链把宝贵的带宽抢光了。3.2 常见后处理特效的“搬运量价目表”下面这张表是我在项目中喜欢贴给策划看的按照纯带宽开销做的粗略分级。数值不是绝对精确但量级足够说明问题。后处理效果大致Pass数1080p RGBA16F下每帧搬运量发热风险色调映射单Pass Tonemap1约33MB低FXAA抗锯齿1约33MB低Bloom4级降采样模糊合成6~8约200MB中高SSAO环境光遮蔽3~5约150MB中景深DOF5~10约300MB高TAA抗锯齿2~3约80MB中从这个表能看出来Bloom和DOF这类效果就是“搬运量惯犯”中的惯犯——效果本身确实好看但它让整条后处理链在帧末附加了一大段额外的读写。优化的基本思路不是把这些效果全砍了而是把它们的操作搬到更低分辨率、更小格式的Buffer上做。比如Bloom的模糊金字塔可以使用半分辨率甚至四分之一分辨率SSAO本身就是低频信息半分辨率完全够DOF的CoC计算可以做低精度模糊部分用半分辨率。视觉上有差异吗有但远没有帧率和功耗的差距大。3.3 合并Pass与计算优化真正省搬运量的办法后处理链太长最直接的省带宽手段就是合并Pass。举例来说很多后处理链路可以这样拆解采样场景颜色提取亮部生成Bloom源对Bloom源做水平模糊再对Bloom源做垂直模糊将模糊结果叠加回场景颜色做最终的色调映射如果按原始思路写就是6个全屏Pass。优化之后可以把“采样场景颜色提取亮部”合并成同一个Pass把“水平模糊垂直模糊”合并进同一个双Pass先横后纵再把“叠加回场景颜色色调映射”合并进同一个Pass。这样6个被砍到3个搬运量直接对折。在Unity里用CommandBuffer实现自定义后处理链时可以自己管理RT的复用。Bloom的中间模糊Buffer可以用R11G11B10F这种更紧凑的格式而主场景颜色Buffer在不需要HDR范围时也可以直接从R16G16B16A16F降成R11G11B10F。这个保守改动在多数画面上看不出差别但带宽会明显下降。还有一点值得强调的是后处理Pass会打断Tile-Based GPU的优化。移动GPU是分块渲染架构本来一整块场景在片上完成所有光照和混合后再写回内存效率很高。但你一旦在后处理里“读取上一帧的全屏RT”Tile-Based的优势就被架空了数据必须从内存读回来。这也是移动端后处理比PC端更贵的内在原因。3.4 移动GPU的特殊机会Subpass与Tile Memory聊后处理优化不能漏掉移动GPU的Subpass特性。在Vulkan或Metal里Subpass允许同一个RenderPass内的多个Pass共享Tile Memory不用把中间结果写回全局内存。换句话说后处理链条里的某些连续步骤可以在GPU片上直接传递数据省掉内存读写。实际项目中Bloom的“亮度提取降采样”这两个阶段如果符合Subpass条件带宽可以降得极明显。Vulkan上可以通过VkSubpassDescription的inputAttachment来声明依赖Unity的SRP如果用自定义Pass也可以利用RenderPass的LoadAction和StoreAction去规避中间RT写回。当然Subpass对Pass的输入输出有严格要求前一个Subpass输出的附件必须能被后一个Subpass以Input Attachment读取不能随意混合外部RT。操作起来不会像普通后处理那样灵活但值得投入。移动端发热优化的本质就是跟内存要效率Subpass是现在架构下最物理性的省钱方案。3.5 别把“后处理”这个词搞混图形、AI、CNC各有各的后处理写这篇的时候我发现“后处理”这个关键词在搜索时特别容易跑偏。图形学里的后处理叫Post-Processing指渲染完成后的全屏特效处理音视频处理里的后处理则是去噪、锐化等工业CNC里也有“后处理”比如五轴机床的刀路转G代码HyperMill和UG的后处理制作要处理4轴旋转、Z轴回零这些逻辑深度学习里YOLO也有后处理流程指模型预测输出之后的NMS和坐标解码。这些领域的“后处理”虽然字面相同但解决的问题截然不同。这篇文章涉及的是图形渲染方向的后处理如果你是被五轴后处理或YOLO后处理搜进来的那这篇帮不上忙。不过底层逻辑有一点是通的凡是后处理都是在主流程之后补一笔数据处理工作这笔工作的效率决定整个链路的消耗。这个原则放哪里都成立。4. 实测排查怎么证实纹理和后处理在发烧4.1 用Profile工具抓纹理侧的搬运量验证纹理问题比猜重要得多。我在项目里常用的路径是第一步打开RenderDoc截帧查看每个Draw Call的纹理绑定和采样器状态检查是否有大纹理被低LOD场景反复采样。RenderDoc里能直接看到纹理大小和MipMap范围如果有Draw Call绑定的纹理在1000×1000以上但该Draw Call的屏幕占比其实很小就说明LOD策略有优化空间。第二步用Arm Streamline或Snapdragon Profiler采集GPU计数器。需要重点关注的指标是Texture Read Bandwidth或者说DDR Read Total。如果场景在旋转视角时这个值跳变很大多半就是纹理Mip选择不当远处物体的高分辨率mip一直在被强制读取。第三步做一个对照实验把所有纹理临时替换成单色小图或者统一压到4×4 ASTC跑同一段帧率测试。如果在某个特定场景帧率显著上升就说明纹理带宽就是瓶颈。这个实验不用改代码用引擎的GlobalTextureSettings或者在AssetBundle阶段做替换就行。注意替换纹理做对照实验时要确保LOD Bias和Mipmap策略不变否则对比结果不干净。我见过有人为了测试把mipmap全关了结果是帧率掉了一半。4.2 用Profile工具抓后处理侧的搬运量后处理侧的排查其实更简单因为它的带宽集中体现在“全屏RT的读写次数”上。我建议先列一遍现有后处理链有哪些Pass、每个Pass访问哪些RT然后按我前面说的“一个Pass ≈ 读一次RT 写一次RT”估算出理论带宽。拿Unity举例Frame Debugger里可以看到每一个后处理Pass包括SRP的RenderPass列表。确认两个事情每个RT的格式是不是过于奢侈RGBA16F对很多中间结果没必要每个RT的生命周期是不是过长有些中间Buffer完全可以复用但引擎或代码没有复用。实操上还有个土办法全局关闭所有后处理特效跑固定场景看温度和稳定帧率再一个一个打开每次打开都记录温度增长。这个办法比较笨但直观。多数情况下你会发现最后一个打开的那个“看起来很小的效果”才是压垮帧率的元凶。4.3 常见问题速查表现象大概率原因处理方向GPU频率高但利用率低带宽瓶颈纹理或RT搬运过大查纹理压缩格式、mip策略查后处理Pass数量场景某个视角转一下帧率暴跌该视角内大面积纹理高Mip被采样调MipBias加纹理LOD距离压ASTC开Bloom就发热明显Bloom pass链过长高分辨率RT过多降采样到半分辨率合并PassR11G11B10F同场景iPhone正常Android发热ASTC/ETC2差异、驱动Subpass支持不同分平台走不同压缩格式和RT格式策略后处理越多越清晰但一多就卡同帧内全屏RT切换过于频繁利用FrameGraph合并Pass复用RTSubpass4.4 纹理与后处理的联动优化最后分享一个容易被忽视的联动场景当你在场景里放置了大量高光材质模型Bloom的亮度提取会遇到大面积高亮区域Bloom的模糊半径会为了覆盖这些高亮区域而加大搬运量随之上升。反过来如果你降低了贴图的粗糙度对比高光收敛得更锐利Bloom的采样范围反而会变小。所以纹理发布规范和后处理参数不是孤立的两件事它们共享同一个“带宽预算”。我倾向于在项目里维护一张总带宽预算表把场景渲染、后处理、UI、粒子各自的带宽估算列出来任何一个模块要加效果就先在这个表里看还剩多少余量。美术调纹理参数、TA调后处理链参数时拿这个表做判断依据而不是等到发烫之后再来回扯皮。最后补充一点实操心得做发烫优化做到现在我最深的体会是发热问题很少是某一个单点造成的纹理和后处理虽然是最容易出问题的两个“惯犯”但它们往往是在Shader和Draw Call优化得差不多之后才浮出水面的。前面的文章减掉了一大堆无效调用结果带宽瓶颈暴露得更明显了这时候再动纹理和后处理收益才会最大化。另外优化完之后记得做长时间的压力测试至少让设备跑20分钟以上观察温度曲线和降频曲线。有些优化在短时间测试里看不出来但在长时间运行后会因为温度临界点的不同带来完全不一样的体验。这个工作没有捷径数据会告诉你真正有效的是什么。
返回列表