GPU深度發掘 -- GPGPU數學基礎教程

相關鏈接:

 https://blog.csdn.net/i53nd/article/details/2497336

https://blog.csdn.net/zq0921/article/details/6207833

實驗代碼

#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();

    /*在我們對紋理進行運算或存取的時候,爲了能夠正確地控制每一個數據元素,我
    們得選擇一個比較特殊的投影方式,把3D世界映射到2D屏幕上(從世界座標空間到
    屏幕設備座標空間),另外屏幕像素與紋理元素也要一一對應。這種關係要成功,
    關鍵是要採用正交投影及合適的視口。這樣便能做到幾何座標(用於渲染)、紋理
    座標(用作數據輸入)、像素座標(用作數據輸出)三者一一對應。有一個要提醒
    大家的地方:如果使用texture2D,我們則須要對紋理座標進行適當比例的縮放,
    讓座標的值在0到1之間,前面有相關的說明。
    爲了建立一個一一對應的映射,我們把世界座標中的Z座標設爲0,把下面這段代碼
    加入到initFBO()這個函數中    */

    // 視口的比例是 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);

    /*首先,通過使用一些OpenGL的擴展函數,我們可以給GPU提供32位精度的浮點數。
    另外有一個叫 EXT_framebuffer_object 的OpenGL的擴展, 該擴展允許我們把一個
    離屏緩衝區作爲我們渲染運算的目標,這個離屏緩衝區中的RGBA四個通道,每個都
    是32位浮點的,這樣一來, 要想GPU上實現四分量的向量運算就比較方便了,而且
    得到的是一個全精度的浮點數,同時也消除了限定數值範圍的問題。我們通常把這
    一技術叫FBO,也就是Frame Buffer Object的縮寫。
    要使用該擴展,或者說要把傳統的幀緩衝區關閉,使用一個離屏緩衝區作我們的渲
    染運算區,只要以下很少的幾行代碼便可以實現了。有一點值得注意的是:當我用
    使用數字0,來綁定一個FBO的時候,無論何時,它都會還原window系統的特殊幀緩
    衝區,這一特性在一些高級應用中會很有用,但不是本教程的範圍,有興趣的朋友
    可能自已研究一下。*/

    // 生成並綁定一個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);

    /*其實一個紋理,它不僅可以用來作數據輸入對像,也還可以用作數據輸出對
    像。這也是提高GPU運算效率和關鍵所在。通過使用 framebuffer_object 這
    個擴展,我們可以把數據直接渲染輸出到一個紋理上。但是有一個缺點:一個
    紋理對像不能同時被讀寫,也就是說,一個紋理,要麼是隻讀的,要麼就是隻
    寫的。
    FBO 擴展提供了一個簡單的函數來實現把數據渲染到紋理。爲了能夠使用一個
    紋理作爲渲染對像,我們必須先把這個紋理與FBO綁定,這裏假設離屏幀緩衝
    已經被指定好了。    */

    //把當前的FBO對像,與FBO紋理綁定在一起
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, 
        GL_COLOR_ATTACHMENT0_EXT, 
        GL_TEXTURE_RECTANGLE_ARB,fboTex,0);

    /*第一個參數的意思是很明顯的。第二個參數是定義一個綁定點(每個FBO最大
    可以支持四個不同的綁定點,當然,不同的顯卡對這個最大綁定數的支持不一
    樣,可以用GL_MAX_COLOR_ATTACHMENTS_EXT來查詢一下)。第三和第四個參數
    應該清楚了吧,它們是實際紋理的標識。最後一個參數指的是使用多重映像紋
    理,這裏沒有用到,因此設爲0。
    爲了能成功綁定一紋理,在這之前必須先用glTexImage2D()來對它定義和分配
    空間。但不須要包含任何數據。我們可以把FBO想像爲一個數據結構的指針,爲
    了能夠對一個指定的紋理直接進行渲染操作,我們須要做的就調用OpenGL來給
    這些指針賦以特定的含義。*/


    /*爲了把數據傳輸到紋理中去,我們必須綁定一個紋理作爲紋理目標,並通過一
    個GL函數來發送要傳輸的數據。實際上就是把數據的首地址作爲一個參數傳遞給
    該涵數,並指定適當的紋理大小就可以了。如果用LUMINANCE格式,則意味着數
    組中必須有texSize x texSize個元數。而RGBA格式,則是這個數字的4倍。注意
    的是,在把數據從內存傳到顯卡的過程中,是全完不需要人爲來干預的,由驅動
    來自動完成。一但傳輸完成了,我們便可能對CPU上的數據作任意修改,這不會
    影響到顯卡中的紋理數據。 而且我們下次再訪問該紋理的時候,它依然是可用
    的。在NVIDIA的顯卡中,以下的代碼是得到硬件加速的。*/

    // 把本地數據傳輸到顯卡的紋理上。
    glBindTexture(GL_TEXTURE_RECTANGLE_ARB,tex);
    glTexSubImage2D(GL_TEXTURE_RECTANGLE_ARB,0,0,0,texSize,texSize,
        GL_RGBA,GL_FLOAT,data);

    //這裏三個值是0的參數,是用來定義多重映像紋理的,由於我們這裏要求一次把
    //整個數組傳輸一個紋理中,不會用到多重映像紋理,因此把它們都關閉掉。

    /*CPU中的數據都是以行排列的方式映射到紋理中去的。更詳細地說,就是:對於
    RGBA格式,數組中的前四個數據,被傳送到紋理的第一個元素的四個分量中,分別
    與R,G,B,A分量一一對應,其它類推。而對於LUMINANCE 格式的紋理,紋理中第一
    行的第一個元素,就對應數組中的第一個數據。其它紋理元素,也是與數組中的數
    據一一對應的。*/

    //--------------------begin-------------------------
    //以下代碼是渲染一個大小爲texSize * texSize矩形,
    //其作用就是把紋理中的數據,經過處理後,保存到幀緩衝中去,
    //由於用到了離屏渲染,這裏的幀緩衝區指的就是FBO紋理。
    //在這裏,只是簡單地把數據從紋理直接傳送到幀緩衝中,
    //沒有對這些流過GPU的數據作任何處理,但是如果我們會用CG、
    //GLSL等高級着色語言,對顯卡進行編程,便可以在GPU中
    //截獲這些數據,並對它們進行任何我們所想要的複雜運算。
    //這就是GPGPU技術的精髓所在。問題討論:www.physdev.com

    /*這是一個反方向的操作,那就是把數據從GPU傳輸回來,存放在CPU的數組上。同
    樣,有兩種不同的方法可供我們選擇。傳統上,我們是使用OpenGL獲取紋理的方法,
    也就是綁定一個紋理目標,然後調用glGetTexImage()這個函數。這些函數的參數,
    我們在前面都有見過。

    glBindTexture(texture_target,texID);
    glGetTexImage(texture_target,0,texture_format,GL_FLOAT,data);
    */


    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------------------------

    //但是這個我們將要讀取的紋理,已經和一個FBO對像綁定的話,我們可以採用改變
    //渲染指針方向的技術來實現。

    // 從幀緩衝中讀取數據,並把數據保存到result數組中。
    glReadBuffer(GL_COLOR_ATTACHMENT0_EXT);
    glReadPixels(0, 0, texSize, texSize,GL_RGBA,GL_FLOAT,result);

    /*由於我們要讀取GPU的整個紋理,因此這裏前面兩個參數是0,0。表示從0起始點開
    始讀取。該方法是被推薦使用的。
    一個忠告:比起在GPU內部的傳輸來說,數據在主機內存與GPU內存之間相互傳輸,其
    花費的時間是巨大的,因此要謹慎使用。由其是從CPU到GPU的逆向傳輸。
    */

    /*   --------------CPU----------------      -------------GPU------------
        |                               |      |                          |
        |   data arr:                   |      |  texture:                |
        |    [][][][][][][][][]     --------------> [][][]                |
        |                               |      |    [][][]                |
        |                               |      |    [][][]                |
        |                               |      |           //             |
        |   result:                     |      |              FBO:        |
        |    [][][][][][][][][]         |      |              [][][]      |
        |                              <-----------------     [][][]      |
        |                               |      |              [][][]      |
        |-------------------------------|      |--------------------------|   */

    // 顯示最終的結果
    printf("Data before roundtrip:/n");
    for (i=0; i<texSize*texSize*4; i++)
        printf("%f/n",data[i]);

    printf("Data after roundtrip:/n");
    for (i=0; i<texSize*texSize*4; i++)
        printf("%f/n",result[i]);
    // 釋放本地內存
    free(data);
    free(result);

    // 釋放顯卡內存
    glDeleteFramebuffersEXT (1,&fb);
    glDeleteTextures (1,&tex);
    glDeleteTextures(1,&fboTex);

    system("pause");
    return 0;
}

 

Contents

介紹 

準備條件 
硬件設備要求 
軟件設備要求 
兩者選擇

初始化OpenGL 

GLUT 
OpenGL 擴展 
OpenGL 離屏渲染

GPGPU 概念1: 數組 = 紋理 

在CPU上建立數組 
在 GPU上生成浮點紋理 
數組索引與紋理座標一一對應 
使用紋理作渲染對像 
把數據從CPU數組傳輸到GPU的紋理 
把數據從GPU的紋理傳輸到CPU數組 
一個簡單的示例

GPGPU 概念 2: 內核 = 着色器 

面向循環的CPU運算 vs.面向內核的數據並行運算 
用Cg着色語言生成一個着色器 
建立Cg運行環境 
用OpenGL語言建立一個高級着色環境

GPGPU 概念 3: 運算 = 渲染 

準備運算的內核 
設定用於輸入的數組/紋理 
設定用於輸出的數組/紋理 
開始運算

GPGPU 概念 4: 返饋 

多次渲染傳遞 
使用乒乓技術

歸納總結 

一個簡但的代碼 
程序的變量 
命令行參數 
測試模式 
標準模式

附言 

對比 Windows 和 Linux, ATI 和 NVIDIA 
問題 
OpenGL錯誤檢查 
FBOs錯誤檢查 
Cg錯誤檢查 
GLSL錯誤檢查

相關知識 
版權聲明

下載源代碼

These zip files contain a MS VC 2003.NET solution file, a linux Makefile and a set of batch files with preconfigured test environments. You might want to read this section about the differences between Windows and Linux, NVIDIA and ATI first.


Cg version 
GLSL version

引用

對本教程的引用, please use this BibTex citation.

Back to top

介紹

本教程的目的是爲了介紹GPU編程的背景及在GPU上運算所需要的步驟,這裏通過實現在GPU上運算一個線性代數的簡單例子,來闡述我們的觀點。saxpy() 是BLAS庫上的一個函數,它實現的功能主要是這樣的:已知兩個長度爲N的數組 x 和 y ,一個標量alpha,要求我們計算縮放比例數組之和:y = y + alpha * x。這個函數很簡單。我們的目的只是在於向大家闡明一些GPGPU編程入門的必備知識和概念。本教程所介紹的一些編程實現技術,只要稍作修改和擴充,便能運用到複雜的GPU運算應用上。


 

必備條件

 


本文不打算深入到在每一個細節,而是給對OpenGL編程有一定技術基礎的朋友看的,你最好還要對圖形顯卡的組成及管道渲染有一定的瞭解。對於OpenGL剛入門的朋友,推薦大家看一下以下這些知識:Programming Guide (紅寶書). PDF and HTML, 橙寶書 ("OpenGL Shading Language"), 以及 NeHe's OpenGL教程


本教程是基於OpenGL寫,目的主要是爲不被MS Windows平臺的限制。但是這裏所闡述的大多數概念但能直接運用到DirectX上。


更多的預備知識,請到 GPGPU.org 上看一下。其中該網站上以下三篇文章,是作者極力推薦大家去看一下的: 《Where can I learn about OpenGL and Direct3D?》, 《How does the GPU pipeline work?》'《n what ways is GPU programming similar to CPU programming?》


譯者注:在國內的GPGPU論壇可以到http://www.physdev.com物理開發網上討論。該網站主要是交流PhysX物理引擎,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)。


// include the GLUT header file

#include 

// 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來作爲擴展載入函數庫,該函數庫把許多複雜的問題進行了底層的封裝,給我們使用高級擴展提供了一組簡潔方便的訪問函數。


void 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系統的特殊幀緩衝區,這一特性在一些高級應用中會很有用,但不是本教程的範圍,有興趣的朋友可能自已研究一下。


GLuint 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的渲染上下文必須被正確初始化。


int maxtexsize;

glGetIntegerv(GL_MAX_TEXTURE_SIZE,&maxtexsize);

printf("GL_MAX_TEXTURE_SIZE, %d/n",maxtexsize);      

就目前主流的顯卡來說,這個值一般是2048或者4096每個維度,值得提醒大家的就是:一塊顯卡,雖然理論上講它可以支持4096*4096*4096的三維浮點紋理,但實際中受到顯卡內存大小的限制,一般來說,它達不到這個數字。


在CPU中,我們常會討論到數組的索引,而在GPU中,我們需要的是紋理座標,有了紋理座標纔可以訪問紋理中每個數據的值。而要得到紋理座標,我們又必須先得到紋理中心的地址。


傳統上講,GPU是可以四個分量的數據同時運算的,這四個分量也就是指紅、綠、藍、alpha(RGBA)四個顏色通道。稍後的章節中,我將會介紹如何使用顯卡這一併行運算的特性,來實現我們想要的硬件加速運算。

在CPU上生成數組

讓我們來回顧一下前面所要實現的運算:也就是給定兩個長度爲N的數組,現在要求兩數組的加權和y=y +alpha*x,我們現在需要兩個數組來保存每個浮點數的值,及一個記錄alpha值的浮點數。


float* 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 在概念上有兩大不同之處。我們可以從下面這個列表來對比一下,稍後我還會列舉一些例子。


texture2D    texture rectangle
texture target    GL_TEXTURE_2D    GL_TEXTURE_RECTANGLE_ARB
紋理座標    
座標必須被單位化,範圍被限定在0到1之間,其它範圍不在0到1之間的紋理座標不會被支持。


紋理座標不要求單位化

紋理大小    
紋理大小必須是2的n次方,如1024,512等。當然如果你的顯卡驅動支持ARB_non_power_of_two或者OpenGL2.0的話,則不會受到此限制。

紋理尺寸的大小是任意的,如 ( 513 x1025)

 


另外一個重要的影響因素就是紋理格式,我們必須謹慎選擇。在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 and ARB_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.


NV3x    NV4x, G7x (RECT)    NV4x, G7x (2D)    ATI
target    texture rectangle    texture rectangle    texture2D    texture2D and texture rectangle
format    LUMINANCE and RGBA (and RG and RGB)*
internal
format    NV_float_buffer    NV_float_buffer    ATI_texture_float
ARB_texture_float    ATI_texture_float
ARB_texture_float

(*) Warning: 這些格式作爲紋理是被支持的,但是如果作爲渲染對像,就不一定全部都能夠得到良好的支持(see below).


講完上面的一大堆基礎理論這後,是時候回來看看代碼是如何實現的。比較幸運的是,當我們弄清楚了要用那些紋理對像、紋理格式、及內部格式之後,要生成一個紋理是很容易的。


// 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()這個函數中


// 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綁定,這裏假設離屏幀緩衝已經被指定好了。


glFramebufferTexture2DEXT(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一起來使用才行。換句話說,紋理中的浮點數的格式與渲染對像中的浮點數格式有着本質上的區別。

下面這個表格對目前不同的顯卡平臺總結了一下,指的是有哪些紋理格式及紋理對像是可能用來作爲渲染對像的,(可能還會有更多被支持的格式,這裏只關心是浮點數的紋理格式):


NV3x    NV4x, G7x    ATI
texture 2D, ATI/ARB_texture_float, LUMINANCE
no    no    no
texture 2D, ATI/ARB_texture_float, RGB, RGBA
no    yes    yes
texture 2D, NV_float_buffer, LUMINANCE
no    no    no
texture 2D, NV_float_buffer, RGB, RGBA
no    no    no
texture RECT, ATI/ARB_texture_float, LUMINANCE
no    no    no
texture RECT, ATI/ARB_texture_float, RGB, RGBA
no    yes    yes
texture RECT, NV_float_buffer, LUMINANCE
yes    yes    no
texture RECT, NV_float_buffer, RGB, RGBA
yes    yes    no

列表中最後一行所列出來的格式在目前來說,不能被所有的GPU移植使用。如果你想採用LUMINANCE格式,你必須使用ractangles紋理,並且只能在NVIDIA的顯卡上運行。想要寫出兼容NVIDIA及ATI兩大類顯卡的代是可能的,但只支持NV4x以上。幸運的是要修改的代碼比較少,只在一個switch開關,便能實現代碼的可移植性了。相信隨着ARB新版本擴展的發佈,各平臺之間的兼容性將會得到進一步的提高,到時候各種不同的格式也可能相互調用了。

把數據從CPU的數組傳輸到GPU的紋理

爲了把數據傳輸到紋理中去,我們必須綁定一個紋理作爲紋理目標,並通過一個GL函數來發送要傳輸的數據。實際上就是把數據的首地址作爲一個參數傳遞給該涵數,並指定適當的紋理大小就可以了。如果用LUMINANCE格式,則意味着數組中必須有texSize x texSize個元數。而RGBA格式,則是這個數字的4倍。注意的是,在把數據從內存傳到顯卡的過程中,是全完不需要人爲來干預的,由驅動來自動完成。一但傳輸完成了,我們便可能對CPU上的數據作任意修改,這不會影響到顯卡中的紋理數據。 而且我們下次再訪問該紋理的時候,它依然是可用的。在NVIDIA的顯卡中,以下的代碼是得到硬件加速的。


glBindTexture(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的逆向傳輸。


在前面“ 當前顯卡設備運行的問題” 中 提及到該方面的問題。

一個簡單的例子

現在是時候讓我們回頭來看一下前面要解決的問題,我強烈建議在開始一個新的更高級的話題之前,讓我們先弄一個顯淺的例子來實踐一下。下面通過一個小的程序,嘗試着使用各種不同的紋理格式,紋理對像以及內部格式,來把數據發送到GPU,然後再把數據從GPU取回來,保存在CPU的另一個數組中。在這裏,兩個過程都沒有對數據作任何運算修該,目的只是看一下數據GPU和CPU之間相互傳輸,所需要使用到的技術及要注意的細節。也就是把前面提及到的幾個有迷惑性的問題放在同一個程序中來運行一下。在稍後的章節中將會詳細討論如何來解決這些可能會出現的問題。


由於趕着要完成整個教程,這裏就只寫了一個最爲簡單的小程序,採用rectangle紋理、ARB_texture_float作紋理對像並且只能在NVIDIA的顯卡上運行。


#include 
#include 
#include 
#include 

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        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:/n");


    for (i=0; i        printf("%f/n",data[i]);

    printf("Data after roundtrip:/n");

    for (i=0; i        printf("%f/n",result[i]);

    // 釋放本地內存

    free(data);

    free(result);


    // 釋放顯卡內存

    glDeleteFramebuffersEXT (1,&fb);

    glDeleteTextures (1,&tex);

    glDeleteTextures(1,&fboTex);

    return 0;

}

你可以在這裏下載到爲ATI顯卡寫的另一個版本。


        --------------CPU----------------      -------------GPU------------

        |                               |      |                          |

        |   data arr:                   |      |  texture:                |

        |    [][][][][][][][][]     --------------> [][][]                |

        |                               |      |    [][][]                |

        |                               |      |    [][][]                |

        |                               |      |           //             |

        |   result:                     |      |              FBO:        |

        |    [][][][][][][][][]         |      |              [][][]      |

        |                              <-----------------     [][][]      |

        |                               |      |              [][][]      |

        |-------------------------------|      |--------------------------|


以上代碼是理解GPU編程的基礎,如果你完全看得懂,並且能對這代碼作簡單的修改運用的話,那恭喜你,你已經向成功邁進了一大步,並可以繼續往下看,走向更深入的學習了。但如看不懂,那回頭再看一編吧。

Back to top

GPGPU 概念 2:內核(Kernels) = 着色器(shaders)

在這一章節中,我們來討論GPU和CPU兩大運算模塊最基本的區別,以及理清一些算法和思想。一但我們弄清楚了GPU是如何進行數據並行運算的,那我們要編寫一個自已的着色程序,還是比較容易的。

面向循環的CPU運算 vs. 面向內核的GPU數據並行運算

讓我們來回憶一下我們所想要解決的問題:y = y + alpha* x; 在CPU上,通常我們會使用一個循環來遍歷數組中的每個元素。如下:


for (int i=0; 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參數的意思就是:在整個渲染過程中值不會被改變的。以下代碼就是採用較高較的方法寫的。


                              

float 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語義上,這樣便可以在運行時爲每個像素指定一對紋理座標。對於如何在參數中定義一個紋理樣本以及採用哪一個紋理採樣函數,這就要看我們種用了哪一種紋理對像,參考下表:


texture2D    texture rectangle
樣本定義    uniform sampler2D    uniform samplerRECT
紋理查找函數    tex2D(name, coords)    texRECT(name, coords)

如果我們使用的是四通道的紋理而不是LUMINANCE格式的紋理,那們只須把上面代碼中的用來保存紋理查詢結果的浮點型變量改爲四分量的浮點變量(float4 )就可以了。由於GPU具有並行運算四分量數的能力,因此對於使用了rectangle爲對像的RGBA格式紋理,我們可以採用以下代碼:


float4 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的庫函數指定到編譯連接選項中,然後聲明一些變量。


// 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


void 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進行管理。一個完整的着色程序是由頂點着色和片段着色兩大部份組成的,每部分又可以由多個着色程序組成。


// GLSL vars

GLhandleARB programObject;

GLhandleARB shaderObject;

GLint yParam, xParam, alphaParam;

編寫着色程序和使用Cg語言是相似的,下面提供了兩個GLSL的例子,兩個主程序的不同之處在於我們所採用的紋理格式。變量的類型入關鍵字與CG有很大的不同,一定要按照OpenGL的定義來寫。


// 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 網站上看一下


void 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,然後把前面所寫的着色代碼傳送到顯卡上並綁定好。按規定,在同一時間內只能有一個着色器是活動的,更準確的說,是同一時間內,只能分別激活一個頂點着色程序和一個片段着色程序。由於本教程中採用了固定的頂點渲染管線,所以我們只關注片段着色就行了,只需要下面兩行代碼便可以了。


// enable fragment profile

cgGLEnableProfile(fragmentProfile);

// bind saxpy program

cgGLBindProgram(fragmentProgram);

如果使用的是GLSL着色語言,這一步就更容易實現了,如果我們的着色代碼已以被成功地編譯連接,那麼剩下我們所需要做的就只是把程序作爲渲染管線的一部分安裝好,代碼如下:


glUseProgramObjectARB(programObject);    

建立用於輸入的數組和紋理

在CG環境中,我們先要把紋理的標識與對應的一個uniform樣本值關聯起來,然後激活該樣本。這樣該紋理樣本便可以在CG中被直接使用了。


// 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參數。


// 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指爲渲染的輸出目標。


// 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 紋理座標是與像素座標相同的,我樣使用下面一小段代碼便可以實現了。


// 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 ,就必須單位化所有的紋理座標,等價的代碼如下:


// 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個綁定點可以被使用,而且,最後一種方法的運算是最快的,我們在這裏將詳細解釋一下,看看我們是如何在兩個不同的綁定點之間實現“乒乓” 的。


要實現這個,我們首先需要一組用於管理控制的變量。


// 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 

                            };

在運算其間,我們只需要做的就是給內核傳遞正確的參數值,並且每次運算都要交換一次組組的索引值: 


// 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    // 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的擴展。


rect_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運行過程中產生的錯誤。


void 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(")/n.");

    }

}

檢查FBO中的錯誤

EXT_framebuffer_object 擴展,定義了一個很好用的運行時Debug函數。這裏只列出了它的一些常見的反回值作參考,要詳細解釋這些返回信息,請查看規格說明書的framebuffer completeness 部分。


bool 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/n");

            return false;

        case GL_FRAMEBUFFER_UNSUPPORTED_EXT:

            printf("Unsupported framebuffer format/n");

            return false;

        case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT:

            printf("Framebuffer incomplete,missing attachment/n");

            return false;

        case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT:

            printf("Framebuffer incomplete,attached images 

                    must have same dimensions/n");

            return false;

        case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT:

             printf("Framebuffer incomplete,attached images 

                     must have same format/n");

            return false;

        case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT:

            printf("Framebuffer incomplete,missing draw buffer/n");

            return false;

        case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT:

            printf("Framebuffer incomplete,missing read buffer/n");

            return false;

    }

    return false;

}

檢查CG的錯誤

在CG中檢查錯誤有一些細微的不同,一個自寫入的錯誤處理句柄被傳遞給CG的錯誤處理回調函數。


// 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的錯誤

使用以下的函數來查看編譯的結果:


/**

 * 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("/n");

        free(infoLog);

    }

}

大多數情況下,你可以使用以上查詢函數,詳細內容可以查看一下GLSL的規格說明書。還有另一個非常重要的查詢函數,是用來檢查程序是否可以被連接:


GLint success;

glGetObjectParameterivARB(programObject, 

                          GL_OBJECT_LINK_STATUS_ARB, 

                          &success);

if (!success) {

    printf("Shader could not be linked!/n");

}


Back to top

感謝

Writing this tutorial would have been impossible without all contributors at the GPGPU.org forums. They answered all my questions patiently, and without them, starting to work in the GPGPU field (and consequently, writing this tutorial) would have been impossible. I owe you one, guys!


如果沒有GPGPU.org論壇所作出的貢獻,可能也就沒有這篇論文的產生。他們非常耐心地回答了我所有的問題,在大家的幫助下,我才踏入GPGPU的大門,也因此纔有了這篇文章,感謝多位朋友:


Andrew Corrigan, Wojciech Jaskowski, Matthias Miemczyk, Stephan Wagner and especially Thomas Rohkämper were invaluably helpful in proof-reading the tutorial and beta-testing the implementation. Thanks a lot!

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章