“When we get the final hareware, the performance is just going to skyrocket” ——J.Allard(这句话我思考后也不知如何翻译好,那就算了吧,毕竟也没人等着看了)
尽管图形硬件的发展进化速度非常快,仍然有很多通用的概念和架构广泛的在硬件设计中使用。这一章的目的是提供一个对于图形硬件系统各种硬件单元的理解,以及他们之间如何相互联系。这本书里其他的章节讨论了这些硬件在具体的算法下如何被使用。在这里,我们会呈现这些硬件本身。我们从如何描述如何光栅化线和三角形开始,随后是展示GPU如何处理大量计算能力,以及如何进行任务调度,包括处理延迟和占有。然后我们讨论内存系统,缓存,压缩,颜色缓冲,以及一切和GPU中深度系统有关的东西。接下来是贴图系统的细节,再后是GPU的架构类型。Section23.10中会学习三个不同的架构案例,最后,会简要的讨论一些光追的架构。
23.1 光栅化
任意GPU里一个重要的feature就是绘制lines & triangles的速度。如同Section2.4里描述的,光栅化是由三角形setup和三角形遍历组成的。另外,我们将会描述三角形内部如何给attributes做插值,这点和三角形遍历紧密相关。我们会以保守光栅(conservation rasterization)结束,这种架构是标准光栅化的一个拓展。(dx12有应用)
假设pixel的中心位置是(x+0.5,y+0.5),x范围[0, W-1] y范围[0,H-1],并且都为整数。WxH是屏幕分辨率,例如3840x2160。设未经过变换的顶点为vi,i属{0 1 2},变换后(包括投影变换但是还没除w)的顶点qi=Mvi.那么二维的屏幕空间坐标pi = ((qix/qiw + 1)w/2, (qiy/qiw +1)H/2), 执行了透视除w,值被调整匹配屏幕分辨率。这个setup过程入图Figure23.1所示。可以看到,像素格子划分为2x2的像素组,叫做quads。为了能够计算倒数,以此可以计算对应的texture LOD(Section 23.8),对于至少有一个像素点位于三角形内部的quad来说,quads上所有的像素都会进行着色计算(Section 3.8讨论了)。这是大多数GPU的核心设计(不说全部显得严谨),并影响了后续的诸多阶段。三角形越小,落在三角形外的pixel(称作helper pixel)/三角形内的pixel的比例就越大。这个相关性意味着在进行pixel着色时,越小的三角形越耗性能(基于三角形面积定义小)。最坏的情况就是一个三角形就覆盖一个pixel,这就意味着有三个helper pixels浪费了。helper pixel的数量有时被称为quad overshading,四元组额外着色。
Figure 23.1. 一个三角形,有三个屏幕空间的二维顶点P0,P1,P2。屏幕的尺寸是16x8个像素。注意这里一个在(x,y)位置的像素中心点是(x+0.5,y+0.5)。红色的底边的法线向量(缩放为了0.25倍)。只有绿色的像素才是在三角形内部的。黄色的辅助像素属于至少有一个像素点位于三角形之内的quad(quad尺寸2x2像素),并且这些辅助像素的采样点(即像素中心)都位于三角形之外。辅助像素需要用于通过有限差分(finite differences)计算导数(derivatives)。
去判断一个pixel的中心点或者任意采样位置,是否位于三角形内部,硬件使用三角形每条边的函数方程edge function去做判断。这些基于直线方程。例如
23.1
这里n是一个向量,也叫作边的法线,垂直于这条边。p是线上的一个点。这种方程也可以写作ax+by+c=0的形式。下一步,我们会通过点p0 p1来获得边的方程e2(x,y)。边的向量是p1-p0,所以normal就是这个边逆时针转90°。
例如n2 = (-(p1y-p0y),p1x-p0x),这个n2指向三角形的内部,如图Figure 23.1所示。通过带入n2和p0进入Equation 23.1,e2(x,y)变成了
23.2
对于恰好落在一条边上的点(x,y)来说,我们有e(x,y) = 0。指向三角形内部的法线意味着,位于法线指向的这一侧里的点都有e(x,y) > 0。这条边就把空间分为两份,e(x,y)>0有时也叫做正半边,e(x,y)<0叫做负半边。这些性质可以被用于决定一个点是否在三角形内部。把三角形的边称作Ei,i属于{0,1,2}。如果一个采样点(x,y)落在三角形内部或者边上,那么对于所有的i,都有Ei(x,y)≥0。
图形API标准经常会要求屏幕空间的floating格式顶点坐标转化为fixed格式坐标。这一点被强制执行为的是用一种统一的方式定义tie-breaker rule(这里意译做边缘条件比较合适)。这样也可以让一个采样是否位于内部的测试更高效。Pix和Piy都可以被存储在1.14.8bits(共23个bits,格式分布为1-14-8)内,例如,一个符号位,14个整数位,8个小数位。这种情况下,一个像素内x和y分别有2的8次方个位置可以分布,以及整数位的范围可以是[-(214-1),214-1]。实际中,这个小操作(snapping)在边的公式计算前就做好了。
Edge function的另一个重要的特点是他们的递增性。假设我们在某个特定点计算公式的值(x,y) = (xi+0.5,yi+0.5),(xi,yi)是坐标为整数的像素点,例如,我们已经计算好了e(x,y)=ax+by+c。为了计算右边的像素,比如我们想知道(x+1,y)的值,可以写作:
23.3
这就是edge function在当前点的求解值,就是e(x,y)加了个a。类似的推导也可以应用于y方向。这些性质经常被用于快速的计算一个小tile里所有pixels对于三个edge equation的值。例如8x8的像素,去戳出来一个mask,其中每个像素占一个bit,表示这个像素在三角形内部还是外面。后续章节会解释这种分层遍历。
对于理解正好位于边上的情况,或者顶点正好位于像素中心点的情况也很重要。举例来说,假设两个三角形共享一条边,并且这条边穿过一个像素的中心。那么这个像素应该属于第一个三角形还是第二个,或者两个都属于?从效率的角度出发,两个都要的情况是错的因为这个像素会先被第一个写入值,随后被第二个覆盖。对于这个问题,经常会使用边界条件(tie-breaker rule平局制胜规则),这里我们介绍DirectX里使用的top-left规则。对于ei(x,y)的像素点,i属于{0 1 2},都被认为在内部。Top-left规则来一条边穿过中心点时使用。这像素点中心如果是上或者左的边,那么就算作在内部。一个边被认为top edge如果它是水平的并且其他边都在它之下。一个边被认为是左侧的边如果它不是水平的并且位于三角形的左边,这也意味着可能出现2条left edge。判定一个边是top还是left很简单。一个top edge会有a=0(水平)以及b<0,left edge则会a>0。整个判断点是否位于三角形内的测试过程也叫作inside test。
我们还没有解释线(line)是如何被遍历的。一般来说,一条线可以被渲染成为一个长方形,一个像素宽的范围,既可以用两个三角形来表示,也可以用额外的一种edge equation来表示这个区域。这种设计的好处是用于edge equation的硬件也可以用于线。点(point)可以被当做四边形来绘制。
为了提升效率,经常会使用分层的方式来遍历三角形。典型做法是,硬件计算屏幕空间里顶点的包围盒,然后判断哪个tile在包围盒内部并且和这个三角形重叠。决定一个tile是否在一条边之外,可以用Section 22.10.1里二维版本的AABB/Plane测试技术。Figure 23.2显示出了通用的规则。要把这个应用于分块的三角形遍历(tiled triangle traversal),可以首先判断tile的哪个角应该在遍历开始之前进行测试。对于所有tile来说,一个edge都对应同一个tile corner,因为最近的tile corner只取决于边的法线。对于这些提前定好的corners来计算edge equations,如果这个corner在edge之外,那么整个tile就都在外面了,硬件也不需要执行任何的逐像素的内部测试,对于这个tile。为了移动到相邻的tile,这个上面描述的这个递增的性质可以用于每个tile。例如,水平向右移动8个像素,那么只需要加8a即刻。
Figure 23.2. 一个edge functio的负半边,e(x,y) < 0,经常被认为是处于三角形的外部的。这里,4x4像素大小的tile投射到edge的法线之上。只有黑色圈圈的corner需要被拿去对这条边进行测试,所以这个点投影到法线n上是最大的。所以我们可以得出结论,这个tile位于三角形之外。
通过适当的tile/edge相交测试,分层遍历一个三角形就完全有可能了。如图23.3所示。Tiles也需要以某种顺序进行遍历,可以使用zigzag(z字型顺序)顺序或者其他的空间填充曲线,这些都是为了增强遍历顺序的连续性。如果需要,可以在按层遍历中增加额外的层级。例如,可以先访问16x16的tiles,然后对于这一步里每个检测到三角形重叠的,再进行4x4的subtiles测试。
Figure 23.3. 执行4x4个像素tile尺寸的按tile遍历时可以用到的一种遍历顺序。本例中,遍历从左上角开始,向右遍历。第一排每一个tiles都和三角形重叠,尽管右上角那个并没有合适的像素。随后遍历正下方那个(右下),完全位于三角形外部的那个,所以这时完全不需要进行逐像素inside test。然后继续向左遍历,此时的连续两个2tile都和三角形重叠,但是左下角的位于三角形之外。(注:这里一个quad是4x4,一个tile四个quad)
按tile遍历的主要优点,就是采取一种扫描线的顺序使得像素会以一种连续的方式进行处理,这也会使得texels访问变得更加连续。还有个优点是更好的使用局部性,当访问颜色或者深度缓存的时候。想一下,例如一个大三角形在进行扫描线遍历时。Texels被cache起来了,最近访问过的texels会留在cache里等待复用。假设采样贴图时使用了mipmapping,这就更加增多了texels的复用情况(需要一直求ddxddy)。如果我们按照扫描线顺序访问像素,一开始的像素极可能已经从cache中剔除了,当执行到最后时。由于复用cache里的texels会更高效,相比于一直需要从存储里读取,三角形常常分tile处理。这给纹理采样,深度缓冲和颜色缓冲也提供了很大的便利。事实上,纹理,深度缓冲,颜色缓冲也存储在tile,基于同样的理由。这些会在Section 23.4里讨论。
在开始三角形遍历之前,GPUs一般会执行一个三角形设置的阶段。这个阶段是为了计算出来一些per triangle的常数,这样后面的遍历也可以更高效。例如,edge equation里的各项系数,ai bi ci, i属于{0 1 2},在这里只需要一次,后面就可以在遍历这个三角形的时候一直使用了。三角形设置还要负责顶点插值里的一些常数计算(Section 23.1.1)。随着本书讨论继续,我们还会讨论其他的在设置阶段只需要计算一次的constants。
不可避免的,裁剪会在三角形设置之前完成,因为裁剪可能会产生更多的三角形。根据View volume裁剪一个三角形是非常昂贵的操作,所以GPU会在非必要的时候避免这个操作。近平面的裁剪总是需要的,并且这可能产生一两个三角形。对于屏幕边缘,大多数GPU采用保护带裁剪(guard-band clipping),一个可以避免复杂的全部裁剪的方案。这个算法在图23.4里可视化出来。
Figure 23.4. Guard-bands尝试去避免全体裁剪(full clipping)。假设guard-band区域是对于x和y有±16k个像素的大小。中间的屏幕大概是6500x4900个像素,这表示出这些三角形非常的具大。底部这两个绿色的三角形在三角形设置阶段或者更早的阶段被剔除了。常见情况是中部的蓝色三角形,和屏幕有交集并且全部位于guard-band里面。不需要执行全部裁剪操作,因为只有可见的这部分tiles会被处理。红色的三角形部分在guard-band之外并且和屏幕区域相交,所以执行裁剪。注意右侧的那个三角形被裁剪成了2个三角形。
23.1.1 Interpolation 插值
在Section 22.8.1里,重心点坐标是作为计算ray和三角形相交时的一个副产品。对任何per-vertex的attributes,ai,i属于{0 1 2},可以通过重心点(u,v)进行如下插值
a(u,v)是坐标(u,v)在三角形内插值的attribute。重心坐标的定义是
Ai是Figure 23.5里subtriangles的面积。第三个坐标w=A0/(A0+A1+A2)也是定义的一部分,u+v+w=1,则w=1-u-v。我们可以用1-u-v项来代替w。
Edge equation在公式23.2中可以用edge的normal来表述,n2=(a2,b2),如下
p=(x,y)。根据点乘的定义,公式可以重写为如下形式
α是n2和p-p0之间的夹角。b=||n2||等于边p'0p'1的长度,因为n2是这个边旋转90度。第二项的几何意义是,||p-p0||Cosα,是把(p-p0)投射在n2上得到的向量的长度,这个长度也正是面积为A2的subtriangle的高,h。这在Figure23.5的右边显示。值得注意的是,我们有e2(p) =||n2|| ||p − p0|| cos α = bh = 2A2,这非常棒因为我们需要subtriangles的面积来计算重心坐标。这意味着
Figure 23.5. 左边:一个三角形,顶点有标量attributes(a0 a1 a2)。点p的重心坐标和有符号面积(A1 A2 A0)成比例。中间的:显示重心坐标(u,v)在三角形内部如何变化。右侧:n2的长度就是边p0p1逆时针转90度。A2的面积就是bh/2.
三角形计算阶段经常会计算 1、(A0 + A1 + A2) ,因为三角形的面积是不变的,这就不用每个像素都做除法了。所以,当我们用edge equation来遍历三角形,我们可以得到Equation 23.8里的所有项通过计算inside test。这对于插值depth很有用,或者对于正交投影。但是对于透视投影,中心坐标不会得到想要的结果,如Figure 23.6所示。
透视正确的重心点需要逐像素除法。求导过程这里略过,取而代之的是我们总结一些最重要的结果。由于线性插值的性能消耗不大,并且我们知道如何计算(u,v),我们倾向于在尽可能的在屏幕空间里做线性插值,甚至是用于透视矫正。有些出乎所料的,在一个三角形内线性的插值a/w 1/w被证明是可行的,w是进行了所有的转置后的顶点的第四个组成部分。再说这个插值属性,a,是和使用这两个插值的values相关的,
这就是上面提到的逐像素除法。
用一个具体的例子来说明。假设我们在沿着一个水平的三角形的边做插值,左边的a0=4,右边的a1=6.那么这两个点的中点插值是什么?对于正交投影来说(或者当两个端点的w值相同),那么答案很简单是a=5,a0和a1的中间值。
那么当两个端点的w值分别为w0=1和w1=3。这种情况下我们需要插值两次,来得到a/w和1/w。对于a/w,左端点是4/1=4,右端点是6/3=2,所以中点值是2。1/w的值则是1/1和1/3,所以中点值是2/3.然后用3除2/3可以得到a=4.5,对于透视下的中点来说。
在实践中,我们经常需要在一个三角形里使用透视矫正来插值多个attributes数据。因此,计算投影正确的重心坐标是非常常规的计算,我们把它标为(u,v),然后使用它做插值。为了这个目的,我们引入下面的辅助公式:
注意由于e0(x,y)=a0x+b0y+c0,三角形设置阶段可以计算并存储a0/w0,并且存储其他类似的项来加速逐像素计算。另一个选择是,所有的fi-functions可以乘上w0w1w2;例如我们存储w1w2f0(x,y), w0w2f1(x,y), 和w0w1f2(x,y)。那么透视矫正的重心坐标是
这个值每个像素需要计算一次,然后可以被用于插值任何attributes,用正确的透视收缩的方式。注意这里的坐标值就不再和(u,v)一样与subtriangle的面积成比例。另外,在上面的这个重心坐标公式里,这个分母并不是一个常数,所以这就是为什么每个像素都需要计算一次的原因。
最后,注意由于depth值是z/w,我们在Equation 23.10看到,我们不需要再用这些公式,因为已经除过w了。因此,zi/wi应该被逐顶点per-vertex计算,然后用(u,v)进行线性插值。这种做法有很多优点,例如压缩深度缓冲(Section 23.7)。
23.1.2 Conservative Rasterization 保守光栅
从DirectX 11和使用了extensions的OpenGL,一种新的三角形遍历方式,叫做保守光栅conservative rasterization(CR)被提供出来。CR有两种形式,叫做过度估计的CR(overestimated CR, OCR)和估计不足的CR(underestimated CR,UCR)。有时他们也叫做outer-conservative rasterization和inner-conservative rasterization。这些在Figure 23.7有展示。
Figure 23.7. 保守光栅化一个三角形。当使用outer-conservative rasterization时所有有颜色的像素属于三角形。黄色和绿色的像素是使用标准光栅化时位于三角形内部的点,并且在使用inner-conservative rasterization时只有绿色的点会产生出来。
大致上说,所有和三角形重叠或者位于三角形内部的像素都会被OCR访问到,而只有完全位于三角形内部的像素才会被UCR访问到。OCR和UCR都可以通过把tile大小缩为一个像素的逐tile遍历来实现。当硬件不支持的时候,我们可以通过geometry shader来实现OCR,或者使用三角形扩大(triangle expansion)。对于CR的更多信息,我们参考每个API的具体标准。CR是很有用的,比如对于图片空间里的碰撞检测,阴影计算,和抗锯齿,这些都用过其他算法来实现。
最后,我们明白所有的光栅化方式都作为几何处理和像素处理两个阶段之间的桥梁。为了计算三角形顶点的最终位置和计算像素的最终颜色,在GPU中需要大量的灵活的算力。这会在接下来讲解。
23.2 Massive Compute and Scheduling 大规模计算和调度
为了提供巨大的算力可以用于任意的计算,大多数并非全部的GPU架构使用同一的shader架构采用多线程的SIMD处理,有时也叫做SIMT处理或者超线程。可以看看Section 3.10来回顾一下这些概念,thread,SIMD-processing,warps和thread groups。注意我们这里提到的warp,是一个NVIDIA的术语,在AMD的硬件上,这些叫做waves或者wavefronts。在本节中,我们会首先来看一个GPU中使用的典型的数学计算单元ALU。
ALU是一种专门经过优化的,为一个entity执行程序的硬件。例如,一个vertex或者一个fragment(在这个上下文里)。有时我们也会用术语SIMD lane(一个simd 通道?)来代替ALU。Figure 23.8是一个典型的为GPU设计的ALU的可视化的样子。主要的计算单元是一个浮点数单元(FP)和一个整数单元。FP单元典型的定义是依据IEEE754 FP标准,并且支持乘加指令作为其最复杂的指令之一。一个ALU也包含move/compare和load/store能力,以及一个分支单元,但是没有超越数计算例如cosine sine和指数计算。这里需要注意的是,虽然但是,超越数处理单元可能在一些架构下作为一个独立的硬件组成部分来服务于所有的ALU。这是因为其使用频率不如其他的运算指令高。这些被归类放在特殊单元块(SU,special unit),入Figure 23.8的右边所示。一个ALU架构通常是又一些硬件管线阶段构造,例如,有一些并行执行的block存在(actual block built in silicon,强调真的有,用硅造的是显得真实的细节,实际上没有翻译必要,属于阅读障碍)。举例来说,当前的执行正在执行一个乘法运算时,下一条指令可以访问寄存器了。有了n个管线阶段,吞吐量就可以变大n倍,在最理想的状态下。这个操作叫做pipeline parallelism管线并行。另一个使用管线的重要原因是,在一个管线化处理器中,最慢的硬件块影响时钟频率(clock frequency)。增加管线阶段可以让每个阶段的硬件块都更少,这就更能提升时钟频率。。但是,为了简化设计,一个ALU通常只有少数几个管线阶段而已,例如4-10个。
Figure 23.8. 左边:一个例子,一个一次可以操作一个item的ALU设计。发送端口Dispatch port接受当前要执行的指令,然后运算对象收集器operand collector根据指令去读寄存器。右边:这里,一个8x4的被集成在了一起,和其他的几种硬件单元一起被放到一个块中,叫做复合处理器multiprocessor。32个ALu,也叫作SIMD管道,可以以锁步/同步lock-step的方式执行同一个程序,也就是说,他们构成了一个SIMD引擎。另外这里还有一个寄存器文件register file,一个L1缓存 cache,一个本地数据存储单元local storage unit,贴图单元texture unit,和包含一堆在ALU里没有的指令的特殊单元special unit。
这个标准的ALU和CPU的核心是不一样的,因为他们并没有一堆乱七八糟的附加内容,比如说分支预测branch prediction,寄存器重命名register renaming,和深度指令管道deep instruction pipeline(这我不懂)。取而代之的是,芯片上大量的晶体管被用于重复的ALU来提供大量的算力,以及寄存器文件的尺寸,所以warps可以来回切换。举例来说,NVIDIA的GTX1080Ti拥有3584个ALU(流处理器?)。为了能有效地的调度指定给GPU的工作,大多数GPU会把ALU分成32个一组。他们会锁步的执行,意思就是说32个ALU的集合就是一个SIMD引擎。不同的显卡厂商对于这样的一个组,也包含了其他的一些硬件单元在内,会使用不同的命名。这里我们只使用一些比较常规的术语多重处理器multiprocessor(MP)。例如,NVIDIA使用的术语是流处理器stream multiprocessor,Interl叫做执行单元execution unit,AMD叫计算单元compute unit。figure23.8里展示了一个MP的例子。一个MP通常拥有一个调度器scheduler用于发送任务给SIMD engine,还有一个L1 cache,local data storage(LSD),texture unit(TX),和一个处理不包含在ALU里面的特殊指令的单元special unit。一个MP在ALU之上分发指令,这些指令是锁步执行,就是说SIMD-processing(Section 3.10)。要注意的是,MP的具体内容每个厂商都不完全一样,每一代的硬件架构也不会完全一样。
SIMD-processing对于图形计算的意义很容易理解,因为计算里有太多一样的东西,例如vertices和fragments,这些都是在执行相同的程序。这里,架构设计利用了线程级的并行thread-level parallelism,即是vertices和fragments完全可以相互独立于其他vertices和fragments执行。进一步来说,借助于任意一种类型的SIMD/SIMT-processing(单指令多数据/单指令多线程处理),数据级并行可以用起来了,因为在一个SIMD machine上所有的lanes都在执行同一个指令。还存在指令级的并行instruction-level parallelism,意思是说如果处理器可以识别一些独立于其他指令运行的指令,那么就可以同时执行他们,如果有足以并行执行的资源的话。
非常接近一个MP的地方,有一个warp scheduler,它负责接受大量需要在MP上执行的业务。Warp scheduler的任务是以warp的形式把工作分配给MP,在分配寄存器文件RF里的寄存器给warp里的thread,然后尽可能以最佳的方式给工作分配优先级。通常状态下,位于下游的业务拥有比上游业务更高的优先级。例如,像素着色位于可编程极端的最后,拥有高于顶点着色的优先级,顶点着色位于管线更靠前的部分。为了避免阻塞,因为靠后的渲染阶段不太可能会阻挡住前面的阶段。新手可以看本书34页的Figure 3.2里的图形管线图。一个MP可以处理数百或者数千个线程来隐藏一些延迟,比如说内存访问。调度器scheduler可以把MP上当前执行或处于等待状态的warp切换下去,来执行一个准备好了的warp。由于scheduler是每个硬件专门实现的,这个一般可以做到0负担(没有额外开销)。例如,假设当前的warp在执行一个贴图加载命令,这可能会有很长的延迟,scheduler可以马上把当前的warp切换走,替换成另一个,并且继续执行新的warp。这种工作方式下,计算单元可以得到最好的利用。
对于pixel shading的工作,这个warp scheduler会分发几个quads,因为pixels会以quad为粒度进行处理,用来计算导数。这个在Section 23.1里提到过,并且会在Section 23.8里进一步讨论。所以说,如果一个warp的size是32,那么32/4=8个quads可以被调度执行。这里有一个架构设计上的选择,我们可以把整个warp指定给单个的triangle,也可以让warp里的每个quad都属于一个不同的triangle。前者非常容易实现,但是对于小型的triangles,效率就不太行了。后者实现复杂很多,但是对于处理小三角形就更高效。
一般来说,MPs也会在芯片上大量重复存在来获得更高的计算密度,所以这就导致,GPU通常会也有有一个更上层的scheduler。这个scheduler的工作就是给不同的warp scheduler分配工作,基于提交给GPU的工作。在一个warp里有很多个threads也就通常意味着,一个thread上执行的工作需要独立于其他的threads的工作。这就是图形处理的常见情况。例如,对于一个顶点进行shading的时候通常不会依赖其他的顶点,并且一个fragment的color通常也和其他的fragment没有关系。
需要注意的是不同的架构之间存在很多差异。有一些会在section 23.10里高亮,在那里会提供一些不同的案例学习。在这里,我们了解了如何通过大量重复的通用ALUs来进行光栅化和着色。还剩一大块内容是内存系统memory system,所有相关的缓冲区,和贴图采样。这些是接下来Section 23.4会开始讨论的内容,但是在此之前我们会先讨论一些关于延迟latency和占用occupancy的内容。
23.3 延迟和占用 Latency and Occupancy
通常来说,延迟的意思就是在发出一个query和收到结果之间的时间。举例来说,我们可能会查询内存里某个地址的值,从发出query到收到结果之间的时间就是延迟latency。另一个例子是,向texture unit来请求一个filtered color值,这个操作可能消耗数百或者数千个时钟周期。这种延迟就需要隐藏掉,来获取更高效的对GPU计算资源的利用效率。如果这些延迟没有被延迟,内存读取将会主导执行时间。
对此,一种隐藏的机制是SIMD-processing中的多线程比分,在33页里的Figure 3.1里展示。一般的,一个MP会有一个warp的上限值。Active warps的数量取决于寄存器使用率,也可能是texture sampler,L1 caching,插值interpolant,和其他的因素的使用率。这里,我们可以定义占用率,o,如下,
(23.12)
W_max是一个MP上支持的warp上限,W_actice是当前active的warps数量。那就是,o是衡量计算资源使用率的计量标准。举个例子,假设W_max=32,一个shader processor拥有256kB的寄存器,在一个thread上执行的一段shader程序使用27个32-bit的浮点寄存器,另一段程序用150个寄存器。进一步的,我们假设寄存器使用率就是active warps数量的体现。假设SIMD宽度是32,我们可以分别计算对这两种不同情况的active warps的数量,如下
23.13
在第一个case里(分母里的32应该是1个warp32个thread),即这个使用27个register的简短程序,W_active>32,所以占用率就是1,这就很理想,所以这就是一个隐藏延迟的好兆头。但是,但是在第二个case里,W_active≈13.65,所以o≈13.65/32约等于0.43 。因为之类的active warps更少,所以占用率就更低了,这就可能会妨碍到隐藏延迟。因此,设计一种可以平衡好warps最大数量,寄存器最大数量,和其他的共享的资源的最大数量的架构就很重要。
有时太高的占用率会产生相反效果,因为会让cache压力很大,如果你的shader使用了非常多的内存访问。另一个隐藏延迟机制是在一个内存访问请求后继续执行同一个warp,这是有可能的如果有些指令不同依赖这次内存访问的结果。尽管这会使用更多而register,有时这种做法也会很有效的获取低占用率。一个例子是展开循环,这个操作可以用来提高指令级的并行,因为这会产生更长的独立的指令序列,这就使得在发生warp切换之前执行更多的指令。但是,这将会使用更多的临时寄存器。这就是一个通用的规则来争取更高的占用率(就是前面说的unrolling)。低占用率意味着,当shader执行过程中切换warp的概率更低了,例如一次texture access请求。
另一个类型的延迟是来自于从GPU写回数据到CPU。一个好的思维模型是去把GPU和CPU当做两个异步工作的计算机,两者之间的通讯需要费点劲。来自于改变数据流方向(CPU GPU之间谁给谁传数据)会严重的影响效率。当从GPU读回数据时,管线可能会在读取操作之前必须被flush一次。在这个过程中,CPU会等待GPU来完成工作。对于架构来说,如Intel的GEN架构,GPU和CPU在同一个芯片上,使用一种内存共享的模型,这时延迟就会极大的降低。底层的cache会在CPU和GPU之间共享,但是高层的cache不会。通过共享cache来节省的延迟可以让我们尝试一些牛批的算法或者优化手段。举个例子,这个feature可以用于加速光线追踪,光线在GPU和CPU之间来回通讯,完全没有消耗。
23.4 Memory Architecture and Buses
这里,我们会介绍一些数据,讨论一些不同类型的内存架构,然后讲讲压缩和缓冲。
一个端口port是一个在两个设备之间发送数据的通道,一个总线bus是共享于超过2个设备之间发送数据的通道。带宽bandwidth是用于描述端口或者总线数据输出的术语,用每秒多少个bytes来衡量,B/s。Ports和Bus在计算机图形架构里很重要,因为,简而言之,他们把各个部分粘合在了一起。很重要的一点,带宽是一个稀缺资源,所以一个精细的带宽设计或者评估必须在设计一个图形系统之前完成。因为port和bus都提供数据传输能力,port也经常指的是bus,这里我们也会遵从这种习惯。
对于很多GPU,在图形加速转置graphics accelerator上有独有的GPU内容是很常见的事,这个内存通常被叫做显存video memory。访问这个内存的速度会比让GPU通过总线来访问系统内存要快得多,比如说,通过PC上使用的串行总线PCI Express(PCIe)。十六路PCIe v3可以提供双向15.75 GB/s的速度对于双向访问。PCI v4可以提供31.51 GB/s。但是,Pascal架构(GTX1080)的显存访问速度是320 GB/s。
传统上来说,texture和render target存在显存里,但是显存也能存别的东西。场景里的很多物体在每帧变化时不会发生一些明显的形变。甚至一个人物角色也通常是通过一组不变的mesh集合渲染,并通过GPU侧的顶点混和来处理连接部位。对于这类数据,仅仅通过模型的矩阵和顶点着色器来做animation,就会通常使用静态static的顶点和下标buffers,这些buffers都存在显存里。这么可以让GPU读取速度更快。对于每帧都通过CPU来更新的顶点,使用动态dynamic的顶点和下标buffers,这些数据放在系统内存里,可以通过数据总线来访问,例如PCIe。PCI的一个很棒的属性是请求可以被流水线式串行排列起来,所以在等到返回值之前,可以发出多个请求。
大多数游戏主机,例如全系Xboxes和PLAYSTATION 4,使用了统一内存架构unified memory architecture(UMA),着意味着图形加速器可以使用主内存的任意地址来访问贴图和其他不同类型的资源。CPU和图形加速器使用相同的内存,因此也使用相同的数据总线。这相对于使用专用显卡来说是一个明显的区别。Intel也使用了UMA所以内存是在CPU不同核心以及GEN9图形架构之间共享,在Figure 23.9里展示。但是,并非所有的cache都共享。图形处理器有自己的一组L1 caches和L2caches,以及一个L3 cache。最后一级的cache是内存结构memory hierarchy里的第一个共享资源。对于任意的计算机或者图形架构,拥有一个cache hierarchy是非常重要的。这么做可以降低平均访问内存的时间,如果在做访问时有些东西是存在本地的。下一节我们会讨论GPU的缓存和压缩。
Figure 23.9. 一个简化的Intel Soc(在一块芯片上的系统 system-on-a-chip) Gen9 图形架构的内存架构试图,连接CPU核心和一个共享的内存模型。注意最后一级的cached(LLC)在图形处理器和CPU核心之间共享。
23.5 缓存和压缩 Caching and Compression
缓存分布在每个GPU的数个不同部分里,但是他们随着架构的不同而不同,如同我们在Section 23.10里所见。通常来说,一个架构里增加一个内存分级的目的是通过利用内存访问的本地性locality来减少延迟和带宽使用。那就是,如果一个GPU访问一个item,那么访问同一个item或者相邻的item的可能性就更大。大多数的buffer和texture格式以tile的模式存储,这也会增强本地性。比如说一个cache line由512个bits组成,即64bytes,当前使用的颜色格式使用每个像素4B。一个设计选择是会把像素存储在4x4的区域里,也叫做tile,64B。那就说,整个color buffer都会被分成4x4的tile。一个tile也可能横跨多个cache line。
为了得到一个高效的GPU架构,我们需要尽一切努力的降低带宽使用。大多数GPU都包含了在运行时压缩和解压render targets的硬件单元,例如正在渲染的图片(给render targets举例)。实现无损的压缩算法是很重要的,那就是说,我们能总精确的重建原始数据。这些算法最主要的就是我们称作tile table的东西,这里存储了每个tile的一些额外的信息。这个可以存储在芯片上,或者通过memory hierarchy 中的cache来访问。Figure 23.10展示了两种系统的框图。一般来说,相同的设置可以用于depth,color,stencil压缩,有时也需要一些修改。tile table里的每个元素都存储了framebuffer里的一个tile of pixel的状态。每个tile的状态都可以被标记为压缩的,未压缩的,或者清理掉的cleared(随后会讨论)。通常来说也会有不同类型的压缩块。举个例子,一个压缩模式可以压缩25%,而另一个可以压缩50%。根据传输给GPU能够处理的内存传输规模size of memory transfers来设置压缩等级是非常重要的。假设有一个系统最小的内存传输是32B,如果tile的size设计为了64B,那么只可能进行50%的压缩。但是,如果有128B是tile尺寸,则可以压缩到75%,50%,和25%.
Figure 23.10. 框图:GPU里压缩和缓冲render targets的硬件技术。左侧:缓存后压缩post-cache compression,执行压缩和解压的硬件单元位于cache之后。右侧:缓冲前压缩pre-cache compression,执行压缩和解压的硬件单元在cahce之前。
Tile table经常被用于快速的clear一个render target。当系统对render target执行一次clear时,table里每个tile的状态设置为cleared,framebuffer实际上完全没有被触及到。当硬件单元访问这个rt,需要去读取一个cleared的rt的时候,解压器decompressor单元会先检查table里的状态看看是否是cleared。如果是,放在cache里的rt所有值都是clear值,并不需要专门去读取并解压rt数据里实际存的值了。这种方式,访问clear的rt就非常简化了,这就节省了带宽。如果状态上没有写cleared,那么这个tile上的rt就需要被读取了。这个tile存储的数据被读取时,并且如果是压缩的,那么还需要先通过解压器解压后才能输出。
当硬件访问一个刚写好了值的rt时,并且这个tile最终被从cache里逐出,那么将会被送往压缩器compressor,这里会进行对其压缩的尝试。如果存在两种压缩模式,那么都会尝试,并且最终会选择压缩的最小的那个。由于API都需要无损的rt压缩,所以需要有一个fallback情况是使用不压缩的数据,在所有的压缩都失败的情况下。这也暗示着无损rt压缩并不能减少内存的使用,只是减少了带宽的使用。如果压缩成功了,tile的状态会被设置为压缩的compressed,数据也都会通过压缩的格式传输。否则,就是设置uncompressed和非压缩的传输。
注意压缩和解压单元既可以在cache之后(叫做post-cache),也可以在cache之前(叫做pre-cache),如Figure 23.10所示。Pre-cache压缩可以显著提高有效地内存大小,但是往往也会增加系统的负责度。对于压缩depth和color都有特别的算法。后者(说的是color吧)也有一些有损压缩方式,但是我们所知的硬件都没有使用过。大多数的算法都会encode一个锚点值anchor value,代表了一个tile上的所有像素,然后相对于anchor value的差值会用不同的方式encode。对于depth来说,经常会存着一组平面方程plane equation或者用一个不同差异的技术difference-of-differences,这两种方式的效果都很好,因为深度在屏幕空间里是线性的。(这段需要查额外资料理解)
23.6 颜色缓冲 Color Buffering
使用GPU进行渲染时需要访问多个不同的buffers,例如,color,depth和stencil。注意,尽管叫做color buffer,任何形式的数据都可以被渲染并存储在里面。
Color buffer通常有一堆color模型,基于用于表示color的位数多少。这些模式包括:
High color = 每个pixel 2 bytes,这里又15或者16个bits用于color,可以分别表示32768或者65536个不同颜色。
True color或者RGB color - 每个pixel 3或者4个bytes,有24个bits用于表示颜色,范围大概可以表示16777216≈16.8 million个不同颜色。
Depth color - 30,36或者48个bits每pixel范围至少有1billion。
对于有16bits的颜色分辨率去使用的high color。典型的,这些会被分出至少每个5bits给rgb通道,至少每个通道都有32个levels。这样会多出来一个bit,这个bit会分配给绿色,得到一个5-6-5的划分。给绿色通道的原因是因为绿色对于眼睛的亮度影响最大,所以需要更高的精度。High color相对另外两个速度更快。这是因为每个像素2bytes的内存在访问上会比3或者4个bytes更快。即使如此,使用high color的情况也非常的少,几乎没有。如果每个通道只有32或者64个颜色分级,那么相邻的颜色级别之间的差异看上去会非常明显。这个问题有时叫做条带banding或者多色调分posterization。人们的视觉系统会进一步的放大这些色差,由于一种叫做Mach banding的感知现象。见Figure 23.11. 混合Dithering,就是把相邻的颜色等级混合,可以减轻这个banding问题,通过付出空间上的分辨率来换取更有效的颜色分辨率。梯度上的banding即使是在24-bits的显示器上也很明显。对framebuffer图片增加一些噪音可以用于掩盖这个问题。
Figure 23.11. 如这个长方形的颜色从白到黑排列,条带banding现象出现了。尽管这些拥有32个灰度级的条带都有一个固定的密度,但是在分界之处左侧都会更亮右侧都会更暗,因为马赫条带现象Mach band illusion
True color使用24bits的RGB颜色,每个颜色通道1byte。在PC系统中,通道顺序有时候会颠倒为BGR。在程序内部运行时,这些颜色会以32 bits存储,因为大多数内存系统对于4-byte 的元素都有优化。在一些系统上,额外的8bits可以被用于存alpha通道,得到一个RGBA的pixel值。24-bit颜色(不包含alpha的)的表示方法也叫做压缩的像素格式packed pixel format,相比于32-bit未压缩的,这个更省内存。在实时渲染中几乎总是使用24-bits的颜色。我们仍然可能看到颜色的banding,但是相比于只有16 bits的时候好很多了。
Deep color使用30,36或者48个bits每个RGB颜色,即是10,12,16bits每个通道。如果加入了alpha值,这些数字会增加到40 48 64。HDMI 1.3支持全部的30 36 48模式,DisplayPort标准也支持每个通道最高16bits 。
如Section 23.5中描述,Color buffer通常是被压缩并cache起来的。另外,把输入的fragment数据和color buffer上的内容做混合会在Section 23.10的案例学习里讨论。混合操作是由光栅操作单元来处理的raster operation units(ROP),每个ROP都和一个内存分块对应,例如,一个广义的棋盘模式generalized checkboard pattern。我妈接下来会讨论视频播放控制器video display controller,其功能就是输入一个color buffer并显示出来。单buffer,双buffer和三buffer随后会介绍。
23.6.1 Video Display Controller 视频播放控制器
每个GPU都有一个视频播放控制器video display controller(VDC),也叫作播放引擎display engine或者播放接口display interface,这个东西负责把color buffer显示出来。这是GPU里的一个硬件单元,可能会支持大量的接口,比如高分辨率多媒体接口high-definition multimedia interface(HDMI),DisplayPort,digital visual interface(DVI),和video graphics array(VGA)。用于显示的color buffer可能放在和CPU共用的内存,或者专门的framebuffer内存,或者是显存,后者(显存)可能包含GPU的数据但是不会被CPU直接访问。每种接口都用自己的协议来传输color buffer,定时信息,有时甚至是音频。VDC可能也要做更改图像尺寸,噪音降低,混合多个图像源,和一些其他功能。
显示器更新频率,例如一个LCD显示器,通常是在60-144次每秒(Hertz)。这个也叫作垂直刷新率vertical refresh rate。大多数用户会观察到屏幕闪烁,在低于72hz的情况下。这里个主题可以看Section 12.5来获取更多信息。
显示器技术在多个方向上发展,包括刷新率fresh rate,每个component的bit数,色域gamut,和同步sync。刷新率一般都是60hz,不过120HZ越来越常见了,并且理论上可以达到600hz(可以研究一下为啥)。对于高刷新率,为了最小化人眼观察到的在帧显示之间人眼移动时观察到的脏脏的artifacts,图像经常会被显示很多次,有时也会插入一些黑色的帧black frame。显示器也可以拥有每个通道高于8 bits的位宽,HDR显示器会是显示技术领域的下一个大事件,他们可以使用每个通道10bits甚至更多。杜比Dolby有一种hdr技术是使用一组低分辨率的LED背景光来增强他们的LCD显示器。这么做可以让他们的显示器相比普通的显示器亮度增强十倍,对比度增强100倍。拥有广色域的显示器也变得更常见。这些显示器通过表示纯净的光谱色调来显示更大范围的颜色,例如更生动的绿色。可以看Section 8.1.3获取更多信息。
为了避免撕裂效果,各家公司都开发了自适应的同步技术,比如AMD的FreeSync和NVIDIA的G-sync。这些技术的idea就是随着GPU的产出频率去自适应的调整屏幕刷新率而不是使用一个预先指定的固定刷新率。举个例子,如果一帧使用10ms下一帧用了30ms来渲染,图形往显示器上的更新会在渲染完成后立刻执行。这些技术可以让渲染变得更平顺。另外,如果图形没有更新好,那么color buffer也就不需要传输给显示屏,这就可以省电。
23.6.3 单,双和三重缓冲 Single,Double and Triple Buffering
在Section 2.4里,我们提到了双缓冲可以保证图像在渲染完成之后不会被显示在显示器上。这里,我们将会讨论单,双和三重缓冲。
假设我们只有一个缓冲。这个缓冲就必须成为当前显示在屏幕上的。随着三角形的绘制,屏幕上刷新出越来越多的内容,这是一种非常不真实的体验。即是我们的帧率和屏幕的刷新率一致,单缓冲也有问题。如果我们想要clear buffer或者绘制一个大三角形时,我们就会看到color buffer发生了一部分区域的变化因为video display controller把这些正在绘制的区域传输过去了。这也叫做画面撕裂tearing,因为这个时候的画面显示就像是被短暂的撕成两半了,这不是实时渲染想要的效果。在一些古代的系统里,例如Amiga(一个古代计算机牌子),你可以检测beam(理解为画面更新的那条线,也就是撕裂处)的位置,然后避免在那里绘制,这样就可以让单缓冲顺利工作了。近来,单缓冲很少被使用了,可能只有虚拟现实系统有使用,因为他们的“racing the beam”(理解为跟着这个beam来渲染)可以作为一种减少延迟的办法。
为了避免撕裂问题,双缓冲方案很常用。一个绘制完毕的图像显示在前缓冲里front buffer,同时一个离屏的后缓冲back buffer里包含了当前正在绘制的图像。随后前后缓冲通过图形驱动对换,这个操作进行的典型时机就是在一个完整的图像传输到了显示屏,来避免撕裂。交换操作通常也只是交换两个color buffer的指针。对于CRT显示器,这个项目叫做垂直扫描vertical retarce,并且这个过程中的视频信号叫做垂直同步脉冲vertical synchronization pulse或者简称vsync。对于LCD显示器,并没有一个物理的光束进行扫描,但是我们仍然使用同样的术语来这件事。在渲染完成后立刻交换前后缓冲对于确立一个渲染系统的benchmark很有用,并且也用在很多应用里,因为可以最大化帧率。并不是vsync的更新也会导致撕裂,而是因为有两个完全绘制完成的图像,artifacts就不像是单缓冲那么差。在交换完成之后,新的back buffer接受图像绘制命令,新的front buffer用于屏幕显示。这个流程在Figure 23.12里显示。
Figure 23.12. 对于单缓冲(顶上的),front buffer一直处于显示中。对于双缓冲(中间的),第一组,buffer 0是front,buffer 1是back。然后每帧来回交换两个buffer。三重缓冲(底部的)加入了一个等待缓冲pending buffer。这里,首先一个buffer被clear然后对其开始执行渲染(pending)。然后,系统继续使用这个buffer用于渲染直到绘制完成(back)。最后,这个buffer显示在屏幕上。
双缓冲可以通过一个两个back buffer来增强,我们称作pending buffer。这种叫做三重缓冲。pending buffer和back buffer相同之处在于都是离屏的,可以在front buffer显示过程中被操作。pending buffer是三buffer循环的一部分。在一帧中,pending buffer可以被访问。在下一次交换时,它就变成了back buffer,就是渲染完成的buffer。然后再变成显示出来的front buffer。在下一次交换时,这个buffer再次变灰pending buffer。这个过程在Figure 23.12的底部显示。
三重缓冲有一个相比于双缓冲主要的优点。用这个,系统可以在垂直扫描的时候访问这个pending buffer。在双缓冲里,等到垂直扫描的过程会使得swap操作也有时间消耗,系统处于等待状态。这是因为front buffer还要保持显示的状态,back buffer的内容这时不能再进行修改了。三重缓冲的缺点是显示一帧的延迟增加了。这使得用户输入的反应延迟增加了,例如按键或者鼠标触摸板的移动操作。操控感觉迟缓了,因为用户事件在pending buffer开始渲染后被延迟了。
理论上说,也可以使用多于三个buffer的方案。如果每一帧渲染的计算时间发生较大的波动,更多的缓冲会带来更好的平衡感,并且总体的帧率也会提高,代价是更高的延迟。概括一下,多缓冲可以被看作是一个循环结构。有一个渲染指针和一个显示指针,在当前的渲染中的buffer完成渲染之后,指针就会移动向下一个buffer。唯一需要注意的就是,渲染指针和显示指针永远不要是同一个。
一个与之相关的模式,也是用于pc的图像加速的,是使用SLI mode。时间回到1998,3dfx使用SLI作为scanline interleave(扫描线交替)的首字母缩略,就是两个图像芯片并行,一个处理奇数扫描线另一个处理偶数。NVIDIA(买了3dfx的资产)使用这个名字(SLI)但是是完全不同的方式,连接两个或更多的显卡,叫做可升级连接接口scalable link interface。 AMD把这个叫做CrossFire X。这种方式的并行处理把工作分开,通过把屏幕区域分割为两个或者更多的水平区域,每个显卡负责一块。或者让每个显卡都专注于执行自己渲染的帧,然后交替输出。这也是一种抗锯齿的方式。最常见的做法是让每个GPU渲染单独的一帧,叫做交替帧渲染alternate frame rendering(AFR)。尽管这个策略听起来会增加延迟,实际上没啥影响。比如说一个单GPU系统可以渲染10fps。如果是GPU bottleneck,两个使用AFR的GPU系统可以把帧率提升到20FPS,或者使用4个GPU提到40FPS。每个GPU都使用相同的时间来渲染,延迟几乎是不变的。
屏幕的分辨率是持续提升的,这就给渲染器带来了严重的挑战,在逐像素sample的时候。一个保持帧率的方法是相当的改变pixel shading的频率,对于屏幕和表面。(不懂,看参考文献去吧)
23.7 深度裁剪,测试,和缓冲 Depth Culling, Testing, and Buffering
这一章里,我们会涉及与深度相关的方方面面,包括分辨率,测试,裁剪,压缩,缓存,缓冲和early-z。
深度的分辨率是很重要的,因为这可以避免渲染的错误。举个例子,假设你建模了一张纸并放在了桌子上,只比桌子的表面高了一点点。由于深度的精度限制,这个桌子和纸会发生很多的穿插。这个问题叫做z-fighting。如果这张纸和桌面放置在了相同的高度上,就是说这俩共面,那么在没有更多信息的情况下,就无法正确描述他们的相对关系了。这个问题的本质是模型的精度有限,你再提高深度的精度也解决不了。
如同我们在Section 2.5.2里看到的,z-buffer(也叫作深度缓冲)可以被用于解决可见性问题。这种buffer通常是24或者32个bits每个像素,可以用浮点数也可以用定点数。对于正交的视角,距离值和z值长征比,所以可以得到一个均匀的分布。但是,对于透视的视角,这个分布就不均匀了,在99-102页里我们可以看到细节。在进行了透视变换(Equation 4.74或者4.76)后,还需要一个除w操作(Equation 4.72)。深度值就是Pz=Qz/Qw, Q是进行了乘了透视矩阵后的点。对于定点数的表示,Pz=Qz/Qw的值会被从他本身的有效范围(例如DirectX是[0-1])映射到一个整数区间[0, 2^b -1]并存在z buffer里,b是bit的总数。跟过关于深度精度的信息可以看99-102页。
硬件的深度管线在Figure 23.13里展示。这个管线的主要目的是基于depth buffer来测试每个传入的深度值,传入的深度是光栅化图元的时候生成的,并可能会把当前测试的depth写进depth buffer,如果这个fragment通过了depth test。与此同时,这个管线需要做到很高效。图的左边部分从一个粗糙的光栅化coarse rasterization开始,即是在一个tile级别上的光栅化(section 23.1)。此时,只有和一个图元primitive重叠的tile才会被传递到下一个阶段,叫做HiZ单元,这里执行一个z-culling的技术。
HiZ单元开始于一个叫做粗粒度深度测试coarse depth test的阶段,这里通常会执行2种类型的测试。我们先说zmax-culling,这是Greene的分层z-buffering算法的简化,在Section19.7.2章里介绍了。这个算法的思想是存储整个tile里所有z值的最大值,叫做zmax。这个tile的尺寸是每个架构都不同的architecture-dependent,但是通常使用的是8x8的大小。这些zmax值可以存在一个固定的on-chip memory上或者可以通过cache访问的地方。在Figure 23.13里,我们把这个叫做Hiz Cache。简单来说,我们想检测的是这个三角形在这个tile上是否被完全挡住了。为了做到这点,我们需要去计算三角形范围内最小的z值,z_TriMin(三角形的最小z值)。如果这个最小值比zmax大,那么可以保证的是三角形在这个tile上完全被前面的物体所遮挡了。在这个tile上对该三角形的处理可以完全停止了,这样可以省下许多逐像素的计算。需要注意的是,这个操作完全没有省下任何pixel shader的计算,因为管线后面的per-sample的深度测试本来也会把没通过深度测试的fragment剔除掉的。在实践中,我们无法负担计算z_TriMin的准确值,所以作为替代,我们计算一个保守的估值。有几种潜在的计算z_TriMin的方式,每个都有自己的优缺点:
1、三角形的三个顶点里的最小的z值。这并不总是准确的,但是开销是很小的。
2、通过三角形的平面公式plane equation来计算tile的四个角的z值,然后选取其中最小的。
最佳的culling 表现是把这两者进行结合,就是选取二者中更大的那个z值。
Figure 23.13. 一个深度管线可能的实现方式,z-interpolate是简单的计算插值的z值。
另一种粗粒度的深度检测叫做zmin-culling,想法是存储tile上最小的z值。有两个用途,第一,可以避免z-buffer的读取。如果一个三角形确定是在所有的东西之前渲染,那么逐像素的深度测试是不需要的。有些情况下,z-buffer的读取可以完全避免,这可以进一步提高性能。第二,可以用于进行多种深度测试。对于zmax-culling模式,我们假设采用的是标准的“less than”深度测试。但是,如果culling能使用其他的深度测试模式会更好,如果同时有zmax和zmin,这个过程中可以使用所有的深度测试模式。一个更详细的关于硬件上深度管线的描述可以在Andersson的PhD论文里找到。
Figure23.13里的绿色盒子关注于不同的方式来更新tile的zmax zmin值。如果一个三角形覆盖了整个的tile,可以直接在HiZ unit里完成z值计算。否则,整个tile的逐像素的深度值需要被读取,计算出最大最小值,然后传给HiZ unit,这会造成一些延迟。Andersson等人提出了一个方式,不需要昂贵的depth cache反馈开销,并仍能够保留culling的大部分性能。
对于通过粗粒度深度测试的tile来说,每个像素或者采样的覆盖范围是确定的(使用Section 23.1里的edge equation),并计算出了per-sample深度值(叫做Figure 23.13里的z-interpolation)。这些值继续传递给depth unit,在图里的右侧展示。根据API的描述,pixel shader计算应该遵守。但是,在一些情况下,接下来将会讲,一个额外的测试,叫做early-z或者early depth,可能被执行。Early-z就是一个per-sample的涉毒测试在pixel shader之前执行,被挡住的fragment会被舍弃。这个操作可以避免不必要的pixel shader计算。Early-z经常会和z-culling搞混,但是他们是由完全不同的硬件执行的。这俩技术都是独立执行的。
包括Zmax-culling,Zmin-culling和early-z全部都是GPU根据各种情况来自行决定如何使用的。但是,有的情况下这些都会被disable掉,例如,pixel shader里写深度,使用discard操作,或者shader会往UAV资源里写值。如果early-z不能用,那么就会在pixel shader运算完成之后再执行深度测试(叫做后深度测试late depth test)。
在新的硬件上,可以支持原子化的读-改-写操作,在shader里loads和stores一个图片。在这些情况下,你可以明确的开启early-z并且覆盖这些限制,如果你知道这么做是安全的话。另一个feature,可以用于pixel shader输出custom depth的时候,叫做保守的深度conservative depth。在这里,如果程序员能保证custom depth比triangle depth更大,就可以开启early-z。对于这个情况,Zmax-culling也可以启用,但是不能用early-z和Zmin-culling。
一如既往的,遮挡剔除Occlusion culling在从前往后的画的时候有用。另一个有着类似名字和目的的技术叫做z-prepass。这个想法就是先画一遍场景的深度,只写深度关掉pixel shading,并且把深度画到color buffer上。然后在执行后续的正常渲染时,使用ZEqual做比较,这就意味着只有最前面的surface会被画,因为z-buffer里面的值已经确定了的。可以看Section 18.4.5.
总结这一节,我们先简单的描述一下depth pipeline里的缓存caching和压缩compression,在Figure 23.13的下右部显示。这个通用的压缩系统和Section 23.5里描述的系统类似。每个tile可以被压缩为几个固定的尺寸,并且总会有一个fallback是不进行压缩。“Fast clear”操作用于在执行clear depth buffer时节省带宽。由于深度是screen space里是线性的,典型的压缩算法,可以使用高精度的方式存储plane equation,也可以使用一种difference of differences的delta encoding技术,或者使用一些anchor method。Tile table和HiZ cache可能完全被放置在on-chip的buffer里,或者他们可能像depth cache一样使用剩余的memory hierarchy通讯。On-chip的存储是非常昂贵的,因为这需要buffer空间足够大来支持最高的分辨率。
23.8 Texturing 贴图操作
尽管贴图操作(包括取贴图,过滤贴图和解压)可以在GPU的多处理器上以纯软件的方式执行(in pure software running),但是使用专门采样贴图的硬件可以提升四十倍的速度。Texture unit执行取址addressing,过滤filtering,clamping和解压不同贴图格式(Chapter 6)。它通常和一个texture cache一起使用来降低带宽消耗。我们从filtering开始讨论,并顺着说下去。
(analytically 解析的,求精确解。numerically数值的,求近似解)
为了能使用缩小过滤器minification filter,比如mipmaping和各向异性过滤anisotropic filtering,需要贴图坐标在屏幕空间的导数。就是说,为了计算texture的lod值λ,我们需要计算du/dx, dv/dx, du/dy,和dv/dy。这个信息可以告诉我们这个fragment里贴图区域的长度信息extent of the texture's area or function is represented by the fragment。如果从顶点着色器传递来的贴图的坐标直接用于访问贴图,那么导数可以解析计算出来。如果贴图的坐标使用一些函数来变换,例如(u',v')=(cos v,sin u),那么解析计算导数会更复杂。但是还是能算的,使用链式法则chain rule(复合函数求导)或者符号微分symbolic differentiation。尽管如此,图形硬件不会用这些方法中任意一个,因为实际情况里会很复杂。设想计算一个表面的反射,有凹凸法线,使用一个环境贴图environment map。非常难以解析计算,例如,一个反射向量reflection vector从normal map上弹开然后又用于采environment map。所以说,实际中使用数值的方法计算导数,在一个quad为基础的区域,即是2x2的像素,计算x和y有限的差值。这也是GPU架构会围绕着处理quads来设计一样。
一般来说,偏导的计算都是在后台发生的,就是说,他们对于用户来说是隐藏的。具体的实现通常是一些在quad上跨信道的(cross-lane)的指令,像是shuffle/swizzle,并且这些指令是由编译器插入的。有一些GPU取而代之是用专门的硬件来计算偏导。关于偏导应该怎么计算并没有一个明确的标准。一些通用的方法在Figure 23.14。OpenGL4.5接DirectX11支持计算粗糙或者精确的偏导结果。
Figure 23.14. 展示偏导的计算过程。箭头表示像素差,箭头起始和终止的位置的两个像素的差值。举个例子,左上水平差值是由左上像素减去右上像素得到的。对于左侧的粗粒度的偏导,一个水平差值和一个垂直的差值就是对一个quad里全部四个元素使用。对于右侧精确的偏导,则每个都会单独算一次。
贴图缓存Texture caching是所有GPU都会使用来降低贴图带宽的技术。有一些架构会有专用的缓冲给贴图使用,甚至还会做个二级缓冲,不过其他的架构会对于包括贴图在内的所有访问都共享一个缓冲。一般来说,一个小的on-chip memory(一般是SRAM)是会实现一块贴图缓冲。这块cache里存放的是最近读取的texture,访问速度很快。缓冲的更新机制和缓冲大小,每个架构都不同。如果相邻的像素需要访问相同或者邻近的纹素,那么缓冲命中率会很高。如Section23.4里提到的,内存访问是分块的,所以texels并不是按照扫描线顺序存放的,而是按照小的tiles存放。例如,4x4的纹素存放方式会提高效率,因为一次可以同时读进来。因bytes为衡量单位的tile size通常是和cache的一条line的size相同,例如64bytes。另一个存储贴图的方式是用一个swizzled模式。假设贴图的顶点被转化为定点数:(u,v),u和v各有n个bits。u里面的第i个bit叫做Ui。那么把uv映射到一个swizzled贴图地址A就是
B是贴图的起始地址,T是一个texel占据的bytes数量。这个remapping的好处是给了如Figure23.15里展示的texel顺序。可以看到,这一个填充曲线,叫做Morton sequence,这个可以提升一致性。这种情况下,曲线是二维的,贴图也通常是二维的。
Figure 23.15. Texture swizzling可以提升内存访问的texel一致性。注意这里的texel size是4bytes,并且每个texel的地址写在左上角。
Texture unit也包含了一些自定义功能模块来解压集中不同贴图格式(Section 6.2.6)。相比于通过软件实现,专用硬件来做这件事可以提升很多倍的效率。注意如果使用一个贴图同时作为render target和texture mapping,可能会发生压缩操作。如果color buffer的压缩是开启的(Section 23.5),那么会有两种不同的设计用于访问作为texture的render target。如果这是rt已经渲染完毕,一个选项是解压整个color buffer并存起来等待后续的texture访问。第二个选项是在texture unit里面去增加硬件支持,来解压color texture的压缩模式。第二个是更高效的方式,这样rt作为texture被访问的时候,也可以保持压缩的状态。更多关于缓存和压缩的信息可以在Section 23.4里查阅。
Mipmapping对于贴图缓存局部性很重要,因为这会强制执行一个最大texel-pixel比例。当遍历一个三角形的时候,每一个新的像素都代表贴图空间里的一步,近似一个texel。Mipmapping是一个渲染中少有的同时提升效果和性能的技术。
23.9 Architecture 体系架构
最佳的获取图形硬件性能的方式是利用并行parallelism,并且这些可以在GPU的所有阶段实现。这个思想就是同时计算多个结果并且在后续的阶段进行合并。一般来说,一个并行的图形硬件架构如Figure 23.16里所示。应用把任务发送给GPU,并且在经过一些调度指挥,几个几何处理单元geometry units会开始并行的执行几何阶段处理。几何阶段的处理结果会继续输出给一些光栅化单元rasterizer units,在这里进行光栅化处理。像素着色和混合随后执行,同样是采用并行的方式,在一系列的像素处理单元上pixel processing units。最后,得到的结果图被传输到显示器上显示。
Figure 23.16 一个常规的高性能,并行计算的图形硬件架构,由几个几何单元(G's),光栅器单元(R's),像素处理单元(P's)组成。
对于软件和硬件来说,意识到你的代码或者硬件是否有一个单任务的部分a serial part非常重要,这会制约总体上潜在的性能提升。这可以用阿姆达尔定律Amdahl's Law表示(并行计算中计算效率和线程数的关系),即:
这里s表示一个程序或者硬件单任务的部分,因此1-s是服从于并行的百分比。进一步的,p是能够通过并行程序或者硬件来提升的最大的性能比例。举个例子,如果一开始我们有一个多核处理器,并且再加上另外三个,那么p=4。这里,a(s,p)是就是你能获得的提升比例。如果我们有一个架构,比如说10%是单任务化的,即s=0.1,并且我们提升我们的架构使得剩余的部分(非单任务的)可以提升20倍,即p=20,那么我们可以得到a=1/(0.1+0.9/20)≈6.9。可以看到,我们没有提升20倍的速度,原因是可以单任务的部分比例太低严重限制了了性能。事实上,当p趋近于无穷时,我们可以得到a=10。花费精力在并行的部分或者单任务的部分哪个更好并不总是清晰的,但是当并行的部分得到巨大的提升之后,单任务的部分会更加限制整体性能。
对于图形架构,并行的计算多个结果,但是绘制调用draw call里的图元primitive希望能够按照他们从CPU提交过来的顺序来处理。因此,必须进行一些排序工作,这样才能让并行单元按照用户意图去渲染图形。特别是,排序需要从模型空间到屏幕空间(Section 2.3.1和2.4)。需要的注意的是几何单元和像素处理单元可能被映射到同一个单元,即统一的ALUs单元。我们的样例分析里所有的架构都使用统一着色器架构(Section 23.10)。即使在这种情况下,理解排序如何执行也是很重要的。我们对并行架构进行了一个分类。排序可以发生在管线的任何地方,这就提供了四种不同的并行架构任务分布,在Figure 23.17里。这些分别叫做sort-first,sort-middle,sort-last fragment,和sort-last image。注意这些架构提供了了不同的分配工作的方式。
Figure 23.17. 并行图形硬件架构。A是应用程序,G's是几何处理单元,R's是光栅器单元,P's是像素处理单元。从左到右,这些架构分别是sort-firt, sort-middle, sort-last fragment, 和sort-last image。
一个sort-first-based架构在几何阶段开始之前进行图元排序。这个策略是把屏幕分为一系列的区域,并且在某个区域内的图元会被送往一个完全负责该区域的完整管线里。看Figure 23.18. 一个图元在一开始就得到了充分的处理,来获知其属于哪个区域——这就是排序步骤。Sort-first是对于机器来说最保守的结构。这种方案会在那种多个屏幕或者多个屏幕投影来行程一个巨大的屏幕时使用,每一个电脑都专门负责一块屏幕。一个叫做Chromium的系统已经开发了出来,它可以使用一堆工作站来实现任意的并行渲染算法。例如,sort-first和sort-last可以用高效的渲染方式来实现。
Figure 23.18. Sort-first会把屏幕分割为不同的tile,并且给每个tile分配一个处理器,如图所示。一个图元会被发送给它覆盖的每个tile的处理器。这个和sort-middle形成对比,后者需要在几何处理之后对所有的三角形进行排序。只有在所有的三角形都排序之后,逐像素的光栅化才会开始。(图片出处Images courtesy of Marcus Roth and Dirk Reiners.)
Mali架构(23.10.1)是一种sort-middle架构。几何单元单元要处理几乎全部的输入几何数据。然后转换后的几何被排序进一系列不重叠的矩形内,叫做tiles,这些tiles会覆盖整个屏幕。要注意一个转换后的三角形可能会和多个tiles重叠,所以可能被光栅化多次,以及执行多次像素处理。这里效率的关键点就是每一对光栅化和像素处理单元都拥有一个和tile size一样的on chip framebuffer,这意味着访问framebuffer的速度很快。当所有的几何数据都排序好给tile之后,光栅化和像素处理都可以在每个tile上独立的开展了。一些sort-middle结构会在每个tile上对opaque数据执行z-prepass,这意味着每个pixel只会着色一次。但是,并非所有的sort-middle架构都有这种操作。
Sort-last fragment架构会在光栅化(有的也叫作片元生成阶段)之后像素处理阶段之前进行片元的排序。一个例子是GCN架构,在Section 23.10.3里描述。如同sort-middle架构一样,图元尽可能平均的分配给几何单元。sort-last fragment的一个优点是不会有overlap,意味着产生的片元只会被送去一个pixel processing unit,这是最优的。当一个光栅化单元处理大量的三角形,而另一个只处理一点点时,就会产生不平衡的情况。
最后,sort-last image架构在像素处理之后排序。Figure 23.19是这个过程的可视化展示。这个架构可以被看作是一系列独立的处理管线。图元在这些管线里传递,每个管线都渲染出一个有深度的图片。在最终的组合阶段,所有图片会基于z-buffer进行合并。需要注意的是,sort-last image系统不能完全在OpenGL和DirectX这样的API上实现,因为他们需要图片按照传送过来的顺序进行渲染。PixelFlow是一个sort-last image架构的样例。PixelFlow架构值得一看,因为它使用了延迟着色,这意味它只会着色在可视范围里的片元。这里需要注意,由于每个管线执行带来的带宽原因,现在没有架构使用sort-last image的方式。
Figure 23.19、 在sort-last image中,场景里不同的物体被分发给不同的处理器。在组合不同渲染结果的时候,半透明的物体很难处理,所以半透的物体通常会发给所有节点都进行处理。(图片出处Images courtesy of Marcus Roth and Dirk Reiners.)
图像系统里所有的部分(host,geometry processing,rasterization,和pixel processing)联通在一起给我们组成了一个多进程处理的系统。对于这样一个系统,有两个知名的问题,这两个问题也总是伴随着多进程处理一起:负载平衡load balancing和通讯communication。FIFO(先进先出first-in,first-out)队列常被安插进管线不同的地方,这样可以让jobs按照队列执行来避免阻塞管线。举例来说,可以在geometry和rasterizer之间放一个FIFO,这样经过几何处理的三角形可以缓存起来,当rasterizer单元在三角形太多,不能保持和geometry单元同步处理的时候,这是举例而言。
不同排序的架构有不同的负载平衡优缺点。Consult Eldridge的博士论文或者Molnar的论文有关于这些东西更多的信息。程序员也可以影响负载平衡,关于如何操作的技术在Chapter 18里讨论了。当数据总线带宽bandwidth of the buses过低,或者使用的不明智的的时候,通讯也会成为一个问题。因此,设计一个渲染系统的时,不要让瓶颈卡在总线带宽上是极其重要的,即联通主机和图形硬件直接的数据总线。Section 18.2讨论了不同的检测瓶颈的方法。
23.10 案例学习 Case Studies
在本章节里,会学习三个不同的图形硬件架构。首先是ARM Mali G71 Bifrost架构,其目标平台是移动设备和电视。随后是NVIDIA的Pascal架构。最后是AMD的GCN架构,也叫作Vega。
需要注意到,硬件公司经常会基于尚未真正开发出来的软件对GPU使用情况的模拟,来做出其设计决定。就是说,几款应用,比如像是游戏,在他们的指标化的模拟器上运行,分别有不同的配置。一些可能的指标参数有MP(multi processor?)的数量number of MPs,时钟频率clock frequency,缓存数量number of caches,光栅器引擎raster engine或者曲面细分引擎tessellator engine的数量,ROP数量。这些模拟可以收集一些信息,关于性能,耗电量,和内存带宽使用情况。在最后,一套表现最好的配置,在大多数情况下的综合发挥最好的,会被选中并作为芯片的设计参数。另外,这些模拟可以有助于寻找经典的架构瓶颈,像是缓冲的大小增长。对于一个特定的GPU,对于其不同的速度设置和处理单元设计的原因,就是简单的一句话,“这样做效果最好”。
23.10.1 案例学习:ARM Mali G71 Bifrost
Mali的产品线囊括了所有来自于ARM的GPU架构,Bifrost架构是他们从2016年开始使用的架构。这种架构是为了移动端或者嵌入式系统设计的,比如说像手机,平板和电视。在2015年,销售了7.5亿的Mali架构GPU。由于这些芯片大多数是由电池供电,所以设计一个节能的架构就很重要,而不只是关注于性能表现。因此,使用sort-middle的架构就可以理解了,所有的framebuffer都会被留在chip上访问处理,这样电量消耗可以降低。所有的Mali架构都是sort-middle的,也叫作tiling architecture。一个GPU的高层概览视角在Figure 23.20中显示。可以看到,G71可以支持最多32个通用shader engine。ARM会使用Shader core这个术语来替代shader engine,但是我们为了避免和其余章节产生歧义还是使用shader engine。一个shader engine能够同时在12个线程上执行指令,就是说他有12个ALU。使用32个shader engine是G71特有的,这个架构的规模远大于32个engine。
Figure 23.20。 Bifrost G71 GPU,最大规模是32个shader engine,每个shader engine的内部架构在Figure 23.21里显示。
Figure 23.21. Bifrost的shader engine架构,tile memory是on chip的,这样local framebuffer 访问就会很快。
驱动软件向GPU发送作业。Job manager,就是一个调度器,会把作业分发给shader engines。这些engine被GPU的某种结构连接起来,这种结构就是一个让shader engine和其他unit联通的数据总线。所有的访问需求通过内存管理单元发送memory management unit(MMU),这个模块的功能是把虚拟内存翻译为物理内存。
Figure 23.21里是对shader engine的总览。可以看到,包含了3个执行engine,逐quad执行着色。因此,他们被设计为小型的通用处理器,有宽度为4的SIMD。每个执行的engine包含了4个加乘器fused-multiply-and-add(FMA),可以执行32位浮点数运算。还有四个32位的加法器。这意味着每个shader engine一共有3x4个ALU,即12条SIMD lane。Quad等同于一个warp。为了掩盖延迟latency,比如说贴图访问,这个架构可以保持每个shader engine至少256个线程在执行。
我们注意到shader engine是通用的,举几个例子来说,可以执行计算,顶点处理和像素着色。Execution engine也也支持许多超越函数transcendental functions,比如sine和cosine。另外,使用16位浮点数精度来计算的时候,性能可以提升约2倍。这些单元也支持绕过寄存器内容,对于一个寄存器的结果只是作为后续指令的输入内容(这句话我也不太理解)。这样可以省电,因为寄存器文件不需要再访问。另外,当进行一个贴图或别的内存访问时,举例说,一个quad可以被quad manager替换掉,类似于其他的架构降低延迟的操作。要注意着个发生在很细粒度的层面,只交换4个thread而不是全部的12个。Load/store单元负责通用的内存访问,内存地址翻译,和缓存一致性。属性单元Attribute unit处理属性的定位和寻址attribute indexing and addressing。他把访问发送给load/store unit。变量单元varing unit执行变量属性的插值操作。
Tiling 架构(sort-middle)的核心思想是首先执行所有的几何处理,所以每个图元在渲染时的屏幕空间位置就可以确定了。与此同时,一个多边形数据列表polygon list,包含了指向所有覆盖了这个tile的图元的指针,每个tile的framebuffer都创建一个这样的list。在这一步之后,这个tile会涉及的所有图元就都有了。因此,这个tile上的图元可以进行光栅化和着色,并且这些处理结果都会存放在on-chip的tile memory上。当这个tile完成了所有的渲染之后,tile memory上的数据就会通过L2 cache写回外部存储。这样可以降低带宽的使用。然后进行下一个tile的光栅化,如此逐步执行,直到完成所有的渲染。第一个tiling架构叫做Pixel-Planes 5,那套架构和和Mali架构有一些高层次的相似之处。
几何阶段的处理和像素处理在23.22里可视化出来。如我们可以看到的,顶点着色器被分割,一个部分只处理位置,另一个部分在tiling操作之后进行varying shading。这样相比于arm之前的架构可以进一步节省带宽。指定归类binning,就是说决定一个图元覆盖哪些tiles,唯一需要的信息就是顶点的位置。Tiler unit,执行binning操作的,如Figure 23.23里显示的是在分层的形式下进行的。这样的好处是binning操作有更小的空间占用和更好的命中率,因为这不再和图元的大小相关了。
Figure 23.22. 展示了geometry信息如何在Bifrost架构里流动。顶点着色器包含了位置计算,这个结算结果是tiler需要的,以及varying shading,这个是在tiling操作之后,如果有需要才会执行的。
Figure 23.23.这是Bifrost 架构里分层的tiler。在这个例子里,binning在三个层面执行,每一次里,三角形都会被指定给其所覆盖的区域里。
当tiler完成了标注场景里所有图元的归属操作之后,就可以得到一个指定tile上对应哪些图元的精确的结果。同样的,余下的光栅化,像素处理,和混合阶段,无论有多少的tile都可以进行并行处理,只要有足够的shader egine算力。一般来说,一个tile会提交给一个shader engine来处理所有的tile上的图元。在这些工作进行的时候,同样的也有可能可以同时处理下一帧的几何信息以及分tile的操作。这个处理模式其实也暗示了tiling的架构里可能会有更多的处理延迟存在。
此时,光栅化,像素着色,混合和其他的逐像素操作跟着执行。一个tiling架构最重要的一个特征就是一个tile的framebuffer(包括颜色,深度,模板值)可以存放在访问速度很快的on-chip memory,这里叫做tile memory。这个代价是支付得起的,因为tiles非常小(16x16个像素)。当一个tile上所有的渲染工作完成的时候,期望的输出结果(一般来说是颜色信息,也可能是深度信息)会复制到一个off-chip的和屏幕尺寸一样大的framebuffer上(在外部存储空间)。这样就意味着所有framebuffer的访问在逐像素处理的过程中都是高效到免费的地步。避免使用外部的数据总线是一件非常渴望的行为,因为这个操作会带来高昂的耗电。Framebuffer压缩也适用于把on-chip的tile memory内容输出到off-chip framebuffer的过程。
Bifrost架构还支持pixel local storage(PLS),这是一系列的拓展功能,通常会在sort-middle的架构上支持。使用PLS,可以让pixel shader访问framebuffer的颜色值,因此可以实现一些自定义的blending 技巧。作为对比,blending功能通常是对于API的参数配置,并不是可编程的。我们也可以利用这个tile memory来存储任意固定size的逐像素的数据结构。这样编程者就可以高效的实现延迟渲染技术。G-buffer(例如法线,位置,和diffuse贴图)在first pass里存储在PLS。Second pass执行光照计算并把结果累积在PLS上。Third pass使用PLS上的信息来计算最终的像素值。需要注意,对于一个tile来说,这些计算执行的背景都是在所有内存数据都是on-chip的,这就非常的高效。
所有的Mali架构从一开始就按照支持MSAA的思想开发,并且他们实现了page 143提到的旋转网格超采样rotated grid supersampling(RGSS),使用了每个pixel采样4次的方式。Sort-middle架构非常适合抗锯齿算法。这是因为filtering操作可以在tile数据离开GPU并提交给外部存储的过程中。因此,外部存储的framebuffer只需要保存每个像素一个单独的颜色值。一个标准的架构会需要framebuffer占用4倍的空间。对于一个tiling架构,只需要增加4倍的on-chip buffer,或者使用更小的tile分割方式(width和height降低一半)。
Mali的Bifrost架构可以对于每个渲染图元的batch有选择的使用多重采样或者超采样。这意味着更耗的超采样方式,每个sample都对应着一次着色计算,只会在有需要的时候使用。举个例子,渲染一个使用alpha mapping的贴图的tree,这里需要高品质的采样才能避免artifacts。对于这些图元,可以使用超采样。当这种复杂的情况结束,渲染简单的物体时,我们可以切回去使用简单的多重采样方式。这个架构也支持8倍或者16倍的MSAA。
Bifrost(包括前一代架构叫做Midgard的)也支持叫做transaction elimination的技术。这个技术的思想就是,对于两帧之间没有发生场景内容改变的frame,不进行on-chip到off-chip的内存传输。对于当前帧,每个tile都计算一个unique的signature,当被传输到off-chip的framebuffer上时。这个签名是一个校验码。对于下一帧,如果新算出来的签名和前一帧的签名一致,那么这个tile就不会被进行传输操作,因为外部的结果已经是想要的了。这对于休闲手游非常有用(比如愤怒的小鸟),每一帧场景只会改变极小的部分。需要注意的是,这个技术非常难以在sort-last架构上实现,因为他们不是per-tile执行的操作。G71芯片也支持smart composition,这就是transaction elimination在ui上的应用。这可以减少读取,组装,和写入一块像素,如果所有的输入资源和操作都和前一帧相同。
一些底层的省电技巧 也会在架构里重度使用,比如时钟门控clock gating和功率门控power gating。这个意思是不适用的或者不处于激活状态的管线部分会被关闭或者保持低能耗的闲置状态,来达到节能目的。为了减少贴图带宽,有一个texture cache,cache有专门解压ASTC和ETC格式的处理单元。另外,压缩格式的贴图以压缩的形式存储在cache里,而不是一种解压的,把纹素值存在cache里。这意味着当有一个访问texel的请求时,硬件从cahce读取一个block,并同时把这个block内容解压出来。这个设置增加了cache的有效大小,提升了效率。
一般来说,Tiling架构的一个优势是,他内在的就被设计成了并行处理tile的样子。举个例子,更多的shader engine可以被加入进来,每个shader engine都负责独立的渲染一个tile,并同时工作。Tiling架构的一个缺点是,全部的场景数据需要被发送给GPU来做tiling,并且所有处理好的几何信息也会被传到memory里。一般来说,sort-middle架构不能很理想的处理几何增强geometry amplification,比如应用geometry shader和tessellation,因为会产生更多的几何信息,这些内会在内存里来回传送。对于Mali架构来说,geometry shading和tessellation都以软件的形式在GPU上处理,并且Mali最佳实践指南推荐不要使用geometry shader。对于大部分内容来说,sort-middle架构在移动设备和嵌入式系统上运行的很好。
23.10.2 Case Study: NVIDIA Pascal
Pascal是NVIDIA的一个GPU架构。它同时作为graphics part和compute part存在,后者目标定位于高性能计算和深度学习应用。在这里,我们主要会关注graphics part,并且特别关注于一款叫做GeForce GTX 1080的显卡配置。我们将会自底向上的展示这个架构,从最小的通用ALU开始,然后逐步介绍整个GPU。这一节末尾我们也会简要的提到一些其他的芯片配置。
在Pascal图形架构里使用的通用的ALU,NVIDIA称之为CUDA核心,拥有和1002页Figure 23.8里描述的相同的高层视图high level diagram,ALU关注点在于浮点数和整数的运算,不过也支持一些其他的运算操作。为了提升算力,几个ALU会组成一个流处理器streaming multiprocessor(SM)。在Pascal的graphics part里,SM由4个处理块组成,每个块有32个ALU。这意味着SM可以同时执行4个warp,每个warp有32个线程。这个在Figure 23.24里展示。
Figure 23.24. Pascal流处理器有32x2x2个通用ALU,并且每个SM还配有一个polymorph engine,并与其一起组成了一个Texture processing cluster(TPC)。注意顶部的灰色盒子区域,下方也是一个和它一样的复制,不过下面这个复制里面有一部分内容省略没显示,其实两个区域是一样的。
每一个处理块,即是一个宽度为32的SIMT engine,还用8个load/store(LD/ST)units和8个特殊函数单元special function units(SFUs)。Load/Stpre units负责读写寄存器文件里的寄存器值,寄存器文件的大小是16384x4个bytes,即是64kb每个处理块,所以每个SM一共是256KB。SFU处理超越函数指令,比如sine,cosine指数(以2为底),对数(2为底),倒数和倒数平方根。他们也支持属性插值。
SM里所有的ALU都共享一个instruction cache,但是每一个SIMT engine都有一个自己的instruction buffer来存储最近加载的指令,来增加缓存命中率。Warp scheduler每个时钟周期可以发送2个warp instruction,例如一个时钟周期内任务可以同时分配给ALU和LD/ST units。需要注意每个SM也有2个L1缓存,每个有24kb的空间,即每个SM有48kb的空间。使用2个L1的理由看上去是,因为一个大的L1会需要更多的端口port,这样会增加cahce的复杂度以及使得chip上的实现更复杂。另外,每个SM还有八个texture units。
因为着色必须是在2x2的像素的quad上进行,warp调度器会寻找8个quads并且组合起来,然后在32 SIMT lanes上执行。因为这是通用的ALU设计,warp scheduler可以把vertices,pixels,primitives或者computer shader的任务组合在一起。需要注意的是一个SM可以处理同时不同类型的warp(比如vertices,pixels,和primitives)。这个架构同样在切换一个正在执行的warp和一个ready状态的warp的时候,不存在额外开支。在关于如何确定下一个要执行的warp的技术细节,Pascal架构是没有公开的,但是之前的NVIDIA架构里我们可以寻找到一些线索。在NVIDIA从2008年开始使用的Tesla架构里,一个scoreboard被用于在每个时钟周期时整备warp。Scoreboard是一个通用的机制,来保证无序的执行不会产生冲突。Warp scheduler会在warp里选择已经准备好的来执行,例如并不再等待贴图加载返回值的,并且选择高优先级的。Warp种类,指令的种类,和“公平fairness”是用于选择最高优先级warp的参数。
SM和多形体引擎(用于曲面细分的)polymorph engine(PM)组合工作的。这个unit首先是在Fermi chip上出现的。PM处理多个和几何体相关的任务,包括顶点获取vertex fetch,曲面细分tessellation,同步多重投影simultaneous multiprojection,属性设置attribute setup,和流输出stream output。第一个阶段是从全局的vertex buffer里拿顶点数据,然后发送给SM warp来做vertex和hull shading(曲面细分的control shader)。随后是一个可选的tessellation阶段,这一阶段新生成的(u,v)坐标被发送给SM进行domain shading(曲面细分的evaluate部分),以及可选的一个geometry shading。第三个阶段是处理viewport转换和透视矫正。另外,一个可选的同步多重投影也是在这执行,举个例子,这个东西可以用来做VR渲染。接下来第四个阶段也是一个可选的阶段,处理好的vertices可以被选择输出到memory中。最后,处理得到的结果被推向相关的raster engine。
一个raster engine有三个任务,即是,三角形组建triangle setup,三角形遍历triangle traversal,和z-culling。三角形组建阶段获取vertices数据,计算边的公式edge function,执行背面裁剪。三角形遍历使用一个层级分块hierarchical tiled的遍历技术来访问三角形覆盖的每个tile。使用edge function来执行tile测试和内部测试inside test。在Fermi上,每个光栅器一个时钟周期可以处理8个pixel。Pascal则没有公开这一数据。Z-culling单元在一个逐tile的基础上来处理culling操作,使用Section 23.7里描述的技术。如果这个tile被裁剪掉了,那么在这个tile上的处理流程会立刻结束。对于通过测试的三角形,逐顶点属性会被转换到平面方程plane equation,这样可以在pixel shader里更有效的计算。
流处理器和polymorph engine一起被称为texture processing cluster(TPC)。在更高的一个层面,五个TPC组合称为一个graphics processing cluster(GPC),每个GPC都有一个raster engine来服务他们的5个TPC。一个GPC可以被认为是一个小型的GPU,这个设计目标是提供一组平衡的图形硬件单元,即是vertex,geometry,raster,texture,pixel和ROP单元(帧缓冲操作模块,Render output unit)。我们会在这一节末尾看到,创建一个分离的功能模块可以允许设计者更简单的各种功能的GPU芯片。
此时,我们了解了大部分的GeForce GTX1080构建模块。它是由4个GPC组成的,这个常规的设置在Figure23.25里显示。注意还有另一层的调度设计,由GigaThread engine来支持,并拥有一个PCIe v3的接口。GigaThread engine是一个全局的工作分配引擎,可以调度Threads给所有的GPC。
Figure23.25. Pascal GPU GTX 1080配置了20个SM,20个polymorph engine,4个raster engines,8x20=160个texture units(峰值读取率277.3G texels/s),256x20=5120kB的寄存器文件,和总计20x128=2560个通用ALU。
Raster Operation units也在Figure 23.25里显示了,尽管有些隐藏。他们仅仅位于L2缓冲的上下方,在图的中部。每个蓝色的block是ROP unit,这里有8组,每组8个ROP一共64个。ROPunit的主要任务是把输出的pixel写入其他的buffer里,并且执行一些操作像是blending。可以在图里左右部分看到,有一共8个32位的memory controller,总计位宽是256bits。八个ROP units被指定给一个memory controller和256kB的L2 cache。整个芯片有共计2mb的L2 cache。每个ROP都捆绑于一块指定的memory区域,这意味着ROP也只会处理buffer里固定的一部分pixel。ROP unit也可以处理无损压缩。除了支持无压缩并且快速的clear操作以外,还有三种不同的压缩模式。对于2:1的压缩(比如说从256B到128B),每个tile都会存一个reference color值(作为基础值),然后pixels之间会encode差异值,差值相比于未压缩的原始值占用更少的bit。然后说这个4:1的压缩是2:1模式的扩展,但是这种模式只会在差值可以被编码为更小的位数的时候才会开启,所以这个模式只会在这些内容变化很平滑的tile上工作。还有一个8:1的模式,对于2x2的pixel block的constant color进行4:1的压缩,并且在此之上还有个2:1的压缩模式。8:1的模式比4:1的模式优先级更高,4:1又高于2:1,就是说,tile压缩的时候,总是会去使用最高压缩率的模式。如果所有的压缩尝试都是失败了,tile只能用未压缩的模式传输并存储在memory里了。Figure23.26里是Pascal系统的压缩效率展示。
Figure 23.26. 渲染的最终结果展示在左边,中间的是对压缩结果进行可视化的样子,使用的Maxwell架构(Pascal之前的架构),邮编的是Pascal架构的压缩可视化。图片里有更多的洋红色,说明buffer的压缩率更高。
显存使用的是GDDRX5,时钟频率是10GHz。之前我们讲到了8个memory controller提供了总共256bits=32B位宽。者提供了总计320GB/s的总内存带宽峰值,但是拥有压缩技术的多级缓存的组合,提供了更高的效率提升。
芯片的基础时钟频率是1607MHZ,并且可以在电源充裕的时候使用boost模式1733MHz。计算能力的峰值就是
数字2来自于fma指令通常被认为是2个浮点数操作,并且我们除10^6来把单位从MFLOPS转化为TFLOPS。GTX 1080Ti有3584个ALU,这带来了12.3TFLOP的算力。
NVIDIA长期以来都是开发一种sort-last fragment的架构。但是,自从Maxwell这代版本,他们也支持一种新的渲染方式叫做tiled caching,这是一种介于sort-middle和sort-last fragment之间的设计。这种架构在Figure 23.27里展示。这个设计的思想是利用L2 cache的局部性。Geometry按照一种足够的块来处理,这样输出会一直留在缓存里不会被置换出去。另外,framebuffer也存留在L2里,只要和当前tile重叠的geometry没有完成pixel着色。
Figure 23.27. Tiled caching架构引入了一个binner,这个东西的作用是把geometry排序给tile,并且让转换后的geometry停留在L2缓存里。当前进行处理的tile同样留存在L2里,直到当前chunk里这个tile上的geometry全部处理完毕。
Figure 23.25.里有四个raster engine,但是我们知道graphics API(大多数的情况下)必须遵从图元的提交顺序。Framebuffer一般会分割为不同的tile,使用通用棋盘模式generalized checkerboard pattern,并且每个raster engine都负责一系列的tile。当前的三角形会被送往所有raster engine,条件就是其包含tile中有一个被这个三角形覆盖的,这样就可以独立于tile来解决排序问题。这样会提供更好的加载平衡。GPU架构里一般还会有几个先进先出队列FIFO queue,这个东西可以减少硬件单元的等待starvation。这些队列在我们的图表里并没有展示。
Display controller的每个color component有12 bits的位宽,并且有BT.2020色域支持。还支持HDMI 2.0b和DDCP2.2。对于视频处理方面,他支持SMPTE 2084,这是一个对于高动态范围HDR视频的转换方程。Venkataraman介绍了NVIDIA芯片架构从Fermi开始,并在后来拥有了一个或更多copy engine。这些本质上也是memory controller但是可以执行DMA传输(直接访问内存direct memory access)。一次DMA传输在CPU和GPU之间进行,这个传输可以由双方的任意一方发起。并且在这个传输中,发起的处理单元还可以进行其他的计算操作。Copy engine可以发起一次CPU和GPU之间的数据DMA,并且他们可以独立于GPU的其他部分运作。因此,GPU可以在数据在CPU和GPU之间相互传输的过程中,也继续进行渲染或者其他的操作。
Pascal架构也可以搞非图形的应用,例如训练神经网络或者大规模数据分析。Tesla P100是一个这种设计的产品。其和GTX 1080的差异包括使用了高带宽内存2(HBM2),内存总线的位数是4096,提供了总计720GB/s的带宽。另外,他们有原生的16位浮点数运算支持,是32位浮点数运算性能的2倍,并且极大的加速了双精度处理。SM的配置也是不同的,以及register file也是不一样的。
GTX 1080Ti(titanium钛)是一个高端的型号。它拥有3584个ALU,352位的内存总线,总计484GB/s的带宽,88个ROP,和224个texture unit,和GTX 1080相比,这些数据分别是2560,256位,320GB/s,64,160。它的配置里有6个GPC,就是说6个raster engine,在GTX 1080上只有4个。其中4个GPC完全和GTX 1080相同,还有2个GPC是小一号的,只有4个TPC组成而不是正常的5个。1080 Ti的芯片上有120亿个晶体管,1080上只有72亿。Pascal架构是非常灵活的,他可以设计为更小的规模。例如,GTX 1070是1080减少1个GPC,GTX 1080是2个GPC组成,每个GPC只有3个SM。
网友评论