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個綁定點可以被使用,而且,最后一種方法的運算是最快的,我們在這里將詳細解釋一下,看看我們是如何在兩個不同的綁定點之間實現“乒乓” 的。
要實現這個,我們首先需要一組用于管理控制的變量。
?。踓pp] 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
};
在運算其間,我們只需要做的就是給內核傳遞正確的參數值,并且每次運算都要交換一次組組的索引值:
?。踓pp] 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的擴展。
?。踓pp] 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的擴展說明書中得到定義,但驅動沒有實現對它們的支持??梢杂肅G來代替。
在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 部分。
?。踓pp] 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的錯誤
使用以下的函數來查看編譯的結果:
?。踓pp] 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! ”);
}
評論
查看更多