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ù)組中的每個元素。如下:
[cpp] 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ù)組的索引是一樣,更準確地說,是所有輸入數(shù)組下標,都與輸出數(shù)組的下標是相同的,另外,在對于兩個數(shù)組,也沒有下標的錯位訪問或一對多的訪問現(xiàn)像,如:y[i] = -x[i-1] + 2*x[[i] - x[i+1] 。這個公式可以用一句不太專業(yè)的語言來描術(shù):“組數(shù)Y中每個元素的值等于數(shù)組X中對應(yīng)下標元素的值的兩倍,再減去該下標位置左右兩邊元素的值。”
在這里,我們打算使用來實現(xiàn)我們所要的運算的GPU可編程模塊,叫做片段管線(fragment pipeline),它是由多個并行處理單元組成的,在GeFore7800GTX中,并行處理單元的個數(shù)多達24個。在硬件和驅(qū)動邏輯中,每個數(shù)據(jù)項會被自動分配到不同的渲染線管線中去處理,到底是如何分配,則是沒法編程控制的。從概念觀點上看,所有對每個數(shù)據(jù)頂?shù)倪\算工作都是相互獨立的,也就是說不同片段在通過管線被處理的過程中,是不相互影響的。在前面的章節(jié)中我們曾討論過,如何實現(xiàn)用一個紋理來作為渲染目標,以及如何把我們的數(shù)組保存到一個紋理上。因此這里我們分析一下這種運算方式:片段管線就像是一個數(shù)組處理器,它有能力一次處理一張紋理大小的數(shù)據(jù)。雖然在內(nèi)部運算過程中,數(shù)據(jù)會被分割開來然后分配到不同的片段處理器中去,但是我們沒辦法控制片段被處理的先后順序,我們所能知道的就是“地址”,也就是保存運算最終結(jié)果的那張紋理的紋理坐標。我們可能想像為所有工作都是并行的,沒有任何的數(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)顯卡渲染流水線的兩個不同的階段,也就是頂點著色和片段著色。本教程中,頂點著色階段,我們采用固定渲染管線。只在片段著色階段進行編程。在這里,使用片段管線能更容易解決我們的問題,當然,頂點著色也會有它的高級用途,但本文不作介紹。另外,從傳統(tǒng)上講,片段著色管線提供更強大的運算能力。
讓我們從一段寫好了的CG著色代碼開始?;貞浺幌翪PU內(nèi)核中包含的一些算法:在兩個包含有浮點數(shù)據(jù)的數(shù)組中查找對應(yīng)的值。我們知道在GPU中紋理就等同于CPU的數(shù)組,因此在這里我們使用紋理查找到代替數(shù)組查找。在圖形運算中,我們通過給定的紋理坐標來對紋理進行采樣。這里有一個問題,就是如何利用硬件自動計算生成正確的紋理坐標。我們把這個問題壓后到下面的章節(jié)來討論。為了處理一些浮點的常量,我們有兩種處理的方法可選:我們可以把這些常量包含在著色代碼代中,但是如果要該變這些常量的值的話,我們就得把著色代碼重新編譯一次。另一種方法更高效一點,就是把常量的值作為一個uniform參數(shù)傳遞給GPU。uniform參數(shù)的意思就是:在整個渲染過程中值不會被改變的。以下代碼就是采用較高較的方法寫的。
?。踓pp] 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)變量的標識,在前面的章節(jié)中我們把這個叫“地址”。片段著色器的輸出參數(shù)必須綁定為COLOR語義,雖然這個語義不是很直觀,因為我們的輸出參數(shù)并不是傳統(tǒng)作用上顏色,但是我們還是必須這樣做。綁定一個二分量的浮點元組(tuple ,float2)到TEXCOORD0語義上,這樣便可以在運行時為每個像素指定一對紋理坐標。對于如何在參數(shù)中定義一個紋理樣本以及采用哪一個紋理采樣函數(shù),這就要看我們種用了哪一種紋理對像,參考下表:
如果我們使用的是四通道的紋理而不是LUMINANCE格式的紋理,那們只須把上面代碼中的用來保存紋理查詢結(jié)果的浮點型變量改為四分量的浮點變量(float4 )就可以了。由于GPU具有并行運算四分量數(shù)的能力,因此對于使用了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;
}
我們可以把著色代碼保存在字符數(shù)組或文本文件中,然后通過OpenGL的CG運行時函數(shù)來訪問它們。
建立CG運行環(huán)境
在這一小節(jié),中描術(shù)了如何在OpenGL應(yīng)用程序中建立Cg運行環(huán)境。首先,我們要包含CG的頭文件(#include 《cg/cggl.h》),并且把CG的庫函數(shù)指定到編譯連接選項中,然后聲明一些變量。
?。踓pp] 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ù)中完成。這里只作了最簡單的介紹,詳細的內(nèi)容可以查看CG手冊,或者到Cg Toolkit page.網(wǎng)頁上學(xué)習(xí)一下。
譯注:對于CG入門,可以看一下《CG編程入門》這篇文章:http://www.physdev.com/phpbb/cms_view_article.php?aid=7
?。踓pp] 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)的編譯及連接過程,更多的細節(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ù)組的空間,接著是通過渲染一個適當?shù)膸缀螆D形來觸發(fā)GPU的運算,最后一步是簡單驗證一下我們前面所列出的所有的基本理論。
準備好運算內(nèi)核
使用CG運行時函數(shù)來激活運算內(nèi)核就是顯卡著色程序。首先用enable函數(shù)來激活一個片段profile,然后把前面所寫的著色代碼傳送到顯卡上并綁定好。按規(guī)定,在同一時間內(nèi)只能有一個著色器是活動的,更準確的說,是同一時間內(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)境中,我們先要把紋理的標識與對應(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ù)。
[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);
建立用于輸出的紋理及數(shù)組
定義用于輸出的紋理,從本質(zhì)上講,這和把數(shù)據(jù)傳輸?shù)揭粋€FBO紋理上的操作是一樣的,我們只需要指定OpenGL函數(shù)參數(shù)的特定意義就可以了。這里我們只是簡單地改變輸出的方向,也就是,把目標紋理與我們的FBO綁定在一起,然后使用標準的GL擴展函數(shù)來把該FBO指為渲染的輸出目標。
?。踓pp] 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);
準備運算
讓們暫時先來回顧一下到目前為止,我們所做過了的工作:我們實現(xiàn)了目標像素、紋理坐標、要繪制的圖形三者元素一一對應(yīng)的關(guān)系。我們還寫好了一個片段著色器,用來讓每個片段渲染的時候都可以運行一次?,F(xiàn)在剩下來還要做的工作就是:繪制一個“合適的幾何圖形” ,這個合適的幾何圖形,必須保證保存在目標紋理中的數(shù)據(jù)每個元素就會去執(zhí)行一次我們的片段著色程序。換句話來說,我們必須保證紋理中的每個數(shù)據(jù)頂在片段著色中只會被訪一次。只要指定好我們的投影及視口的設(shè)置,其它的工作就非常容易:我們所需要的就只是一個剛好能覆蓋整個視口的填充四邊形。我們定義一個這樣的四邊形,并調(diào)用標準的OpenGL函數(shù)來對其進行渲染。這就意味著我們要直接指定四邊形四個角的頂點坐標,同樣地我們還要為每個頂點指定好正確的紋理坐標。由于我們沒有對頂點著色進行編程,程序會把四個頂點通過固定的渲染管線傳輸?shù)狡聊豢臻g中去。光冊處理器(一個位于頂點著色與片段著色之間的固定圖形處理單元)會在四個頂點之間進行插值處理,生成新的頂點來把整個四邊形填滿。插值操作除了生成每個插值點的位置之外,還會自動計算出每個新頂點的紋理坐標。它會為四邊形中每個像素生成一個片段。由于我們在寫片段著色器中綁定了相關(guān)的語義,因此插值后的片段會被自動發(fā)送到我們的片段著色程序中去進行處理。換句話說,我們渲染的這個簡單的四邊形,就可以看作是片段著色程序的數(shù)據(jù)流生成器。由于目標像素、紋理坐標、要繪制的圖形三者元素都是一一對應(yīng)的,從而我們便可以實現(xiàn):為數(shù)組每個輸出位置觸發(fā)一次片段著色程序的運行。也就是說通過渲染一個帶有紋理的四邊形,我們便可以觸發(fā)著色內(nèi)核的運算行,著色內(nèi)核會為紋理或數(shù)組中的每個數(shù)據(jù)項運行一次。
使用 texture rectangles 紋理坐標是與像素坐標相同的,我樣使用下面一小段代碼便可以實現(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 ,就必須單位化所有的紋理坐標,等價的代碼如下:
?。踓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)用的程序員:在我們的著色程序中,只用到了一組紋理坐標,但是我們也可以為每個頂點定義多組不同的紋理坐標,相關(guān)的更多細節(jié),可以查看一下glMultiTexCoord()函數(shù)的使用。
Back to top
評論
查看更多