OpenGL Performance Optimization
Yang Jian
jyang@cad.zju.edu.cn
这篇文章比较长,希望大家能够看完。^_^
OPenGL State Machine
Typical D3D9 Hardware architecture
Less State Change
GL_TRIANGLE_STRIP instead of GL_TRIANGLES
Texture Loading
Texture Composite
Texture MipMap
Multi Pass vs. Single Pass
Texture Compression
Avoid Pixel Operations
Vertex Array, Display List,
Vertex Buffer Object
Advanced Tech: VS and Ps.
Less Operations for Depth Test, Stencil Test, Alpha Test,
Fast Shadow
MISC: LOD, cull, SwpaBuffers, wglMakeCurrent etc
上一个讲座是关于OpenGL Driver体系结构,估计大家都有很多疑问,而且我自己又看了一遍,发现一些问题讲得不够清晰,而且没有交代讲驱动程序体系结构的目的。其主要目的是,当我们了解了驱动程序的体系结构,我们更好地写出一个OpenGL应用程序框架结构。
今天我将结合OpenGL状态机和一个典型的D3D9硬件体系结构探讨如何对OpenGL应用程序的性能进行优化。MSDN的OpenGL帮助也提 到了关于性能优化方面的问题,但是这已经是多年以前的article,而随着图形加速硬件的发展,许多新的技术不断涌现,我们应该跟上时代发展的步伐。
我今天讲的内容应该是不全面的,希望大家踊跃指正和补充。
1 OpenGL状态机(State Machine)
OpenGL状态机的目前只有1.1版本,也是最经典的,大家可以参考下述链接:
ftp://ftp.sgi.com/opengl/doc/opengl1.1/state.pdf
ftp://ftp.sgi.com/opengl/doc/opengl1.1/state.ps
它们是内容相同而格式不同的状态机表达。整个文件中只有一张Postscript的图。这张图实际上就是SGI RealityEngine的硬件程序流程描述。
首先硬件接受应用程序输入的顶点信息,(Color, Normal, Texture, EdgeFlag,Vertex, ),经过世界坐标变换(glTranslate, glRotate, glScale),接着进行User Clip Plane,之后进入视图变幻和裁减(Projection Matrix),然后视口变换(ViewPort),经过Primitive Setup,光栅化处理(Flat或Phong)生成片断Fragment,下面的对每个依次作纹理贴图计算,纹理混合(Texture Blend),深度测试(Depth Test),模板测试(Stencil测试),透明测试(Alpha Test),透明混合(Apha Blend),然后写入颜色缓冲区,深度缓冲区,模板缓冲区。整个流程如下:
Application
|
Vertex Information (Material , Normal, Textcoord, EdgeFlag, Vertex Position)
|
Lighting
|
World Matrix Transform
|
User Clip Plane Clipping
|
Projection Matrix Transform and Clip
|
ViewPort
|
Primitive Setup ( point, Line, Triangle)
|
Rasterization( Flat or Phong ) == > Generate Fragment
|
Fragment Texture Addressing () == Texture In Video memory
|
Fragment Texture Blend ( blend Diffuse, Specular and Texture of Fragment )
|
Depth Test == with Depth Buffer
|
Stencil Test == with Stencil Buffer
|
Alpha Test == with alpha channel of color buffer
|
Alpha Blend == with color buffer
|
Fragment write to FrameBuffers
我们可以看到OpenGL每处理一个几何图元,需要经过大量的处理过程。大家应该对这个图的每个步骤地工作相当清晰。这里有几个概念需要说明。
第一个概念是Fragment,片断或者片元。每一个片断对应屏幕上的一个像素点,它是光栅化(Rasterization)引擎使用FLAT shading或 Phong Shading生成的。Rasterization引擎产生的片断包含一下信息:
屏幕坐标;
颜色信息,Diffuse和Specular;
深度信息和模板信息;
纹理坐标,u,v
第二个概念是纹理混合(Texture Blend),它是指纹理颜色和片断颜色(Diffuse和Specular)合成的方式。就是指glTexEnv的效果,根据不同的参数决定片断只保留Texel(纹理元)还是使用Texel(纹理元)和片断的颜色做混合。
第三个概念是透明融合Alpha Blend。如果一个片断经过深度测试,模板测试和透明测试,那么它将和缓冲区对应位置的像素作透明融合。
相信大家对OpenGL 状态机有了一定的了解,实际上这也是Direct3D8以前的图形流水线的主要参考模型(graphics processing pipeline)。
==================================================================
如果我们能够在流水线中减少一个操作,我们就能够获得性能的提高,当然前提是我们能够绘制正确的图像。
2 典型的D3D9硬件体系结构
上面的OpenGL状态机实际上就是SGI的Reality Engine和其他Direct3D7及其一下版本的图形硬件流水线结构。下面我向大家介绍D3D9的典型硬件体系结构(或者说Direct3D9的参考模型)。
Application
|
IDirect3DDevice9::DrawIndexedPrimitive
|
D3D Driver (Display Driver ) Send Commands to Hardware by AGP
|
| following is hardware
Command Interpreter
|
|
“Fetch” Indexed Primitive data to Vertex Shader Cache (access index buffer and
|Vertex Buffer)
|
|
“Put“ Cached data to Vertex Shader Input
|
Vertex Shader do Transform, Light and Vertex Blend
|
Vertex Shader Output Vertices in Screen coordinate Space,
| Screen Pos, Diffuse, Texture Coord
|
|
User Clip plane
|
Guard band clip
|
Primitive Setup (Point, Line, Triangle)
|
Rasterizaiton(flat or Phong)
|
Pixel Shader (Texture addressing and texture blend)
|
Depth Test
|
Stencil Test
|
Alpha Test
|
Alpha Blend
|
Frame Buffers
我们可以看到D3D9的流水线和OpenGL 1.1的流水线有很大的不同。
OpenGL的顶点数据是通过调用OpenGL API一个个的送到流水线的几何变换处理单元,立即模式(immediate mode),而D3D9通过 Fetch和Put两步工作,从Vertex Buffer中读出送入Vertex Sahder的Input寄存器;
OpenGL 1.1的光照计算和几何变换是通过传统的固定流水线(TnL: Transform and Lighting)完成的—fixed function graphics processing(FGP),而D3D9时通过Vertex Shader实现,它比FFGP更为复杂,可以完成更多的功能;
OpenGL 1.1的Texture mapping和Texture Blend独立的两个步骤,而D3D9是通过Pixel Shader,PS是可编程的(Programmable Graphics Processing)。
D3D8/D3D9的Vertex Shader和Pixel Shader是两个图形体系结构巨大的进步,当然使得图形程序设计更为灵活,也更为困难和复杂。
对于D3D8/D3D9的硬件体系结构,我们的程序优化工作有多了两个内容,优化Vertex Shader和Pixel Shader。
今天我的重点放在传统图形流水线(TnL)的性能优化上。
3 基本优化方法
3.1 减少OpenGL的状态变化
如果我们应用程序不断地改变OpenGL的状态,那么驱动程序和AGP数据传输,图形硬件的负担会则增 加很多。因为每当我们改变一个OpenGL状态,可能会涉及到硬件的多个寄存器的数据,那么驱动程序就必须将修改的硬件寄存器通过AGP总线发送到硬件, 占用大量的CPU资源和AGP带宽和硬件命令解释器时间。
Advice1:尽可能将状态相近的图形绘制命令放在一起,减少OpenGL状态变化。
Advice2:使用状态集合,降低驱动程序的CPU处理时间,
3.2 避免光照计算特别是高光计算(Specular)
Specular的计算是光照计算中最为耗时的运算之一。Diffuse计算相对比较普通,一般图形硬件都会对Diffuse运算进行优化。
3.3 图元类型优化
我们使用的大多数图元类型都是Triangle。如果我们每次都是用GL_TRIANGLES,我们将浪费大量的CPU时间和AGP带宽和图形硬件资源。原因如下:
(1)使用GL_TRIANGLES,我们每绘制一个三角形,我们就会发送三个定点的数据,如果我们使用G:_TRIANGLE_FAN或者GL_TRIANGLE_STRIP,那么我们可以平均每个三角形一个顶点。
(2)一般的硬件设计中都开辟一定的Cache区域,如果使用GL_TRIANGLE,我们将无法使用图形硬件的Cache,浪费大量的图形硬件TnL时间。
(3)使用GL_TRIANLGES将比GL_TRIANGLE_STRIP多耗费200%的硬件TnL时间。
根据测试,我三年前在Geoforce 3和 Geoforce Quadro 3上对OpenGL做的测试,GL_TRIANGLE_STRIP比GL_TRIANLGES 快100% ~ 200%。
建议:尽可能地使用GL_TRIANGLE_STRIP替代GL_TRIANGLES。
三角形Stripe的成熟软件:
http://www.cs.sunysb.edu/~stripe/
3.4 光照条件下使用glMaterial替代glColor
在光照条件下,如果程序使用glMaterial,那么驱动程序只加载Material属性一遍到硬件,使用glColor将使得驱动程序对每个定点加载颜色信息。将会占用更多的CPU时间和AGP带宽。
4 纹理优化
这方面的话题比较多,所以我把它作为一个独立的话题。
4.1 优化纹理加载
初学OpenGL一个常见的性能优化方面的问题是每次使用一个纹理的时候,都重新设置纹理参数并且调用 glTexImage2D函数。事实上,OpenGL对纹理和Display List都有一个命名机制,glBindTexture,glDeleteTexture,glBindTexture。下面我们比较一下效果。
方法一:每次使用纹理前调用glTexImage2D,并重新设置纹理参数。那么驱动程序将不断地调用IDirectDraw7::CreateSurface并且将数据从用户内存区拷贝到驱动程序系统内存区,然后再从系统内存区域复制到video memory。
方 法二:使用glTexEnv和glTexImage2D设置当前的纹理参数和纹理内容,,然后调用glBindTexture,例如5号纹理;如果需要使 用该纹理,再次调用glBindTexture函数,glBindTexture会把5号纹理设置为当前的纹理,并且参数上次设置的参数,你可以根据需要 决定是否修改参数。方法二的主要优点在于应用程序仅仅调用glTexImage2D,从而节省大量的CPU和AGP时间,因为从CPU往video memory复制是最耗时,overhead is very high。
Advice:
当应用程序需要多个Textures,在调用wglMakeCurrent成功后,调用glGenTextures产生命名纹理,并且使用glBindTexture分别进行纹理绑定;
在wglDeleteContext之前使用glDeleteTexture将所有的纹理从驱动程序内存和video memory释放。
每次需要使用纹理时,再次调用glBindTexture
进一步阅读:
OpenGL Spec & OpenGL manual:
http://www.opengl.org/developers/documentation/specs.html
Glut examples:
http://www.opengl.org/developers/documentation/glut.html
4.2 尽量使用MipMap纹理
一般图形硬件都支持Mipmap,如果应用程序使用Mipmap,那么图形硬件会根据当前的片断对应的纹 理LOD计算Texel,这样能够节省大量的纹理元video memory寻址时间,而且图形硬件对纹理元做Cache,mipmap中尺寸较小的纹理(Level比较大的)能够节约大量的计算时间。如果应用程序仅 仅提供Level 0的最大的纹理,那么图形硬件每次都将使用这个纹理作纹理元计算,不但会浪费大量的计算资源,而且消耗很多的图形芯片带宽。
Advice:1. 不要使用特别大的纹理. > 256 X 256
2.使用MipMap。
Tips: gluBuild*DMipmaps 能够将非2^n的纹理转化带有MipMaps的标准OpenGL纹理。不过gluBuild*DMipMaps不支持压缩纹理的自动Mipmap。
进一步阅读:glu Manual:
ftp://ftp.sgi.com/opengl/doc/opengl1.2/glu1.3.pdf
4.3 纹理组合
在游戏或者可视化应用中,我们总是会遇到许多非常小的纹理,一种比较好的办法是我们把这些纹理组合成一个比较大的纹理,例 如256X256,这样驱动程序在加载纹理的video memory的地址时候,驱动程序仅仅需要加载一次家可以了。这种方法在多个造型软件中也经常见到,例如人体造型软件Pose,它将一个人的头发,脸,眼 睛,等组合为一个纹理。
Advice: 将多个小纹理组合为一个大纹理,然后修改对应三角形定点的纹理坐标,或者使用glMatrixMode(GL_TEXTURE)对定点的纹理坐标作几何变换。
4.4 使用MultiTexture替代Multi-Pass
OpenGL 1.2.1 extension: GL_ARB_multitexture
Direct3D7(OpenGL .2.1)及更高版本支持的显示卡都支持MutliTexture功能,我们可以充分利用这个特性做多纹理贴图替代Multi-Pass。
例如我们希望会绘制一个可乐瓶子,而且这个可乐瓶子需要两层标签,利用Multi-Pass我们可以分三次绘制,
//绘制瓶子的本色,例如绿色,
glMaterial (…) ;
glDisable(GL_BLEND);
glDepthFunc(GL_LEQUAL);
glBegin(GL_TRIANGLE_STRIP);
//Texture
glNormal();
glVertex(); ….
glEnd();
//绘制里面的标签
glDpethFunc(GL_EQUAL);
glEnable(GL_BLEND);
glBindTexture(0,);
glBegin();
glTextCoord();
glVertex();
glEnd();
//绘制第二层标签
glDpethFunc(GL_EQUAL);
glEnable(GL_BLEND);
glBindTexture(1,);
glBegin();
glTextCoord();
glVertex();
glEnd();
如果使用MutliTexture(OpenGL.2.1扩展),我们只需要Single Pass完成这项工作:
glMaterial();
glDepthFunc(GL_LEQUAL);
glDisable(GL_BLEND);
glActiveTExtureARB(GL_TEXTURE0_ARB);
glTexEnv(,,GL_MODULATE);
glBindTExture(0);
glActiveTExtureARB(GL_TEXTURE1_ARB);
glTexEnv(,,GL_MODULATE);
glBindTExture(1);
glBegin(GL_TRIANGLE_STRIP);
glNormal();
glMultiTexCoord2fARB (GL_TEXTURE0_ARB,u0, v0 );
glMultiTexCoord2fARB (GL_TEXTURE1_ARB, u1, v1);
glVertex();
glEnd();
Mutlitexture的方法将比第一种方法节约流水线的4个运算步骤,Depth Test,Alpha Test,Alpha Blend,和 write to frame Buffers。
Advice:检查OpenGL extension支持,尽可能使用MultiTexture。
进一步阅读:
OpenGL specs:
http://www.opengl.org/developers/documentation/specs.html
OpenGL extension Registry:
http://oss.sgi.com/projects/ogl-sample/registry
4.5 使用压缩纹理
OpenGL支持的压缩纹理包括:
GL_COMPRESSED_RGB_S3TC_DXT1_EXT
GL_COMPRESSED_RGBA_S3TC_DXT1_EXT
GL_COMPRESSED_RGBA_S3TC_DXT3_EXT
GL_COMPRESSED_RGBA_S3TC_DXT5_EXT
压缩纹理比非压缩纹理具有更快的运算速度和更小的存储空间要求,而且很容易使用图形硬件纹理Cache。因此能够显著地提高应用程序性能,特别应用程序的纹理数据量巨大。
缺点:要求纹理的色彩空间规律性极强,否则会造成严重的颜色失真。
建议:检查下面的三个OpenGL Extension,尽可能地使用压缩纹理。
GL_ARB_texture_compression
GL_EXT_texture_compression_s3tc
GL_S3_s3tc
Advice:检查OpenGL extension支持,尽可能使用MultiTexture。
进一步阅读:
OpenGL specs:
http://www.opengl.org/developers/documentation/specs.html
OpenGL extension Registry:
http://oss.sgi.com/projects/ogl-sample/registry
我们可以使用DirectX SDK的工具产生压缩纹理dxtex ,或者从nivdia获得工具和Tutorial:
http://developer.nvidia.com/object/nv_texture_tools.html
4.6 合理的纹理尺寸
图形硬件系统一般使用4X4,8X8,最高到64X64的纹理Cache策略,如果你的纹理比较简单,在满足可识感官的要求下,尽可能地使用较小的纹理尺寸。
5 Vertex Array
相对于glBegin, glEnd以及Display List, Vertex Array对于驱动程序而言具有最高的内存复制效率,因为驱动程序仅仅需要一次内存数据移动,glBend, glEnd和Display List,则需要三次数据移动。因此尽可能多地使用glDrawArrays和glArrayElement的方式。
针对Vertex Array,OpenGL 有如下的Extensions:
GL_EXT_vertex_array
GL_ATI_element_array
GL_EXT_draw_range_elements
GL_EXT_compiled_vertex_array
GL_SUN_mesh_array
GL_ATI_vertex_attrib_array_object
其中前面三个是经常使用OpenGL extension,例如QuakeIII, CS, Half Life等。
进一步阅读:
OpenGL specs:
http://www.opengl.org/developers/documentation/specs.html
OpenGL extension Registry:
http://oss.sgi.com/projects/ogl-sample/registry
6 Buffer Object
事实上,我上面所讲到的内容都是传统的OpenGL图元定义,本质上都是通过glBegin和glEnd定义,都属于立即模式绘制的一种方法。而 Direct3D都是通过Vertex Buffer和Index Buffer实现图元及其组成顶点的属性定义。而Vertex Buffer和Index Buffer都在保存在video memory中,这样应用程序不需要每次都把地顶点数据通过AGP发送给硬件,从而加快了处理速度。为了在弥补这个缺陷,Nvidia和ATI推出了下面 的Extension:
GL_ARB_vertex_buffer_object
同时这个extesion也为OpenGL 的Vertex Proram(即 D3D9的Vertex Shader)服务,关于这个Extension相关内容比较多,我就不展开这个讲述了。这里告诉大家,它是比所有立即模式图元定义方法都快的一个 OpenGL extension。原因如下:
(1)它只需要一次复制到OpenGL申请的video memory,随后驱动程序仅仅每次向图形硬件报告它的物理地址;
(2)而对于立即模式的图元定义,驱动程序每次都需要从内存中把数据复制到AGP non-local video memory,然后通过AGP总线发送到图形硬件处理器。
请参考
OpenGL extension Registry:
http://oss.sgi.com/projects/ogl-sample/registry
7 Advanced Tech :Vertex Program 和 Fragment Program( D3D Vertex Shader和 Pixel Shader)
这篇内容太长长了,我把它放入到D3D9的专题中。
8 Less Operation for Depth Test,Stencil Test和 Alpha Test
事实上,Depth Test,Stencil Test,Alpha Test能够影响到OpenGL 像素填充的30%。也就是说,如果你对他们进行优化,能够获得30%的性能。
我曾经对quake III的性能优化做过测试,得到下面结果;
Disable Depth Test 2% gain
Disable Alpha Test 6% gain
Disable Alpha blend 2% gain
Disable Depth Clear always15% gain
事实上,Quake III本身能够进一步优化,大家都知道Quake III是最经典的一个游戏引擎,它绘制图形采用BSP的结构,使用多纹理贴图和Alpha Blend获得非常好的光照效果,绘制图元的顺序是从最远处的物体到最近处的物体,由远及近的次序,那么如果QuakeIII把它改作由近及远的次序, Quake III中也少数的三角形遮挡关系,采用由近及远的次序绘制图形的时候,Depth Test将扔掉5%~10%甚至更多的片断(像素),那么流水线后面的操作将不会被执行,从而获得性能的提高,我相信这将会带来5%~15%的性能提高。
那么对于室外场景的漫游,我建议大家采用由近及远的次序。也许会带来极大的性能提高。
9 Fast Shadow
很多人都在做类似的工作,我想以后抛砖引玉,作为一个单独的专题介绍。
10 MISC: LOD, cull, SwpaBuffers, wglMakeCurrent etc
最后一部分的小标题比较古怪,是个大杂烩。
10.1 LOD
这个许多人都知道了,我就不多说了,就是较少几何数据量(Vertex) 和纹理运算量(Texture LOD: mipmap)。
10.2 CULL Face
CULL Face就是北面删除,如果不绘制背面的三角形,理论上可以获得接近50%的性能提高,前提是假设TnL或者Vertex Shader足够的快。
glEnable(GL_CULL_FACE) ;
glCullFace(GL_BACK);
在我对QuakeIII的测试中,尽管QuakeIII是基于BSP树的,理论上QuakeIII不应该有背面的物体,我仍然获得了3%~5%的性能提高(不同的CPU和总线速度)。
10.3SwapBuffers
事实上,全屏幕的OpenGL程序是调用IDirectDrawSurface7::Flip或 IDirect3DDevice8::Present,那么每进行FLIP操作将比窗口的OpenGL程序少作1024X768X4 bytes的显示内存数据移动,将设分辨率为1024X768X32bits,根据不同的应用,能够获得相当可观的性能提高,大家可以自己算算。
10.4 wglMakeCurrent
wglMakeCurrent是一个非常耗时的操作,2001年我对Geoforce3 Ti500进行了测试,在最好的情况下,Geoforce3 Ti500能够做5000次/秒。当时的CPU速度好像是800M还是1.4Ghz。我不太清楚了。同时wglMakeCurrent也许会带来副作用, 一些图像可能发生丢失。其中一个典型的测试,indy3D就是采用这种方法,我在跟踪这种程序的时候,觉得Sense8(开发vtk的那个公司)程序设计 能力太糟糕了。
Advice:一定要避免调用wglMakeCurrent。
写了3。5小时,手指头已经很痛了,休息一下。
11 避免像素操作(Pixel)
在OpenGL的实现中,都是使用纯软件的方法实现从系统内存到video memory 的复制,那么这些将中断整个图形流水线的执行,等待硬件空闲后使用CPU完成,它们将大大降低程序的执行效率。
glBitmap,
glDrawPixels
glReadPixels
glCopyPixels
解决办法:使用纹理替代像素操作,例如建设你希望在屏幕输出一行字,例如”Qauke III Arena”, 那首先产生一个纹理,它包含所有的字母和数字,我这里无法贴BMP图像,我画一个存储结构:代表RGBA各式的2D 纹理,这是Quake III的字母纹理顺序。
A B C D E F G H I J KLM
N O P Q R S T U V WX Y Z
a b c d ….
1 2 3 4 5 6 9 8 9 0
使用两个三角形产生一个字母或者数字。
补充:4.5 使用压缩纹理
我们可以使用DirectX SDK的工具产生压缩纹理dxtex ,或者从nivdia获得工具和Tutorial:
http://developer.nvidia.com/object/nv_texture_tools.html