原文:《A trip through the Graphics Pipeline 2011》
此时,咱们一路上经过多个驱动层和命令处理器将draw call从应用程序发送过来。最后终于要作图形处理了。最后一部分,来看一下顶点管线。不过在开始以前…
一些名词
咱们如今所在的3D管线依次由若干阶段构成,每一个阶段都有特殊功能。来给这些将要谈到的阶段命下名——基本上是按照D3D10/11的命名结构——加上相应的缩写。咱们将在旅程的最终部分看到他们,可是还须要一些时间才能所有看到——我写了一个大纲,用一句话总结了每一个阶段都作了什么。
- IA——输入组合器(Input Assembler)。读索引和顶点数据。
- VS——顶点着色器(Vertex Shader)。获取输入的顶点数据,写入下一个阶段用到的顶点数据。
- PA——图元装配(Primitive Assimbly)。读取顶点数据,组装成图元继续传递。
- HS——外壳着色器(Hull Shader)。接收补丁图元,将变换过的(或者没变换的)修补控制点写入域着色器(Domain Shader),加上驱动细分的额外数据。
- TS——细分阶段(Tessellator Stage)。创造顶点并连通细分的直线或三角面。
- DS——域着色器(Domain Shader)。取出已着色着色的控制点,HS中的额外数据,以及TS中的细分位置,并把他们再次变换成顶点。
- GS——几何着色器(Geometry Shader)。输入图元,选择邻接信息,而后输出成不一样的图元。
- SO——输出流(Stream-Out)。将GS输出的(如变换后的图元)写入内存缓冲。
- RS——光栅器(Rasterizer)。光栅化图形。
- PS——像素着色器(Pixel Shader)。将通过插值的顶点数据输出像素颜色。还能写入UAV(无序访问视图 Unordered Access View)。
- OM——输出混合器(Output Merger)。从PS获得着色后的像素,作半透混合处理并把它们这回后缓冲区。
- CS——计算着色器(Compute Shader)。本身有一套独立的管线。输入只有常量缓冲和线程ID。能够写入缓冲和UAV。
如今全部的都交待完了,这里列出了多种数据流程,我来按顺序说明一下(这里不讲IA、PA、RS、OM阶段,他们和主题无关,他们不对数据作任何处理,仅仅重组数据——就像是粘合剂)
- VS→PS:历史悠久的管线。在D3D9时代,这就是所有管线了。目前为止仍然是常规渲染的重要流程。我从头到位走一遍,而后再回头换一种更丰富的流程。
- VS→GS→PS:几何着色(D3D10中新增)
- VS→HS→TS→DS→PS,VS→HS→TS→DS→GS→PS:曲面细分(D3D11中新增)
- VS→SO,VS→GS→SO,VS→HS→TS→DS→GS→SO:输出流(有/无 曲面细分)
- CS:计算(D3D11中新增)
如今你知道接下来要发生什么了,让咱们从Vertex Shader开始吧。
输入组合器阶段(Input Assembler Stage)
这里发生的第一件事是从Index Buffer中载入索引——若是它是个包含索引的渲染批次。若是不是的话,就当成是序号一致的Index Buffer(0 1 2 3 4……)做为索引来使用。若是有Index Buffer,它的内容可不是从内存读取的,IA阶段一般经过一个数据缓存来访问Index/Vertex Buffer。还要注意,读取到的Index Buffer(实际上,在D3D10以上的全部资源访问都是这样)是作了边界检查的;若是你引用了原始Index Buffer以外的元素(例如,在只有5个索引的Index Buffer中,执行DrawIndexed函数,IndexCount参数设为6)全部的越界读取都将返回0。这么作(这种特殊状况)虽然彻底没用,可是包含了必定意义。一样,你能够用一个NULL Index Buffer集合调用DrawIndexed——若是你的Index Buffer长度设为0,这么作也是同样的,全部读取都算越界,因此也返回0。在D3D10以上,你须要对未定义的东东多作一些处理:)
一旦有了索引,咱们就有了须要从顶点数据流中读取的预处理顶点(pre-vertex)和预处理实例(pre-instance)数据(当前这个阶段的实例ID仅仅是一个简单的计数器)。这很简单——咱们已经声明了数据布局(data layout);从缓存/内存中读取它,而且解包成浮点格式做为shader内核的输入数据。然而,读取不是当即完成的;硬件使用了一个着色顶点的缓存,以致于顶点能够被多个三角形引用(在常规的闭包mesh中,每一个顶点都被6个三角形引用),就不用每次都重复渲染同一个顶点了——咱们仅引用已经着色后的数据。
顶点缓存和着色
注意:本段部份内容包含了猜想,都是依据专家给出的关于现代GPU的评论。可是仅告诉了我是什么,却没解释缘由,全部这块有一些是推断的。还有,我仅是猜想了一些细节。就是说,我不知道的就不会在这里彻底阐述了——我描述的东西都是我认为靠谱可信的,我还不能保证明际在硬件里确实这么实现的,没准会漏过一些技巧和细节。
长久以来(直到shader model 3.0的时代),vertex & pixel shader都使用不一样的处理单元实现,它们有各自不一样性能权衡和顶点缓存,是很简单的东西:通常只是 个包含少许顶点的FIFO,对于最糟糕的输出属性也保留了足够空间,各自标记了顶点和索引。很简单吧。
以后,Unified Shader出现了。若是在两种类型的Shader中统一处理不一样事物,这种设计必然要作出妥协让步。换句话说,Vertex Shader一般一帧可能达到1百万个顶点,而Pixel Shader在1920x1200分辨率上填充全屏 一帧至少 须要二百三十万个像素——还会有更多的渲染内容。那么猜一下那个处理单元会拖后腿?
有一个解决办法:用大量的统一着色单元(unified shader unit)替换掉每次只渲染若干个顶点的旧vertex shader uint来最大化吞吐量,避免延迟,于是就能处理大量批次的渲染工做(有多大?目前这个数貌似是一个批次 处理16~64个着色顶点)。
若是不想下降渲染效率,在你执行一次顶点着色负载(vertex shading load)以前,会有16~64次顶点cache miss。可是整个FIFO实际上并非按照这个想法批处理顶点cache miss,且一口气渲染完他们。由于问题在于:若是你一次性渲染整个批次的顶点,就只能在顶点着色以后才能开始组装三角形。而此时,你刚刚才添加了一整个顶点批次(好比这里是32个)到FIFO的队尾,就意味着如今有32个旧的顶点被挤出队列了——可是这32个顶点中的每一个顶点,可能已经命中了,在当前批次里咱们正在组装的三角形的顶点缓存。哦!那就行不通了。很明显,咱们实际上不能在FIFO里统计32个旧的顶点做为顶点缓存命中(vertex cache hits),由于正在引用的顶点已经不在了!那咱们该须要多大的FIFO?若是咱们正在渲染的一个批次里有32个顶点,就至少 须要32个条目大的空间,可是由于咱们不能使用32个旧的条目(由于咱们要移出他们),意味着实际上每一个批次都是用的一个空的FIFO。那就让它大一点,64个条目呢?至关大了吧。注意,每次顶点缓存查找要涉及到比较全部FIFO中的标记(顶点序号)——这彻底是并行的,但也很耗电;咱们在这里用一个彻底关联缓存来高效率实现它。还有,在派发执行32个顶点着色负载和收到结果之间里作什么呢——只能是等待吗?着色要花费几百个cycle,等待可不是个好主意!或许应该同事有两个着色负载,并行执行?可是如今咱们的FIFO须要至少64个条目长度,而且咱们不能统计上次的64个条目做为顶点命中,由于当咱们收到结果的时候,他们都将被移出队列。而且,一个FIFO对应大量的shader内核吗?Amdahl定律——将一系列彻底串行化的份量 (不能并行化)加入到管线里,必然会产生性能瓶颈。
整个FIFO真的不适合这个环境,因此,好吧,咱们只能抛弃他了。回到画板上来。咱们实际想要作什么?拿到一个大小合适的顶点批次来渲染,而且不渲染没必要要的顶点。
那么,好吧,简单点:为32个顶点(1个批次)保留足够大的缓存空间,而且一样留出32个条目的标记的缓存空间。从一个空的缓冲开始,例如全部条目都是非法的。对于index buffer中的每一个图元,从全部的顶点中查找一次;若是他命中缓存,那最好了。若是没命中,在当前的批次里分配一个插槽而且添加一个新的索引到 缓存标记数组(the cache tag array)里。当咱们没有足够剩余空间再添加新的图元时,派发所有的顶点着色批次,保存缓存标记数组(例如刚刚着色过得32个顶点索引),而且再次从一个空的缓存开始设置下一个批次——确保渲染批次都是彻底独立的。
每一个批次都将占用shader unit一段时间(可能至少几百cycles!)。可是这不会有问题,由于咱们有足够的shader uint——仅须要选择一个不一样的shader unit来执行每一个批次!咱们最终可以高效并行的获得返回结果。在这点上咱们可使用保存的缓存标记和袁术index buffer数据来组装图元,发送到管线里(这就是我后面部分要讲到的“图元组装”的概念)。
顺便说下,我刚才说的“获得返回结果”,是什么意思呢?他们在哪结束的? 主要有两个选项:1. 特定的缓存里 或 2. 一些通用的缓存/临时内存。过去通常都用选项1,在 顶点数据周围用一个固定组织结构设计的缓存(每一个顶点有16个float4向量的属性空间,之类的等等),可是后来GPU开始朝着选项2发展,仅仅是内存。这很灵活,一个重要的好处是你 能够在别的shader阶段也使用这个内存,然而举例来讲,特定的顶点缓存对于像素着色或者计算管理是没什么用的。
目前为止所描述的顶点着色数据流程
Shader Unit内部
简而言之:这就是你想要看到的HLSL编译器的 反汇编输出结果(fxc/dumpbin)。它只是个擅长执行这种代码的处理器,在硬件中负责将 某些shader代码编译 成近似的shader字节码。与我以前谈论的东西不一样,这块内容有丰富的资料——若是你感兴趣的话,能够从AMD和NVidia找到一些会议演示文档,或者阅读一下CUDA/Stream SDK的文档。
概括一下:高速ALU主要布置在FMAC(浮点乘法累加 Floating Multiply-Accumulate)单元周围,某些硬件支持倒数,倒数平方根,log2,exp2,sin,cos运算,高吞吐量和高密度无延迟的优化,运行大量的线程来下降上述延迟,每一个线程有不多的寄存器(由于正在运行的线程太多了!),很是适合执行直接式代码(无循环的),不适合运行有分支的(特别是不连贯的代码)。
上述的一般就是全部的实现。还有一些区别;AMD的硬件一般直接用4-位宽的SIMD表示HLSL/GLSL以及shader自节码(尽管它们后来不用了),而NVidia不久以前打算将4-通路SIMD转变成标量指令(scalar instruction)。再次提醒,全部的这些在web上都有资料。
尾注
我再次免责声明“顶点缓存与着色”一节:其中有一部分是个人猜想,因此讲的有点不清楚。
我也不打算讲如何写缓存的细节,这部分是托管的;缓存大小取决于处理批次的大小和想要输出的顶点属性。缓存大小和管理对于性能是很重要的,但我不在这里详细解释,我也不想解释;虽然颇有趣,可是这部份内容与谈论的硬件很是特殊,不用深刻了解。