GPGPU数学基础教程.docx
《GPGPU数学基础教程.docx》由会员分享,可在线阅读,更多相关《GPGPU数学基础教程.docx(42页珍藏版)》请在冰豆网上搜索。
GPGPU数学基础教程
GPGPU:
:
数学基础教程
Contents:
1.介绍
1.准备条件
2.硬件设备要求
3.软件设备要求
4.两者选择
2.初始化OpenGL
1.GLUT
2.OpenGL扩展
3.OpenGL离屏渲染
3.GPGPU概念1:
数组=纹理
1.在CPU上建立数组
2.在GPU上生成浮点纹理
3.数组索引与纹理坐标一一对应
4.使用纹理作渲染对像
5.把数据从CPU数组传输到GPU的纹理
6.把数据从GPU的纹理传输到CPU数组
7.一个简单的示例
4.GPGPU概念2:
内核=着色器
1.面向循环的CPU运算vs.面向内核的数据并行运算
2.用Cg着色语言生成一个着色器
3.建立Cg运行环境
4.用OpenGL语言建立一个高级着色环境
5.GPGPU概念3:
运算=渲染
1.准备运算的内核
2.设定用于输入的数组/纹理
3.设定用于输出的数组/纹理
4.开始运算
6.GPGPU概念4:
返馈
1.多次渲染传递
2.使用乒乓技术
7.归纳总结:
1.一个简但的代码
2.程序的变量
3.命令行参数
4.测试模式
5.标准模式
8.附言
1.对比Windows和Linux,ATI和NVIDIA
2.问题
3.OpenGL错误检查
4.FBOs错误检查
5.Cg错误检查
6.GLSL错误检查
9.相关知识
10.版权声明
ThesezipfilescontainaMSVC2003.NETsolutionfile,alinuxMakefileandasetofbatchfileswithpreconfiguredtestenvironments.YoumightwanttoreadthissectionaboutthedifferencesbetweenWindowsandLinux,NVIDIAandATIfirst.
1.介绍:
本教程的目的是为了介绍GPU编程的背景及在GPU上运算所需要的步骤,这里通过实现在GPU上运算一个线性代数的简单例子,来阐述我们的观点。
saxpy()是BLAS库上的一个函数,它实现的功能主要是这样的:
已知两个长度为N的数组x和y,一个标量alpha,要求我们计算缩放比例数组之和:
y=y+alpha*x。
这个函数很简单。
我们的目的只是在于向大家阐明一些GPGPU编程入门的必备知识和概念。
本教程所介绍的一些编程实现技术,只要稍作修改和扩充,便能运用到复杂的GPU运算应用上。
必备条件:
本文不打算深入到在每一个细节,而是给对OpenGL编程有一定技术基础的朋友看的,你最好还要对图形显卡的组成及管道渲染有一定的了解。
对于OpenGL刚入门的朋友,推荐大家看一下以下这些知识:
ProgrammingGuide(红宝书).PDFandHTML,橙宝书("OpenGLShadingLanguage"),以及NeHe'sOpenGL教程
本教程是基于OpenGL写,目的主要是为不被MSWindows平台的限制。
但是这里所阐述的大多数概念但能直接运用到DirectX上。
更多的预备知识,请到GPGPU.org上看一下。
其中该网站上以下三篇文章,是作者极力推荐大家去看一下的:
《WherecanIlearnaboutOpenGLandDirect3D?
》,《HowdoestheGPUpipelinework?
》'《nwhatwaysisGPUprogrammingsimilartoCPUprogramming?
》
译者注:
在国内的GPGPU论坛可以到物理开发网上讨论。
该网站主要是交流PhysX物理引擎,GPU物理运算等计算机编程的前沿技术
硬件需求:
你需要有NVIDIAGeForceFX或者ATIRADEON9500以上的显卡,一些老的显卡可能不支持我们所需要的功能(主要是单精度浮点数据的存取及运算)。
软件需求:
首先,你需要一个C/C++编译器。
你有很多可以选择,如:
VisualStudio.NET2003,Eclipse3.1plusCDT/MinGW,theIntelC++Compiler9.0及GCC3.4+等等。
然后更新你的显卡驱动让它可以支持一些最新特性。
本文所附带的源代码,用到了两个扩展库,GLUT和GLEW。
对于windows系统,GLUT可以在这里下载到,而Linux的freeglut和freeglut-devel大多的版本都集成了。
GLEW可以在SourceForge上下载到,对于着色语言,大家可以选择GLSL或者CG,GLSL在你安装驱动的时候便一起装好了。
如果你想用CG,那就得下载CgToolkit。
二者择其一:
大家如果要找DirectX版本的例子的话,请看一下JensKrügers的《ImplicitWaterSurface》demo(该例子好像也有OpenGL版本的)。
当然,这只是一个获得高度评价的示例源代码,而不是教程的。
有一些从图形着色编程完全抽象出来的GPU的元程序语言,把底层着色语言作了封装,让你不用学习着色语言,便能使用显卡的高级特性,其中BrookGPU和Sh就是比较出名的两个项目。
2.初始化OpenGL:
GLUT:
GLUT(OpenGLUtilityToolkit)该开发包主要是提供了一组窗口函数,可以用来处理窗口事件,生成简单的菜单。
我们使用它可以用尽可能少的代码来快速生成一个OpenGL开发环境,另外呢,该开发包具有很好的平台独立性,可以在当前所有主流的操作系统上运行(MS-WindowsorXfree/XorgonLinux/UnixandMac)。
//includetheGLUTheaderfile
#include
//callthisandpassthecommandlineargumentsfrommain()
voidinitGLUT(intargc,char**argv){
glutInit(&argc,argv);
glutCreateWindow("SAXPYTESTS");
}
OpenGL扩展:
许多高级特性,如那些要在GPU上进行普通浮点运算的功能,都不是OpenGL内核的一部份。
因此,OpenGLExtensions通过对OpenGLAPI的扩展,为我们提供了一种可以访问及使用硬件高级特性的机制。
OpenGL扩展的特点:
不是每一种显卡都支持该扩展,即便是该显卡在硬件上支持该扩展,但不同版本的显卡驱动,也会对该扩展的运算能力造成影响,因为OpenGL扩展设计出来的目的,就是为了最大限度地挖掘显卡运算的能力,提供给那些在该方面有特别需求的程序员来使用。
在实际编程的过程中,我们必须小心检测当前系统是否支持该扩展,如果不支持的话,应该及时把错误信息返回给软件进行处理。
当然,为了降低问题的复杂性,本教程的代码跳过了这些检测步骤。
OpenGLExtensionRegistryOpenGL扩展注册列表中,列出了几乎所有的OpenGL可用扩展,有需要的朋友可能的查看一下。
当我们要在程序中使用某些高级扩展功能的时候,我们必须在程序中正确引入这些扩展的扩展函数名。
有一些小工具可以用来帮助我们检测一下某个给出的扩展函数是否被当前的硬件及驱动所支持,如:
glewinfo,OpenGLextensionviewer等等,甚至OpenGL本身就可以(在上面的连接中,就有一个相关的例子)。
如何获取这些扩展函数的入口指针,是一个比较高级的问题。
下面这个例子,我们使用GLEW来作为扩展载入函数库,该函数库把许多复杂的问题进行了底层的封装,给我们使用高级扩展提供了一组简洁方便的访问函数。
voidinitGLEW(void){
//initGLEW,obtainfunctionpointers
interr=glewInit();
//Warning:
Thisdoesnotcheckifallextensionsused
//inagivenimplementationareactuallysupported.
//FunctionentrypointscreatedbyglewInit()willbe
//NULLinthatcase!
if(GLEW_OK!
=err){
printf((char*)glewGetErrorString(err));
exit(ERROR_GLEW);
}
}
OpenGL离屏渲染的准备工作:
在传统的GPU渲染流水线中,每次渲染运算的最终结束点就是帧缓冲区。
所谓帧缓冲区,其实是显卡内存中的一块,它特别这处在于,保存在该内存区块中的图像数据,会实时地在显示器上显示出来。
根据显示器设置的不同,帧缓冲区最大可以取得32位的颜色深度,也就是说红、绿、蓝、alpha四个颜色通道共享这32位的数据,每个通道占8位。
当然用32位来记录颜色,如果加起来的话,可以表示160万种不同的颜色,这对于显示器来说可能是足够了,但是如果我们要在浮点数字下工作,用8位来记录一个浮点数,其数学精度是远远不够的。
另外还有一个问题就是,帧缓存中的数据最大最小值会被限定在一个范围内,也就是[0/255;255/255]
如何解决以上的一些问题呢?
一种比较苯拙的做法就是用有符号指数记数法,把一个标准的IEEE32位浮点数映射保存到8位的数据中。
不过幸运的是,我们不需要这样做。
首先,通过使用一些OpenGL的扩展函数,我们可以给GPU提供32位精度的浮点数。
另外有一个叫EXT_framebuffer_object的OpenGL的扩展,该扩展允许我们把一个离屏缓冲区作为我们渲染运算的目标,这个离屏缓冲区中的RGBA四个通道,每个都是32位浮点的,这样一来,要想GPU上实现四分量的向量运算就比较方便了,而且得到的是一个全精度的浮点数,同时也消除了限定数值范围的问题。
我们通常把这一技术叫FBO,也就是FrameBufferObject的缩写。
要使用该扩展,或者说要把传统的帧缓冲区关闭,使用一个离屏缓冲区作我们的渲染运算区,只要以下很少的几行代码便可以实现了。
有一点值得注意的是:
当我用使用数字0,来绑定一个FBO的时候,无论何时,它都会还原window系统的特殊帧缓冲区,这一特性在一些高级应用中会很有用,但不是本教程的范围,有兴趣的朋友可能自已研究一下。
GLuintfb;
voidinitFBO(void){
//createFBO(off-screenframebuffer)
glGenFramebuffersEXT(1,&fb);
//bindoffscreenbuffer
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,fb);
}
3.GPGPU概念1:
数组=纹理:
一维数组是本地CPU最基本的数据排列方式,多维的数组则是通过对一个很大的一维数组的基准入口进行坐标偏移来访问的(至少目前大多数的编译器都是这样做的)。
一个小例子可以很好说明这一点,那就是一个MxN维的数组a[i][j]=a[i*M+j];我们可能把一个多维数组,映射到一个一维数组中去。
这些数组我开始索引都被假定为0;
而对于GPU,最基本的数据排列方式,是二维数组。
一维和三维的数组也是被支持的,但本教程的技术不能直接使用。
数组在GPU内存中我们把它叫做纹理或者是纹理样本。
纹理的最大尺寸在GPU中是有限定的。
每个维度的允许最大值,通过以下一小段代码便可能查询得到,这些代码能正确运行,前提是OpenGL的渲染上下文必须被正确初始化。
intmaxtexsize;
glGetIntegerv(GL_MAX_TEXTURE_SIZE,&maxtexsize);
printf("GL_MAX_TEXTURE_SIZE,%d\n",maxtexsize);
就目前主流的显卡来说,这个值一般是2048或者4096每个维度,值得提醒大家的就是:
一块显卡,虽然理论上讲它可以支持4096*4096*4096的三维浮点纹理,但实际中受到显卡内存大小的限制,一般来说,它达不到这个数字。
在CPU中,我们常会讨论到数组的索引,而在GPU中,我们需要的是纹理坐标,有了纹理坐标才可以访问纹理中每个数据的值。
而要得到纹理坐标,我们又必须先得到纹理中心的地址。
传统上讲,GPU是可以四个分量的数据同时运算的,这四个分量也就是指红、绿、蓝、alpha(RGBA)四个颜色通道。
稍后的章节中,我将会介绍如何使用显卡这一并行运算的特性,来实现我们想要的硬件加速运算。
在CPU上生成数组:
让我们来回顾一下前面所要实现的运算:
也就是给定两个长度为N的数组,现在要求两数组的加权和y=y+alpha*x,我们现在需要两个数组来保存每个浮点数的值,及一个记录alpha值的浮点数。
float*dataY=(float*)malloc(N*sizeof(float));
float*dataX=(float*)malloc(N*sizeof(float));
floatalpha;
虽然我们的实际运算是在GPU上运行,但我们仍然要在CPU上分配这些数组空间,并对数组中的每个元素进行初始化赋值。
在GPU上生成浮点纹理:
这个话题需要比较多的解释才行,让我们首先回忆一下在CPU上是如何实现的,其实简单点来说,我们就是要在GPU上建立两个浮点数组,我们将使用浮点纹理来保存数据。
有许多因素的影响,从而使问题变得复杂起来。
其中一个重要的因素就是,我们有许多不同的纹理对像可供我们选择。
即使我们排除掉一些非本地的目标,以及限定只能使用2维的纹理对像。
我们依然还有两个选择,GL_TEXTURE_2D是传统的OpenGL二维纹理对像,而ARB_texture_rectangle则是一个OpenGL扩展,这个扩展就是用来提供所谓的texturerectangles的。
对于那些没有图形学背景的程序员来说,选择后者可能会比较容易上手。
texture2Ds和texturerectangles在概念上有两大不同之处。
我们可以从下面这个列表来对比一下,稍后我还会列举一些例子。
texture2D
texturerectangle
texturetarget
GL_TEXTURE_2D
GL_TEXTURE_RECTANGLE_ARB
纹理坐标
坐标必须被单位化,范围被限定在0到1之间,其它范围不在0到1之间的纹理坐标不会被支持。
纹理坐标不要求单位化
纹理大小
纹理大小必须是2的n次方,如1024,512等。
当然如果你的显卡驱动支持ARB_non_power_of_two或者OpenGL2.0的话,则不会受到此限制。
纹理尺寸的大小是任意的,如(513x1025)
另外一个重要的影响因素就是纹理格式,我们必须谨慎选择。
在GPU中可能同时处理标量及一到四分量的向量。
本教程主要关注标量及四分量向量的使用。
比较简单的情况下我们可以在中纹理中为每个像素只分配一个单精度浮点数的储存空间,在OpenGL中,GL_LUMNANCE就是这样的一种纹理格式。
但是如果我们要想使用四个通道来作运算的话,我们就可以采用GL_RGBA这种纹理格式。
使用这种纹理格式,意味着我们会使用一个像素数据来保存四个浮点数,也就是说红、绿、蓝、alpha四个通道各占一个32位的空间,对于LUMINANCE格式的纹理,每个纹理像素只占有32位4个字节的显存空间,而对于RGBA格式,保存一个纹理像素需要的空间是4*32=128位,共16个字节。
接下来的选择,我们就要更加小心了。
在OpenGL中,有三个扩展是真正接受单精度浮点数作为内部格式的纹理的。
分别是:
NV_float_buffer,ATI_texture_float和ARB_texture_float.每个扩展都就定义了一组自已的列举参数及其标识,如:
(GL_FLOAT_R32_NV),(0x8880),在程序中使用不同的参数,可以生成不同格式的纹理对像,下面会作详细描述。
在这里,我们只对其中两个列举参数感兴趣,分别是GL_FLOAT_R32_NV和GL_FLOAT_RGBA32_NV.前者是把每个像素保存在一个浮点值中,后者则是每个像素中的四个分量分别各占一个浮点空间。
这两个列举参数,在另外两个扩展(ATI_texture_floatandARB_texture_float)中也分别有其对应的名称:
GL_LUMINANCE_FLOAT32_ATI,GL_RGBA_FLOAT32_ATI和GL_LUMINANCE32F_ARB,GL_RGBA32F_ARB。
在我看来,他们名称不同,但作用都是一样的,我想应该是多个不同的参数名称对应着一个相同的参数标识。
至于选择哪一个参数名,这只是看个人的喜好,因为它们全部都既支持NV显卡也支持ATI的显卡。
最后还有一个要解决的问题就是,我们如何把CPU中的数组元素与GPU中的纹理元素一一对应起来。
这里,我们采用一个比较容易想到的方法:
如果纹理是LUMINANCE格式,我们就把长度为N的数组,映射到一张大小为sqrt(N)xsqrt(N)和纹理中去(这里规定N是刚好能被开方的)。
如果采用RGBA的纹理格式,那么N个长度的数组,对应的纹理大小就是sqrt(N/4)xsqrt(N/4),举例说吧,如果N=1024^2,那么纹理的大小就是512*512。
以下的表格总结了我们上面所讨论的问题,作了一下分类,对应的GPU分别是:
NVIDIAGeForceFX(NV3x),GeForce6and7(NV4x,G7x)和ATI.
NV3x
NV4x,G7x(RECT)
NV4x,G7x(2D)
ATI
target
texturerectangle
texturerectangle
texture2D
texture2Dandtexturerectangle
format
LUMINANCEandRGBA(andRGandRGB)*
internal
format
NV_float_buffer
NV_float_buffer
ATI_texture_float
ARB_texture_float
ATI_texture_float
ARB_texture_float
(*)Warning:
这些格式作为纹理是被支持的,但是如果作为渲染对像,就不一定全部都能够得到良好的支持(seebelow).
讲完上面的一大堆基础理论这后,是时候回来看看代码是如何实现的。
比较幸运的是,当我们弄清楚了要用那些纹理对像、纹理格式、及内部格式之后,要生成一个纹理是很容易的。
//createanewtexturename
GLuinttexID;
glGenTextures(1,&texID);
//bindthetexturenametoatexturetarget
glBindTexture(texture_target,texID);
//turnofffilteringandsetproperwrapmode
//(obligatoryforfloattexturesatm)
glTexParameteri(texture_target,GL_TEXTURE_MIN_FILTER,GL_NEAREST);
glTexParameteri(texture_target,GL_TEXTURE_MAG_FILTER,GL_NEAREST);
glTexParameteri(texture_target,GL_TEXTURE_WRAP_S,GL_CLAMP);
glTexParameteri(texture_target,GL_TEXTURE_WRAP_T,GL_CLAMP);
//settexenvtoreplaceinsteadofthedefaultmodulate
glTexEnvi(GL_TEXTURE_ENV,GL_TEXTURE_ENV_MODE,GL_REPLACE);
//andallocategraphicsmemory
glTexImage2D(texture_target,0,internal_format,
texSize,texSize,0,texture_format,GL_FLOAT,0);
让我们来消化一下上面这段代码的最后那个OpenGL函数,我来逐一介绍一下它每个参数:
第一个参数是纹理对像,上面已经说过了;第二个参数是0,是告诉GL不要使用多重映像纹理。
接下来是内部格式及纹理大小,上面也说过了,应该清楚了吧。
第六个参数是也是0,这是用来关闭纹理边界的,这里不需要边界。
接下来是指定纹理格式,选择一种你想要的格式就可以了。
对于参数GL_FLOAT,我们不要被它表面的意思迷惑,它并不会影响我们所保存在纹理中的浮点数的精度。
其实它只与CPU方面有关系,目的就是要告诉GL稍后将要传递过去的数据是浮点型的。
最后一个参数还是0,意思是生成一个纹理,但现在不给它指定任何数据,也就是空的纹理。
该函数的调用必须按上面所说的来做,才能正确地生成一个合适的纹理。
上面这段代码,和CPU里分配内存空间的函数malloc(),功能上是很相像的,我们可能用来对比一下。
最后还有一点要提醒注意的:
要选择一个适当的数据排列映射方式。
这里指的就是纹理格式、纹理大小要与你的CPU数据相匹配,这是一个非常因地制宜的问题,根据解决的问题不同,其相应的处理问题方式也不同。
从经验上看,一些情况下,定义这样一个映射方式是很容易的,但某些情况下,却要花费你大量的时间,一个不理想的映射方式,甚至会严重影响你的系统运行。
数组索引与纹理坐标的一一对应关系:
在后面的章节中,我们会讲到如何通过一个渲染操作,来更新我们保存在纹理中的那些数据。
在我们对纹理进行运算或存取的时候,为了能够正确地控制每一个数据元素,我们得选择一个比较特殊的投影方式,把3D世界映射到2D屏幕上(从世界坐标空间到屏幕设备坐标空间),另外屏幕像素与纹理元素也要一一对应。
这种关系要成功,关键是要采用正交投影及合适的视口。
这样便能做到几何坐标(用于渲染)、纹理坐标(用作数据输入)、像素坐标(用作数据输出)三者一一对应。
有一个要提醒大家的地方:
如果使用texture2D,我们则须要对纹理坐标进行适当比例的缩放,让坐标的值在0到1之间,前面有相关的说明。
为了建立一个一一对应的映射,我们把世界坐标中的Z坐标设为0,把下面这段代码加入到i