OpenGL深入探索——《OpenGL程式設計指南(原書第8版)》——計算著色器

panda1234lee發表於2016-06-28

轉載自 《OpenGL程式設計指南(原書第8版)》——計算著色器


概述


由於圖形處理器每秒能夠進行數以億計次的計算,它已成為一種效能十分驚人的器件。過去,這種處理器主要被設計用於承擔實時圖形渲染中海量的數學運算。然而,其潛在的計算能力也可用於處理與圖形無關的任務,特別是當無法很好地與固定功能的圖形管線結合的時候。為了使得這種應用成為可能,OpenG引入一種特殊的著色器:計算著色器。計算著色器可以認為是一個只有一級的管線,沒有固定的輸入和輸出,所有預設的輸入通過一組內建變數來傳遞。當需要額外的輸入時,可以通過那些固定的輸入輸出來控制對紋理和緩衝的訪問。所有可見的副作用是影象儲存,原子操作,以及對原子計數器的訪問。然而加上通用的視訊記憶體讀寫操作,這些看上去似乎有限的功能使計算著色器獲得一定程度的靈活性,同時擺脫圖形相關的束縛,以及開啟廣闊的應用空間。

OpenGL中的計算著色器和其他著色器很相似。它通過glCreateShader() 函式建立,用glCompilerShader()進行編譯,通過glAttachShader()對程式進行繫結,最後按通用的做法用glLinkProgram()對這些程式進行連結。計算著色器使用GLSL編寫,原則上,所有其他圖形著色器(比如頂點著色器,幾何著色器或者片元著色器)能夠使用的功能它都可以使用。當然,這不包括諸如幾何著色器中的EmitVertex()或者EndPrimitive()等功能以及其他類似的與圖形管線特有的內建變數。另一方面,計算著色器也包含一些獨有的內建變數和函式,這些變數和函式在OpenGL管線的其他地方無法訪問。


工作組及其執行


正如圖形著色器被置於管線的不同階段用來操作與圖形相關的單元一樣,將計算著色器被有效地放入一個一級的計算管線中,然後處理與計算相關的單元。按照這種類比,頂點著色器作用於每個頂點,幾何著色器作用於每個圖元,而片元著色器則作用於每個片元。圖形硬體主要通過並行來獲得效能,這種並行則通過大量的頂點、圖元和片元流過相應的管線階段而得以實現。而在計算著色器中,這種並行性則顯得更為直接,任務以組為單位進行執行,我們稱為工作組(work group)。擁有鄰居的工作組被稱為本地工作組(local workgroup), 這些組可以組成更大的組,稱為全域性工作組(global workgroup),而其通常作為執行命令的一個單位。

計算著色器會被全域性工作組中每一個本地工作組中的每一個單元呼叫一次,工作組的每一個單元稱為工作項(work item),每一次呼叫稱為一次執行。執行的單元之間可以通過變數視訊記憶體進行通訊,且可執行同步操作保持一致性。圖12-1 對這種工作方式進行了說明。在這個簡化的例子中,全域性工作組包含16個本地工作組, 而每個本地工作組又包含16個執行單元,排成4*4的網格。每個執行單元擁有一個2維向量表示的索引值。

儘管在圖12-1中,全域性和本地工作組都是2維的,而事實上它們是3維的,為了能夠在邏輯上適應1維、2維的任務,只需要把額外的那2維或1維的大小設為0即可。計算著色器的每一個執行單元本質上是相互獨立的,可以並行地在支援OpenGL的GPU硬體上執行。實際中,大部分OpenGL硬體都會把這些執行單元打包成較小的集合(lockstep),然後把這些小集合拼起來組成本地工作組。本地工作組的大小在計算著色器的原始碼中用輸入佈局限定符來設定。全域性工作組的大小則是本地工作組大小的整數倍。當計算著色器執行的時候,它可以內建變數來知道當前在本地工作組中的相對座標本地工作組的大小, 以及本地工作組在全域性工作組中的相對座標。基於這些還能進一步獲得執行單元在全域性工作組中的座標等。著色器根據這些變數來決定應該負責計算任務中的哪些部分,同時也能知道一個工作組中的其他執行單元,以便於共享資料。


輸入佈局限定符在計算著色器中宣告本地工作組的大小,分別使用local_size_xlocal_size_y以及local_size_z,它們的預設值都是1。舉例來說如果忽略local_size_z,就會建立N * M的2維組。比如在例子12.1中就宣告瞭一個本地工作組大小為16 * 16的著色器。

例12.1簡單的本地工作組宣告

#version 430 core

// 輸入佈局限定符宣告一個16*16(*1)的本地工作組
layout(local_size_x = 16, local_size_y = 16) in;

void main(void)
{
	// 什麼都不做
}

儘管例子12.1中的著色器什麼事情也沒做,它仍然是一個“完整”的著色器,可以正常的編譯、連結並且在OpenGL硬體中執行。要建立一個計算著色器,只需呼叫glCreateShader ()函式,將型別設定為GL_COMPUTE_SHADER,並且呼叫glShaderSource()函式來設定著色器的原始碼, 接著就能按正常編譯了。然後把著色器附加到一個程式上,呼叫glLinkProgram()。這樣就會產生計算著色器階段需要的可執行程式。例12.2展示了從建立到連結一個計算程式(使用“計算程式”來表示使用計算著色器來編譯的程式)的完整步驟。

例12.2 建立,編譯和連結計算著色器

GLuint shader, program;
static const GLchar* source[] = 
{
	"#version 430 core\n"
	"\n"
	"// 輸入佈局限定符宣告一個16*16(*1)的本地工作組\n"
	"layout(local_size_x = 16, local_size_y = 16) in;\n"
	"\n"
	"void main(void)\n"
	"{\n"
		"// 什麼都不做\n"
	"}\n"
};

shader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(shader, 1, source, NULL);
glCompileShader(shader);

program = glCreateProgram();
glAttachShader(program, shader);
glLinkProgram(program);

一旦像例12.2中那樣建立並連結一個計算著色器後,就可以用glUseProgram()函式把它設定為當前要執行的程式,然後用glDispatchCompute()把工作組傳送到計算管線上,其原型如下:
void glDispatchCompute(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);



在3個維度上分發計算工作組。num_groups_xnum_groups_ynum_groups_z分別設定工作組在X,Y和Z維度上的數量。每個引數都必須大於0,小於或等於一個與裝置相關的常量陣列GL_MAX_COMPUTE_WORK_GROUP_SIZE的對應元素。

在呼叫glDispatchCompute()時,OpenGL會建立一個包含個數為num_groups_x * num_groups_y * num_gourps_z的本地工作組的3維陣列。注意三個維度中一個或兩個維度可以為1或者glDispatchCompute()的引數的任何值。所以計算著色器中執行單元的總數是這個3維陣列的大小乘以著色器程式碼中定義的本地工作組的大小。可想而知,這種方法可以為影象處理器建立非常大規模的工作負載,而通過計算著色器則可以相對容易地獲得並行性。

正如glDrawArraysIndirect()和glDrawArrays()的關係一樣,除了使用glDispatchCompute()之外通過glDispatchComputeIndirect()可以使用儲存在緩衝區物件上的引數來傳送計算任務。緩衝區物件被繫結在GL_DISPATCH_INDIRECT_BUFFER上,並且緩衝區中儲存的引數包含三個打包在一起的無符號整數。這三個無符號整數的作用和glDispatchCompute()中的引數是等價的。參考glDispatchComputeIndirect的原型如下:

void glDispatchComputeIndirect(GLintptr indirect);



在三個維度上分發計算工作組,同時使用快取物件中儲存的引數。indirect表示快取資料中儲存引數的位置偏移量,使用基本機器單位。快取中當前偏移位置的引數,是緊密排列的三個無符號整數值,用來表示本地工作組的數量。這些無符號整數值等價於glDispatchCompute()中的num_groups_x,num_groups_y和num_groups_z引數。每個引數都必須大於0,小於或等於一個裝置相關的常量陣列GL_MAX_COMPUTE_WORK_GROUP_SIZE的對應元素。

繫結在GL_DISPATCH_INDIRECT_BUFFER上的緩衝區資料的來源可以多種多樣,比如由另外一個計算著色器生成。這樣一來,圖形處理器就能夠通過設定緩衝區中的引數來給自身傳送任務做計算或繪圖。例12.3中使用glDispatchComputeIndirect()來傳送計算任務。

例12.3 分發計算工作量

// program是一個已經成功連結的程式物件
// 其中包含了計算著色器
GLuint program = ...;

// 啟用程式物件
glUseProgram(program);

// 建立快取,將它繫結給DISPATCH_INDIRECT_BUFFER
// 繫結點,並填充資料
glGenBuffer(1, &dispatch_buffer);
glBindBuffer(GL_DISPACH_INDIRECT_BUFFER, dispatch_buffer);

static const struct
{
	GLuint num_groups_x;
	GLuint num_groups_y;
	GLuint num_groups_z;
} dispatch_params = {16, 16, 1};

glBufferData(GL_DISPACH_INDIRECT_BUFFER,
	sizeof(dispatch_params),
	&dispatch_params,
	GL_STATIC_DRAW);
	
// 使用快取物件中填充的
// 引數來分發計算著色器
glDispatchComputeIndirect(0);

注意到例12.3簡單地使用glUseProgram()把當前的程式物件指向某個特定計算程式。除了不能訪問圖形管線中的那些固定功能部分(如光柵器或幀快取),計算著色器及其程式是完全正常的,這意味著你可以用glGetProgramiv()來請求它們的一些屬性(比如有效的uniform常量,或者儲存塊)或者像往常一樣訪問uniform常量。當然,計算著色器可以訪問所有其他著色器能訪問的資源,比如影象,取樣器,緩衝區,原子計數器,以及常量儲存塊。

計算著色器及其程式還有一些獨有的屬性。比如,獲得本地工作組的大小(在原始碼的佈局限定符中設定),呼叫glGetProgramiv()時將pname設定成GL_MAX_COMPUTE_WORK_GROUP_SIZE以及把param設定成包含三個無符號整型數的陣列地址。這陣列中的三個數會按順序被賦值為本地工作組在X,Y和Z方向上的大小。

知道工作組的位置


一旦開始執行計算著色器,它就有可能需要對輸出陣列的一個或多個單元賦值(比如一副影象或者一個原子計數器陣列),或者需要從一個輸入陣列的特定位置讀取資料。為此得知道當前處於本地工作組中的什麼位置,以及在更大範圍的全域性工作組中的位置。於是,OpenGL為計算著色器提供一組內建變數。如例12.4所示,這些內建變數被隱含地宣告。

例12.4 計算著色器中的內建變數宣告

const uvec3 gl_WorkGroupSize;
in uvec3 gl_NumWorkGroups;
in uvec3 gl_LocalInvocationID;
in uvec3 gl_WorkGroupID;
in uvec3 gl_GlobalInvocationID;
in uint gl_LocalInvocationIndex;

這些計算著色器的定義如下:

  1. gl_WorkGroupSize是一個用來儲存本地工作組大小的常數。它已在著色器的佈局限定符中有local_size_x,local_size_y和local_size_z宣告。之所以拷貝這些資訊,主要是為了兩個目的:首先,它使得工作組的大小可以在著色器中被訪問很多次而不需要依賴於預處理;其次,它使得以多維形式表示的工作組大小可以直接按向量處理,而無需顯式地構造。
  2. gl_NumWorkGroups是一個向量,它包含傳給glDispatchCompute()的引數(num_groups_x,num_groups_y和 num_groups_z)。 這使得著色器知道它所屬的全域性工作組的大小。除了比手動給uniform顯式賦值要方便外,一部分OpenGL硬體對於這些常數的設定也提供了高效的方法。
  3. gl_LocalInvocationID 表示當前執行單元在本地工作組中的位置。它的範圍從uvec3(0)到gl_WorkGroupSize – uvec3(1)
  4. gl_WorkGroupID 表示當前本地工作組在更大的全域性工作組中的位置。該變數的範圍在uvec3(0)和gl_NumWorkGroups – uvec3(1)之間。
  5. gl_GlobalInvocationID 由gl_LocalInvocationID、gl_WorkGroupSize和gl_WorkGroupID派生而來。它的準確值是gl_WorkGroupID * gl_WorkGroupSize + gl_LocalInvocationID,所以它是當前執行單元在全域性工作組中的位置的一種有效的3維索引。
  6. gl_LocalInvocationIndex是gl_LocalInvocationID的一種扁平化形式。其值等於gl_LocalInvocationID.z*gl_WorkGroupSize.x*gl_WorkGroupSize.y+gl_LocalInvocationID.y * gl_WorkGroupSize.x + gl_LocalInvocationID.x. 它可以用1維的索引來代表2維或3維的資料。
假設已經知道自己在本地工作組和全域性工作組中的位置,則可以利用資訊來運算元據。如例12.5所示,加入一個影象變數使得我們能夠將資料寫入由當前執行單元座標決定的影象位置中去,並且可以在計算著色器中更新。

例12.5 資料的操作

#version 430 core

layout(local_size_x = 32, local_size_y = 16) in;
// 儲存資料的影象變數
layout (rg32f) uniform image2D data;

void main(void)
{
	// 將本地請求ID儲存到影象中
	imageStore(data, 
	ivec2(gl_GlobalInvocationID.xy),
	vec4(vec2(gl_LocalInvocationID.xy) / 
		vec2(gl_WorkGroupSize.xy),
		0.0, 0.0));
}

例12.5中的著色器把執行單元在本地工作組中的座標按本地工作組大小進行歸一化, 然後將該結果寫入由全域性請求ID確定的影象位置上去。 影象結果表達了全域性和本地的請求ID的關係,並且展示在計算著色器中定義的矩形的工作組。(本例有32*16個執行單元,影象如12.2所示)

為了生成如圖12-2的影象, 在計算著色器寫完資料後,只需簡單地將紋理渲染至一個全屏的三角條帶上即可。


圖12-2 全域性和本地的請求ID的關係

通訊與同步


當呼叫glDispatchCompute()(或者glDispatchComputeIndirect())的時候,圖形處理器的內部將執行大量的工作。圖形處理器會盡可能採取並行的工作方式,並且每個計算著色器的請求都被看作是一個執行某項任務的小隊。我們必然要通過通訊來加強團隊之間的合作,所以即使OpenGL並沒有定義執行順序和並行等級的資訊,我們還是可以在請求之間建立某種程度的合作關係,以實現變數的共享。此外,我們還可以對一個本地工作組的所有請求進行同步,讓它們在同一時刻同時抵達著色器的某個位置。

通訊


我們可以使用shared關鍵字來宣告著色器中的變數,其格式與其它的關鍵字,例如uniform、in、out等類似。例12.6給出了一個使用shared關鍵字來進行宣告的示例。

例12.6 宣告共享變數的示例

// 一個共享的無符號整型變數
shared uint fool;

// 一個共享的向量陣列
shared vec4 bar[128];

// 一個共享的資料塊
shared struct baz_struct
{
	vec4 a_vector;
	int an_integer;
	ivec2 an_array_of_integers[27];
}baz[42];

如果一個變數被宣告為shared,那麼它將被儲存到特定的位置,從而對同一個本地工作組內的所有計算著色器請求可見。如果某個計算著色器請求對共享變數進行寫入,那麼這個資料的修改資訊將最終通知同一個本地工作組的所有著色器請求。在這裡我們用了“最終”這個詞,這是因為各個著色器請求的執行順序並沒有定義,就算是同一個本地工作組內也是如此。因此,某個著色器請求寫入共享shared變數的時刻可能與另一個請求讀取該變數的時刻相隔甚遠,無論先寫入後讀取還是先讀取後寫入。為了確保能夠獲得期望的結果,我們需要在程式碼中使用某種同步的方法。下一個小節詳細介紹這一問題。
通常訪問共享shared變數的效能會遠遠好於訪問影象或者著色器儲存快取(shader storage buffer)(例如主記憶體)的效能。因為著色器處理器會將共享記憶體作為區域性量處理,並且可以在裝置中進行拷貝,所以訪問共享變數可能比使用緩衝區的方法更迅速。因此我們建議,如果你的著色器需要對一處記憶體進行大量的訪問,尤其是可能需要多個著色器請求訪問同一處記憶體地址的時候,不妨先將記憶體拷貝到著色器的共享變數中,然後通過這種方法進行操作,如果有必要,再把結果寫回到主記憶體中。

因為需要把宣告為shared的變數儲存到圖形處理器的高效能資源環境中,而這樣的資源環境是有限的,所以需要查詢和了解某個計算著色器程式的共享變數的最大數量。要獲取這個限制值,可以呼叫glGetIntegerv()並設定pname為GL_MAX_COMPUTE_SHARED_MEMORY_SIZE

同步


如果本地工作組請求的執行順序,以及全域性工作組中的所有本地工作組的執行順序都沒有定義,那麼請求執行操作的時機與其他請求就是完全無關的。如果請求之間不需要互相通訊,只需完全獨立地執行,那麼這樣並沒有什麼問題。但是,如果請求之間需要進行通訊,無論是通過影象,快取還是共享記憶體,那麼我們就有必要對它們的操作進行同步處理了。

同步命令的型別有兩種。首先是執行屏障execution barrier),可以通過barrier()函式觸發。它與細分控制著色器中的barrier()函式類似,後者可以用來實現控制點處理過程中的請求同步。如果計算著色器的一個請求遇到了barrier(),那麼它會停止執行,並等待同一個本地工作組的所有請求到達為止。當請求從barrier()中斷的地方重新開始執行的時候,我們可以斷定其它所有的請求也已經到達了barrier(),並且在此之前的所有操作均已經完成。barrier()函式在計算著色器中的用法比在細分控制著色器中更為靈活。尤其是,不需要限制在著色器中的main()函式中執行barrier()。但是,必須在統一的流控制過程中呼叫barrier()。也就是說,如果本地工作組的一個請求執行了barrier()函式,那麼同一工作組的所有請求都必須執行這個函式。這樣是合理的,因為著色器的某個請求不可能知道其它請求的控制流情況,所以只能假設其它請求也能到達屏障的位置,否則將會發生死鎖的情形。

如果在本地工作組內進行請求間的通訊,那麼可以在一個請求中寫入共享變數,然後在另一個請求中讀取。但是,我們必須確定目標請求中讀取共享變數的時機,即在源請求已經完成對應的寫入操作之後。為了確保這一點,我們可以在源請求中寫入變數,然後在兩個請求中同時執行barrier()函式。當目標請求從barrier()返回的時候,源請求必然已經執行了同一個函式(也就是完成共享變數的寫入),因此可以安全地讀取變數的值了。

第二種型別的同步叫做記憶體屏障memory barrier)。記憶體屏障的最直接的版本就是memoryBarrier()。如果呼叫memoryBarrier(),那麼就可以保證著色器請求記憶體的寫入操作一定是提交到記憶體端,而不是通過緩衝區(cache)或者排程佇列之類的方式。所有發生在memoryBarrier()之後的操作在讀取同一處記憶體的時候,都可以使用這些記憶體寫入的結果,即使是同一個計算著色器的其它請求也是如此。此外,memoryBarrier()還可以給著色器編譯器做出指示,讓它不要對記憶體操作重排序,以免因此跨越屏障函式。如果你覺得memoryBarrier()的約束過於嚴格,那麼你的感覺很正確。事實上,memoryBarrier()系列中還有其它不同的記憶體屏障子函式。memoryBarrier()所做的只是簡單地按照某種未定義的順序(這個說法不一定準確)依次呼叫這些子函式而已。

memoryBarrierAtomicCounter()函式會等待原子計數器更新,然後繼續執行。memoryBarrierBuffer()和memoryBarrierImage()函式會等待快取和影象變數的寫入操作完成。memoryBarrierShared()函式會等待帶有shared限定符的變數更新。這些函式可以對不同型別的記憶體訪問提供更為精細的控制和等待方法。舉例來說,如果正在使用原子計數器來實現快取變數的訪問,我們可能希望確保原子計數器的更新被通知到著色器的其它請求,但是不需要等待快取寫入操作本身完成,因為後者可能會花費更長的時間。此外,呼叫memoryBarrierAtomicCounter()允許著色器編譯器對快取變數的訪問進行重排序,而不會受到原子計數器操作的邏輯影響。

注意,就算是呼叫memoryBarrier()或者它的某個子函式,我們依然不能保證所有的請求都到達著色器的同一個位置。為了確保這一點,我們只有呼叫執行屏障函式barrier(),然後再讀取記憶體資料,而後者應該是在memoryBarrier()之前被寫入的。

記憶體屏障的使用,對於單一著色器請求中記憶體交換順序的確立來說並不是必需的。在著色器的某個請求中讀取變數的值總是會返回最後一次寫入這個變數的結果,無論編譯器是否對它們進行重排序操作。

我們介紹的最後一個函式叫做groupMemoryBarrier(),它等價於memoryBarrier(),但是它只能應用於同一個本地工作組的其它請求。而所有其它的屏障函式都是應用於全域性的。也就是說,它們會確保全域性工作組中的任何記憶體寫入請求都會在提交之後,再繼續執行程式。

本文摘自《OpenGL程式設計指南(原書第8版)》第12章:計算著色器,機械工業出版社出版。

最後是配套Sample原始碼:

/* $URL$
   $Rev$
   $Author$
   $Date$
   $Id$
 */

// #define USE_GL3W
#include <vermilion.h>

#include "vapp.h"
#include "vutils.h"
#include "vbm.h"

#include "vmath.h"

#include <stdio.h>

BEGIN_APP_DECLARATION(SimpleComputeShaderExample)
    // Override functions from base class
    virtual void Initialize(const char * title);
    virtual void Display(bool auto_redraw);
    virtual void Finalize(void);
    virtual void Reshape(int width, int height);

    // Member variables
    GLuint  compute_prog;
    GLuint  compute_shader;

    // Texture for compute shader to write into
    GLuint  output_image;

    // Program, vao and vbo to render a full screen quad
    GLuint  render_prog;
    GLuint  render_vao;
    GLuint  render_vbo;
END_APP_DECLARATION()

DEFINE_APP(SimpleComputeShaderExample, "Simple Compute Shader Example")

void SimpleComputeShaderExample::Initialize(const char * title)
{
    base::Initialize(title);

    // Initialize our compute program
    compute_prog = glCreateProgram();

    static const char compute_shader_source[] =
        "#version 430 core\n"
        "\n"
        "layout (local_size_x = 32, local_size_y = 16) in;\n"
        "\n"
        "layout (rgba32f) uniform image2D output_image;\n"
        "void main(void)\n"
        "{\n"
        "    imageStore(output_image,\n"
        "    ivec2(gl_GlobalInvocationID.xy),\n"
        "    vec4(vec2(gl_LocalInvocationID.xy) / vec2(gl_WorkGroupSize.xy), 0.0, 0.0));\n"
        "}\n"
    ;

    vglAttachShaderSource(compute_prog, GL_COMPUTE_SHADER, compute_shader_source);

    glLinkProgram(compute_prog);

    // This is the texture that the compute program will write into
    glGenTextures(1, &output_image);
    glBindTexture(GL_TEXTURE_2D, output_image);
    glTexStorage2D(GL_TEXTURE_2D, 8, GL_RGBA32F, 256, 256);

    // Now create a simple program to visualize the result
    render_prog = glCreateProgram();

    static const char render_vs[] =
        "#version 430 core\n"
        "\n"
        "in vec4 vert;\n"
        "\n"
        "void main(void)\n"
        "{\n"
        "    gl_Position = vert;\n"
        "}\n";

    static const char render_fs[] =
        "#version 430 core\n"
        "\n"
        "layout (location = 0) out vec4 color;\n"
        "\n"
        "uniform sampler2D output_image;\n"
        "\n"
        "void main(void)\n"
        "{\n"
        "    color = texture(output_image, vec2(gl_FragCoord.xy) / vec2(textureSize(output_image, 0)));\n"
        "}\n";

    vglAttachShaderSource(render_prog, GL_VERTEX_SHADER, render_vs);
    vglAttachShaderSource(render_prog, GL_FRAGMENT_SHADER, render_fs);

    glLinkProgram(render_prog);

    // This is the VAO containing the data to draw the quad (including its associated VBO)
    glGenVertexArrays(1, &render_vao);
    glBindVertexArray(render_vao);
    glEnableVertexAttribArray(0);
    glGenBuffers(1, &render_vbo);
    glBindBuffer(GL_ARRAY_BUFFER, render_vbo);
    static const float verts[] =
    {
        -1.0f, -1.0f, 0.5f, 1.0f,
         1.0f, -1.0f, 0.5f, 1.0f,
         1.0f,  1.0f, 0.5f, 1.0f,
        -1.0f,  1.0f, 0.5f, 1.0f,
    };
    glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, NULL);
}

void SimpleComputeShaderExample::Display(bool auto_redraw)
{
    // Activate the compute program and bind the output texture image
    glUseProgram(compute_prog);
    glBindImageTexture(0, output_image, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F);
    glDispatchCompute(8, 16, 1);

    // Now bind the texture for rendering _from_
    glBindTexture(GL_TEXTURE_2D, output_image);

    // Clear, select the rendering program and draw a full screen quad
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glUseProgram(render_prog);
    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

    base::Display();
}

void SimpleComputeShaderExample::Finalize(void)
{
    glUseProgram(0);
    glDeleteProgram(compute_prog);
    glDeleteProgram(render_prog);
    glDeleteTextures(1, &output_image);
    glDeleteVertexArrays(1, &render_vao);
}

void SimpleComputeShaderExample::Reshape(int width, int height)
{
    glViewport(0, 0, width, height);
}







相關文章