ARTICLE DETAIL

资讯详情

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

移动端GPU带宽优化:纹理与后处理两大惯犯的排查实战

移动端GPU带宽优化:纹理与后处理两大惯犯的排查实战 1. 先把这篇文章的敌人认清楚带宽、搬运和那两个惯犯这个系列写到第 4 篇前面我们把发热排查的基本思路过了一遍。如果你是自己一台一台真机去摸设备温度会发现一种很常见的现象帧率没崩功耗先炸手机壳先烫。这通常不是 GPU 在拼命算而是 GPU 在拼命“搬”数据。发热优化的核心说穿了就是四个字省带宽。这篇文章要抓的两个惯犯纹理和后处理正好是移动端 GPU 上搬运量最大的两个环节。纹理是“每帧反复读”后处理是“每帧反复读写”一个靠大量采样把带宽吃到饱一个靠一串全屏 Pass 把带宽榨干。两个叠加起来基本就是中等偏上画质项目发烫的大半原因。先说个基本认知计算和搬运不是一回事。GPU 做一次乘加运算shader core 内部就能完成功耗很小但从显存里读一个纹理、写一张 RenderTarget数据要走总线、过控制器功耗要大得多。业界一般估算是访问外部 DRAM 的开销比片上 SRAM 高出 20 到 50 倍。也就是说一个看起来“什么都没算”的采样操作消耗的能量可能比一堆 ALU 运算还多。纹理单元往 L1 Cache 拿数据和从 DRAM 拿数据代价差着一个数量级。希望你在往下读之前先建立这个视角每次看到一个 Pass、一张纹理、一个 RT脑子里要自动冒出一句话——这玩意儿一帧要搬多少字节如果你现在还没有这种条件反射这篇读完应该就有了。1.1 发热的本质是功耗功耗的大头在数据通路移动 SoC 的发热本质来自功耗密度。芯片里同面积晶体管翻得越勤、总电容越大、跑得越快功耗就越高。但功耗并不只看逻辑电路内存接口、总线上挂着的请求、数据翻转的位数都是烧电大户。很多项目的功耗测试里GPU 内部占比最高的往往不是 ALU而是 Texture Unit 和 Memory Controller。我自己调试过一款中端设备上的开放场景跑城主区域 60 帧整机电流比空闲高了一倍多。抓内存带宽发现单帧画面搬运总量接近 300MB60 帧每秒就是 18GB/s 左右的持续读写。那款设备的 DRAM 带宽上限大概 25GB/s实际还有 CPU、NPU、编解码器在抢。这样算下来GPU 已经把总线的 7 成以上吃掉了能不烫吗。这个例子告诉我们一件事发烫优化的第一步不是调算法、不是换 shader而是“清点搬运量”。知道每帧到底搬了多少字节哪些 Pass 是吞吐大户哪些纹理是采样热点才能确定朝哪下手。1.2 一个容易踩的“后处理”概念错位说纹理和后处理之前先把术语边界划清楚。中文互联网里“后处理”这个词至少有三个完全不相干的领域在用很多人搜索资料时容易串台。图形渲染里的后处理Post-Processing指渲染完场景之后在屏幕上叠加的一串全屏效果Bloom、景深、色调映射、抗锯齿、色彩分级都算。这是本文讨论的核心。CAM 数控加工里的后处理Post Processor指把刀路轨迹转换成特定机床能识别的 G 代码程序像 HyperMill 五轴后处理、UG 后处理判断四轴变化时 Z 轴回零都属于机械加工软件的范畴要学的是机床运动学、碰撞检查和各轴行程约束。AI 推理里的后处理例如 YOLO 目标检测的 NMS 和坐标解码是把网络输出原始张量转成最终检测框的那段逻辑。这三个东西术语都叫“后处理”工作性质完全不同。看到标题里的“后处理”如果你第一反应是 CAM那方向就偏了。图形后处理解决的是“把渲染结果加工成更讨眼球喜欢的画面”它完全是 GPU 带宽赛道上的常客。1.3 为什么偏偏是纹理和后处理“惯犯”顺手盘一下一个典型帧的搬运账本。首先是几何和光照这部分数据量很大但大多通过顶点 buffer、uniform 传递而且现代管线常用 index buffer、顶点压缩来控制。真正敏感的是每帧都要读的纹理资源贴图、光照图、阴影图、HDR 环境反射这些加起来动辄几十 MB。如果纹理没压缩、没开 mipmap各个采样点还在频繁 miss cache数据只能一次次从 DRAM 拉带宽消耗瞬间起来。再往后是延迟管线的 G-Buffer 或者前向管线直接上后处理。后处理的问题在于“每个全屏 Pass 都要读一遍、写一遍”。比如 1080p 下一张 RGBA16F 的 RT一个 Pass 读写就是 31MB 左右十个 Pass 就是 310MB一帧画面在纯后处理阶段就可能搬掉一个巨大的量。很多项目的后处理栈随便一搭就是 Binomial Bloom、景深、TAA、Color Grading、Tonemapping串四五层以上非常常见。所以这两个“惯犯”不是偶然。纹理是静态数据里的最大头后处理是动态读写里的最大头。它们天然就是带宽账单里排最前面的两行。2. 纹理优化从格式、尺寸到别一次全塞进显存纹理优化的核心目标只有一个在保证画面可接受的前提下最大化降低每帧“采样搬运”的字节数。这条路可以从格式、mipmap、尺寸、流送四个层面依次展开。2.1 纹理压缩选型ASTC 还是 ETC2别再无脑 RGBA32先算一笔基础账。一张 1024×1024 的 RGBA8 纹理在显存里就是 4MB 原始数据。如果它在一个场景里被高频采样每帧读一次就是 4MB 的搬运量如果是 4096×4096 的 RGBA8那就是 64MB。你再看一眼项目的 Asset 列表随便数数有多少 4096 以上的贴图就知道问题出在哪了。移动端正确做法是使用块压缩格式让纹理单元按块直接解码GPU 读到的不是逐像素字节流而是压缩后的小块数据带宽只花“压缩后”的密度。RGBA8 如果切到 ETC2能压到原来的四分之一左右切到 ASTC 4×4也只有原来的四分之一ASTC 6×6、8×8 能压到八分之一、十六分之一代价是压缩块更大边缘细节颗粒感会强一点。贴一张实际参考表格式每像素位数1024×1024 近似大小常见用途RGBA8 未压缩32 bpp4MB基本不可接受除非极小图标ETC24 bpp0.5MB移动端最低保障兼容性好ASTC 4×48 bpp1MB需要较高细节/带 alpha 的图ASTC 6×63.56 bpp0.45MB大纹理/颜色渐变丰富场景ASTC 8×82 bpp0.25MB漫反射、粗糙度、大面积无细节纹理选型逻辑上iOS 8 以后、Android 4.4 以上绝大多数设备支持 ASTC。如果你的最低配置没有太老ASTC 是移动端的首选。ETC2 在旧设备上更稳但要付出视觉质量略下降的代价。最不建议的是无脑 RGBA32除非你是做 UI 小图标或者某些需要精确颜色读取的极特殊场景。这里还要提一个容易忽略的技巧通道打包。把 specular 的 smoothness 和 metallic 塞进同一张纹理的 G 通道和 B 通道两张图合成一张搬运量直接减半。美术听起来觉得难受但性能上非常值。纹理压缩管线的关键就是让每一个被搬进来的字节都承载尽可能多有用的信息。2.2 mipmap 不是可选项是带宽审查的第一道关很多项目资产导入时没勾选 Generate Mip Maps。你说“我原图 4K直接贴上去够清楚”但 GPU 渲染远距离物体时屏幕上一个像素对应纹理上可能几十个像素采样子片没有 mip 分层纹理缓存会频繁失效带宽消耗指数上升。可以这样理解mipmap 相当于给纹理做了一套“预查表”让 GPU 在远处直接读一个已经很模糊的小贴图而不是反复把 4K 原图往 cache 里拉。代价是存储多约三分之一收益是带宽可能跌掉一大截大概率是稳赚的。实际操作时要注意三线性过滤和 mip bias。开了 Trilinear 之后颜色过渡平滑但带宽比 Bilinear 多一点各向异性过高也会带来额外开销。我个人的经验是移动端 Anisotropy 开到 4 就够了别为了“顶级画质”无脑 16。做一个动态大世界时可以在远处把 mip bias 人为调大一点让远山、远树直接用更小的 mip肉眼几乎看不出差别带宽又省一点。顺带提一个热词“纹理图”。纹理图指的就是把多张小纹理合并到一张大图图集里配合 mipmap 一起用。图集如果跨 mip 层有接缝问题可以做 padding 或者用阵列纹理方案。剪辑好纹理图集之后一个场景里可能需要绑定多张纹理的次数大幅减少绑定开销和切换开销都降下来了。2.3 屏幕上的像素密度决定纹理尺寸不要盲目上 4096纹理尺寸越大不代表画质越好。一张 4096 贴图贴在一个屏幕占比只有 50 像素的小道具上最终显示效果和 256 贴图没有区别但搬运成本差着十六倍。确定纹理上限的经验法则找到玩家最近可能看到的距离算屏幕上占多少像素再留 50% 冗余就是这个纹理在实际渲染中该有的密度上限而不是“美术画了个多大多大我就用多大”。在具体项目中这个错误有一个典型来源离线重建资产。比如 OpenMVS 这类开源三维重建算法生成的纹理贴图动辄输出 2K、4K 甚至 8K 的高精度贴图。这些算法面向影视后期和存档级重建输出为了保证每个角度的细节局部清晰度非常高但对实时渲染来说完全是灾难。直接塞进游戏引擎显存报警、带宽爆炸。正确流程应该是先烘焙出适合原始几何的UV集再降采样到目标平台的合理密度上限同时做透贴和异形边缘的处理。所以又回到那个判断标准一张纹理该多大取决于屏幕上的显示需求和压缩格式而不是资源生产软件里导出的尺寸参数。2.4 纹理流送别把未来一帧要用的东西提前全搬进显存场景大到一定程度即使每张纹理都优化过整体打包仍然会超出 GPU 内存预期。这时候就该考虑纹理流送Virtual Texture。思路并不复杂把超大地图按 Tile 切成块建立一个页表根据相机位置动态加载需要的块淘汰不需要的块。Chunk 切多大切小需要调试。切得太大单次加载量猛容易卡顿切得太小页表管理开销变大纹理更新频繁。我见过移动端项目用 128×128 到 256×256 的页块比较顺手。判定加载优先级时可以把相机朝向的相邻页优先级提高再叠加距离和 mip 层级。流送能极大缓解显存压力但代价是需要一套异步加载和 LRU 淘汰机制。一个常见坑是玩家视野快速移动时淘汰策略把“刚转过去还没敲定加载”的块清掉了于是远处纹理反复闪现、泛灰。解决办法是给淘汰策略加一个“最近请求时间”的考量并保证已提交加载的块不立刻被回收。另外加载优先级里永远把当前帧视野中心的块放在最前面玩家的眼睛对屏幕中心最敏感。如果你的项目纹理总量明显超过目标设备常见压力线就应该认真考虑流送方案而不是简单加一句“用异步加载碰运气”。3. 后处理优化全屏 Pass 的带宽地理与交通规划后处理优化的思路可以理解成城市交通规划。每个全屏 Pass 都是一条必经主干道车辆像素数据每过一次都要从“内存”一侧搬到“GPU”一侧做点加工再搬回去。城市RT越多过车越频繁交通压力越大。3.1 全屏 Pass 的带宽成本读一次、写一次乘以 Pass 数算后处理带宽有一个非常朴素又非常准的心算公式单个 Pass 的带宽成本 宽度 × 高度 × 每像素字节数 × 2一次读取 一次写入以 1080p 为例像素数约 207 万。如果你用 RGBA16F每像素 8 字节一个 Pass 读写就是 207 万 × 8 × 2约 31MB。如果是 10 个 Pass就是约 310MB。如果后处理栈里定义了多张中间 RT比如 Bloom ping-pong 两三个 buffer场景光度链 5 层再加 TAA、景深、Tonemapping一个 60 帧的帧仅仅是后处理阶段就能吃掉 10GB/s 到 20GB/s 的带宽。也因此后处理优化的第一性原则就一句话减少 Pass 数和减少每个 Pass 的字节数。具体手法包括半分辨率、降低像素格式精度、多 Pass 合并、算法级简化。这些方法的优先级永远排在“微调某个效果具体参数”之前。3.2 逐 Pass 排查谁是带宽吞金兽后处理栈里每个效果的成本结构都不一样要逐个盯。Bloom 是典型的“Pass 数量堆出来的效果”。完整 Bloom 一般要做亮度提取、多级降采样、多次模糊、逐级上采样合成每个环节都是一张新 RT。高性能做法是把它直接压缩到 1/4 甚至 1/8 分辨率跑因为模糊本身就是低频信息不需要全分辨率。结合分离模糊先横后纵能把单个 pass 复杂度从 O(n²) 降到 O(2n)带宽占用也降一截。景深比较坑因为默认习惯是拿全分辨率做 CoC 计算和 scatter。移动端野路子是用 1/2 分辨率处理更大半径的散景再用近景信息浅合成视觉上限没那么高但能耗低很多。色调映射和 Color Grading 理论上只需要一两个 Pass但如果做成 LUT 查找后再跑一次全分辨率调色依然多了一次读写。可以把它和 Tonemapping、Gamma 校正合并到一个最终 Pass 里完成减少一次中间 RT 的往返。TAA 的 MotionVector 渲染同样容易变成搬运大户。有些项目为它单独写一遍 GBuffer 读或者额外生成一张速度和深度贴图开销并不低。实际可以将 MotionVector 拼进 GBuffer 的空闲通道里省掉独立的 motion vector pass。一个个排查完你会发现热点经常集中在两个模式上一是“高精度 RT 太多”二是“全分辨率 Pass 串得太多”。这两个模式只要撞上一个带宽账单就下不来。3.3 后处理栈的“移动端友好”重排移动端的后处理栈应该一开始就按“移动优先”来排而不是从主机项目里拷一套过来再削。我的移动端常用排序是这样先做渲染分辨率的动态缩放再用半分辨率通道处理 Bloom、景深这类低频效果再在最终 Pass 一次性合并 Tonemapping、Color Grading、颜色校正。动态分辨率阈值可以写成以 GPU 时间为主轴的控制而不是固定档位。这样即使在复杂场景下帧率也能稳在一个可接受区间避免因为后处理导致发热剧烈增长。精度上能用 RGBA8 的尽量不要用 RGBA16F。Bloom 中间量可以用 R11G11B10 或者 RG16F画面观感在这个环节几乎不敏感。RT 类型选小了带宽直接砍一半这是一项零成本改造。移动端还要特别留意一点不要同时开多张中高精度全屏 RT。比如一边要 TBDR 上的 memoryless attachment一边又想保留历史帧供 TAA两张 RT 同时活跃会让带宽预算一下子爆表。优先复用已有 RT 的通道减少“同时活跃”的 RT 数量。3.4 后处理与纹理的联动带宽预算是一盘棋纹理优化和后处理优化不是各自为战它们共享同一个带宽池。比如延迟管线的 G-Buffer既是场景渲染阶段的“写目标”又是光照/后处理阶段反复读取的输入。如果 G-Buffer 里法线用了非常高精度的 RGBA8甚至还带额外一张法线贴图那么后续每次后处理采样法线都会把高精度数据重新搬一遍。这里可以做一个联动调整法线用 Octahedral 编码存进两个通道或者直接压到 GBuffer 的 R8G8 通道搭配 view space 重构G-Buffer 总带宽立刻降一档。后处理阶段若只需要深度、法线和一些材质 ID可以对 G-Buffer 做“只读”访问甚至换成更低精度的临时 RT。这些都是纹理与后处理在带宽维度上的交叉点。理解了这一层你就明白为什么我会把纹理和后处理并称为“惯犯”它们经常在同一个渲染通路里互相放大。纹理每帧多搬 50MB后处理再反复采样它 3 次总成本就是 4 个 50MB而不只是 50MB。4. 可实操的量化工具和排查流程优化不能靠感觉。要精准定位哪些纹理、哪些 Pass 真正吃带宽还是得靠工具。下面给一套我习惯的工具组合和排查流程可以直接套用。4.1 Profiling 工具清单与上手顺序如果你在 iOS 平台Xcode 的 Metal System Trace 是最直接的选择。它能抓 GPU Active 占比、Bus Utilization、DRAM Bandwidth 和 Core Utilization直接看到整个 GPU 的带宽水位。Android 端可以根据芯片用 Snapdragon Profiler、Mali Offline Compiler 或 AGPAndroid GPU Inspector。这些工具体验各异但核心指标是同一套GPU Freq、DDR Freq、Bus Usage、Core Activity。RenderDoc 是抓帧神器可以拉开整个渲染流程看每个 draw call 绑定了哪些纹理和 RT。Windows 上 PIX 也能做类似的事。用 RenderDoc 时建议直接导出 buffer 资源列表按大小排序看到“哪一个 RT 从创建到销毁一直没降过精度”就是嫌疑对象。上手顺序我给一条固定路线先用系统工具的带宽统计看整体水位 → 再用抓帧工具看 Pass 顺序和资源 → 最后针对重点 RT 做像素级成本审计。先整体后局部避免一开始就在某个 Pass 里抠细节浪费时间。4.2 定位“搬运大户”的三个检查点第一个检查点是列出所有 RT 的分辨率和格式。看到一个 RT1920×1080 RGBA16F就可以估算 32MB 每帧如果它从帧头活到帧尾就又是整帧带宽预算的重要消耗。把所有 RT 列完你会直观地看出谁是重量级选手。第二个检查点是数全屏 Pass 数量和高精度 RT 数量。十来个全屏 Pass 是上限里的上限。超过之后去思考哪些 Pass 能降到一半分辨率、哪些可以合并、哪些中间的往返读写可以被省掉。第三个检查点是扫资源管理面板里纹理的原始格式和尺寸。筛出所有“未压缩、未生成 mipmap、4096 以上”的资产一个个复查。很多历史原因导进来的资源带着原始美术文件格式成了隐藏搬运大户。曾有一个项目角色皮肤贴图导成 4096 未压缩 RGBA单角色身上贴图就有几十 MB 拖动空间压缩加 mipmap 之后再配合 mip bias整场景带宽立刻低了 40% 以上。这三个检查点做完该动刀的地方基本已经全部暴露。4.3 给优化上“规矩”自动化预防优化做完一轮还要防止回归。最有效的办法是把它变成自动化规则。纹理管线里配好预设禁止新资源用未压缩 RGBA32强制生成 mipmap。美术或 TA 提交新资产时CI 检查会自动标记不合规资源。后处理侧维护一张“每帧带宽预算表”指定关键平台的带宽上限和 RT 精度要求。谁想加一个全屏 Pass先填这张预算表写清楚新增大概多少 MB/帧有没有替代方案。本质上不是不能加而是加得明明白白。这种“规则化”动作看似繁琐长期收益却很大。项目跑久之后资源会越来越臃肿如果没有制度约束任何一次美术资源更新都可能重新引入带宽黑洞。5. 我的优化决策清单该做什么不该做什么到了实操阶段给一套我经常用的决策优先级。项目发烫瓶颈如果确认在带宽侧按这个顺序动作基本能收到很好效果。5.1 按性价比排序的优化动作顺带附一张优化优先级表方便对照排期优先级优化动作成本收益P0纹理改成 ASTC/ETC2打开 mipmap低极大降低静态纹理搬运量P0后处理合并 Pass 到最终色调映射中减少 1 次以上全屏读写P1Bloom/景深等低频效果降为 1/2 或 1/4 分辨率低每 Pass 带宽减半以上P1RT 从 RGBA16F 降为 R11G11B10/RGBA8低字节数直接折半P2动态分辨率或 mip bias 适配场景复杂段中拉平峰值带宽P3纹理流送/Virtual Texture高解决超大地图显存和带宽双压力P0 和 P1 基本是“花小钱办大事”。有很多项目只做了这一层功耗就已经优化到可接受区间根本不需要动架构。5.2 哪些“优化”不值得做分享几个我踩过的坑。第一盲目把纹理质量降到 128 分辨率来省带宽。观感崩了反而触发回退或用户差评。省带宽不等于降画质压缩格式和 mip 优先分辨率是最后一张牌。第二为了“性能”疯狂拆分 Pass。例如把 Bloom 的每级降采样都拆成独立 kernel中间多出无数临时 RT带宽反升。虽然用分离模糊可以优化 pass 复杂度但不要无意义地制造中间 buffer。第三把 Shading 质量降到没人能接受的底。省是省了画面成交互式设计评审都过不了优化就失去了意义。伪优化的共同特点是它们只降了“理论计算量”没有降“实际搬运量”有些反而增加了读写次数。5.3 一次真实排查记录超宽屏后处理导致的发热说一个最近的实战案例。项目在 16:9 设备上表现正常换到 21:9 带鱼屏后整机温度直接高了 3℃ 左右帧率也没明显掉。查功耗发现 GPU 带宽持续高位。抓帧一看后处理相关 RT 全都按全屏渲染解析度从 1920 拉到 2520 左右单 Pass 成本大约涨了 30% 以上。但看起来帧率没变只是 GPU 跑更高的频率去顶着热量自然上来了。把这个后处理阶段的 RT 改到以渲染分辨率 1/2 为主再用全分辨率只做最终合并功耗立刻掉回正常区间画面观感几乎没有变化。这类问题最骗人因为帧率数字不崩但手机已经默默降频。发热优化不只看 frame time还要看功耗和频率曲线。另一个案例是纹理流送在开放世界里翻车。我们把大地图分块加载后玩家快速转向时曾经出现远处建筑贴图反复从低 mip 跳到高 mip 的情况。排查发现淘汰策略把一些“刚提交加载”的块记录成可回收。修复方案是给加载器加了一个“未提交成功不可淘汰”的标记并提高视野中心页的加载优先级。从那以后跳 mip 和闪贴现象基本消失。最后再分享一个我自己的小习惯每次优化前先随手算一遍“改动前后每帧少搬多少字节”。不要只说“感觉更快了”要说具体数字例如“这个 Pass 降低到 1/2 分辨率后一帧省 15MB60 帧就是 900MB/s”。在项目评审里白纸黑字的带宽数字比“很流畅不发热”有说服力得多。优化是持续性工作把这一篇做完下一件该做的事通常就是拿起 profiler 重新抓一帧看看下一个埋伏在路上的惯犯是谁。
返回列表