當前位置: 妍妍網 > 碼農

OpenGL 計算著色器(Compute Shader )你用過嗎?

2024-02-23碼農

Compute Shader 是 OpenGL ES(以及 OpenGL )中的一種 Shader 程式型別,用於在GPU上執行通用計算任務。

與傳統的頂點著色器和片段著色器不同,Compute Shader 被設計用於在 GPU 上執行各種通用計算任務,而不是僅僅處理圖形渲染。

Compute Shader 使用場景廣泛,除了影像處理之外,還可以用於物理模擬計算、數據加密解密、機器學習、光線追蹤等。

OpenGL ES 是 3.1 版本開始支持 Compute Shader (OpenGL 是 4.3 版本開始支持),引入表頭檔或者 import package 時需要註意下。

計算空間

使用者可以使用一個稱為工作群組的概念定義計算著色器正在執行的空間。這個空間是三維(x,y,z)的,使用者可以將任意維度設定為 1 ,以此在一維或二維空間中執行計算。

工作群組是使用者可以(從主機應用程式)執行的最小計算操作量,在計算著色器執行期間,工作群組順序可能會任意變化。

在下圖中,每個綠色立方體都是一個工作群組。

每個 Compute shader 都執行在單個工作單元上,這個工作單元稱為工作項 ,一個工作群組包含一個或多個工作項。

如下圖所示,一個工作群組可以在三維空間中被劃分成若幹個工作項。

工作項的劃分是在 Compute shader 中定義的,稱為本地空間或者局部尺寸 local_size(x,y,z)。

例如劃分一個工作群組對應的本地空間為(16,8,8):

#version 310 es
layout (local_size_x = 16, local_size_y = 8, local_size_z = 8) in;

如果設定工作群組空間為(100,100,1),使用局部空間(工作項)(16,8,8),則 Compute shader 將會被呼叫 100*100*1*16*8*8 = 10240000 次 ,每個呼叫可以都有唯一的標識 gl_GlobalInvocationID (三維向量)

工作群組的設定透過 glDispatchCompute 函式

voidglDispatchCompute(GLuint num_groups_x, GLuint num_groups_y, GLuint num_groups_z);

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

內建變量

Compute shader 沒有任何固定的輸入或輸出,除了一些內建的變量來告訴著色器它正在處理哪個計畫

in uvec3 gl_NumWorkGroups;

gl_NumWorkGroups 該變量包含傳遞給排程函式的工作群組數,glDispatchCompute() 設定

in uvec3 gl_WorkGroupID;

gl_WorkGroupID 表示此著色器呼叫的當前工作群組

in uvec3 gl_LocalInvocationID;

gl_LocalInvocationID 表示工作群組中著色器的當前呼叫

in uvec3 gl_GlobalInvocationID;

gl_GlobalInvocationID 表示當前執行單元在全域工作群組中的位置的一種有效的 3 維索引。數學計算表示

gl_GlobalInvocationID = (gl_WorkGroupID * gl_WorkGroupSize + gl_LocalInvocationID)

inuint gl_LocalInvocationIndex;

gl_LocalInvocationID 標識該呼叫在工作群組內的索引。數學計算表示

gl_LocalInvocationID =(gl_LocalInitationID.z * gl_WorkGroupSize.x * gl_WorkGroupSize.y + gl_LocalInspirationID.y * gl_WorkGroupSize.x + gl_LocalInitationID.x)

共享變量

計算著色器中的全域變量可以使用共享儲存限定符來聲明 。此類變量的值在工作群組內的所有呼叫之間共享

shared vec3 result;

如果一個變量被聲明為 shared,那麽它將被保存到特定的位置,從而對同一個本地工作群組內的所有 Compute shader 請求可見。

通常存取共享 shared 變量的效能會遠遠好於存取影像或者著色器儲存緩存(shader storage buffer)(例如主記憶體)的效能。

而這樣的資源環境是有限的,所以需要查詢和了解某個 Compute shader 程式的共享變量的最大數量。

要獲取這個限制值,可以呼叫 glGetIntegerv() 並設定 pname 為 GL_MAX_COMPUTE_SHARED_MEMORY_SIZE。

同步計算

barrier() 函式是 Compute shader 的同步函式,用於工作群組內呼叫同步。

當在 Compute shader 執行這個函式時,他將在這裏阻塞,直到同一原生的工作群組中所有其他 Compute shader 都呼叫到這個點的時候才繼續。

glMemoryBarrier(GLbitfield barriers)

glMemoryBarrier 定義對記憶體事務進行排序的屏障,主機端(CPU)呼叫。barriers 不同型別的作用參考:

https://registry.khronos.org/OpenGL-Refpages/gl4/html/glMemoryBarrier.xhtml

參數常用 GL_SHADER_STORAGE_BARRIER_BIT ,使用這個函式之後後續使用對應緩沖區的數據的時候,取到的數據必然是Barrier 之前就已經寫入的,實作一個強制同步的效果。

程式碼驗證

現在寫一個簡單的 demo ,申請一塊 2x4 大小的 buffer , 然後利用 Compute shader 進行簡單的平行計算,最後輸出計算結果。

Compute shader 指令碼如下:

#version 310 es
layout (local_size_x = 1, local_size_y = 1, local_size_z = 1) in;
layout (std430, binding = 0) buffer DataBuffer {
float data[];
} buffer1;
voidmain(){
ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
buffer1.data[pos.y * int(gl_NumWorkGroups.x) + pos.x] *float(pos.y);
}

其中 local_size 設定為 (1,1,1)。

編譯 Compute shader :

GLuint GLUtils::LoadComputeShader(constchar* computeShaderSource) {
GLuint computeShader = glCreateShader(GL_COMPUTE_SHADER);
glShaderSource(computeShader, 1, &computeShaderSource, NULL);
glCompileShader(computeShader);
GLint success;
glGetShaderiv(computeShader, GL_COMPILE_STATUS, &success);
if (!success) {
GLchar infoLog[512];
glGetShaderInfoLog(computeShader, 512NULL, infoLog);
LOGCATE("GLUtils::LoadComputeShader Compute shader compilation failed: %s", infoLog);
return0;
}
GLuint computeProgram = glCreateProgram();
glAttachShader(computeProgram, computeShader);
glLinkProgram(computeProgram);
glGetProgramiv(computeProgram, GL_LINK_STATUS, &success);
if (!success) {
GLchar infoLog[512];
glGetProgramInfoLog(computeProgram, 512NULL, infoLog);
LOGCATE("GLUtils::LoadComputeShader Compute shader linking failed: %s", infoLog);
return0;
}
glDeleteShader(computeShader);
return computeProgram;
}


申請一塊 2x4 大小的 buffer ,上傳初始數據並設定繫結點。

glGenBuffers(1, &m_DataBuffer);
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_DataBuffer);
float data[2][4] = {1.02.03.04.05.06.07.08.0};
glBufferData(GL_SHADER_STORAGE_BUFFER, sizeof(data), data, GL_DYNAMIC_COPY);
glBindBufferBase(GL_SHADER_STORAGE_BUFFER, 0, m_DataBuffer);//binding=0

執行 Compute shader 並打印計算結果

// Use the program object
glUseProgram (m_ProgramObj);
//2x4
int numGroupX = 2;
int numGroupY = 4;
glDispatchCompute(numGroupX, numGroupY, 1);
glMemoryBarrier(GL_SHADER_STORAGE_BARRIER_BIT);
// 讀取並打印處理後的數據
glBindBuffer(GL_SHADER_STORAGE_BUFFER, m_DataBuffer);
auto* mappedData = (float*)glMapBufferRange(GL_SHADER_STORAGE_BUFFER, 0, m_DataSize, GL_MAP_READ_BIT);
LOGCATE("ComputeShaderSample::Draw() Data after compute shader:\n");
for (int i = 0; i < m_DataSize/ sizeof(float); ++i) {
LOGCATE("ComputeShaderSample::Draw() => %f", mappedData[i]);
}
glUnmapBuffer(GL_SHADER_STORAGE_BUFFER);

輸出打印結果:

參考文章

https://learnopengl.com/Guest-Articles/2022/Compute-Shaders/Introduction
https://www.khronos.org/opengl/wiki/Compute_Shader

-- END --

進技術交流群, 掃碼添加我的微信:Byte-Flow

獲取相關資料和源碼

學習音視訊、OpenGL ES、Vulkan 、Metal、影像濾鏡、視訊特效及相關渲染技術的付費社群,面試指導,1v1 簡歷服務,職業規劃。

我的付費社群

推薦: