GPGPU基础(五):使用compute shader进行通用计算及示例

1.工作组及其执行

compute shader是在OpenGL4.3(Opengl es 3.1)以后引入的一种专门用于并行计算的着色器。在计算着色器中,任务以组为单位进行执行,我们称之为工作组(work group)。拥有邻居的工作组被称为本地工作组(local workgroup), 这些组可以组成更大的组,称为全局工作组(global workgroup),而其通常作为执行命令的一个单位。

计算着色器会被每个本地工作组中的每个单元调用一次。工作组的每一个单元称为工作项(work item),每次调用称为一次执行。执行的单元之间可以通过变量和显存进行通信,且可以通过执行同步操作保持一致性。图12-1显示了一个全局工作组。这个全局工作组包括16个本地工作组,每个本地工作组又包括16个执行单元,排成4X4的网格,每个执行单元拥有一个二维向量表示的索引值。尽管图示中,全局和本地工作组都是2维的,而事实上它们都是3维的,为了适应1维、2维的任务,只需把额外的2维或1维设为0即可。计算着色器的每个执行单元本质是相互独立的,可以并行地在支持OpenGL地GPU上执行。


大部分OpenGL硬件会将这些执行单元打包成较小地集合(lockstep),然后将这些小集合拼成本地工作组。本地工作组的大小在计算着色器的代码中输入布局限定符莱设置。全局工作组的大小则是本地工作组大小的整数倍。当计算着色器执行时,它可以通过内置变量来知道当前在本地工作组中的相对坐标、本地工作组的大小,及本地工作组在全局工作组中的相对坐标。基于这些还能进一步获得执行单元在全局工作组红的坐标。着色器根据这些变量来决定应该负责计算任务中的哪些部分,同时也能知道一个工作组中的其他执行单元,以便共享数据。

通过布局限定符在计算着色器中声明本地工作组的大小,分别使用local_size_x,local_size_y,local_size_z,它们的默认值为1.如忽略local_size_z,就会创建一个NXM的2维组。如声明一个本地工作组大小为16x16的着色器。

#version 430 core
layout (local_size_x = 16, local_size_y = 16) in;
void main(void){
    ...
}

当创建并链接一个计算着色器后,就可以通过glUseProgram将它设置为当前要执行的程序,然后用glDispatchCompute将工作组发送到计算管线上。

void glDispatchCompute(GLuint num_group_x, GLuint num_group_y, GLuint num_group_z);
在3个维度上分发执行计算工作组,num_group_x、num_group_y和num_group_z分别设置工作组在X、Y、Z维度上的数量。
每个参数均需大于0,小于或等于一个与设备相关的常量数组GL_MAX_COMPUTE_WORK_GROUP_SIZE的对应元素。

2.知道工作组的位置

当执行计算着色器时,它可能需要对输入数组的多个单元赋值,或者需要读取输入数组的特定位置的数据。因此计算着色器需要知道当前处于本地和全局工作组的具体位置。这是坐标是通过OpenGL的一组内置变量获得的。

  • gl_WorkGroupSize是一个用于存储本地工作组大小的常数
  • gl_NumWorkGroups是一个向量,它包含传给glDispatchCompute的参数(num_group_x、num_group_y、num_group_z)
  • gl_LocalInvocationID表示当前执行单元在本地工作组中的位置。它的范围从uvec3(0)到gl_WorkGroupSize-uvec3(1)
  • gl_WorkGroupID表示当前本地工作组在全局工作组中的位置。该变量的范围在uvec3(0)和gl_NumWorkGroups-uvec3(1)之间
  • gl_GlobalInvocationID由gl_LocalInvocationID、gl_WorkGroupSize和gl_WorkGroupID派生而来。它的准确值是gl_WorkGroupID *gl_WorkGroupSize + gl_LocalInvocationID,所以它是当前执行单元在全局工作组中的位置的一种有效的3维索引。
  • 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维的数据。

3.通信

在计算着色器中可以使用shared关键字来声明着色器变量,其格式与其他关键字uniform等类似。

//一个共享的无符号整型变量
shared uint foo;
//一个共享的向量数组
shared vec4 bar[128];
//一个共享的数据块
shared struct baz_struct{
    vec4 a_vector;
    int an_integer;
    ivec2 an_array_of_integers[27];
}baz[42];
一个变量被声明维shared,那么它将被保存到特定的位置,从而对同一个本地工作组内所有计算着色器可见。如果某个计算着色器请求对共享变量进行写入,那么这个数据的修改信息将最终通知给同一个本地工作组的所有着色器。通常访问共享shared变量的性能会远好于访问图像或者着色器存储缓存(如主内存)的性能。因为着色器会将共享内存作为局部量处理,并且可以在设备中进行拷贝,所以访问共享变量可能比使用缓冲区的方法更迅速。因此,如果着色器需要对同一处内存进行大量的访问,优先考虑将内存拷贝到共享变量中,然后操作。

4.同步

如果本地工作组请求的执行顺序,以及全局工作组中所有本地工作组的执行顺序都没有定义,那么请求执行操作的时机与其他请求完全无关。如果请求之间不需要相互通信,只需要完全独立执行,那么没有问题。但如果请求之间需要进行通信,无论是通过图像,缓存还是共享内存,那么我们就有必要对它们的操作进行同步处理。

同步命令有两种。首先是运行屏障(execution barrier),可以通过barrier()函数触发。如果计算着色器的一个请求遇到barrier,那么它会停止运行,等待同一个本地工作组的所有请求也到达barrier,然后才会执行后面的代码。

第二种同步叫做内存屏障(memory barrier)。它的最直接版本就是memoryBarrier()。如果调用memoryBarrier,那么久可以保证着色器请求内存的写入操作一定是提交到内存端,而不是通过缓冲区(cache)或者调度队列之类的方式。所有发生在memoryBarrier之后的操作在读取同一处内存时,都可以使用这些内存写入的结果,即使同一个计算着色器的其他请求也是如此。

5.例子

本例运行环境为ubuntu(非虚拟机,虚拟机一运行compute shader相关的代码就报 exit code 139),需要先安装好OpenGL环境,GLUT和GLEW。在这个例子中,会先产生一个 1024 个数据,并将这些数据赋予一个 16 x 16 x 4 的image2D,然后通过计算着色器对这个image2D的每个坐标点的4个通道分别加上0、1、2、3,最后将计算结果读取出来。

首先初始化GL环境

void initGLUT(int argc, char **argv) {
    glutInit(&argc, argv);
    glutWindowHandle = glutCreateWindow("GPGPU Tutorial");
}

创建FBO

void initFBO() {
    // create FBO (off-screen framebuffer)
    glGenFramebuffers(1, &fb);
    // bind offscreen framebuffer (that is, skip the window-specific render target)
    glBindFramebuffer(GL_FRAMEBUFFER, fb);
}

创建texture

/**
 * create textures and set proper viewport etc.
 */
void createTextures() {
    glGenTextures(1, &outputTexID);
    glGenTextures(1, &intermediateTexID);
    glGenTextures(1, &inputTexID);
    // set up textures
    setupTexture(outputTexID);
    setupTexture(intermediateTexID);
    setupTexture(inputTexID);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, textureParameters.texTarget, inputTexID, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, textureParameters.texTarget, intermediateTexID, 0);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, textureParameters.texTarget, outputTexID, 0);
    transferToTexture(pfInput, inputTexID);
    // set texenv mode
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
}

/**
 * Sets up a floating point texture with the NEAREST filtering.
 */
void setupTexture(const GLuint texID) {
    // make active and bind
    glBindTexture(textureParameters.texTarget, texID);
    glTexStorage2D(GL_TEXTURE_2D, 8, GL_RGBA32F, 16, 16);
    // turn off filtering and wrap modes
    glTexParameteri(textureParameters.texTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(textureParameters.texTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(textureParameters.texTarget, GL_TEXTURE_WRAP_S, GL_CLAMP);
    glTexParameteri(textureParameters.texTarget, GL_TEXTURE_WRAP_T, GL_CLAMP);
}
注意:不要用 glTexImage2D生成纹理。

A very important restriction for using shader images is that the underlying texture must have been allocated using "immutable" storage, i.e. via glTexStorage*()-like functions, and not glTexImage2D().

将数据赋予inputTexID

void transferToTexture(float *data, GLuint texID) {
    glBindTexture(textureParameters.texTarget, texID);
    glTexSubImage2D(textureParameters.texTarget, 0, 0, 0, unWidth, unHeight, textureParameters.texFormat, GL_FLOAT, data);
}

执行计算

void performCompute(const GLuint inputTexID, const GLuint outputTexID) {
    // enable GLSL program
    glUseProgram(glslProgram);
    glUniform1fv(glGetUniformLocation(glslProgram, "v"), 4, v);

    // Synchronize for the timing reason.
    glFinish();

    CTimer timer;
    long lTime;
    timer.reset();

    glBindImageTexture(0, inputTexID, 0, GL_FALSE, 0, GL_READ_ONLY, GL_RGBA32F);
    glBindImageTexture(1, outputTexID, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F);
    glDispatchCompute(1, 1, 1);

    glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);

    glFinish();
    lTime = timer.getTime();
    cout << "Time elapsed: " << lTime << " ms." << endl;
}

compute shader代码

#version 430 core

layout (local_size_x = 16, local_size_y = 16) in;

// 传递卷积核
uniform float v[4];

layout (rgba32f, binding = 0) uniform image2D input_image;
layout (rgba32f, binding = 1) uniform image2D output_image;

shared vec4 scanline[16][16];

void main(void)
{
    ivec2 pos = ivec2(gl_GlobalInvocationID.xy);
    scanline[pos.x][pos.y] = imageLoad(input_image, pos);
    barrier();
    vec4 data = scanline[pos.x][pos.y];
    data.r = v[0] + data.r;
    data.g = v[1] + data.g;
    data.b = v[2] + data.b;
    data.a = v[3] + data.a;
    imageStore(output_image, pos.xy, data);
}

读回数据

void transferFromTexture(float *data) {
    glReadBuffer(GL_COLOR_ATTACHMENT2);
    glReadPixels(0, 0, unWidth, unHeight, textureParameters.texFormat, GL_FLOAT, data);
}

主流程代码

#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <GL/glew.h>
#include <GL/glut.h>
#include "utils/CReader.h"
#include "utils/CTimer.h"

#define WIDTH    16    //data block width
#define HEIGHT    16    //data block height

using namespace std;

void initGLSL(GLenum type);
void initFBO();
void initGLUT(int argc, char **argv);
void createTextures(void);
void setupTexture(const GLuint texID);
void performCompute(const GLuint inputTexID, const GLuint outputTexID);
void transferFromTexture(float *data);
void transferToTexture(float *data, GLuint texID);

// 纹理标识
GLuint outputTexID;
GLuint intermediateTexID;
GLuint inputTexID;

// GLSL 变量
GLuint glslProgram;
GLuint fragmentShader;

// FBO 标识
GLuint fb;

// 提供GL环境
GLuint glutWindowHandle;

struct structTextureParameters {
    GLenum texTarget;
    GLenum texInternalFormat;
    GLenum texFormat;
    char *shader_source;
} textureParameters;

float *pfInput;
unsigned unWidth = (unsigned) WIDTH;
unsigned unHeight = (unsigned) HEIGHT;
unsigned unSize = unWidth * unHeight;

GLfloat v[4];  // 传如compute shader中

int main(int argc, char **argv) {
    int i;
    // 创建测试数据
    unsigned unNoData = 4 * unSize;        //total number of Data
    pfInput = new float[unNoData];
    float *pfOutput = new float[unNoData];
    for (i = 0; i < unNoData; i++) pfInput[i] = i * 0.001f;
    for (i = 0; i < 4; i++) {
        v[i] = i;
    }

    // create variables for GL
    textureParameters.texTarget = GL_TEXTURE_2D;
    textureParameters.texInternalFormat = GL_RGBA32F;
    textureParameters.texFormat = GL_RGBA;
    CReader reader;

    initGLUT(argc, argv);
    glewInit();

    initFBO();
    createTextures();

    char c_convolution[] = "../convolution.cs";
    textureParameters.shader_source = reader.textFileRead(c_convolution);
    initGLSL(GL_COMPUTE_SHADER);
    performCompute(inputTexID, intermediateTexID);

    performCompute(intermediateTexID, outputTexID);

    // get GPU results
    transferFromTexture(pfOutput);

    for (int i = 0; i < unNoData; i++) {
        cout << "input:" << pfInput[i] << " output:" << pfOutput[i] << endl;
    }

    // clean up
    glDetachShader(glslProgram, fragmentShader);
    glDeleteShader(fragmentShader);
    glDeleteProgram(glslProgram);
    glDeleteFramebuffersEXT(1, &fb);
    glDeleteTextures(1, &inputTexID);
    glDeleteTextures(1, &outputTexID);
    glutDestroyWindow(glutWindowHandle);

    // exit
    delete pfInput;
    delete pfOutput;
    return EXIT_SUCCESS;
}

/**
 * Set up the GLSL runtime and creates shader.
 */
void initGLSL(GLenum type) {
    // create program object
    glslProgram = glCreateProgram();
    // create shader object (fragment shader)
    fragmentShader = glCreateShader(type);
    // set source for shader
    const GLchar *source = textureParameters.shader_source;
    glShaderSource(fragmentShader, 1, &source, nullptr);
    // compile shader
    glCompileShader(fragmentShader);

    // attach shader to program
    glAttachShader(glslProgram, fragmentShader);
    // link into full program, use fixed function vertex shader.
    // you can also link a pass-through vertex shader.
    glLinkProgram(glslProgram);

}
全部代码


  • 5
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
OpenGL Compute Shader是OpenGL 4.3引入的一种新型Shader,它主要用于通用计算GPGPU)任务而不是图形渲染。它可以在GPU上执行高度并行的计算任务,例如物理模拟、图像处理和机器学习等。Compute Shader与其他Shader不同之处在于它没有渲染管道的输入和输出,并且可以从CPU上下文中直接调用。它还可以读写各种类型的缓冲区和纹理,使其非常灵活和强大。 Compute Shader使用步骤如下: 1.创建Compute Shader对象:使用glCreateShader函数创建Compute Shader对象。 2.编译Compute Shader代码:使用glShaderSource和glCompileShader函数将Compute Shader代码编译为OpenGL可识别的二进制格式。 3.创建Compute Program对象:使用glCreateProgram函数创建Compute Program对象。 4.将Compute Shader附加到Compute Program对象上:使用glAttachShader函数将Compute Shader附加到Compute Program对象上。 5.链接Compute Program对象:使用glLinkProgram函数将Compute Program对象链接到OpenGL渲染管道。 6.使用Compute Shader使用glUseProgram函数激活Compute Program对象,并通过glDispatchCompute函数调用Compute Shader。 7.清理资源:使用glDeleteShader和glDeleteProgram函数删除Compute ShaderCompute Program对象。 下面是一个简单的Compute Shader示例代码: ```glsl #version 430 layout(local_size_x = 16, local_size_y = 16) in; layout(std430, binding = 0) buffer InputBuffer { float data[]; }; layout(std430, binding = 1) buffer OutputBuffer { float result[]; }; void main() { uint idx = gl_GlobalInvocationID.x + gl_GlobalInvocationID.y * gl_NumWorkGroups.x * gl_WorkGroupSize.x; result[idx] = data[idx] * data[idx]; } ``` 这个Compute Shader使用输入缓冲区和输出缓冲区,对输入缓冲区的每个元素进行平方运算,并将结果存储在输出缓冲区中。在主函数中,使用gl_GlobalInvocationID获取全局线程ID,计算出要处理的输入元素的索引,并在输出缓冲区中存储计算结果。 最后,通过调用glDispatchCompute函数启动Compute Shader。该函数需要指定调度的工作组数量,以及每个工作组中线程的数量。在这个示例中,我们使用16x16的工作组,并将其应用于输入缓冲区的所有元素。 ```c++ glDispatchCompute(numGroupsX, numGroupsY, numGroupsZ); ``` 以上就是OpenGL Compute Shader的原理与使用方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值