圖形處理器(英語:Graphics Processing Unit,縮寫:GPU),又稱顯示核心、視覺處理器、顯示芯片,是一種專門在個人電腦、工作站、游戲機和一些移動設備(如平板電腦、智能手機等)上圖像運算工作的微處理器。
圖形處置單元(或簡稱GPU)會賣力處置從PC外部傳送到所銜接表現器的一切內容,不管你在玩游戲、編纂視頻或只是盯著桌面的壁紙,一切表現器中表現的圖象都是由GPU停止襯著的。本文體系極客將向人人先容甚么是GPU、它是若何事情的,和為何要為游戲和圖象密集型應用程序設置裝備擺設公用顯卡。
對通俗用戶來講,現實上不消要自力顯卡就能夠向表現器「供給」內容。像筆記本電腦或平板用戶,平日CPU芯片都邑集成GPU內核,也便是人人熟稱的「核顯」,如許便可認為對表現請求不高的低功耗裝備供給更好的性價比。
正因如斯,部門筆記本電腦、平板電腦和某些PC用戶來講,要想將其圖形處置器進級到更高級別也很艱苦,乃至不太能夠。這就會招致游戲(和視頻編纂等)機能欠安,只能將圖形品質設置低落能力事情。對此類用戶而言,只要在主板支撐和余暇空間充足的情況下,增加新顯卡才能夠或許把(游戲)表現體驗進步到一個新的程度。
GPU發展和現狀
1. GPU原來就是為了加速3D渲染的,后來被拿過來做計算。
2. 現在GPU可以支持通用的指令,可以用傳統的C和C++,還有Fortran來編程。
3. 現在單個高端GPU的性能已經達到了傳統多核CPU集群的性能
4. 有的應用通過GPU加速相比傳統的多核CPU來說可以達到100X的加速。對某些特定應用來說GPU還是更適合的。
GPU編程模型
1. 在GPU中,工作的分配是通過在調度空間上并行地應用或者映射一個函數(或者叫做kernel)。舉例來說,一個矩陣中的每一個點就是調度空間。
2. kernel就是描述在一個線程在調度空間中的每一個點要完成的工作。在調度空間中,每一個點都要啟動一個線程。
3. 由于GPU是在單個PCI-e卡上的協處理器,數據必須通過顯式地從系統內存拷貝到GPU板上內存。
4. GPU是以SIMD的多個group的形式組織的。在每一個SIMD的group(或者叫warp,在NIVIDA CUDA編程中為32個線程)中,所有的線程在lockstep中執行相同的指令。這樣的在lockstep中執行相同指令的多個線程就叫做warp,雖然分支是被允許的,但是如果同一個warp中的線程出現不同的執行路徑,會帶來一些性能開銷。
4. 對于memory-bound的應用來說,可能的話,同一個warp中的所有線程應當訪問相鄰的數據元素,同一個warp中相鄰的線程應當訪問相鄰的數據元素。這可能要對數據布局和數據訪問模式進行重新安排。
5. GPU有多個內存空間可用于開發數據訪問模式。除了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的產業標準,還有另一個是為了C/C++ Fortran的CUDA,在NVIDIA的GPU上編程。
7. OpenCL/CUDA編譯器并不是把C代碼轉換成CUDA代碼,編程人員最主要的工作還是選擇算法和數據結構。例如在GPU上,基數排序和歸并排序要比堆排序和快速排序好。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.
什么應用適合GPU
1. 內核中有豪多并行線程的應用
2. 對于線程間的數據交換都發生在kernel調度空間中的相鄰線程之間的應用,因為這樣就可以用到per-block shared memory.
3. 數據并行的應用,多個線程做相似工作,循環是數據并行的主要來源。
4. 那些能得到很好的天然硬件支持的應用,如倒數和反平方根,不過在編程中要打開“fastmath”選項,確保使用硬件支持功能。
5. 需要對每個數據元素做大量的計算,或者能夠充分利用寬內存接口(wide memory interface這里有疑問)
6. 做同步操作較少的應用。
什么應用不適合GPU
1. 并行度小的應用,如需要的線程數小于100個,那么使用GPU加速效果不明顯
2. 不規則的任務并行---盡管應用需要很多線程,但是這些線程都做不同的工作,那么GPU不能得到有效的利用。不過這也依賴于具體工作,多久對線程調度一次,加速的可能仍然存在。
3. 頻繁的全局同步,這要求全局的barrier,帶來很大性能開銷。
4. 在線程之間,會出現隨機的點對點同步的應用。GPU對這個的支持不好,通常需要在每次同步的時候做一個全局barrier,如果要利用GPU,最好重構算法避免出現這個問題。
5. 要求計算量(相比于數據傳輸量)少的應用。盡管在CPU+GPU計算結構中,GPU可以帶來計算性能的提升,但是這些提升都被向GPU傳輸數據所消耗的實踐覆蓋了。舉個例子,對于兩個向量求和運算,如果非常大的向量的話,一般都選擇在CPU上算,否則傳輸到GPU上的時間開銷很大。
硬件需求。
你需要有NVIDIA GeForce FX 或者 ATI RADEON 9500 以上的顯卡, 一些老的顯卡可能不支持我們所需要的功能(主要是單精度浮點數據的存取及運算) 。
軟件需求
首先,你需要一個C/C++編譯器。你有很多可以選擇,如:Visual Studio .NET 2003, Eclipse 3.1 plus CDT/MinGW, the Intel C++ Compiler 9.0 及 GCC 3.4+等等。然后更新你的顯卡驅動讓它可以支持一些最新特性。
本文所附帶的源代碼,用到了兩個擴展庫,GLUT 和 GLEW 。對于windows系統,GLUT可以在 這里下載到,而Linux 的freeglut和freeglut-devel大多的版本都集成了。GLEW可以在SourceForge 上下載到,對于著色語言,大家可以選擇GLSL或者CG,GLSL在你安裝驅動的時候便一起裝好了。如果你想用CG,那就得下載Cg Toolkit 。
二者擇其一
大家如果要找DirectX版本的例子的話,請看一下Jens Krügers的《 Implicit Water Surface》 demo(該例子好像也有OpenGL 版本的)。當然,這只是一個獲得高度評價的示例源代碼,而不是教程的。
有一些從圖形著色編程完全抽象出來的GPU的元程序語言,把底層著色語言作了封裝,讓你不用學習著色語言,便能使用顯卡的高級特性,其中BrookGPU 和Sh 就是比較出名的兩個項目。
Back to top
初始化OpenGL
GLUT
GLUT(OpenGLUtility Toolkit)該開發包主要是提供了一組窗口函數,可以用來處理窗口事件,生成簡單的菜單。我們使用它可以用盡可能少的代碼來快速生成一個OpenGL 開發環境,另外呢,該開發包具有很好的平***立性,可以在當前所有主流的操作系統上運行 (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內核的一部份。因此,OpenGL Extensions通過對OpenGL API的擴展, 為我們提供了一種可以訪問及使用硬件高級特性的機制。OpenGL擴展的特點:不是每一種顯卡都支持該擴展,即便是該顯卡在硬件上支持該擴展,但不同版本的顯卡驅動,也會對該擴展的運算能力造成影響,因為OpenGL擴展設計出來的目的,就是為了最大限度地挖掘顯卡運算的能力,提供給那些在該方面有特別需求的程序員來使用。在實際編程的過程中,我們必須小心檢測當前系統是否支持該擴展,如果不支持的話,應該及時把錯誤信息返回給軟件進行處理。當然,為了降低問題的復雜性,本教程的代碼跳過了這些檢測步驟。
OpenGL Extension Registry OpenGL擴展注冊列表中,列出了幾乎所有的OpenGL可用擴展,有需要的朋友可能的查看一下。
當我們要在程序中使用某些高級擴展功能的時候,我們必須在程序中正確引入這些擴展的擴展函數名。有一些小工具可以用來幫助我們檢測一下某個給出的擴展函數是否被當前的硬件及驅動所支持,如:glewinfo, OpenGL extension viewer等等,甚至OpenGL本身就可以(在上面的連接中,就有一個相關的例子)。
如何獲取這些擴展函數的入口指針,是一個比較高級的問題。下面這個例子,我們使用GLEW來作為擴展載入函數庫,該函數庫把許多復雜的問題進行了底層的封裝,給我們使用高級擴展提供了一組簡潔方便的訪問函數。
[cpp] 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離屏渲染的準備工作
在傳統的GPU渲染流水線中,每次渲染運算的最終結束點就是幀緩沖區。所謂幀緩沖區,其實是顯卡內存中的一塊,它特別這處在于,保存在該內存區塊中的圖像數據,會實時地在顯示器上顯示出來。根據顯示器設置的不同,幀緩沖區最大可以取得32位的顏色深度,也就是說紅、綠、藍、alpha四個顏色通道共享這32位的數據,每個通道占8位。當然用32位來記錄顏色,如果加起來的話,可以表示160萬種不同的顏色,這對于顯示器來說可能是足夠了,但是如果我們要在浮點數字下工作,用8位來記錄一個浮點數,其數學精度是遠遠不夠的。另外還有一個問題就是,幀緩存中的數據最大最小值會被限定在一個范圍內,也就是 [0/255; 255/255]
如何解決以上的一些問題呢?一種比較苯拙的做法就是用有符號指數記數法,把一個標準的IEEE 32位浮點數映射保存到8位的數據中。不過幸運的是,我們不需要這樣做。首先,通過使用一些OpenGL的擴展函數,我們可以給GPU提供32位精度的浮點數。另外有一個叫EXT_framebuffer_object 的OpenGL的擴展, 該擴展允許我們把一個離屏緩沖區作為我們渲染運算的目標,這個離屏緩沖區中的RGBA四個通道,每個都是32位浮點的,這樣一來, 要想GPU上實現四分量的向量運算就比較方便了,而且得到的是一個全精度的浮點數,同時也消除了限定數值范圍的問題。我們通常把這一技術叫FBO,也就是Frame Buffer Object的縮寫。
要使用該擴展,或者說要把傳統的幀緩沖區關閉,使用一個離屏緩沖區作我們的渲染運算區,只要以下很少的幾行代碼便可以實現了。有一點值得注意的是:當我用使用數字0,來綁定一個FBO的時候,無論何時,它都會還原window系統的特殊幀緩沖區,這一特性在一些高級應用中會很有用,但不是本教程的范圍,有興趣的朋友可能自已研究一下。
[cpp] 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: 數組 = 紋理
一維數組是本地CPU最基本的數據排列方式,多維的數組則是通過對一個很大的一維數組的基準入口進行坐標偏移來訪問的(至少目前大多數的編譯器都是這樣做的)。一個小例子可以很好說明這一點,那就是一個MxN維的數組 a[i][j] = a[i*M+j];我們可能把一個多維數組,映射到一個一維數組中去。這些數組我開始索引都被假定為0;
而對于GPU,最基本的數據排列方式,是二維數組。一維和三維的數組也是被支持的,但本教程的技術不能直接使用。數組在GPU內存中我們把它叫做紋理或者是紋理樣本。紋理的最大尺寸在GPU中是有限定的。每個維度的允許最大值,通過以下一小段代碼便可能查詢得到,這些代碼能正確運行,前提是OpenGL的渲染上下文必須被正確初始化。
[cpp] view plaincopyint maxtexsize;
glGetIntegerv(GL_MAX_TEXTURE_SIZE,&maxtexsize);
printf(“GL_MAX_TEXTURE_SIZE, %d ”,maxtexsize);
就目前主流的顯卡來說,這個值一般是2048或者4096每個維度,值得提醒大家的就是:一塊顯卡,雖然理論上講它可以支持4096*4096*4096的三維浮點紋理,但實際中受到顯卡內存大小的限制,一般來說,它達不到這個數字。
在CPU中,我們常會討論到數組的索引,而在GPU中,我們需要的是紋理坐標,有了紋理坐標才可以訪問紋理中每個數據的值。而要得到紋理坐標,我們又必須先得到紋理中心的地址。
傳統上講,GPU是可以四個分量的數據同時運算的,這四個分量也就是指紅、綠、藍、alpha(RGBA)四個顏色通道。稍后的章節中,我將會介紹如何使用顯卡這一并行運算的特性,來實現我們想要的硬件加速運算。
在CPU上生成數組
讓我們來回顧一下前面所要實現的運算:也就是給定兩個長度為N的數組,現在要求兩數組的加權和y=y+alpha*x,我們現在需要兩個數組來保存每個浮點數的值,及一個記錄alpha值的浮點數。
[cpp] view plaincopyfloat* dataY = (float*)malloc(N*sizeof(float)); float* dataX = (float*)malloc(N*sizeof(float)); float alpha;
雖然我們的實際運算是在GPU上運行,但我們仍然要在CPU上分配這些數組空間,并對數組中的每個元素進行初始化賦值。
在GPU上生成浮點紋理
這個話題需要比較多的解釋才行,讓我們首先回憶一下在CPU上是如何實現的,其實簡單點來說,我們就是要在GPU上建立兩個浮點數組,我們將使用浮點紋理來保存數據。
有許多因素的影響,從而使問題變得復雜起來。其中一個重要的因素就是,我們有許多不同的紋理對像可供我們選擇。即使我們排除掉一些非本地的目標,以及限定只能使用2維的紋理對像。我們依然還有兩個選擇,GL_TEXTURE_2D是傳統的OpenGL二維紋理對像,而ARB_texture_rectangle則是一個OpenGL擴展,這個擴展就是用來提供所謂的texture rectangles的。對于那些沒有圖形學背景的程序員來說,選擇后者可能會比較容易上手。texture2Ds 和 texture rectangles 在概念上有兩大不同之處。我們可以從下面這個列表來對比一下,稍后我還會列舉一些例子。
另外一個重要的影響因素就是紋理格式,我們必須謹慎選擇。在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_float andARB_texture_float )中也分別有其對應的名稱:GL_LUMINANCE_FLOAT32_ATI,GL_RGBA_FLOAT32_ATI 和 GL_LUMINANCE32F_ARB,GL_RGBA32F_ARB 。在我看來,他們名稱不同,但作用都是一樣的,我想應該是多個不同的參數名稱對應著一個相同的參數標識。至于選擇哪一個參數名,這只是看個人的喜好,因為它們全部都既支持NV顯卡也支持ATI的顯卡。
最后還有一個要解決的問題就是,我們如何把CPU中的數組元素與GPU中的紋理元素一一對應起來。這里,我們采用一個比較容易想到的方法:如果紋理是LUMINANCE格式,我們就把長度為N的數組,映射到一張大小為sqrt(N) x sqrt(N)和紋理中去(這里規定N是剛好能被開方的)。如果采用RGBA的紋理格式,那么N個長度的數組,對應的紋理大小就是sqrt(N/4) x sqrt(N/4),舉例說吧,如果N=1024^2,那么紋理的大小就是512*512 。
以下的表格總結了我們上面所討論的問題,作了一下分類,對應的GPU分別是: NVIDIA GeForce FX (NV3x), GeForce 6 and 7 (NV4x, G7x) 和 ATI.
(*) Warning: 這些格式作為紋理是被支持的,但是如果作為渲染對像,就不一定全部都能夠得到良好的支持(seebelow)。
講完上面的一大堆基礎理論這后,是時候回來看看代碼是如何實現的。比較幸運的是,當我們弄清楚了要用那些紋理對像、紋理格式、及內部格式之后,要生成一個紋理是很容易的。
[cpp] 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函數,我來逐一介紹一下它每個參數:第一個參數是紋理對像,上面已經說過了;第二個參數是0,是告訴GL不要使用多重映像紋理。接下來是內部格式及紋理大小,上面也說過了,應該清楚了吧。第六個參數是也是0,這是用來關閉紋理邊界的,這里不需要邊界。接下來是指定紋理格式,選擇一種你想要的格式就可以了。對于參數GL_FLOAT,我們不要被它表面的意思迷惑,它并不會影響我們所保存在紋理中的浮點數的精度。其實它只與CPU方面有關系,目的就是要告訴GL稍后將要傳遞過去的數據是浮點型的。最后一個參數還是0,意思是生成一個紋理,但現在不給它指定任何數據,也就是空的紋理。該函數的調用必須按上面所說的來做,才能正確地生成一個合適的紋理。上面這段代碼,和CPU里分配內存空間的函數malloc(),功能上是很相像的,我們可能用來對比一下。
最后還有一點要提醒注意的:要選擇一個適當的數據排列映射方式。這里指的就是紋理格式、紋理大小要與你的CPU數據相匹配,這是一個非常因地制宜的問題,根據解決的問題不同,其相應的處理問題方式也不同。從經驗上看,一些情況下,定義這樣一個映射方式是很容易的,但某些情況下,卻要花費你大量的時間,一個不理想的映射方式,甚至會嚴重影響你的系統運行。
數組索引與紋理坐標的一一對應關系
在后面的章節中,我們會講到如何通過一個渲染操作,來更新我們保存在紋理中的那些數據。在我們對紋理進行運算或存取的時候,為了能夠正確地控制每一個數據元素,我們得選擇一個比較特殊的投影方式,把3D世界映射到2D屏幕上(從世界坐標空間到屏幕設備坐標空間),另外屏幕像素與紋理元素也要一一對應。這種關系要成功,關鍵是要采用正交投影及合適的視口。這樣便能做到幾何坐標(用于渲染)、紋理坐標(用作數據輸入)、像素坐標(用作數據輸出)三者一一對應。有一個要提醒大家的地方:如果使用texture2D,我們則須要對紋理坐標進行適當比例的縮放,讓坐標的值在0到1之間,前面有相關的說明。
為了建立一個一一對應的映射,我們把世界坐標中的Z坐標設為0,把下面這段代碼加入到initFBO()這個函數中
[cpp] 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);
使用紋理作為渲染對像
其實一個紋理,它不僅可以用來作數據輸入對像,也還可以用作數據輸出對像。這也是提高GPU運算效率和關鍵所在。通過使用 framebuffer_object這個擴展,我們可以把數據直接渲染輸出到一個紋理上。但是有一個缺點:一個紋理對像不能同時被讀寫,也就是說,一個紋理,要么是只讀的,要么就是只寫的。顯卡設計的人提供這樣一個解釋:GPU在同一時間段內會把渲染任務分派到幾個通道并行運行, 它們之間都是相互獨立的(稍后的章節會對這個問題作詳細的討論)。如果我們允許對一個紋理同時進行讀寫操作的話,那我們需要一個相當復雜的邏輯算法來解決讀寫沖突的問題, 即使在芯片邏輯上可以做到,但是對于GPU這種沒有數據安全性約束的處理單元來說,也是沒辦法把它實現的,因為GPU并不是基von Neumann的指令流結構,而是基于數據流的結構。因此在我們的程序中,我們要用到3個紋理,兩個只讀紋理分別用來保存輸入數組x,y。一個只寫紋理用來保存運算結果。用這種方法意味著要把先前的運算公式:y = y + alpha * x 改寫為:y_new = y_old + alpha * x.
FBO 擴展提供了一個簡單的函數來實現把數據渲染到紋理。為了能夠使用一個紋理作為渲染對像,我們必須先把這個紋理與FBO綁定,這里假設離屏幀緩沖已經被指定好了。
[cpp] view plaincopyglFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, texture_target, texID, 0);
第一個參數的意思是很明顯的。第二個參數是定義一個綁定點(每個FBO最大可以支持四個不同的綁定點,當然,不同的顯卡對這個最大綁定數的支持不一樣,可以用GL_MAX_COLOR_ATTACHMENTS_EXT來查詢一下)。第三和第四個參數應該清楚了吧,它們是實際紋理的標識。最后一個參數指的是使用多重映像紋理,這里沒有用到,因此設為0。
為了能成功綁定一紋理,在這之前必須先用glTexImage2D()來對它定義和分配空間。但不須要包含任何數據。我們可以把FBO想像為一個數據結構的指針,為了能夠對一個指定的紋理直接進行渲染操作,我們須要做的就調用OpenGL來給這些指針賦以特定的含義。
不幸的是,在FBO的規格中,只有GL_RGB和GL_RGBA兩種格式的紋理是可以被綁定為渲染對像的(后來更新這方面得到了改進),LUMINANCE這種格式的綁定有希望在后繼的擴展中被正式定義使用。在我定本教程的時候,NVIDIA的硬件及驅動已經對這個全面支持,但是只能結會對應的列舉參數NV_float_buffer一起來使用才行。換句話說,紋理中的浮點數的格式與渲染對像中的浮點數格式有著本質上的區別。
下面這個表格對目前不同的顯卡平臺總結了一下,指的是有哪些紋理格式及紋理對像是可能用來作為渲染對像的,(可能還會有更多被支持的格式,這里只關心是浮點數的紋理格式):
列表中最后一行所列出來的格式在目前來說,不能被所有的GPU移植使用。如果你想采用LUMINANCE格式,你必須使用ractangles紋理,并且只能在NVIDIA的顯卡上運行。想要寫出兼容NVIDIA及ATI兩大類顯卡的代是可能的,但只支持NV4x以上。幸運的是要修改的代碼比較少,只在一個switch開關,便能實現代碼的可移植性了。相信隨著ARB新版本擴展的發布,各平臺之間的兼容性將會得到進一步的提高,到時候各種不同的格式也可能相互調用了。
把數據從CPU的數組傳輸到GPU的紋理
為了把數據傳輸到紋理中去,我們必須綁定一個紋理作為紋理目標,并通過一個GL函數來發送要傳輸的數據。實際上就是把數據的首地址作為一個參數傳遞給該涵數,并指定適當的紋理大小就可以了。如果用LUMINANCE格式,則意味著數組中必須有texSize x texSize個元數。而RGBA格式,則是這個數字的4倍。注意的是,在把數據從內存傳到顯卡的過程中,是全完不需要人為來干預的,由驅動來自動完成。一但傳輸完成了,我們便可能對CPU上的數據作任意修改,這不會影響到顯卡中的紋理數據。 而且我們下次再訪問該紋理的時候,它依然是可用的。在NVIDIA的顯卡中,以下的代碼是得到硬件加速的。
[cpp] view plaincopyglBindTexture(texture_target, texID);
glTexSubImage2D(texture_target,0,0,0,texSize,texSize,
texture_format,GL_FLOAT,data);
這里三個值是0的參數,是用來定義多重映像紋理的,由于我們這里要求一次把整個數組傳輸一個紋理中,不會用到多重映像紋理,因此把它們都關閉掉。
以上是NVIDIA顯卡的實現方法,但對于ATI的顯卡,以下的代碼作為首選的技術。在ATI顯卡中,要想把數據傳送到一個已和FBO綁定的紋理中的話,只需要把OpenGL的渲染目標改為該綁定的FBO對像就可以了。
glDrawBuffer(GL_COLOR_ATTACHMENT0_EXT);glRasterPos2i(0,0);glDrawPixels(texSize,texSize,texture_format,GL_FLOAT,data);
第一個函數是改變輸出的方向,第二個函數中我們使用了起點作為參與點,因為我們在第三個函數中要把整個數據塊都傳到紋理中去。
兩種情況下,CPU中的數據都是以行排列的方式映射到紋理中去的。更詳細地說,就是:對于RGBA格式,數組中的前四個數據,被傳送到紋理的第一個元素的四個分量中,分別與R,G,B,A分量一一對應,其它類推。而對于LUMINANCE 格式的紋理,紋理中第一行的第一個元素,就對應數組中的第一個數據。其它紋理元素,也是與數組中的數據一一對應的。
把數據從GPU紋理,傳輸到CPU的數組
這是一個反方向的操作,那就是把數據從GPU傳輸回來,存放在CPU的數組上。同樣,有兩種不同的方法可供我們選擇。傳統上,我們是使用OpenGL獲取紋理的方法,也就是綁定一個紋理目標,然后調用glGetTexImage()這個函數。這些函數的參數,我們在前面都有見過。
glBindTexture(texture_target,texID);glGetTexImage(texture_target,0,texture_format,GL_FLOAT,data);
但是這個我們將要讀取的紋理,已經和一個FBO對像綁定的話,我們可以采用改變渲染指針方向的技術來實現。
glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);glReadPixels(0,0,texSize,texSize,texture_format,GL_FLOAT,data);
由于我們要讀取GPU的整個紋理,因此這里前面兩個參數是0,0。表示從0起始點開始讀取。該方法是被推薦使用的。
一個忠告:比起在GPU內部的傳輸來說,數據在主機內存與GPU內存之間相互傳輸,其花費的時間是巨大的,因此要謹慎使用。由其是從CPU到GPU的逆向傳輸。
在前面“ 當前顯卡設備運行的問題” 中 提及到該方面的問題。
一個簡單的例子
[cpp] view plaincopy#include 《stdio.h》
#include 《stdlib.h》
#include 《GL/glew.h》
#include 《GL/glut.h》
int main(int argc, char **argv) {
// 這里聲明紋理的大小為:teSize;而數組的大小就必須是texSize*texSize*4
int texSize = 2;
int i;
// 生成測試數組的數據
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的環境
glutInit (&argc, argv);
glutCreateWindow(“TEST1”);
glewInit();
// 視口的比例是 1:1 pixel=texel=data 使得三者一一對應
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);
// 生成兩個紋理,一個是用來保存數據的紋理,一個是用作渲染對像的紋理
GLuint tex,fboTex;
glGenTextures (1, &tex);
glGenTextures (1, &fboTex);
glBindTexture(GL_TEXTURE_RECTANGLE_ARB,fboTex);
// 設定紋理參數
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);
// 分配數據紋理的顯存空間
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);
//把當前的FBO對像,與FBO紋理綁定在一起
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
GL_COLOR_ATTACHMENT0_EXT,
GL_TEXTURE_RECTANGLE_ARB,fboTex,0);
// 把本地數據傳輸到顯卡的紋理上。
glBindTexture(GL_TEXTURE_RECTANGLE_ARB,tex);
glTexSubImage2D(GL_TEXTURE_RECTANGLE_ARB,0,0,0,texSize,texSize,
GL_RGBA,GL_FLOAT,data);
//--------------------begin-------------------------
//以下代碼是渲染一個大小為texSize * texSize矩形,
//其作用就是把紋理中的數據,經過處理后,保存到幀緩沖中去,
//由于用到了離屏渲染,這里的幀緩沖區指的就是FBO紋理。
//在這里,只是簡單地把數據從紋理直接傳送到幀緩沖中,
//沒有對這些流過GPU的數據作任何處理,但是如果我們會用CG、
//GLSL等高級著色語言,對顯卡進行編程,便可以在GPU中
//截獲這些數據,并對它們進行任何我們所想要的復雜運算。
//這就是GPGPU技術的精髓所在。問題討論: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------------------------
// 從幀緩沖中讀取數據,并把數據保存到result數組中。
glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
glReadPixels(0, 0, texSize, texSize,GL_RGBA,GL_FLOAT,result);
// 顯示最終的結果
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]);
// 釋放本地內存
free(data);
free(result);
// 釋放顯卡內存
glDeleteFramebuffersEXT (1,&fb);
glDeleteTextures (1,&tex);
glDeleteTextures(1,&fboTex);
return 0;
}
現在是時候讓我們回頭來看一下前面要解決的問題,我強烈建議在開始一個新的更高級的話題之前,讓我們先弄一個顯淺的例子來實踐一下。下面通過一個小的程序,嘗試著使用各種不同的紋理格式,紋理對像以及內部格式,來把數據發送到GPU,然后再把數據從GPU取回來,保存在CPU的另一個數組中。在這里,兩個過程都沒有對數據作任何運算修該,目的只是看一下數據GPU和CPU之間相互傳輸,所需要使用到的技術及要注意的細節。也就是把前面提及到的幾個有迷惑性的問題放在同一個程序中來運行一下。在稍后的章節中將會詳細討論如何來解決這些可能會出現的問題。
由于趕著要完成整個教程,這里就只寫了一個最為簡單的小程序,采用rectangle紋理、ARB_texture_float作紋理對像并且只能在NVIDIA的顯卡上運行。
你可以在這里下載到為ATI顯卡寫的另一個版本。
以上代碼是理解GPU編程的基礎,如果你完全看得懂,并且能對這代碼作簡單的修改運用的話,那恭喜你,你已經向成功邁進了一大步,并可以繼續往下看,走向更深入的學習了。但如看不懂,那回頭再看一編吧。
Back to top
GPGPU 概念 2:內核(Kernels) = 著色器(shaders)
在這一章節中,我們來討論GPU和CPU兩大運算模塊最基本的區別,以及理清一些算法和思想。一但我們弄清楚了GPU是如何進行數據并行運算的,那我們要編寫一個自已的著色程序,還是比較容易的。
面向循環的CPU運算 vs. 面向內核的GPU數據并行運算
讓我們來回憶一下我們所想要解決的問題:y = y + alpha* x; 在CPU上,通常我們會使用一個循環來遍歷數組中的每個元素。如下:
[cpp] view plaincopyfor (int i=0; i《N; i++)
dataY[i] = dataY[i] + alpha * dataX[i];
每一次的循環,都會有兩個層次的運算在同時運作:在循環這外,有一個循環計數器在不斷遞增,并與我們的數組的長度值作比較。而在循環的內部,我們利用循環計數器來確定數組的一個固定位置,并對數組該位置的數據進行訪問,在分別得到兩個數組該位置的值之后,我們便可以實現我們所想要的運算:兩個數組的每個元素相加了。這個運算有一個非常重要的特點:那就是我們所要訪問和計算的每個數組元數,它們之間是相互獨立的。這句話的意思是:不管是輸入的數組,還是輸出結果的數組,對于同一個數組內的各個元素是都是相互獨立的,我們可以不按順序從第一個算到最后一個,可先算最后一個,再算第一個,或在中間任意位置選一個先算,它得到的最終結果是不變的。如果我們有一個數組運算器,或者我們有N個CPU的話,我們便可以同一時間把整個數組給算出來,這樣就根本不需要一個外部的循環。我們把這樣的示例叫做SIMD(single instruction multiple data)。現在有一種技術叫做“partial loop unrolling”就是讓允許編譯器對代碼進行優化,讓程序在一些支持最新特性(如:SSE , SSE2)的CPU上能得到更高效的并行運行。
在我們這個例子中,輸入數數組的索引與輸出數組的索引是一樣,更準確地說,是所有輸入數組下標,都與輸出數組的下標是相同的,另外,在對于兩個數組,也沒有下標的錯位訪問或一對多的訪問現像,如:y[i] = -x[i-1] + 2*x[[i] - x[i+1] 。這個公式可以用一句不太專業的語言來描術:“組數Y中每個元素的值等于數組X中對應下標元素的值的兩倍,再減去該下標位置左右兩邊元素的值。”
在這里,我們打算使用來實現我們所要的運算的GPU可編程模塊,叫做片段管線(fragment pipeline),它是由多個并行處理單元組成的,在GeFore7800GTX中,并行處理單元的個數多達24個。在硬件和驅動邏輯中,每個數據項會被自動分配到不同的渲染線管線中去處理,到底是如何分配,則是沒法編程控制的。從概念觀點上看,所有對每個數據頂的運算工作都是相互獨立的,也就是說不同片段在通過管線被處理的過程中,是不相互影響的。在前面的章節中我們曾討論過,如何實現用一個紋理來作為渲染目標,以及如何把我們的數組保存到一個紋理上。因此這里我們分析一下這種運算方式:片段管線就像是一個數組處理器,它有能力一次處理一張紋理大小的數據。雖然在內部運算過程中,數據會被分割開來然后分配到不同的片段處理器中去,但是我們沒辦法控制片段被處理的先后順序,我們所能知道的就是“地址”,也就是保存運算最終結果的那張紋理的紋理坐標。我們可能想像為所有工作都是并行的,沒有任何的數據相互依賴性。這就是我們通常所說的數據并行運算(data-paralel computing)。
現在,我們已經知道了解決問題的核心算法,我們可以開始討論如何用可編程片段管線來編程實現了。內核,在GPU中被叫做著色器。所以,我們要做的就是寫一個可能解決問題的著色器,然后把它包含在我們的程序中。在本教程程中,我們會分別討論如何用CG著色語言及GLSL著色語言來實現,接下來兩個小節就是對兩種語言實現方法的討論,我們只要學會其中一種方法就可以了,兩種語言各有它自已的優缺點,至于哪個更好一點,則不是本教程所要討論的范圍。
用CG著色語言來編寫一個著色器
為了用CG語言來著色渲染,我們首先要來區分一下CG著色語言和CG運行時函數,前者是一門新的編程語言,所寫的程序經編譯后可以在GPU上運行,后者是C語言所寫的一系列函數,在CPU上運算,主要是用來初始化環境,把數據傳送給GPU等。在GPU中,有兩種不同的著色,對應顯卡渲染流水線的兩個不同的階段,也就是頂點著色和片段著色。本教程中,頂點著色階段,我們采用固定渲染管線。只在片段著色階段進行編程。在這里,使用片段管線能更容易解決我們的問題,當然,頂點著色也會有它的高級用途,但本文不作介紹。另外,從傳統上講,片段著色管線提供更強大的運算能力。
讓我們從一段寫好了的CG著色代碼開始。回憶一下CPU內核中包含的一些算法:在兩個包含有浮點數據的數組中查找對應的值。我們知道在GPU中紋理就等同于CPU的數組,因此在這里我們使用紋理查找到代替數組查找。在圖形運算中,我們通過給定的紋理坐標來對紋理進行采樣。這里有一個問題,就是如何利用硬件自動計算生成正確的紋理坐標。我們把這個問題壓后到下面的章節來討論。為了處理一些浮點的常量,我們有兩種處理的方法可選:我們可以把這些常量包含在著色代碼代中,但是如果要該變這些常量的值的話,我們就得把著色代碼重新編譯一次。另一種方法更高效一點,就是把常量的值作為一個uniform參數傳遞給GPU。uniform參數的意思就是:在整個渲染過程中值不會被改變的。以下代碼就是采用較高較的方法寫的。
[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。它會接收幾個輸入參數,并返回一個浮點值。用作變量復制的語法叫做語義綁定(semantics binding):輸入輸出參數名稱是各種不同的片段靜態變量的標識,在前面的章節中我們把這個叫“地址”。片段著色器的輸出參數必須綁定為COLOR語義,雖然這個語義不是很直觀,因為我們的輸出參數并不是傳統作用上顏色,但是我們還是必須這樣做。綁定一個二分量的浮點元組(tuple ,float2)到TEXCOORD0語義上,這樣便可以在運行時為每個像素指定一對紋理坐標。對于如何在參數中定義一個紋理樣本以及采用哪一個紋理采樣函數,這就要看我們種用了哪一種紋理對像,參考下表:
如果我們使用的是四通道的紋理而不是LUMINANCE格式的紋理,那們只須把上面代碼中的用來保存紋理查詢結果的浮點型變量改為四分量的浮點變量(float4 )就可以了。由于GPU具有并行運算四分量數的能力,因此對于使用了rectangle為對像的RGBA格式紋理,我們可以采用以下代碼:
[cpp] 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;
}
我們可以把著色代碼保存在字符數組或文本文件中,然后通過OpenGL的CG運行時函數來訪問它們。
建立CG運行環境
在這一小節,中描術了如何在OpenGL應用程序中建立Cg運行環境。首先,我們要包含CG的頭文件(#include 《cg/cggl.h》),并且把CG的庫函數指定到編譯連接選項中,然后聲明一些變量。
[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。為了簡單起見,我們還聲明了三個句柄,分別對應了著色程序中的三個沒有語義的入口參數。我們用一個全局的字符串變量來保存前面所寫好的著色代碼。現在就把所有的CG初始化工作放在一個函數中完成。這里只作了最簡單的介紹,詳細的內容可以查看CG手冊,或者到Cg Toolkit page.網頁上學習一下。
譯注:對于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的高級著色語言,我們不需要另外引入任何的頭文件或庫文件,因因它們在安裝驅動程序的時候就一起被建立好了。三個OpenGL的擴展:(ARB_shader_objects,ARB_vertex_shader 和ARB_fragment_shader)定義了相關的接口函數。它的說明書(specification )中對語言本身作了定義。兩者,API和GLSL語言,現在都是OpenGL2.0內核的一個重要組成部份。但是如果我們用的是OpenGL的老版本,就要用到擴展。
我們為程序對像定義了一系列的全局變量,包括著色器對像及數據變量的句柄,通過使用這些句柄,我們可以訪問著色程序中的變量。前面兩個對像是簡單的數據容器,由OpenGL進行管理。一個完整的著色程序是由頂點著色和片段著色兩大部份組成的,每部分又可以由多個著色程序組成。
[cpp] view plaincopy// GLSL vars
GLhandleARB programObject;
GLhandleARB shaderObject;
GLint yParam, xParam, alphaParam;
編寫著色程序和使用Cg語言是相似的,下面提供了兩個GLSL的例子,兩個主程序的不同之處在于我們所采用的紋理格式。變量的類型入關鍵字與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的初始化工作放在一個函數中實現,GLSL API是被設計成可以模擬傳統的編譯及連接過程,更多的細節,請參考橙皮書(Orange Book),或者查找一些GLSL的教程來學習一下,推薦到Lighthouse 3D’s GLSL tutorial 網站上看一下
[cpp] 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:運算 = 繪圖
在這一章節里,我們來討論一下如何把本教程前面所學到的知識拼湊起來,以及如何使用這些知識來解決前面所提出的加權數組相加問題:y_new =y_old +alpha *x 。關于執行運算的部份,我們把所有運算都放在performComputation()這個函數中實現。一共有四個步驟:首先是激活內核,然后用著色函數來分配輸入輸出數組的空間,接著是通過渲染一個適當的幾何圖形來觸發GPU的運算,最后一步是簡單驗證一下我們前面所列出的所有的基本理論。
準備好運算內核
使用CG運行時函數來激活運算內核就是顯卡著色程序。首先用enable函數來激活一個片段profile,然后把前面所寫的著色代碼傳送到顯卡上并綁定好。按規定,在同一時間內只能有一個著色器是活動的,更準確的說,是同一時間內,只能分別激活一個頂點著色程序和一個片段著色程序。由于本教程中采用了固定的頂點渲染管線,所以我們只關注片段著色就行了,只需要下面兩行代碼便可以了。
[cpp] view plaincopy// enable fragment profile
cgGLEnableProfile(fragmentProfile);
// bind saxpy program
cgGLBindProgram(fragmentProgram);
如果使用的是GLSL著色語言,這一步就更容易實現了,如果我們的著色代碼已以被成功地編譯連接,那么剩下我們所需要做的就只是把程序作為渲染管線的一部分安裝好,代碼如下:
glUseProgramObjectARB(programObject);
建立用于輸入的數組和紋理
在CG環境中,我們先要把紋理的標識與對應的一個uniform樣本值關聯起來,然后激活該樣本。這樣該紋理樣本便可以在CG中被直接使用了。
[cpp] 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參數。
[cpp] 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);
建立用于輸出的紋理及數組
定義用于輸出的紋理,從本質上講,這和把數據傳輸到一個FBO紋理上的操作是一樣的,我們只需要指定OpenGL函數參數的特定意義就可以了。這里我們只是簡單地改變輸出的方向,也就是,把目標紋理與我們的FBO綁定在一起,然后使用標準的GL擴展函數來把該FBO指為渲染的輸出目標。
[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);
準備運算
讓們暫時先來回顧一下到目前為止,我們所做過了的工作:我們實現了目標像素、紋理坐標、要繪制的圖形三者元素一一對應的關系。我們還寫好了一個片段著色器,用來讓每個片段渲染的時候都可以運行一次。現在剩下來還要做的工作就是:繪制一個“合適的幾何圖形” ,這個合適的幾何圖形,必須保證保存在目標紋理中的數據每個元素就會去執行一次我們的片段著色程序。換句話來說,我們必須保證紋理中的每個數據頂在片段著色中只會被訪一次。只要指定好我們的投影及視口的設置,其它的工作就非常容易:我們所需要的就只是一個剛好能覆蓋整個視口的填充四邊形。我們定義一個這樣的四邊形,并調用標準的OpenGL函數來對其進行渲染。這就意味著我們要直接指定四邊形四個角的頂點坐標,同樣地我們還要為每個頂點指定好正確的紋理坐標。由于我們沒有對頂點著色進行編程,程序會把四個頂點通過固定的渲染管線傳輸到屏幕空間中去。光冊處理器(一個位于頂點著色與片段著色之間的固定圖形處理單元)會在四個頂點之間進行插值處理,生成新的頂點來把整個四邊形填滿。插值操作除了生成每個插值點的位置之外,還會自動計算出每個新頂點的紋理坐標。它會為四邊形中每個像素生成一個片段。由于我們在寫片段著色器中綁定了相關的語義,因此插值后的片段會被自動發送到我們的片段著色程序中去進行處理。換句話說,我們渲染的這個簡單的四邊形,就可以看作是片段著色程序的數據流生成器。由于目標像素、紋理坐標、要繪制的圖形三者元素都是一一對應的,從而我們便可以實現:為數組每個輸出位置觸發一次片段著色程序的運行。也就是說通過渲染一個帶有紋理的四邊形,我們便可以觸發著色內核的運算行,著色內核會為紋理或數組中的每個數據項運行一次。
使用 texture rectangles 紋理坐標是與像素坐標相同的,我樣使用下面一小段代碼便可以實現了。
[cpp] 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 ,就必須單位化所有的紋理坐標,等價的代碼如下:
[cpp] 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();
這里提示一下那些做高級應用的程序員:在我們的著色程序中,只用到了一組紋理坐標,但是我們也可以為每個頂點定義多組不同的紋理坐標,相關的更多細節,可以查看一下glMultiTexCoord()函數的使用。
Back to top
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! ”);
}
評論
查看更多