A trip through the graphics pipeline 2011 Part 10(翻译)

时间:2023-03-09 17:37:24
A trip through the graphics pipeline 2011 Part 10(翻译)

之前的几篇翻译都烂尾了,这篇希望。。。。能好些,恩,还有往昔呢。

-------------------------------------------------------------

primitive 图元

tris   三角形

quad  栅格

------------------------------------------------------------

第十部分

译:minggoddess

欢迎回来。上一次,我们一头扎进了像素管线的最底端。这次,切换到管线的中间位置看一下伴随D3D10而来的大概是最引人注目扩展:几何着色器(Geometry Shaders)。

但首先呢,我会先讲一下我在本系列中如何分解图形管线,这与你从APIs角度看到的景象是多么的不同。

多重管线/对管线阶段的剖析

尽管第三部分已经提及过,因为它特别重要,所以我还要再重复一次:如果你去看比如,D3D10的文档,你就会发现D3D10管线的文档。你会发现有一份D3D10管线的示意图

包含了可能涉及到的各个阶段。这个D3D10管线里会包含几何着色器,,即使你没有设置过几何着色器,同样对输出流(Stream-Out)也是如此。在纯功能型的D3D10模型中,

几何着色器阶段总是在那里的;如果你没有设置几何着色器,它就会很简单(和没意思):数据仅仅从这里以不被修改的方式传递到下面的阶段(光栅化阶段/输出流)。

这是使用API的正确方法,但是却不是我们在本系列中了解这件事的好方法。我们更关心的是硬件是如何实现这种功能模型的呢。那么,到目前为止,我们看到的这两个着色器阶段

(VS和PS)是怎样的呢?对于顶点着色器(VS), 我们了解了输入装配器(IA--Input Assembler)。IA为着色准备了顶点数据块,然后把这一批次分发到一个着色单元。

着色单元处理了一会儿之后,我们得到了返回结果并把结果写到缓冲区(供面片装配用)。确保它们按照正确的顺序进行装配,然后把它们发到下一个管线阶段(剔除/裁剪等等)。

对于像素着色器(PS),我们接受到从光栅化阶段发过来的,准备被着色的栅格,把它们批量缓存起来,直到ps着色器单元空闲下来,可以接收新的批次时,把这一批次的数据分发

到一个着色单元。同样在着色器单元处理一会儿之后,我们的到处理结果并把结果写到缓冲区(供ROPs使用),确保它们是正确的顺序,然后做混合和延迟深度剔除(late Z)然后把结果

发到内存,听起来有点熟悉,是吗?

实际上,如果我们想让着色器单元帮我们做事情,就总是这样的情况:我们先是需要一个缓冲区,然后一些分发逻辑(实际上这部分逻辑很普遍,所以可以被所有着色器类型

所共享),然后我们扩展这一行为到一束着色器上进行并行运算。最终我们需要另一个缓冲区和有排序功能的单元来处理前面的运算结果。从着色器出来的运算结果有潜在乱序

的可能,需要把它转回API顺序。

我们已经了解了着色器单元(和着色器执行),并且了解了分发机制;实际上,我们已经了解了像素着色器(Pixel Shaders),它具有例如衍生计算,辅助像素,舍弃输出和属性插值

等特性。接下来就是计算着色器,在这之间我们不会遇到什么着色器单元的额外功能了。计算着色器有它们专门的缓冲和内核。所以在接下来的部分中,我不会涉及着色器单元,因为

不同着色器类型仅仅是进出数据的结构和解释不同。着色器部分不处理输入输出(算术,纹理采样)的地方是一样的,所以我也不讲相同的这部分了。

着色片元(Tris)的形状

我们一起看下几何着色器的输入输出缓冲。让我们从输入开始 。额。。,理论上这很合理--是我们写入顶点着色器的数据。或者,额。。并不完全是;几何着色器处理的是片元数据,

而不是分立的顶点,所以我们真正需要的是片元组装(Primitive Assemble -PA)的输出。注意,有多种方法可以处理这些。PA可以扩展片元输出(如果顶点被引用多次就复制顶点),

或者只给我们一个块顶点数据(我继续使用之前用过的32个顶点),把它们用小的索引缓冲关联起来(因为需要索引的数据块有32个顶点,所以我们每个索引只需要5位[译注:2的5次幂])

这两种方法都行;前者是用于PA之后的裁减剔除的自然输入格式,但是后者在运行GS(几何着色器)时需要少的多的缓冲空间,所以在这里我们采用第二种模型。

你需要操心GS的缓存空间的一个理由是它一下处理很大量的片元,因为它不仅支持纯线和三角形(每个片元2/3个顶点),也支持有邻接信息的线和三角形(每个片元4/6个顶点)。

D3D11增加了体形更大的片元,但一个GS仍然可以消耗高达32个控制点的片作为输入。拿复制顶点来讲,

困死了不翻了明天中午再翻些

  其实好多天过去了,并且刚刚翻译的一段mei保存,消失了。。。。

一个有16个控制点的面片,可以拥有高达16个矢量属性(D3D11下32个)?这是严重的内存浪费。所以我假设没有复制,而是采用索引顶点的方法。这样VS的输出,外加一个相对而言

比较小的索引缓存就成为一个批次片元的输入。

现在,每个片元上都有几何着色器了。对顶点着色器而言,我们需要收集一个批次的顶点,并且我们用简单的贪心算法来决定批次的大小。贪心算法在不把一个片元分到多个批次的情况下

包尽可能多的顶点进一个批次--足够公平。至于像素着色,我们从光栅化阶段得到足够多的栅格且把它们全部包进批次。几何着色器就有些不方便了,输入块被保证至少包含一块完整的

片元,很有可能是多个--但是除此之外,块里面片元的数量完全是取决于顶点缓冲的命中率的。如果命中率高并且我们使用三角形片元,我们大概可以得到40-43个;如果我们使用包含邻接

信息的三角形,不走运的话就只能得到5个片元了。

当然,我们在这里可以从不同的输入块里收集片元, 但那就有点尴尬了。现在我们需要为一个GS批次保留多个输入块和索引缓存,如果一个批次涉及多个索引缓存就意味着那个批次里的每个

片元都要知道去哪里找索引和顶点数据。这就需要更多存储空间,更多管理,更多开销。很恶心。并且即使你使用两个输入块,如果顶点缓存的命中低的话那利用率仍然很糟糕。你可以支持

更多的输入块,但那会吃掉内存--记住,你还需要空间用于几何输出(我待会儿讲)。

所以这是我们遇到的第一个障碍:我们用VS可以大概选择我们目标批次的大小,并且为了让我们在PA(Primitive Assemble)阶段好过一些(这里是指GS,待会讲到的HS也是这样),

我们并不总是产生最大的批次。对于PS, 我们总是对栅格着色,即使特别小的三角形也总是会命中多个栅格,所以我们可以得到一个合适的栅格数量与三角形数量的比率。但是

对于GS,我们对于管线的任何一个终点都没有完全的控制权(因为我们在中间阶段!),并且我们需要每个图元的多个输入顶点(与之相反的是一个输入三角形的多个栅格),所以

缓存很多输入开销很大(在内存和管理方面的开销)。

在这一阶段,你基本上可以想合并多少输入块就得到多少来获得几何着色图元的一个块;由于内存的限制块的数量会很低(超过4块会让我很吃惊的)。根据你认为GS有多重要,块的数量

有可能只是选为1。例如,根本不合并输入块。对于三角形这并不好,对于那种有更多顶点的片元就真的很糟糕了,但是如果你用GS的目的主要是把点扩展成栅格(点精灵)或者是偶尔出现的

立方体阴影贴图(用Viewport索引数组合Rendertarget索引--我一会就会讲到),这就不太称得上一个问题了。

GS 输出:那里也没有玫瑰园

在输出那面是什么情况?这比普通的VS数据流更为复杂。实际上是复杂的多;一个VS输出一样东西(着色后的顶点),顶点在着色前后的比率是1:1。一个GS输出大量顶点,(可以达到编译时

确定的最大值),至于在D3D11,可以有多输出流--然而,输出流的最大值可以在管线接下来的阶段送出,这就是我现在在谈的路径。GS数据的其它目的地将在接下来的章节里谈及。

一个GS产生大小可变的输出,但是它需要运行在有限的内存需求上(除其它事项外,可用的内存缓冲区数量决定了GS可以并行处理的图元数量),这就是为什么输出顶点数量的最大值在编译时确定。

这个最大值(和写入的输出属性的数量)决定了需要分配多少缓冲空间,从而间接决定了调用的并行GS数量;如果数量太小,延迟不能被完全隐藏,GS就会以一定时间比例暂停。

还需要注意的是GS的输入的是图元(例如,点,线,三角形和面片,可选的邻接信息),但是输出的是顶点--即使我们把图元发到下一阶段做光栅化!如果输出图元的类型是点,这是微不足道的。

但是对于线和三角形来说,我们需要重新组装顶点回图元。这是通过使输出顶点形成一条线或者三角形带来分别进行处理的。这种方式很好地处理了三个大概是最重要的案例:单一的线,三角形,

或者四边形。如果GS试着做一些实际的挤压或者是产生某些比较复杂的几何图形就不那么方便了,这就需要几个“重启带”标志(这可以归结为每个顶点上的一个位来决定当前带继续还是新带的开始)

那么为什么会有限制?在API层面,看起来是非常武断的--为什么GS不能只输出一个顶点列表外加一个小的顶点索引?

答案可以归结为两个字(在汉语里是四个。。。):图元装配。这就是我们在这里做的--拿一些顶点把它们装配满一个图元然后送到管线下个阶段。但我们已经在这条数据通路上使用了这个功能块了,

就在GS前面。所以对GS来说,我们需要第二次图元装配阶段,我们希望这阶段可以很简单。装配三角形确实很简单:三角形总是按照输出缓存的里面排列的顺序三个三个的排下去。换句话说,

采用带并没有比按理说最简单的图元(没有索引的线和三角形)产生更为显著的复杂度增加,但他们仍然为像四边形这样典型的图元节省了输出缓冲空间(因此提供了更高潜在并行的可能性)。

再谈API顺序

但是,这里有几个问题:在常规顶点着色的时候,我们很明确的知道一个批次里有多少图元,并且它们在哪,即使在被着色的顶点到达PA缓存之前--这些在我们设置被着色的批次时就得到确定了。

如果我们,比如有多个单元来进行拣选/裁剪/三角形组装,它们都可以并行进行;它们知道去哪里得到顶点数据,并且它们可以提前知道三角形要有的序号,这样它们就可以被有序放置了。

对于GS, 在我们得到输出之前,我们一般不知道我们将会产生多少图元--实际上,我们可能一个也不生成!但是我们仍然需要注意API顺序:首先所有的图元从GS的0号调用生成,然后所有的图元

产生于1号调用,这样,贯穿到批次的末尾(当然批次们需要被和VS一样按序处理)。所以对GS来说,我们得到返回结果,我们首先需要扫描输出数据来决定整个图元从哪里开始。只有在这以后,

我们才可以开始拣选,裁剪和三角形安装(可能并行)更多额外的工作!

VPAI 和RTAI

这是两个和GS一起加进来的功能,它们实际上不会影响GS的执行,但是确实会对接下来的处理流产生影响,所以我认为我们需要在这里提一下:ViewPort索引数组(VPAI作为简称)和

RenderTarget索引数组(RTAI)。先讲RTAI,因为它解释起来更容易一些:正如你希望知道的那样,D3D11增加了对纹理阵列的支持。嗯哼,RTAI给与了渲染道纹理阵列的支持:你可以把

纹理阵列作为渲染目标(RenderTarget),然后在GS里,你可以为每个图元选择应该去哪个索引。注意因为GS是写到顶点但不是图元上的,,我们需要对一个单独的顶点选择对每个图元的RTAI

(VPAI也是如此);这总是个“领头的顶点”,例如第一个属于图元的那个顶点。使用RTAI的一个例子是在一个通道里渲染立方体贴图:GS决定立方体的每个面应该被送到哪个片元(可能其中几个),

VPAI是一个正交功能,允许你设置多个viewport和裁剪矩形(高达15个),然后来决定每个图元用哪个viewport。这可以被用来在一个通道渲染CSM(Cascaded Shadow Map)的多个级联,

并且它可以和RTAI结合。

至于说,这两个功能都没有对GS的处理产生显著影响--它们只是被附加到图元上之后被使用的额外数据:VPAI在视口变换时被使用,而RTAI在之后像素着色器一路在用。

到目前为止的总结

好,所以在输入端有些麻烦--我们不能全部选择输入数据的格式,所以需要用于输入数据的额外缓冲,

感觉我有大半个月没动这篇翻译了。。

    没想到之前翻译了那么多,原来就剩两节了,早知道我就早点接着翻译了。。。

这样,就会产生可变数量的输入图元,虽然我们并不需要把这些图元分成合适的大批次。在输出端呢,我们又在组装可变数量的图元,不必知道GS会产生多少图元

(尽管对于有些GS来说,我们可以从编译代码静态地来确定这一点,例如因为所有顶点要么在控制流外面产生,要么在一个已知迭代数量并且没有提前输出的循环之内)

这就不得不在送往三角形装配阶段之前,花点时间来解析输出。

如果这听起来比只有VS的状况复杂,那是因为确实如此。这就是为什么我上面要提到:认为GS一直在运行是错误的--即使只有一个非常简单的GS除了在两个缓冲阶段传数据什么也不做,

还会有额外的一轮图元装配可能以极低的利用率在着色单元上执行。所有这些都有代价,并且代价趋于累积:我在D3D硬件非常新的时候检查过这一点,还有AMD和NVidia硬件上。一个

纯粹传递作用的GS比没有GS慢了3到7倍(这是在一个几何受限的情景下)。我没有在更新的硬件上重新测了;我假设现在情况好转一些(那是实现GS的第一代,在实现它们的第一代GPU上

功能的表现通常不是很好),但是观点仍然成立:即便只是用GS传递数据,那里什么也不做,都会产生可观的开销。

让GS按顺序产生带状图元也不会有帮助; 对一个顶点着色器来说,每个顶点会调用它一次来读和写一个顶点(很好)。对一个GS来说,

代码写完了,遇到写TDD的任务,感觉还是翻译这个难度低些。。continue

虽然我们最终可能只有一个批次11个GS运行(因为在输入缓存没有足够的图元),他们中的每一个都会运行足够长的时间,并产生大概八个顶点输出。在低利用率的情况下,这需要很长时间!

(记住我们在每个批次需要介于16到64之间的某个数量的独立任务分配到着色器单元)。如果GS主要由循环构成,这就很烦人了--例如,我在RTAI提及的渲染到立方体贴图的例子,我们

在一个立方体里为六个面循环6次,检查一个三角形在那个面是否可见,如果是这样的话就输出这个三角形。对这六个面的计算是相当独立的,如果可能的话,我们希望可以平行计算。

附赠:GS实例化

嗯哼,涉及GS实例化就需要提到D3D11的另一个新特性,可惜相关文档很少(我不确定在SDK里面有没有好点的例子)。虽然,解释起来是很简单的:对每个输入图元,GS并不是只运行一次

而是很多次(这是一个在编译阶段可以决定的静态数值)。基本上这相当于把整个着色器包在下面这循环里

for(int i = 0; i < N; i++)

{

  //...大括号分行 欧耶

}

每个输入图元产生多个GS调用,在着色器外处理这循环。这种方式可以帮我们产生更大的批次因而提升利用率。这里的i作为一个系统产生的值被送到着色器(在D3D11,由SV_GSInstanceID定义)。

所以如果你有一个那样的GS,就去掉外面的循环,声明一个[instances(N)]用合适的语义声明i。这样就会快很多--对大规模并行机分配独立工作的魔法!

不管怎么说,这就是几何着色器.我略过了输出流,但这个阶段已经很长了,何况输出流是个很大的话题(足以独立于GS!)。下个阶段会更精细,到时你就知道了!

----------------------------------------------------------------------------------------

我终于翻译完了,这要感谢往昔翻译其它部分一直很快,这种鞭策。。。