ARTICLE DETAIL

资讯详情

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

纹理与后处理:移动端GPU发烫的两大带宽“惯犯”优化实战

纹理与后处理:移动端GPU发烫的两大带宽“惯犯”优化实战 前面发烫优化系列写到第4篇我干脆把这几天被问得最多、也是实测下来搬运量最大的两个“惯犯”单独拎出来聊纹理和后处理。后台经常有人私信说“我场景面数也不多DrawCall也压了为什么手机还是跟暖宝宝一样”这种案例十有八九最后查出来都是带宽被这两个家伙吃光了。这篇不讲什么高深算法就是把我自己项目里一轮轮压功耗的记录翻出来讲讲纹理和后处理为什么是最容易让GPU“跑腿跑得冒汗”的地方以及我实际验证过、真正有效果的优化手段。其他行业的朋友也不用急着划走咱们聊的是图形渲染这一侧的事理解清楚数据搬运这个概念很多优化思路是相通的。1. 先算清一笔账GPU发热很大一部分烧在“搬数据”上1.1 带宽是什么为什么它和发热直接挂钩很多朋友一提发烫就先怀疑CPU频率、怀疑是不是游戏逻辑写得蠢其实移动端发热的大头往往不在这里。GPU要渲染一帧画面数据不是凭空出来的。Shader要采样纹理要读深度要把结果写进渲染目标Render Target后面统称RT下一帧后处理还要把上一帧的结果再翻出来用。每一次访问显存和内存本质上都是一次“搬运”。移动端和桌面端还有个区别手机SoC的GPU和CPU共享同一块DRAM带宽是大家一起抢的。发热的本质是功耗功耗来自晶体管翻转而访问片外DRAM的功耗远高于芯片内部的运算。换句话说让GPU的计算单元拼命算功耗反而不一定恐怖让GPU一遍遍地去内存里搬数据温度才真的压不住。我记得之前看到过一组比较一次片内ALU浮点运算消耗的能量和一次访问DRAM的能耗差了至少一个数量级。所以移动端图形优化很多时候不是在优化“算法复杂度”而是在优化“这帧画面到底从内存里搬了多少东西”。这也是为什么纹理和后处理会成为发烫的惯犯因为它们两个是游戏里数据搬运量最大的模块。做个类比你一下子就懂了你是厨师动作特别快炒个菜三秒钟但现在厨房离菜市场特别远每炒一个菜都要骑车去菜市场进货。一天下来你真正炒菜的时间没多少全在路上耗体力了。GPU计算单元就是这位厨师纹理采样和后处理读写就是那趟去菜市场的路发热就是路上消耗的体力。1.2 纹理和后处理凭什么成为最大的两个惯犯先说纹理。每一帧画面上几乎所有东西都要贴图地面、墙面、角色、UI、字体图集凡是你能看到的像素背后几乎都站着一张或几张纹理。一张2048×2048的RGBA8888纹理未压缩时是16MB。一个稍微像样点的场景就算不多纹理资产加起来500MB都很正常。当然GPU不会在每一帧把所有纹理都搬进搬出它有很多缓存。但关键点来了如果纹理尺寸过大、格式没压缩、或者采样分布太散导致缓存命中率低那么每一帧都会发生大量“缓存没命中、回DRAM重新搬”的情况。一旦出现这个等于把贴图资产按原样过一遍总线带宽数字直接起飞手机不烫才怪。再说后处理。它最坑的地方是全屏。只要做一次全屏后处理不管效果本身多简单都要对整帧图像做一次读、一次写。我们算笔账一张1080p的RGBA16F RT约16.6MB一个后处理链里常见的有Bloom、景深、色调映射、噪点、暗角可能四五个pass每个pass一读一写那一帧光后处理就是一百多MB的搬运量。如果游戏跑60帧这单个后处理链每秒钟就要搬8GB以上。这还只是后处理没算主场景本身的纹理和几何读写。所以我把这两个列为“搬运量最大的两个惯犯”一点不冤枉它们纹理是“每次采样都可能搬一大堆”后处理是“每帧都逃不掉的全屏读写”。优化发热这两个方向不动手就别指望温度能降下来。1.3 “后处理”这个名字容易串台先钉死讨论范围查资料的时候你应该也看到了搜“后处理”会冒出来一大堆不同行业的内容像“yolo后处理流程”“hypermill五轴后处理”“ug后处理判断4轴”这些和我们今天要聊的不是一回事。深度学习里的后处理指的是模型输出后的NMS、类别过滤那一步CNC数控加工里的后处理是把刀路转成机床能识别的G代码。咱们这篇系列文章聊的发烫优化只讨论图形渲染里的post-processing也就是画面已经渲染出一版之后再对整张图片做的滤镜、模糊、抗锯齿、Bloom之类的效果。这个概念如果不钉死后面所有的优化讨论都会串台。咱们接着往下聊的都是图形渲染侧的后处理各位放心。2. 纹理优化把每一份“食材”的份量先减下来2.1 纹理压缩格式别让RGBA8888裸奔纹理这块我第一个想说的就是压缩格式。很多项目从PC端搬过来美术资源还是习惯性导出成PNG然后转RGBA8888放到移动端就是一场灾难。每个像素4个字节一张图就是几MB甚至几十MB采样时带宽压力巨大。这里要强调一下GPU纹理压缩和你在PhotoShop里做的“把图片存成JPG”完全是两回事。纹理压缩格式如ASTC、ETC2是GPU硬件直接支持的采样时按块实时解压CPU不参与。所以压缩是零运行时性解压成本的牺牲的是极少量画质换来的是几倍到几十倍的带宽下降。移动端主流压缩格式我列个表给你参考格式压缩比相对RGBA8888颜色Mipmap支持兼容性使用建议ETC1约4:1不支持透明通道支持老安卓设备基本淘汰ETC2约4:1支持透明通道支持GLES 3.0以上强制支持安卓兜底格式ASTC 4x4约4:1支持HDR也可支持2017年后的主流苹果/安卓UI、需要锐利细节的贴图ASTC 6x6约9:1支持HDR也可支持同左常用漫反射贴图ASTC 8x8约16:1支持HDR也可支持同左大纹理、背景、墙体PVRTC约8:1~12:1有限支持仅PowerVR系GPU苹果早期设备现在的建议很明确新项目直接统一ASTC。iOS从A8芯片开始就支持ASTC了安卓主流Mali、Adreno从2017年之后也基本全覆盖兼容性已经不是问题。ASTC的好处是块大小从4x4到12x12可以灵活选择。同一张贴图你想保质量就ASTC 4x4想极致省带宽就ASTC 8x8甚至10x10取舍非常自由。我自己的项目里UI贴图用ASTC 4x4或者6x6场景大纹理用ASTC 8x8法线贴图用ASTC 5x5或者6x6实测画质损失肉眼几乎不可见但采样带宽能降一半以上。如果项目还有比较老的安卓设备建议做一层格式回退ETC2保底。特别注意千万不要用PVRTC作为跨平台格式非PowerVR设备上驱动可能把它转成RGBA8888那带宽直接翻好几倍。2.2 分辨率和mipmap不是所有贴图都需要“超清原图”格式压缩做好了接下来就得看尺寸。我见过不少项目UI上一个图标实际显示200×200像素结果资源是1024×1024的PNG。这不只是浪费内存每次采样这张图标GPU的纹理单元都要去搬一个大块但如果只显示四分之一大小很多数据都白搬了。实操原则很简单贴图原始尺寸不要超过最终显示尺寸的2倍。比如卡牌最终渲染尺寸是256×256那贴图用512×512已经是顶天不是越大越清晰。这条意见给美术时建议直接写成资源规范而不是靠口头纠正不然项目量大之后根本管不过来。再说mipmap这个东西对降低带宽作用被很多人低估了。mipmap就是给一张纹理生成从小到大的一串预过滤图。当物体离相机远、贴图被缩得很小时GPU如果直接在大图上采样要做区域平均还要处理纹理缓存命中率降低的问题带宽消耗是很大的。有了mipmapGPU直接去对应大小的小图上采样一次就完事DRAM读取量会明显下降。mipmap的代价是纹理内存增加约三分之一。但对于场景贴图、需要缩小显示的模型贴图来说这个代价完全值得因为它换来的缓存命中率提升和带宽下降非常可观。需要注意的反而是另一面如果贴图从来不会被缩小比如全屏UI那开mipmap就是纯浪费内存还会增加一点采样开销这类资源建议关掉。另外提一句各向异性过滤。这个功能在场景贴图上能显著提升斜向视角的清晰度但代价是可能要从多个mip层采样更多纹理。移动端别无脑拉到16x4x基本够了。实测在Mali和Adreno上4x和16x的画面差异很小但16x的带宽消耗明显更高。2.3 纹理上传与图集策略别每帧都在“进货”压缩格式和尺寸搞定了还要小心纹理上传的问题。所谓上传就是把纹理数据从CPU侧送到GPU显存。很多同学没意识到纹理上传同样会走DRAM总线而且是写操作功耗比读操作还要高。最常见的反面案例是UI系统。有的项目为了实现某个动画效果每帧都在往GPU上传一整张大图集或动态文字纹理这在负载图上就是一条直线爬升的写入带宽。更隐蔽的是有些项目频繁使用RenderTexture把A拷贝到B再拷到C每次拷贝都是一次全量纹理数据搬运。这类操作做完发烫几乎不可避免。优化方向有三条静态纹理尽量提前打幂等运行时不要动。动态更新的纹理要么缩到足够小的尺寸要么降低更新频率比如从每帧一次改成每三帧一次视觉上通常看不出差别。多个小贴图打成图集可以避免频繁绑定切换但图集不要搞太大。我见有人为了省DrawCall把UI图集做到4096×4096结果纹理缓存命中率崩了而且部分机型驱动加载非常慢。用2048×2048的图集加ASTC压缩实测温度和加载时间都比4096要好。2.4 纹理资产检查清单照着这条线走一遍每次项目出现发热问题我会让人按下面这张表把纹理资产过一遍基本能找出80%的问题检查项常见问题优化后建议纹理格式RGBA8888裸奔统一ASTC 6x6/8x8UI用ASTC 4x4纹理尺寸超过实际显示需求太多按显示尺寸2倍以内裁剪mipmap场景贴图没开场景贴图开启UI动态图关闭法线贴图用RGBA存四通道压缩为ASTC保留RG通道配合导出即可动态更新每帧上传大纹理缩小尺寸/降低更新频率/局部更新图集大小4096大图集2048为上限按缓存友好度取舍重复资源一张图一个贴图合理合图共享纹理这张表看起来简单但磨合下来纹理方向少说能砍掉一半带宽。所有优化做完以后记得重新用Profile跑一遍看带宽数据是不是真的降了别凭感觉。3. 后处理优化全屏RT就是带宽无底洞3.1 先算一笔账一张全屏RT到底有多重后处理为什么难优化因为它一上来就是全屏。一张1920×1080的RTRGBA8格式大约是8.3MBRGBA16F直接翻倍到16.6MB。看起来单张不算特别夸张但后处理是连续好几道工序的每个pass都至少一读一写。假设一个比较标准的后处理链有4个pass且全都在一张RGBA16F HDR RT上操作那每帧搬运量是16.6MB×2读写×4pass133MB。跑60帧就是每秒8GB的总线流量。这8GB是“额外”的因为场景本身的纹理采样、顶点数据读取还要另外算。移动端主流的SoC总带宽一般在三四十到七八十GB每秒这一套后处理就可能吃掉十分之一甚至四分之一。所以别小看一个“看起来不卡”的后处理它的带宽账往往比主场景还难看。我后来做优化时养成一个习惯任何画面功能在写shader之前先把这个功能的带宽预算算出来。算完如果不合理再好看的特效方案也得改。3.2 先降分辨率再把格式砍到LDR后处理优化第一招降低RT分辨率。很多效果本质上是在做“模糊”或者“低通滤波”比如Bloom、景深、SSAO、体积光这些效果天然就应该在低分辨率下做。它们的内容本身就是模糊的放到HalfRes甚至QuarterRes上算最后的视觉差异几乎看不出来但带宽能省多少呢一张1920×1080的RT面积是207万像素。如果降到960×540面积只有原来的四分之一。RT四个像素里只做一个的工作量而且RT总数据量也降了四分之三。如果再搭配上格式从RGBA16F改成RGBA8这一步又把带宽减半。两个操作叠加这个后处理链的带宽理论上能降到原来的八分之一。这是我在项目里实测最有效、也最立竿见影的一招。具体操作上Bloom、SSAO、景深这些低通类效果直接分到HalfRes的RT上渲染主图最后在合成阶段把低分辨率结果放大采样回去。色调映射、颜色校正这些需要逐像素精度的效果才放全分辨率。有些效果甚至能放QuarterRes主要看画面需求。3.3 合并pass减少全屏读写次数减完分辨率和格式还得盯pass数量。老旧的渲染管线经常是一个效果一个pass比如雾效一个pass、Bloom一个pass、颜色校正一个pass、噪点一个pass。每个pass都是一个全屏三角形都是一次全屏RT读写来回。一串下来可能十几个pass带宽再好看也会被拖垮。更聪明的做法是把能合并的合并。比如常见的final pass可以把颜色校正、线性空间转换、暗角、噪点、色调映射全部写进同一个shader一次全屏读写全部搞定。Bloom这类效果也只要维护两张低分辨率RT——一张提取高亮区域一张做模糊最后在主画面合成时采样一次就行。切忌让一张图在Bloom里过五六遍高斯模糊。移动端建议多了解一下FrameBuffer Fetch机制它允许后处理shader直接读取当前帧缓冲的内容不需要先写出去再重新采样回来可以省掉一次RT读写。这个特性在GLES 3.0的扩展里支持得不错Vulkan里对应的就是subpass机制。不过要注意FrameBuffer Fetch在部分GPU上并不是绝对零成本有些驱动实现得很粗糙做之前先在你目标机型上A/B对比一下带宽数据别想当然。另外提醒一点不要在中间RT之间做无谓的像素拷贝也不要动不动把RT从GPU读回CPU。很多项目的后处理卡不是因为shader多复杂而是这些来回搬运操作太多。3.4 常用后处理特效的功耗性价比不同后处理特效的性价比差别很大我把常见的几个列出来方便你按预算取舍特效带宽消耗优化手段备注Bloom泛光高HalfRes/QuarterRes 低pass数低分辨率下视觉损失很小景深DoF高HalfRes尽量不用散射内核半分辨率通常能达到电影感TAA抗锯齿高需要历史帧和运动向量带宽高但画质稳预算紧可不加FXAA抗锯齿低一次边界检测移动端性价比之王噪点/暗角极低合并进final pass几乎不占额外带宽SSR/SSAO/SSGI较高QuarterRes或关闭屏幕空间特效都贵动态模糊高慎用实时项目优先考虑砍掉我现在的项目里Bloom和景深都跑在HalfRes抗锯齿只开FXAASSR和SSGI直接不开画面观感依然能打。每一次后处理特效堆叠都不是“加一个shader而已”而是给每帧增加几MB甚至几十MB的搬运量。特效取舍本质就是带宽预算的取舍。4. 用Profile数据说话怎么定位到底是谁在搬4.1 该看哪些工具和关键指标聊完原理和方案最后还是得回到数据上来。优化发热这种事最忌讳凭感觉。你觉得是纹理的问题关掉后处理测一下你觉得是后处理的问题把所有后处理关掉再做A/B对比。别只看温度要结合工具看带宽因为温度有滞后性带宽数据是即时的、可对比的。常用工具我推荐这几个Android GPU InspectorAGI谷歌官方工具能看GPU频率、帧耗时、带宽指标适合安卓性能分析。Snapdragon ProfilerAdreno GPU专用DRAM bandwidth、总线占用都能看到。Mali Performance AdvisorMali GPU平台能看GPU内部的计数器包括总线读写量。RenderDoc抓帧分析适合看一个Frame里RT数量和采样情况。Unity/Unreal里自带的Frame Debugger加Profiler也能帮上大忙。关键指标我记得最牢的是几个GPU频率曲线、Memory Read/Write带宽、Framebuffer带宽、纹理采样次数、纹理缓存命中率。其中“总线带宽”和“DRAM带宽”是最直接反映“搬运量”的指标。你看负载图如果某一项功能开关后带宽曲线明显上升那它就是发烫的真凶。4.2 常见问题速查表照着排查下面这张表是从我自己的项目经验里整理出来的按“症状”来排查非常快症状可能原因排查思路场景静止不动也烫后处理链过长、RT尺寸太大关掉后处理对比带宽曲线UI界面温度高UI图集未压缩、动态UI频繁上传检查UI纹理格式与上传频率一开某个新特效就烫新增后处理pass多、RT格式用了16F计算该特效每帧带宽同样场景iOS正常安卓烫纹理压缩格式兼容问题统一ASTC重新打包测试帧率不低但温度高带宽饱和、GPU高频持续运行看bus/dram带宽曲线做减法顺便提一个容易被忽略的现象帧率高不等于不烫。很多游戏场景里画面明明是60帧但屏幕一直在发烫就是因为GPU虽然没在等谁但它一直在全速搬数据。这种情况下你把帧率降下来几帧或者把纹理和后处理的带宽降下去温度反而能明显下降体验还更稳定。4.3 几个踩过的坑直接说结论最后分享几个我亲测踩坑的案例希望你们别再来一遍了。第一个坑是迷信MSAA。当年贪图MSAA画面抗锯齿效果在移动端全屏上开了4x MSAA结果带宽数据直接破纪录。MSAA的本质是给每个像素存多份采样点帧缓冲和深度缓冲的带宽消耗都翻倍。后来改成FXAA加一个轻量的TAA组合画质下降不明显功耗明显好转。第二个坑是只降分辨率不降格式。有次我把Bloom改到HalfRes以为带宽已经省了四分之三结果一看Profiler带宽没怎么降。排查半天发现RT还是RGBA16F每像素8字节比原来的RGBA8多了整整一倍。所以降分辨率和降格式要一起做只做一半等于没做。第三个坑是纹理格式没检查兼容性。有个旧景区资源用的PVRTC在非PowerVR设备上被驱动转成RGBA8888结果那台机器上整个场景带宽异常高。做跨平台项目之前一定要先确认所有纹理在目标设备上的实际存储格式别只看打包设置。第四个坑是UI图集过度追求大。一个4096×4096的动态UI图集表面上减少了DrawCall但缓存命中率崩了而且动态更新一次就要上传64MB数据。换成两个2048图集加ASTC压缩后DrawCall多了一点点但温度、加载时间、流畅度全都改善了。做优化做到后面我最大的体会是画面效果没有免费的每一样看得见的东西背后都有一堆看不见的搬运量在买单。与其到处找有没有“玄学优化”不如把“这帧要搬多少数据”当成第一性原理一条条抠。纹理和后处理这两个惯犯抓准了很多手机发烫问题已经解决了一大半。之后如果大家还想看我可以继续把帧率、内存、Shader复杂度这些方向挨个写下去。
返回列表