WEBKT

深入底层:Adreno GPU 的 GMEM Tiling 机制是如何让 Vertex Shader 执行两次的?

17 0 0 0

在 PC 端显卡(IMR 架构)上,顶点着色器(Vertex Shader, 简称 VS)的执行逻辑非常直观:每个顶点进,VS 跑一次,输出结果直接送去光栅化。

但在移动端,以高通 Adreno 为代表的 GPU 普遍采用 TBR(Tile-Based Rendering,分块渲染)或 TBDR 架构。为了极致地节省带宽,Adreno 引入了 GMEM(高速片上图形内存)Tiling(分块) 机制。

这种机制在带来带宽红利的同时,也彻底改变了 Vertex Shader 的生命周期,直接导致 Vertex Shader 在底层往往会被执行两次。本文将从底层硬件架构出发,拆解这一机制对 VS 执行频率的影响。


一、 TBR 架构的硬伤:为什么要分阶段?

移动端 GPU 最大的瓶颈不是算力,而是功耗和带宽。直接读写系统内存(System RAM)极其耗电。

Adreno 的解法是:在 GPU 芯片内部集成一块极快、极省电但容量很小(通常几 MB)的 GMEM
为了把整张渲染目标(Render Target)塞进这几 MB 的空间里,GPU 必须把屏幕切成一个个小块(Tiles,例如 $32 \times 32$ 或 $64 \times 64$ 像素),逐个 Tile 进行渲染。

为了知道“哪些三角形落在哪个 Tile 里”,GPU 必须引入一个前置步骤——Binning Pass(分箱阶段)

因此,整个渲染管线被切分裂为两个物理阶段:

  1. Binning Pass:生成可见性流(Visibility Stream)。
  2. Rendering Pass(Color Pass):逐 Tile 进行光栅化和像素着色。

正是这个切分,重塑了 Vertex Shader 的执行频率。


二、 VS 的“双重人格”:两次执行的本质

在 Adreno 架构中,为了配合上述两个阶段,Vertex Shader 被拆分成了两个版本在不同的时间点运行:

[顶点数据输入] 
     │
     ├─► 阶段 1:Binning Pass ──► 仅执行 Position-only VS (只计算 gl_Position)
     │                                 │
     │                                 ▼ 生成 Visibility Stream (确定三角形与 Tile 关系)
     │
     └─► 阶段 2:Rendering Pass ─► 仅对可见三角形执行 Full VS (计算 Varyings / UV / Normal)

1. 第一阶段:Position-only VS(只算位置)

Binning Pass 阶段,GPU 的核心目的是进行几何分类(Tiling)和裁剪(Culling)。此时,GPU 根本不需要知道顶点的纹理坐标(UV)、法线(Normal)或是顶点颜色,它只需要知道顶点的 屏幕空间坐标(gl_Position

因此,Adreno 驱动会动态生成一个简化版的 VS,称为 Position-only VS(或 Coordinate Shader)。

  • 执行频率:所有被 Draw Call 提交的顶点,在这个阶段全部都会执行一次 Position-only VS(除非在输入端被 Index Buffer 过滤)。
  • 行为:只计算与 gl_Position 相关的指令,其余所有的 Varying 输出、纹理采样等无关代码全部被剥离。

2. 第二阶段:Full VS(完整计算)

到了 Rendering Pass,GPU 开始逐个 Tile 渲染。此时要跑 Pixel Shader,必须要对顶点属性(Varyings)进行插值。

  • 执行频率:只有在 Binning Pass 中被判定为可见(通过了视锥体裁剪、背面剔除,且其所在的三角形至少覆盖了当前渲染 Tile)的顶点,才会触发 Full VS
  • 行为:执行完整的 Vertex Shader 代码,计算 UV、法线、切线等所有 Varying 属性,并将数据存入 VPC(Vertex Parameter Cache,顶点参数缓存),供后续光栅化和插值使用。

三、 Tiling 机制对 VS 执行频率的边际效应

这种“拆成两次跑”的机制,对 VS 整体执行频率和开销带来了极其微妙的影响,主要体现在以下三个维度:

1. 裁剪率(Culling Rate)带来的负反馈

  • 高裁剪率场景:如果场景中存在大量视锥体外、或者背面的三角形,它们在第一阶段(Binning Pass)就会被无情剔除。这意味着这部分顶点永远不会执行第二阶段的 Full VS。此时,总体 VS 算力消耗被大幅节省。
  • 低裁剪率场景:如果几乎所有三角形都可见,那么绝大多数顶点都会切实地跑满一次 Position-only VS + 一次 Full VS。这无形中增加了 GPU 的 ALU(算术逻辑单元)压力。

2. 顶点参数缓存(VPC)溢出的灾难

Full VS 计算出来的 Varyings 是要存在片上 VPC 中的。

  • 如果你的 Shader 输出了太多的 Varying 属性(例如大量的 highp vec4),或者一个 Tile 内的三角形数量过多,导致 VPC 容纳不下这些顶点数据
  • 此时,Adreno 无法将数据保留在片上,被迫将未处理完的数据溢写(Spill)到系统内存(System RAM),或者在需要时强行重新运行 Full VS
  • 这会导致可见顶点的 Full VS 执行频率由于 Cache Miss 再次倍增,造成严重的性能掉帧(Stall)。

3. 顶点不连续性(Index Buffer 优化)

如果顶点索引(Index Buffer)杂乱无章,GPU 的 Vertex Cache 命中率极低,在 Binning 阶段同一个顶点可能会被多次拉取并重复执行 Position-only VS,这会直接拉高第一阶段的执行频率基数。


四、 针对 Adreno 架构的 Vertex Shader 优化指南

理解了 GMEM Tiling 对 VS 频率的影响,我们在编写 Shader 和准备美术资源时,就能进行极其精确的逆向优化:

1. 彻底分离顶点属性(Position Split / Stream Out)

既然 Binning Pass 只需要位置信息,我们就应该让 GPU 读取位置信息时尽可能快。

  • 做法:在应用层,将顶点的 Position 属性和 UV/Normal 属性分开存放(使用两个独立的 Vertex Buffer 绑定,即 Non-interleaved 格式)。
  • 原理:在 Binning 阶段,GPU 只需要加载 Position Buffer。由于去掉了冗余的 UV/Normal 数据,Cache 命中率极高,带宽消耗降到最低。
// 不推荐:交错布局(Interleaved)
[ X, Y, Z, U, V, Nx, Ny, Nz ] [ X, Y, Z, U, V, Nx, Ny, Nz ]

// 推荐:分离布局(Split-stream)
Buffer 1: [ X, Y, Z ] [ X, Y, Z ]  <-- Binning Pass 仅读取此 Buffer,极快
Buffer 2: [ U, V, Nx, Ny, Nz ] [ U, V, Nx, Ny, Nz ]

2. 控制 gl_Position 的计算复杂度

在 VS 中,尽量让计算 gl_Position 的路径简单。

  • 不要在计算 gl_Position 时去采样纹理(例如顶点位移贴图 Vertex Texture Fetch)。如果必须采样,这会导致 Position-only VS 也要执行昂贵的纹理采样,彻底毁掉 Binning Pass 的速度。
  • 将那些与位置无关的复杂数学计算(如复杂的法线变换、复杂的动画混合)放到计算 Varying 的逻辑中去。

3. 极力精简 Varying 数量与通道数

Varying 直接占用 VPC 空间。

  • 砍掉不必要的 Varying 输出。如果能把两个 vec2 合并为一个 vec4,尽量合并。
  • 适当降低 Varying 的精度(例如在支持的平台上使用 mediump),这能直接减少 VPC 的物理占用,避免因 VPC 溢出导致的 Full VS 额外重跑。

4. 离线顶点 Cache 优化

使用工具(如 meshoptimizer)对导出的 Mesh 进行顶点 Cache 优化(Vertex Cache Optimization)。

  • 这样可以确保在 Binning Pass 中,相邻三角形复用相同顶点时,Position-only VS 能够直接命中后变换缓存(Post-Transform Cache),从而将第一阶段的执行频率逼近理论最小值(即顶点数量,而非三角形数量 $\times 3$)。

总结

在 Adreno GPU 上,GMEM Tiling 机制通过 Binning Pass 将 Vertex Shader 的执行拆解为 “快速粗筛(Position-only)”“精细重塑(Full)” 两个阶段。

这一机制对 VS 执行频率的直接影响是:位置计算高频发生(100% 顶点覆盖),而完整计算低频发生(仅覆盖未被剔除的可见顶点)。通过分离顶点流、精简 Varying 以及避免在位置计算中引入复杂逻辑,可以最大化地释放 TBR 架构的性能红利。

图形性能哨兵 Adreno GPU

评论点评