[图形渲染]讲透RenderTarget 第五章:硬件视角——GPU 怎么“画进去“的

张开发
2026/4/4 23:18:14 15 分钟阅读

分享文章

[图形渲染]讲透RenderTarget 第五章:硬件视角——GPU 怎么“画进去“的
第五章硬件视角——GPU 怎么画进去的一句话概括ROP 是 GPU 管线的笔尖显存布局决定纸的质地。生活类比工厂流水线的最后一站——包装工ROP把产品像素放进不同型号的箱子显存布局里。⏱ 30 秒概览ROPRender Output Unit是 GPU 管线末端负责深度测试、模板测试、混合和最终写入的硬件单元。显存中 RT 的物理布局有三种线性CPU 友好、分块 TiledGPU Cache 友好、DCC 压缩带宽减半但透明。带宽是 RT 性能的核心敌人——1080p RGBA16F 一次读写 16 MB。移动端Tile-Based GPU把 RT 切成 16×16 Tile在片上 SRAM 完成渲染只做一次 Store 回显存省带宽 2~4×。MSAA RT 每像素存 N 个 sample需要 Resolve 成单 sample 才能采样。现代 API 的loadOp/storeOp声明给 driver 提前知道哪些数据可以不 Load/不 Store——这是移动端最重要的性能承诺。前四章建立了对 RenderTarget 的概念认知。从本章开始我们深入底层——看看 GPU 硬件是如何把像素写进RenderTarget 的。理解硬件才能真正理解为什么某些操作昂贵、某些优化有效。5.1 GPU 管线末端ROPRender Output UnitGPU 的图形管线从顶点开始经过顶点着色、图元装配、光栅化、片段着色最后来到管线的末端——ROPRender Output Unit也叫Output MergerDX 术语。ROP 是 GPU 中专门负责把像素写进RenderTarget 的硬件单元。每个 ROP 单元在一个时钟周期内处理一个或多个像素的输出。ROP 干的事情片段着色器输出 → [深度测试] → [模板测试] → [混合(Blending)] → [写入 RT]深度测试Depth TestROP 读取 Depth RT 中对应位置的旧深度值与新片段的深度比较如果新片段更近或满足设定的比较函数→ 通过测试如果更远 → 丢弃该片段如果通过测试新的深度值也会写回 Depth RT。Early-Z提前深度测试现代 GPU 会在片段着色器之前进行一次深度测试预判。如果能提前确定片段会被遮挡就不用执行昂贵的片段着色器了。但如果 Shader 中修改了深度值gl_FragDepth或使用了discard/clip()Early-Z 会被禁用。模板测试Stencil TestROP 读取 Stencil RT 中的模板值与参考值比较。根据通过/失败的结果可以更新模板值。混合Blending如果像素通过了深度和模板测试ROP 执行混合操作。混合需要读取 RT 中该位置的旧颜色值最终颜色 SrcFactor × 新颜色 DstFactor × 旧颜色常见混合模式不透明SrcFactor1, DstFactor0新颜色完全替换旧颜色无需读旧值Alpha 混合SrcFactorSrcAlpha, DstFactor1-SrcAlpha半透明效果加法混合SrcFactor1, DstFactor1用于光效叠加注意混合需要**读-改-写Read-Modify-Write**操作——先读旧值再计算新值最后写入。这意味着混合比不透明渲染多了一次读操作带宽更高。写入最后ROP 把最终的颜色值写入 Color RT把深度值写入 Depth RT把模板值写入 Stencil RT。ROP 数量与性能GPU 的 ROP 数量直接影响像素填充率Fill Rate。例如NVIDIA RTX 4090176 ROPsAMD RX 7900 XTX96 ROPs移动端 GPU如 Mali-G784~8 ROPs更多的 ROP 更高的像素吞吐量。但 ROP 的性能也受限于显存带宽——如果带宽不够ROP 再多也跑不满。5.2 显存布局线性 / 分块Tiled/ Delta Color CompressionROP 把像素写进显存但显存中的数据不是简单地按行存储的。显存布局Memory Layout决定了数据在物理显存中的排列方式直接影响缓存命中率和带宽效率。线性布局Linear / Row-Major像素按照行的顺序存储——第 0 行的所有像素、第 1 行的所有像素……内存地址: [P(0,0)][P(1,0)][P(2,0)]...[P(W-1,0)][P(0,1)][P(1,1)]...优点简单CPU 容易读写适合回读。缺点空间局部性差。当 GPU 渲染一个三角形时像素分布在二维空间中但线性布局只在水平方向有连续性。垂直方向相邻的像素在内存中可能相隔很远整整一行的宽度导致缓存不命中。分块布局Tiled / Morton / Z-Order把图像分成小块Tile每个块内的像素在内存中是连续的。常见的块尺寸是 4×4、8×8 或 16×16 像素。┌─────┬─────┐ │Tile0│Tile1│ 每个 Tile 内部的像素在内存中连续 │ 4×4 │ 4×4 │ ├─────┼─────┤ │Tile2│Tile3│ │ 4×4 │ 4×4 │ └─────┴─────┘ 内存: [Tile0 的 16 个像素][Tile1 的 16 个像素][Tile2 的 16 个像素]...很多 GPU 使用Morton 曲线Z-order curve来排列块内像素确保空间上相邻的像素在内存中也尽量相邻。优点二维局部性好——上下左右相邻的像素在内存中距离更近缓存命中率大幅提高。缺点CPU 直接读写不直观需要 Detile从 GPU 回读数据时需要格式转换。Delta Color CompressionDCC现代桌面 GPUNVIDIA、AMD对 RT 数据进行无损压缩。核心思想是相邻像素的颜色值往往非常接近——同一面墙的颜色几乎一样只有微小差异。GPU 只存储一个基准值和各像素的差值Delta大幅减少显存带宽。AMDDelta Color CompressionDCCNVIDIA类似的无损压缩具体名称不公开但原理相同ARM MaliAFBCArm Frame Buffer CompressionDCC 的关键特性对 Shader 透明——Shader 读写时看到的是正常的像素值压缩/解压由硬件自动完成某些操作会打断压缩——当数据变得难以压缩如噪声图或者经过某些格式转换时GPU 可能被迫解压数据并以未压缩格式存储带宽突然翻倍详见第 12.8 节5.3 Color / Depth / Stencil Attachment 的物理结构当你创建一个带 Color Depth Stencil的 Framebuffer 时GPU 在显存中实际分配了多块独立的内存区域Color Attachment存储 RGBA 颜色值。物理上是一块二维的显存区域格式由你创建时指定RGBA8、RGBA16F 等。如果使用 MRT每个 Target 独占一块内存。Depth Attachment存储深度值。常见格式D16每像素 2 字节D24每像素 3 字节通常对齐到 4 字节与 Stencil 共存D32F每像素 4 字节Depth 数据有很好的压缩特性——因为相邻像素的深度值通常非常接近同一平面上。GPU 对深度数据的压缩比颜色数据更激进通常能达到 2:1 到 4:1 的压缩比。Stencil Attachment存储 8 位整数模板值。物理上Stencil 通常和 Depth 打包在一起D24S824 位深度 8 位模板共 32 位/像素D32F_S832 位浮点深度 8 位模板实际占 64 位/像素8 位模板对齐到 32 位它们是独立的还是打包的取决于格式D24S8Depth 和 Stencil 交错打包在同一块内存中。每个像素占 4 字节前 24 位是深度后 8 位是模板。某些 GPU 可以选择分离模式Depth 和 Stencil 各占一块独立的内存。这在只需要读 Depth 不需要 Stencil 时可以减少带宽——读 Depth 不会浪费地读入 Stencil 的数据。5.4 带宽是大敌——RT 的分辨率和格式为什么如此敏感显存带宽Memory Bandwidth是 GPU 执行 RT 读写时最大的瓶颈。让我们算一下假设渲染一帧需要4 张 G-Buffer RT每张 1920×1080RGBA16F8 字节/像素1 张深度 RT1920×1080D32F4 字节/像素每张 RT 被写一次 被读一次单张 Color RT 大小 1920 × 1080 × 8 16.6 MB 4 张 Color RT 66.4 MB 1 张 Depth RT 1920 × 1080 × 4 8.3 MB 总写入 66.4 8.3 74.7 MB 总读取 ≈ 74.7 MB后续 Pass 要采样这些 RT 单帧 RT 总带宽 ≈ 150 MB这只是 G-Buffer 的。加上后处理链Bloom 需要多次降采样/升采样、Shadow Map、SSAO 等一帧的 RT 带宽很容易超过500 MB。如果帧率是 60fps每秒 RT 带宽500 MB×6030 GB/s\text{每秒 RT 带宽} 500 \text{ MB} \times 60 30 \text{ GB/s}每秒RT带宽500MB×6030GB/s而 GPU 的总显存带宽是有限的比如 RTX 4070 Ti 的带宽约 504 GB/s移动端 Adreno 730 约 51 GB/s。RT 的读写只是众多带宽消耗者之一——顶点数据、纹理采样、Compute Shader 等都在争抢带宽。下面的表格列出了不同级别 GPU 的显存带宽帮你建立直觉GPU类型显存带宽上述 RT 30 GB/s 占比RTX 4090 (2022)桌面旗舰1,008 GB/s~3%RTX 3060 (2021)桌面中端360 GB/s~8%Apple M2 Pro (2023)统一内存200 GB/s~15%Adreno 730 (2022)移动旗舰51 GB/s~59%Mali-G78 (2020)移动中端~25 GB/s超过 100%⚠️结论同样的 RT 方案桌面 GPU 绰绰有余移动端 GPU 连理论上限都不够。——这就是 Tile-Based 架构存在的根本原因。这也是为什么格式选择如此重要RGBA16F8 字节比 RGBA84 字节多一倍带宽。如果不需要 HDR 范围用 RGBA8 能直接砍半带宽。分辨率如此敏感4K3840×2160是 1080p 的 4 倍像素——RT 带宽也翻 4 倍。半分辨率渲染是常见优化SSAO、反射等效果用半分辨率 RT 就够了带宽减 75%。5.5 Tile-Based GPU移动端On-chip Memory 与 Load/Store移动端 GPUARM Mali、Qualcomm Adreno、Apple GPU、Imagination PowerVR几乎全部采用Tile-Based 架构TBR / TBDR。这种架构对 RenderTarget 的处理方式与桌面 GPU 截然不同。桌面 GPUImmediate Mode的方式桌面 GPU 采用即时模式每个三角形一画、像素一写入显存。所有的 RT 读写直接走显存带宽。Tile-Based GPU 的方式Tile-Based GPU 把屏幕分成小块通常 16×16 或 32×32 像素每个 Tile 的处理完全在 GPU 片上内存On-chip Memory / Tile Buffer中完成各家 GPU 的 Tile 大小与 Tile Buffer 容量有所不同GPUTile 大小Tile Buffer 容量单 Tile 可容纳的 RT 数据Mali-G78 (2020)16×16~16 KB/core4×RGBA8 D24S8 刚好Adreno 730 (2022)可变最大 32×32~64 KB/core充裕Apple M2 GPU (2022)32×32~128 KB/core可轻松支持 4×RGBA16FPowerVR BXS (2023)32×32~64 KB/core核心限制如果 MRT 数量 × 格式大小 Tile Buffer 容量GPU 被迫拆分 Tile 或 Flush——性能悬崖式下降。这就是移动端 G-Buffer 不能随意加 RT 的硬性原因。每个 Tile 的处理流程 1. Load把 RT 中该 Tile 区域的数据从显存加载到片上内存 2. 渲染所有影响该 Tile 的三角形都在片上内存中完成读写都是 On-chip 3. Store把片上内存中的最终结果写回显存片上内存的优势速度极快比显存快 10~100 倍功耗极低数据不过外部总线容量有限通常每个 Tile 只有几 KB 到几十 KB关键性能启示Load 和 Store 操作走显存带宽——它们是需要最小化的目标Tile 内部的读写是免费的——同一 Render Pass 内的深度测试、混合等操作都在片上内存中完成不消耗外部带宽如果你不需要 Load 旧内容可以用loadOp DontCare——直接省掉 Load省的就是实打实的带宽如果中间 RT 只是临时用最后不需要保留可以用storeOp DontCare——不写回显存省 Store 的带宽这就是为什么移动端的loadOp/storeOp设置如此重要——每个设置直接映射到读不读显存和写不写显存的决定。5.6 MSAA RenderTarget 与 Resolve 操作MSAAMulti-Sample Anti-Aliasing是一种硬件抗锯齿技术。它对 RenderTarget 有特殊的要求和影响。MSAA 的原理不使用 MSAA 时每个像素只有一个采样点——光栅化判断三角形是否覆盖这个点如果覆盖就执行片段着色器。使用 MSAA比如 4x MSAA时每个像素有4 个采样点。光栅化分别判断每个采样点是否被三角形覆盖片段着色器仍然只执行一次在像素中心或覆盖区域的重心位置但着色器的输出会被写入所有被覆盖的采样点每个采样点有独立的深度值这样做的效果三角形的边缘处部分采样点被覆盖、部分没有最终颜色是多个采样点的混合——边缘就不那么锯齿了。MSAA RT 的显存占用4x MSAA 意味着每个像素存 4 份颜色 4 份深度。RT 的显存占用直接乘以采样数普通 1080p RGBA8 RT 1920 × 1080 × 4 bytes 8.3 MB 4x MSAA 1080p RGBA8 RT 8.3 × 4 33.2 MB带宽也相应翻倍。Resolve 操作MSAA RT 不能直接用作纹理采样或者说效率很低。要把 MSAA RT 变成普通单采样纹理需要做Resolve解析MSAA RT每像素 4 个采样点 ↓ Resolve计算每个像素 4 个采样点的平均值 普通 RT每像素 1 个值Resolve 可以是显式操作glBlitFramebufferOpenGL、ResolveSubresourceDX11/12隐式操作在 Vulkan/Metal 的 Render Pass 中声明pResolveAttachments/resolveTextureGPU 在 Render Pass 结束时自动 Resolve在 Tile-Based GPU 上Resolve 可以在 Tile 内完成——MSAA 采样点只存在于片上内存中Resolve 后写回显存的是单采样数据。这样 MSAA 的显存占用几乎为零只增加了片上内存的使用。这就是 Metal 的storeAction multisampleResolve和 Vulkan Subpass 的 Resolve 机制的核心优势。5.7 Render Pass 设计与 RT 的 Load/Store 策略为什么现代 API 要你声明 loadOp / storeOp在 DX11/OpenGL 时代驱动不知道你打算怎么用 RT——所以它只能默认每次绑定 RT 时从显存加载旧内容Load每次解绑时写回显存Store。这可能做了大量无用功。现代 API 让你挑明意图loadOp 选项 - Load加载旧内容你想在已有内容上继续画 - Clear用指定颜色清除你要重新开始画 - DontCare不关心旧内容你知道每个像素都会被覆盖storeOp 选项 - Store把结果写回显存后续还要用 - DontCare不需要保留这只是临时中间数据 - Resolve写回并同时做 MSAA Resolve在 Tile-Based GPU 上这些选项直接映射到硬件行为loadOp硬件行为Load从显存读数据到 Tile Buffer消耗带宽ClearTile Buffer 直接置为清除值不读显存几乎免费DontCareTile Buffer 不初始化不读显存最快storeOp硬件行为Store从 Tile Buffer 写回显存消耗带宽DontCare不写回Tile Buffer 中的数据丢弃不写显存省带宽Subpass 合并的条件与收益Vulkan 中同一个 VkRenderPass 可以包含多个 Subpass。如果 GPU尤其是 Tile-Based能把多个 Subpass 合并在一个 Tile 内执行中间数据就不用写回显存再读回来。典型例子——延迟渲染Subpass 0G-Buffer Pass画几何体输出到 4 张 Color Attachment DepthSubpass 1Lighting Pass全屏四边形读取 G-Buffer Attachment 做光照输出到最终颜色 RT如果两个 Subpass 合并在同一个 Tile 中执行G-Buffer 数据留在 Tile Buffer 中不写回显存Lighting Pass 直接从 Tile Buffer 读取 G-Buffer 数据省下的带宽4 张 G-Buffer 的 Store4 张 G-Buffer 的 Load8×16.6 MB133 MB/帧\text{省下的带宽} 4 \text{ 张 G-Buffer 的 Store} 4 \text{ 张 G-Buffer 的 Load} 8 \times 16.6 \text{ MB} 133 \text{ MB/帧}省下的带宽4张G-Buffer的Store4张G-Buffer的Load8×16.6MB133MB/帧假设 4 张 1080p RGBA16F G-Buffer合并条件以 Vulkan 为例后续 Subpass 只能读取前一个 Subpass 写入的 Attachment且只能逐像素读取Input Attachment——不能随机访问其他像素分辨率必须相同要在同一个 Render Pass 内具体能否合并取决于 GPU 驱动的实现桌面端 Driver 如何利用 Render Pass 信息优化桌面端 GPU 虽然不是 Tile-Based 架构但现代桌面驱动也能利用 Render Pass 的声明信息做优化知道 loadOp DontCare→ 不需要 Flush 之前的缓存该 RT 的旧数据无所谓了知道 storeOp DontCare→ 不需要确保数据完整写回可以省去某些缓存一致性操作知道整个 Render Pass 的所有 Attachment→ 可以更好地规划缓存使用DX12 Render Pass TierDX12 后来引入了BeginRenderPass/EndRenderPass但分为三个 TierTier含义Tier 0驱动将 Render Pass 映射为传统的OMSetRenderTargets Barrier没有额外优化Tier 1驱动可以利用 loadOp/storeOp 信息做优化类似 Vulkan 的桌面端优化Tier 2完整支持包括 Tile-Based 优化如 Qualcomm 的 Windows on ARM大多数桌面 GPU 支持 Tier 0 或 Tier 1。对桌面端来说DX12 Render Pass 的主要价值是未来兼容和代码清晰性实际性能优化不如 Vulkan/Metal 在 Tile-Based GPU 上那么显著。本章小结硬件概念与 RT 的关系性能影响ROP管线末端负责写入 RTROP 数量决定像素填充率上限显存布局Tiled / DCC影响 RT 数据的存储效率影响缓存命中率和有效带宽带宽RT 读写的核心瓶颈分辨率×格式×RT数量 带宽消耗Tile-Based GPULoad/Store 模型loadOp/storeOp 性能的关键开关MSAART 显存翻倍需要 Resolve移动端可在 Tile 内 Resolve 省显存Render Pass声明 RT 的使用意图让 GPU 做精确的 Load/Store 优化设计哲学抽象泄漏——为什么理解硬件对写好 RT 代码至关重要 Joel Spolsky 提出的抽象泄漏定律在 RT 领域体现得淋漓尽致图形 API 试图把硬件差异抽象掉但性能关键路径上抽象必然泄漏。如果你不知道 Tile-Based GPU 的 Load/Store 模型你写的loadOp Load在桌面端无所谓在移动端却白白浪费 30% 带宽。如果你不知道 DCC 压缩的存在一次不经意的 UAV 读写就可能让带宽翻倍。这意味着跨平台不等于不用管硬件。真正的跨平台能力是在统一的代码架构下为不同硬件做出正确的性能决策。这也是为什么引擎需要 Render Pass 抽象第十章——它让上层逻辑统一同时允许底层针对不同 GPU 架构做优化。 思考题为什么桌面 GPU 选择了 Immediate Mode立即执行每个三角形而移动 GPU 选择了 Tile-Based先排序再逐 Tile 执行各自的设计取舍是什么如果你要设计一个同时在 Mali GPU 和 RTX 4090 上跑的引擎Render Pass 的抽象层需要暴露哪些必要参数哪些可以隐藏DCC/AFBC 隐式压缩是免费午餐吗有没有场景你会主动放弃压缩以换取其他能力下一章我们从硬件抬头看 API——六套图形 API 分别怎么创建和管理 RenderTarget。你会看到同一件事“创建一张 RT 并往上画”被六种语言说出来的样子以及为什么有些 API 需要你写 50 行代码有些只要 5 行——而那 45 行的差距恰恰是本章讲的硬件知识在 API 层的投影。

更多文章