Something about PS2(完整版)

2005-06-29 01:28 | mazda

鉴于《PS位元与PC位元》一贴中的某些问题,并想对一些资料文章进行一定程度的补充,再加上比较闲,于是写了这样的文章,作为知识普及用。其中尽量使用通俗的,易于理解的表述方式,不过限于水平,恐怕还有诸多问题,望指出。

一:一些基础知识,对于有基本的3D图形学知识的人可以略过

1:计算机中的3D空间、变换及其表示方法

如大家通常所用,3D空间中任何一点均可由形如(x,y,z)的一组矢量表示,x,y,z代表的是该点在三个互相垂直的坐标轴上的投影(即该矢量与各正交基矢量的点积)。所有(x,y,z)的集合即构成了整个3D空间,空间中同一点在不同的坐标系下会有不同的坐标值表示(唯一性),任何一个坐标系都可以表示整个3D空间(完备性)。

如何将一个矢量进行平移,旋转,缩放等操作呢?如果单独考虑,那么平移可以认为是矢量相加,缩放可以认为是矢量分量相乘等等,不过这样没有一个公共的,统一的方案,让人觉得非常麻烦而丑陋。后来,随着线性代数的发展,数学家发明了矩阵这个东西,通过矢量与矩阵的乘法就可以将一个矢量作线性变换,同时还有诸如“不同变换所对应的矩阵逆向相乘可以得到这些变换的和变换所对应的矩阵”等等方便的性质,于是矩阵就顺理成章地成为了空间变换的最佳表示方法。

不过,存在一个问题,比如对于3D矢量来说,一个3x3的矩阵可以轻松实现矢量缩放,旋转等操作,但对平移却毫无作用,如何将平移也统一到矩阵操作中呢?数学家们又发明了“齐次坐标表示法”——即将N维矢量用N+1维齐次坐标表示,进行变换,然后再齐次映射回原有空间的方式。拿3D矢量来说,(x,y,z)的齐次坐标表示为(nx,ny,nz,n)——n为任意非零数,一般取n为1,即(x,y,z)->(x,y,z,1),于是原先的3x3变换矩阵变为4x4变换矩阵。熟悉线性代数的人一定可以很快发现,矢量的平移可以很轻易地统一到4x4矩阵中去,于是,3D中的线性变换问题就迎刃而解,所有线性变换都可以用矩阵表示了。回头来看,无论是PS2的VU,IA32中声称为专3D加速的SSE还是GPU中的VS,PS都非常重视四元向量运算,原因就在于“用四元齐次坐标进行3D空间运算”所造成的4D运算极其重要。

2:计算机如何将3D空间中的东西显示出来?

首先推荐一种理解方式,在计算机的3D显示过程中存在两类空间,一种是连续的3D空间,另一种是离散的2D空间,前一种很好理解,因为3D矢量,3D物体等必然存在于3D空间中,所以这个空间有其存在的必要性。而离散的2D空间是什么呢?呵呵,那就是屏幕的空间啊,仔细一想,屏幕是由二维的、离散的像素点构成嘛,而我们直接看到的也就是这个东西,于是,离散的2D空间也有其存在的必然性。

很显然,计算机不可避免地要在这两种空间中作变换,将3D空间映射到2D空间的变换叫做“投影+齐次坐标归一”变换,而将连续的2D空间离散化叫做“光栅化”操作。

关于前者,可分为两个部分,一个是投影变换,该变换是一个线性变换,可以通过一个矩阵相乘来实现。而“齐次坐标归一”是指将(x,y,z,w)变成(x/w,y/w,z/w,1)的过程,投影变换使越远的物体的w值越大,从而经过齐次坐标归一后就有“近大远小的透视关系”这种效果,另外很显然齐次归一是一个非线性变换过程。
关于空间变换、投影矩阵和齐次归一的细节内容大家不必了解,只需要知道它的作用即可:一个向量表示的顶点(x0,y0,z0,1)要经过前期变换(如平移,旋转等,称该变换为世界变换)以达到它应到的位置,然后要将坐标系变换到摄像机空间(即以摄像机位置为坐标原点,摄像机朝向和摄像机正上方向为坐标轴方向的空间),设此时点坐标为(x1,y1,z1,1)。然后再经过这“透视归一”变换后,变为(x2,y2,z2,1),这时,x2,y2直接与屏幕上的2D位置线性对应,而z2表示的是相对的深度,可用作遮挡关系判断。

将一个三角形的3个顶点都如上变换完毕后接下来的工作就是光栅化操作,把每个三角形对应到屏幕上相应的像素点。很显然,就标准情况下,这个精度为像素宽度,所以锯齿等现象就不可避免了。分辨率越高,锯齿就越细越不易分辨。而FSAA等操作的原理是使用更高精度的光栅化操作,然后使用低分辨率高比特率采样(也就是down sample操作)来达到减少锯齿的效果。

光栅化后,一个三角形就已经形成一片一片的点的集合,在OpenGL中每个点称为“片断”(fragment),之后的操作就是片断级别操作,比如纹理贴图,方式是在光栅化时计算出每个片断对应的纹理坐标,然后到纹理中使用该坐标采样来确定该片断的颜色。

接下来的工作就是将一个个的片断写入缓存(后台缓冲区)中。写完所有片断后就可以将该缓冲翻到前台显示出来。至于写入缓存前还有一些操作,比如,深度检测,用来确定遮挡关系(不要把本应该被遮挡的片断显示出来)alpha检测(只显示一定alpha值以上的片断)等,写入缓存时还有一种操作,叫做alpha混合,也就是把要写入的片断的颜色与缓存中已经存在的颜色按照一定规则混合起来,比如:将混合模式设为相加,要写入的片断为红色,而缓存中已经存在了绿色,那么最终的颜色就是“红色+绿色 = 某种黄色”这样。
到此,基础知识告一段落。

二,PS2,XBOX,NGC,的各部件在3D显示中的分工及其区别

为方便起见,此后的代码都用asm shader代码代替,这样便于阅读的统一性
由上一部分,大家粗略了解了计算机显示3D部分的流程,从mesh数据准备->顶点变换处理->光栅化->片断处理->后片断处理->alpha混合->显示,为一个典型方式,一般将“顶点变换处理”至“alpha混合”称为一个pass。三角形的一个pass就代表它从它的三个顶点几何变换开始,直到它被写入缓存的过程。

可以想象,顶点处理要大量使用4元矩阵和4元向量运算,而现在的一个典型的场景数万个三角形是很正常的事情,即使前期剪裁(诸如可视性判断等)做得非常好,预先剔除了该帧中大部分不可见三角形,一帧中还是会显示成千上万的三角形。所以,顶点变换是一个相当繁重的工作,需要单独硬件来处理。而光栅化和光栅化之后的操作更加繁重,比如640x480的分辨率,即使假设没有重复绘制(overdraw),那么处理的片断也在640 * 480,也就是数十万级别上,再加上纹理采样等比较复杂的工作,所以更需要专用硬件来处理,从PC的3D发展历程来看,是经历了“纯CPU运算,显卡只提供帧缓存(早期诸如trident 9440等)”->“出现硬件光栅化渲染器(如TNT)加速光栅化和光栅化之后的操作”->“出现可以加速全过程的GPU(如Geforce)减少了CPU的负荷”->“出现可编程的GPU(如Geforce3),大大增加了灵活性和功能”。而PS,N64等属于专用硬件,所以走得比同期PC为先,那么到了PS2,NGC,XBOX呢?让我们一一来看。

PS2走的是完全不同于PC的路子,它使用了一个矢量处理性能强劲的CPU——EE和一个海量填充率,内部带宽超高的光栅化渲染器——GS。至于其性能,大家可能早有耳闻,EE的R5900和VU0,VU1(EE的两个矢量处理单元)合作起来最大可以达到21Flops/cycle,乘上频率300Mhz得到了6.2GFlops/second的惊人能力,而由于EE本身属于顺序执行结构,对流式数据(如顶点流)非常适合,所以作为顶点处理单元十分胜任。而至于GS,它提供了令人惊讶的16管线和8个纹理单元,也就是说每周期可以输出8个无纹理像素和8个有纹理像素,不过今天看来无纹理像素其实比较多余……GS内部使用的是eRAM,与GS之间的带宽达到了1024+1024+512 = 2560位为48G/s,容量为4M,至于这个的用法后面会详细叙述。同时,EE和GS之间的带宽为1.2G/s,EE和主内存带宽为3.2G/s.
看来PS2走的是注重性能的一条新路,那么NGC和XBOX呢?

XBOX走的路和PC如出一辙,采用x86 CPU(类似于赛扬的733Mhz处理器,带有MMX和SSE)+第一代可编程GPU(NV2A,类似Geforce4 Ti的产品,两个顶点处理单元,4x2的像素处理结构,233Mhz,支持VertexShader1.1,PixelShader1.1及对应的XBOX shader扩展和NV特有的depth-stencil texture)属于DX8+级别硬件,XBOX采用统一内存架构,64MB内存公用带宽6.4G/s,就灵活性上,VertexShader1.1无法和R5900+VU1(PS2中VU1负责顶点处理)相比,而PixelShader的灵活性和功能远强于GS,但是看来性能指标输GS不少,真的如此么?我们后面会说。

NGC走的是一条中庸路线,它的主CPU是强化了单精度浮点的gekko处理器。用来执行通用操作,而顶点处理和片断处理交给了ArtX(为ATI收购)的Flipper芯片,它类似于DX7级别的GPU,有固定功能的顶点处理管线(fixed function vertex pipeline)和可执行8次纹理贴图(也就是说每个贴图单元可以loopback达到一个pass的8重贴图,不过这比较消耗时间)的4x1像素管线,主频162Mhz。就灵活性上,固定功能顶点管线灵活性低,属于牺牲灵活性来换效率的方式,而像素单元的灵活性又介于GS和NV2A之间。它采用低延迟的3MB的1T_SRAM作为片内缓存(1M作为纹理缓存),带宽为10.4G/s纹理和7.6G/s帧缓存,系统内存带宽为2.6G/s。

可见,XBOX和NGC都是使用GPU的产物,而PS2属于使用强大CPU+向量处理+光栅化渲染器的产品,各有各的优点和缺点,粗看来PS2的性能最为强劲,事实如何呢?我们先来看一下顶点处理能力。

VU1为PS2的标准顶点处理单元,不过VU0+R5900也可以作为顶点处理单元使用(它和VU1的运行模式不同,向GS中传送数据的通道也不同),在两者并行下,sony标称其峰值能力为66M triangles/s。这个数是如何得来的呢?

当VU1,VU0进行4D矢量处理时,可以做到“乘加”mad指令(mad a,b,c,d代表 a = b*c+d)的延迟为4周期,吞吐为1周期,所谓延迟4周期,就是说我将指令发出,那么4个周期后能够得到运算结果,而吞吐1周期,代表我每个周期都可以发出一条mad指令,这是由流水线结构得到的,只要两个指令中没有相关(相关指诸如上一步的运算结果作为下一步的运算变量等等)那么就可以尽量排满流水线,比如弱相关指令如下排列(假设无cache miss):mad,mad,mad,mad,mad,mad……一共N个,那么,第5个周期时,第一个周期的mad结果就出来了,而第6个周期时第二个周期的mad结果也出来了,依此类推,当最后一个结果出来时,共需要N+4个周期,于是每个指令的平均时间可认为是(N+4)/N,当N比较大时,该值趋近1,也就是吞吐量的值,可见,对于流式弱相关指令,吞吐量可基本代表该指令的速率。这也就是所谓的“单周期指令”的由来。

就是说,两个VU每秒可以执行300M次mad指令,很显然,mul,add等指令同样可以达到这个速度。同时,VU还具备FDIV单元,可以执行除法和方根等运算,当VU运行于单独模式(micro模式)时,指令长度为64bit,是高32位与低32位并发,高32位控制mad单元,低32位控制FDIV等单元。VU0与R5900协作模式(macro模式)受到一些限制,不过还是可以使用一些方法达到近似的处理能力。

第一章说了,任何一个顶点都需要做空间变换才能进入光栅化操作,空间变换即为4x4矩阵,如果在理想情况下预先把世界变换,摄像机空间变换,投影的变换的矩阵乘起来(因为摄像机变换和投影变换是场景属性的,和场景中的物体无关,而假设所有物体,都同样运动,那么它们的世界变换矩阵也都相同),那么针对每个顶点,则只需进行一次矩阵乘法即可,4x4矩阵乘法,可以分解为一个4D乘法mul和3个4D乘加mad,再带入该指令单周期性质,可得到变换一个顶点需要4个周期。

接下来的情况是做“齐次归一操作”,对于PS2体系来说,这个操作同样需要VU来计算,这里涉及一个除法问题,按照SCE的《EE overview》的说法,矩阵乘法与齐次归一一共需要8个周期完成(即吞吐量为8)。至于这个8是如何得来,那么据我分析应该如下:

1:归一化的计算方法是先求出1/w,然后用该结果与xyz相乘,一个顶点的处理顺序为mul mad mad mad rcp mul
2:SCE的《EE overview》提到,FDIV的指令延迟和吞吐量均为7。
3:由于一个顶点变换至少需要一次FDIV,所以一个顶点变换的吞吐量不可能小于7
4:隐藏空周期可以使用多顶点交叉处理(好在VU还存在一定数量的寄存器),设顶点为v0 - vn,mad vn 代表与顶点vn相关的mad指令。则代8 throughput的vertex transformation代码可能如下:

mul v0//开始处理v0
mad v0
mad v0
mad v0
nop
nop
nop
rcp v0 //到此mad v0的latency结束,w值结果得到,开始执行rcp v0
mul v1//开始处理v1
mad v1
mad v1
mad v1
nop
nop
mul v0//到此rcp v0的latency结束,执行v0的最后一步,(x,y,z)* (1/w)
rcp v1 //到此mad v1的latency结束,执行rcp v1
mul v2//开始处理v2
mad v2
mad v2
mad v2
nop
nop
mul v1//到此rcp v0的latency结束,执行v0的最后一步
rcp v2//到此mad v2的latency结束,执行rcp v2
……………………
……………………

可见:
1:输出每个顶点结果相隔8个周期,即throughput(吞吐量)为8
2:输出每个顶点的总延迟为8+7+4=19,即latency为19
3:以上两数值与《EE overview》中资料吻合。
4:可见,由于多周期吞吐量,多周期延迟与强相关指令并存,所以给程序优化带来了很大难度。而且99%的情况会有空周期(nop)存在,也就是说流水线利用率不完全,存在流水线气泡。这是UV处理的特点,而GPU的Hardware T&L和VertexShader则编程省事很多,且流水线效率更高(应用硬件多线程来隐藏延迟),这是牺牲一定程度的灵活性换来的效率提高。


变换后的顶点xyz坐标需要传送至VU内存(VU1)和主内存(R5900+VU0)中才能供GS使用,传送为单周期指令。于是,经过8+1 = 9个周期,2个顶点处理完毕,所以每秒处理的顶点数量正好为300M*2/9 = 66.67M

有人一定会问:难道顶点数 == 三角形数么?
这里有一个小trick,即为三角形绘制方式,三角形通常有三种绘制方式,如下图所示(引自GS user's manual):一种是三角形列表(triangle list)为每3个顶点绘制一个三角形,3N个顶点可以绘制出N个三角形,如P1,P2,P3,P4,P5,P6六个点,则P1,P2,P3为一个三角形,P4,P5,P6为第二个三角形;第二种为三角形条带(triangle strip),如P1——P6六个点,那么P1,P2,P3为第一个三角形,P3,P2,P4为第二个三角形,P3,P4,P5为第三个三角形,P5,P4,P6为第四个三角形。也就是说N+2个顶点可以画出N个三角形;还有一种叫做三角扇形(triangle fan)比如P1——P6六点,那么P1,P2,P3为第一个三角形,P1,P3,P4为第二个三角形,P1,P4,P5为第三个三角形,P1,P5,P6为第四个三角形,这种情况下同样N+2个顶点可以绘制出N个三角形,那么每个三角形所需要的顶点数为(N+2)/N,当N很大时,这个值趋于1,也就是1个顶点对应一个三角形。


于是大家可以看出,PS2标称的66M triangles/s是如何来的,总结一下,66M/s的条件为:1,没有光照,没有纹理,没有顶点颜色,且物体的运动极其统一易于分类(骨骼动画什么的,带光照什么的就没戏了,最好都是无光照的静态物体)2:完全以三角形条带或者三角扇形绘制(这个很难,基本实际过程中顶点数与三角数的比在2左右)。

按照这种算法,那么XBOX的NV2A的三角转换性能为233M*2/(4+2) = 77.7M triangle/s,NGC的Flipper为162M/4 = 40M triangles/s(可见NGC的效率更高,那是因为它是固定管线结构,可以大量采用hardwire来优化,在存在顶点颜色前提下,NGC的三角形速率为32M/s)。这还不算XBOX和NGC的CPU也辅助运算的理想情况

很显然,这种方式得出的数值在实际中没有意义。实际中情况如何呢?我们不妨做个假设,设顶点数/三角形数 = 2;采用点光源照明,计算逐顶点漫反射和高光(镜面反射)3:非骨骼动画,4:未加光线衰减。这个应该近似于通常情况中较复杂的运算量。下面用asm vertex shader写下代码:(很接近于XBOX方式)

//c0-c3 4个4D寄存器储存视点、投影矩阵的乘积,c4-c7储存世界矩阵的转置逆矩阵,c8-c11储存世界矩阵。c12储存点光源位置,c13储存摄像机位置,c14储存环境光照强度

def c15,2.0,1.0,0.0,1.0
dcl_position v0//顶点初始位置位于寄存器v0
dcl_normal v1 //顶点法线位于寄存器v1
dcl_texcoord0 v2 //顶点纹理坐标位于寄存器v2

mov oT0, v2 //输出纹理坐标

mul r0, v0.x, c8
mad r0, v0.y, c9, r0
mad r0, v0.z, c10, r0
mad r0, v0.w, c11, r0 //顶点进行世界转换

mul r1,r0.x,c0
mad r1,r0.y,c1,r1
mad r1,r0.z,c2,r1
mad oPos,r0.w,c3,r1 //再进行视点——投影转换并输出到位置寄存器

mul r1,v1.x,c4
mad r1,v1.y,c5,r1
mad r1,v1.z,c6,r1
mad r1,v1.w,c7,r1 //顶点法线的世界变换
dp3 r2,r1,r1
rsq r2.y,r2.x
mul r1,r1,r2.y //法线归一化

add r2, c12,-r0
dp3 r3,r2,r2
rsq r3.y,r3.x
mul r2,r2,r3.y //光源向量及其归一化

add r3, c13,-r0
dp3 r4,r3,r3
rsq r4.y,r4.x
mul r3,r3,r4.y //视点向量及其归一化

dp3 r4,r3,r1
add r4,r4,r4
mad r3,r4,r1,-r3 //得到反射矢量

dp3 r1,r1,r2
add oD0,r1,c14 //漫反射强度输出到漫射颜色寄存器

dp3 r2,r2,r3 //反射矢量点乘光线
max r2,r2,c15.z //剪掉小于0部分归其为0
mul r3,r2.x,r2.x //二次方
mul r3,r3,r3 //四次方
mul r3,r3,r3 //八次方
mul oD1,r3,r3 //十六次方,为 高光的指数公式
//-----END-----


以上代码在XBOX的GPU单元需要的周期约为35 cycle(还可以优化),所以对于XBOX来说,所以三角形变换速度为233M*2/(35*2)约为6.6M。NGC由于其为固定功能管线,所以无法按照这个算法计算,不过固定功能管线易于硬件优化,所以预计实现相同效果效率应该更高也就是周期更少,事实上,NGC如果不计算点光源高光反射(NGC的固定管线不能正确计算点光源的高光反射,只支持平行光源的高光反射)上述代码的其他的效果不变,则速度可以达到10M triangles/s。 至于PS2,首先VU需要做齐次归一运算,且要输出到内存中这需要多出数个周期(我们假设为5周期,这样到此一共为40周期),其次由于该代码复杂,强相关指令众多,多延迟指令众多,所以流水线气泡必然众多(有兴趣的人可以自行排列一下指令,就会发现无论如何优化顺序,想做到无空周期都是不可能的,同时也可以了解一下VU编程的辛苦呵呵),假设只有20%的流水线气泡(对于这种代码已经极其理想),那么就一共需要50周期。于是对于PS2来说,三角形速度为300M*2/(2*50) 约为 6M/s左右,速度看来相当不错(注意这些数值只是一个大概)。

还有一点需要注意,以上数值是在R5900+VU0 与 VU1同时运行三角形转换的情况时达到的,实际上,R5900+VU0通常需要处理物理运算,或者同时要处理物理运算及其相关顶点运算,所以R5900+VU0与VU1完美并行处理同种三角运算的机会极其稀少,所以实际情况中EE的三角形处理能力还要减少,而对于XBOX和NGC来说,由于它们是CPU+GPU结构,所以物理等运算可以完全交给CPU处理(事实上GPU就是想处理物理运算也基本是无能为力),GPU的顶点单元全力处理顶点运算等3D mesh相关。所以没有EE的担忧,当然,EE由于灵活性更高,处理诸如物理作用下的动态模型时,比CPU+GPU更为有效,因为它既直接有效操纵顶点又具备通用性。(NGC和XBOX的CPU也可以直接处理顶点作为对GPU的辅助,不过速度不是很出色)

到此大家对顶点处理有了个大概的认识,总结一下,EE在具有不错的性能前提下具有相当好的灵活性。缺点是编程相对麻烦,优化方式复杂。

三:PS2光栅化数据详解。

先贴一些数据:
1:无贴图,flat着色模式,无抗锯齿——75M triangles/s
2:有贴图,flat/gouraud着色模式,无雾化,无抗锯齿——37.5M triangles/s
3:有贴图,flat/gouraud着色模式,无雾化,有抗锯齿——18M triangles/s
等等……

这些数字表示的是什么呢?这要先从GS的光栅化方式说起。

由第一章的说明可知,光栅化即为将一个三角形离散为像素点(或称为片断)的过程,由于经过透视变换和齐次归一操作后,三角形的顶点x,y坐标已经和屏幕点线性对应,那么问题就转为如何确定这三个顶点所包含的像素区域,经典算法为扫描线光栅化算法,也就是从一个顶点(如左上顶点)对应的屏幕像素开始,逐行扫描,先确定每一行中的三角形的两条边所对应的像素,然后处理这两个像素之间的像素。以次类推就可处理完三角形所包含的所有像素。

这种算法本身没什么问题,那么主要问题就是如何提高速度,要尽量减少除法运算,目前比较通用的是DDA(Digital Differential Algorithm,差值算法),简单说就是:
1:通过处理三个顶点数据确定每条边的斜率。
2:斜率倒数即为该边对应相邻扫描线的横坐标差值(称为边横坐标的DDA值),由于直线的恒定斜率,于是逐条扫描线的两端点就可以通过从顶点逐次加上对应两边的“边横坐标的DDA值”得到。
3:根据三角形性质设定(如着色模式)计算顶点属性(如深度,颜色,纹理坐标*等)对应三角形每条边的DDA值。同样使用逐次加法就可以得到每条扫描线的端点的属性。
4:得到每条扫描线的端点的属性后,计算该扫描线上这些属性的DDA值(每条扫描线的横坐标DDA值显然是1)然后从一个端点延水平方向逐像素累加DDA值就可得到这一扫描线上所有属于三角形内像素的属性。

*特别提及一下纹理坐标的DDA方法,首先,传入GS的顶点纹理坐标(t,s)为(T/w,S/w),T,S为实际的的顶点纹理坐标,w为投影变换后的顶点齐次坐标。为何要这样,因为:
1:在几何空间中,T,S和几何空间坐标X,Y,Z线性对应(建模特性)
2:X,Y,Z和w线性对应(齐次坐标和线性变换特性)
3:所以T/w,S/w和X/w,Y/w,Z/w线性对应
4:所以t,s的DDA和X/w,Y/w的DDA线性对应,如果X/w,Y/w的DDA为常量,则t,s的DDA也为常量。
5:同样,rhw = 1/w的DDA值也和X/w,Y/w的DDA线性对应。
6:因此,DDA累加得到的是T/w,S/w的值,需要除rhw从而得到对应的T,S值。
7:这个过程叫做纹理的透视校正。
下图举了一个纹理透视校正的例子,设一张一半白一半红的纹理贴在一个长方形上,则左下为无纹理透视校正的情况,右下的图为有透视校正的情况。

就理论上说,任何顶点属性光栅化时都需要透视校正,包括纹理坐标,顶点颜色(gouraud shading),雾化因子等,不过对于GS来说,只有纹理坐标可以进行透视校正。


回到本章开始的内容,那些数值代表的是Triangle Setup,所谓Triangle Setup,即为根据三角形顶点得到各属性的DDA值的过程,可见,不同的三角形绘制属性需要的DDA数量不同。所以所需要时钟周期数不同。比如对应“无贴图(无需计算t,s,rhw的DDA),flat着色模式(无需计算顶点颜色的DDA),无抗锯齿(无需计算Cov的DDA)”情况下,计算一个三角形的DDA一共需要2个时钟周期,按照GS的150Mhz频率,那么这种情况下的triangle setup速率即为75M/s
在开发人员眼中。上面这些数据说明了一个问题,那就是,triangle setup速度不会成为瓶颈,因为EE的三角形变换速度多数情况在10M上下,这些三角形还要经过:背面拣选、视锥剪裁和自定义平面剪裁(**)三个过程,这三个剪裁已经刨除了相当数量的三角形,所以即使在一些情况下triangle setup的三角形数量比变换剪裁后的数量翻倍(这种现象在第五部分会有解释),triangle setup速度依然不会成为瓶颈。也就是说,GS的triangle setup速度属于fast enough,再快也没有意义了,至于XBOX等,包括PC上的显卡,似乎没见他们如何宣传triangle setup速度,那是因为从很早以前,triangle setup就已经不是瓶颈所在了(记忆力好的人大概还记得Voodoo1、Voodoo2、TNT当初宣传的1M triangle/s、3M triangle/s和8M triangle/s吧)总之,triangle setup属于“够用就ok”的东西,过分强调是没有道理的。

**背面拣选——只允许顶点按照某方向排列(顺时针或逆时针)的三角形通过拣选,这个的原因在于,三角形的顶点排列方向决定了三角形的正面朝向(左手或右手定则),而对于凸多面体来说,只有正面的三角形才是可见的。所以通过背面拣选过程就可以直接除去一些不可见的多边形。

视锥剪裁——在顶点经过齐次坐标归一后,x,y直接和屏幕坐标线性对应,而z值小于0的表示在近摄像机剪裁平面以前,z值大于1的表示在远摄像机剪裁平面之后,于是,将顶点xy在屏幕之外的三角形,z值小于0和大于1的三角形直接剔除不参与triangle setup和光栅化操作。这些三角形显然是不可见的。可见,参与triangle setup和光栅化的三角形存在于齐次归一化的空间内(和屏幕空间同性质)的一个长方体中(长宽与屏幕对应,深度在0,1之间),而在未归一化的几何空间中,这个长方体就变为一个截锥体,称为视截锥,所以这个剪裁也称为视锥剪裁。见下图(引自MSDN)

自定义平面剪裁,这个容易理解,就是开发者自己定义的一个平面,只有平面一侧的三角形才能显示,这个剪裁如果使用,同样会剔除一部分三角形。


四:GS像素处理(片断处理)之功能篇

(下面图中,红色的线代表选一输入/输出,即从诸多选择中选择一个输入或者从诸多输出目标中选择一个输出)
关于后片断处理,各个console都大同小异(GS相比XBOX的NV2A,缺少模板缓冲检测但多了目标alpha检测),所以在这里只讲一下片断处理,对应GS的也就是Texture Function单元。它的特点是这样的(见下图)

1:在一次片断处理中,它可以执行一次texture sample,也就是纹理取样,根据在前面设定的纹理缓存首地址和光栅化得到的该片断的纹理坐标到纹理缓存中取得对应的纹理颜色。也就是说,它不支持单pass(见第二章的pass定义)多纹理。而XBOX的NV2A支持单pass 4次纹理取样(可以从4个不同纹理使用复杂可编程纹理坐标)NGC的Flipper支持单pass 8次纹理取样(可以从8个不同纹理中,使用固定功能单元生成的纹理坐标)。
2:GS的片断运算属于固定功能的,也就是说,它的方式是直接设定某些参数然后就会按照预定的方式处理,在运算单元中,源数据是片断对应的纹理(设为Ct)和片断本身的颜色(设为Cf)输出数据到后片断处理单元(雾化,片断检测等)。处理方法共有4种,分别为Modulate,Decal,Highlight1,Highlight2,Modulate即为Ct*Cf,Decal直接输出Ct,Highlight1,2公式均为Ct*Cf+Af(Af为片断颜色中的Alpha值)区别在于对输出颜色的alpha通道处理,HighLight1使用At+Af或者Af(当纹理不含alpha通道时),Highlight2使用At或者Af(当纹理不含alpha通道时)。这三种方式很好理解,对于第一种,可以认为是片断颜色代表了漫反射颜色和强度,而纹理为材质表面细节,于是两者相乘就得到了在光照下材质的最终颜色;对于第二种,可以认为纹理就代表材质的最终颜色(在静止物体和静止光源的情况下,可以直接将光照信息合成到纹理上);对于第三种,认为片断的rgb通道代表的漫反射颜色和强度,alpha通道代表的是高光强度,于是在第一种的基础上将alpha通道加上,相当于增加了白色的高光成分。


可见,对于GS来说,texture function相当有限,甚至连单周期的多纹理都不支持(这可是DX6的特性),为了方便对比,在这里提及一下DX7的相应功能(NGC近似于DX7的功能,XBOX是DX8+功能,对于DX7有一个飞跃)
如图所示

1:在DX7中,片断运算分为8个stage,从stage0至stage7,每一个stage的运算结果可以直接输出到后片断处理单元从而中止片断运算,或者输出直接作为下一个stage的源数据或者输入temp寄存器留作后用。同时,RGB通道运算和Alpha通道运算是分开的(也就是说在一个stage中两者可以做完全不同的运算)。可见,相对于GS的一个pass只可进行1次运算,DX7要强大得多(最多8次)

2:对于每一个stage来说,源数据可以是:1:上一个stage的运算结果(stage0除外)2:该stage的纹理坐标在该stage对应的纹理中采样的结果,3:片断的漫反射颜色(RGBA)、4:片断的高光颜色(RGBA)、5:temp寄存器内容、6:TEXTUREFACTOR寄存器内容(需在渲染前预先设置)7:该stage对应的常量寄存器内容(需在渲染前预先设置,不是所有硬件都支持)。同时每个源数据可以使用“补数”和“alpha通道替代RGB”两个修改标志。可见,相对于GS的只有纹理颜色和片断颜色两个源数据,DX7要多得多,同时结合第一点,可见,如果硬件支持,最多可以在一个三角形上使用8套纹理坐标和8个纹理(多数DX7级别硬件只支持2重纹理或3重纹理)

3:每个stage要设置运算指令,DX7的运算指令包含25种左右,诸如一元线性运算指令SELECTARGx系列,二元线性运算指令ADD系列,二元非线性运算指令MODULATE系列,混合指令BLENDxxxALPHA系列、二元插值指令ADDSMOOTH、三元插值指令LERP、二元乘加指令MODULATExxx_ADDxxx系列、三元乘加指令MULTIPLYADD、二元点积指令DOTPRODUCT3、“片断级别纹理坐标扰动再取样”指令BUMPENVMAP系列等等。可见,对比GS的只有三种运算操作可选(分别对应DX7中的MODULATE、SELECTARG、MODULATECOLOR_ADDALPHA三个指令)DX7的运算指令在等级上有着巨大强势。尤其是BUMPENVMAP系列指令和DOTPRODUCT3这些指令,前者是实现像素级别纹理扰动(制作诸如水面细致涟漪下的反射折射,haze效果等,也可以用来做伪凹凸贴图)的基础。后者是实现像素级别光照的关键(比如真正的凹凸贴图)。

可见,GS的功能是远远逊于DX7的,那么自然无法和近似DX7的NGC(NGC拥有更多的stage,更多的temp register,更多的input source,更强大的indirect texture功能,但是operation比DX7少,缺少DotProduct等指令,但可以通过多个stage完成,性能稍低)与远胜于DX7的XBOX相提并论了。

那GS在这方面有什么补救办法呢?不是没有,部分功能是可以补救的(但那些稍微复杂点的运算,就实在没有办法了,毕竟功能级数摆在那里)。方法是用多pass+alpha混合来代替部分功能。

拿最简单的“两重纹理相加”来说,如下图所示:

对于支持单pass多纹理的硬件来说,在一个pass中分别取两个纹理的结果T1和T2,然后设置运算方式为Tf = T1+T2即可,对于GS来说,则需要:
1:先画一次三角形(使用第一套纹理坐标),附上纹理1,选择Decal模式,将其写入帧缓存中
2:再画一次三角形(使用第二套纹理坐标),附上纹理2,选择Decal模式,写入帧缓存,并设置Alpha Blending选项。

对于GS来说,Alpha混合的公式为(A-B)*C+D。ABD可以选择要写入的片断颜色Cs、帧缓存相应位置的已经存在的像素颜色Cd或者0,C可以选择要写入的片断Alpha值As、帧缓存相应位置的已经存在的像素Alpha值Ad或者设定的固定值Af,对于上面的情况来说,A选择Cs,B选择0,C选择1(设置Af = 1),D选择Cd,此时为Cs+Cd即为所需情况。
从这里可以发现,由于C只能选择alpha通道的值或固定值,所以GS的Alpha混合能够做得的效果有限,类似两纹理相乘(T1 * T2),点积(T1 dot T2)、文理坐标扰动等等复杂一些的效果是不可能的。

顺便提及一下DX7中的Alpha blending方式,它的公式为(Cs*A)OP(Cd*B),Cs和Cd的意义和上面相同。AB可从13个数据中选择,包括0,1,Cs,Cd,As,Ad,1-Cs,1-Cd,1-As,1-Ad,Af,1-Af,min(As,1-Ad);而Op可以从加,减,反减,取最大、取最小5个运算方式中选择。可见,DX7的Alpha混合功能比GS更为强大。

接下来说明一下GS的抗锯齿功能。GS使用的抗锯齿算法既非SSAA(超取样抗锯齿)也非MSAA(多重取样抗锯齿),准确地说,它是用的COV算法+Alpha blending实现。Cov代表一个处于多边性边缘处的片断的多边形覆盖率。将其作为alpha值输出。然后使用alpha混合单元。就可以实现反锯齿效果。这要求
1:各三角形必须从后到前排序后绘制,这是半透明效果的要求。相当耗费资源
2:如果启用抗锯齿,那么alpha混合就不可用,因为alpha值已经被固定为Cov,且alpha blending单元用作反锯齿的cov混合。这对于强烈依赖alpha混合实现效果的GS来说是一致命打击

因此,PS2游戏极少使用抗锯齿,抗锯齿在PS2上并不实用。而NGC采用RGSS,XBOX同时支持OGSS和OGMS方式,比GS的AA要实用得多。

五:GS的性能特点分析。

从上一部分看来,GS的功能上相当令人丧气,那么如何理解GS性能上的优势呢,包括填充率,带宽等?我们先说填充率

很显然的,pass越多就需要越多的填充率,因为每一个pass都要将三角形绘制一遍。从这点上看,GS的高填充率正是为多pass渲染而设计的,换句话说,如果游戏中只是简单的无纹理方式,那么GS的16条管线(150Mhz)都能发挥作用,在填充速度上要超出XBOX(4管线233Mhz)和NGC(4管线166Mhz)很多,不过这种情况在实际中是不可能发生的,而加入使用2重纹理(这种情况在Quake2中就已经大量应用了)那GS需要2个pass,而每个pass中只有8个管线能够发挥作用(只有8个纹理单元所致),而XBOX只需要一个pass即可完成,而且由于XBOX的NV2A每条管线上存在2个纹理单元。所以可以同时在2个纹理上取样,性能没有损失,于是在这种情况下,NV2A由于较高的频率所以速度要比GS更快,NGC的Flipper每条管线没有更多纹理单元需要loop back才能读取两个纹理,但由于固定管线的优化和纹理单元,算术单元并行,所以性能损失并不很大,也就是,在这种情况下,NGC的速度不会比GS慢多少。可见,纹理层越多,GS的填充率强势就会被越发抵消。

下面从显存带宽角度考虑,众所周知,GS拥有非常夸张的显存带宽,从Framebuffer中并行存取的速度都可以达到接近20G/s的速度,从多pass的角度来看,这种设计也是特为多pass渲染而生的。因为每一个pass都需要往framebuffer写一次数据,而每次alpha混合也都需要从framebuffer中读取数据。pass越多,需要的framebuffer带宽就越大,而对于XBOX和NGC来说,多pass应用远不如GS这样频繁,所以对framebuffer的需求也就不那么高。所以带宽低实际影响并不很大。

接下来讨论一下多pass对EE的负荷问题,就pass的定义可知,一个pass对应了一次3D变换运算。所以pass越多,对EE的要求也越高,不过对于2重纹理方式,EE和GS有特殊的处理方式,因为这种情况下,各pass的相对应的三角形的位置变换是相同的(因为是将同一个三角形使用2个纹理绘制2次),所以对于EE来说,没有必要再次进行3D空间变换和齐次归一,只需要计算一次位置属性和对应不同pass的纹理坐标和顶点颜色即可,然后输出时先输出该三角形的位置和对应第一重纹理的属性(三个顶点),然后重复再输出一次三个顶点的位置,属性则用第二重纹理对应的属性代替。这样可以避免不必要的重复计算。(输出列表如下表示:X1,Y1,Z1,RHW1,s1_1,t1_1,RGBA1_1,X1,Y1,Z1,RHW1,s1_2,t1_2,可见,避免了XYZRHW的重复计算)

而GS则拥有“2相渲染”特征,每相渲染都可以设置单独的纹理指针,纹理函数,alpha混合方式等等。所以只需将两相的各个属性分别对应两个纹理,然后对于输入的绘制列表使用triangle list进行绘制,每画一个三角形变换一次绘制相。这样连续渲染2N个三角形,就完成了N个三角形的2重纹理。这也就是我在第三章中提到的“参与光栅化的三角形数量是经过EE运算的三角形数量2倍”的来源。虽然这种方法并没有降低GS的负担,但却降低了EE的负担。

接下来继续讨论GS显存的特点,由于其只有4M的容量,且framebuffer,zbuffer要占去不少。所以留给纹理的容量极其有限,所以其作用更类似于一个较大的纹理cache。纹理要不断被更新(从系统内存往GS显存中传输),于是对总线与GS的带宽(1.2G/s)要构成不小的冲击,实际上,由于GS显存的容量过小,所以实际上GS显存的带宽受到了相当大的抑制。无法发挥100%的能力。

而对于NGC来说,尽管它的1T-SRAM容量同样很小,不过它支持纹理的硬件S3TC压缩解压缩,压缩比为4:1至6:1,所以比GS的情况就会好很多,而XBOX的UMA结构导致开发人员基本不需为它的显存容量而操心。

通过以上两章分析,可以对GS得出一个评价:牺牲功能来追求性能,以多pass渲染作为设计目标的光栅化渲染器,适合做各种多pass效果(如运动模糊)但缺少众多高级运算效果支持是一个不小的遗憾,而处于成本的考虑,过小的ERAM容量和无法纹理压缩更是它的致命伤。

六:总结

可见,sony研制的PS2作为一个新的架构,并没有体现出对于NGC和XBOX架构的优势,尽管PS2拥有兼备灵活和性能的EE单元,但开发并不简单(当初SS的双核心并行开发更为困难从而导致失败,sony看来没有吸取足够的教训),而GS的设计更基本可以认为是一次失败的尝试。从这次sony寻求和IBM与NVIDIA共同研发PS3可以看出sony已经放弃闭门造车的行为。而无论从PS3,XBOX360还是未发布的Revolution,都可以看出,GPU才是发展的方向。至于PS3中的Cell参与光栅化渲染,倒是一个令人感兴趣的话题。效果如何有待检验。