不必要的 RT 切换与 Resolve:手机 GPU 最贵的那笔隐形账单

发布时间:2026/9/21 0:52:25

不必要的 RT 切换与 Resolve:手机 GPU 最贵的那笔隐形账单 抓帧报告1080p训练场静止不动Draw Calls : 412 ← 还行 Triangles : 1.2 M ← 还行 Texture Read : 180 MB/s ← 还行 ──────────────────────────────── RenderPass : 23 ← ⚠️ Framebuffer Traffic : 1,840 MB/s ← 一个站着不动的画面每秒往内存里搬了1.8 GB。这些流量不是纹理不是顶点不是任何内容。它是同一块画布被反复地从桌上收进仓库、又从仓库搬回桌上——来回 23 趟。而在手机上搬运比绘制贵得多。第一幕一张便签和一个仓库手机 GPU 和 PC GPU是两种完全不同的生物PC 显卡IMR立即模式渲染显存带宽 500 GB/s 起步有独立供电有风扇。它的策略很简单粗暴Framebuffer 就放在显存里随便读随便写。手机 GPUTBR / TBDR分块渲染和 CPU 共享同一条 LPDDR 内存总带宽约30~60 GB/s还要和 CPU、ISP、显示控制器抢。它的策略是把屏幕切成一个个小方块Tile通常 16×16 或 32×32 像素 ↓ 每次只处理【一个 Tile】 ↓ 这个 Tile 的颜色/深度/模板全部放在【片上 SRAM】里Tile Memory ↓ 整个 Tile 画完了才一次性写回 DRAM用一个比喻类比速度能耗Tile Memory片上 SRAM你手边的便签纸极快极低System MemoryDRAM走廊尽头的仓库慢高 100~1000 倍这是全篇的地基在手机上一次外部 DRAM 访问的能耗约等于几百次片上运算。Arm、Imagination 在历年移动图形技术分享中反复强调的量级所以移动端渲染优化的第一原则不是少算而是“让数据待在便签上别往仓库跑。”一个 RenderPass 的一生【Load】 从仓库把画布搬到便签上或者直接在便签上清空 ↓ 【Render】 在便签上画画画多少笔都不贵因为都在片上 ↓ 【Store】 把便签上的结果搬回仓库注意 Render 那一步在同一个 RenderPass 内部你画 10 层还是 100 层 Overdraw都不产生 DRAM 流量。全部在片上完成。而 Load 和 Store每一次都是实打实的全屏内存搬运。算笔账1080pColor RGBA8 : 1920 × 1080 × 4 B 8.3 MB Depth D24S8 : 1920 × 1080 × 4 B 8.3 MB ──────────────────────────────────────────── 一次 Store Load 往返 33 MB × 60 fps 约 2 GB/s一次多余的 RT 切换就是 2 GB/s。而你的总带宽预算只有几十 GB/s还要分给 CPU 和纹理采样。这就是序幕里那 1840 MB/s 的来源。第二幕两个被全世界忽略的枚举Metal / Vulkan 在 API 层面把 Load 和 Store显式暴露了出来。这是移动图形 API 设计里最重要的一处细节也是最少人正确配置的一处。Load Action这个 RenderPass 开始时怎么拿到画布值干什么成本Load从 DRAM 把上一帧/上一个 pass 的内容读回来全屏读Clear直接在片上填一个固定值几乎免费DontCare什么都不做内容是垃圾完全免费Store Action这个 RenderPass 结束时结果怎么处理值干什么成本Store写回 DRAM全屏写DontCare丢弃完全免费MultisampleResolve在片上把 MSAA 多采样合并成单采样只写回结果几乎免费StoreAndMultisampleResolve两个都写最贵由此产生的两条黄金规则规则一Clear永远优于Load。// ❌ 最常见的错误以为 Clear 是浪费于是关掉了// 结果 GPU 只能老老实实把上一帧的 8.3MB 读回来colorAttachment.loadActionLoad;// ✅ Clear 是在片上做的它反而比 Load 便宜colorAttachment.loadActionClear;// ✅✅ 如果你的场景保证铺满全屏天空盒/全屏背景最优colorAttachment.loadActionDontCare;这条规则违反直觉但在移动端是铁律“不清屏不是优化是把成本从片上填充换成了全屏 DRAM 读取”。规则二深度缓冲99% 的情况应该DontCare。这是最常见、最昂贵、最容易修的浪费。主 Pass 画完 → 深度缓冲被 Store 回 DRAM8.3MB → 然后……再也没有人读它 → 下一帧开头它被 Clear 掉每秒白白搬运 0.5 GB纯浪费。depthAttachment.storeActionDontCare;// 改一行省 0.5 GB/s什么时候不能 DontCare只有两种后面有 pass 要读深度软粒子、SSAO、雾效、深度描边分了多个 pass 但要共享深度如不透明和半透明分开而即便如此你也应该重新设计让它们待在同一个 RenderPass 里下一幕。一个价值巨大的推论MSAA 在移动端几乎免费PC 上 MSAA 4x 很贵因为 framebuffer 变成 4 倍大全在显存里进出。但在 TBDR 上多采样数据只存在于 Tile Memory 里MultisampleResolve在片上完成写回 DRAM 的仍然是单采样结果。❌ storeAction Store → 4× 数据全写回 DRAM33 MB ✅ storeAction MultisampleResolve → 只写单采样8.3 MB移动端 MSAA 4x 的正确成本是多一点片上计算 多占一点 tile memory可能让 tile 尺寸变小。带宽成本接近于零。所以在手机上MSAA 往往比 FXAA/TAA 更划算——后者是全屏后处理要多一次完整的 RT 往返。⚠️唯一的代价MSAA 会占用 tile memory可能导致 GPU 把 tile 尺寸从 32×32 降到 16×16binning 开销上升。所以要实测不是无脑开。第三幕什么叫不必要必要的切换后一个 pass 需要随机采样前一个 pass 的整张图。比如高斯模糊——每个像素要读周围 9 个像素它必须等整张图画完。不必要的切换后一个 pass 只需要当前像素的前一个结果。这个区别就是一切。需要「整张图」 → 必须落 DRAM → 真的要切 RT 需要「同一个像素」→ 数据就在片上 → 【根本不该切】而现实是大量后处理只需要同一个像素却被写成了独立的 RenderPass。效果真实需求常见实现该不该切色调映射Tonemap当前像素独立 pass❌ 不该颜色分级LUT当前像素独立 pass❌ 不该暗角Vignette当前像素独立 pass❌ 不该色差 / 胶片颗粒当前像素独立 pass❌ 不该Bloom 模糊邻域 / 降采样独立 pass✅必须景深邻域独立 pass✅ 必须延迟光照当前像素的 GBuffer独立 pass❌可用 Subpass第四幕现代武器库——让数据留在便签上三大厂商给了同一件事三个名字本质完全一样API机制做什么VulkanSubpass Input Attachment多个 pass 在同一块 tile memory 上接力MetalTile Shading / Imageblock、Memoryless Texture同上且 RT 可声明为永不落 DRAMOpenGL ESFramebuffer Fetch、Pixel Local Storage片元着色器直接读当前像素的颜色/深度核心能力只有一句话让下一个 pass 直接读取当前像素在 tile memory 里的值中间结果永不写回 DRAM。这让移动端延迟渲染成为可能❌ 传统延迟渲染移动端灾难 GBuffer Pass → Store 4 张 RT 到 DRAM33 MB Lighting Pass → Load 4 张 RT 回来33 MB 每帧 66 MB60fps 4 GB/s ✅ Subpass 延迟渲染 GBuffer Subpass → 结果留在 tile memory Lighting Subpass → 直接从 tile memory 读Input Attachment GBuffer 声明为 Memoryless / DontCare 只有最终颜色写回 DRAM8.3 MB这就是为什么 Unity URP 在 Android/iOS 上支持延迟渲染时必须依赖 Native RenderPass API。Unity 侧的接口// URP 的 ScriptableRenderPasspublicoverridevoidConfigure(CommandBuffercmd,RenderTextureDescriptordesc){ConfigureInputAttachments(gbufferHandles);// 声明我从 tile 里读ConfigureClear(ClearFlag.None,Color.clear);}// 并在 Project Settings 中开启 Native RenderPassMetal 侧// 中间 RT 声明为 memoryless —— 它根本不会被分配 DRAMletdescMTLTextureDescriptor()desc.storageMode.memoryless// ★ 只存在于 tile memory另一件重要的事别在 RenderPass 中间打断它以下操作会强制 GPU “flush tile”把便签内容全部写回仓库操作为什么致命Texture.ReadPixels/AsyncGPUReadbackCPU 要读必须落 DRAM中途切换 RT 再切回来两次完整往返对刚画完的 RT 立刻采样必须等它完整落盘Compute Shader 插在两个 pass 之间打断 tile 流水GL.Clear在 pass 中间调用语义冲突可能触发 flush⚠️Unity 里的一个经典坑Camera.targetTexture频繁赋值/置空、或在OnRenderImage里多次Blit——每一次 Blit都是一次完整的 RenderPassLoad Store。第五幕射击游戏里的六个战场战场一那个从来没人读的深度缓冲现场序幕里的 23 个 RenderPass第一刀就砍在这里。抓帧发现主几何 pass 的depthAttachment.storeAction Store。而整条后处理链里没有任何一个 shader 采样深度。为什么会这样Unity 默认的_CameraDepthTexture开关只要有任何一个组件哪怕是一个禁用的软粒子曾经请求过深度它就会被保留。修法// URP Renderer Asset// Depth Texture: ☐ 关闭确认无人使用后// 或在自定义 pass 里显式指定cmd.SetRenderTarget(colorRT,depthRT,RenderBufferLoadAction.Clear,RenderBufferStoreAction.DontCare);// ★收益−0.5 GB/s功耗 −0.28 W。改一行配置。顺带一条如果你确实需要深度软粒子也不要用全精度 D24S8。很多效果用R16F 的线性深度就够了带宽直接减半。战场二七趟后处理合成两趟改造前的 pass 链① Main Geometry → Store (8.3 MB) ② Bloom Prefilter → Load Store ③ Blur Horizontal → Load Store ④ Blur Vertical → Load Store ⑤ Bloom Composite → Load Store ⑥ Tonemap → Load Store ⑦ Vignette Grain → Load Store ──────────────────────────────────── 7 个 RenderPass约 13 次全屏往返关键洞察⑥⑦ 只需要当前像素②~⑤ 才真的需要邻域。改造后① Main Geometry → Store ② Bloom1/4 分辨率降采样链在小图上完成 ③ ★ Uber PassBloom 合成 Tonemap LUT Vignette Grain —— 全部塞进一个 shader一趟搞定 ──────────────────────────────────── 3 个 RenderPass两个要点Bloom 必须在低分辨率做。1/4 分辨率 1/16 的像素 1/16 的带宽。而 Bloom 本来就是糊的肉眼看不出差别。Uber Shader 用multi_compile做变体而不是运行时 if——关掉的效果应该在编译期就消失。// UberPost.shader half4 frag(v2f i) : SV_Target { half3 c SAMPLE(_MainTex, i.uv).rgb; #if _BLOOM c SAMPLE(_BloomTex, i.uv).rgb * _BloomIntensity; #endif #if _TONEMAP c ACESFilm(c); #endif #if _LUT c ApplyLut(c, _LutTex); #endif #if _VIGNETTE c * Vignette(i.uv); #endif return half4(c, 1); }收益帧缓冲流量1840 → 620 MB/sGPU 功耗−0.9 W。战场三狙击镜的画中画需求8 倍镜开镜时镜片里显示一个独立视角的画面。朴素实现灾难scopeCamera.targetTexturescopeRT;// 1080p 全分辨率scopeCamera.Render();// 完整的一遍场景渲染 RT 往返// 主相机采样 scopeRT 贴到镜片上成本多一整套 RenderPass 33 MB 往返且全场景重新绘制。四项优化措施说明收益① 降分辨率镜片在屏幕上只占约 1/5 面积 → RT 用 512×512带宽 −85%② 剔除半径收缩镜内只画 300m 内 目标层不画地形细节DrawCall −60%③ 不开镜时彻底禁用scopeCamera.enabled false不是targetTexture null未开镜时零成本④ storeAction 只保留 color镜片 RT 的 depthDontCare−4 MB/帧⚠️ 一个必须守住的红线镜内的准星、弹道提示必须在主 pass 的原生分辨率上绘制不能跟着 RT 降分辨率。竞技公平性不能让步。实测开镜时的额外功耗从1.4 W 降到 0.25 W。战场四透视描边队友/敌人轮廓需求队友被墙挡住时显示蓝色轮廓这是团队射击游戏的标配。常见实现① 把角色渲染到一张 Mask RTStencil 或 R8 → RT 切换 #1 ② 对 Mask 做边缘检测/膨胀 → RT 切换 #2 ③ 合成回主画面 → RT 切换 #3三次全屏往返为了几条线。更好的方案按推荐度方案 AStencil 同 Pass 内完成最优主 Pass 内 ① 角色正常渲染时写入 Stencil 1 ② 紧接着用背面外扩 Stencil NotEqual 1 ZTest Always画一遍轮廓 → 全程在同一个 RenderPass 里零 RT 切换方案 BFramebuffer Fetch在片元着色器里直接读当前 tile 的颜色/模板做本地边缘检测——不需要整张图。方案 C如果必须用 Mask RTMask 用R8 格式不是 RGBA8带宽 1/4Mask 用1/2 分辨率轮廓略粗反而更显眼Mask 的 depthDontCare该项目选了 ART 切换 3 → 0−0.6 W。战场五MSAA 的 storeAction 写错了现象开了 MSAA 4x帧率掉 40%——远超预期。抓帧一看colorAttachment.storeAction Store // ❌后果4 倍采样的数据全部写回了 DRAM。8.3 MB × 4 33 MB 每帧只 color 一项 60fps 2 GB/s 的纯浪费修法一行colorAttachment.storeActionMultisampleResolve;// 片上解析只写单采样depthAttachment.storeActionDontCare;// MSAA depth 永远不该 store结果MSAA 4x 的性能损失从40% 降到 6%。这是本篇最值得记住的一条在移动端MSAA 不贵——错误配置的 MSAA才贵。而且贵到让人误以为手机不该开 MSAA于是转去用 FXAA/TAA——那反倒是一次真实的全屏 RT 往返。战场六UI 的三宗罪罪一UI 渲到独立 RT 做整体淡入// ❌ 为了一个 0.3 秒的淡入动画每帧多一次全屏 RT 往返uiCamera.targetTextureuiRT;修法淡入用CanvasGroup.alpha顶点色零成本只在真正需要整体后处理时才用 RT且用完立刻释放。罪二小地图每帧全量重绘// ❌ 60Hz 渲染一张 256×256 的俯视图// ✅ 降到 10Hz 只在玩家移动超过阈值时更新if(Time.time-lastMinimapUpdate0.1fmoved2f){RenderMinimap();}收益小地图成本 −83%。罪三血条/伤害数字触发的多余 Canvas 重建Unity 的Canvas 重建Rebuild虽然不是 RT 切换但它会打断渲染批次间接增加 pass 数量。✅ 动态元素血条、伤害数字、准星放【独立 Canvas】 ✅ 静态元素背景、边框放另一个 Canvas永不 dirty终幕怎么看、怎么改怎么看工具看什么Xcode GPU Capture最直观直接显示每个 RenderPass 的 Load/Store Action 和Memoryless标记Snapdragon ProfilerAdrenoRead Total (Bytes)/Write Total (Bytes)、RenderPass 数、是否走 binning modeArm Streamline / Mali Graphics DebuggerMali 的 tile 相关计数器、外部带宽RenderDocPass 结构、附件配置PC/Android 通用Unity Frame Debugger快速看有多少次Blit和 SetRenderTarget一个 5 秒判断法Framebuffer 流量 ÷ (宽 × 高 × 4B × fps) ≈ 全屏往返次数这个数字应该 ≤ 4。超过 8基本可以断定有大量不必要的切换。怎么改按性价比排序优先级动作典型收益⭐⭐⭐⭐⭐DepthstoreAction DontCare−0.5 GB/s改一行⭐⭐⭐⭐⭐后处理合并成 Uber Pass−1 GB/s⭐⭐⭐⭐⭐MSAA 用MultisampleResolve让 MSAA 从不可用变几乎免费⭐⭐⭐⭐loadAction用Clear而非Load−0.5 GB/s反直觉但重要⭐⭐⭐⭐Bloom / 粒子 / 模糊在 1/4 分辨率做−0.8 GB/s⭐⭐⭐⭐描边用 Stencil 同 pass 完成省 2~3 次往返⭐⭐⭐Subpass / Framebuffer Fetch延迟渲染必需让移动端延迟渲染可行⭐⭐⭐中间 RT 用memorylessMetal完全不分配 DRAM⭐⭐⭐降低非关键 RT 的分辨率与格式R8 / R16F按比例⭐⭐小地图、镜像等降频渲染按比例绝对不要做的五件事❌ 在 RenderPass 中间ReadPixels/ 回读 GPU❌ 对刚渲染完的 RT 立刻采样强制 flush❌ 为了省一次 Clear而用Load❌ 每帧创建/销毁 RenderTexture改用 RTHandle / 临时 RT 池❌ 用Store保存一个没人读的 attachment收尾回到序幕那 23 个 RenderPass。它们每一个看起来都合理——一个效果一个 pass代码干净职责清晰。在 PC 上这样写完全没问题。但在手机上每一次切换都不是一个 API 调用而是一次搬家。把 8 MB 的画布从便签收进仓库 走到仓库门口再把它搬回来 只为了在上面多画一笔。做 23 次。每秒 60 遍。所以移动端渲染优化最核心的思维转变是这一句不要问我画了多少东西要问我的画布来回跑了几趟仓库。Draw Call 是台面上的账人人都在看。而帧缓冲流量是那笔躺在账本背面、却真正决定你手机温度的隐形账单。它不体现在 Profiler 的任何一个醒目数字里。它只体现在——第 12 分钟玩家手心里的那片滚烫。
延伸阅读

更多相关文章

2026/9/21 0:52:25

Vue这个响应式更新陷阱你可能也踩过

上周排查一个线上问题时,我盯着屏幕上的列表数据愣了足足十秒——明明更新了数组里的对象属性,视图却像是被冻住了一样纹丝不动。你可能也遇到过这种场景:你以为 Vue 的响应式系统该触发更新了,但它偏偏没动静。今天我们就扒一扒这…

2026/9/21 0:52:25

Vue响应式数据这个坑我栽了两次,分享给你避坑

"为什么这个列表渲染总是慢半拍?"我在一次用户行为分析页面的性能优化中盯着控制台沉思,明明数据量不大(500条记录),展开折叠操作却卡得像是处理上万条数据。后来在另一个后台系统中,动态表单的字…

2026/9/21 0:52:25

Java线程池用错参数,我的服务居然悄悄崩溃了

上周四凌晨,监控突然报警:核心服务的线程池队列积压了 3 万任务,下游调用超时率飙升到 40%,但 CPU 使用率却只有 5%。你一定猜到了——线程池又双叒叕配错了!但这次的问题比想象中更隐蔽:服务没有直接崩溃&…

2026/9/21 1:57:30

SAP按销售订单结算全解析:从配置到月结实践

简介:按销售订单结算之SAP系统的配置及操作是一份聚焦SAP中按单结算场景的专业操作资料,面向SAP实施顾问、财务成本控制关键用户及制造企业IT人员,重点区分“无差异模式”与“有差异模式”两种业务场景,系统梳理从生产订单结算、销…

2026/9/21 1:57:30

TensorRT与ONNX Runtime全流程实战:选型、转换与推理优化

在深度学习模型从“能跑”到“能上线”这件事上,推理引擎的选择和调优几乎决定了最终体验。TensorRT 和 ONNX Runtime(ORT)是我最近一年多里用得最多的两套推理方案,也是社区里讨论热度一直居高不下的组合。如果你正在做模型部署&…

2026/9/21 1:57:30

华为五看三定:从战略规划到落地执行的全套实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/21 1:57:30

Fun-CosyVoice3 Windows本地部署实战:TTS声音克隆与数字人接入

数字人项目里最磨人的往往不是形象和驱动,而是背后那条“出声”的链路。我最近把一套数字人播报系统往Windows机器上迁移,核心TTS选的是Fun-CosyVoice3-0.5B-2512。本想着这类开源模型在Linux上跑得很顺,换到Windows最多改改路径,…

2026/9/21 1:52:29

嵌入式Linux存储系统设计:介质选型、掉电保护与OTA容错方案

简介:一份关于基于嵌入式Linux的存储系统设计的技术文档,适合嵌入式开发者和NAS设备选型工程师阅读。针对中小企业和家庭用户大容量数据存储、共享与安全管理的难题,这份文档提出并阐述了以Cortina CS3516芯片为核心的低成本整体解决思路。资…

2026/9/20 0:04:49

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/20 0:04:49

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/21 0:02:23

OpenResearch:构建可复现的开放式研究工作流

第一次看到“OpenResearch”这个名字,我脑子里冒出的不是某个具体软件,而更像一种研究方式的宣言:开放、可复现、可验证。这三件事放在一起,其实比大多数人想象中难得多。过去几年我一直在折腾自己的研究工作流,从纯纸…

2026/9/20 4:54:47

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/20 5:01:23

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/20 5:09:33

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码