圖形處理器(英語:Graphics Processing Unit,縮寫:GPU),又稱顯示核心、視覺處理器、顯示芯片,是一種專門在個人電腦、工作站、游戲機和一些移動設(shè)備(如平板電腦、智能手機等)上圖像運算工作的微處理器。
圖形處置單元(或簡稱GPU)會賣力處置從PC外部傳送到所銜接表現(xiàn)器的一切內(nèi)容,不管你在玩游戲、編纂視頻或只是盯著桌面的壁紙,一切表現(xiàn)器中表現(xiàn)的圖象都是由GPU停止襯著的。本文體系極客將向人人先容甚么是GPU、它是若何事情的,和為何要為游戲和圖象密集型應(yīng)用程序設(shè)置裝備擺設(shè)公用顯卡。
對通俗用戶來講,現(xiàn)實上不消要自力顯卡就能夠向表現(xiàn)器「供給」內(nèi)容。像筆記本電腦或平板用戶,平日CPU芯片都邑集成GPU內(nèi)核,也便是人人熟稱的「核顯」,如許便可認(rèn)為對表現(xiàn)請求不高的低功耗裝備供給更好的性價比。
正因如斯,部門筆記本電腦、平板電腦和某些PC用戶來講,要想將其圖形處置器進級到更高級別也很艱苦,乃至不太能夠。這就會招致游戲(和視頻編纂等)機能欠安,只能將圖形品質(zhì)設(shè)置低落能力事情。對此類用戶而言,只要在主板支撐和余暇空間充足的情況下,增加新顯卡才能夠或許把(游戲)表現(xiàn)體驗進步到一個新的程度。
GPU發(fā)展和現(xiàn)狀
1. GPU原來就是為了加速3D渲染的,后來被拿過來做計算。
2. 現(xiàn)在GPU可以支持通用的指令,可以用傳統(tǒng)的C和C++,還有Fortran來編程。
3. 現(xiàn)在單個高端GPU的性能已經(jīng)達到了傳統(tǒng)多核CPU集群的性能
4. 有的應(yīng)用通過GPU加速相比傳統(tǒng)的多核CPU來說可以達到100X的加速。對某些特定應(yīng)用來說GPU還是更適合的。
GPU編程模型
1. 在GPU中,工作的分配是通過在調(diào)度空間上并行地應(yīng)用或者映射一個函數(shù)(或者叫做kernel)。舉例來說,一個矩陣中的每一個點就是調(diào)度空間。
2. kernel就是描述在一個線程在調(diào)度空間中的每一個點要完成的工作。在調(diào)度空間中,每一個點都要啟動一個線程。
3. 由于GPU是在單個PCI-e卡上的協(xié)處理器,數(shù)據(jù)必須通過顯式地從系統(tǒng)內(nèi)存拷貝到GPU板上內(nèi)存。
4. GPU是以SIMD的多個group的形式組織的。在每一個SIMD的group(或者叫warp,在NIVIDA CUDA編程中為32個線程)中,所有的線程在lockstep中執(zhí)行相同的指令。這樣的在lockstep中執(zhí)行相同指令的多個線程就叫做warp,雖然分支是被允許的,但是如果同一個warp中的線程出現(xiàn)不同的執(zhí)行路徑,會帶來一些性能開銷。
4. 對于memory-bound的應(yīng)用來說,可能的話,同一個warp中的所有線程應(yīng)當(dāng)訪問相鄰的數(shù)據(jù)元素,同一個warp中相鄰的線程應(yīng)當(dāng)訪問相鄰的數(shù)據(jù)元素。這可能要對數(shù)據(jù)布局和數(shù)據(jù)訪問模式進行重新安排。
5. GPU有多個內(nèi)存空間可用于開發(fā)數(shù)據(jù)訪問模式。除了golbal memory以外,還有constant memory(read-only, cached),,texture memory(read-only, cached, optimized for neighboring regions of an array)和per-block shared memory(a fast memory space within each warp processor, managed explicitly by the programmer)。
6. GPU編程有兩個主要平臺,一個是OpenCL,一個編程方式類似OpenGL的產(chǎn)業(yè)標(biāo)準(zhǔn),還有另一個是為了C/C++ Fortran的CUDA,在NVIDIA的GPU上編程。
7. OpenCL/CUDA編譯器并不是把C代碼轉(zhuǎn)換成CUDA代碼,編程人員最主要的工作還是選擇算法和數(shù)據(jù)結(jié)構(gòu)。例如在GPU上,基數(shù)排序和歸并排序要比堆排序和快速排序好。Some programming effort is also required to write the necessary CUDA kernel(s) as well as to add code to transfer data to the GPU,launch the kernel(s), and then read back the results from the GPU.
什么應(yīng)用適合GPU
1. 內(nèi)核中有豪多并行線程的應(yīng)用
2. 對于線程間的數(shù)據(jù)交換都發(fā)生在kernel調(diào)度空間中的相鄰線程之間的應(yīng)用,因為這樣就可以用到per-block shared memory.
3. 數(shù)據(jù)并行的應(yīng)用,多個線程做相似工作,循環(huán)是數(shù)據(jù)并行的主要來源。
4. 那些能得到很好的天然硬件支持的應(yīng)用,如倒數(shù)和反平方根,不過在編程中要打開“fastmath”選項,確保使用硬件支持功能。
5. 需要對每個數(shù)據(jù)元素做大量的計算,或者能夠充分利用寬內(nèi)存接口(wide memory interface這里有疑問)
6. 做同步操作較少的應(yīng)用。
什么應(yīng)用不適合GPU
1. 并行度小的應(yīng)用,如需要的線程數(shù)小于100個,那么使用GPU加速效果不明顯
2. 不規(guī)則的任務(wù)并行---盡管應(yīng)用需要很多線程,但是這些線程都做不同的工作,那么GPU不能得到有效的利用。不過這也依賴于具體工作,多久對線程調(diào)度一次,加速的可能仍然存在。
3. 頻繁的全局同步,這要求全局的barrier,帶來很大性能開銷。
4. 在線程之間,會出現(xiàn)隨機的點對點同步的應(yīng)用。GPU對這個的支持不好,通常需要在每次同步的時候做一個全局barrier,如果要利用GPU,最好重構(gòu)算法避免出現(xiàn)這個問題。
5. 要求計算量(相比于數(shù)據(jù)傳輸量)少的應(yīng)用。盡管在CPU+GPU計算結(jié)構(gòu)中,GPU可以帶來計算性能的提升,但是這些提升都被向GPU傳輸數(shù)據(jù)所消耗的實踐覆蓋了。舉個例子,對于兩個向量求和運算,如果非常大的向量的話,一般都選擇在CPU上算,否則傳輸?shù)紾PU上的時間開銷很大。
硬件需求。
你需要有NVIDIA GeForce FX 或者 ATI RADEON 9500 以上的顯卡, 一些老的顯卡可能不支持我們所需要的功能(主要是單精度浮點數(shù)據(jù)的存取及運算) 。
軟件需求
首先,你需要一個C/C++編譯器。你有很多可以選擇,如:Visual Studio .NET 2003, Eclipse 3.1 plus CDT/MinGW, the Intel C++ Compiler 9.0 及 GCC 3.4+等等。然后更新你的顯卡驅(qū)動讓它可以支持一些最新特性。
本文所附帶的源代碼,用到了兩個擴展庫,GLUT 和 GLEW 。對于windows系統(tǒng),GLUT可以在 這里下載到,而Linux 的freeglut和freeglut-devel大多的版本都集成了。GLEW可以在SourceForge 上下載到,對于著色語言,大家可以選擇GLSL或者CG,GLSL在你安裝驅(qū)動的時候便一起裝好了。如果你想用CG,那就得下載Cg Toolkit 。
二者擇其一
大家如果要找DirectX版本的例子的話,請看一下Jens Krügers的《 Implicit Water Surface》 demo(該例子好像也有OpenGL 版本的)。當(dāng)然,這只是一個獲得高度評價的示例源代碼,而不是教程的。
有一些從圖形著色編程完全抽象出來的GPU的元程序語言,把底層著色語言作了封裝,讓你不用學(xué)習(xí)著色語言,便能使用顯卡的高級特性,其中BrookGPU 和Sh 就是比較出名的兩個項目。
Back to top
初始化OpenGL
GLUT
GLUT(OpenGLUtility Toolkit)該開發(fā)包主要是提供了一組窗口函數(shù),可以用來處理窗口事件,生成簡單的菜單。我們使用它可以用盡可能少的代碼來快速生成一個OpenGL 開發(fā)環(huán)境,另外呢,該開發(fā)包具有很好的平***立性,可以在當(dāng)前所有主流的操作系統(tǒng)上運行 (MS-Windows or Xfree/Xorg on Linux / Unix and Mac)。
[cpp] view plaincopy// include the GLUT header file
#include 《GL/glut.h》
// call this and pass the command line arguments from main()
void initGLUT(int argc, char **argv) {
glutInit ( &argc, argv );
glutCreateWindow(“SAXPY TESTS”);
}
OpenGL 擴展
許多高級特性,如那些要在GPU上進行普通浮點運算的功能,都不是OpenGL內(nèi)核的一部份。因此,OpenGL Extensions通過對OpenGL API的擴展, 為我們提供了一種可以訪問及使用硬件高級特性的機制。OpenGL擴展的特點:不是每一種顯卡都支持該擴展,即便是該顯卡在硬件上支持該擴展,但不同版本的顯卡驅(qū)動,也會對該擴展的運算能力造成影響,因為OpenGL擴展設(shè)計出來的目的,就是為了最大限度地挖掘顯卡運算的能力,提供給那些在該方面有特別需求的程序員來使用。在實際編程的過程中,我們必須小心檢測當(dāng)前系統(tǒng)是否支持該擴展,如果不支持的話,應(yīng)該及時把錯誤信息返回給軟件進行處理。當(dāng)然,為了降低問題的復(fù)雜性,本教程的代碼跳過了這些檢測步驟。
OpenGL Extension Registry OpenGL擴展注冊列表中,列出了幾乎所有的OpenGL可用擴展,有需要的朋友可能的查看一下。
當(dāng)我們要在程序中使用某些高級擴展功能的時候,我們必須在程序中正確引入這些擴展的擴展函數(shù)名。有一些小工具可以用來幫助我們檢測一下某個給出的擴展函數(shù)是否被當(dāng)前的硬件及驅(qū)動所支持,如:glewinfo, OpenGL extension viewer等等,甚至OpenGL本身就可以(在上面的連接中,就有一個相關(guān)的例子)。
如何獲取這些擴展函數(shù)的入口指針,是一個比較高級的問題。下面這個例子,我們使用GLEW來作為擴展載入函數(shù)庫,該函數(shù)庫把許多復(fù)雜的問題進行了底層的封裝,給我們使用高級擴展提供了一組簡潔方便的訪問函數(shù)。
?。踓pp] view plaincopyvoid initGLEW (void) {
// init GLEW, obtain function pointers
int err = glewInit();
// Warning: This does not check if all extensions used
// in a given implementation are actually supported.
// Function entry points created by glewInit() will be
// NULL in that case!
if (GLEW_OK != err) {
printf((char*)glewGetErrorString(err));
exit(ERROR_GLEW);
}
}
OpenGL離屏渲染的準(zhǔn)備工作
在傳統(tǒng)的GPU渲染流水線中,每次渲染運算的最終結(jié)束點就是幀緩沖區(qū)。所謂幀緩沖區(qū),其實是顯卡內(nèi)存中的一塊,它特別這處在于,保存在該內(nèi)存區(qū)塊中的圖像數(shù)據(jù),會實時地在顯示器上顯示出來。根據(jù)顯示器設(shè)置的不同,幀緩沖區(qū)最大可以取得32位的顏色深度,也就是說紅、綠、藍、alpha四個顏色通道共享這32位的數(shù)據(jù),每個通道占8位。當(dāng)然用32位來記錄顏色,如果加起來的話,可以表示160萬種不同的顏色,這對于顯示器來說可能是足夠了,但是如果我們要在浮點數(shù)字下工作,用8位來記錄一個浮點數(shù),其數(shù)學(xué)精度是遠遠不夠的。另外還有一個問題就是,幀緩存中的數(shù)據(jù)最大最小值會被限定在一個范圍內(nèi),也就是 [0/255; 255/255]
如何解決以上的一些問題呢?一種比較苯拙的做法就是用有符號指數(shù)記數(shù)法,把一個標(biāo)準(zhǔn)的IEEE 32位浮點數(shù)映射保存到8位的數(shù)據(jù)中。不過幸運的是,我們不需要這樣做。首先,通過使用一些OpenGL的擴展函數(shù),我們可以給GPU提供32位精度的浮點數(shù)。另外有一個叫EXT_framebuffer_object 的OpenGL的擴展, 該擴展允許我們把一個離屏緩沖區(qū)作為我們渲染運算的目標(biāo),這個離屏緩沖區(qū)中的RGBA四個通道,每個都是32位浮點的,這樣一來, 要想GPU上實現(xiàn)四分量的向量運算就比較方便了,而且得到的是一個全精度的浮點數(shù),同時也消除了限定數(shù)值范圍的問題。我們通常把這一技術(shù)叫FBO,也就是Frame Buffer Object的縮寫。
要使用該擴展,或者說要把傳統(tǒng)的幀緩沖區(qū)關(guān)閉,使用一個離屏緩沖區(qū)作我們的渲染運算區(qū),只要以下很少的幾行代碼便可以實現(xiàn)了。有一點值得注意的是:當(dāng)我用使用數(shù)字0,來綁定一個FBO的時候,無論何時,它都會還原window系統(tǒng)的特殊幀緩沖區(qū),這一特性在一些高級應(yīng)用中會很有用,但不是本教程的范圍,有興趣的朋友可能自已研究一下。
?。踓pp] view plaincopyGLuint fb;
void initFBO(void) {
// create FBO (off-screen framebuffer)
glGenFramebuffersEXT(1, &fb);
// bind offscreen buffer
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fb);
}
Back to top
GPGPU 概念
1: 數(shù)組 = 紋理
一維數(shù)組是本地CPU最基本的數(shù)據(jù)排列方式,多維的數(shù)組則是通過對一個很大的一維數(shù)組的基準(zhǔn)入口進行坐標(biāo)偏移來訪問的(至少目前大多數(shù)的編譯器都是這樣做的)。一個小例子可以很好說明這一點,那就是一個MxN維的數(shù)組 a[i][j] = a[i*M+j];我們可能把一個多維數(shù)組,映射到一個一維數(shù)組中去。這些數(shù)組我開始索引都被假定為0;
而對于GPU,最基本的數(shù)據(jù)排列方式,是二維數(shù)組。一維和三維的數(shù)組也是被支持的,但本教程的技術(shù)不能直接使用。數(shù)組在GPU內(nèi)存中我們把它叫做紋理或者是紋理樣本。紋理的最大尺寸在GPU中是有限定的。每個維度的允許最大值,通過以下一小段代碼便可能查詢得到,這些代碼能正確運行,前提是OpenGL的渲染上下文必須被正確初始化。
?。踓pp] view plaincopyint maxtexsize;
glGetIntegerv(GL_MAX_TEXTURE_SIZE,&maxtexsize);
printf(“GL_MAX_TEXTURE_SIZE, %d ”,maxtexsize);
就目前主流的顯卡來說,這個值一般是2048或者4096每個維度,值得提醒大家的就是:一塊顯卡,雖然理論上講它可以支持4096*4096*4096的三維浮點紋理,但實際中受到顯卡內(nèi)存大小的限制,一般來說,它達不到這個數(shù)字。
在CPU中,我們常會討論到數(shù)組的索引,而在GPU中,我們需要的是紋理坐標(biāo),有了紋理坐標(biāo)才可以訪問紋理中每個數(shù)據(jù)的值。而要得到紋理坐標(biāo),我們又必須先得到紋理中心的地址。
傳統(tǒng)上講,GPU是可以四個分量的數(shù)據(jù)同時運算的,這四個分量也就是指紅、綠、藍、alpha(RGBA)四個顏色通道。稍后的章節(jié)中,我將會介紹如何使用顯卡這一并行運算的特性,來實現(xiàn)我們想要的硬件加速運算。
在CPU上生成數(shù)組
讓我們來回顧一下前面所要實現(xiàn)的運算:也就是給定兩個長度為N的數(shù)組,現(xiàn)在要求兩數(shù)組的加權(quán)和y=y+alpha*x,我們現(xiàn)在需要兩個數(shù)組來保存每個浮點數(shù)的值,及一個記錄alpha值的浮點數(shù)。
?。踓pp] view plaincopyfloat* dataY = (float*)malloc(N*sizeof(float)); float* dataX = (float*)malloc(N*sizeof(float)); float alpha;
雖然我們的實際運算是在GPU上運行,但我們?nèi)匀灰贑PU上分配這些數(shù)組空間,并對數(shù)組中的每個元素進行初始化賦值。
在GPU上生成浮點紋理
這個話題需要比較多的解釋才行,讓我們首先回憶一下在CPU上是如何實現(xiàn)的,其實簡單點來說,我們就是要在GPU上建立兩個浮點數(shù)組,我們將使用浮點紋理來保存數(shù)據(jù)。
有許多因素的影響,從而使問題變得復(fù)雜起來。其中一個重要的因素就是,我們有許多不同的紋理對像可供我們選擇。即使我們排除掉一些非本地的目標(biāo),以及限定只能使用2維的紋理對像。我們依然還有兩個選擇,GL_TEXTURE_2D是傳統(tǒng)的OpenGL二維紋理對像,而ARB_texture_rectangle則是一個OpenGL擴展,這個擴展就是用來提供所謂的texture rectangles的。對于那些沒有圖形學(xué)背景的程序員來說,選擇后者可能會比較容易上手。texture2Ds 和 texture rectangles 在概念上有兩大不同之處。我們可以從下面這個列表來對比一下,稍后我還會列舉一些例子。
另外一個重要的影響因素就是紋理格式,我們必須謹(jǐn)慎選擇。在GPU中可能同時處理標(biāo)量及一到四分量的向量。本教程主要關(guān)注標(biāo)量及四分量向量的使用。比較簡單的情況下我們可以在中紋理中為每個像素只分配一個單精度浮點數(shù)的儲存空間,在OpenGL中,GL_LUMNANCE就是這樣的一種紋理格式。但是如果我們要想使用四個通道來作運算的話,我們就可以采用GL_RGBA這種紋理格式。使用這種紋理格式,意味著我們會使用一個像素數(shù)據(jù)來保存四個浮點數(shù),也就是說紅、綠、藍、alpha四個通道各占一個32位的空間,對于LUMINANCE格式的紋理,每個紋理像素只占有32位4個字節(jié)的顯存空間,而對于RGBA格式,保存一個紋理像素需要的空間是4*32=128位,共16個字節(jié)。
接下來的選擇,我們就要更加小心了。在OpenGL中,有三個擴展是真正接受單精度浮點數(shù)作為內(nèi)部格式的紋理的。分別是:NV_float_buffer,ATI_texture_float 和ARB_texture_float.每個擴展都就定義了一組自已的列舉參數(shù)及其標(biāo)識,如:(GL_FLOAT_R32_NV) ,( 0x8880),在程序中使用不同的參數(shù),可以生成不同格式的紋理對像,下面會作詳細(xì)描述。
在這里,我們只對其中兩個列舉參數(shù)感興趣,分別是GL_FLOAT_R32_NV和GL_FLOAT_RGBA32_NV. 前者是把每個像素保存在一個浮點值中,后者則是每個像素中的四個分量分別各占一個浮點空間。這兩個列舉參數(shù),在另外兩個擴展(ATI_texture_float andARB_texture_float )中也分別有其對應(yīng)的名稱:GL_LUMINANCE_FLOAT32_ATI,GL_RGBA_FLOAT32_ATI 和 GL_LUMINANCE32F_ARB,GL_RGBA32F_ARB 。在我看來,他們名稱不同,但作用都是一樣的,我想應(yīng)該是多個不同的參數(shù)名稱對應(yīng)著一個相同的參數(shù)標(biāo)識。至于選擇哪一個參數(shù)名,這只是看個人的喜好,因為它們?nèi)慷技戎С諲V顯卡也支持ATI的顯卡。
最后還有一個要解決的問題就是,我們?nèi)绾伟袰PU中的數(shù)組元素與GPU中的紋理元素一一對應(yīng)起來。這里,我們采用一個比較容易想到的方法:如果紋理是LUMINANCE格式,我們就把長度為N的數(shù)組,映射到一張大小為sqrt(N) x sqrt(N)和紋理中去(這里規(guī)定N是剛好能被開方的)。如果采用RGBA的紋理格式,那么N個長度的數(shù)組,對應(yīng)的紋理大小就是sqrt(N/4) x sqrt(N/4),舉例說吧,如果N=1024^2,那么紋理的大小就是512*512 。
以下的表格總結(jié)了我們上面所討論的問題,作了一下分類,對應(yīng)的GPU分別是: NVIDIA GeForce FX (NV3x), GeForce 6 and 7 (NV4x, G7x) 和 ATI.
(*) Warning: 這些格式作為紋理是被支持的,但是如果作為渲染對像,就不一定全部都能夠得到良好的支持(seebelow)。
講完上面的一大堆基礎(chǔ)理論這后,是時候回來看看代碼是如何實現(xiàn)的。比較幸運的是,當(dāng)我們弄清楚了要用那些紋理對像、紋理格式、及內(nèi)部格式之后,要生成一個紋理是很容易的。
?。踓pp] view plaincopy// create a new texture name
GLuint texID;
glGenTextures (1, &texID);
// bind the texture name to a texture target
glBindTexture(texture_target,texID);
// turn off filtering and set proper wrap mode
// (obligatory for float textures atm)
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);
// set texenv to replace instead of the default modulate
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
// and allocate graphics memory
glTexImage2D(texture_target, 0, internal_format,
texSize, texSize, 0, texture_format, GL_FLOAT, 0);
讓我們來消化一下上面這段代碼的最后那個OpenGL函數(shù),我來逐一介紹一下它每個參數(shù):第一個參數(shù)是紋理對像,上面已經(jīng)說過了;第二個參數(shù)是0,是告訴GL不要使用多重映像紋理。接下來是內(nèi)部格式及紋理大小,上面也說過了,應(yīng)該清楚了吧。第六個參數(shù)是也是0,這是用來關(guān)閉紋理邊界的,這里不需要邊界。接下來是指定紋理格式,選擇一種你想要的格式就可以了。對于參數(shù)GL_FLOAT,我們不要被它表面的意思迷惑,它并不會影響我們所保存在紋理中的浮點數(shù)的精度。其實它只與CPU方面有關(guān)系,目的就是要告訴GL稍后將要傳遞過去的數(shù)據(jù)是浮點型的。最后一個參數(shù)還是0,意思是生成一個紋理,但現(xiàn)在不給它指定任何數(shù)據(jù),也就是空的紋理。該函數(shù)的調(diào)用必須按上面所說的來做,才能正確地生成一個合適的紋理。上面這段代碼,和CPU里分配內(nèi)存空間的函數(shù)malloc(),功能上是很相像的,我們可能用來對比一下。
最后還有一點要提醒注意的:要選擇一個適當(dāng)?shù)臄?shù)據(jù)排列映射方式。這里指的就是紋理格式、紋理大小要與你的CPU數(shù)據(jù)相匹配,這是一個非常因地制宜的問題,根據(jù)解決的問題不同,其相應(yīng)的處理問題方式也不同。從經(jīng)驗上看,一些情況下,定義這樣一個映射方式是很容易的,但某些情況下,卻要花費你大量的時間,一個不理想的映射方式,甚至?xí)?yán)重影響你的系統(tǒng)運行。
數(shù)組索引與紋理坐標(biāo)的一一對應(yīng)關(guān)系
在后面的章節(jié)中,我們會講到如何通過一個渲染操作,來更新我們保存在紋理中的那些數(shù)據(jù)。在我們對紋理進行運算或存取的時候,為了能夠正確地控制每一個數(shù)據(jù)元素,我們得選擇一個比較特殊的投影方式,把3D世界映射到2D屏幕上(從世界坐標(biāo)空間到屏幕設(shè)備坐標(biāo)空間),另外屏幕像素與紋理元素也要一一對應(yīng)。這種關(guān)系要成功,關(guān)鍵是要采用正交投影及合適的視口。這樣便能做到幾何坐標(biāo)(用于渲染)、紋理坐標(biāo)(用作數(shù)據(jù)輸入)、像素坐標(biāo)(用作數(shù)據(jù)輸出)三者一一對應(yīng)。有一個要提醒大家的地方:如果使用texture2D,我們則須要對紋理坐標(biāo)進行適當(dāng)比例的縮放,讓坐標(biāo)的值在0到1之間,前面有相關(guān)的說明。
為了建立一個一一對應(yīng)的映射,我們把世界坐標(biāo)中的Z坐標(biāo)設(shè)為0,把下面這段代碼加入到initFBO()這個函數(shù)中
?。踓pp] view plaincopy// viewport for 1:1 pixel=texel=geometry mapping
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0, texSize, 0.0, texSize);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glViewport(0, 0, texSize, texSize);
使用紋理作為渲染對像
其實一個紋理,它不僅可以用來作數(shù)據(jù)輸入對像,也還可以用作數(shù)據(jù)輸出對像。這也是提高GPU運算效率和關(guān)鍵所在。通過使用 framebuffer_object這個擴展,我們可以把數(shù)據(jù)直接渲染輸出到一個紋理上。但是有一個缺點:一個紋理對像不能同時被讀寫,也就是說,一個紋理,要么是只讀的,要么就是只寫的。顯卡設(shè)計的人提供這樣一個解釋:GPU在同一時間段內(nèi)會把渲染任務(wù)分派到幾個通道并行運行, 它們之間都是相互獨立的(稍后的章節(jié)會對這個問題作詳細(xì)的討論)。如果我們允許對一個紋理同時進行讀寫操作的話,那我們需要一個相當(dāng)復(fù)雜的邏輯算法來解決讀寫沖突的問題, 即使在芯片邏輯上可以做到,但是對于GPU這種沒有數(shù)據(jù)安全性約束的處理單元來說,也是沒辦法把它實現(xiàn)的,因為GPU并不是基von Neumann的指令流結(jié)構(gòu),而是基于數(shù)據(jù)流的結(jié)構(gòu)。因此在我們的程序中,我們要用到3個紋理,兩個只讀紋理分別用來保存輸入數(shù)組x,y。一個只寫紋理用來保存運算結(jié)果。用這種方法意味著要把先前的運算公式:y = y + alpha * x 改寫為:y_new = y_old + alpha * x.
FBO 擴展提供了一個簡單的函數(shù)來實現(xiàn)把數(shù)據(jù)渲染到紋理。為了能夠使用一個紋理作為渲染對像,我們必須先把這個紋理與FBO綁定,這里假設(shè)離屏幀緩沖已經(jīng)被指定好了。
?。踓pp] view plaincopyglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, texture_target, texID, 0);
第一個參數(shù)的意思是很明顯的。第二個參數(shù)是定義一個綁定點(每個FBO最大可以支持四個不同的綁定點,當(dāng)然,不同的顯卡對這個最大綁定數(shù)的支持不一樣,可以用GL_MAX_COLOR_ATTACHMENTS_EXT來查詢一下)。第三和第四個參數(shù)應(yīng)該清楚了吧,它們是實際紋理的標(biāo)識。最后一個參數(shù)指的是使用多重映像紋理,這里沒有用到,因此設(shè)為0。
為了能成功綁定一紋理,在這之前必須先用glTexImage2D()來對它定義和分配空間。但不須要包含任何數(shù)據(jù)。我們可以把FBO想像為一個數(shù)據(jù)結(jié)構(gòu)的指針,為了能夠?qū)σ粋€指定的紋理直接進行渲染操作,我們須要做的就調(diào)用OpenGL來給這些指針賦以特定的含義。
不幸的是,在FBO的規(guī)格中,只有GL_RGB和GL_RGBA兩種格式的紋理是可以被綁定為渲染對像的(后來更新這方面得到了改進),LUMINANCE這種格式的綁定有希望在后繼的擴展中被正式定義使用。在我定本教程的時候,NVIDIA的硬件及驅(qū)動已經(jīng)對這個全面支持,但是只能結(jié)會對應(yīng)的列舉參數(shù)NV_float_buffer一起來使用才行。換句話說,紋理中的浮點數(shù)的格式與渲染對像中的浮點數(shù)格式有著本質(zhì)上的區(qū)別。
下面這個表格對目前不同的顯卡平臺總結(jié)了一下,指的是有哪些紋理格式及紋理對像是可能用來作為渲染對像的,(可能還會有更多被支持的格式,這里只關(guān)心是浮點數(shù)的紋理格式):
列表中最后一行所列出來的格式在目前來說,不能被所有的GPU移植使用。如果你想采用LUMINANCE格式,你必須使用ractangles紋理,并且只能在NVIDIA的顯卡上運行。想要寫出兼容NVIDIA及ATI兩大類顯卡的代是可能的,但只支持NV4x以上。幸運的是要修改的代碼比較少,只在一個switch開關(guān),便能實現(xiàn)代碼的可移植性了。相信隨著ARB新版本擴展的發(fā)布,各平臺之間的兼容性將會得到進一步的提高,到時候各種不同的格式也可能相互調(diào)用了。
把數(shù)據(jù)從CPU的數(shù)組傳輸?shù)紾PU的紋理
為了把數(shù)據(jù)傳輸?shù)郊y理中去,我們必須綁定一個紋理作為紋理目標(biāo),并通過一個GL函數(shù)來發(fā)送要傳輸?shù)臄?shù)據(jù)。實際上就是把數(shù)據(jù)的首地址作為一個參數(shù)傳遞給該涵數(shù),并指定適當(dāng)?shù)募y理大小就可以了。如果用LUMINANCE格式,則意味著數(shù)組中必須有texSize x texSize個元數(shù)。而RGBA格式,則是這個數(shù)字的4倍。注意的是,在把數(shù)據(jù)從內(nèi)存?zhèn)鞯斤@卡的過程中,是全完不需要人為來干預(yù)的,由驅(qū)動來自動完成。一但傳輸完成了,我們便可能對CPU上的數(shù)據(jù)作任意修改,這不會影響到顯卡中的紋理數(shù)據(jù)。 而且我們下次再訪問該紋理的時候,它依然是可用的。在NVIDIA的顯卡中,以下的代碼是得到硬件加速的。
?。踓pp] view plaincopyglBindTexture(texture_target, texID);
glTexSubImage2D(texture_target,0,0,0,texSize,texSize,
texture_format,GL_FLOAT,data);
這里三個值是0的參數(shù),是用來定義多重映像紋理的,由于我們這里要求一次把整個數(shù)組傳輸一個紋理中,不會用到多重映像紋理,因此把它們都關(guān)閉掉。
以上是NVIDIA顯卡的實現(xiàn)方法,但對于ATI的顯卡,以下的代碼作為首選的技術(shù)。在ATI顯卡中,要想把數(shù)據(jù)傳送到一個已和FBO綁定的紋理中的話,只需要把OpenGL的渲染目標(biāo)改為該綁定的FBO對像就可以了。
glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);glRasterPos2i(0,0);glDrawPixels(texSize,texSize,texture_format,GL_FLOAT,data);
第一個函數(shù)是改變輸出的方向,第二個函數(shù)中我們使用了起點作為參與點,因為我們在第三個函數(shù)中要把整個數(shù)據(jù)塊都傳到紋理中去。
兩種情況下,CPU中的數(shù)據(jù)都是以行排列的方式映射到紋理中去的。更詳細(xì)地說,就是:對于RGBA格式,數(shù)組中的前四個數(shù)據(jù),被傳送到紋理的第一個元素的四個分量中,分別與R,G,B,A分量一一對應(yīng),其它類推。而對于LUMINANCE 格式的紋理,紋理中第一行的第一個元素,就對應(yīng)數(shù)組中的第一個數(shù)據(jù)。其它紋理元素,也是與數(shù)組中的數(shù)據(jù)一一對應(yīng)的。
把數(shù)據(jù)從GPU紋理,傳輸?shù)紺PU的數(shù)組
這是一個反方向的操作,那就是把數(shù)據(jù)從GPU傳輸回來,存放在CPU的數(shù)組上。同樣,有兩種不同的方法可供我們選擇。傳統(tǒng)上,我們是使用OpenGL獲取紋理的方法,也就是綁定一個紋理目標(biāo),然后調(diào)用glGetTexImage()這個函數(shù)。這些函數(shù)的參數(shù),我們在前面都有見過。
glBindTexture(texture_target,texID);glGetTexImage(texture_target,0,texture_format,GL_FLOAT,data);
但是這個我們將要讀取的紋理,已經(jīng)和一個FBO對像綁定的話,我們可以采用改變渲染指針方向的技術(shù)來實現(xiàn)。
glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);glReadPixels(0,0,texSize,texSize,texture_format,GL_FLOAT,data);
由于我們要讀取GPU的整個紋理,因此這里前面兩個參數(shù)是0,0。表示從0起始點開始讀取。該方法是被推薦使用的。
一個忠告:比起在GPU內(nèi)部的傳輸來說,數(shù)據(jù)在主機內(nèi)存與GPU內(nèi)存之間相互傳輸,其花費的時間是巨大的,因此要謹(jǐn)慎使用。由其是從CPU到GPU的逆向傳輸。
在前面“ 當(dāng)前顯卡設(shè)備運行的問題” 中 提及到該方面的問題。
一個簡單的例子
?。踓pp] view plaincopy#include 《stdio.h》
#include 《stdlib.h》
#include 《GL/glew.h》
#include 《GL/glut.h》
int main(int argc, char **argv) {
// 這里聲明紋理的大小為:teSize;而數(shù)組的大小就必須是texSize*texSize*4
int texSize = 2;
int i;
// 生成測試數(shù)組的數(shù)據(jù)
float* data = (float*)malloc(4*texSize*texSize*sizeof(float));
float* result = (float*)malloc(4*texSize*texSize*sizeof(float));
for (i=0; i《texSize*texSize*4; i++)
data[i] = (i+1.0)*0.01F;
// 初始化OpenGL的環(huán)境
glutInit (&argc, argv);
glutCreateWindow(“TEST1”);
glewInit();
// 視口的比例是 1:1 pixel=texel=data 使得三者一一對應(yīng)
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluOrtho2D(0.0,texSize,0.0,texSize);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glViewport(0,0,texSize,texSize);
// 生成并綁定一個FBO,也就是生成一個離屏渲染對像
GLuint fb;
glGenFramebuffersEXT(1,&fb);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,fb);
// 生成兩個紋理,一個是用來保存數(shù)據(jù)的紋理,一個是用作渲染對像的紋理
GLuint tex,fboTex;
glGenTextures (1, &tex);
glGenTextures (1, &fboTex);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB,fboTex);
// 設(shè)定紋理參數(shù)
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
GL_TEXTURE_WRAP_T, GL_CLAMP);
// 這里在顯卡上分配FBO紋理的貯存空間,每個元素的初始值是0;
glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,0,GL_RGBA32F_ARB,
texSize,texSize,0,GL_RGBA,GL_FLOAT,0);
// 分配數(shù)據(jù)紋理的顯存空間
glBindTexture(GL_TEXTURE_RECTANGLE_ARB,tex);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
GL_TEXTURE_WRAP_S, GL_CLAMP);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,
GL_TEXTURE_WRAP_T, GL_CLAMP);
glTexEnvf(GL_TEXTURE_ENV,GL_TEXTURE_ENV_COLOR,GL_DECAL);
glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,0,GL_RGBA32F_ARB,
texSize,texSize,0,GL_RGBA,GL_FLOAT,0);
//把當(dāng)前的FBO對像,與FBO紋理綁定在一起
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
GL_COLOR_ATTACHMENT0_EXT,
GL_TEXTURE_RECTANGLE_ARB,fboTex,0);
// 把本地數(shù)據(jù)傳輸?shù)斤@卡的紋理上。
glBindTexture(GL_TEXTURE_RECTANGLE_ARB,tex);
glTexSubImage2D(GL_TEXTURE_RECTANGLE_ARB,0,0,0,texSize,texSize,
GL_RGBA,GL_FLOAT,data);
//--------------------begin-------------------------
//以下代碼是渲染一個大小為texSize * texSize矩形,
//其作用就是把紋理中的數(shù)據(jù),經(jīng)過處理后,保存到幀緩沖中去,
//由于用到了離屏渲染,這里的幀緩沖區(qū)指的就是FBO紋理。
//在這里,只是簡單地把數(shù)據(jù)從紋理直接傳送到幀緩沖中,
//沒有對這些流過GPU的數(shù)據(jù)作任何處理,但是如果我們會用CG、
//GLSL等高級著色語言,對顯卡進行編程,便可以在GPU中
//截獲這些數(shù)據(jù),并對它們進行任何我們所想要的復(fù)雜運算。
//這就是GPGPU技術(shù)的精髓所在。問題討論:www.physdev.com
glColor4f(1.00f,1.00f,1.00f,1.0f);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB,tex);
glEnable(GL_TEXTURE_RECTANGLE_ARB);
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex2f(0.0, 0.0);
glTexCoord2f(texSize, 0.0);
glVertex2f(texSize, 0.0);
glTexCoord2f(texSize, texSize);
glVertex2f(texSize, texSize);
glTexCoord2f(0.0, texSize);
glVertex2f(0.0, texSize);
glEnd();
//--------------------end------------------------
// 從幀緩沖中讀取數(shù)據(jù),并把數(shù)據(jù)保存到result數(shù)組中。
glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
glReadPixels(0, 0, texSize, texSize,GL_RGBA,GL_FLOAT,result);
// 顯示最終的結(jié)果
printf(“Data before roundtrip: ”);
for (i=0; i《texSize*texSize*4; i++)
printf(“%f ”,data[i]);
printf(“Data after roundtrip: ”);
for (i=0; i《texSize*texSize*4; i++)
printf(“%f ”,result[i]);
// 釋放本地內(nèi)存
free(data);
free(result);
// 釋放顯卡內(nèi)存
glDeleteFramebuffersEXT (1,&fb);
glDeleteTextures (1,&tex);
glDeleteTextures(1,&fboTex);
return 0;
}
現(xiàn)在是時候讓我們回頭來看一下前面要解決的問題,我強烈建議在開始一個新的更高級的話題之前,讓我們先弄一個顯淺的例子來實踐一下。下面通過一個小的程序,嘗試著使用各種不同的紋理格式,紋理對像以及內(nèi)部格式,來把數(shù)據(jù)發(fā)送到GPU,然后再把數(shù)據(jù)從GPU取回來,保存在CPU的另一個數(shù)組中。在這里,兩個過程都沒有對數(shù)據(jù)作任何運算修該,目的只是看一下數(shù)據(jù)GPU和CPU之間相互傳輸,所需要使用到的技術(shù)及要注意的細(xì)節(jié)。也就是把前面提及到的幾個有迷惑性的問題放在同一個程序中來運行一下。在稍后的章節(jié)中將會詳細(xì)討論如何來解決這些可能會出現(xiàn)的問題。
由于趕著要完成整個教程,這里就只寫了一個最為簡單的小程序,采用rectangle紋理、ARB_texture_float作紋理對像并且只能在NVIDIA的顯卡上運行。
你可以在這里下載到為ATI顯卡寫的另一個版本。
以上代碼是理解GPU編程的基礎(chǔ),如果你完全看得懂,并且能對這代碼作簡單的修改運用的話,那恭喜你,你已經(jīng)向成功邁進了一大步,并可以繼續(xù)往下看,走向更深入的學(xué)習(xí)了。但如看不懂,那回頭再看一編吧。
Back to top
GPGPU 概念 2:內(nèi)核(Kernels) = 著色器(shaders)
在這一章節(jié)中,我們來討論GPU和CPU兩大運算模塊最基本的區(qū)別,以及理清一些算法和思想。一但我們弄清楚了GPU是如何進行數(shù)據(jù)并行運算的,那我們要編寫一個自已的著色程序,還是比較容易的。
面向循環(huán)的CPU運算 vs. 面向內(nèi)核的GPU數(shù)據(jù)并行運算
讓我們來回憶一下我們所想要解決的問題:y = y + alpha* x; 在CPU上,通常我們會使用一個循環(huán)來遍歷數(shù)組中的每個元素。如下:
?。踓pp] view plaincopyfor (int i=0; i《N; i++)
dataY[i] = dataY[i] + alpha * dataX[i];
每一次的循環(huán),都會有兩個層次的運算在同時運作:在循環(huán)這外,有一個循環(huán)計數(shù)器在不斷遞增,并與我們的數(shù)組的長度值作比較。而在循環(huán)的內(nèi)部,我們利用循環(huán)計數(shù)器來確定數(shù)組的一個固定位置,并對數(shù)組該位置的數(shù)據(jù)進行訪問,在分別得到兩個數(shù)組該位置的值之后,我們便可以實現(xiàn)我們所想要的運算:兩個數(shù)組的每個元素相加了。這個運算有一個非常重要的特點:那就是我們所要訪問和計算的每個數(shù)組元數(shù),它們之間是相互獨立的。這句話的意思是:不管是輸入的數(shù)組,還是輸出結(jié)果的數(shù)組,對于同一個數(shù)組內(nèi)的各個元素是都是相互獨立的,我們可以不按順序從第一個算到最后一個,可先算最后一個,再算第一個,或在中間任意位置選一個先算,它得到的最終結(jié)果是不變的。如果我們有一個數(shù)組運算器,或者我們有N個CPU的話,我們便可以同一時間把整個數(shù)組給算出來,這樣就根本不需要一個外部的循環(huán)。我們把這樣的示例叫做SIMD(single instruction multiple data)?,F(xiàn)在有一種技術(shù)叫做“partial loop unrolling”就是讓允許編譯器對代碼進行優(yōu)化,讓程序在一些支持最新特性(如:SSE , SSE2)的CPU上能得到更高效的并行運行。
在我們這個例子中,輸入數(shù)數(shù)組的索引與輸出數(shù)組的索引是一樣,更準(zhǔn)確地說,是所有輸入數(shù)組下標(biāo),都與輸出數(shù)組的下標(biāo)是相同的,另外,在對于兩個數(shù)組,也沒有下標(biāo)的錯位訪問或一對多的訪問現(xiàn)像,如:y[i] = -x[i-1] + 2*x[[i] - x[i+1] 。這個公式可以用一句不太專業(yè)的語言來描術(shù):“組數(shù)Y中每個元素的值等于數(shù)組X中對應(yīng)下標(biāo)元素的值的兩倍,再減去該下標(biāo)位置左右兩邊元素的值。”
在這里,我們打算使用來實現(xiàn)我們所要的運算的GPU可編程模塊,叫做片段管線(fragment pipeline),它是由多個并行處理單元組成的,在GeFore7800GTX中,并行處理單元的個數(shù)多達24個。在硬件和驅(qū)動邏輯中,每個數(shù)據(jù)項會被自動分配到不同的渲染線管線中去處理,到底是如何分配,則是沒法編程控制的。從概念觀點上看,所有對每個數(shù)據(jù)頂?shù)倪\算工作都是相互獨立的,也就是說不同片段在通過管線被處理的過程中,是不相互影響的。在前面的章節(jié)中我們曾討論過,如何實現(xiàn)用一個紋理來作為渲染目標(biāo),以及如何把我們的數(shù)組保存到一個紋理上。因此這里我們分析一下這種運算方式:片段管線就像是一個數(shù)組處理器,它有能力一次處理一張紋理大小的數(shù)據(jù)。雖然在內(nèi)部運算過程中,數(shù)據(jù)會被分割開來然后分配到不同的片段處理器中去,但是我們沒辦法控制片段被處理的先后順序,我們所能知道的就是“地址”,也就是保存運算最終結(jié)果的那張紋理的紋理坐標(biāo)。我們可能想像為所有工作都是并行的,沒有任何的數(shù)據(jù)相互依賴性。這就是我們通常所說的數(shù)據(jù)并行運算(data-paralel computing)。
現(xiàn)在,我們已經(jīng)知道了解決問題的核心算法,我們可以開始討論如何用可編程片段管線來編程實現(xiàn)了。內(nèi)核,在GPU中被叫做著色器。所以,我們要做的就是寫一個可能解決問題的著色器,然后把它包含在我們的程序中。在本教程程中,我們會分別討論如何用CG著色語言及GLSL著色語言來實現(xiàn),接下來兩個小節(jié)就是對兩種語言實現(xiàn)方法的討論,我們只要學(xué)會其中一種方法就可以了,兩種語言各有它自已的優(yōu)缺點,至于哪個更好一點,則不是本教程所要討論的范圍。
用CG著色語言來編寫一個著色器
為了用CG語言來著色渲染,我們首先要來區(qū)分一下CG著色語言和CG運行時函數(shù),前者是一門新的編程語言,所寫的程序經(jīng)編譯后可以在GPU上運行,后者是C語言所寫的一系列函數(shù),在CPU上運算,主要是用來初始化環(huán)境,把數(shù)據(jù)傳送給GPU等。在GPU中,有兩種不同的著色,對應(yīng)顯卡渲染流水線的兩個不同的階段,也就是頂點著色和片段著色。本教程中,頂點著色階段,我們采用固定渲染管線。只在片段著色階段進行編程。在這里,使用片段管線能更容易解決我們的問題,當(dāng)然,頂點著色也會有它的高級用途,但本文不作介紹。另外,從傳統(tǒng)上講,片段著色管線提供更強大的運算能力。
讓我們從一段寫好了的CG著色代碼開始。回憶一下CPU內(nèi)核中包含的一些算法:在兩個包含有浮點數(shù)據(jù)的數(shù)組中查找對應(yīng)的值。我們知道在GPU中紋理就等同于CPU的數(shù)組,因此在這里我們使用紋理查找到代替數(shù)組查找。在圖形運算中,我們通過給定的紋理坐標(biāo)來對紋理進行采樣。這里有一個問題,就是如何利用硬件自動計算生成正確的紋理坐標(biāo)。我們把這個問題壓后到下面的章節(jié)來討論。為了處理一些浮點的常量,我們有兩種處理的方法可選:我們可以把這些常量包含在著色代碼代中,但是如果要該變這些常量的值的話,我們就得把著色代碼重新編譯一次。另一種方法更高效一點,就是把常量的值作為一個uniform參數(shù)傳遞給GPU。uniform參數(shù)的意思就是:在整個渲染過程中值不會被改變的。以下代碼就是采用較高較的方法寫的。
[cpp] view plaincopyfloat saxpy (
float2 coords : TEXCOORD0,
uniform sampler2D textureY,
uniform sampler2D textureX,
uniform float alpha ) : COLOR
{
float result;
float yval=y_old[i];
float y = tex2D(textureY,coords);
float xval=x[i];
float x = tex2D(textureX,coords);
y_new[i]=yval+alpha*xval;
result = y + alpha * x;
return result;
}
從概念上講,一個片段著色器,就是像上像這樣的一段小程序,這段代碼在顯卡上會對每個片段運行一編。在我們的代碼中,程序被命名為saxpy。它會接收幾個輸入?yún)?shù),并返回一個浮點值。用作變量復(fù)制的語法叫做語義綁定(semantics binding):輸入輸出參數(shù)名稱是各種不同的片段靜態(tài)變量的標(biāo)識,在前面的章節(jié)中我們把這個叫“地址”。片段著色器的輸出參數(shù)必須綁定為COLOR語義,雖然這個語義不是很直觀,因為我們的輸出參數(shù)并不是傳統(tǒng)作用上顏色,但是我們還是必須這樣做。綁定一個二分量的浮點元組(tuple ,float2)到TEXCOORD0語義上,這樣便可以在運行時為每個像素指定一對紋理坐標(biāo)。對于如何在參數(shù)中定義一個紋理樣本以及采用哪一個紋理采樣函數(shù),這就要看我們種用了哪一種紋理對像,參考下表:
如果我們使用的是四通道的紋理而不是LUMINANCE格式的紋理,那們只須把上面代碼中的用來保存紋理查詢結(jié)果的浮點型變量改為四分量的浮點變量(float4 )就可以了。由于GPU具有并行運算四分量數(shù)的能力,因此對于使用了rectangle為對像的RGBA格式紋理,我們可以采用以下代碼:
?。踓pp] view plaincopyfloat4 saxpy (
float2 coords : TEXCOORD0,
uniform samplerRECT textureY,
uniform samplerRECT textureX,
uniform float alpha ) : COLOR
{
float4 result;
float4 y = texRECT(textureY,coords);
float4 x = texRECT(textureX,coords);
result = y + alpha*x;
// equivalent: result.rgba=y.rgba+alpha*x.rgba
// or: result.r=y.r+alpha*x.y; result.g=。。。
return result;
}
我們可以把著色代碼保存在字符數(shù)組或文本文件中,然后通過OpenGL的CG運行時函數(shù)來訪問它們。
建立CG運行環(huán)境
在這一小節(jié),中描術(shù)了如何在OpenGL應(yīng)用程序中建立Cg運行環(huán)境。首先,我們要包含CG的頭文件(#include 《cg/cggl.h》),并且把CG的庫函數(shù)指定到編譯連接選項中,然后聲明一些變量。
[cpp] view plaincopy// Cg vars
CGcontext cgContext;
CGprofile fragmentProfile;
CGprogram fragmentProgram;
CGparameter yParam, xParam, alphaParam;
char* program_source = “float saxpy( [。。。。] return result; } ”;
CGcontext 是一個指向CG運行時組件的入口指針,由于我們打算對片段管線進行編程,因此我們要一個fragment profile,以及一個程序container。為了簡單起見,我們還聲明了三個句柄,分別對應(yīng)了著色程序中的三個沒有語義的入口參數(shù)。我們用一個全局的字符串變量來保存前面所寫好的著色代碼?,F(xiàn)在就把所有的CG初始化工作放在一個函數(shù)中完成。這里只作了最簡單的介紹,詳細(xì)的內(nèi)容可以查看CG手冊,或者到Cg Toolkit page.網(wǎng)頁上學(xué)習(xí)一下。
譯注:對于CG入門,可以看一下《CG編程入門》這篇文章:http://www.physdev.com/phpbb/cms_view_article.php?aid=7
[cpp] view plaincopyvoid initCG(void) {
// set up Cg
cgContext = cgCreateContext();
fragmentProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
cgGLSetOptimalOptions(fragmentProfile);
// create fragment program
fragmentProgram = cgCreateProgram (
cgContext,CG_SOURCE,program_source,
fragmentProfile,“saxpy”,NULL);
// load program
cgGLLoadProgram (fragmentProgram);
// and get parameter handles by name
yParam = cgGetNamedParameter (fragmentProgram,“textureY”);
xParam = cgGetNamedParameter (fragmentProgram,“textureX”);
alphaParam = cgGetNamedParameter (fragmentProgram,“alpha”);
}
用OpenGL著色語言來編寫一個著色器
使用OpenGL的高級著色語言,我們不需要另外引入任何的頭文件或庫文件,因因它們在安裝驅(qū)動程序的時候就一起被建立好了。三個OpenGL的擴展:(ARB_shader_objects,ARB_vertex_shader 和ARB_fragment_shader)定義了相關(guān)的接口函數(shù)。它的說明書(specification )中對語言本身作了定義。兩者,API和GLSL語言,現(xiàn)在都是OpenGL2.0內(nèi)核的一個重要組成部份。但是如果我們用的是OpenGL的老版本,就要用到擴展。
我們?yōu)槌绦驅(qū)ο穸x了一系列的全局變量,包括著色器對像及數(shù)據(jù)變量的句柄,通過使用這些句柄,我們可以訪問著色程序中的變量。前面兩個對像是簡單的數(shù)據(jù)容器,由OpenGL進行管理。一個完整的著色程序是由頂點著色和片段著色兩大部份組成的,每部分又可以由多個著色程序組成。
?。踓pp] view plaincopy// GLSL vars
GLhandleARB programObject;
GLhandleARB shaderObject;
GLint yParam, xParam, alphaParam;
編寫著色程序和使用Cg語言是相似的,下面提供了兩個GLSL的例子,兩個主程序的不同之處在于我們所采用的紋理格式。變量的類型入關(guān)鍵字與CG有很大的不同,一定要按照OpenGL的定義來寫。
[cpp] view plaincopy// shader for luminance data | // shader for RGBA data
// and texture rectangles | // and texture2D
|
uniform samplerRect textureY; | uniform sampler2D textureY;
uniform samplerRect textureX; | uniform sampler2D textureX;
uniform float alpha; | uniform float alpha;
|
void main(void) { | void main(void) {
float y = textureRect( | vec4 y = texture2D(
textureY, | textureY,
gl_TexCoord[0].st).x; | gl_TexCoord[0].st);
float x = textureRect( | vec4 x = texture2D(
textureX, | textureX
gl_TexCoord[0].st).x; | gl_TexCoord[0].st);
gl_FragColor.x = | gl_FragColor =
y + alpha*x; | y + alpha*x;
} | }
下面代碼就是把所有對GLSL的初始化工作放在一個函數(shù)中實現(xiàn),GLSL API是被設(shè)計成可以模擬傳統(tǒng)的編譯及連接過程,更多的細(xì)節(jié),請參考橙皮書(Orange Book),或者查找一些GLSL的教程來學(xué)習(xí)一下,推薦到Lighthouse 3D’s GLSL tutorial 網(wǎng)站上看一下
?。踓pp] view plaincopyvoid initGLSL(void) {
// create program object
programObject = glCreateProgramObjectARB();
// create shader object (fragment shader) and attach to program
shaderObject = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);
glAttachObjectARB (programObject, shaderObject);
// set source to shader object
glShaderSourceARB(shaderObject, 1, &program_source, NULL);
// compile
glCompileShaderARB(shaderObject);
// link program object together
glLinkProgramARB(programObject);
// Get location of the texture samplers for future use
yParam = glGetUniformLocationARB(programObject, “textureY”);
xParam = glGetUniformLocationARB(programObject, “textureX”);
alphaParam = glGetUniformLocationARB(programObject, “alpha”);
}
Back to top
GPGPU 概念3:運算 = 繪圖
在這一章節(jié)里,我們來討論一下如何把本教程前面所學(xué)到的知識拼湊起來,以及如何使用這些知識來解決前面所提出的加權(quán)數(shù)組相加問題:y_new =y_old +alpha *x 。關(guān)于執(zhí)行運算的部份,我們把所有運算都放在performComputation()這個函數(shù)中實現(xiàn)。一共有四個步驟:首先是激活內(nèi)核,然后用著色函數(shù)來分配輸入輸出數(shù)組的空間,接著是通過渲染一個適當(dāng)?shù)膸缀螆D形來觸發(fā)GPU的運算,最后一步是簡單驗證一下我們前面所列出的所有的基本理論。
準(zhǔn)備好運算內(nèi)核
使用CG運行時函數(shù)來激活運算內(nèi)核就是顯卡著色程序。首先用enable函數(shù)來激活一個片段profile,然后把前面所寫的著色代碼傳送到顯卡上并綁定好。按規(guī)定,在同一時間內(nèi)只能有一個著色器是活動的,更準(zhǔn)確的說,是同一時間內(nèi),只能分別激活一個頂點著色程序和一個片段著色程序。由于本教程中采用了固定的頂點渲染管線,所以我們只關(guān)注片段著色就行了,只需要下面兩行代碼便可以了。
?。踓pp] view plaincopy// enable fragment profile
cgGLEnableProfile(fragmentProfile);
// bind saxpy program
cgGLBindProgram(fragmentProgram);
如果使用的是GLSL著色語言,這一步就更容易實現(xiàn)了,如果我們的著色代碼已以被成功地編譯連接,那么剩下我們所需要做的就只是把程序作為渲染管線的一部分安裝好,代碼如下:
glUseProgramObjectARB(programObject);
建立用于輸入的數(shù)組和紋理
在CG環(huán)境中,我們先要把紋理的標(biāo)識與對應(yīng)的一個uniform樣本值關(guān)聯(lián)起來,然后激活該樣本。這樣該紋理樣本便可以在CG中被直接使用了。
?。踓pp] view plaincopy// enable texture y_old (read-only)
cgGLSetTextureParameter(yParam, y_oldTexID);
cgGLEnableTextureParameter(yParam);
// enable texture x (read-only)
cgGLSetTextureParameter(xParam, xTexID);
cgGLEnableTextureParameter(xParam);
// enable scalar alpha
cgSetParameter1f(alphaParam, alpha);
但在GLSL中,我們必須把紋理與不同的紋理單元綁定在一起(在CG中,這部分由程序自動完成),然后把這些紋理單元傳遞給我們的uniform參數(shù)。
?。踓pp] view plaincopy// enable texture y_old (read-only)
glActiveTexture(GL_TEXTURE0);
glBindTexture(textureParameters.texTarget,yTexID[readTex]);
glUniform1iARB(yParam,0); // texunit 0
// enable texture x (read-only)
glActiveTexture(GL_TEXTURE1);
glBindTexture(textureParameters.texTarget,xTexID);
glUniform1iARB(xParam, 1); // texunit 1
// enable scalar alpha
glUniform1fARB(alphaParam,alpha);
建立用于輸出的紋理及數(shù)組
定義用于輸出的紋理,從本質(zhì)上講,這和把數(shù)據(jù)傳輸?shù)揭粋€FBO紋理上的操作是一樣的,我們只需要指定OpenGL函數(shù)參數(shù)的特定意義就可以了。這里我們只是簡單地改變輸出的方向,也就是,把目標(biāo)紋理與我們的FBO綁定在一起,然后使用標(biāo)準(zhǔn)的GL擴展函數(shù)來把該FBO指為渲染的輸出目標(biāo)。
[cpp] view plaincopy// attach target texture to first attachment point
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
GL_COLOR_ATTACHMENT0_EXT,
texture_target, y_newTexID, 0);
// set the texture as render target
glDrawBuffer (GL_COLOR_ATTACHMENT0_EXT);
準(zhǔn)備運算
讓們暫時先來回顧一下到目前為止,我們所做過了的工作:我們實現(xiàn)了目標(biāo)像素、紋理坐標(biāo)、要繪制的圖形三者元素一一對應(yīng)的關(guān)系。我們還寫好了一個片段著色器,用來讓每個片段渲染的時候都可以運行一次。現(xiàn)在剩下來還要做的工作就是:繪制一個“合適的幾何圖形” ,這個合適的幾何圖形,必須保證保存在目標(biāo)紋理中的數(shù)據(jù)每個元素就會去執(zhí)行一次我們的片段著色程序。換句話來說,我們必須保證紋理中的每個數(shù)據(jù)頂在片段著色中只會被訪一次。只要指定好我們的投影及視口的設(shè)置,其它的工作就非常容易:我們所需要的就只是一個剛好能覆蓋整個視口的填充四邊形。我們定義一個這樣的四邊形,并調(diào)用標(biāo)準(zhǔn)的OpenGL函數(shù)來對其進行渲染。這就意味著我們要直接指定四邊形四個角的頂點坐標(biāo),同樣地我們還要為每個頂點指定好正確的紋理坐標(biāo)。由于我們沒有對頂點著色進行編程,程序會把四個頂點通過固定的渲染管線傳輸?shù)狡聊豢臻g中去。光冊處理器(一個位于頂點著色與片段著色之間的固定圖形處理單元)會在四個頂點之間進行插值處理,生成新的頂點來把整個四邊形填滿。插值操作除了生成每個插值點的位置之外,還會自動計算出每個新頂點的紋理坐標(biāo)。它會為四邊形中每個像素生成一個片段。由于我們在寫片段著色器中綁定了相關(guān)的語義,因此插值后的片段會被自動發(fā)送到我們的片段著色程序中去進行處理。換句話說,我們渲染的這個簡單的四邊形,就可以看作是片段著色程序的數(shù)據(jù)流生成器。由于目標(biāo)像素、紋理坐標(biāo)、要繪制的圖形三者元素都是一一對應(yīng)的,從而我們便可以實現(xiàn):為數(shù)組每個輸出位置觸發(fā)一次片段著色程序的運行。也就是說通過渲染一個帶有紋理的四邊形,我們便可以觸發(fā)著色內(nèi)核的運算行,著色內(nèi)核會為紋理或數(shù)組中的每個數(shù)據(jù)項運行一次。
使用 texture rectangles 紋理坐標(biāo)是與像素坐標(biāo)相同的,我樣使用下面一小段代碼便可以實現(xiàn)了。
?。踓pp] view plaincopy// make quad filled to hit every pixel/texel
glPolygonMode(GL_FRONT,GL_FILL);
// and render quad
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex2f(0.0, 0.0);
glTexCoord2f(texSize, 0.0);
glVertex2f(texSize, 0.0);
glTexCoord2f(texSize, texSize);
glVertex2f(texSize, texSize);
glTexCoord2f(0.0, texSize);
glVertex2f(0.0, texSize);
glEnd();
如果使用 texture2D ,就必須單位化所有的紋理坐標(biāo),等價的代碼如下:
?。踓pp] view plaincopy// make quad filled to hit every pixel/texel
glPolygonMode(GL_FRONT,GL_FILL);
// and render quad
glBegin(GL_QUADS);
glTexCoord2f(0.0, 0.0);
glVertex2f(0.0, 0.0);
glTexCoord2f(1.0, 0.0);
glVertex2f(texSize, 0.0);
glTexCoord2f(1.0, 1.0);
glVertex2f(texSize, texSize);
glTexCoord2f(0.0, 1.0);
glVertex2f(0.0, texSize);
glEnd();
這里提示一下那些做高級應(yīng)用的程序員:在我們的著色程序中,只用到了一組紋理坐標(biāo),但是我們也可以為每個頂點定義多組不同的紋理坐標(biāo),相關(guān)的更多細(xì)節(jié),可以查看一下glMultiTexCoord()函數(shù)的使用。
Back to top
GPGPU 概念 4: 反饋
當(dāng)運算全部完成之后,的、得到的結(jié)果會被保存在目標(biāo)紋理y_new中。
多次渲染傳遞。
在一些通用運算中,我們會希望把前一次運算結(jié)果傳遞給下一個運算用來作為后繼運算的輸入變量。但是在GPU中,一個紋理不能同時被讀寫,這就意味著我們要創(chuàng)建另外一個渲染通道,并給它綁定不同的輸入輸出紋理,甚至要生成一個不同的運算內(nèi)核。有一種非常重要的技術(shù)可以用來解決這種多次渲染傳遞的問題,讓運算效率得到非常好的提高,這就是“乒乓”技術(shù)。
關(guān)于乒乓技術(shù)
乒乓技術(shù),是一個用來把渲染輸出轉(zhuǎn)換成為下一次運算的輸入的技術(shù)。在本文中(y_new =y_old +alpha*x) ,這就意味我們要切換兩個紋理的角色,y_new 和y_old 。有三種可能的方法來實現(xiàn)這種技術(shù)(看一下以下這篇論文Simon Green‘s FBO slides ,這是最經(jīng)典的資料了):
為每個將要被用作渲染輸出的紋理指定一個綁定點,并使用函數(shù)glBindFramebufferEXT()來為每個渲染通道綁定一個不同的FBO.
只使用一個FBO,但每次通道渲染的時候,使用函數(shù)glBindFramebufferEXT()來重新綁定渲染的目標(biāo)紋理。
使用一個FBO和多個綁定點,使用函數(shù)glDrawBuffer()來交換它們。
由于每個FBO最多有4個綁定點可以被使用,而且,最后一種方法的運算是最快的,我們在這里將詳細(xì)解釋一下,看看我們是如何在兩個不同的綁定點之間實現(xiàn)“乒乓” 的。
要實現(xiàn)這個,我們首先需要一組用于管理控制的變量。
[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
};
在運算其間,我們只需要做的就是給內(nèi)核傳遞正確的參數(shù)值,并且每次運算都要交換一次組組的索引值:
[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
把所有東西放在一起
對本文附帶源代碼的一個簡要說明
在附帶的代碼例子中,使用到了本文所有闡述過的所有概念,主要實現(xiàn)了以下幾個運算:
為每個數(shù)組生成一個浮點的紋理。
把初始化的數(shù)據(jù)傳輸?shù)郊y理中去 。
使用CG或者GLSL來生成一個片段著色器。
一個多次重復(fù)運算的模塊,主要是用來演試“乒乓”技術(shù)。
把最終的運算結(jié)果返回到主內(nèi)存中。
把結(jié)果與CPU的參考結(jié)果進行比較。
執(zhí)行過行中的可變化部份
在代碼中,我們使用了一系列的結(jié)構(gòu)體來保存各種可能的參數(shù),主要是為了方便OpenGL的調(diào)用,例如:不同類型的浮點紋理擴展,不同的紋理格式,不同的著色器之間的細(xì)微差別,等等。下面這段代碼就是這樣一個結(jié)構(gòu)體的示例,采用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; }”;
為了給不同的情況取得一個合適的工作版本,我們只須要查找和替換就可以了?;蛘呤褂玫诙€命令行參數(shù)如:rect_nv_r_32。在應(yīng)用程序中,一個全局變量textureParameters 指向我們實現(xiàn)要使用的結(jié)構(gòu)體。
命令行參數(shù)
在程序中,使用命令行參數(shù)來對程序進行配置。如果你運行該程序而沒帶任何參數(shù)的話,程序會輸出一個對各種不同參數(shù)的解釋。提醒大家注意的是:本程序?qū)γ钚袇?shù)的解釋是不穩(wěn)定的,一個不正確的參數(shù)有可能會造成程序的崩潰。因此我強烈建義大家使用輸出級的參數(shù)來顯示運算的結(jié)果,這樣可以降低出現(xiàn)問題的可能性,尤其是當(dāng)你不相信某些運算錯誤的時候。請查看包含在示例中的批處理文件。
測試模式
本程序可以用來對一個給定的GPU及其驅(qū)動的 結(jié)合進行測試,主要是測試一下,看看哪種內(nèi)部格式及紋理排列是可以在FBO擴展中被組合在一起使用的。示例中有一個批處理文件叫做:run_test_*.bat,是使用各種不同的命令行參數(shù)來運行程序,并會生成一個報告文件。如果是在LINUX下,這個文件也可能當(dāng)作一個shell腳本來使用,只需要稍作修改就可以了。這ZIP文檔中包含有對一些顯卡測試后的結(jié)果。
基準(zhǔn)模式
這種模式被寫進程序中,完全是為了好玩。它可以對不同的問題產(chǎn)成一個運算時序,并在屏幕上生成MFLOP/s速率圖,和其它的一些性能測試軟件一樣。它并不代表GPU運算能力的最高值,只是接近最高值的一種基準(zhǔn)性能測試。想知道如何運行它的話,請查看命令行參數(shù)。
Back to top
附言
簡單對比一下Windows 和 Linux,NVIDIA 和 ATI 之間的差別
對于NVIDIA的顯卡,不管是Windows還是Linux,它們都提供了相同的函數(shù)來實現(xiàn)本教程中的例子。但如果是ATI的顯卡,它對LINUX的支持就不是很好。因此如果是ATI顯卡,目前還是建義在Windows下使用。
看一看這片相關(guān)的文章 table summarizing renderable texture formats on various hardware.
本文中提供下載的源代碼,是在NV4X以上的顯卡上編譯通過的。對于ATI的用戶,則要作以下的修改才行:在transferToTexture() 函數(shù)中,把NVIDIA相應(yīng)部份的代碼注釋掉,然使用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顯卡,當(dāng)我們把數(shù)據(jù)傳送到紋理中去時,如果使用glTexSubImage2D(),會產(chǎn)生一個非常奇怪的問題:就是原本是RGBA排列的數(shù)據(jù),會被改變?yōu)锽GRA格式。這是一個已得到確認(rèn)的BUG,希望在以后的版本中能得到修正,目前只能用glDrawPixels() 來代替。
而對于NV3X系列顯卡,如果想用glDrawPixels() ,則要求一定要在GPU中綁定一個著色程序。因此這里用glTexSubImage()函數(shù)代替(其實對于所有的NVIDIA 的顯卡,都推薦使用該函數(shù))。
ATI顯卡,在GLSL中不支持rectangles紋理采樣,甚至這樣的著色代碼沒法被編譯通過。samplerRect 或sampler2DRect 被指定為保留的關(guān)鍵字,ARB_texture_rextangle的擴展說明書中得到定義,但驅(qū)動沒有實現(xiàn)對它們的支持。可以用CG來代替。
在ATI中,當(dāng)我們使用glDrawPixels() 下載一個紋理的時候,如果紋理是被enable的,則會導(dǎo)致下載失敗,這不是一個BUG,但是也是一個有爭議性的問題,因為這樣會使程序難以調(diào)試。
對于NVIDIA的顯卡,我們不能把紋理渲染到紋理最大值的最后一行中去。也就是說,盡管我們用函數(shù)glGetIntegerv(GL_MAX_TEXTURE_SIZE,&maxtexsize); 得到的值是4096,但是你也只能渲染一張4095 x 4095 紋理。這是一個已知的BUG,同樣也希望以后能得到修正。
檢查OpenGL的錯誤
高度推薦大家在代碼中經(jīng)常使用以下函數(shù)來檢測OpenGL運行過程中產(chǎn)生的錯誤。
[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函數(shù)。這里只列出了它的一些常見的反回值作參考,要詳細(xì)解釋這些返回信息,請查看規(guī)格說明書的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中檢查錯誤有一些細(xì)微的不同,一個自寫入的錯誤處理句柄被傳遞給CG的錯誤處理回調(diào)函數(shù)。
?。踓pp] 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的錯誤
使用以下的函數(shù)來查看編譯的結(jié)果:
?。踓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);
}
}
大多數(shù)情況下,你可以使用以上查詢函數(shù),詳細(xì)內(nèi)容可以查看一下GLSL的規(guī)格說明書。還有另一個非常重要的查詢函數(shù),是用來檢查程序是否可以被連接:
?。踓pp] view plaincopyGLint success;
glGetObjectParameterivARB(programObject,
GL_OBJECT_LINK_STATUS_ARB,
&success);
if (!success) {
printf(“Shader could not be linked! ”);
}
評論