GRAPHICS × PRODUCTION
中EN

皮了个牛

↓

计算机体系结构

Evolution history of Vertex Reuse in GPU

以顶点着色器重复调用和几何带宽为切口,完整比较传统索引去重与 Post-Transform Cache 的两级硬件重用机制、NVIDIA Warp、AMD Primitive Subgroup、移动端 Tile 与 AMD DSBR 的边界;文章进一步讨论多视图重用、控制权从固定几何流程转向软件、Mesh Shader meshlet 的显式顶点重用,以及离线聚类、共享内存和属性压缩的工程职责。

2026-02-2033 分钟阅读
Evolution history of Vertex Reuse in GPU
AI 生成概念封面

几何管线里有个反复出现、却少有人专门说清的问题:控制权从硬件向软件迁移。顶点重用是观察这次迁移的切入点,直接决定顶点着色器的实际调用次数,也决定几何阶段的带宽效率。三角形网格里多个三角形共享相同顶点是常态,如果 GPU 每处理一个图元都把坐标变换、法线处理重新跑一遍,冗余计算会把顶点着色器调用次数推到理论下限的数倍。

这套机制的工作方式几十年来一直在变:早期硬件自动完成索引去重与变换缓存,到 Mesh Shader 范式下重担交还开发者,重用的成败变成数据结构的事。每一代方案的迭代,都围绕同一个核心变量:顶点重用行为的决定权归属于硬件还是软件。

一、顶点重用基石

在 Mesh Shader 出现之前,长达几十年的时间里,几何体都是靠 DrawIndexed、DrawIndexedInstanced 这类 API 提交给 GPU 的。这个"传统"时代里,顶点重用高度自动化,由硬件在幕后完成;开发者能做的只是调整索引数据的排列顺序,去引导硬件的取舍,无法直接控制重用的具体行为。理解这套自动系统后来为什么被替代,先得拆开两个核心组件:索引去重(Index Deduplication) 和后变换缓存(Post-Transform Cache,PTC)。

这两个名词容易混淆,有经验的开发者也会问:到底谁承担顶点重用的主要职责?两者处在数据流的不同阶段,职责不同,作用范围也不同,合起来才是一套完整的两级重用体系。meshoptimizer 这类工具里的 vertex cache 优化模式,会把共享顶点的三角形排到相邻位置,排好的那份索引,正是下面两个硬件环节的输入。

1.1 经典数据流:索引绘制的内部执行路径

一次 DrawIndexed 从 CPU 提交到 GPU 完成光栅化,内部大致走下面这条路,标出来的都是顶点重用会介入的环节:

[CPU]
   ↓
DrawIndexedInstanced()
   ↓
+-----------------------------[ GPU ]-----------------------------+
| 1. Index Fetch                                                  |
|    → 从Index Buffer加载索引序列                                 |
| 2. Geometry Engine                                              |
|    → Index Dedup(子组内去重)                                 |
|    → 得到唯一索引流                                             |
| 3. Vertex Shader调度前                                          |
|    → 查询PTC(Post-Transform Cache)                          |
|       ↳ 命中:复用缓存结果                                     |
|       ↳ 未命中:执行VS → 写入PTC                               |
| 4. Primitive Assembly                                           |
|    → 按原始索引还原图元拓扑                                    |
| 5. Rasterizer                                                  |
+----------------------------------------------------------------+

顶点重用发生在两个阶段,位置不同、分工不同:

  • 索引去重:在 VS 调度之前,识别同一子组内重复出现的索引,把唯一的索引流筛出来;
  • 后变换缓存:在每个顶点真正进入 VS 之前,拿索引去查缓存,命中就跳过这一次执行。

1.2 索引去重(Index Deduplication)

索引去重只做一件事:在当前处理批次里找出重复索引,避免对同一顶点重复调度。作用范围到此为止:一个 Primitive Subgroup 或一个 Warp,跨出这个批次,去重不再生效。

多数 GPU 在子组级别维护一个轻量哈希结构或位图表,逐个索引判断在本组里是否已经出现过:出现过就跳过,只有唯一索引才触发顶点加载。

这个机制有两个硬约束。第一,索引去重处理不了跨子组共享:多个子组引用同一个顶点,照样各调度一次。第二,去重效率完全取决于索引序列的局部性:共享同一批顶点的三角形排得相邻,就容易落进同一个子组,重用成功率高;排得远,重用失败,顶点被重复处理。传统管线里 meshoptimizer 这类工具专门重排索引、把重复引用聚拢,优化的对象就是这一步。

1.3 后变换缓存(Post-Transform Cache,PTC)

PTC 缓存的是顶点变换完成之后的结果,用来避免重复执行 Vertex Shader。和索引去重不同,PTC 的作用范围大得多,通常是滑动窗口的形式,可以覆盖跨子组、甚至跨 Draw Call 的重用。

执行 VS 前,硬件拿目标索引去 PTC 里查:命中,跳过 VS,直接用缓存;未命中,执行 VS,把结果写进 PTC。PTC 一般按小容量、高关联度设计,常见的规格是 8~32 个条目的全相联缓存,配 FIFO 或 LRU 滑窗策略,去捕捉访问顺序上的局部性。传统引擎里说的"vertex cache 友好"的索引布局,优化目标基本就是 PTC。

这里有个常见的误解需要澄清:名字叫"后变换缓存",查询动作却发生在 VS 执行之前。缓存里存的是"变换完的结果"而不是原始输入,所以一旦命中,整个 VS 的执行流程都可以跳过。

1.4 双机制协同关系

两个机制在一次索引绘制的提交里各守一段:

对比维度Index DedupPost-Transform Cache (PTC)
执行时机几何阶段,VS 调度前VS 执行前查询,执行后写入
重用粒度子组内部可跨子组/跨 DrawCall
成本与复杂度低成本,轻量判断高效缓存,需命中控制与驱逐策略
控制因素依赖索引局部性依赖访问顺序局部性与缓存容量
功能定位快速预过滤器核心重用机制,主导性能影响

分工是一前一后:索引去重是轻量前端,快速清掉批次内的冗余;PTC 承担全局复用,通过缓存结果避免重复着色。两个机制怎么接力,看一个例子就明白。顶点 A 在子组 1 内出现,Dedup 放行一次,VS 执行后结果进 PTC;同一个 A 稍后在子组 2 里又出现,Dedup 无法识别(Dedup 的作用范围不覆盖跨子组),但只要 PTC 里还留着 A 的变换结果,这次就直接命中,跳过重复计算。据此可以给出清晰界定:PTC 是顶点重用的基础设施,Dedup 是 PTC 前面的低成本局部加速器。

两个机制的边界条件并排比对,约束结构清晰。索引去重的作用范围被限制在一个 Warp 或一个 Primitive Subgroup 内部,PTC 的容量被限制在 8~32 个条目的滑窗上。这两个约束都不是设计目标,而是硅片面积与访问延迟的妥协:去重比较必须在当前调度周期内对组内索引完成,PTC 查询必须在 VS 发射前返回结果,条目数每增加一档,全相联比较器的面积与延迟就同步上涨。多数游戏的索引流在这个容量范围内可以获得足够的命中率,硬件也就没有动机继续扩大缓存。

这意味着传统管线的顶点重用建立在两组静态假设上:重复索引会落在同一个子组内,活跃顶点集不会超过滑窗容量。假设成立,重用自动发生;假设失效,Dedup 与 PTC 同时失守,同一顶点被反复调度、反复着色,而 API 层没有提供任何显式的干预手段。meshoptimizer 这类索引重排工具之所以成为传统管线的必备优化,原因正在于此:索引重排是开发者在隐式机制下唯一能调整的变量,而且调整的是数据,不是硬件。

这两级机制的存在,说明硬件曾经愿意为顶点重用投入晶体管;而两者在容量上的紧缩,说明这份投入有明确的预算上限。假设成立的前提是几何规模始终小于硬件窗口的容量,这个前提一旦不成立,整套体系没有任何退路,因为容量与去重逻辑从未对开发者开放。

二、架构实现对比

"索引去重 + 后变换缓存"这个基本模型,落到不同厂商、不同渲染架构里,执行粒度、缓存策略和批处理方式差异显著。各架构的共同优化方向是:提高 PTC 命中率,降低 Vertex Shader 的冗余调度,用最小的硬件成本换最大的重用效果。

2.1 Immediate Mode Rendering (IMR) 下的策略

主流桌面 GPU 一直是 Immediate Mode Rendering(IMR):绘制命令提交后立刻从头执行到尾,顶点处理完才轮到片元,没有移动端那种"先把几何分到屏幕块里、再分块渲染"的两遍结构。这个架构下,顶点重用能力主要受两件事影响:线程怎么调度、图元怎么分批。

2.1.1 NVIDIA 的 Warp 执行模型

NVIDIA 的并行调度单元是 Warp,每个 Warp 32 个线程。这个粒度决定了图元如何分批、顶点重用发生在哪个范围内。每个 Warp 处理一批图元,典型规格是 32 个三角形、合计 96 个索引;Warp 级别共享一份 PTC 缓冲;一批三角形里有共享顶点时,通过这份 PTC 直接复用;所以整个模型对索引排列的局部性高度敏感。

这个 96 索引批次是可以被计数器直接读出来的实测边界。Kerbl 等人在 HPG 2018 的《Revisiting The Vertex Cache》里做过一个干净的实验:向 GPU 提交一段不断重复 {0,1,2} 的索引流,理论上 Vertex Shader 调用次数应恒为 3,实测却呈现阶梯状爬升——NVIDIA GPU 上每推进 96 个索引(恰好 32 个三角形)就多出 3 次调用,说明重用只在当前批次内部成立,跨出批次边界的同一顶点会被重新调度。Interplay of Light 后来在 RTX 3090 上复现了同一实验,阶梯边界同样落在 32 三角形 / 96 索引处,与 Warp 的 32 线程一一对应。Warp 在这里同时充当两个角色:调度单位和重用边界。

边界既然固定在这,开发者能做的就只剩索引排列:把共享顶点的图元排进同一个 Warp 批次,重用才有机会发生。meshoptimizer 这类工具做的重排,就是把三角形尽量排入同一个 96 索引的批次之内。

2.1.2 AMD 的 Primitive Subgroup 模型

AMD RDNA 系列对应的几何批处理单元叫原语子组(Primitive Subgroup),职责与 NVIDIA 的 Warp 类似,但批次大小和硬件行为细节不同。子组内部先做一次 Index Dedup;VS 执行结果在组内可复用;PTC 负责子组之间的缓存。对开发者来说,可操作项依旧是索引布局:让共享顶点落入同一个子组。

AMD 白皮书给出了这一步的量化结果:索引布局优化得当,平均每个顶点的重复着色次数可以从 4 次降到 1.4 次。这个 1.4 到不了 1,因为子组边界仍然存在,后面会拿这个数字说明问题。

2.1.3 Intel 早期架构的重用瓶颈

早期 Intel 集成 GPU(比如 HD4000 那一代)在顶点重用上效率很低。文献里能找到的描述是:几何引擎只有 3 个顶点的局部缓存,每处理一个三角形就重复调度三个顶点,没有有效的跨图元重用逻辑,优化主要依赖 CPU 侧处理。结果是顶点处理吞吐明显低于同期 AMD/NVIDIA 的独显,顶点重用机制的缺失成为吞吐的第一瓶颈。

2.2 Tile-Based Rendering (TBR) 的限制与权衡

移动 GPU(Adreno、Mali、PowerVR)普遍走 Tile-Based Rendering,目标是用空间局部性压住内存访问,降低带宽和功耗。

2.2.1 分箱与 Tile 范围限制

TBR 的基本流程是三段:屏幕空间划成固定大小的 Tile;所有图元先做 Binning,生成"图元—Tile"映射;然后每个 Tile 在自己的片区内跑完整的几何与光栅处理。这套流程的直接后果是:顶点处理被限制在单个 Tile 内部。一个顶点横跨多个 Tile,就得被每个 Tile 各加载、各处理一遍;PTC 与 Dedup 都只在 Tile 范围内生效,跨 Tile 的顶点不存在重用。

这套两级机制在移动阵营的厂商文档里有更细的官方描述。ARM 的 Mali Performance Counters Reference Guide 在描述 Tiling 管线时写明:管线使用 post-transform vertex cache 保存最近着色顶点的位置结果,当索引重用的时间局部性不足时,顶点会在被复用之前先被驱逐出缓存,从而被重复着色;着色请求则以 4 个连续索引为一组提交。同一份文档给出的效率判据是:重用良好的网格,平均每三角形着色的顶点数应低于 1.5。三条信息合在一起,就是移动端"组内去重 + 滑窗缓存"的官方描述:去重粒度只有 4 个索引,缓存以最近访问为界,重用依然完全由索引流的局部性驱动。

2.2.2 硬件未普及的跨 Tile 重用尝试

跨 Tile 的重用缺失不是没人看到。Imagination 在白皮书里提过 Tile Reuse Cache 的概念,想在各 Tile 之间共享顶点变换结果;但到目前为止没有见到商用 GPU 实现,受限于一致性维护成本和硬件面积开销,方案被搁置。

2.2.3 混合探索:AMD 的 DSBR

桌面阵营里 AMD 在 Vega 上做过一次接近 TBR 的尝试:Draw Stream Binning Rasterizer(DSBR),在渲染早期按屏幕位置对图元做粗粒度分发。每个 Bin 独立跑后续几何流程,换来更好的负载分布和缓存局部性,代价是顶点重用范围同样被 Bin 边界切开。在遮挡复杂、图元密集的场景里,DSBR 能省下不少带宽和片元处理压力,但跨 Bin 的冗余顶点处理是固有开销,逻辑上与 TBR 的跨 Tile 冗余属于同一类问题。

2.3 传统架构的重用边界

综合以上各家方案,这一阶段的顶点重用可以用一句话概括:开发者只能通过数据布局去引导重用,控制权始终在硬件手里。这套机制还能成立多久,取决于一个物理条件:场景的几何规模是否始终能被硬件为重用预留的固定容量覆盖。

架构类型重用机制范围控制手段重用局限
IMR (NVIDIA)Warp 内部 Dedup + PTC索引排序控制重用范围需依赖 Warp 边界配合
IMR (AMD)Primitive Subgroup + PTC索引聚簇控制分组子组外重用靠 PTC
TBR/TBDRTile 本地 Dedup + PTCTile 切分不可控无法跨 Tile 重用
早期 Intel几乎无重用依赖 CPU 优化每图元重复处理三顶点

这张表的四行可以合并成一个判断:每一种传统架构都把重用边界固定在一个硬件结构上。NVIDIA 固定在 Warp 边界,AMD 固定在 Primitive Subgroup 边界,移动 GPU 固定在 Tile 边界,早期 Intel 的局部缓存只有 3 个顶点的容量,近乎无边界可言。边界的形状不同,约束相同:由晶体管预算决定的常量,不随场景规模伸缩。开发者可以用索引重排适配边界,但无法移动边界本身。跨 Tile 与跨 Bin 的重用缺失,与子组边界的重用中断属于同一类容量约束。

这个常量在小几何时代无关紧要。传统游戏场景的顶点工作集,用几十条容量的滑窗配合索引重排,就能维持可接受的命中率。2.1.2 节引过的 AMD 白皮书数据已经暗示了上限的存在:索引布局优化只能把平均重复着色次数从 4 次降到 1.4 次,无法降到 1 次,因为子组边界是索引重排无法消除的。问题真正暴露是在集群几何(Cluster Geometry)这个量级:以 Nanite 为代表的高密度几何路径把单场景可见三角形推入千万级乃至亿级,活跃顶点工作集远超任何固定滑窗的容量。PTC 的驱逐策略在这种访问模式下退化为近似随机替换,命中率随滑窗覆盖率同步下跌直至失效(Occupancy Cliff)。滑窗的覆盖能力等于容量除以工作集,工作集扩大三个数量级,命中率不是下降三个数量级,而是直接归零:新写入的条目在复用发生之前就被驱逐出缓存。隐式重用在此时不是变慢,而是失效:Dedup 依旧只在子组内生效,PTC 依旧只有几十条容量,两者对跨子组、跨集群的重复顶点完全无力处理。

NVIDIA Turing/Ampere 的 Task Shader / Mesh Shader 单元,与 AMD RDNA 的 Primitive Shader / NGG 几何路径,表面上是两套并行的可编程设计,放到微架构层面看指向同一个事实:两家厂商在同一时间窗口认定固定功能前端的几何调度能力已经到达物理上限。硬件的应对不是把去重电路做得更大,而是把去重从职责清单里移除。隐式去重电路的收益正比于命中率,命中率正比于局部性,而局部性在亿级三角形规模下已经不存在,继续保留这套电路,等于为不会发生的事件支付面积与功耗。片上共享存储(Shared Memory / LDS)被直接暴露给线程组;索引去重、顶点缓存、剔除判断全部移交软件,由开发者在着色器中显式编写,包括像素级精准剔除(Shader Culling)在内的几何调度逻辑。AMD 在 RDNA3 架构说明里明确指出:硬件不再执行任何重用逻辑。

权责转移的代价是直接的。顶点重用从硬件保证降级为数据结构的约定:meshlet 布局正确的渲染器得到可预测的性能,布局错误的渲染器连传统管线的下限都守不住,因为硬件不再执行任何默认重用。第五章给出软件侧的具体方案,即 meshlet 的唯一索引列表加 groupshared memory,恢复的重用效率与 PTC 相当,但确定性与可预测性来自数据结构,不再来自硬件的推测行为。代价的另一面是 API 语义的变化。当索引解析、顶点调度、图元组装全部由线程组内的代码完成,传统 Draw 调用所承诺的固定功能行为已经没有对应的硬件实体。Retained-mode PSO 中围绕固定管线阶段组织的那些状态块,失去的是物理依据,而不只是易用性。Draw 正在退化为 Dispatch 的一个特例。

三条证据指向同一个结论:子组级去重的范围限制、滑窗式 PTC 的容量限制、跨 Tile 重用的结构性缺失,表明几何管线的控制权移交软件,不是 API 设计者的偏好,而是固定功能硬件到达物理边界后的唯一出口。后续章节对宏观调度与 Mesh Shader 的展开,都建立在这个判断之上。

三、宏观流程重用

前面分析的 Index Dedup 与 PTC,解决的都是单次 Draw Call 内部的局部重算,作用范围停在微观层级。多视图渲染场景下还有另一类冗余超出两者的处理范围:VR 左右眼、立方体贴图、级联阴影这类任务会把同一份顶点数据放在多套投影矩阵下重复处理。这种"流程级"的重复是结构性的,再大的缓存也消不掉,只能靠跨视图的统一调度来消除。

3.1 重复路径的典型场景

这类场景的共同点:几何输入完全一致,只有视角矩阵不同。立体渲染(VR 左右眼)数据一致,仅视角矩阵不同;Cubemap 生成(点光源阴影或反射)要把同一场景往 6 个面方向各画一遍;Cascade Shadow Map 在光源方向不变的前提下,按多级深度区间做分级渲染;Voxelization 投影 6 个方向,但在正交关系下只需处理与输出对齐的 3 个方向。

共同的瓶颈有三处。一是 CPU 调用压力:每个视图单独发 Draw Call,状态切换、资源绑定、调度指令生成跟着成倍增加,高频多视图任务里这常常是主导瓶颈。二是几何处理重复:顶点提取和 VS 逻辑反复执行,其中相当一部分计算与视图无关。三是 PTC 覆盖失效:PTC 名义上能跨 Draw Call 复用,但不同视图给同一顶点引入的位置差异会降低命中率,维持不住重用效果。三条加起来,局部优化无法弥补,必须引入跨视图的统一调度与复用机制。

3.2 单次调用的多视图执行演进

把视图分裂这个动作往流水线前端挪,硬件先后换过三种做法:先是把多视图逻辑放进 GS,然后用专用硬件做旁路加速,最后把分裂点放到 VS 之前的输入装配端。约束是两重的:管线顺序不能乱,硬件开销要可控,共享输入、最小化重复变换都得在这两条约束之内实现。

3.2.1 几何着色器(GS)的早期尝试与固有限制

最早用 GS 处理多视图是很自然的选择:GS 本来就是逐图元执行的,开发者只需在 GS 里对每个图元跑一个 for 循环,手动把三角形顶点投到多个视角,写出不同的 Viewport Index,再对每个视图 Append 输出、用 RestartStrip 分隔图元。这条路径不需要多个 Draw Call,逻辑上多视图就合并进一次提交了。具体分工是:VS 只做模型变换、输出到共享空间,各级 VP 矩阵的投影全部挪到 GS 里逐个完成,用 ViewportIndex 决定后续图元送进哪个视口。

实际执行的代价随即暴露。GS 的启动依赖图元装配,必须等三个顶点都完成 VS 才能调度,流水线在这里被阻塞;VS 到 GS 之间的数据要写进片上中转缓冲,数量一大还会引入显式 Load/Store;GS 本身是图元级线程,不利于大规模并行,图元放大倍率越高,资源调度越不均衡。NVIDIA 在 GDC 2015 的 Maxwell 架构分析里给出了明确结论:即便是无变形的 Pass-through GS,最优实现下开销也相当可观,实测性能劣于多个 Draw Call 串行提交。GS 方案可行,但距离理想路径差距明显。

3.2.2 Multi-Projection Acceleration (MPA):几何阶段的硬件旁路

Maxwell 把问题收窄了一点:最常见的多视图模式是拓扑不变的,输入一个图元,输出 N 个同拓扑的图元。针对这一种模式,NVIDIA 在架构里加了 Multi-Projection Acceleration (MPA),由两部分组成:Fast Geometry Shader 是一条轻量级的图元复制旁路,只支持拓扑结构恒定;Viewport Multicast 把复制动作下沉到硬件,用掩码控制广播到哪些视口。

工作路径如下:开发者仍然写 GS,但逻辑必须结构恒定、不改图元拓扑;在 GS 里设置 ViewportMask(32-bit 位掩码)声明这个图元要广播到哪组视口;硬件识别掩码后在 Fast GS 单元里生成多个图元实例,每个实例由硬件注入不同的 ViewportIndex 作为执行上下文,分别送入各自的光栅化路径。驱动会在编译阶段做静态分析,判定这段 GS 是否满足旁路条件,满足就绕开通用 GS 路径,走专用 MPA 单元。

收益对应着 GS 的三笔开销:GS 阶段的调度阻塞消除,GS 输入/输出缓冲的 Load-Store 跳过,多视图复制可以并行完成。Cubemap、CSM、Voxelization 这些高频路径是 MPA 的主要受益场景。限制也同样明确:只接受拓扑保持的 GS,复制个数必须静态可确定,变形放大、动态分支、按实例差异化的数据路径均不支持。

这里有一个结构问题值得注意:MPA 的硬件放在 GS 之后,复制与投影仍然发生在 GS 阶段,分裂点没有真正前移,顶点重用向前融合的可能仍然被阻断。

3.2.3 View Instancing / Vertex Amplification:前端分裂与顶点实例化

彻底摆脱 GS 的做法,是把分裂动作搬到图元装配之前。这条路有两个模型。Multi-View Rendering(MVR)(Turing 架构 + NvAPI)让 VS 输出一个包含多个 position[i] 的结构体,每项对应一个视图;GPU 前端把 VS 输出做宽度展开,每个输出送进独立的装配器路径,各视图路径从装配开始相互隔离。View Instancing(D3D12)/ Vertex Amplification(Metal)则让 VS 只处理单个视图:GPU 前端调度器按 ViewInstanceCount 把每个图元输入分发多次,每次执行携带独立的 ViewID(如 SV_ViewID、Metal 的 [[amplification_id]]),VS 靠 ViewID 加载对应的 VP 矩阵,后续流水线完全一致,只有变换逻辑随视图分化。

View Instancing 相对 MVR 的"宽输出",优势在结构上更干净。View Instancing 不增大 VS 输出尺寸,VS 逻辑结构稳定、便于统一优化;不依赖某个厂商的扩展,能同时兼容 Tile-Based 与 Immediate Mode 架构。硬件方面,图元输入在 IA 阶段只 Fetch 一次,但会被调度成多个独立执行项,每项带不同 ViewID,前端 Fetch 压力被削减,多个视图还能共享同一份几何输入缓存结果,特定条件下 PTC 依然可以命中。

对比项GS 多视图MPAMVRView Instancing / VA
分裂位置GS 阶段GS 旁路VS 输出后VS 输入/调度器
视图注入方式for-loop 手动变换ViewportMask 广播position[i] 并列输出ViewID 驱动变换
顶点重用潜力无法共享 VS仅复用 VS 输入有限共享高
编程模型明确但复杂约束较多结构笨重简洁一致
硬件支持度通用Maxwell+Turing(NvAPI)D3D12 SM6.1+ / Metal

四种方案并排比较,演进方向清晰:GS 在图元装配之后复制,MPA 在旁路里广播,View Instancing 在 VS 调度之前完成分裂。分裂点每前移一级,共享输入的范围就扩大一级,后续几何阶段的任务合并、调度优化、负载融合都依赖于这一步的位置。

3.3 TBR 架构下的 Vertex Amplification:几何阶段的深度融合

移动 GPU 的执行模型不同于桌面。TBR 的内存访问模式和执行顺序和 IMR 根本不同,桌面的 View Instancing 没法直接照搬,Vertex Amplification(VA)必须嵌进 TBR 几何处理路径的前端,跟分块机制协同工作。

3.3.1 TBR 两阶段模型与几何处理时序

TBR 把渲染切成两个明确阶段。几何阶段(Binning / Setup):所有 Draw Call 按顺序解析,GPU 执行顶点着色与剔除,但不启动光栅或像素着色,核心任务是判断每个图元覆盖哪些 Tile,把"图元—Tile"关系记进分桶表。渲染阶段(Tile Rendering):GPU 以 Tile 为单位调度,把该 Tile 的图元和资源加载进片上内存,执行光栅化与像素着色,完成后把颜色写回主存。在这个模型里,顶点重用逻辑、视图分裂、图元装配全部集中在几何阶段完成,Tile 分发结果决定后面像素处理的范围。

3.3.2 VA 嵌入几何路径的执行流程

Metal 的 Vertex Amplification 基于"实例化输入"范式,恰好贴合 TBR 的调度特征。硬件行为如下:CPU 提交 Draw Call 并设置 amplificationCount 为 N;GPU 前端调度器从 IA 提取每个图元,执行一次输入装配;随后把这个图元复制成 N 个独立视图实例,每个实例绑定唯一的 amplification_id;每个实例独立进入 VS,加载各自视图的 VP 矩阵完成投影;所有视图实例在几何阶段结束前完成坐标变换与剔除判断;Tiling 硬件读取每个图元在各视图下的投影结果,计算 Tile 覆盖范围并写入对应分桶表。最终每个 Tile 的图元列表里可能同时包含多个视图的投影图元,渲染阶段按 Tile 统一处理。

结构上有三处收益:输入装配只做一次,输入带宽节省;分裂发生在顶点阶段,剔除和变换有提前完成的余地;多个视图的图元汇入同一条 Tile 分发路径,调度和带宽都更紧凑。

3.3.3 与 IMR 中 VI 路径的比较特征

对比维度IMR 架构中的 View InstancingTBR 架构中的 Vertex Amplification
分裂时机VS 执行前IA 后,VS 调度前
图元调度路径按 Draw Call 顺序传递到图元装配分裂后直接进入 Tile 分发器
后端光栅路径视图间完全隔离多视图图元合并入同一瓦块渲染队列
内存模型各视图独立绑定 RT 与 DSV多视图 RT 通过 RenderPass attachment 管理
重用粒度图元输入 Fetch 可复用,PTC 命中概率高Tile 分发可共享中间数据,适配片上内存结构
适用目标桌面多摄像头渲染、VR、CSM 等移动端多视图场景(如 VR、反射探针)

TBR 对存取顺序和数据驻留敏感,VA 把视图变换统一在几何阶段完成、图元直接分发到各自 Tile,省掉了中间状态管理和延迟调度,等效于把原本要多个 Draw Call 分开执行的视图路径压缩成一个统一的几何分发过程。这个模型适合视图数少(2~6 个)但更新频繁的场景:Mobile VR 的左右眼、点光源 Cubemap 阴影的 6 视图、Probe baking 与反射缓存更新、监控系统的多摄像头同步渲染。VA 嵌在 TBR 的几何阶段里,不引入多余的调度或状态切换,多视图几何路径的共享能力是结构内生的。

3.4 多视图重用在重用体系里的位置

回到全文的分析框架:Dedup 与 PTC 优化的是"点对点"的执行冗余,一个顶点被重复调度、重复变换;多视图的问题不一样,每个视图作为独立调用进流水线,整条几何路径被重复执行,这种冗余超出了微观缓存机制能处理的范围。View Instancing / VA 给出的解法是让几何在逻辑上合并、在执行上分化:一次 Draw Call 驱动多视图投影路径,顶点数据只 Fetch 一次,视图无关的 VS 计算统一执行,视图间的重用是显式行为,不依赖 PTC 命中。绕开缓存局部性限制的办法,是直接从源头去掉重复路径,而不是在缓存层优化。

这条路径同时重排了 CPU 和 GPU 的分工。传统做法每个视图一次 Draw Call,命令编码和状态切换成倍重复,提交顺序和并发性依赖驱动策略,多视图负载容易被 CPU 调度能力限制。改成 View Instancing 后,多视图的生成整体下沉到 GPU 调度器:所有视图共享一个 Draw Call 上下文,前端按视图数实例化执行,同一图元输入被多次调度,VS 只在视图差异处分化行为。流程控制转移到 GPU 本地后,剔除、LOD 选择这类中间阶段还能继续共享统一上下文。

所以现代 GPU 里的顶点重用实际上是三套粒度叠加使用:子组级的 Index Dedup 消除重复调度,跨子组的 PTC 滑窗消除重复变换,视图级的 View Instancing / VA 合并路径结构。三者不互相替代:宏观分裂产生多个 VS 实例后,局部仍可以靠 PTC 消除余下的冗余。在 Mesh Shader 方向上,DispatchMesh 启动本身就不再受图元输入结构限制,天然是宏观调度行为;Task Shader 做前向剔除和 meshlet 选择时,视图相关性可以融进剔除逻辑,实现进一步的路径级合并。至于多视图直接通过 Task Shader 分支决策、在 meshlet 的线程组里一次生成多视图输出、顶点加载后共享给多个投影,这些还只是沿着这条结构推导的方向,目前没有公开的硬件实现能对号入座。

四、控制权的转移

转移的位置在硬件层面可以直接读出来。AMD 在 GPUOpen 的《From vertex shader to mesh shader》里写到,Mesh Shader 线程组经由 Geometry Engine 启动时走一条专门的 fast launch 通路,直接绕过 vertex reuse checking 与 primitive subgroup 组建这两级传统前端逻辑;文档同时指出,绕过 Input Assembler 的代价是顶点重用必须前移到离线预处理中完成,否则每一帧、每一次绘制都要把重用信息重算一遍。传统管线里 Dedup 与 PTC 自动承担的工作,在 Mesh Shader 路径上对应的电路根本不在启动路径上。

这套结构与 compute shader 的执行模型类似:Mesh Shader 路径不再承诺"同一个顶点不会算第二遍",顶点重用成为开发者的显式责任。传统 Draw 路径中由硬件隐式管理的环节,从 Mesh Shader 这一代开始逐步移出固定功能单元。

4.1 固定几何流程的结构性瓶颈

传统几何管线有三项结构性约束。一是顶点重用依赖硬件内部的 Index Dedup 与 PTC,行为不可预测,优化了半天拿到的帧数还随驱动和批次波动,调试成本高。二是输入数据必须满足固定格式(顶点缓冲加索引缓冲),程序化生成、压缩存储、复杂解构这些需求无法适配。三是每个物体、每个 LOD、每个实例都得由 CPU 单独发起一次绘制,几何粒度切得越细,CPU 线程调度与驱动开销越大。三项约束叠加的结果是:程序化几何生成缺乏灵活性,LOD 切换受制于 CPU 的调度频次,大规模剔除无法在 GPU 端闭环完成。几何处理要继续演化,这条固定流程本身就是障碍。

4.2 并行计算范式重构几何管线

Mesh Shader 的替代方案是把几何处理整个变成可编程的计算路径,用两级结构:Task Shader(可选) 做粗粒度剔除与工作分发,每个 Task Shader 线程可以决定启动一个或多个 Mesh Shader 工作组;Mesh Shader(必选) 取代传统的 Input Assembler、Vertex Shader 与 Primitive Assembly,显式生成一批顶点和图元。执行流程是数据主导的:顶点与索引由开发者自行生成或读取;图元数量通过 SetMeshOutputCounts() 主动申明;所有线程共享 Groupshared Memory,本地重用在这里发生。Mesh Shader 不再假定输入结构,也不承担任何索引去重职责。

4.3 顶点重用机制的控制权迁移

对照传统路径,控制权转移的边界清晰。DrawIndexed() 进来后,GPU 自动解析索引流、执行 Dedup、调度 VS、缓存结果,每一步都不需要开发者参与;DispatchMesh() 则不传递任何具体索引或顶点缓冲,只给出执行线程组的数量。这条路径上不存在索引去重阶段,不触发后变换缓存,顶点是否被重用完全取决于 Mesh Shader 线程内部的逻辑。如果某个工作组把同一顶点数据重复读进来处理,系统不会阻止,顶点会被重复计算。硬件放弃自动推测,换来的是三样东西:不必再处理缓存失效与重入判断的复杂性,以更低的系统成本拿到完全程序化的控制能力,以及压缩数据、计算生成几何、精细剔除这些传统输入格式无法支撑的需求。AMD 在 RDNA3 架构说明里明确指出:Mesh Shader 为开发者提供完全的几何生成控制能力,硬件不再执行任何重用逻辑。

4.4 显式重用范式的设计前提

控制权既然交到开发者手里,显式重用就从可选项变成了性能的刚性前提:缺少显式重用,同一顶点会在每个引用它的图元里被重复变换,着色器调用次数回退到没有缓存的水平。这正好接上第二章结尾的推论:重用效率与确定性的来源,从硬件的推测行为转移到离线数据结构上,第五章展开的 meshlet 唯一顶点列表、局部索引与 groupshared memory 执行模型,就是这套数据结构的具体实现。

五、显式顶点重用

第四章的结论是:硬件不再做默认重用后,重用保证必须写进数据。软件侧的标准方案是 meshlet 结构加 groupshared memory。

5.1 meshlet 数据结构:重用控制的基本单位

一个 meshlet 定义为可由单个 Mesh Shader 线程组独立处理的小型网格块,里面只装两种列表:唯一顶点索引列表记录这个 meshlet 用到的所有全局顶点索引,每个值唯一;局部图元索引列表记录 meshlet 内三角形的连接关系,索引值是指向唯一列表的局部偏移。看一组数字就清楚了。传统索引表示法里,三角形 A 是 [4, 5, 6]、三角形 B 是 [8, 4, 6],Index Buffer 展开是 [4, 5, 6, 8, 4, 6];转成 meshlet 后,唯一顶点列表是 [4, 5, 6, 8],局部图元索引是 [0, 1, 2, 3, 0, 2]。

NVIDIA 官方博客《Introduction to Turing Mesh Shaders》在讲解 meshlet 构建时给出了同一个例子:{4,5,6, 8,4,6} 的原始索引流被拆成 {4,5,6,8} 的唯一顶点表与 {0,1,2,3,0,2} 的局部索引,NVIDIA 把这一步直接称为 vertex de-duplication。AMD GPUOpen 在描述 NGG 时用了同一个类比:primitive subgroup 概念上就是硬件在运行时临时构建的一个小 meshlet。两家厂商的文档指向同一件事:meshlet 结构把硬件曾经每次绘制时临时构建的唯一顶点表加局部连接,固化为离线数据移交开发者持有。

这套结构的优势体现在数据层面:顶点索引唯一,冗余从源头消失;图元组装只读共享内存,不需要重复读取或计算;数据组织方式天然支持 GPU 局部重用。执行路径是固定的五步:线程组加载 meshlet 数据;所有线程协作读取唯一顶点数据、写入 groupshared memory;只对唯一顶点执行一次顶点变换;图元组装从共享内存读变换结果;组装结果输出。顶点变换的次数由第一步读进来的数据决定,不由硬件推测。

5.2 meshlet 工作流:从离线构建到运行时执行

5.2.1 离线阶段:网格聚类(Mesh Clustering)

meshlet 不是从三角形集合中自动生成的。离线阶段用 meshoptimizer 这类工具把传统网格切成块,每个 meshlet 要满足厂商推荐的上限(典型是 ≤64 顶点、≤126 三角形),优化目标围绕局部性、重用密度与剔除效率来权衡。NVIDIA 自 Turing 代起把 64 顶点 / 126 三角形定为推荐上限,84 三角形是同样受推荐的折中粒度,3×84+4 字节恰好填满两个 128 字节的分配块;官方示例的 Mesh Shader 线程组是 local_size_x=32(单 Warp),64 个顶点需要每线程处理 2 个,84 个三角形与 32 线程并没有一一对应关系,顶点与图元是按组内分工写入输出,而不是绑死到固定线程。

这组数字的原始出处是 NVIDIA《Introduction to Turing Mesh Shaders》官方博客,原文对 126 的解释同样具体:第一代硬件按 128 字节粒度分配图元索引输出空间,并需为图元计数预留 4 字节,3×126+4=382 字节,在 3×128=384 字节的分配块里实现了最大化利用,写到第 127 个三角形就会触发下一个 128 字节块的分配;84 与 40 是原文列出的另外两个对齐良好的粒度。推荐上限因此是输出缓冲分配粒度反推出来的算术结果。

5.2.2 运行时阶段:Mesh Shader 执行路径

运行时的契约很薄:渲染器不再绑定顶点缓冲和索引缓冲,而是绑定 meshlet 数组;DispatchMesh(N, 1, 1) 启动 N 个 Mesh Shader 线程组,每组处理一个 meshlet,把加载、变换、组装、输出走完。启用 Task Shader 后,剔除逻辑搬进 GPU 内部,不可见的 meshlet 在到达 Mesh Shader 之前就被滤掉,判断依据是包围盒、遮挡信息或屏幕空间尺寸。调度语义也换了:传统 DrawIndexed 路径以"物体"为调度单位、由 CPU 主导发起绘制;Mesh Shader 路径围绕"任务"构建提交,一次提交从剔除开始,覆盖一批 meshlet 从读到算到写的完整路径。

到这里,引擎侧的配套结构基本由这份接口契约驱动:离线构建器批量执行网格聚类、压缩与包围体计算,产出优化后的 meshlet 列表;GPU 驱动的剔除系统用 Task Shader 把不可见 meshlet 筛掉,输出可见列表;统一 Dispatch 把剔除结果直接作为 DispatchMesh 的输入,一次提交启动覆盖所有可见 meshlet 的线程组。CPU 调用次数与状态切换被压下去,几何处理更接近"GPU 完全驱动"。

5.3 重用机制的内聚性与工程职责

显式模型下,顶点重用成为一种数据驱动的工程策略,硬件不再提供保障。meshlet 的唯一索引列表保证重用语义,线程组内部的共享内存消除重复开销,剩下的责任全部落在数据生成环节:开发者必须保证 meshlet 结构正确、索引去重完全。假如直接把传统索引传进 Mesh Shader 而不做去重和聚类,每个图元都会重复读取并变换顶点,着色器调用次数立刻回退到逐图元全量执行的水平。第二章那个"布局错误就连传统管线下限都守不住"的警告,指的就是这种情况。

5.3.1 附加优势

meshlet 结构带来的不止是重用。局部索引可用相对地址表达,压缩索引数据体积,典型压缩比约 25%;顶点与图元的组织方式也让 meshlet 能更顺畅地配合剔除、LOD 与流送策略。25% 这个数字出处:NVIDIA 在同一篇博客里报告,meshlet 化之后的三段缓冲(唯一顶点索引、局部图元索引、meshlet 描述符)合计体积通常降至原索引缓冲的 75% 左右。收益来自两处:唯一索引表消除了 meshlet 内部的重复索引项;局部索引的值域受 64 顶点上限约束,可以用 8-bit 存储,NVIDIA 示例代码里局部索引缓冲的类型正是 uint8_t。

5.3.2 顶点属性怎么压、怎么进共享内存

索引之外,顶点属性同样受 meshlet 的空间局部性影响。网格被切成小块后,块内顶点的坐标相对集中,可以量化:局部坐标用 16-bit 整数存,法线与切线用 GL_INT_2_10_10_10_REV 这类紧致格式,属性打包成结构化数组,对 GPU 的聚合读取与缓存行命中更友好。共享内存一侧同样有硬约束:meshlet 尺寸必须落在线程组共享内存容量内,组内线程数、SM 本地存储能力与数据打包尺寸需要放在一起权衡;保持厂商建议的 64 顶点以内的规格,才能实现"一次加载、广播使用"的运行模式。

结语

顶点重用这条线索梳理到尾,真正的变化是调用语义:渲染指令从"请求"变成"控制"。请求式的 Draw 把几何输入交给一条固定功能管线,硬件自己决定去重发生在哪、缓存命中多少、顶点算几遍,API 承诺的是结果,不是执行细节;Mesh Shader 路径上的 Dispatch 只给出线程组数量,索引从哪读、变换做几遍、谁去重、哪些图元被剔除,全部由组内代码和数据结构决定。拿同一份共享密集的索引网格走这两条路,差异清晰可见:Draw 路径的调用次数与缓存行为由子组切分和 PTC 容量决定,代码里看不到,要靠计数器反复拼才能得出轮廓;Mesh Shader 路径把这些展开在 meshlet 的唯一列表和局部索引上,一个线程组算几个顶点、写几条输出,读一遍数据就能确定。前者的确定性依赖硬件内部状态,后者的确定性依赖数据结构。

特性传统管线(Implicit Era)Mesh Shader 管线(Explicit Era)
控制模式硬件自动化开发者显式控制
重用机制Index Dedup + Post-Transform Cachemeshlet 数据结构(唯一索引 + 缓存)
输入路径顶点缓冲 + 索引缓冲自定义缓冲 + DispatchMesh
优化策略索引重排、Cache 优化数据结构组织、共享内存访问
可预测性中低,依赖硬件内部状态高,由开发者设计与着色器逻辑决定

演进不会停在 Mesh Shader。已经在落地的方向中有三个沿着控制权迁移的趋势延伸:DirectX 12 的 Work Graphs 把 LOD 选择、几何剔除、Mesh Shader 与光追更新组合成 GPU 端驱动的 DAG,节点以 Dispatch 为单位,所有中间数据保留在 GPU 本地,帧内阶段间的数据流通不再绕 CPU;Mesh Shader 可以直接构建或动态更新 BLAS、生成代理几何、把剔除粒度掌握在着色器手里,光栅路径与光追在几何表示上的耦合在加深;Nanite 的 meshlet 数据库加剔除与按需流送,示范了实时场景下高密度几何管理能走到哪一步,打破的是传统的三角形面数预算和 Draw Call 数量约束,但能否成为主流引擎的默认几何组织,取决于工具链和美术工作流的跟进程度,现在还不能当成既定结论。AI 参与 meshlet 聚类、LOD 构建与剔除预测这类设想,目前没有公开的证据支撑,这里不做展开。

顶点重用的演进到这里可以收束:硬件让出职责,数据结构承接职责。控制权的下一站是调度本身,Work Graphs 已经在往那个方向推进1。


参考文献

  1. 羲和啦. GPU体系结构视频集. bilibili. https://space.bilibili.com/250050043?spm_id_from=333.1391.0.0
  2. Khronos. Post Transform Cache. OpenGL Wiki. https://www.khronos.org/opengl/wiki/Post_Transform_Cache
  3. Interplay of Light. Shaded vertex reuse on modern GPUs. Interplay of Light, 2021. https://interplayoflight.wordpress.com/2021/11/14/shaded-vertex-reuse-on-modern-gpus
  4. Kerbl et al. Revisiting The Vertex Cache: Understanding and Optimizing Vertex Processing on the modern GPU. HPG, 2018. https://arbook.icg.tugraz.at/schmalstieg/Schmalstieg_351.pdf
  5. NVIDIA. New GPU Features of NVIDIA Maxwell Architecture. GDC, 2015. https://developer.download.nvidia.com/assets/events/GDC15/GEFORCE/Maxwell_Archictecture_GDC15.pdf
  6. Microsoft. DirectX-Specs | Engineering specs for DirectX features. DirectX-Specs. https://microsoft.github.io/DirectX-Specs/d3d/ViewInstancing.html
  7. NVIDIA. Turing Multi-View Rendering in VRWorks. NVIDIA Technical Blog. https://developer.nvidia.com/blog/turing-multi-view-rendering-vrworks
  8. Apple. Improving Rendering Performance with Vertex Amplification. Apple Developer Documentation. https://developer.apple.com/documentation/metal/improving-rendering-performance-with-vertex-amplification
  9. Interplay of Light. Meshlets and Mesh Shaders. Interplay of Light, 2025. https://interplayoflight.wordpress.com/2025/05/05/meshlets-and-mesh-shaders
  10. NVIDIA. Introduction to Turing Mesh Shaders. NVIDIA Technical Blog. https://developer.nvidia.com/blog/introduction-turing-mesh-shaders
  11. AMD. Mesh Shaders in AMD RDNA 3 Architecture. GDC, 2024. https://gpuopen.com/gdc-presentations/2024/GDC2024_Mesh_Shaders_in_AMD_RDNA_3_Architecture.pdf
  12. AMD. From vertex shader to mesh shader. GPUOpen. https://gpuopen.com/learn/mesh_shaders/mesh_shaders-from_vertex_shader_to_mesh_shader
  13. Arm. Mali-G615 Performance Counters Reference Guide. Arm Developer Documentation. https://documentation-service.arm.com/static/656f34d978913d7bfdc18281

Footnotes

  1. 传统 Draw 语义与 Retained-mode PSO 的物理依据如何逐条消失,将在系列终章中展开论证。 ↩