GPGPU 概念 4: 反馈
当运算全部完成之后,的、得到的结果会被保存在目标纹理y_new中。
多次渲染传递。
在一些通用运算中,我们会希望把前一次运算结果传递给下一个运算用来作为后继运算的输入变量。但是在GPU中,一个纹理不能同时被读写,这就意味着我们要创建另外一个渲染通道,并给它绑定不同的输入输出纹理,甚至要生成一个不同的运算内核。有一种非常重要的技术可以用来解决这种多次渲染传递的问题,让运算效率得到非常好的提高,这就是“乒乓”技术。
关于乒乓技术
乒乓技术,是一个用来把渲染输出转换成为下一次运算的输入的技术。在本文中(y_new =y_old +alpha*x) ,这就意味我们要切换两个纹理的角色,y_new 和y_old 。有三种可能的方法来实现这种技术(看一下以下这篇论文Simon Green‘s FBO slides ,这是最经典的资料了):
为每个将要被用作渲染输出的纹理指定一个绑定点,并使用函数glBindFramebufferEXT()来为每个渲染通道绑定一个不同的FBO.
只使用一个FBO,但每次通道渲染的时候,使用函数glBindFramebufferEXT()来重新绑定渲染的目标纹理。
使用一个FBO和多个绑定点,使用函数glDrawBuffer()来交换它们。
由于每个FBO最多有4个绑定点可以被使用,而且,最后一种方法的运算是最快的,我们在这里将详细解释一下,看看我们是如何在两个不同的绑定点之间实现“乒乓” 的。
要实现这个,我们首先需要一组用于管理控制的变量。
[cpp] view plaincopy// two textures identifiers referencing y_old and y_new
GLuint yTexID[2];
// ping pong management vars
int writeTex = 0;
int readTex = 1;
GLenum attachmentpoints[] = { GL_COLOR_ATTACHMENT0_EXT,
GL_COLOR_ATTACHMENT1_EXT
};
在运算其间,我们只需要做的就是给内核传递正确的参数值,并且每次运算都要交换一次组组的索引值:
[cpp] view plaincopy// attach two textures to FBO
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
attachmentpoints[writeTex],
texture_Target, yTexID[writeTex], 0);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
attachmentpoints[readTex],
texture_Target, yTexID[readTex], 0);
// enable fragment profile, bind program [。。。]
// enable texture x (read-only) and uniform parameter [。。。]
// iterate computation several times
for (int i=0; i《numIterations; i++) {
// set render destination
glDrawBuffer (attachmentpoints[writeTex]);
// enable texture y_old (read-only)
cgGLSetTextureParameter(yParam, yTexID[readTex]);
cgGLEnableTextureParameter(yParam);
// and render multitextured viewport-sized quad
// swap role of the two textures (read-only source becomes
// write-only target and the other way round):
swap();
}
Back to top
把所有东西放在一起
对本文附带源代码的一个简要说明
在附带的代码例子中,使用到了本文所有阐述过的所有概念,主要实现了以下几个运算:
为每个数组生成一个浮点的纹理。
把初始化的数据传输到纹理中去 。
使用CG或者GLSL来生成一个片段着色器。
一个多次重复运算的模块,主要是用来演试“乒乓”技术。
把最终的运算结果返回到主内存中。
把结果与CPU的参考结果进行比较。
执行过行中的可变化部份
在代码中,我们使用了一系列的结构体来保存各种可能的参数,主要是为了方便OpenGL的调用,例如:不同类型的浮点纹理扩展,不同的纹理格式,不同的着色器之间的细微差别,等等。下面这段代码就是这样一个结构体的示例,采用LUMINANCE格式,RECTANGLES纹理,及NV_float_buffer的扩展。
[cpp] view plaincopyrect_nv_r_32.name = “TEXRECT - float_NV - R - 32”;
rect_nv_r_32.texTarget = GL_TEXTURE_RECTANGLE_ARB;
rect_nv_r_32.texInternalFormat = GL_FLOAT_R32_NV;
rect_nv_r_32.texFormat = GL_LUMINANCE;
rect_nv_r_32.shader_source = “float saxpy (”
“in float2 coords : TEXCOORD0,”
“uniform samplerRECT textureY,”
“uniform samplerRECT textureX,”
“uniform float alpha ) : COLOR {”
“float y = texRECT (textureY, coords);”
“float x = texRECT (textureX, coords);”
“return y+alpha*x; }”;
为了给不同的情况取得一个合适的工作版本,我们只须要查找和替换就可以了。或者使用第二个命令行参数如:rect_nv_r_32。在应用程序中,一个全局变量textureParameters 指向我们实现要使用的结构体。
命令行参数
在程序中,使用命令行参数来对程序进行配置。如果你运行该程序而没带任何参数的话,程序会输出一个对各种不同参数的解释。提醒大家注意的是:本程序对命令行参数的解释是不稳定的,一个不正确的参数有可能会造成程序的崩溃。因此我强烈建义大家使用输出级的参数来显示运算的结果,这样可以降低出现问题的可能性,尤其是当你不相信某些运算错误的时候。请查看包含在示例中的批处理文件。
测试模式
本程序可以用来对一个给定的GPU及其驱动的 结合进行测试,主要是测试一下,看看哪种内部格式及纹理排列是可以在FBO扩展中被组合在一起使用的。示例中有一个批处理文件叫做:run_test_*.bat,是使用各种不同的命令行参数来运行程序,并会生成一个报告文件。如果是在LINUX下,这个文件也可能当作一个shell脚本来使用,只需要稍作修改就可以了。这ZIP文档中包含有对一些显卡测试后的结果。
基准模式
这种模式被写进程序中,完全是为了好玩。它可以对不同的问题产成一个运算时序,并在屏幕上生成MFLOP/s速率图,和其它的一些性能测试软件一样。它并不代表GPU运算能力的最高值,只是接近最高值的一种基准性能测试。想知道如何运行它的话,请查看命令行参数。
Back to top
附言
简单对比一下Windows 和 Linux,NVIDIA 和 ATI 之间的差别
对于NVIDIA的显卡,不管是Windows还是Linux,它们都提供了相同的函数来实现本教程中的例子。但如果是ATI的显卡,它对LINUX的支持就不是很好。因此如果是ATI显卡,目前还是建义在Windows下使用。
看一看这片相关的文章 table summarizing renderable texture formats on various hardware.
本文中提供下载的源代码,是在NV4X以上的显卡上编译通过的。对于ATI的用户,则要作以下的修改才行:在transferToTexture() 函数中,把NVIDIA相应部份的代码注释掉,然使用ATI版本的代码,如这里所描述的。
Cg 1.5 combined with the precompiled freeglut that ships with certain Linus distributions somehow breaks “true offscreen rendering” since a totally meaningless empty window pops up. There are three workarounds: Live with it. Use “real GLUT” instead of freeglut. Use plain X as described in the OpenGL.org wiki (just leave out the mapping of the created window to avoid it being displayed)。
问题及局限性
对于ATI显卡,当我们把数据传送到纹理中去时,如果使用glTexSubImage2D(),会产生一个非常奇怪的问题:就是原本是RGBA排列的数据,会被改变为BGRA格式。这是一个已得到确认的BUG,希望在以后的版本中能得到修正,目前只能用glDrawPixels() 来代替。
而对于NV3X系列显卡,如果想用glDrawPixels() ,则要求一定要在GPU中绑定一个着色程序。因此这里用glTexSubImage()函数代替(其实对于所有的NVIDIA 的显卡,都推荐使用该函数)。
ATI显卡,在GLSL中不支持rectangles纹理采样,甚至这样的着色代码没法被编译通过。samplerRect 或sampler2DRect 被指定为保留的关键字,ARB_texture_rextangle的扩展说明书中得到定义,但驱动没有实现对它们的支持。可以用CG来代替。
在ATI中,当我们使用glDrawPixels() 下载一个纹理的时候,如果纹理是被enable的,则会导致下载失败,这不是一个BUG,但是也是一个有争议性的问题,因为这样会使程序难以调试。
对于NVIDIA的显卡,我们不能把纹理渲染到纹理最大值的最后一行中去。也就是说,尽管我们用函数glGetIntegerv(GL_MAX_TEXTURE_SIZE,&maxtexsize); 得到的值是4096,但是你也只能渲染一张4095 x 4095 纹理。这是一个已知的BUG,同样也希望以后能得到修正。
检查OpenGL的错误
高度推荐大家在代码中经常使用以下函数来检测OpenGL运行过程中产生的错误。
[cpp] view plaincopyvoid checkGLErrors(const char *label) {
GLenum errCode;
const GLubyte *errStr;
if ((errCode = glGetError()) != GL_NO_ERROR) {
errStr = gluErrorString(errCode);
printf(“OpenGL ERROR: ”);
printf((char*)errStr);
printf(“(Label: ”);
printf(label);
printf(“) .”);
}
}
检查FBO中的错误
EXT_framebuffer_object 扩展,定义了一个很好用的运行时Debug函数。这里只列出了它的一些常见的反回值作参考,要详细解释这些返回信息,请查看规格说明书的framebuffer completeness 部分。
[cpp] view plaincopybool checkFramebufferStatus() {
GLenum status;
status=(GLenum)glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT);
switch(status) {
case GL_FRAMEBUFFER_COMPLETE_EXT:
return true;
case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT:
printf(“Framebuffer incomplete,incomplete attachment ”);
return false;
case GL_FRAMEBUFFER_UNSUPPORTED_EXT:
printf(“Unsupported framebuffer format ”);
return false;
case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:
printf(“Framebuffer incomplete,missing attachment ”);
return false;
case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:
printf(“Framebuffer incomplete,attached images
must have same dimensions ”);
return false;
case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:
printf(“Framebuffer incomplete,attached images
must have same format ”);
return false;
case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:
printf(“Framebuffer incomplete,missing draw buffer ”);
return false;
case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:
printf(“Framebuffer incomplete,missing read buffer ”);
return false;
}
return false;
}
检查CG的错误
在CG中检查错误有一些细微的不同,一个自写入的错误处理句柄被传递给CG的错误处理回调函数。
[cpp] view plaincopy// register the error callback once the context has been created
cgSetErrorCallback(cgErrorCallback);
// callback function
void cgErrorCallback(void) {
CGerror lastError = cgGetError();
if(lastError) {
printf(cgGetErrorString(lastError));
printf(cgGetLastListing(cgContext));
}
}
检查GLSL的错误
使用以下的函数来查看编译的结果:
[cpp] view plaincopy/**
* copied from
* http://www.lighthouse3d.com/opengl/glsl/index.php?oglinfo
*/
void printInfoLog(GLhandleARB obj) {
int infologLength = 0;
int charsWritten = 0;
char *infoLog;
glGetObjectParameterivARB(obj,
GL_OBJECT_INFO_LOG_LENGTH_ARB,
&infologLength);
if (infologLength 》 1) {
infoLog = (char *)malloc(infologLength);
glGetInfoLogARB(obj, infologLength,
&charsWritten, infoLog);
printf(infoLog);
printf(“ ”);
free(infoLog);
}
}
大多数情况下,你可以使用以上查询函数,详细内容可以查看一下GLSL的规格说明书。还有另一个非常重要的查询函数,是用来检查程序是否可以被连接:
[cpp] view plaincopyGLint success;
glGetObjectParameterivARB(programObject,
GL_OBJECT_LINK_STATUS_ARB,
&success);
if (!success) {
printf(“Shader could not be linked! ”);
}
评论
查看更多