第十二章性能优化一句话概括RT 优化的核心就是三个字——省带宽。生活类比快递物流——包裹越大格式越高、发得越多RT 越多、跑的路越长分辨率越高运费带宽就越贵。⏱ 30 秒概览1080p 延迟渲染管线一帧 RT 带宽约310 MB60 FPS 18.6 GB/s。优化手段按性价比排序①格式降档RGBA16F→R11G11B10F 省 50%AO 用 R8 省 87%②降分辨率SSAO/雾/SSR 可半分辨率甚至 1/4配合双边升采样③loadOpDontCare移动端省一次全屏 Load但不等于 Clear④SubpassVulkan/Metal Tile 内传递 MRTG-Buffer 不走显存⑤Memoryless RT不分配系统内存只存在于 Tile Buffer⑥RT Pool Transient RT避免 alloc/free⑦Memory AliasingDX12 PlacedResource / Vulkan Memory Binding生命周期不重叠的 RT 共享显存省 ~40%⑧不要破坏 GPU 的 DCC/AFBC 隐式压缩UAV 读写、CPU Readback、格式重解释都可能打断。12.1 实测一帧到底用了多少 RT 带宽先看真实数据建立直觉。以一个 1080p 延迟渲染管线为例用 RenderDoc / Nsight 抓帧后统计PassRT 写入RT 读取带宽估算G-Buffer4 MRT Depth4×RGBA8 D32F—写 37 MBShadow Map4 CSM, 2048²4×D16(2048²)—写 32 MBSSAO半分辨率R8(960×540)Depth Normal写 0.5 MB 读 16 MBLightingRGBA16FG-Buffer×4 Shadow SSAO写 16 MB 读 ~90 MBBloom5 级降采样升采样10 张各级 RT同级 RT写读 ≈ 30 MBTAA ResolveRGBA16FCurrent History MV写 16 MB 读 48 MBToneMapping UIRGBA8Back BufferHDR RT写 8 MB 读 16 MB总计~310 MB/帧在 60 FPS 下310 MB × 60 18.6 GB/s。这只是 RT 的带宽——还不包括顶点数据、纹理采样、Buffer 读写。一块中端 GPU如 RTX 3060的总带宽约 360 GB/sRT 带宽可能占 10~20%。4K 分辨率下数字粗暴地乘以 4~1240 MB/帧74 GB/s——接近总带宽的 20~30%。结论RT 带宽是实实在在的性能瓶颈尤其在高分辨率和移动端。优化前的飞行检查清单在开始优化之前先用这份清单快速审计你的管线列出一帧中所有 RT标注格式、尺寸、读写次数计算每张 RT 的 字节/像素 × 分辨率 × 读写次数 单张带宽汇总总带宽对比 GPU 理论带宽计算占比标记所有loadOp Load的 RT——哪些可以改为DontCare或Clear标记所有storeOp Store的 RT——哪些 Transient RT 可以改为DontCare检查每张 RT 的格式——有没有RGBA16F 但只用了 R 通道的情况检查 SSAO/Fog/SSR 等效果——是否可以降到半分辨率或更低检查生命周期——有没有两张 RT 生命周期不重叠、可以 Alias检查 RT 切换模式——有没有可以合并为同一 Render Pass 的相邻 Pass移动端是否用了 SubpassMemoryless12.2 格式选择优化——用最小够用的格式格式每低一档带宽直接减半。这是最直接的优化。场景过度格式最优格式节省AO BufferRGBA16F4×28 B/pixR81 B/pix87.5%Shadow MapD32F4 B/pixD162 B/pix50%HDR 场景色RGBA32F16 B/pixRGBA16F8 B/pix50%HDR 场景色无 AlphaRGBA16F8 B/pixR11G11B10F4 B/pix50%Motion VectorRGBA16F8 B/pixRG16F4 B/pix50%G-Buffer NormalRGBA16F8 B/pixRG16_SNORM Octahedron4 B/pix50%常见过度配置陷阱SSAO 用 RGBA16F——AO 只是一个 0~1 的标量R8 足矣Shadow Map 用 D32F——方向光 CSM 用 D16 精度完全够D32F 仅在点光源近距离阴影有必要所有 G-Buffer 用 RGBA16F——BaseColor 和材质参数用 RGBA8 精度足够Motion Vector 用 RGBA16F——只需要 RG 两个通道BA 浪费R11G11B10F 的妙用R11G11B10F是一个 32 位的 HDR 格式R 和 G 各 11 位浮点B 为 10 位浮点带宽和 RGBA8 一样但可以存 HDR 颜色。限制没有 Alpha 通道B 通道精度稍低10 位 vs 11 位不支持负数只有正范围浮点适用场景HDR 场景颜色如果不需要 Alpha 混合——Bloom、Lighting 输出环境探针——Cubemap 的颜色存储12.3 分辨率策略半分辨率 / 1/4 分辨率渲染不是所有效果都需要全分辨率。效果典型降分辨率原因SSAO半分辨率1/2AO 变化频率低降分辨率后模糊可弥补体积雾/体积光1/4 ~ 1/8 分辨率雾是低频信号SSR半分辨率反射本身比较模糊Bloom 低级 Mip1/16 ~ 1/32本就是降采样的产物后视镜1/4 ~ 1/8后视镜物理尺寸很小升采样的质量关键降分辨率渲染后需要升采样回全分辨率。如果直接双线性插值边缘会出现明显的阶梯或线条模糊。好的做法双边/保边升采样——用深度作为权重确保升采样时不跨越深度边缘。// 双边升采样伪代码 float4 bilateral_upsample(float2 uv) { float centerDepth FullResDepth.Sample(uv); float4 result 0; float totalWeight 0; // 对低分辨率RT的2×2邻域采样 for (int y 0; y 1; y) for (int x 0; x 1; x) { float2 sampleUV ... ; // 低分辨率纹理的采样点 float sampleDepth HalfResDepth.Sample(sampleUV); float4 sampleColor HalfResRT.Sample(sampleUV); float depthWeight 1.0 / (abs(centerDepth - sampleDepth) * 1000 0.001); result sampleColor * depthWeight; totalWeight depthWeight; } return result / totalWeight; }12.4 RT 切换代价——为什么频繁切换是灾难每次切换 RTSetRenderTarget/BeginRenderingGPU 需要Flush 当前管线——等所有 in-flight 的像素写完可能压缩/解压当前 RT——DCC 等压缩格式需要 Finalize可能Resolve MSAA——如果是 MSAA RT加载新 RT 的元数据到 Cache执行 Clear如果有在 Tile-Based GPU 上代价更大——RT 切换意味着Tile Buffer 的 Store 和新 RT 的 Load直接等于一次全屏显存读写。⚠️行业经验某团队将 SSAO 从独立 Render Pass 改为 Compute Shader同一 Render Pass 内用 UAV 输出减少了一次 RT 切换。在 Adreno 650 上单次切换省了 0.15ms——看似微小但他们一帧有 28 次 RT 切换类似优化累积后省了 2ms从 14ms→12ms帧率从 71fps→83fps。移动端每次 RT 切换都有价格标签。实际测量在 Mali G78 上测量一次 RT 切换的代价约0.1~0.3ms取决于分辨率和是否有 Store/Load。如果一帧切换 30 次 RT就是 3~9ms——已经占掉 16ms60FPS 预算的 20~50%。优化策略排序 Pass 减少切换——把写入同一 RT 的 Draw Call 放一起Merge Render Pass——Vulkan/Metal 的 Subpass 可以在同一 Render Pass 内切换 Input Attachment不需要真正的 RT 切换Atlas 代替多张小 RT——把多个 Shadow Map 画到同一张大 Atlas 上而非切换 RTFrame Graph 自动编排——系统自动最小化 RT 切换次数12.5 移动端三板斧loadOpDontCare / Subpass / Memoryless移动端 GPU 是 Tile-Based 架构RT 性能优化有三板斧第一板斧loadOp DontCare如果你确定后续渲染会覆盖 RT 的所有像素比如全屏 Quad那么 Load 旧内容毫无意义——浪费一次全屏读取。// MetalrenderPassDescriptor.colorAttachments[0].loadAction.dontCare// 不加载旧内容renderPassDescriptor.colorAttachments[0].storeAction.store// 写完存回显存// VulkanattachmentDesc.loadOpVK_ATTACHMENT_LOAD_OP_DONT_CARE;在 Tile-Based GPU 上DontCare省掉的是从显存加载到 Tile Buffer 的带宽——对于 1080p RGBA8 就是 8 MB。注意DontCare不等于ClearDontCare意味着 Tile Buffer 中残留随机旧数据。如果你的渲染没 100% 覆盖所有像素会看到花屏。第二板斧SubpassVulkan / Metal Tile Shader第八章已经讲过 Subpass 的原理。在移动端 Tile-Based GPU 上Subpass 的收益是决定性的无 Subpass G-Buffer Pass → Store 到显存4 RT × 8 MB 32 MB Lighting Pass → 从显存 Load G-Buffer32 MB 有 Subpass G-Buffer Subpass → 数据留在 Tile Buffer0 带宽 Lighting Subpass → 直接从 Tile Buffer 读取0 带宽 只把最终颜色 Store 到显存8 MB 节省~56 MBStore 32 MB Load 32 MB - Store 8 MB第三板斧Memoryless RT标记为 Memoryless 的 RT 根本不分配系统显存——数据只存在于 Tile Buffer 中。适用条件RT 只在同一个 Render Pass 内被写入和读取不需要跨帧保持不需要被其他 Pass 的纹理采样器读取典型场景G-Buffer如果用 Subpass 合并了 LightingMSAA RT如果 Resolve 在同一 Render Pass 内完成Transient Depth Buffer// MetalletdescMTLTextureDescriptor.texture2DDescriptor(...)desc.storageMode.memoryless// 不分配系统内存// VulkanVkMemoryPropertyFlags flagsVK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT;// Lazy allocation 只在实际需要时分配如果整个生命周期在 Tile 内完成则不分配12.6 RT Pool 与 Transient RT 的生命周期管理Pool 的清理策略RT Pool 如果只进不出显存会不断增长。需要过期清理// 每帧检查 Pool 中的空闲 RTfor(autort:pool.idleRTs){rt.idleFrameCount;if(rt.idleFrameCountMAX_IDLE_FRAMES){// 如 300 帧 5 秒未使用rt.release();pool.remove(rt);}}帧内 Transient RT 的优化一帧中很多 RT 用完就不需要了Pass 1: 写 RT_SSAO Pass 2: 读 RT_SSAO, 写 RT_Lighting Pass 3: RT_SSAO 不再需要 → 可以释放回 PoolFrame Graph 自动追踪每个 RT 的最后使用 Pass及时归还 Pool。手动管理则需要程序员显式调用 Release。12.7 Aliasing Memory显存共享两张 RT生命周期不重叠时可以共享同一块显存。DX12Placed Resource Heap Aliasing// 创建一个 Heap一块显存D3D12_HEAP_DESC heapDesc{};heapDesc.SizeInBytes64*1024*1024;// 64MBheapDesc.Properties.TypeD3D12_HEAP_TYPE_DEFAULT;device-CreateHeap(heapDesc,heap);// 两个 RT 放在同一个 Heap 的同一个偏移device-CreatePlacedResource(heap,0,rtDescA,stateA,nullptr,rtA);device-CreatePlacedResource(heap,0,rtDescB,stateB,nullptr,rtB);// 同偏移// 使用前需要 Aliasing BarrierD3D12_RESOURCE_BARRIER aliasingBarrier{};aliasingBarrier.TypeD3D12_RESOURCE_BARRIER_TYPE_ALIASING;aliasingBarrier.Aliasing.pResourceBeforertA;aliasingBarrier.Aliasing.pResourceAfterrtB;cmdList-ResourceBarrier(1,aliasingBarrier);// 现在 rtB 可以使用rtA 的内容作废VulkanMemory Binding// 分配一块 MemoryVkDeviceMemory memory;vkAllocateMemory(device,allocInfo,nullptr,memory);// 两个 Image 绑到同一块 MemoryvkBindImageMemory(device,imageA,memory,0);// ... 用完 imageA 后vkBindImageMemory(device,imageB,memory,0);// 同偏移覆盖 imageA实际节省根据 Frostbite 团队的数据Frame Graph 的自动 Aliasing 可以在 Battlefield V 中节省约40% 的 Transient RT 显存。12.8 RT 压缩——你看不见的带宽优化与它的破坏者DCC / AFBC / Lossless Compression 原理概述现代 GPU 有内置的无损 RT 压缩——数据在显存中存储的是压缩形式ROP 读写时自动解压/压缩。GPU 厂商压缩技术典型压缩率NVIDIADCCDelta Color Compression2:1 ~ 4:1AMDDCC类似名称但不同实现2:1 ~ 4:1ARM MaliAFBCArm Frame Buffer Compression1.5:1 ~ 4:1Qualcomm AdrenoUBWCUniversal Bandwidth Compression2:1 ~ 4:1压缩对开发者完全透明——你不需要做任何事GPU 硬件自动完成。一张 RGBA8 1080p 的 RT 理论上占 8 MB压缩后可能只占 2~4 MB 带宽。哪些操作会打断压缩压缩有一个压缩元数据metadata记录每个 Tile 的压缩状态。某些操作会导致元数据失效强制 GPU 解压整个 RT格式重新解释——同一块显存作为不同格式读取Typed UAV 读取一个本该是 RT 的资源CPU Readback——Map 到 CPU 时需要先解压跨 GPU 传输——SLI / CrossFire 场景某些 Resolve 操作——MSAA Resolve 可能导致目标 RT 失去压缩Aliasing——两个不同格式的 RT 共享同一块 Heap 时压缩元数据可能冲突如何通过 Profiling 发现压缩失效Nsight Graphics 和 Mali Offline Compiler 可以显示 RT 的压缩状态NsightResource Inspector 中查看 “Compression State”StreamlineArm看 “External Read/Write Bytes” 是否远大于理论最小值RenderDocTexture Viewer 中看实际显存占用 vs 理论占用如果你发现某张 RT 的实际带宽接近未压缩大小检查是否有 UAV 读写这张 RT是否有 CPU Readback是否做了格式转换或 Resolve本章小结RT 性能优化速查表优化手段节省的资源实施难度适用平台选择最小格式带宽 50~87%★☆☆全平台降分辨率带宽 75%1/2 res★★☆全平台loadOp DontCare带宽1次全屏 Load★☆☆移动端收益大Subpass带宽Tile 内传递★★☆Vulkan/MetalMemoryless显存 100%某张 RT★★☆移动端RT PoolCPU 时间避免 alloc★★☆全平台Memory Aliasing显存 ~40%★★★DX12/Vulkan减少 RT 切换GPU 空闲时间★★☆全平台移动端尤其重要RT 压缩保护带宽 2~4×★☆☆全平台隐式注意不要破坏核心原则算清楚帐——先量化每张 RT 的带宽代价再有的放矢格式能降就降——这是性价比最高的优化移动端用好 loadOp/storeOp Subpass Memoryless——这三个加起来可以节省一半以上 RT 带宽不要破坏 GPU 的隐式压缩——这是免费带宽别不小心弄丢了设计哲学性能优化的本质是用信息换效率 回顾本章所有优化手段背后有一条统一的线索你向 GPU 提供的信息越精确GPU 能做的优化就越多。告诉 GPU “这张 RT 之前的内容我不要了”loadOp DontCare→ GPU 省一次全屏 Load告诉 GPU “这张 RT 写完不用存”storeOp DontCare→ GPU 省一次全屏 Store告诉 GPU “这两张 RT 生命周期不重叠”Memory Aliasing→ GPU 让它们共享同一块显存告诉 GPU “这些 Pass 的数据依赖关系”Frame Graph→ GPU 自动推导最优调度旧 API 的驱动靠猜来做这些优化经常猜错。新 API 把告诉 GPU的责任交给了开发者——优化的本质是把程序员脑中的知识显式化让机器可以利用。这和数据库查询优化提供统计信息 → 优化器选择更好的执行计划的哲学完全一致。 思考题一张 RGBA16F 的 1080p RT一帧读写各一次消耗多少带宽60 FPS 下占总带宽的百分比是多少假设 GPU 带宽 360 GB/s如果你有一个 SSAO Pass 目前使用全分辨率 RGBA16F RT列出至少 3 种可以同时叠加的优化计算每种的带宽节省比例和累积效果。Memory Aliasing 能省 ~40% 显存但有没有可能引入 Bug需要什么前提条件才能安全使用下一章讲当 RT 出了问题如何快速诊断。优化做得越激进出错的可能性越高——一张 DontCare 用错的 RT、一个漏掉的 Barrier、一条 sRGB 转换链路中断——这些 Bug 的症状是画面看起来差不多但哪里不对。你将获得一套系统化的 RT 故障排查流程像查病历一样对着症状表找病因。