GRAPHICS × PRODUCTION
中EN

皮了个牛

↓

计算机体系结构

GPU Rendering Architecture: Past Present Future

从 Geometry Architecture 与 Hidden Surface Removal 两条主线梳理 IMR、TBR、TBIMR、Mali 的 IDVS/DVS、PowerVR 的 UDL,以及 Hi-Z、ISP、LRZ、FPK、Fragment Prepass 和 Apple Punch Through 等可见性机制;文章比较各架构的提前裁决位置、局限和失效条件,并给出面向不同渲染架构的几何组织、管线状态和最佳实践。

2026-01-2862 分钟阅读
GPU Rendering Architecture: Past Present Future
AI 生成概念封面

GPU 把三维场景在有限功耗预算内实时转换为二维图像,围绕这一目标的架构演进集中在两件事上:几何数据如何变换与组织,以及不可见部分由谁来挡、在哪一级挡掉。后者通常称为隐藏面消除(Hidden Surface Removal,HSR),直接决定后端要为多少永远不会出现在最终画面中的像素承担计算与带宽开销。

几何侧看形态变化:IMR 按提交顺序把图元一路推到光栅,不做任何空间上的整理;TBR 先把图元按屏幕位置归类再逐块渲染,代价是几何要经过一趟 Binning;TBIMR 则是桌面在 IMR 的骨架上补入 Tile 化的局部性。HSR 侧从最朴素的 Z-Test,到 Hi-Z、ISP、LRZ 等分层方案,围绕的问题始终是:深度裁决的信息从哪来、能提前多远、提前的代价由谁承担。以下按机制本身展开这两条线。


一、Geometry Architecture

几何架构是渲染管线的前端,负责把顶点和图元从原始输入处理成光栅化与片段着色能直接处理的形态。设计目标明确:在尽量低的内存访问成本下应对越来越大的几何复杂度。带宽受限或图元冗余率高的场景中,瓶颈几乎总是先在几何阶段暴露,后端再快,前端供给不足也无法发挥。

1.1 Immediate Mode Rendering (IMR)

IMR 是 GPU 架构的原始形态,设计逻辑是按序执行。GPU 顺着提交顺序处理每个 Draw Call,图元一完成变换立刻进光栅化,整条管线没有分离、没有重排。早期硬件选择 IMR 顺理成章:延迟最小、结构最清晰、最易实现。图形复杂度增长之后,问题集中在三处:显存带宽、Overdraw 处理和功耗。

1.1.1 流程概述

  • Command Decode & Vertex Fetch:API 提交的 Draw Call 被解析后,GPU 从 VRAM 加载顶点数据。
  • Vertex Shading:每个顶点执行位置变换与相关属性计算。
  • Primitive Assembly & Clipping:顶点组装成图元,超出视锥或用户裁剪面的部分被切掉。
  • Rasterization:光栅器把图元变成片段,产出 Quad 列表与覆盖掩码。
  • Fragment Shading:每个片段做着色计算,通常伴随一次或多次纹理采样。
  • Per-Fragment Ops:Z-Test、Stencil Test、Blend 等固定功能处理,结果写入 Color/Depth Buffer。
  • Output Merge:结果合并进帧缓冲。

这条链路是严格的单向流。图元的处理时序完全由应用层提交顺序决定,硬件没有跨图元做全局优化的机会,IMR 后续的所有结构性缺陷都由此而来。

1.1.2 架构特性与瓶颈

  • 高频显存访问。顶点数据、纹理样本、Constant Buffer、Z/Color Buffer 都需要频繁读写。片段阶段最严重:Color/Depth 的 Read-Modify-Write 天然缺乏空间局部性,缓存容易失效,访问被迫落到慢速外部 DRAM。
  • Overdraw 的代价。Overdraw 的完整定义与分类在 2.1.2,这里只看它在 IMR 下的性能含义:所有片段都必须完整执行 Fragment Shader 和深度测试,最终只有通过的那一小部分写入帧缓冲,其余全是无效计算,消耗的是 ALU 和纹理带宽。
  • 能效差。显存接口的频繁读写直接驱动外部芯片,是主要功耗来源之一;大量被深度剔除的着色计算相当于晶体管级的无效开关,进一步推高功耗。

IMR 在负载稳定、遮挡少的场景中延迟低、行为直白,表现足够。天花板来自对带宽和功耗的敏感:高 Overdraw、资源受限的平台都难以承受。后续所有架构演进的出发点一致:降低 IMR 这套高冗余执行模型的带宽与计算开销。

1.2 Tile-Based Rendering (TBR)

TBR 用空间局部性换带宽。移动平台的 DRAM 带宽和功耗预算紧张,屏幕被切成 Tile,先花一道几何处理把图元按覆盖关系归类,再逐 Tile 在片上 SRAM 内完成整套渲染,尽量避免访问高功耗的外部 DRAM。这条路径是 Apple GPU、Adreno、Mali、PowerVR 的共同选择,已成为移动 GPU 的标准架构。

1.2.1 执行阶段

  • 几何阶段(Binning Pass):

    • 接收 Render Pass 内全部 Draw Call,统一做几何处理。
    • Vertex Shader 计算屏幕空间坐标,经 Culling 后留下可见 Primitive。
    • 硬件把变换后的图元映射到其覆盖的屏幕 Tile,记录索引。
    • 每个 Tile 输出一份 Bin List(图元列表),通常写入 DRAM 供下一阶段使用。

    Binning 是当前通道几何信息的全量收集,存储成本随 Tile 数和场景复杂度增长;没有图元剔除优化时,这一趟 DRAM 写入是不可忽略的开销。

  • 像素阶段(Rendering Pass):

    • 逐 Tile 从主存加载对应 Bin List。
    • 按图元索引从显存取顶点数据、纹理样本,填充寄存器与缓存。
    • Rasterization、Fragment Shading、Z/Stencil Test、Blending 全部在片上 SRAM(Tile Buffer)内完成读写。
    • Tile 渲染结束,颜色、深度等结果一次性批量写回帧缓冲。

    片上缓冲的复用是串行的:不同 Tile 之间顺序执行,Tile 内部才有 CU / Shader Core 的并行调度。

1.2.2 优势与局限

TBR 将片段阶段的读写约束在片上 SRAM 内部:深度测试和混合不触发 DRAM 访问,SRAM 功耗远低于 DRAM,冗余片段的处理随之减少,计算资源利用率提高。Tile 的区域性和操作封闭性使缓存优化、功耗控制均可预测,这是 TBR 成为移动标准的原因。

代价也写在架构结构中:Bin List 要落一次主存再读回,是 TBR 固有的带宽开销;片上 Tile Buffer 的容量直接限制支持的 Tile 尺寸、颜色精度与 MSAA 级别。这条约束的具体形状,ARM 在 Immortalis-G925 的技术博客中给过数字:渲染通道每像素位宽越过 128 bit 的阈值,Tile 尺寸从 64×64 收缩到 64×32;越过 256 bit,再缩到 32×32。Tile 一缩,同样尺寸的图元覆盖的 Tile 数变多,跨 Tile 图元比例上升,Binning 写出与后续重计算的负担同步放大。片上容量不是抽象上限,会直接折算成周期数。另一方面,Binning 用的 Tile Unit 是共享资源,Tile 间无法完全并行,只能在不同阶段上做流水线式的并行。

1.3 Tile-Based Immediate Mode Rendering (TBIMR)

桌面的功耗与带宽预算比移动宽裕,但图形复杂度上来之后,IMR 的带宽浪费同样难以掩盖。NVIDIA 与 AMD 各自在 IMR 框架内引入 Tile 化的空间重排与图元聚合,保留 IMR 的执行流,同时解决带宽与小图元利用率问题,这类做法统称 TBIMR。最具代表性的两条实现路径:NVIDIA 的 Tiled Caching(自 Maxwell 起)与 AMD 的 DSBR(Draw Stream Binning Rasterization,自 Vega/RDNA 起)。

1.3.1 要解决的问题

IMR 在现代负载下的瓶颈集中在两类。

第一类是带宽压力。逐图元立即执行意味着每个图元的像素处理都可能直接对主 Color Buffer 发起一次读写(Z-Test、Blend 均如此)。图元提交顺序通常与屏幕位置无关,在不做空间重排的前提下,这些访问高度随机,缓存命中率骤降,带宽需求成倍放大。几何阶段同样浪费:现代场景中大量微图元(Micro-triangle)只覆盖极少像素,却要各自独立走完 Vertex Fetch、Vertex Shading 的固定成本,Geometry 端的贡献与 Fragment 端严重失衡。

第二类是光栅器周期性闲置。Rasterizer 的硬件吞吐可以很高,例如 64 pixel/clock,但不代表任意图元都能使吞吐达到这一水平。图元按顺序流经 Vertex Fetch、Shading、Primitive Assembly 与 Culling 等阶段,每一步的耗时随图元规模、拓扑类型以及依赖的 Shader Resource 不同而参差,访存延迟波动明显。连续图元之间没有批处理机制兜底,前端一旦阻塞,后端光栅器能处理的量不可能超过前端供给的上限,光栅器因此周期性空闲,形成跨周期的吞吐中断。

两类问题的根源相同:IMR 没有面向 Tile 粒度的图元聚合与前置调度,无法保证后端(尤其是 Rasterizer)的连续供给。TBIMR 的策略是把空间划分与聚合提前到 Binning 阶段消化,再以有序、成批的方式把图元供给后端,让 Rasterizer 按 Tile 为单位跑出稳定、连续的周期级吞吐。微图元主导的场景中,这条路径的收益尤为显著。

1.3.2 Screen Mapped Rasterization(SMR)

在基于缓存的 Tiled Caching 之前,NVIDIA 从 G70 到 Kepler 用过一种叫 Screen Mapped Rasterization(SMR)的调度机制:把屏幕静态划分成 Checkerboard 栅格(如 4×4 或 8×8 的 Tile),硬件中的 Screen Mapped Engine 用查找表把这些 Tile 交错映射到不同 GPC。每个三角形顶点处理完后,按首个像素所在 Tile 的归属决定由哪个 GPC 接管后续光栅;跨越多个 Tile 的图元则被复制到多个 GPC,走广播式处理。

SMR 针对的是负载不均:屏幕内容分布不均时(例如多数图元堆在屏幕左侧),若光栅任务全由左侧 GPC 承担,右侧资源整帧闲置。静态交错映射用于避免这种结构性空载,让多个光栅单元都有负载。按专利 US8698814 的描述,映射还具备一定可编程性,GPC 降频或故障时可以调整映射维持运行能力。

但 SMR 不是完整意义上的 Tiling,更接近 IMR 框架下对空间重排的早期试探,目标在资源均衡,不在缓存效率。两条硬伤:

  1. 带宽问题没解决。SMR 实质是分区化的 IMR:每个 GPC 依旧直访全局帧缓冲,做深度与颜色的随机读写,既没构建局部缓存路径,也没减少访问次数。
  2. 光栅效率反降。对频繁跨越 Tile 的微图元,SMR 要重复做图元 Setup,上游开销更重;粒度过细导致图元被拆开复制到多个 GPC,冗余处理增加,整体光栅化效率反而下降。

SMR 是面向多核调度优化的 IMR 变体,重心在资源分发,不在性能瓶颈突破。带宽与光栅效率两项均未突破,真正改变设计方向的是 Maxwell 的 Tiled Caching Rasterization。

1.3.3 NVIDIA Tiled Caching Rasterization

Tiled Caching 没有引入完整的全局 Binning Pass,而是在单个 Draw Call 内、以 L2 Cache 为中心给 Rasterizer 加了一道图元信息缓冲,按 Tile 组织光栅化负载。首次出现在 Maxwell,一路延续到 Pascal、Turing、Ampere。

工作机制:图元经过 Vertex/Geometry Shader 后,硬件把变换结果、覆盖范围等信息按 Tile 编组,缓存进 L2;Rasterizer 不再被动地逐图元接收输入,而是主动从 Cache 中把某个 Tile 的全部图元批量拉出来执行光栅化。多图元并行处理消除了光栅器的周期性空闲,微图元场景的吞吐提升尤其明显。Tile 内图元在屏幕空间上彼此接近,Fragment Shading 与纹理采样因此获得更高的 Cache 命中率,带宽压力随之缓解。Output Merge 也能直接在 L2 层级完成,虽然不如 TBR 在 L1 层级操作,但已经避开了主存读写,带宽效率相对 IMR 有数量级的改善。

并行特性与 SMR 一脉相承:不同 Tile 的处理可以并行,多个光栅化单元同时工作在不同 Tile 区域,不引入串行瓶颈。这是桌面 GPU 内部并行能力与高带宽互联支撑下的自然演化。

定位上,Tiled Caching 仍属 IMR 框架内的优化:不引入全局 Binning、DRAM Bin List、片上 Tile Buffer 这些 TBR 结构,只在保留 IMR 时序执行的前提下做图元数据的局部化与处理粒度的聚合。

1.3.4 AMD Draw Stream Binning Rasterization(DSBR)

DSBR 从 Vega 开始引入,在 RDNA 中借 NGG 进一步深化。相比 NVIDIA 的局部缓存机制,DSBR 更接近 TBR:建立显式的 Binning Pass,支持跨 Draw Call 的图元聚合与遮挡剔除,但依然不是 TBR 那种对整个 Render Pass 的图元聚合。

硬件流程分四段。流式 Binning:Draw Call 进入后,图元先由专用硬件做空间划分与 Binning,逐 Tile 建立 Bin List,依次往后收集,直到缓存满载触发 Flush。Early Culling:在 Bin 构建期间做视锥裁剪、小图元过滤与遮挡剔除,遮挡判断基于 Tile 内已有图元的深度近似信息,思路类似 LRZ,但 DSBR 从未出现过独立的 LRZ 式硬件。光栅化:后续光栅器依赖 Bin List 做片段生成、着色与合并,图元聚合度高,调度灵活。输出混合:对 Framebuffer 的读写和 TCR 一样在 Cache 内完成,避开主存交互的高额带宽开销。

RDNA 的 Primitive Shader 与 DSBR 是协同关系:Shader 阶段就能输出裁剪结果与 Tile Index,空间组织更紧凑(顶点重用机制与此相关,涉及 Vertex Reuse 的详细讨论属于独立议题)。内存管理上,活跃 Tile 的 Bin List 尽量驻留 L2 Cache 或 Infinity Cache;缓存容量不足时,部分 Bin 数据 Spill 至 VRAM,由内存控制器协调访问;驱动还设有中断策略:缓存压力过高时硬件暂停接收 Draw Call,执行局部刷新与调度切换,保证资源可持续利用。

DSBR 在遮挡剔除与图元重组上比 TCR 更接近 TBR,但仍保留 IMR 的时序结构与高带宽读写能力,适合桌面与 APU 这类大缓存、高吞吐的平台。DSBR 代表的方向是 Tile-Aware 但并未完全 Tile-Driven 的折中方案。AMD 在 Vega 白皮书中明确表述,DSBR 的设计意图是把已在手持设备上广泛使用的 tiled rendering 与桌面 immediate-mode rendering 的优势结合,削减 GPU 上不必要的处理与数据搬运,在提升性能的同时压低功耗。这一官方定位与上述硬件流程分析一致。

1.3.5 小结

三条形态对照:IMR 顺序执行、不碰空间信息;TBR 用 Binning 换局部性,代价是 Bin List 落一次主存;TBIMR 在不放弃 IMR 灵活性与并行性的前提下引入 Tiling,NVIDIA 选 L2 Cache 为中心的图元聚合,AMD 导入更完整的空间组织流程。两者共同表明:即便高带宽平台,适度的空间重排与批量处理仍是现代负载吞吐的关键策略。

将 TBIMR 放回 IMR→TBR→TBIMR 的形态演进中,结构脉络更加清晰。IMR 按提交顺序即刻执行,对几何流不做任何重排;TBR 用 Binning 把几何流按屏幕空间重排,代价是 Bin List 要落一次主存;TBIMR 的混合设计连这一步也要省掉,Tiled Caching 不引入全局 Binning Pass,DSBR 重建了显式 Binning Pass 与跨 Draw Call 聚合,但让 Bin List 尽量驻留 Cache 而非主存。Tiled Caching 与 DSBR 的共同特征由此可见:两者均是厂商在固定功能框架内对几何局部性的末段补救。两者都不改动 Draw 流的提交语义,只在硬件内部做空间重排。补救的对象相同,即 1.3.1 所描述的脱节:图元提交顺序与屏幕空间位置无关,访问模式高度随机,光栅器周期性空闲。补救的手段也相同:以缓存容量换取局部性重建。这套交换成立的前提是活跃图元集始终装得进聚合窗口,而窗口容量是晶体管预算决定的常量,几何密度不是。DSBR 的内存管理已经把这条边界写在明处:Bin List 驻留 L2 Cache 或 Infinity Cache,容量不足即 Spill 至 VRAM,缓存压力过高时硬件暂停接收 Draw Call,由驱动介入调度切换。固定功能补救局部性的能力存在明确的物理上限,Spill 与中断策略是这个上限的直接证据。

3.1.2 的策略三与 3.2 的策略五进一步说明这一点:要让 Tiled Caching 与 DSBR 的批量调度充分生效,引擎层必须预先构造 Mesh Cluster 或 meshlet 这类空间聚合单元,让图元集中覆盖少数 Tile。聚合单元的尺寸、空间范围与图元密度全部由软件决定,硬件只是消费这个结果。控制权从这一步起已经在移交:固定功能的末段补救,其生效前提是开发者在数据组织层先替硬件完成一半调度。既然聚合单元由软件构造,把聚合、去重与剔除的执行一并交给软件,中间不存在任何语义缺口。

NVIDIA Turing/Ampere 的 Task Shader / Mesh Shader 单元与 AMD RDNA 的 Primitive Shader / NGG 几何路径,是两家厂商在同一时间窗口对固定功能前端几何调度能力物理上限的回应。1.3.4 中 Primitive Shader 以 DSBR 协同单元的身份出场,换一个角度看,它是固定功能前端让位的第一步。IMR 到 TBR 到 TBIMR,形态史上的每一步都是硬件在不交出控制权的前提下重排几何流;混合路径之后,固定功能前端已没有下一种形态可出,剩下的动作是把片上共享存储(Shared Memory / LDS)作为可由线程组直接访问的核心资源暴露出来,由开发者在软件层显式编写去重与像素级精准剔除(Shader Culling)代码。Retained-mode PSO 与传统 Draw 语义的适用边界由此开始收窄。

1.4 Index Driven Vertex Shading(IDVS — Mali)

IDVS、DVS、UDL 三种延迟变换变体不在 IMR→TBR→TBIMR 的主干上,而是 TBR 内部另一个战场的产物:顶点属性计算到底该不该在 Binning 阶段全做完。

问题出在 Parameter Buffer。传统 TBR 的 Binning 阶段会对图元执行完整的顶点变换与属性插值,把结果写进片上或片外的 Parameter Buffer,供像素阶段使用。问题在于:很多已经变换完、插值完的属性,对应的图元最终被裁剪或遮挡,这些计算成本没有转化为有效像素。带宽受限的移动平台受此影响最大:白白完成的顶点计算和白白写出的属性,都要从 DRAM 带宽中承担开销。

Mali 在 IDVS(Index Driven Vertex Shading)中的解法是把 Vertex Shader 拆成两个阶段,属性计算推迟到图元确认可见之后。驱动在编译期把 VS 编译成两个版本:PositionOnly VS 只做位置变换;VaryingOnly VS 才算传给 Fragment Shader 的 Varying 属性。Binning 阶段只跑轻量的 PositionOnly VS,用变换结果把图元分到对应 Tile,顺带做初步裁剪或剔除(视锥剔除、小图元过滤等)。到了 Shading 阶段,只有通过裁剪、被标记为可见的图元才执行 VaryingOnly VS,所得属性用于插值与 Fragment Shader 输入。

收益有两层:没通过剔除测试的图元根本不执行属性着色,节省的是计算;只有可见图元才写出属性,Parameter Buffer 的压力也随之减轻。部分平台还会把 PositionOnly 的结果留在片上,避免像素阶段重复计算位置。

代价同样明确:属性计算被推迟到像素阶段,引入一次额外的调度与执行负担;而且最终 Varying 仍然要写进 Tile Buffer,只是写入量变小了。这是一次典型的"以计算换带宽",但只换了一半:节省的是被剔除图元的开销,写 Parameter Buffer 这条路径本身还在。

1.5 Deferred Vertex Shading(DVS — Mali)

IDVS 之后 Mali 往前走了一步。既然属性计算可以推迟,为什么变换结果还要在 Binning 写出去?DVS(Deferred Vertex Shading)的目标就是进一步压缩 Binning 的写出量,甚至完全取消变换结果的写出,只保留原始索引,用极端手段压缩 DRAM 带宽。

机制上 DVS 把推迟贯彻到底:Binning 阶段照样跑 PositionOnly VS、做剔除测试,但不再写出任何变换结果或 Varying,只把图元的原始索引(Primitive Index)记进 Tile Buffer。到 Rendering 阶段,GPU 从 Tile Buffer 读出索引列表,拿它回溯 Vertex Buffer 里的原始顶点数据,执行完整 Vertex Shader(位置变换加属性计算),结果直接用于光栅化、插值与片段着色。

收益来自索引本身的形态:原始索引是整数,存储体积比变换后的顶点数据小一个量级;完整 VS 只为真实参与片段阶段的图元执行;对移动 SoC 而言,DRAM 访问压力下降显著。

开销的另一面也须明确。每个进入渲染阶段的图元都要重跑完整 VS,这意味着额外的 VBO 读取;跨 Tile 的图元可能被多个 Tile 各自取回、重复计算,渲染阶段的负载随之增加。相比 IDVS,DVS 完全依赖渲染阶段重做顶点处理,对 Shader Core 的压力更高。Mali 接受这部分开销,前提是片上计算能力的增长远快于 DRAM 带宽的增长。在移动平台上这个前提成立,于是"以算换带宽"在这条线上走到了极端:VS 的执行被绑定在图元可见性判断之后,数据流方向被整个重构了。

1.6 Untransformed Display Lists(UDL — PowerVR)

如果 Mali 的两步是把"算"往后推,PowerVR 的 Untransformed Display Lists(UDL)则是在同一个问题上选了另一个支点:复用。UDL 同样想压缩 Binning 阶段的写出成本,但出发点更激进:场景几何静态不变时,为什么要让同一份数据被反复变换、反复缓存?

PowerVR 注意到的浪费比 Mali 多一层:在 Mali 的方案中,同一个图元若被不同 Tile 引用,每个 Tile 都会各自变换一遍相同的顶点数据,计算重复了,只是没算被遮挡的那些。UDL 要用跨 Tile、跨帧共享静态数据引用来消除这层重复。

执行流分成三段,每段只做最小必要的事。Transform1 在几何阶段进行:从 Draw Call 里只解析 Position 字段,送入专用的 Transform1 单元做屏幕空间变换,随后做粗粒度的视锥剔除或裁剪,不写完整顶点属性、不进 Parameter Buffer,只把通过裁剪的图元索引传给 Tile Binning Unit。Tile Binning 与传统 TBR 相同:按 Transform1 得到的屏幕坐标把图元分配进各 Tile,但显示列表中只存原始数据的引用(Primitive Index),完全不复制 Varying 或坐标,Bin List 因此极度紧凑,天然适合大规模静态场景。Transform2 放到 Rendering 阶段:按 Tile Bin List 中的索引反向查找原始顶点数据,执行完整 Vertex Shader,结果存入 Tile Memory 供光栅化、插值与片段着色使用。

到这里 UDL 与 DVS 的差别开始清晰:DVS 的"渲染阶段重算"是每次都要支付的;UDL 则意识到静态几何会在多个 Tile、多帧之间被反复引用,于是给 Transform2 加了一道变换缓存(Transformed Data Cache)。缓存条目是"顶点标识符 → 位置 + 属性"的映射,命中策略按图元的 Index 做哈希查找,适配粒度可以组织到三角形或顶点级别,支持跨 Tile 复用;跨帧是否有效,由驱动管理。

这道缓存是 UDL 和 Mali 最大的分歧点:Mali 接受跨 Tile 图元各自重算一次,PowerVR 用缓存消除重复计算,让执行粒度更接近 IMR 那种"一次变换、多次使用"的模型,同时保留 TBR 的分块存储优势。代价是额外的片上缓存与跨帧有效性管理。两条路的取舍对比如下:

特性项Mali DVSPowerVR UDL
Bin List 内容图元 Index图元 Index(可跨帧共享)
Transform 结果写出无,属性在片上生成无,Transform2 执行时引入缓存机制
跨 Tile 复用无有(通过 Transformed Data Cache)
目标优化方向减少 Binning 写出减少写出并提升跨 Tile 复用能力
结构成本简化,但有重复计算风险引入额外片上缓存,降低重复执行

UDL 的工程收益集中在静态几何:Bin List 只有索引,带宽占用远低于写完整属性;跨 Tile、跨帧的复用让 UI、场景地形这类内容不用反复承担变换开销;Transform1 的结果不落地,Binning 阶段的压力也轻。整体设计更接近"存引用,不存副本"的原则,那道缓存是硬件层级的 Memoization,把"按需变换"与"变换结果复用"拼成了一条压缩、可共享的渲染输入链路。


三种延迟变换变体讲完,回看全篇梳理的几何架构(IMR、TBR、TBIMR,加上 IDVS/DVS/UDL),所有方案全部在同一个前提内做优化:几何调度权归硬件。Mali 两代方案与 PowerVR 的 UDL,再怎么推迟、再怎么复用,顶点何时算、算几次、写不写,最终都由硬件与驱动的既定结构决定。4.2 节把 Mesh Shading 列进未来方向,定性为:Mesh Shading 不是这条形态史的新起点,而是终点1。

二、Hidden Surface Removal(HSR)

几何处理完,GPU 面对一个直接的现实:送进管线的图元,绝大多数在最终画面里不可见,要么被更靠前的物体遮挡,要么已经越过了裁剪面。这些图元若仍被完整地光栅化、片段着色,等于把算力消耗在永远不会被看见的像素上。隐藏面消除(Hidden Surface Removal,HSR)是为此构建的一整套硬件机制,目标一致:在管线尽可能早的阶段、用尽可能低的成本,把最终不可见的片段或图元剔除,省掉不必要的着色与内存访问。判断得越早,节省越多;判断得越准,漏网越少。HSR 的历史就是在早期性与准确性之间反复寻找平衡。

先明确三个前置概念:GPU 以什么为粒度处理像素,重复着色的浪费长什么样,以及最基础的深度裁决 EarlyZ/LateZ 各自的边界。

2.1 前置概念

2.1.1 Pixel Quad

现代 GPU 在 Rasterization 与 Fragment Shading 阶段处理像素时,基本单位是 2×2 的 Pixel Quad,而不是单个像素。原因有两个:其一,片段偏导数(dFdx/dFdy)要算纹理 LOD、各向异性采样角度,必须依赖邻近像素的值,Quad 提供了这个邻域;其二,以 Quad 为单位能提高 SIMD 效率与空间局部性。

这条规则带出一个工程含义:即使三角形只覆盖 Quad 里的部分像素,整个 Quad 也得生成并送进后续阶段。那些不输出颜色的"辅助像素"同样参与导数计算和遮挡判断,既是机制的一部分,也是潜在的性能冗余源。Quad 这个粒度会一路影响后面几乎所有剔除机制的设计。

2.1.2 Overdraw

Overdraw 指同一像素位置被多个图元反复着色的现象,是片段阶段资源消耗的主要成因。它分两类:Quad Overdraw,处理单位是 Quad,哪怕只有一个像素被覆盖,整个 Quad 也要处理;Pixel Overdraw,Quad 本身合法,但不同图元在同一像素上依次执行着色,最终只有最前面的片段能写进帧缓冲,其余全是无效计算。

后一种在遮挡关系复杂、或排序混乱的 Forward Rendering 场景里尤其严重。每层额外的着色都会触发 ALU、Texture Unit 与 Color/Depth Buffer 的多重冗余访问,带宽消耗与功耗被一起推高。后续所有 HSR 机制,都在设法消除这两类 Overdraw。

2.1.3 Depth Test

深度测试决定一个片段有没有资格写进帧缓冲,它按执行时机分成两条路径:

  • EarlyZ:在 Fragment Shader 执行前做。以 Pixel Quad 为粒度,用图元插值出的深度与 Z-Buffer 比较,若整个 Quad 全部失败,Shader 可以直接跳过。它能成立有三个限制条件:Shader 不修改 FragDepth、不使用 discard、不开启顺序敏感的 Alpha Blending。
  • LateZ:在 Fragment Shader 之后做。支持 per-sample 粒度,适配 MSAA。代价是片段着色已经执行完,即使最终被深度测试挡掉,所有计算成本都已经发生了。

工程上,现代 GPU 会尽量把片段安排进 EarlyZ 路径,一旦命中上述限制就降级到 LateZ。这条路径转移通常由 Shader 编译器分析得出,并在生成的代码里插入控制标记,硬件据此选择跳转路径。对开发者来说,少用 discard、避免动态写深度、优化提交顺序,都是在帮 EarlyZ 提高命中率,能走 EarlyZ 的片段越多,整条渲染路径的资源利用越好看。

2.2 Hierarchical Z Cull

EarlyZ 的粒度还是太细。一个 Quad 一个 Quad 地比较,遇到大片被遮挡区域时,大量比较是在做无用功。Hierarchical Z Cull(Hi-Z)在 EarlyZ 前面又加了一道粗筛:用低开销的层次化深度结构,在光栅化之后、片段阶段之前,先把被遮挡的 Quad 成批拦截。桌面 GPU 基本都带 Hi-Z,部分移动架构也有。

2.2.1 架构原理

Hi-Z 在主深度缓冲(Z-Buffer)之上建一座金字塔。每一层都是下一层的空间降采样,对大区域的深度信息做保守性归约:Level 0 对应主 Z-Buffer,保持原始分辨率;Level 1~N 每层覆盖更大的区域(2×2、4×4、8×8 像素……),保存该区域的最远深度值(Max Z)或最浅深度值(Min Z),具体存哪个看剔除目标。硬件中的专用模块(通常是 ROP 子单元或 ZCull 引擎)会在每次深度写入后自动更新金字塔,维持一定程度的同步一致性,这套维护对应用层完全透明。

2.2.2 剔除流程

光栅阶段产出的 Quad 列表进入 FIFO 后,Hi-Z 并行开始测试。每个 Quad 取出后,硬件在金字塔里选择合适的层级做遮挡判断:粒度选择通常取决于图元大小,大图元用较高层级,小图元则可能逐级向下找更细的粒度。判断逻辑是保守比较:把 Quad 最浅的深度与对应 Hi-Z 层级里存的最远深度比,若前者更深,说明 Quad 完全被挡,直接丢弃;若比不出结果,就放进 SurvivedQuadFIFO,进入下一阶段。所有通过 Hi-Z 的 Quad 最后汇入标准 EarlyZ,做 per-pixel 或 per-quad 的详细深度测试。

2.2.3 价值与三家实现差异

这套机制的价值很清楚:被剔除的 Quad 完全跳过 Fragment Shader 与后续写出,显存访问压力下降,Shader、Texture Unit 的周期占用同步降低。只要不触发 Z 清除或改变深度测试函数,Hi-Z 的剔除状态能跨多个渲染批次持续生效。

原理相同(分层缓冲 + 粗粒度保守比较),三家在实现上各选各的部署位置,差异集中在 pipeline 位置、失效恢复机制与 metadata 组织方式上。

平台别名Pipeline 位置失效恢复机制
NVIDIAZCullRASTER → ZCull → PROP → PS → ZROP → CROP公开资料不足
AMDHyperZPA/Raster → HiZ → Detail/PreZ → PS → PostZ → ColorHiZ Range Resummarize
IntelHiZRaster → Coarse HiZ → Fine HiZ → PS → Depth/ColorResolve / Ambiguate / Transition

NVIDIA 的 ZCull 摆在 RASTER 与 PS 之间:Rasterizer 输出的 Quad 先按 coarse tile granularity 做保守深度比较,通过后进入 PROP(Pre-ROP)单元。PROP 负责维护 API ordering、控制 Early-Z / Late-Z 路径选择,再把幸存片段送进 Pixel Shader;PS 输出经 ZROP(Depth+Stencil)与 CROP(Color Blend/Store)完成最终写入。整条链路是 RASTER → ZCull → PROP → PS → ZROP → CROP。ZCull 的具体层级数 NVIDIA 没有公开,公开可见的 Nsight counter 只暴露 ZCull rejection rate。

AMD 的 HyperZ 是一个技术家族的总称,涵盖 Hierarchical Z、Early-Z、Z Compression、Fast Z Clear。现代 RDNA 的 fragment path 是 PA/Raster → HiZ → Detail/PreZ → Pixel Shader → PostZ → Color Buffer。GPUOpen counter 分别暴露 HiZTilesRejected、HiZQuadsCulled、PreZSamplesPassing、PostZSamplesPassing,能从计数器层面确认硬件确实存在 Shader 前/后两条 Z path。失效场景值得拆开看:当 Depth Surface 被非标准路径(UAV/Copy)修改,导致 HiZ metadata 与真实 Depth 不一致时,驱动需要触发 HiZ Range Resummarize,从 depth 数据重新生成合法的 coarse ranges。这与"当前 Shader 不能 Early-Z"是两个不同层次的问题:前者是 metadata 失去可信度,后者只是路径降级。

Intel 在 Xe-HPG 上是先 coarse depth test、再 fine depth test 的结构。HiZ 在这里同时承担 hierarchical depth acceleration 和 auxiliary/compression metadata 两份职责:HiZ Auxiliary Surface 可以用 Clear Value、Plane Equation、Min/Max、Pass-through 等多种方式描述主 Depth Block,与主 Depth Buffer 构成 coherent representation。aux state 不兼容时,通过 Resolve / Ambiguate / transition 恢复。Intel 的失效模式比 Adreno LRZ 宽松,不存在"某个 DepthFunc 出现后整个 RenderPass 的 HiZ 永久关闭"这种规则。

2.2.4 局限

Hi-Z 的边界也很明确。片段 Shader 一旦使用 discard 或开启 Alpha Blending,像素的最终输出状态无法预知,Hi-Z 就不能提前剔除;显式修改 FragDepth 会让预先构建的金字塔直接失效;存储的是保守性摘要,存在假阴性:明明被挡住的 Quad 也可能被放行。遮挡体在屏幕空间太小、无法在高层级形成有效阻挡时,剔除能力会明显下降;频繁的相机移动或遮挡关系切换会让金字塔反复重建,硬件压力随之上升。

还有一层容易混淆的边界:Hi-Z 是硬件级的封闭路径,与应用层的 Software Occlusion Culling 不是一回事。后者通常通过 Occlusion Query 或基于 Hi-Z 的粗测写入实现,属于开发者主动控制的剔除逻辑,不依赖 GPU 内部的自动化路径。

Hi-Z 的分层摘要 + 保守比较 + EarlyZ 收尾这套流程节省显著,但永远存在不确定性:被保守性放行的假阴性 Quad 还是要走完着色。要让遮挡判断从概率性接近确定性,需要不同的结构。

2.3 Image Synthesis Processor(ISP — PowerVR)

桌面 GPU 让 Quad 逐级撞保守比较,PowerVR 在 TBR 架构里给出了另一个答案:既然几何已经按 Tile 归好类、所有片段反正要先积攒在片上,不如攒齐了再做精确裁决。承载这个思路的硬件叫 Image Synthesis Processor(ISP),它取代了以 Hi-Z 为代表的 Quad 粒度 EarlyZ 逻辑,把深度裁决推进到 per-sample,这也是 PowerVR 将自家架构称为 TBDR 的依据。

2.3.1 执行流程

ISP 以 Tile 为处理单位,在每个 Tile 内部维护一份完整的渲染栈,流程分五步。

  • Tile 准备与图元加载:拿到当前 Tile 的 Bin List(图元索引集合),从 VRAM 读取对应顶点数据,准备纹理与常量,填充寄存器与片上缓存。
  • 精细光栅化:ISP 内部的 Fine Rasterizer 光栅化所有图元;MSAA 开启时对每个采样点计算覆盖关系。输出不是片段本身,而是"片段描述符队列",每个片段的最小化元数据:屏幕位置、图元索引、深度值、着色器 ID 等。此阶段不执行片段着色,只缓存待着色片段的信息。
  • On-Chip Hidden Surface Removal:对所有片段的描述符做裁决。硬件在每个像素(或采样点)上构建临时栈,在同一位置的多份描述符中选出深度最小者,把唯一性 ID 写进 Tag Buffer,其余直接丢弃。这一步取代了 EarlyZ 与 LateZ,是决定性的遮挡判定,被遮挡片段在此阶段全部终止,不会进入后续调度。
  • Conditional Fragment Rendering:读 Tag Buffer,只把标记为可见的片段调度到 Execution Unit,执行 Fragment Shader,用图元插值信息填充 Varying,结果写入 Tile Buffer。
  • Tile Flush:Tile 渲染完成后,把 Tile Buffer 里的颜色、深度、模板一次性整体写回帧缓冲,清空资源,切换下一个 Tile。

2.3.2 架构优势

逐像素、逐采样点的剔除,准确性远不是 Quad 粒度 Hi-Z 能比的。Imagination 官方架构文章把输出端的特性强调了两遍:HSR 完成后,每个 Tile 得到的数据"only encodes the visible pixels that pass the depth test",只编码通过深度测试的可见像素。Tag Buffer 的唯一 ID 裁决,换来的正是这份没有冗余的结果集。同文给出的参照是,Rogue 架构常规渲染的 Tile 尺寸是 32×32,在这个粒度下于片内维护整个 Tile 的片段状态,物理上是可行的。

被遮挡片段不执行 Shader、不发起 Texture Load、不参与写回,这是 TBDR 能效的根本来源。所有遮挡判断收敛在统一路径上,控制路径极简;所有图元光栅化后统一裁决,渲染顺序对最终可见性的影响被完全消除:不透明物体先画谁后画谁,在这里第一次变得无所谓。

2.3.3 与 Hi-Z 的对比

对比维度Hi-Z(桌面)ISP(TBDR)
处理阶段Rasterization 后、Shader 前所有图元光栅完毕后、着色前
粒度Pixel QuadPer-Sample(支持 MSAA 粒度)
判断机制深度范围比较(保守)精确深度比较
可见性输出保留所有可能可见片段仅保留最前片段
顺序依赖性存在(受 Draw Call 顺序影响)无(统一剔除决策)
着色路径需通过 EarlyZ 再判断被遮挡片段根本不调度至 Shader Core

精度的代价都在工程侧:粒度更细、裁决更晚、判定更强,意味着管线复杂度更高,对片上 SRAM 与控制逻辑的要求更苛刻。ISP 需要维护完整片段缓存,Tag Buffer 必须覆盖整个 Tile,片上资源需求高;光栅输出、剔除判断、Tag 路由、Fragment 启动需要彼此协调,控制路径复杂度随之上升。ISP 对 Shader 行为的兼容性受限,不支持 discard、alpha blend、depth offset 这类操作;MSAA 下样本数剧增会把 Tag Buffer 压力顶上去。Tile 内部不能并发执行,高分辨率下 Tile 数量上升,总处理时延也会拉长。

ISP 的定位是一笔"以时换空、以空间换能效"的交易:靠更长的等待与更贵的片上结构,换来几乎为零的无效着色与最低的带宽写出。它在能效与遮挡能力上的表现,至今仍是移动 GPU 剔除机制中最极端的一档,但这套精确裁决只在 Tile 模式中成立,而且确定性越高,对 Shader 行为的兼容性要求越严格。接下来每一家的方案,都在试着用不同的折中绕开确定性裁决对 Shader 行为的约束。

2.4 Low Resolution Z(LRZ — Adreno)

桌面有 Hi-Z,PowerVR 有 ISP,Adreno 选了第三条路:把遮挡判断提到比 EarlyZ 更早的地方,而且不是做在片段上,是做在图元上。Low Resolution Z(LRZ)是 Qualcomm Adreno 的多阶段隐藏面剔除机制,核心动作是在 Binning 与 Rendering 两个阶段分别做遮挡判断,让不可见图元在进入片段处理路径之前就被尽量拦截。

LRZ 和桌面 Hi-Z 的位置不同:Hi-Z 主要管光栅化之后的片段裁决;LRZ 的目标直接作用在流入量上,削减的是根本不会进入渲染阶段的图元。

2.4.1 双阶段部署

Binning Pass 里的 LRZ(粗剔除):图元完成 PositionOnly Vertex Shader、还没进 Tiling 时,LRZ 用一个简化深度模型估算图元的最浅深度,与低分辨率深度图做区域比较。若图元完全落在已有 LRZ 区域的最远深度之后,直接从图元流中移除,不进任何 Tile 的 Bin List,更不会见到 Rasterizer。这层可以看作传统 HSR 最前沿的剔除层,直接压缩 Rendering 阶段要处理的图元数量。

LRZ 的数据格式需要精确理解,因为它决定了这套机制的很多行为。每个 cell 覆盖 8×8 像素,存储格式固定为 Z16_UNORM,不跟随 native Depth Buffer 的 D24/D32F 变化。每个 cell 存的是单方向保守极值:深度函数为 LESS 时存 conservative farthest Z,为 GREATER 时存另一个方向的极值,而不是同时保存 {MinZ, MaxZ} 对。Z32 精度压到 Z16 的损失,由硬件的 conservative comparison slack 消化。架构版本在这里划线:A6xx 及之前只支持单方向 LRZ;A7xx 引入 Bidirectional LRZ,实现方式是维护两套独立的 direction-specific LRZ surface(LRZ_LE + LRZ_GE),方向切换时直接换用另一份 surface,无需 invalidate。

Rendering Pass 里的 LRZ(细剔除):进入 Tile 后、Rasterizer 前,用更高分辨率的 LRZ Buffer 再做一次遮挡判断。若 LRZ 预测图元完全被遮挡,直接跳过 Rasterizer;若部分可见,走正常光栅路径,交给 EarlyZ 与 Fragment Shader 收尾。这一层相当于 Hi-Z 的简化版,作用是把无效片段的生成掐在光栅启动之前。

这套双阶段部署在多个独立来源中得到印证。Igalia 的 Danylo Piliaiev 在为 Mesa 的 Turnip 驱动逆向 LRZ 时,引用了 Adreno 官方文档的定义:Binning Pass 中构建一份低分辨率 Z-buffer,可以按 LRZ-Tile 宽度整块剔除图元贡献以提升 Binning 性能;同一份 LRZ 在 Rendering Pass 中继续生效,用于在与全分辨率 Z-buffer 比对之前高效剔除像素。官方定义与上述两段式结构逐项对应。

2.4.2 与 Hi-Z / ISP 的位置对比

对比维度Hi-Z(桌面)ISP(TBDR)LRZ(Adreno)
应用阶段Rasterization 后Tile 光栅后、着色前Binning + Rendering 双阶段
剔除粒度Pixel QuadPer-Sample(高精度)Tile Region(低精度区域)
遮挡机制多级深度金字塔(MaxZ)精确深度排序 + Tag BufferTile 区域深度 Range(估算)
剔除目标Quad每个采样点的片段Primitive(图元)
覆盖范围Rasterized Quad 列表Tile 内所有片段缓冲区Rasterizer 启动前的图元入口
控制流影响不影响 Draw Call 顺序完全消除绘制顺序依赖Binning 阶段减少图元排队,存在部分耦合
兼容性限制少量(如 discard / gl_FragDepth)中等(如 alpha blend / frag discard)高(详见下文)

2.4.3 兼容性限制与失效条件

位置越靠前,能容忍的意外越少。LRZ 的使用条件比 Hi-Z 或 ISP 严格得多,失效路径也更容易被触发。按失效影响范围可以归成 A、B、C 三类。

A 类:全局永久失效(Clear 前不可恢复)。现象是 LRZ 测试与写入完全禁用,退化到 Early-Z / Late-Z。三个触发点:深度函数设为 ALWAYS,LRZ 依赖深度比较提前剔除,这个模式下比较逻辑本身失去意义;深度函数设为 NOT_EQUAL,LRZ 存的是块级深度极值,只支持保守测试,无法精确判断不等关系;切换深度测试方向(如 LESS → GREATER),Adreno 7 系列之前的旧架构没有 Bidirectional LRZ,只有单向,硬件无法同时维护 ZMax/ZMin 两套金字塔。

B 类:写入禁用、测试保留(当前渲染批次失效)。现象是 LRZ 停止更新,只能拿历史数据做测试。根本原因是 Read-Modify-Write(RMW)操作破坏了 LRZ 的异步更新流水线。典型的触发场景:

场景RMW 冲突逻辑
深度写入 + 颜色混合混合需读目标颜色 → 阻塞 LRZ 深度更新
模板操作模板测试/写入本身即 RMW 操作
Subpass 间帧缓冲读取显式依赖前序 Subpass 数据 → 强制同步
逻辑操作 / 部分颜色掩码需读原颜色值 → 触发 RMW
MRT 部分附件未写入 + 深度写入保留未写入附件值需完整读-改-写 → 深度更新路径阻塞

C 类:单次绘制写入禁用(仅影响当前 Draw Call)。现象是本次绘制不更新 LRZ,但测试功能正常。核心原因是片段的最终状态依赖后期着色器决策,在着色执行完之前,硬件不知道这个片段到底会怎样:

  • discard:早期 LRZ 无法预知片段是否存活;
  • 动态修改深度(FragDepth):最终深度值在着色器阶段才确定;
  • 修改样本覆盖(SampleMask):片段实际写入的样本数后期才确定;
  • 向 UAV 写入:memory 竞争会破坏 LRZ 保守策略下的数据更新时序。

上述失效清单在第三方驱动实现中得到逐条验证。Igalia 的逆向记录复核了各触发条件:Blending、Logic Op、Color Write Mask 会让新片段的值依赖旧片段的值,LRZ 随之临时禁用,对应 B 类;discard、写 FragDepth、向 SSBO/Image 写入把片段的最终状态推迟到着色阶段才能确定,LRZ 同样只能退让,对应 C 类;深度比较方向切换直接命中 A 类:LRZ 为每个像素块按当前方向只存一份极值,方向为 GREATER 时存块内最小深度、为 LESS 时存块内最大深度,存量数据对新方向立即作废。第三方驱动实现与这份清单逐项吻合,表明这些失效条件是 LRZ 结构自身的边界,而非某一代产品的工程妥协。

2.4.4 工程效果

LRZ 的收益集中在图元入口:Binning Pass 的粗剔除能筛掉大批无效图元,降低后续 Tile 里的 Primitive 数量,大遮挡体场景收益最明显;Rendering Pass 里配合 EarlyZ,能进一步减少不可见图元产生的片段进入 Shader;Bin List 里记录图元数变少,TileBuffer 的写入负载也同步下降。

与 Hi-Z 的关键差异值得再强调一次:Hi-Z 是光栅化之后剔除片段,LRZ 是光栅化启动之前拦截图元流,控制力更前置。代价是 LRZ 对遮挡信息与判定模型的稳定性要求更高,一旦失效就是整条路径的回退。A/B/C 三类清单之所以这么长,原因在于 Adreno 把早期剔除做到最前端的同时,也完整地承接了"判断前置必然带来的假设脆弱性"这个成本结构。

2.5 Forward Pixel Kill(FPK — Mali)

如果 Adreno 的解法是"在入口拦截图元",Mali 早期没有 PowerVR 的 TBDR 核心专利,也无法实现整 Tile 延迟裁决,给出的近似方案是在渲染阶段用队列窗口模拟延迟裁决。Forward Pixel Kill(FPK)的设计初衷与 LRZ 类似:减少 Pixel Overdraw,提升渲染效率;作用位置则明显不同,FPK 在渲染管线的 Rendering Stage,光栅化之后对已生成的片段做动态剔除。

2.5.1 核心思想与机制

FPK 维护一个片段队列(Fragment Queue,可理解成先进先出的 FIFO),实时监测新到达片段与队列内已有片段的遮挡关系,动态终止那些被更近片段遮挡的、尚未进入 Fragment Shader 的旧片段。FPK 不是整 Tile 攒齐再裁决,而是在一个滑动窗口里边流边剔。

图元被光栅化后,Quad 先过 Early-ZS 测试,存活的进入 FPK pre-pipe buffer,该 buffer 位于 Shader Core Fragment Frontend 内部的本地队列(ARM Bifrost performance counter 明确称其为 "fragment pre-pipe buffer"),而不是 Tile RAM。两者的职责要分清:Tile RAM 存的是 Color/Depth/Stencil attachment state,FPK buffer 存的是待执行 Quad 的 work descriptor 与 coverage/kill bookkeeping。数据流模型上,Producer 是 Rasterizer + Early-ZS frontend(不只是 Rasterizer),Consumer 是 Fragment Spawn / Warp Scheduler(也不是 Rasterizer);FPK Logic 本身既不是 Producer 也不是 Consumer,而是挂在队列旁的 snoop + dependency lookup + invalidation engine。当后到的 opaque Quad 覆盖同一像素位置时,FPK Logic 在 buffer 里把旧 Quad 标记为 killed。

这个队列与 Hi-Z/LRZ 用的 FIFO 不是一回事。前面 Hi-Z、LRZ(渲染阶段)的 FIFO(如 InputQuadFIFO、SurvivedQuadFIFO)缓存的是刚光栅化的 Quad,用来做 Hi-Z 测试,这些测试发生在更细的 EarlyZ 之前;FPK 的队列更靠后,处理的已经是经过早期剔除的片段,在更接近着色的位置上做逐像素的精细判断。具体动作是:新 Quad 到达某像素位置时,FPK 检查该位置上已有的、尚未确定发给着色器的候选片段,比较两者深度。如果新片段更靠前且能完全遮住某个旧片段,旧片段被标记终止、直接从处理流程移除,永远不会被发送到片段着色器。最终每个像素位置理论上只有存活到最后的那个最前片段会被调度执行。

这套队列式动态剔除并非仅见于专利文本。ARM 官方介绍 FPK 的技术博客描述了同一套逻辑:FPK 自 Mali-T62X/T678 一代起进入 Mali GPU,已发射的着色线程不再"提交即必须跑完",只要同像素位置有更近的不透明片段到达,流水线中在飞的计算可以随时终止;管线入口那个 FIFO 的作用,正是把这个 kill zone 拉长,让遮挡关系有更大概率被硬件观察到。

2.5.2 局限

FPK 与 TBDR 的分界点在于决策范围。FPK 的剔除决策是片段的、局部的、依赖绘制顺序的,在片段流经队列的过程中动态做出,而不是像 TBDR 那样等一个 Tile 内所有片段都生成并存好再做一次性 HSR。窗口能看到的只是队列里的那一段,窗口之外来了更近的片段,旧片段已经放行就追不回来了。

这个时序窗口也决定了兼容性格局。FPK 与 Hi-Z 这类针对 EarlyZ 阶段的改进高度相似,对 AlphaBlend、Fragment Discard、Depth-Offset 同样不具备兼容性;但 FPK 不维护 persistent 的深度 metadata,所以不会像 LRZ 那样对 Stencil / DepthFunction 产生不兼容问题。同时,因为 FPK 在 EarlyZ 之后处理,接收与处理的粒度自然是 Quad 层级,而非 TBDR 那种 Per-Sample 精度。

真正的约束在排序上:动态 FIFO Kill 依赖队列里同一像素位置的片段彼此相邻,否则旧片段可能还没等到 Kill 就已经发往着色器。所以使用 FPK 时,开发者需要在上层按绘制顺序预先对不同 Draw Call 的 RasterOrder 排序后再提交处理,否则 FIFO 连续元素区间无法有效执行 Kill,效率会受影响。ARM 官方博客的口径与此存在张力:其说法是 FPK 借 FIFO 扩展 kill zone 后,不强制要求应用层排序即可接近 front-to-back 提交的效果。更精确的读法是排序属于增益而非必需,提交越接近有序,连续可 kill 区间越长。

2.6 Fragment Prepass(Mali)

FPK 的队列窗口有一个天然上限:能观察到的只是 FIFO 里的那一段遮挡关系。Mali 在 Immortalis-G925(以及更早的 Mali-G725/G625)上把窗口升级到了整个 Tile。Fragment Prepass 是一个完全由硬件驱动的片上隐藏面剔除机制:在片段主处理流程开始前,于 Tile 内先跑一遍快速预处理,把所有兼容 Draw Call 的片段统一裁决一遍,只把真正可见的放行进主通道。

2.6.1 执行流程

三个阶段。预处理阶段(Prepass):在光栅化之后、主通道之前,硬件对 Tile 内所有兼容 Draw Call 的片段做轻量级处理,记录每个片段的屏幕覆盖范围(Coverage)与初步深度信息,不执行 Varying 插值,不做颜色计算。可见性分析:当所有兼容 Draw Call 的预处理完成后,硬件分析覆盖信息,识别出被遮挡的片段并剔除。主处理阶段(Main Pass):只剩可见片段进入主通道执行完整 Fragment Shader,输出颜色并写入 Tile Buffer。这条路径下,只有对最终图像真正有贡献的片段才做后续处理。

与开发者熟悉的软件 Z-Prepass 相比,关键差异是不需要 CPU 层参与:传统做法里为提升 EarlyZ 命中率,引擎要在 CPU 上预排序 Draw Call;Fragment Prepass 由硬件自己在 Tile 内处理遮挡关系,降低了对 CPU 排序逻辑的依赖,线程利用率随之受益。Fragment Prepass 也支持复杂的模板操作与深度比较,目标是把片段可见性判断做到 sample-perfect。ARM 官方博客明确:Fragment Prepass 按 Sample 粒度精确裁决,剔除效率与图元的 Z 向提交顺序无关,这正是它与依赖时序窗口的 FPK 的区分所在。设计时还考虑了对多种 Shader 行为的容忍度,包括混合模式、非对称写入等。

2.6.2 与 DVS 的协同

Fragment Prepass 与 Mali GPU 上已部署的 Deferred Vertex Shading(DVS)天然咬合:DVS 把 Vertex Shader 拆成位置着色(Position Shading)与属性计算(Varying Shading)两段;Prepass 用位置数据做遮挡判断;如果某个图元在某个 Tile 内的所有片段都被遮挡,它的 Varying Shading 整段都可以跳过,不可见片段的顶点插值与 Varying 写入都被省掉了。这套组合在几何复杂度高、遮挡关系密集的场景里收益最厚,同时也给 Varying 数据提出了约束:结构过大或无法拆分,会直接影响 DVS 与 Prepass 的协同路径。

2.6.3 与 FPK 的关系

Fragment Prepass 可以看作 FPK 的架构级迭代,把"局部、时序"换成了"全局、顺序无关":

特性比较Forward Pixel KillFragment Prepass
剔除策略基于队列内逐像素遮挡判断基于 Tile 内全局分析
决策范围局部,依赖绘制顺序全局,顺序无关
插值控制无法规避 Varying 插值可阻断 Varying Shading
HSR 影响面受 FIFO 内容影响Tile 级遮挡剔除
扩展性逻辑简单,效果有限能处理复杂混合与遮挡

Fragment Prepass 是结构化、预分析导向的剔除机制,不是流水过程中的局部观察。正因为决策在预处理完成后才做出,单个 Draw Call 的 Shader 行为对 HSR 状态的影响可以被隔离在自身,不会扩散到整个 Tile。不过这份隔离有边界:Tile 里一旦混入不兼容 Draw Call(非典型 Framebuffer 配置、Shader 写 FragDepth、使用 discard 等),该 Tile 的部分或全部预处理路径会退化为 FPK 模式;discard、alpha-to-coverage、动态 SampleMask 这些行为都会削弱预处理路径的可靠性。工程上的对策是把兼容性高的内容合并处理,把需要 discard 或其他副作用行为的透明对象延后提交。

2.6.4 收益的量级

ARM 官方博客给出的细节与上面的判断吻合:一个简单的 Early-Z Draw Call,其 position 计算确实最多要执行三次(Binning、Prepass 与 Main Pass 各一次)。官方在固定频率平台上给出的开关对比实测,把收益的量级摆了出来:Fortnite 降落伞场景下整体 GPU cycles 下降 6.5%、浮点算术指令减少 16.1%、纹理操作减少 13.9%;Roblox 海盗湾场景的片段读带宽下降达 39%;但同一组测试里,星穹铁道行政区场景的 cycles 收益只有 0.6%。收益与成本都很具体,波动也同样具体。VS 执行次数翻到三次,在如今 VS 需求越来越多样的情况下到底算不算正向收益,还需要各家应用去验证。

2.7 Punch Through Techniques(Apple)

discard 这类不透明性控制,是所有隐式可见性裁决的共同障碍:它让"片段最终会不会写进帧缓冲"变得只能在 Fragment Shader 执行完才知道,EarlyZ 路径当场失效,降级到 LateZ。已经产生的 Overdraw 和能效损耗无法追回,移动平台上影响尤其显著。Apple 在 TBDR 架构里给了这类 Feedback Object 一条专门的出路,机制叫 Punch Through,主要通过专利文档(US10074210B1)与开发者会议资料间接披露。

历史脉络值得一提:Imagination(PowerVR)早在 2005 年的专利里就讨论过把 shader 拆成"决定 visibility 的部分"与"最终 shading 的部分"两阶段执行的思路。Apple 的 Punch Through 与 PowerVR 的 PT Feedback 属于同一条 TBDR "Shader → Visibility Hardware Feedback" 演进路线。TBDR 阵营很早就意识到,放弃延迟裁决去迁就 discard 不是唯一选项,可以让 Shader 把可见性"反馈"给硬件。

Punch Through 的目标是:在保持整体 HSR 连续性与片上数据一致性的前提下,让包含 Feedback Object 的渲染路径不至于中断裁决、不至于产生无效着色。它提供两条执行路径。

2.7.1 Fast Path:最小中断策略

Fast Path 下,Feedback Object 的片段不进 Tag Buffer,而是绕过它直接执行完整 Fragment Shader。执行结果立即用于颜色输出,同时把深度信息与遮挡状态和原 Tag Buffer 里的已有数据比较,触发对被遮挡片段的移除;最终 Feedback Object 的片段结果再被写回 Tag Buffer,参与后续片段的裁决。

这条路径的取舍清楚:兼容性好,调度代价低,Tile 渲染流程持续推进,不引入状态清空或 pipeline flush,剔除行为完整,执行路径不分裂,还能避免对已有 opaque 内容的重复着色。但软肋也在明处:如果 Feedback Object 自己被后来的物体遮挡,已经完成的着色开销追不回来,这构成浪费。Fast Path 无法避免 Feedback Object 自身的无效着色支出。

2.7.2 Two Pass Path:冗余剥离策略

要连 Feedback Object 自身的无效计算也压缩,Apple 准备了 Two Pass Path,把 Feedback Object 的着色阶段拆开。第一阶段 Visibility Pass:硬件运行一个"可见性着色器",它可能由编译器从主 Shader 拆解生成,只保留用于 discard 判断所需的分支,不做颜色计算;可见片段的信息(如深度值)被用来更新 Tag Buffer、触发对已有片段的裁决,未通过 discard 或被遮挡的片段直接丢弃,不进下一阶段。第二阶段 Color Pass:只对通过了 Visibility Pass、被标记为可见的片段执行完整 Fragment Shader,输出颜色写入 Tile Buffer。

Two Pass Path 能在保留完整着色语义的同时,避免 Feedback Object 侵蚀全局着色吞吐率,适合 discard 条件复杂或容易被遮挡的透明对象路径。用一段伪流程对比三条路径的时序可以清楚说明。以渲染顺序 A(opaque)→ B(feedback)→ C(opaque)为例:

  • 传统路径:B 被判定为透明,触发 Tag Buffer flush,A 的片段要提前执行着色;B 的片段也完整着色,即便后续 C 会挡住它的一部分,着色资源已经消耗掉。
  • Fast Path:B 绕过 Tag Buffer,执行着色输出,再触发对 A 片段的遮挡剔除;C 继续写入。A 被遮挡的部分避免了着色,但 B 自身若被 C 遮挡,那部分无效着色无法回避。
  • Two Pass:B 先做可见性判断(Visibility Only),提前剔除被 discard 的部分,只保留可见片段进入 Color Pass;C 写入前不会触发 A 与 B 的提前着色。B 既避免了自身的冗余部分,也避开了被 C 遮挡部分的计算。

Two Pass Path 是在硬件层对传统 Alpha-Test 与 LateZ 限制的结构级破解,多层遮挡或碎片化反馈内容较多时,节省的片段执行资源相当可观。

2.7.3 与 Fragment Prepass 的对比与启示

Punch Through 与 Mali 的 Fragment Prepass 目标有重叠,策略明显不同:

对比维度Fragment PrepassPunch Through
目标场景通用 HSR 提升Feedback Object 优化
执行模型Tile 级统一裁决针对特定片段的路径控制
剔除机制预处理后统一筛选动态插队或两阶段执行
兼容 Draw Call 行为敏感,遇到不兼容路径退化可局部隔离处理不影响其它片段
控制粒度Draw Call 级Fragment 级

Punch Through 的关键优势在"局部隔离":执行 discard 或 alpha 控制行为的 Draw Call 存在时,对整个 Tile 内其它片段的 HSR 状态没有任何干扰。这是一种硬件层的路径路由与调度剥离,把 discard 类行为从"全路径退化"中解放出来,变成"局部隔离处理"。

这条设计的背后假设很务实:实际应用中,UI、植被、特效这类透明内容绕不开,与其让它们拖垮整条渲染路径,不如给硬件一个机制去隔离负面影响。对开发者的启示是:在 Apple 平台上组织透明对象时,明确区分 discard 控制区段与不透明区段,能让剔除收益最大化地保留下来。


2.8 跨架构 HSR 失效对比与统一框架

前六节的失效条件散落在各自的章节里。要看清 HSR 机制之间的结构关系,需要把各家拉上同一张表,用同一套度量。这一节先定义失效等级,再逐项对比,最后引入统一的评估基准。

2.8.1 失效等级定义

对照各家的失效条件,可以按影响范围归成三个等级:

等级含义典型结果恢复方式
A:持久状态失效hierarchical metadata 本身失去可信度后续所有 Draw 的 HSR 判断可能错误显式 Clear / Rebuild / Resummarize
B:渲染批次截断当前 deferred/HSR 窗口被迫中断Tile 或 batch 内剩余内容退化为无 HSR等下一个 Tile / 新 batch 开始
C:单次 Draw 失效当前 Draw 无法使用/更新某种 HSR,但不污染后续状态Draw N 变 Late-Z;N+1 恢复正常无需恢复

A 类最严重但最少见,C 类最常见但影响最局部。这个等级分布本身暴露了两大体系的性格差异:IMR 系的一个关键特点是坏 Draw 通常只触发 C 类(自己变 Late-Z,不天然污染后面的 Draw),AMD/Intel/NVIDIA 的 HiZ metadata 与单个 Draw 的 Shader 行为相对解耦。TBR 系则存在 B 类(ISP 的 Tag Flush、Mali FPP 的 Tile suffix incompatible),deferred execution 把多个 Draw 的片段积攒在同一个 Tile 窗口内,一个不兼容的 Draw 就能打断整个窗口。解耦与否,不是实现细节,是"边算边删"与"攒齐再删"两种模型的结构差异。

2.8.2 跨架构失效对比表

把各家机制按触发条件摊开:

触发条件AMD HiZIntel HiZNVIDIA ZCullAdreno LRZMali FPKMali FPPApple ISP/PT
普通 opaque最佳最佳最佳最佳最佳最佳最佳
discardC:当前 Draw PostZC:Early-Z 受限C:可能关闭 Early-ZC:Binning LRZ write off;Rendering Feedback 恢复C:FPK 效果差(kill 条件不确定)支持(执行到 coverage known)支持(Punch Through Feedback)
FragDepth 写入C:PostZC:Shader-before-ZC:关闭 Early-ZC:当前 Draw skip LRZC:同 discard支持(Prepass 含 depth 判断)支持(Two-Pass Path)
Alpha BlendEarly-Z 仍可用(HiZ reject behind-depth)同左同左LRZ Test 保留,LRZ Write 禁止FPK 不可 Forward Kill部分支持B:Translucent flush deferred fragments
UAV/SSBO WriteC:Shader elimination 受限同类原则同类原则C:Late-ZPure write compatible;Read+Write incompatiblePure write compatible要求执行
Depth 被外部路径修改A:HiZ ResummarizeA:Aux Resolve公开资料不足A:LRZ direction invalid无(无 persistent metadata)无无
DepthFunc 方向切换无影响无影响无影响A(pre-A7xx)/ 无影响(A7xx Bidirectional)无影响无影响无影响

一眼能读出的规律是:越往右、越是攒齐再裁决的机制,支持面越宽;但没有 persistent metadata 的 FPK 反而躲过了最重的 A 类,原因是 FPK 根本不维护那份需要保持可信的摘要。

2.8.3 统一评估基准:硬件在片段着色前获取可见性信息的时机与范围

若以"硬件在片段着色前获取可见性信息的时机与范围"作为评估基准,上述机制在管线中的前置程度依次为:

                         信息前置程度
                              ↑
Apple / PowerVR ISP           │ 整 Tile 的全部图元(延迟到最后才着色)
Mali Fragment Prepass          │ 整 compatible Tile prefix(compatible 段内全局裁决)
Adreno LRZ Binning            │ Full-frame coarse visibility(Binning 扫全帧)
Mali FPK                      │ Pipeline temporal window(FIFO 内的时序重叠窗口)
IMR HiZ / ZCull               │ 已提交图元的 coarse depth history
Fine Early-Z                  │ 已提交图元的 pixel/sample depth
                              └────────────────────────────────────────────

这个基准的含义:信息前置程度越大,能消灭的 Overdraw 越多,但对 ordering、side-effect、RMW barrier 的敏感度也越高。看两端就明白:ISP 和 Fragment Prepass 看到整个 Tile,discard/FragDepth 对它们的挑战最大,因为必须执行 Shader 才能确认 visibility,这与"defer to last"的策略正面冲突;IMR HiZ 只看已经提交的历史深度,discard 对它的影响仅限于当前 Draw 变 Late-Z、不波及后续,正因为信息量最小,容错也最高。

这个基准也解释了前文的一个现象:为什么 Apple 和 Mali 需要 Punch Through、Fragment Prepass 这类补偿机制。信息前置程度大带来高剔除率,遇到不可预测的 Shader 行为时就需要局部旁路来止损;IMR 系不需要这类补偿,因为其基线信息量不依赖未来知识。信息前置的收益和它要求的代价,始终成正比。

2.8.4 Mali FPP 对 UAV/SSBO 的精细兼容规则

Fragment Prepass 对 side-effect 的兼容判断,比"有 UAV 就不兼容"这种一刀切精细得多。Mali 是按访问模式拆开看的:

UAV 访问模式兼容性原因
Pure Write(无读取返回值)Compatible不存在 previous shader result → current shader decision 的因果链
Read-onlyCompatible不修改状态
Read + Write 同一 resourceIncompatible读到的值依赖执行顺序,Prepass 重排会破坏语义
Atomic(返回值不参与后续计算)Compatiblefire-and-forget,无因果依赖
Atomic(使用返回值做判断)Incompatible结果参与控制流,执行顺序有意义

这套准则可以从第一性原理推出来:只要不存在"前一个 Shader 的执行结果影响当前 Shader 的决策"这条因果链,Prepass 的重排就不会改变程序的可观察行为。判断准则落在因果链上,而不是落在"写了没写"上。这是各家失效规则中唯一一个用结构而不是枚举来定义兼容性的做法。


HSR 技术链到此收尾。把六条机制并排放,它们共享同一个结构模式:硬件在着色之前维护一份隐式的可见性状态(Hi-Z 的金字塔、LRZ 的区域极值、ISP 的 Tag Buffer、FPK 的候选片段队列),再以保守比较或队列裁决提前剔除,换取片段阶段的带宽与 ALU 节省。六条机制的差别只在状态的组织方式与裁决的部署位置。这份状态由固定功能电路维护与更新,对应用层透明,代价是每条机制都带着一张兼容性失效清单:discard、FragDepth、Alpha Blend、SampleMask,凡是让片段最终状态在着色前不可知的 Shader 行为,都会迫使路径退化。最基础的 EarlyZ 在这些行为面前要降级为 LateZ;LRZ 的 A/B/C 三类失效条件是这张清单最完整的写法;Fragment Prepass 遇到不兼容 Draw Call 退化为 FPK,是同一现象的另一种表现。应用层对清单的唯一应对是调整提交内容与顺序,硬件路径本身不可编程。失效清单不是实现瑕疵,而是结构必然:隐式裁决的有效性建立在"最终状态可提前保守预测"这个前提上,Shader 的可编程性每多一分,这个前提就多一分不成立。固定功能裁决的有效域,一直在被可编程性压缩。

粒度上限的审视同样支持这一判断。Hi-Z 的保守比较存在假阴性,小遮挡物在高层级形不成有效阻挡;ISP 把裁决精度推到 Per-Sample,代价是整条 Tag 路由、覆盖整个 Tile 的片上缓存与 Tile 内的串行执行;Fragment Prepass 为了维持硬件裁决的完整性,已经要把 VS 执行到三次。这条成本曲线是固定功能路径复杂度预算接近耗尽的直接读数。逐图元的像素级精准剔除,固定功能做不到,这件事最终由 Shader Culling 完成:开发者在着色器里用确切计算替代保守状态,没有兼容清单,不依赖提交顺序,也不存在路径退化。这与第一章几何调度侧的判断方向一致:固定功能前端的裁决能力接近物理上限,控制权逐步移交软件。几何调度与可见性裁决两条线的证据至此并排摆好。

三、最佳实践

机制讲完,落到开发侧。这一章不是通吃的优化教条:没有哪种实践能在两种互斥的硬件模型上同时最优。下面的清单按硬件阵营分开,每条背后的机制都在前面章节展开过,这里只写动作和代价边界。按 TBR 阵营(Mali / PowerVR / Apple / Adreno)和 IMR / TBIMR 阵营(NVIDIA / AMD / Intel)分组。

3.1 渲染路径设计:与硬件架构协同

3.1.1 面向 TBR 架构(Mali / PowerVR / Apple / Adreno)

TBR 平台的全部优势建立在 Tile 级片上执行模型上,路径设计围绕一件事:让片段处理尽量不发生片外访问、尽量不打断 Tile 状态。

  • 策略一:合并 Opaque Pass,保持 Tile 内状态一致性。所有不透明对象放进同一个 Render Pass(Vulkan 里对应单个 Subpass,Metal 里对应一个 Render Command Encoder)。这样 Binning 阶段能全局扫描所有图元、生成稳定的 Bin List,Tile Buffer 不用反复写回与清空,ISP / LRZ / Fragment Prepass 才能以完整 Tile 范围执行。
  • 策略二:禁用手动 Z-Prepass。现代 TBR 上 Z-Prepass 是冗余路径:ISP 或 Fragment Prepass 这类片上模块已经自动做完提前裁决。强行再跑一遍会引入额外的 TileBuffer flush 与 DRAM 写入,多数场景是负优化。
  • 策略三:限制 Tile 内图元数量与顶点数据密度。Bin List、Parameter Buffer、Tag Buffer 的片上容量都有限。某个 Tile 被过量小图元覆盖时,Bin List 溢出到 DRAM、片段数超过片上 HSR 模块负载上限,两条路都会把收益抵消。建模时避免大量跨 Tile 的狭长图元,用 HLOD、meshlet 分组、屏幕剪裁抑制输入复杂度。

3.1.2 面向 IMR / TBIMR 架构(NVIDIA / AMD / Intel)

桌面带宽宽裕、光栅吞吐高,路径设计的任务是触发其内部的多级缓存与批量调度机制。

  • 策略一:按 front-to-back 排序提交不透明对象。提交越接近由近到远,Hi-Z 金字塔更新越快,coarse-grain Z 层级被提前填满,后续图元更容易在早期被批量剔除。大遮挡体场景下,Shader 执行次数与带宽消耗一起降低。
  • 策略二:针对深度复杂场景引入 Z-Prepass。遮挡关系复杂的场景(森林、密集城市建筑)中,Z-Prepass 能有效提升 Hi-Z 的粒度命中率。这是一笔需要评估的开销:执行成本对节省的片段计算。遮挡层次扁平、或光栅开销高于 Shader 开销的场景不该用。
  • 策略三:组织空间聚合单元以配合图元批量调度。Tiled Caching 与 DSBR 的批量调度效率取决于图元的空间聚集程度(机理见 1.3.5)。引擎应在离线阶段构造 Mesh Cluster / meshlet,让每个聚合单元的图元尽量收敛在少数 Tile 内,提高 L2 / Infinity Cache 里的数据局部性。单个图元横跨多个 Tile 会直接削弱聚合效率。

路径设计得对不对,最终落到四个可观察量上:TileBuffer 有没有效命中、Shader 执行有没有避开冗余输入、片段调度有没有频繁阻塞或顺序依赖、pipeline flush 与隐式同步有没有被最小化。优化方向是让这四项尽量干净,而不是对每个平台套同一套流程。

3.2 几何数据组织:从源头降低负载

渲染瓶颈常常在几何阶段就定了型。顶点数据的组织方式决定 Vertex Shader 吞吐、缓存命中率、后变换重用效率与片上数据流的稳定性。错误组织会带来冗余变换、数据搬运放大与 Tile Buffer 溢出,把后续优化空间全部锁死。下面五条按影响面排。

  • 策略一:使用索引缓冲以激活后变换缓存(Post-Transform Cache)。索引缓冲让 GPU 能复用已处理过的顶点变换结果:PTC 依赖顶点索引构建局部缓存队列,索引复用率足够高时,Vertex Shader 的重复执行被大量消除。每个顶点都显式展开的非索引绘制,会完整失去这类优化。
  • 策略二:按访问模式组织顶点属性字段。把常访问的字段(如 Position)与不一定每帧都用的字段(如 Tangent、SkinWeight)物理分离,减少 Vertex Fetch 阶段无谓的传输与缓存占用。保持对齐结构,避免硬件做额外插值与字段重排。在启用 DVS / IDVS 的架构上,这条还能减轻参数写入负担。
  • 策略三:避免跨 Tile 分布的狭长图元。跨多个 Tile 的细长图元,在 TBDR / IDVS / DVS 架构里会被多个 Tile 分别激活处理,顶点插值与 Shader 调度都无法复用,顶点数据被多次加载、多次变换。静态物体(UI 边框、地形缝隙)中这问题尤其明显,用几何拆分、屏幕空间 clip 或阈值过滤把图元控制在单 Tile 内。
  • 策略四:控制单位面积图元密度。过高的三角形密度会让 Tile Bin List 溢出、ISP/FPK 队列溢出、LRZ 判断粒度下降。注意一种隐蔽的浪费:堆在不可见区域的小图元,即便被剔除,也先付出了变换与调度成本。开发中用 Overdraw Heatmap 或 GPU Debugger 观察 Tile 内图元密度变化,必要时用 HLOD / Impostor 削减远距与遮挡区的小图元实例。
  • 策略五:采用空间一致性策略构建 meshlet / Cluster。图元分组在桌面平台用于触发 DSBR / Tiled Caching 的批量调度,在移动平台是控制 Tile 冲突与纹理/常量局部性的基础。若 Cluster 内图元跨越过多 Tile,内部属性无法集中写入 Parameter Buffer,写回粒度与 cache-line 分布一起恶化。资源构建阶段就该按 Tile 适配性评估 Cluster 的屏幕空间尺寸。

几何组织的方式不只是资源文件的存储格式,它决定整条管线从顶点加载到图元光栅的调度路径连不连贯。对移动 GPU 而言,图元是否适配 Tile 粒度、是否有位置局部性,常常比 Shader 计算复杂度更影响性能。排查时过四个问题:有没有激活后变换缓存、会不会引发多 Tile 重复调度、有没有引入 Varying 写入浪费、会不会导致缓存资源早期失效或溢出。

3.3 着色器与渲染状态管理:避免管线中断

几何和路径都对,着色器配置与渲染状态没贴住硬件行为模型,管线一样会堵。这一节的问题都出在 GPU 后段:Shader 的可编程性是双刃剑,用得不对,会以隐藏的硬件代价呈现。

  • 策略一:审慎使用 discard / depthOffset / alphaBlend。这三类行为让片段输出不可预测,EarlyZ 降级 LateZ,所有片段被迫执行完整 Fragment Shader,再由深度测试决定是否写入,Overdraw 直接放大。移动平台更严重,它们会破坏 ISP / LRZ / Fragment Prepass 的剔除路径。含这些行为的对象应放在渲染路径末尾,不影响其它对象的遮挡判定。
  • 策略二:避免在片段着色器中写入 FragDepth。动态修改深度会阻止一切早期剔除(Hi-Z、LRZ、FPK 都要求片段在执行前深度可知)。确有深度调整需求(如屏幕空间阴影偏移)时,用 Uniform 控制偏移并在顶点阶段算出深度、经插值传入;条件不允许,可以考虑 Dual-Source Blending 这类不碰深度的替代手段。
  • 策略三:控制 Varying 结构体尺寸与写入模式。Varying 会缓存写入 Tile Buffer 或 Parameter Buffer;在 DVS / IDVS / UDL 平台上,只有真正进入 Rasterizer 或 Prepass 的片段才激活 Varying 写入。结构过大(超过 8 个 float4)会占满片上带宽与 Tile 缓存,把不常用属性拆到单独路径或用查表替代直传。
  • 策略四:避免过度切换渲染状态。每次 Pipeline State 切换都触发缓存失效与调度刷新(BlendMode、DepthFunction、RenderTarget Format、Multisample Count 均是)。对 TBIMR 架构,频繁切换还会打断 Bin List 的合并粒度。引擎调度里按 PSO 哈希聚合 Draw Call,批次连贯性会明显改善。
  • 策略五:控制片段着色器的结构分支复杂度。分支的实际代价取决于 GPU 的 SIMD 分布与动态遮罩逻辑:对大面积遮挡后的片段,即便部分分支不触发,ALU 预算仍按所有分支路径预支。用 pre-branch 策略,在顶点阶段或 CPU 侧预先算好分支索引,再用 switch 精准调度,避免无效执行路径的激活。

排查 Shader 与状态问题时,判断标准四条:有没有打破 EarlyZ 剔除逻辑、有没有引入分支或深度写入的路径不确定性、有没有造成片上 Varying 写出冗余、有没有频繁触发管线状态刷新或同步。


四、总结

4.1 架构演进的回顾

几何与 HSR 两条线各自走到当前阶段,回看时是一个动作的两面:硬件一次次把"该在哪一级决定一个图元/片段的存活"前移,又在每处前移被可编程性追上后把控制权交出去。HSR 路径的演化趋势清晰:硬件的裁决能力从被动补救(EarlyZ)走向全 Tile 预测(ISP / Fragment Prepass),同时不断被可编程性的扩张逼退。

4.2 未来展望

未来值得关注的,不是哪家又发布了什么新单元,而是第一章与第二章两条判断各自落地的样子。几何处理范式上,Mesh Shading 已经把传统顶点流水线重构为以 meshlet 为单元的调度结构,图元剔除、LOD 选择与视锥裁剪都能在 Shader 层按需完成,空间裁决从硬件下沉到可编程路径,调度粒度与跨 Draw Call 整合能力都变了。1.3.5 中的定性是:Mesh Shading 不是这条形态史的新起点,而是终点1。

HSR 侧,随着 VRS(Variable Rate Shading)这类技术把"片段可见"拆碎,可见性已经不足以单独评估一个片段的执行必要性。剔除机制的方向是融合几何复杂度、材质成本、运动状态与感知模型多个维度,构建面向整帧执行效率的优先级体系。HSR 从孤立的早期优化模块,变成资源分配路径上的一环。

GPU 渲染体系正从"显式提交 + 硬件执行"的双层结构,过渡到"结构调度 + 控制"的统一调度模型。代价是软件开发栈必须精确理解底层执行机制:更结构化的数据与调度描述,只有在能看见执行路径时才是资产,否则只是新的抽象负担。


参考文献

  1. 羲和啦. GPU体系结构视频集. Bilibili. https://space.bilibili.com/250050043?spm_id_from=333.1391.0.0
  2. Simon Schreibt. Render Hell 2. https://simonschreibt.de/gat/renderhell/
  3. Encelo. A trip through the Graphics Pipeline. https://encelo.github.io/trip_through_graphics_pipeline_2011.html
  4. Qualcomm. Adreno GPU on Mobile Best Practices. Qualcomm Documentation. https://docs.qualcomm.com/bundle/publicresource/topics/80-78185-2/best_practices.html
  5. ARM. ARM Mali GPU Best Practices - Developer Guide. ARM Developer. https://developer.arm.com/documentation/101897/latest
  6. Imagination Technologies. A look at the PowerVR graphics architecture: TBDR. Imagination Blog. https://blog.imaginationtech.com/the-dr-in-tbdr-deferred-rendering-in-rogue/
  7. Igalia. Low-resolution-Z on Adreno GPU. Igalia Blog. https://blogs.igalia.com/dpiliaiev/adreno-lrz/
  8. ARM. Hidden Surface Removal in Immortalis-G925: The Fragment Prepass. ARM Community Blog. https://community.arm.com/arm-community-blogs/b/mobile-graphics-and-gaming-blog/posts/immortalis-g925-the-fragment-prepass
  9. ARM. Killing Pixels - A New Optimization for Shading on ARM Mali GPUs (FPK). ARM Community Blog. https://community.arm.com/arm-community-blogs/b/mobile-graphics-and-gaming-blog/posts/killing-pixels---a-new-optimization-for-shading-on-arm-mali-gpus
  10. Apple. Apple GPU "Punch Through" - US10074210B1. Google Patents. https://patents.google.com/patent/US10074210B1/en
  11. Programmable compute engine screen mapping. Google Patents. https://patents.google.com/patent/US8698814
  12. Distributed rendering of texture data. Google Patents. https://patents.google.com/patent/US7969444B1
  13. AnandTech. The NVIDIA Maxwell GPU Architecture (GM204) - Tiled Caching. https://www.anandtech.com/show/8526/nvidia-geforce-gtx-980-review/4
  14. Beyond3D Forum. Tile-based Rasterization in Nvidia GPU. https://forum.beyond3d.com/threads/tile-based-rasterization-in-nvidia-gpus.58296/
  15. Silhouettes For You. Life of a triangle. https://silhouettesforyou.github.io/2023/04/20/ed6487d7b99b/
  16. AMD. AMD Vega Architecture - Whitepaper. TechPowerUp. https://www.techpowerup.com/gpu-specs/docs/amd-vega-architecture.pdf
  17. AMD. AMD RDNA 2 ISA Reference Guide. https://www.amd.com/content/dam/amd/en/documents/radeon-tech-docs/instruction-set-architectures/rdna2-shader-instruction-set-architecture.pdf
  18. Reddit r/Amd. With DSBR enabled. https://www.reddit.com/r/Amd/comments/6qntvi/with_dsbr_enabled_rx_vega_56_64_will_have_the/
  19. NVIDIA. NVIDIA Turing Architecture In-Depth - Mesh Shading. NVIDIA Developer Blog. https://developer.nvidia.com/blog/nvidia-turing-architecture-in-depth/
  20. Microsoft. DirectX 12 Ultimate - Mesh Shaders. DirectX-Specs. https://microsoft.github.io/DirectX-Specs/d3d/MeshShader.html

Footnotes

  1. Mesh Shading 的完整分析在系列终章中展开。 ↩ ↩2