GPU 架构剖析 · 第 九 篇

几何前端:几何着色器、流输出与细分

2026-09-1912,007 字16 张配图

核心问题:图元装配(primitive assembly)把顶点连成三角形之后、光栅化把三角形倒成像素之前,GPU 上还有一段能改变图元命运的"几何前端"——按图元改/生成图元(几何着色器,geometry shader)、把顶点旁路写回内存(流输出,stream-out)、把低位图元细分到更细(细分曲面,tessellation)。本篇把这三件事当作同一条几何前端来读:它们都在光栅化之前运行,都以图元/补片(patch)为单位而非以像素为单位,而且都付着一笔"改变图元形态"的量价账——二次装配、不可预知的输出规模、成倍增长的顶点流。全篇收在"GS 的能力常被计算着色器与 mesh shader 取代"这句对几何前端的告别上。

本文由@溯光 出品,转载请注明出处。本文同步发布于公众号与个人博客:技术博客页。

题图
题图渲染管线全景:Hull shader → Tessellation → Domain shader → Geometry shader 正是本篇的"几何前端"——在光栅化之前,按图元改/生成图元、把顶点旁路写回内存、把 patch 细分到更细@ Wikimedia Commons · Martin Wantke / AntiCompositeNumber(CC0)

一句话点题:光栅化之前还有一段"几何前端"(set-context)

好了,上一系列我们把三角形一路送进了屏幕:顶点着色器把顶点搬到裁剪空间,图元装配把它们连成三角形,光栅化 setup 算好边函数,光栅化器把覆盖的像素逐一产生出来。故事讲到这儿,似乎三角形一旦连好,就该直接倒成像素了。但管线在装配与光栅化之间,还夹着一小段很多人听过名字、没认真读过的地带——按图元工作的可编程段与固定功能段,它们能在光栅化之前改写图元本身。

你可能会问:都已经把三角形准备好了,为什么还要在它身上动刀?说白了,问题恰恰藏在"图元"这两个字里。顶点着色器只能 1 进 1 出(fgiesen · Part 3:顶点处理),它没有能力把一个三角形变成五个、把顶点的中间结果写回内存、或把一个粗糙的大三角形按补片细分出几千个精细的小三角形。这些事需要一个以图元为单位的前端——而这正是本篇的主角:几何着色器(geometry shader,GS)、流输出(stream-out,SO)与细分(tessellation)。

本篇不涉及什么、为什么: - 不写"怎么写 GS/HS/DS 着色器"——HLSL/GLSL 语法、细分因子的调参技巧属于算法层(《实时渲染管线探析》)。 - 不重复网格着色器(mesh shader)的完整实现——那是 ep12"2011 之后"的预告,本篇只做对照与交棒。 - 不展开 Catmull-Clark 这类离线细分曲面(subdivision surface)——硬件 tessellation 是按 patch 实时铺点,两者机制不同。

出处:Fabian Giesen《A trip through the Graphics Pipeline 2011》· Part 10/11/12(GS / Stream-Out / Tessellation 的硬件机制)(CC0 公有领域)。

这一章要回答的,是几何前端这条线:图元从哪来、GS 按图元执行到底要付什么代价、SO 怎么把顶点旁路写回内存、细分那五个阶段各自干什么、域坐标与缝合为什么是工程难点——更重要的是,为什么到 2011 年之后,GS 的这些能力被逐渐边缘化。读完全篇你会发现,"为什么被边缘化"的答案其实就藏在每一节的账里,其中:二次装配、不可预知的输出规模、成倍增长的顶点流——这些代价在 2011 年靠专用单元与固定功能硬扛,到 2011 年之后被更通用的计算模型摊平。这条"专用 → 通用"的迁移线,正是本系列从本篇到 ep12 的主旋律。

OK,下面我们先把这条几何前端放进系列的全景图里。我们已经走完的命令前端(ep01)把 Draw 送进 GPU、显存与地址空间(ep02)讲清了数据怎么进出、着色器核(ep03)解释了可编程阶段如何并行、图元装配(ep05)把顶点连成图元、光栅化(ep06)把图元倒成像素、像素后端(ep08)负责把像素写回显存——而本篇夹在装配之后、光栅化之前:这一小段"几何前端"不是装配的一部分,也不是光栅化的前置,它是能改写图元本身的独立地带。系列的知识地图在技术博客页汇总,本篇负责其中"几何前端"这一站。

而在谈"图元从哪来"之前,先来看看实物:下面的 Intel 810 芯片组是 1999 年的一块整合北桥,图形核心(含图元装配与光栅化)就嵌在这颗芯片里——"几何前端"在独立 GPU 诞生前,就是北桥里的几百万个晶体管。这块芯片与显卡实拍(§九)一头一尾,恰好框出几何硬件从北桥时代到独立 GPU 的二十年跨度。

老式整合芯片组实物:几何硬件的北桥时代
图 9-1Intel 810 芯片组实物:1999 年的整合北桥,图形核心(装配/光栅化)嵌在芯片内部——几何前端从北桥时代一路走到独立 GPU@ Wikimedia Commons · Jonathan Zander(CC BY-SA 3.0)

一、图元从哪来:先再谈一次 primitive assembly(set-context)

好了,要把"几何前端"讲清楚,得先回到它的输入——图元装配(primitive assembly,PA)。上一系列 ep05 我们讲过装配的机制,这里只回放关键的几条:顶点着色器输出的是顶点流(一个顶点一个顶点),而 GS 的输入是完整图元——三角形三点、线段两端、或一个点。装配器(assembly unit)负责把索引缓冲引出的顶点按拓扑连成图元,再交给后续阶段。

这里值得再钉一层:顶点流从哪来。顶点数据躺在顶点缓冲(vertex buffer)里,但 GPU 不会按顺序把缓冲从头读到尾——绘制指令带上索引缓冲(index buffer),装配器按索引取值(fgiesen · Part 5:Primitive Assembly)。索引缓冲的意义是复用:一个顶点被 6 个三角形共享时,索引缓冲让它在内存里只存一份,装配阶段按索引引用三次(这正是索引绘制的带宽账,ep02 展开过)。拓扑则决定了装配器"几个顶点连一个图元":D3D11 定义了 8 种图元拓扑(D3D11 · Primitive Topologies),其中:除了点表、线段表/线段带、三角形表/三角形带,还有带邻接(adjacency)的变体——三角形带邻接一次喂 6 个顶点,多出的 3 个就是"共享这条边的邻居顶点",专供 GS 读取邻接信息(§十 会讲到这是 GS 的一项关键能力)。

装配之前还有最后一站值得钉一下:顶点变换后缓存(post-transform cache)。索引缓冲让装配器"按索引取顶点",但同一个顶点被 6 个三角形共享,意味着它可能被取 6 次——如果每次都重新跑一遍顶点着色器,就是 6 份重复计算。硬件于是在 VS 输出与装配之间加了一层按索引暂存的缓存:顶点经 VS 变换后按索引暂存,同索引再次被引用时直接命中缓存、跳过重复变换。缓存容量有限(2011 年前后桌面硬件是几十到上百个顶点),命中率决定装配的实际计算成本——这也是三角形带/扇这类顺序访问拓扑在旧硬件上更快的原因:共享顶点还活在缓存里时就被再次引用,命中率天然更高。这条缓存是"索引绘制为什么省"的第二层账:第一层省带宽(顶点只存一份,ep02),第二层省变换(共享顶点只算一次)。

先来看看装配器的输入打包成什么样:D3D11 把"顶点缓冲 + 索引缓冲 + 拓扑 + 实例化参数"捆成一个装配任务,交给 input assembler(IA)阶段执行。IA 同时还负责一层装配级实例化(instancing):不换顶点缓冲,靠"每实例数据"(instance step rate)让同一批顶点按实例重复装配——这跟 §二 的 GS 实例化是两种粒度:IA 实例化在装配层复制图元,GS 实例化在着色器层复制输出,前者不花着色器指令、后者不花装配带宽,工程上各有用武之地。

硬件得到的不是"随手扔过来的顶点",而是一份份装配好的图元数据。这带来一个直接后果:GS 必须先有 PA 才能工作。因为 GS 的输入拓扑是"三角形列表"或"三角形带",如果装配没有发生,GS 根本拿不到"一个完整三角形"。

术语一致性声明:下文"装配(PA)/ 二次装配(二次 PA)/ 几何着色器(GS)/ 流输出(SO)/ 细分"以 D3D11 世代的可编程图形管线为骨架,硬件实现以 2011 年前后 NVIDIA/AMD 的实例为参照——不代表所有 GPU 的细节完全一致,这点会在各节声明限定。

出处:Vulkan 规范 · Vertex Post-Processing/D3D11 图形管线/D3D11 · Input Assembler 阶段/fgiesen · Part 3(顶点变换后缓存)。

把输入钉牢,我们就可以下钻到第一个主角了——以图元为单位执行的几何着色器。

二、几何着色器:以 primitive 为单位执行(explain-structure)

好了,几何着色器(geometry shader,GS):以完整图元为输入、输出零到多个新图元的可编程阶段。它插在 PA 与光栅化之间,是管线里唯一一个"一个图元进、任意多个图元出"的可编程阶段。

GS 在管线上的独立成段
图 9-2几何着色器管线示意图:GS 独立成段,夹在顶点/装配与光栅化之间,输出再入光栅化@ Wikimedia Commons · Davi.trip(CC BY-SA 4.0)

GS 的输入是完整图元(三角形三点,可选带邻接的六点),输出是新的顶点流——拓扑可变、顶点数可变。这与顶点着色器的根本差异在于:输出规模在运行时才知道。VS 的预算固定(1 顶点进 1 顶点出,硬件的执行槽可以预先排满),而 GS 一进可以出 0 个、1 个、几十个顶点——硬件无法预先知道这次调用要占多少执行槽、写多少数据。

GS 调用还有一层"实例化"(geometry shader instancing):一次输入图元可以按声明中的实例数重复执行多次,其中:每次实例拿到的 SV_GSInstanceID(D3D11 语义)从 0 起递增。这让 GS 能用一份代码对同一个图元做多份输出——比如把一个粒子点同时展开成面向六个方向的公告板(billboard),就是靠实例数 6 一次跑完,而不是把图元复制六遍交给六次调用(实例化的通用机制见 GPU Gems 2 · Ch.3 Inside Geometry Instancing)。输出规模则受声明上限约束:GS 在编译期声明 MaxVertexCount(每个实例最多输出多少顶点,D3D11 硬件上限通常 1024),这个上限直接决定硬件要预留多大的暂存——上限声明得越大,暂存越宽,这也是"输出规模不可预知"的第一层代价。

GS 执行模型:可变输出规模的执行槽分配
图 9-3GS 执行模型:一个图元进、N 个顶点出,执行槽按运行时实际输出分配(本系列绘制@ 本站自绘)

发射语义也值得说清:GS 不是"返回一个顶点数组",而是用 EmitVertex() 把当前顶点逐个发射、用 EndPrimitive() 结束当前图元(HLSL 语法)。发射出去的顶点立刻进入输出暂存,攒够一个图元(按输出拓扑的点数)就往下游送。输出拓扑被限定为点、线带、三角形带三种——因为只有 strip 是下游装配能流式消费的拓扑(§三 第二笔账)。发射多少次、何时结束一个图元,完全由着色器在运行时决定——这正是"GS 的输出规模不可预知"在 API 语义层面的来源。

先来看看硬件怎么应对这种"无法固定预算"——答案是不预先固定:GS 的执行以图元为单位派发,输出通过一块可增长的暂存区攒起来,攒够一个图元再往下游送。这带来两个直接后果:其一,GS 的执行开销不与输入顶点数成正比,而与输出顶点数成正比;其二,下游的装配必须重新做一次——这就是下一节的主角,二次 PA。

限定声明:GS 的硬件实现在不同厂商差异很大(有整段专用硬件、也有完全靠通用着色器核模拟的),但"输出规模不可预知"这条性质是 API 语义决定的,任何实现都躲不掉。

出处:Vulkan 规范 · Geometry Shading/WDDM 驱动模型 · Geometry Shader 阶段/fgiesen · Part 10。

GS 能按图元生成,那代价呢?下一节把这笔账算清楚。

三、GS 真正的代价:二次 PA 与 pass-through(prove-claim)

好了,先来看看 GS 的代价——它不在"着色器指令本身",而在破坏了下游两个假设:拓扑连续与预算固定。

第一笔账:二次装配(second PA)。 顶点着色器的输出是连续顶点流,装配器可以流式地"攒 3 个连成三角形";而 GS 的输出是可变数量的零散图元——其中:"零散"是关键。它可能把一个三角形变成 5 个三角形,也可能输出 0 个(直接丢弃)。下游光栅化器需要的是连续图元流,于是硬件必须把 GS 攒出来的零散顶点再装配一次。这一趟二次 PA 意味着:顶点数据要再走一遍装配单元、拓扑信息要重新计算——纯开销,不产生任何新几何。

第二笔账:只能输出 strip。 GS 的输出拓扑被限定为"点/线带/三角形带"(point/line strip/triangle strip),因为 strip 是下游装配唯一能流式消费的拓扑——GS 想输出独立的三角形列表,也得自己用 strip 语义拼。可 strip 拓扑有个老问题:切分(cut)时会产生退化图元,下游光栅化要浪费吞吐剔除它们。

第三笔账:pass-through 也慢。 说白了,fgiesen 在 2011 年的实测里指出:即便 GS 什么都不做、原样转发输入顶点(pass-through),整个几何阶段的吞吐也会比不启用 GS 慢 3–7 倍——因为顶点必须经过"装配 → GS 执行 → 二次装配 → 光栅化"这条更长的路径,而且 GS 输出暂存区的读写都占用片上带宽。这里要补一层背景:3–7 倍的差距不是"着色器指令贵",而是执行槽的浪费——GS 以图元为单位派发,一个三角形(哪怕只输出 3 个顶点)也要占一个完整的执行槽;硬件为可变输出准备的暂存区,无论这次实际发射几个顶点都要读写一遍;再加上二次装配的纯开销,几何阶段的吞吐被这条"多出来的路径"系统性拖慢。

GS 代价模型:二次 PA 与 pass-through 对照
图 9-4GS 代价:启用 GS 后顶点多走一趟二次 PA,pass-through 吞吐降为 1/3–1/7(本系列绘制@ 本站自绘)

量化口径:3–7 倍为 fgiesen 2011 年对当时硬件的实测观察,不代表现代 GPU 的绝对值;但"启用 GS 必经二次装配、pass-through 不比不启用快"这个方向性的账,到今天依然成立。

出处:fgiesen · Part 10(GS 开销)/fgiesen · Part 5(装配与 strip 语义)(CC0)。

既然 GS 这么贵,那"把顶点写回内存"这种事,能不能不走光栅化?能——这就是流输出。

四、流输出:把顶点旁路写回内存(show-flow)

流输出(stream-out,SO):从 GS(或 VS,或 DS)的输出口,把变换后的顶点原样写进显存里的 vertex buffer,旁路掉光栅化与像素阶段。它是"几何前端"里唯一一个不往屏幕走、往内存走的出口。

先来看看它在管线里的位置:D3D11 的管线里,SO 挂在可编程阶段输出口之后——启用 GS 时它是 GS 的输出口,不启用 GS 时可以直接从 VS(或细分后的 DS)输出口接出。这一点很关键:SO 不需要 GS 在场,它只是"输出口上的一根分叉"。而 SO 写什么、按什么布局写,由流输出声明(SoDecl)在编译期定死——声明里列出要写回的元素(位置、法线、自定义属性)与它们在顶点缓冲里的偏移,运行时把每个输出顶点按声明打包写回。布局有两种:交错式(interleaved)把所有元素打进一个缓冲、按顶点交错排列;分离式(separate)每个元素一个缓冲(D3D11 最多 4 个分离流)。选择取决于下游怎么读——粒子系统通常交错单缓冲,而需要单独取位置做碰撞的场景用分离流更省带宽。

SO 旁路光栅化、写回 vertex buffer
图 9-5流输出:GS/VS 输出口分叉,顶点流旁路光栅化直接写回显存 vertex buffer(本系列绘制@ 本站自绘)

好了,SO 的典型用例是把 GPU 算过的顶点数据拿回来继续用:物理粒子的位置更新后写回,下一帧再读出来渲染;布料模拟的顶点位移写回,用来做碰撞;网格变形、GPU 蒙皮——凡是"顶点要变、变完还要再进管线一次"的场合,SO 都是最直接的回写口。但它的工程账不便宜,有四个限制要记牢:

  • 写口带宽:SO 写回的是完整顶点(可能几十字节一个),写回量 = 顶点数 × 顶点大小。粒子系统几百万粒子一帧,就是几百 MB 的写带宽——这笔账通常比渲染本身还贵。
  • 缓冲 counter 上限:SO 写入的缓冲带一个隐藏的 counter(记录已写顶点数),D3D11 硬件这一代上限是 2^24 或 2^32 个顶点(64M/4096M)。写满后 SO 会静默停止写入,下一帧不加处理就会读到残缺数据。
  • 同步/回读延迟:CPU 要读回 GPU 写的顶点,必须走"等待 GPU 完成 → 映射 → 拷贝回 CPU 可见内存",一帧的延迟是常态;每帧回读在移动/低功耗设备上基本不可行。
  • 与光栅化的互斥:一条 GS 输出要么全走光栅化、要么全走 SO——想"既渲染又回写"得开两条 pass 或复制一份顶点流。

SO 与 counter 的搭配,还有一层工程细节值得展开:D3D11 提供了 DrawAuto——让 GPU 按 SO 写回的顶点数自动决定本次绘制取多少顶点,CPU 完全不用知道上一帧 SO 实际写了多少(否则 CPU 得读回 counter,又落进同步延迟的坑)。于是"SO 写回 → 下一帧 DrawAuto 读回再渲染"构成一条免 CPU 介入的 GPU 内循环:物理/粒子/布料这类"GPU 算、GPU 用"的数据,可以整条留在 GPU 上流转。与之对应的另一条路径是把 SO 缓冲绑成 UAV(unordered access view)给计算着色器当输入——这是 SO 与 CS 的第一次交汇,也是 §十 的核心论点"CS 直写 buffer 取代 SO"的接口基础:既然 SO 写回的数据最终要能被任意阶段读,那"写回"这件事本身由 CS 来做,就把 SO 这条专用路径整段省掉了。

需要说明:SO 的 counter 上限与同步延迟都是硬件与驱动实现层的限制,API 层只保证"SO 能写回",不保证"写回后立刻可用"——这正是 ep11"同步与资源生命周期"要展开的主题。

出处:fgiesen · Part 11(Stream-Out)/D3D11 · Stream-Output Stage/D3D11 · SO 计数器上手/UWP · Stream-Output Stage。

SO 是把顶点往内存送,而第三个主角的方向相反——它把一个粗图元往"细"里做。这就是细分。

五、细分:D3D11 世代的新固定功能段(explain-structure)

细分(tessellation):把输入的低模补片(patch)按细分因子实时铺成更密的网格。它在 D3D11 世代被引入管线,由两个可编程阶段 + 一个固定功能段组成,插在 PA 与 GS 之间(无 GS 时插在 PA 与光栅化之间):

细分五阶段:IA→HS→Tessellator→DS→GS 插入位置
图 9-6细分插入位置:IA 之后 HS 算 patch 常量 → Tessellator 固定铺点 → DS 映射回对象空间 → 可选 GS → 光栅化(本系列绘制@ 本站自绘)
  • Hull shader(HS,外壳着色器):可编程。输入 patch 的控制点(D3D11 里一个 patch 最多 32 个控制点),输出两样东西:①变换后的控制点(交给 DS);②细分因子(tessellation factors)——沿 patch 每条边和内部要细分多少段(交给 Tessellator)。
  • Tessellator(细分器):固定功能,不可编程。按细分因子在 patch 的域(domain)上铺点,生成一批带域坐标(u, v, w)的新顶点。它不碰任何几何数据,只负责"铺多少点、按什么规则连"。
  • Domain shader(DS,域着色器):可编程。对 Tessellator 铺出的每个域坐标执行一次,把域坐标映射到对象/世界空间(控制点插值 + 可选的位移映射)。

先来看看 HS——它是"一个可编程阶段、两个入口"的典型:patch constant function 每个 patch 只执行一次(算因子、算 patch 级常量),main 函数每个控制点执行一次(变换控制点)。两个入口的输入输出在编译期分开声明,这决定了硬件如何派发——因子计算只有一次、控制点变换并行 N 次,两者天然是不同粒度的工作。说白了,Tessellator 的"不可编程"要往深里读一层:它不是"没有着色器",而是整个铺点算法被焊死在硬件里——域的类型、铺点规则、因子取整规则都是固定的,API 只传"域 + 因子"两个参数。固定换来的是吞吐:一个 Tessellator 单元每周期能铺出几十个新顶点(fgiesen 在 Part 12 里把它比作"一个极窄但极快的专用单元"),没有任何可编程单元能追上这个吞吐。

//-----------------------------【细分三阶段伪代码】-------------------------------
// 描述:HS/Tessellator/DS 各自负责什么(对应 D3D11 管线语义)
//-------------------------------------------------------------------------------
//【1】HS:读控制点(≤32) → 输出变换后控制点 + 内/外细分因子
//【2】Tessellator(固定功能):按因子铺域坐标(u,v,w),生成新顶点"壳"
//【3】DS:每个域坐标执行一次 → 插值控制点 + 位移映射 → 输出世界空间顶点
// 可选第 4 步:GS 按图元处理细分后的三角形(再入二次 PA 的代价)

关键在第 2 步的"不可编程":Tessellator 是固定功能单元,不执行着色器指令——它是被硬件"焊死"的铺点算法,这也是它吞吐极高的原因(一个 Tessellator 每周期能铺几十个新顶点)。这也是为什么我们说细分是"D3D11 世代的新固定功能段"。

限定声明:本节以 D3D11/Vulkan 的细分语义为骨架。OpenGL 4.x 的同名阶段(TES/TCS)行为等价,只是名称不同;移动端 GPU 上 Tessellator 的实现与桌面差异较大,详见《移动端图形与功耗》。

出处:Vulkan 规范 · Tessellation/D3D11 · Tessellation 阶段总览/fgiesen · Part 12。

五阶段的骨架有了,那 Tessellator 到底怎么"铺点"?下一节看域的铺法与细分因子。

六、域的铺点与细分因子(show-flow + prove-claim)

好了,Tessellator 在域(domain)上铺点。D3D11 支持三种域,对应三种"怎么把一个 patch 切成网格"的规则:

  • triangle 域:patch 是三角形,域坐标是重心坐标 (u, v, w),u+v+w=1。
  • quad 域:patch 是四边形,域坐标是 (u, v),各取 [0,1]。
  • isolines 域:patch 是线带,域坐标 (u, v) 铺出等值线(适合做地形等高线这类"线"几何)。
三种域的铺点:quad/triangle/isolines 与内外因子
图 9-7域铺点:quad 域按内外因子铺方格、triangle 域按重心铺三角、isolines 铺线(本系列绘制@ 本站自绘)

铺多密由细分因子(tessellation factor)决定。其中:每边一个边因子(edge factor)、patch 内部一个内部因子(inside factor)。HS 算出因子后,Tessellator 严格按因子铺点——因子是浮点数,实际铺出的段数是 ceil(factor)(内部因子还有向偶数取整的规则,保证内部网格能对缝)。

"向偶数取整"值得展开:D3D11 的 Tessellator 支持四档取整模式(D3D11 · Tessellator 初始化)——integer(直接取整)、pow2(取到 2 的幂)、fractional_odd(取到 ≥1 的奇数)、fractional_even(取到 ≥2 的偶数)。后两档"分数取整"的妙处在于因子渐变时不跳变:相邻 patch 的因子从 4 平滑过渡到 5 时,顶点数不会突然蹦一跳,网格的 LOD 过渡是连续的(这对 §八 的缝合问题直接相关——因子取整模式决定了"共享边两侧的段数是否天然对齐")。内部因子还必须 ≥2(偶数档)或 ≥1(奇数档),避免内部退化成没有网格的空 patch。这些规则不是 API 的边角料,而是"铺点结果可预测"的契约——下游的 DS 与光栅化都依赖"这个 patch 到底会铺出多少顶点"这件事是确定的。

四档取整各自的主场值得点一下,工程上它们不是随便选的:integer 用于程序化生成的固定网格(铺点结果必须逐帧完全一致,比如 UI 网格、体素表面);pow2 用于要与 mipmap 层级对齐的场景(§七 会讲因子与 mip 的联动,因子取 2 的幂可以让几何密度与纹理密度同步进退);fractional_odd/even 用于地形、角色这类"近看细、远看粗"的连续 LOD——它们牺牲一点"铺点结果完全整数化"的确定性,换回 LOD 过渡不闪跳。而 isolines 域的铺法与三角/quad 不同:它沿 u 方向铺线段、沿 v 方向铺行数(边因子决定行数、内部因子决定每行段数),输出是线而非面——地形等高线、天气雷达剖面这类"要线不要面"的场景专用。三种域 + 四档取整,是"铺点规则"这个设计空间的全部切面,Tessellator 固定功能把它们焊死,DS 才拿到"输入 (u,v,w) 必落在已知网格上"的确定性。

先来看看顶点数怎么估:quad 域、内部因子 f 时,内部铺 (⌈f⌉+1)² 个顶点(含边界),三角形化后约 (⌈f⌉+1)²×2 个三角形。把数字放大看:

顶点数简式:因子涨一格,顶点翻一个数量级
图 9-8顶点数随因子增长:quad 域 f=8 → 约 162 个三角形,f=32 → 约 2178 个,f=64 → 约 8450 个(本系列绘制@ 本站自绘)

量化口径:quad 域 f=32 时,(33)²×2 ≈ 2178 个三角形——一个 patch 从"4 个顶点"变成"两千多个三角形",顶点量翻了两个数量级以上。这也是为什么"细分因子失控"是性能灾难的常见来源:铺点不花着色器指令,但下游光栅化与像素阶段要吃下全部新增三角形。

出处:fgiesen · Part 12(Tessellation 因子与顶点数)(CC0)。

Tessellator 铺出的只是一堆"域坐标",真正把它们变成世界里的顶点,是 DS 的活。

七、域坐标、DS 映射与带宽账(show-flow + compare-options)

好了,域坐标(domain coordinates):Tessellator 输出的、定义在 [0,1](或重心)空间里的归一化坐标。它不是世界空间里的位置——只是"这个新顶点在 patch 上的相对位置"。

DS 的映射:对每个域坐标,DS 做两件事——①把 patch 的控制点按插值权重合成位置(三角形用重心插值,quad 用双线性插值);②可选地叠加位移映射(displacement mapping):采样一张高度图,沿法线把顶点"顶"出去。这一步把域坐标变成对象空间顶点,交给光栅化。

位移映射的细节值得展开:高度图是普通纹理,DS 按域坐标采样得到高度值,再沿插值出的法线把顶点位移出去。问题随之而来——位移后的表面法线不再是插值法线,光照会错。标准做法是在 DS 里对高度图做有限差分(取相邻 texel 的高度差近似偏导),重建位移后的切线与法线,再传给像素阶段。这条链让"细分 + 位移"几乎等于"在 GPU 上实时做置换建模":低模控制点只定义大致形状,细节全部来自高度图。也正因为细节在纹理里,LOD 的粒度就多了一维——细分因子控制几何密度,mipmap 层级控制高度图分辨率,两者要联动才不会出现"几何密了、高度图糊了"的错位(这正是 §六 因子取整与 mip 选择的联动场景)。

DS 映射:域坐标 → 控制点插值 + 位移映射 → 对象空间
图 9-9DS 映射:域坐标经控制点插值与位移映射,落到对象空间(本系列绘制@ 本站自绘)

位移映射的工程限制也要说透。其一,三角形密度与高度图分辨率要联动:细分把几何密度提上去之后,若高度图 mip 没有跟着提,顶点被"顶"出去的细节会糊成一片——§六 说的"因子取整与 mip 联动"就是为这件事准备的两道旋钮。其二,自相交问题:位移幅度超过 patch 局部特征尺寸时,相邻顶点可能互相穿透、网格自交,业界通常把位移幅度钳制在特征尺寸的某个比例以内。其三,碰撞与剔除的复杂度:位移后的表面与原始低模不再一致,物理碰撞、背面剔除都要以"位移后表面"为准,这部分成本常被低估。这些限制不是细分特有的,而是"运行时生成几何"共有的——DS 现场算顶点的自由度,同时把"正确性"也交到了着色器手里。

先来看看 DS 映射的带宽账——细分最漂亮的一张表。对比两条路:

  • 传统 LR(linear rendering)顶点流:精度靠预生成——要精细就得在内存里存几百万个预细分顶点,加载、变换、写回全程吃显存带宽。
  • patch 域按需生成:内存里只存低模控制点(几百个顶点 + 一张高度图),精细顶点是 DS 现场算出来的——同样精细度,内存带宽省一个量级(不预存、不回写,只有控制点进出)。
LR 顶点流 vs patch 域按需生成的带宽账
图 9-10带宽对照:预生成 LR 顶点流全程进出显存 vs patch 域按需生成只搬运控制点(本系列绘制@ 本站自绘)

量化口径:"省一个量级"指同精细度下,patch 方案搬运的数据是预生成方案的 1/10 上下(控制点密度 vs 细分后顶点密度之比),具体比值取决于细分因子与 patch 大小。这是细分在带宽受限的移动/嵌入式场景(如 terrain、角色细分)被广泛采用的核心原因。

出处:fgiesen · Part 12(DS 与带宽)(CC0)。

DS 按域坐标现场算顶点,省了带宽,却埋下一个坑——相邻 patch 各算各的,共享边怎么办?这就是缝合问题。

八、缝合与工作集(show-flow + explain-structure)

先来看看细分最经典的工程坑:裂缝(crack)。相邻两个 patch 如果细分因子不一致,共享边上两侧的顶点密度就不同——一侧把边切成 8 段,另一侧切成 4 段,两边的顶点位置对不上,网格就裂开了一条缝(光照、深度测试都会穿帮)。

相邻 patch 因子不同步 → 裂缝
图 9-11裂缝:共享边两侧因子不同(8 vs 4),顶点错位产生缝隙(本系列绘制@ 本站自绘)

消除裂缝的标准手段,是让共享边的因子一致:

  • 沿边取统一因子:每个 patch 的每条边,在 HS 里用与邻居相同的规则计算(通常由边两端的顶点属性 + LOD 函数决定)——只要规则一致,相邻 patch 在同一根边上就会算出同一个因子,顶点就对齐了。
  • 顶点落边规则:DS 里对"落在共享边上的顶点",必须用同一条边的插值规则(而非自己 patch 的面规则)来算位置——这保证即使因子一致,两个 patch 算出的同一边上顶点坐标也逐位一致。
新顶点落边规则与 width 增长
图 9-12落边规则:共享边顶点按边插值(左)vs 按面插值(右)——按面插值会错位(本系列绘制@ 本站自绘)

好了,工作集(working set)是第二个隐性成本:细分按 patch 逐块处理,但一个 patch 的因子会影响相邻 patch(LOD 选择要考虑邻居密度)。硬件为了一次性处理一块"相互影响的 patch 区域",要暂存一批 patch 的数据——这个暂存区就是细分的工作集。patch 越大、因子越细,工作集越大;width(一次能并行处理的 patch 宽度)决定了细分的吞吐上限。

这里再往工程里走一层:LOD 选择怎么算因子。标准做法是屏幕空间误差驱动——HS 在 patch constant function 里取每条边两端顶点的世界位置,投影到屏幕算出边长(像素数),再除以一个目标误差阈值(比如 1–4 像素),就得到这条边的细分段数。边越近、越大,因子越高;边退到屏幕外,因子可以降到 1(不再细分)。这套"投影尺寸 ÷ 误差预算"的公式,与 mipmap 的 LOD 选择是同一个思路——只是它作用在几何上。而 width 的硬件含义则是:Tessellator 一次吃进 width 个 patch 并行铺点(fgiesen 在 Part 12 里提到当时桌面硬件一次并行处理 32 个 patch 量级),工作集就是这 width 个 patch 的暂存。因子越细,单个 patch 铺出的顶点越多,暂存压力越大——所以"因子失控"不只是下游光栅化的问题,它首先撞爆的是细分自己的工作集。

"屏幕空间误差驱动"不是本篇发明的说法,它是细分曲面进入实时管线时的经典方案:GPU Gems 2 第 7 章《Adaptive Tessellation of Subdivision Surfaces》就把"按屏幕空间误差自适应决定细分密度"作为细分的主要动力之一,并给出过一套基于曲率的误差估计方法(以视距与表面曲率共同驱动因子)。这条谱系要说明的是:因子不是美术随手填的,它是一套可量化预算的输出——屏幕空间误差是"视觉质量"的度量,因子是"几何成本"的度量,两者之间的换算正是 HS 的 patch constant function 要干的活。工程上还会叠加一层帧间稳定:因子不允许逐帧剧烈跳动(否则 LOD 过渡在运动时闪烁),常见做法是把因子变化钳制在每帧 ±1 档以内——这与 §六 的取整模式配合,构成"细分 LOD 平滑性"的两道保险。

width 再往深一层读,它其实是"硬件把细分当作什么"的说明书:Tessellator 不是"给任意多个 patch 慢慢铺",而是一批吃下 width 个 patch、各自独立铺点——它把细分当成"固定批量的小任务",而不是"流水线式的流"。这带来一个工程后果:patch 的铺点负载不均衡(一个 patch 因子 64、邻居因子 2)时,硬件按 width 个中最慢的 对齐完成时间,负载落差直接变成吞吐浪费。所以网格划分时"让相邻 patch 的因子尽量接近"不仅是缝合问题的要求,也是让 Tessellator 满载的要求——负载均衡从 §八 的第一行起就是设计约束。

需要说明:裂缝消除的"沿边统一因子"是通用做法但不是 API 强制——API 只保证"相邻 patch 各算各的",不保证"结果不裂"。工程上裂缝处理属于"使用者要自己负责的正确性",这也是细分比想象中难用的主要原因。

出处:fgiesen · Part 12(缝合与工作集)(CC0)。

机制讲完了,把几何前端的所有账拢到一张表上——顺便看看它工作在哪片硬件上。

九、量化代价账:几何前端各单元吞吐(prove-claim)

好了,把几何前端的账拢在一起,看每个单元在"一个 patch 从 4 个顶点被细分到两千个三角形"的过程中各吃多少吞吐:

单元 输入 输出 吞吐压力 性质
PA(装配) 顶点流 图元 顶点数线性 固定功能
GS(按图元执行) 图元 可变顶点 输出顶点数(不可预知) 可编程
二次 PA GS 输出 图元 输出顶点数 纯开销
Tessellator 因子 + 域 域坐标顶点 因子平方级 固定功能
DS 域坐标 对象空间顶点 因子平方级 可编程
光栅化 三角形 像素 三角形数线性 固定功能
几何前端量价汇总:各单元吞吐压力
图 9-13量价账汇总:GS 按输出顶点付账、Tessellator 按因子平方级铺点、二次 PA 是纯开销(本系列绘制@ 本站自绘)

把"因子平方级"这条读成数字,才能感受到这条账的分量。设一个 quad 域 patch、内部因子 f=16,则 (⌈16⌉+1)²=289 个内部顶点、约 578 个三角形。其中:若场景里是一块 16×16 的 patch 网格(256 个 patch,一块地形 chunk 的常见尺寸),一次细分就铺出 256×578≈14.8 万个三角形——而输入只有 256×4=1024 个控制点。换句话说:细分把"几何带宽"换成了"几何吞吐"——你不再从内存搬几百万个预细分顶点,但光栅化与像素阶段要为这 14.8 万个小三角形全数付账。这正应了 §七 的结论:省下的是带宽,涨上去的是吞吐,账是同一笔。

GS 那条账的读法相反——它不是把顶点"变多",而是把"未知"塞进管线。GS 的执行槽按运行时实际输出分配(§二),下游必须准备二次 PA 与可增长暂存(§三),于是哪怕 GS 原样转发输入(pass-through),几何阶段吞吐也会掉到不启用时的 1/3–1/7(fgiesen 在 Part 10 的实测)。这条账的启示是:GS 的代价不随"效果"走,而随"启用"走——启用它,管线就为"输出规模不可预知"这件事本身付钱,与你实际发射了多少顶点无关。

SO 的账则写在带宽上。一个粒子系统 200 万粒子、每粒子 32 字节(位置+速度),SO 写回一帧就是 64 MB;若还要把结果读回 CPU 做碰撞(§四 第三笔账),映射拷贝再加 64 MB——一帧 128 MB 的带宽,从"GPU 写"到"CPU 读"来回两趟。作为对照,2011 年前后桌面 GPU 的系统内存带宽在 20–40 GB/s 量级,几何输出带宽是受限资源的常态;今天(2020 年代中期)GDDR6X 的带宽高了一个量级,但"写回→读回"这条链的延迟与带宽成本结构没有变——SO 依然适合"低频、批量、不需要 CPU 即时介入"的回写,不适合"每帧都读回来"的紧耦合循环。

先来看看当年的硬件坐标系:2011 年前后,桌面旗舰的填充率规格放在今天看小得惊人——GeForce GTX 580(GF110,Fermi 架构)的像素填充率 37.05 GP/s、纹理填充率 49.41 GT/s,带宽 192.4 GB/s;而今天(2020 年代中期)的旗舰已高出不止一个数量级。这个数量级的跨越,正是本篇叙事主线的注脚:2011 年"几何"是稀缺资源,GS 每多输出一个三角形都要精打细算;今天几何吞吐大幅宽松,"按图元编程"的价值被计算着色器与 mesh shader 重新定价。细分在当年是"用固定功能换吞吐"的奢侈品,今天依然靠固定功能保持单位面积最高吞吐,只是它服务的场景从"省带宽"扩展到了"程序化生成"。

这张表的读法是:几何前端把"几何复杂度"提前到了光栅化之前——GS 放大图元数、细分放大顶点数,下游光栅化与像素阶段要消化全部新增量。于是"几何前端省下的带宽"(patch 按需生成)和"几何前端制造的压力"(新增三角形全要光栅化)是同一枚硬币的两面。

而这一切账,最终都结算在那片真实的硬件上:

现代显卡实物:几何前端工作在那片硬件上
图 9-14GeForce RTX 50 系列显卡实物:本篇的 PA/GS/Tessellator/DS/光栅化单元,就分布在这块 PCB 中央的 GPU 芯片里@ Wikimedia Commons · Alphaaloy(CC BY-SA 4.0)

出处:fgiesen · Part 10/11/12 全篇量化数据(CC0)。

账算完了。最后一个问题:GS 的这些能力,到 2011 年之后为什么被逐渐取代?

十、GS 之后:CS 与 mesh shader(compare-options)

好了,GS 的定位其实很尴尬:它能做的三件事——按图元生成/复制、可变输出、读取邻接信息——每一件都有更强的替代品:

需求 GS 的做法 替代方案 替代的优势
按图元生成新几何 图元进、图元出 计算着色器(CS) 输出规模不受拓扑限制,任意读图元
把顶点数据回写再用 SO 回写 CS 直接写 buffer 无需走 GS 执行槽,完全可编程
读邻接图元做效果 GS 邻接输入 CS 读顶点索引 拓扑之外任意访问,无 strip 限制
整块生成网格 GS 逐图元生成 mesh shader GPU 上整块生成 mesh,可预知输出规模
GS 能力 vs CS/mesh shader 对照
图 9-15替代路线:GS 能做的,CS 与 mesh shader 大多能做且更便宜(本系列绘制@ 本站自绘)

为什么 CS 是更好的 GS? 说白了,根本原因回到 §二 的那条性质:GS 的输出规模不可预知,硬件只能靠二次 PA + 可增长暂存区兜底;而 CS 在拓扑之外任意读写 buffer——它可以在启动前就把输出规模算清楚(dispatch 的大小已知),把"不可预知"变成"可预知"。mesh shader(ep12 详述)更进一步:它在 GPU 上整块生成 mesh,用可预知的输出规模 + 无二次 PA 的方式,取代 GS 的逐图元生成。

CS 具体怎么"做 GS 的事"?两条路径值得点出。其一是间接绘制(indirect draw):CS 在 buffer 里写好"要画多少个实例、多少个顶点"的绘制参数,DrawIndirect 直接按它绘制——GS 当年靠 SO 写回 + counter + DrawAuto(§四)凑出来的"GPU 决定画什么",CS 用一次 dispatch 就能写全。其二是Append/Consume 缓冲:CS 用 AppendStructuredBuffer 把不定量输出攒进一个可增长的 buffer,再交给绘制或下一个 CS 消费——这正是 GS 输出暂存区的软件版,区别是 CS 完全可编程、没有 strip 拓扑限定、没有二次 PA。fgiesen 在 Part 13 里把 CS 称为"新的几何引擎"的预言,指的就是这两条路径合流:几何生成从"管线的可编程段"迁移到"通用的并行计算模型"。

mesh shader 的 Turing 落地:2018 年 NVIDIA Turing 架构引入 mesh shader 与 task shader(amplification shader),把"可编程几何生成"从逐图元推进到逐 mesh。它定义的核心数据结构是 meshlet——把大网格切成不超过 64 顶点、126 个图元的"小 mesh"(上限由硬件规定),每个 meshlet 由一次 mesh shader 调用整块生成;task shader 则负责在 meshlet 粒度上做簇级剔除(cluster culling)——整个 meshlet 不可见就直接丢弃,不必生成。对比 GS 的逐图元执行,mesh shader 的粒度大一个数量级、输出规模可预知、无二次 PA,还顺带解决了 GS 的邻接信息痛点(meshlet 内索引随意访问)。Vulkan 侧由 VK_EXT_mesh_shader 扩展提供(task/mesh 两阶段),D3D12 在 12.0 起原生支持——到 2020 年代中期,mesh shader 已成为新几何生成的主流路径,而 GS 在 D3D12/Vulkan 里保留但被明确标注为"兼容与遗留用途"。

GS 为什么没死透? 三个现实原因。其一,API 兼容:D3D11/OpenGL 时代的管线必须继续工作,GS 是那条管线里"按图元编程"的唯一选项,存量代码(粒子、草地、点精灵、阴影体积)还在跑。其二,小图元场景仍划算:GS 的二次 PA 开销对小输出规模影响小,一个 GS 输出 3–10 个顶点时,启用它的绝对成本可接受(3–7 倍是"全几何阶段吞吐"的口径,不是每次调用的成本)。其三,引擎兼容层:许多引擎的 geometry 通道仍把 GS 当"最后兜底"——当 mesh shader 不可用(旧设备、某些移动端)时,同一份效果逻辑退回 GS 实现。所以准确的图景是:GS 不是被删除,而是从"主路径"降级为"兜底路径"——新代码走 CS/mesh shader,旧代码继续走 GS。

限定声明:mesh shader 的完整实现、与光栅化的衔接、在桌面/移动端的支持现状,属于 ep12"2011 之后"的内容——本篇只给方向性对照,不展开。

出处:NVIDIA · Introduction to Turing Mesh Shaders(文字引用)/NVIDIA · Mesh Shaders for Professional Graphics/Khronos · Vulkan-Samples(mesh/tess 示例)/fgiesen · Part 10 结语(CC0)。

GS 退场、细分成了固定功能标准件、SO 被 CS 直写取代——几何前端这条线到这里收束。

十一、这一章留下的问题(bridge-to-next)

好了,几何前端走完了:GS 因二次 PA 与不可变预算被边缘化,细分成了固定功能标准件(Tessellator 不可编程、吞吐极高),SO 与回读延迟仍受限,最终被 CS 直写 buffer 取代。下一个主角是计算着色器(compute shader)——它不是"管线的某一段",而是一整套并行计算模型:波前(wavefront)、共享内存、三种屏障(barrier)。ep10 我们看它在 GPU 上怎么被调度、怎么用共享内存做协作、以及它凭什么成为 GS 之后"几何生成"的新主场。

但在把指挥棒交给 CS 之前,有一笔账必须在这里钉牢:谁保证"GPU 写"的数据,在"CPU/GPU 再读"时是就绪的? 这是本篇所有"写回"路径(SO 写回、CS 直写 buffer、间接绘制参数)共同的隐藏前提,也是工程里最常见的翻车点。

问题从 GPU 的异步本性开始。 其中:一次 Draw/Dispatch 提交后,命令只是进了队列——GPU 什么时候执行、执行到哪一步,CPU 完全不知道。于是"SO 写回完成"、"CS 把间接绘制参数写进 buffer"这些事件,对 CPU 而言是无通知的。如果 CPU 立刻映射这块 buffer 去读,读到的可能是半帧前的旧数据,甚至是正在被 GPU 写入的中间态。这就是 §四 第三笔账"同步/回读延迟"的底层机制:不是"读回慢",而是你根本不知道什么时候能安全地读。

D3D11 时代的答案:query 与多线程录制。 先来看看 query:D3D11 用 query(查询)对象做"GPU 事件"的观测:SO 写回计数可以用 SOStatistics query 取回,帧是否画完可以用 Event query 探知。query 的返回是异步的——CPU 先"布置"一个查询,等 GPU 走到那个位置时标记完成,CPU 之后轮询或等待结果。这条机制的局限很明显:它是观测不是同步——CPU 依然要猜"多久之后去取",而每帧取回 SO 计数本身就落回 §四 的回读延迟。同期 D3D11 还提供了 deferred context,把命令录制挪到 CPU 多线程并行——它解决的是"CPU 端录制瓶颈",不解决"GPU 执行到哪了",两条问题线不能混为一谈。

D3D12/Vulkan 的答案:fence 与 resource barrier。 现代 API 把"同步"显式化了。D3D12 用 fence(ID3D12Fence)做 GPU-CPU 同步点,其原理是:CPU Signal 一个值、GPU 执行到某处后把它推进,CPU Wait 到该值再访问资源——"写回 → fence → 映射读回"变成了标准姿势;命令列表之间、队列之间(图形/计算/拷贝)也用 fence 串行,跨队列依赖(比如拷贝队列把 SO 结果搬到 CPU 可见内存)是 fence 的典型场景。Vulkan 侧把同步分得更细:GPU-GPU 用 semaphore、GPU-CPU 用 fence,另有 event 与 pipeline barrier 管管线内部依赖——规范里"同步与内存可见性"是一整个章节,因为"两个命令谁先执行"与"前一个的写对后一个可见"是两件必须分别声明的事。resource barrier(D3D12)/ image layout transition + memory barrier(Vulkan)负责后者:一块 buffer 从"CS 当 UAV 写"切到"光栅化当顶点缓冲读",必须显式转换状态,否则硬件缓存/内存序不保证可见。这里最反直觉的一点是:CS 取代 SO 时,把"SO 的隐式同步"变成了"显式同步"——SO 写回 + DrawAuto 是一条免 CPU 介入的 GPU 内循环(§四),而 CS 直写 buffer 后,下游要么在同一 dispatch 里消费(靠 barrier 保证),要么跨命令消费(靠 fence/barrier 双重保证)。能力变强了,责任也变重了。

好了,把账收束。SO 与 CS 的写回路径,本质都是"生产者-消费者"问题——生产者是 GPU 的某个阶段,消费者是另一个阶段或 CPU。D3D11 时代靠 query 观测 + 隐式行为兜底(慢、不可靠、容易踩坑);D3D12/Vulkan 时代靠 fence/semaphore/barrier 显式声明(快、可控、但要求开发者懂同步)。ep11 展开的正是这张同步地图:fence/semaphore/资源状态机的完整语义、多队列并行、以及"GPU 写-CPU 读"在生产管线里的真实成本。在那之前,ep10 先把 CS 自己的内部同步讲透——波前内的 barrier 与共享内存,是比"跨命令同步"更靠前的一层。

待续:同步与资源生命周期(ep11)——SO 写回、CS 直写这些"GPU 写、CPU/GPU 再读"的路径,靠 fence/semaphore/barrier 保证不乱序;2011 之后(ep12)——mesh shader 完整实现、硬件细分器的演进。

出处:D3D12 · Fence-Based Resource Management/D3D12 · Executing and Synchronizing Command Lists/D3D11 · 多线程与 deferred context/Vulkan 规范 · 同步与内存可见性/fgiesen · Part 13(CS 与同步)(CC0)。

相关链接 - 上一篇:【GPU 架构剖析】(八)像素后端:fork/join、ROP 与那笔带宽账 - 下一篇(预告):【GPU 架构剖析】(十)计算着色器:波前、共享内存与三种屏障 - 核心参考:Fabian Giesen《A trip through the Graphics Pipeline 2011》Part 10 / Part 11 / Part 12 - 相关系列:技术博客页

With Best Wishes.

参考资料与可回溯出处

  1. Fabian Giesen《A trip through the Graphics Pipeline 2011》· 索引页(CC0)· Part 10(GS)/ Part 11(Stream-Out)/ Part 12(Tessellation)
  2. Vulkan 规范 · Tessellation
  3. Vulkan 规范 · Geometry Shading
  4. Vulkan 规范 · Vertex Post-Processing
  5. D3D11 Graphics Pipeline
  6. D3D11 · Stream-Output Stage
  7. NVIDIA · Introduction to Turing Mesh Shaders
  8. Wikimedia Commons:3D-Pipeline.svg(CC0)、Geometry-shader-pipeline-simple.png(CC BY-SA 4.0)、ZOTAC Gaming's GeForce RTX 50 Series Graphics Carda.jpg(CC BY-SA 4.0)、Intel 810 Chipset Digon3.JPG(CC BY-SA 3.0)

下篇预告:《GPU 架构剖析》系列进度:已完成【一】命令前端、【二】显存与地址空间、【三】着色器核、【六】光栅化、【七】Z 缓冲、【八】像素后端与 ROP,本篇【九】几何前端收束——接下来是【十】计算着色器:波前、共享内存与三种屏障。几何前端这条线的账已结清:GS 的二次装配与不可预知输出、SO 的回读延迟、细分的固定功能吞吐——下一站,这些"按图元/按补片工作"的能力将整体迁移到"通用并行计算"的模型里,由计算着色器接管;而所有写回路径的时序,则交给 ep11 的同步与资源生命周期。

(本篇完)