【Graphics Pipeline 2011】3D管线概览,顶点处理部分

原文链接

到现在为止,我们已经介绍了Draw Call指令从应用层发出后经过的多个driver层以及command processor处理逻辑,现在终于要开始介绍Draw Call的图形处理部分了,本文开始介绍顶点处理管线,不过在那之前还要先来介绍一些其他内容。

Have some Alphabet Soup!

图形管线包含多个stage,每个stage负责一项专项事务。下面先给出后面要介绍的各个stage的名称以及首字母的缩写,大部分都会跟D3D10/11保持一致:

  • IA — Input Assembler. 读取顶点/索引数据。

  • VS — Vertex shader. 对输入的顶点数据进行处理,并输出处理后的顶点数据

  • PA — Primitive Assembly. 将上个阶段输出的顶点数据组合成primitive向下传递。

  • HS — Hull shader; 接收patch primitives, 输出经过变换(或者不经过任何变换)后的patch control points,domain shader的输入数据以及其他用于tessellation的数据。

  • TS — Tessellator stage.构建tessellated后的顶点与连接数据。

  • DS — Domain shader; 以shaded control points,HS输出的额外数据,TS输出的tessellated位置作为输入并再次将之转换为顶点数据。

  • GS — Geometry shader;以primitive作为输入(可能还会有一些邻接信息,可选),输出不同的primitives数据(Also the primary hub for…)

  • SO — Stream-out. 将GS的输出(比如说变换后的primitives数据)输出到内存中的一个buffer中。

  • RS — Rasterizer. 对primitives进行光栅化。

  • PS — Pixel shader. 以插值后的顶点数据为输入,以像素颜色数据作为输出,可能还会将数据写入到UAVs (unordered access views).

  • OM — Output merger. 以PS输出的着色完成的像素作为输入,进行alpha blending处理,并将最终的结果写入到backbuffer中。

  • CS — Compute shader.这个Stage处于另外一条单独的管线中,以constant buffer + 线程ID作为输入,其输出结果将写入到buffer或者UAVs中。

下面给出不同的管线的stage执行路线: (IA, PA, RS 以及 OM 等stages这里省略了没有列出来,因为这些stage并没有对数据做实际上的处理,只是对数据进行了重排,起到的是一个胶水的作用。)

  1. VS→PS: 早期的编程管线,比如D3D9中管线的所有内容就是这两个stage了。同时这也是常规渲染图形管线中最重要的两个stage。

  2. VS→GS→PS: Geometry Shading (D3D10新增管线路径).

  3. VS→HS→TS→DS→PS, VS→HS→TS→DS→GS→PS: Tessellation (D3D11新增).

  4. VS→SO, VS→GS→SO, VS→HS→TS→DS→GS→SO: Stream-out 管线(针对是否需要tessellation,有多种配置).

  5. CS: Compute. D3D11新增.

上面对管线的路径进行了整理介绍,下面先从vertex shaders开始进行介绍。

Input Assembler stage

如果当前渲染的API调用的是一个带索引的batch,那么IA Stage要做的第一件事就是从IB中加载索引数据,否则就会按照一个标准索引buffer来进行绘制(0 1 2 3 4 …) 。带索引的batch的索引数据是从内存中获取得到的(当然并不是直接读取),IA Stage上会有一个数据缓存区用于保持顶点/索引数据的cache locality以提高访问执行效率。另外,这里还需要注意IB数据的读取操作(实际上D3D10+上的所有资源的获取)会需要经过bounds check(越界检测)。如果需要引用的元素数据超出了当前访问的IB的范围 (比如对一个只有5个索引的IB调用DrawIndexedIndexCount == 6 ) ,那么这些越界访问的结果返回的就是0,而这个数值是无意义的。相似的,我们可以用NULL IB来调用 DrawIndexed ,其表现跟使用空IB调用这个接口的表现一致。在D3D10+中,想要触发undefined逻辑,大家还要加倍努力才行。

索引数据有了之后,那么就可以从输入的vertex streams中获取需要的per-vertex或者per-instance数据(当前instance的ID对应的是另外一个计数器,索引对应的只是顶点数据,这里暂时不需要考虑instance ID的使用)。vertex stream中的数据都是按照预先定义好的数据layout排布的,这里有了索引就可以按照这个layout进行定位访问,并将返回的数据unpack成shader core所需要的float格式。不过,这个访问并不是立即完成的,硬件会将渲染完成的顶点数据塞入到一个缓存中,从而当某个顶点被多个面片共用的时候(这种情况显然是非常常见的),这个顶点不需要进行多次渲染,只需要从缓存中将数据取出来就可以了。

Vertex Caching and Shading

注意,这个section的内容有一部分是原文作者的一些猜测,这些猜测是基于当代GPU上的一些已知知识进行的,这些知识只是给了结果,但是没有给出原因跟过程,因此这里会向外做一些扩展性的猜测,当然这些猜测都是具有一定的合理性,且在通常情况下是行得通的,虽然跟实际情况可能会有些出入。

不管怎么样,在很长一段时间里(至少可以追溯到GPU的SM 3.0),VS & PS是使用不同的单元来执行的,因此对于性能trade-off(平衡)有着不同的考虑,顶点cache的概念其实比较简单:通常是用一个FIFO来存储少量的顶点数据(比如一二十个),这块空间能够存储下最差情况下的顶点属性数据,并使用一个顶点index作为tag。

之后就出现了unified shader架构。如果想要将原本不同的两类shader统一到一起,那么在设计上就必须要做折中。一方面,VS每帧处理的顶点数目大概是一百万,而另一方面,PS处理的像素数目则是二百三十万(1920x1200),如果算上一些其他的渲染(比如后处理)的话,那么这个数目还会更更多。想一想,在这场拉锯战中,到底谁占上风呢?

结果就是:老派的VS每次只处理一个顶点,而现在的unified shader架构的目标是最大化吞吐量(throughput)而非缩小时延,那么这里就需要按照大批次来进行处理(多大呢,目前这个数值大概是每批处理16~24个顶点)。

如果不考虑shading的效率,那么按照上述方法,在发出新的vertex shading load(即在cache中没有找到结果)之前,可能会遭遇16~64次cache miss。而FIFO结构也不支持将所有cache miss的vertex shading load打包到一起,之后一次性load。问题在于:如果希望一次性处理一整个批次的顶点,就意味着必须要等到所有顶点shading完成之后才能开始进入到triangle assembling阶段,在那之后,就会将一个批次的顶点数据(这里以32为例,下同)添加到FIFO的末尾,也就意味着需要将FIFO中老的顶点数据移除出去(FIFO尺寸限制导致)——而这些移除出去的顶点数据可能会是当前shading完成的顶点所从属的面片中的一个组成顶点,完蛋了~~显然我们这里就无法再依赖于FIFO中之前shading完成的顶点数据而需要再次进行渲染。此外,在上述这种情况下,FIFO到底需要多大才足够呢?如果一个批次包含32个顶点,那么FIFO至少要能装下这些数据,也就是需要32个entries,而实际上在这种情况下,老的shading完成的数据如果希望保留的话,那么这里就需要更大一点的,比如64个entries,这个真的太大了。另外,由于每个顶点cache的查询都需要对tag进行一次比对,这个过程是并行的,但是电量消耗却是实实在在的。还有,在发起一个shading vertex load到收到数据之间的这段时间在做什么呢?等待?这个过程可能会需要消耗数百个cycle,如果只是等待的话,就太浪费了。那么如果同时开始两个shading load呢?那么在这种情况下FIFO就必须要要包含至少64个entries,且我们无法依赖最后的64个entries来实现cache hit。此外,一个FIFO对应着多个shader cores这种机制也会有问题, Amdahl’s law 依然有效 – 将一个严格串行执行的组件塞入到一个并行管线中,那么这个新加入的组件大概率会成为整个管线的瓶颈。

总的来说,这套FIFO方案并不是非常的合适,好吧,将之抛开,再继续思考。我们需要的到底是什么呢?对一个合适的顶点数目的batch进行渲染,且尽量避免对同一个顶点进行多次渲染。

简单一点:为一个batch32个顶点数据预留足够的空间,同时预留出32个entries的cache tag空间。对于每个batch而言,从empty “cache”开始,这个时候所有的entries都是无效的。之后对于IB中的每个primitive数据,先在所有(这里的所有是指这个batch中的所有index?)的索引数据中进行查询,判断是否已经被渲染过了(怎么判断的?cache空间能存下所有顶点的渲染结果?),如果cache命中了,没关系,就不需要再次渲染了;而如果cache miss了,那么就在当前的batch中分配一个slot,并将这个新的索引添加到cache tag array中。一旦当前batch已经不能再继续塞下一个primitive了,那么就可以开始将整个patch发送到vertex shading阶段了,之后将cache tag array保存下来,之后从一个全新的empty cache开始继续下一个batch。各个batch之间的处理是相互独立的,也就是说,是局部cache,没有相邻batch之间的cache。

每个batch的执行过程,可能会占用一定的shader unit的时间(大概是数百个cycle),不过没关系,shader unit数目足够,因此完全可以使用不同的shader unit来处理不同的batch,非常的并行。最终所有的shader unit会返回执行完成后的结果,然后我们就可以用存储下来的cache tag以及原始的IB数据来对primitive进行组装,并将结果传输到管线的下一个stage(这个就是primitive assembly的工作内容)。

顺带,当这里提到的执行完成的结果,通常有两种选项:

  1. 专属的buffer数据

  2. 通用化的内存结构

返回的结果通常使用的是第一种方式,对应的是为特定顶点数据所设计的组织结构,不过后面GPU会将数据转换为第二种方式,这种方式对于后续的处理而言会更方便一点,因为其他的shader stages可能其实不是很关心顶点属性的组织结构之类的数据。

Update: 上面给出的是一张关于VS的数据流走向的手绘图

Shader Unit internals

简而言之:看起来跟反汇编(disassembled)HLSL的输出差不多 (fxc /dumpbin is your friend!)。这类代码适合计算机处理器直接运行,且具有非常丰富的文档,具体可以参考AMD/NVidia的一些会议陈述或者CUDA/Stream SDKs的一些官方文档。

这里是一些总结性内容:大多数ALU是围绕着FMAC (Floating Multiply-ACcumulate) 框架构建的,部分硬件支持为一些指令如reciprocal, reciprocal square root, log2, exp2, sin, cos, optimized 实现高吞吐量而非低时延的优化,低时延通常是通过大量的线程来遮掩,每个线程之只能占用少数几个寄存器,因此比较适合执行一些比较简单(straight-line)的代码,在一些具有分支的代码上的表现可能就不太好。

大多数的实现方式在整体框架上是非常一致的,不过还依然有着少量的区别。比如AMD硬件还依然坚持于HLSL/GLSL以及shader字节码中(即使现在逐渐移除了这些限定)所标明的4路SIMD框架,而NVidia则开始从4路SIMD框架专项标量指令(这些内容在网上都可以查到)。

更有意思的是不同的shader stage之间的区别。简单来说,少数的指令会存在区别,大多数的算术逻辑指令在各个stage上是完全一致的。部分结构(比如微分指令以及PS中的插值后的属性数据)只存在于部分stage上。大多数stage的区别在于输入跟输出数据的不同。

这里还有一点跟shader有关的大课题,那就是贴图采样,不过这个会放在后面介绍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352