Tistory View

ComputeShader는 한개의 쉐이더만 있으면 된다. 다음은 ComputeShader를 컴파일하는 함수다.

GLuint CompileComputeShader( const char* szShaderCode  ) {
    GLuint rc = 0;

    int rvalue;
    GLchar log[1024];
    GLsizei length;

    GLuint program = glCreateProgram();
    GLuint shader = glCreateShader(GL_COMPUTE_SHADER);

    glShaderSource( shader, 1, &szShaderCode, nullptr );

    glCompileShader(shader);


    glGetShaderiv(shader, GL_COMPILE_STATUS, &rvalue );

    if (!rvalue) {
        length = sizeof(log);
        glGetShaderInfoLog( shader, sizeof(log), &length, log);
        LOGD( TAG, "Error: Compiler log:\n%s\n", log);
        goto last;
    }

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


    glGetProgramiv( program, GL_LINK_STATUS, &rvalue);

    if ( !rvalue ) {
        length = sizeof(log);
        glGetProgramInfoLog( program, sizeof(log), &length, log);
        LOGD( TAG, "Error: Linker log:\n%s\n", log);
        goto last;
    }

    rc = program;
    program = 0;

last:
    if( program ) {
        glDeleteProgram( program );
    }

    if( shader ) {
        glDeleteShader( shader );
    }

    return rc;
}

파라미터로 쉐이더코드를 넘겨주면 알아서 쉐이더 프로그램을 만들어 준다.

 

다음은 우리가 초등학교 때 혼나기 싫어서 신나게 외웠던 구구단을 만들어내는 쉐이더 코드다.

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

layout(binding = 0 ) writeonly buffer Ninenine {
    uvec4 e[];
} ninenine;

void main() {
    uint idx = gl_WorkGroupID.y * gl_NumWorkGroups.x + gl_WorkGroupID.x;
    ninenine.e[idx] = uvec4( gl_WorkGroupID.y, gl_WorkGroupID.x, gl_WorkGroupID.y * gl_WorkGroupID.x, idx );
}

 

마지막으로 구구단을 GPU에서 처리하고 출력하는 코드다.

struct vec4 {
 GLuint x;
 GLuint y;
 GLuint z;
 GLuint w;
};

const char* shaderCode = "~~~~~구구단 쉐이더 코드~~~~~";

const int MAX_DAN = 10; // 0 ~ 9
const int MAX_OPR = 10; // 0 ~ 9

const int NINExNINE_SIZE = MAX_DAN * MAX_OPR * sizeof( vec4 );

GLuint prg  = 0;
GLuint ssbo = 0;
vec4* pBuf  = nullptr;

char buf[32];
std::string res;

if( !prg ) {
    prg = CompileComputeShader(shaderCode);
}

if( !ssbo ) {
    glGenBuffers( 1, &ssbo );
    assert( ssbo );
    glBindBuffer( GL_SHADER_STORAGE_BUFFER, ssbo );
    glBufferData( GL_SHADER_STORAGE_BUFFER, NINExNINE_SIZE, nullptr, GL_STREAM_DRAW );
    glBindBuffer( GL_SHADER_STORAGE_BUFFER, 0 );
}

if( prg ) {
    glUseProgram( prg );
    CGles2::BindBufferBase( GL_SHADER_STORAGE_BUFFER, 0, ssbo );
    CGles2::DispatchCompute(MAX_OPR, MAX_DAN, 1 );
    glUseProgram( 0 );


glMemoryBarrier( GL_ALL_BARRIER_BITS ); // 필요없나???

glBindBuffer( GL_SHADER_STORAGE_BUFFER, ssbo );
pBuf = (vec4*)glMapBufferRange( GL_SHADER_STORAGE_BUFFER, 0, NINExNINE_SIZE, GL_MAP_READ_BIT );

if( pBuf ) {
        for(int i = 0 ; i < MAX_OPR ; i++ ) {
            for(int j = 0 ; j < MAX_DAN ; j++ ) {
                sprintf( buf, "[%3d]%2dx%2d=%2d ",
                      (int)pBuf[j * MAX_OPR + i ].w,
                      (int)pBuf[j * MAX_OPR + i ].x,
                      (int)pBuf[j * MAX_OPR + i ].y,
                      (int)pBuf[j * MAX_OPR + i ].z
                );
                res += buf;
            }
            LOGD( TAG, "\n%s", res.c_str() );
            res.clear();
        }
    } else {
        res = "MapBufferRange Failure";
    }
} else {
    res = "nine x nine program not exists";
}

if( pBuf ) {
    glUnmapBuffer( GL_SHADER_STORAGE_BUFFER );
    pBuf = nullptr;
}

 

 

이제 하나하나 뜯어 보자.

 

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

3.1버전임을 명시하고, local_size를 지정한다. local_size는 이전 글에서 말한 WorkGroup내의 Thread수를 지정하는 것이다. 워크그룹내의 총 쓰레드수는 local_size_x * local_size_y * local_size_z 이렇게 계산 된다.

이 구구단예제는 Workgroup내에 1개의 Thread만을 사용하고 있다.

 

layout(binding = 0 ) writeonly buffer Ninenine {
    uvec4 e[];
} ninenine;

구구단의 결과를 저장할 버퍼를 지정하는 것이다. 이는 C코드에서 다음 부분에 해당한다.

glBindBufferBase( GL_SHADER_STORAGE_BUFFER, 0, ssbo );

두번째 파라미터인 "0"이 binding=0에 해당하는 값이다.

접근은 ninenine.e[인덱스]로 접근을 하면 된다.

 

void main() {
    uint idx = gl_WorkGroupID.y * gl_NumWorkGroups.x + gl_WorkGroupID.x;
    ninenine.e[idx] = uvec4( gl_WorkGroupID.y, gl_WorkGroupID.x, gl_WorkGroupID.y * gl_WorkGroupID.x, idx );
}

실제 쉐이더의 Kernel코드는 main함수로 지정한다.

ninenine버퍼에 해당하는 인덱스를 구하고 구구단을 만들어 저장을 한다.

 

gl_WorkGroupID는 C코드의 glDispatchCompute에서 넘긴 값에 해당하는 WorkGroup의 ID이다.

C코드에서 glDispatchCompute( 10, 10, 1 )로 호출하게 되는 데, 넘기는 값은 호출할 개수라

 

gl_WorkGroupID.x = 0~9의 값을 가지게 되고(y도 마찬가지)

gl_WorkGroupID.z = 0~0의 값을 가지게 된다.

 

gl_NumWorkGroups가 glDispatchCompute에 넘긴 x=10, y=10, z=1을 가지고 있는 변수다.

 

이제 예제를 하나 해 봤으니 자세한 설명을 하겠다.

 

 

"WorkGroup내의 Thread 수"와 "WorkGroup수" 지정

WorkGroup내의 Thread 수는 아예 Shader코드안에 다음과 같이 들어간다.

layout(local_size_x = 8, local_size_y = 8, local_size_z = 1 ) in;

위 코드는 Thread수를 8x8x1=64개를 사용하겠다는 의미다.

3차원방식이라 x,y,z를 따로 지정할 수 있고, x,y의 최대값은 GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS으로 확인 할 수 있다. z값은 GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 값의 반이다. GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS는 최소 128이다.

 

GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS값이 128이라면

 

local_size_x : 1 ~ 128

local_size_y : 1 ~ 128

local_size_z : 1 ~ 64

 

까지만 쓸 수 있다.

 

또한 이 GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS값은 Workgroup내의 전체 Thread수를 나타내는 것이므로

local_size_x * local_size_y * local_size_z 또한 GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS 값을 넘으면 안된다.

 

local_size_x * local_size_y * local_size_z <= GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS

 

local_size_*값이 1인경우는 그냥 생락할 수도 있다.[하지만 최소한 아무 것이나 한개는 지정을 해줘야 할 듯]

따라서 위의 코드는 다음과 같이 바꿀 수 있다.

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

 

WorkGroup수는 glDispatchCompute의 파라미터로 넘긴다.

glDispatchCompute( 8, 9, 10 );

총 WorkGroup수는 [8 * 9 * 10 = 720]번 이 되고 WorkGroup

내의 Thread는 [8*8*1=64]번이 된다.

위와 같이 호출을 하면 총 Thread수는 다음과 같다.

( 8 * 9 * 10 ) * ( 8 * 8 * 1 ) = 46080

 

혹시나 하나라도 0넣었다가는 하나도 실행 안된다.

 

 

쉐이더 내에서 어떤 WorkGroup에 속한 어떤 Thread인지 구분하기 위해 기본적으로 여러 변수를 넣어놨다.

uvec3 gl_NumWorkGroups WorkGroup수 glDispatch의 파라미터에 해당하는 값 예에서 xyz=>8, 9, 10
uvec3 gl_WorkGroupSize WorkGroup내의 Thread수 예에서 xyz=>8, 8, 1
uvec3 gl_LocalInvocationID WorkGroup내의 Thread ID 값 예에서
x : 0~7[8]
y : 0~7[8]
z : 0~0[1]
uvec3 gl_WorkGroupID WorkGroup ID 예에서
x : 0~7[8]
y : 0~8[9]
z : 0~9[10]
uint gl_LocalInvocationIndex WorkGroup내의 Thread ID값을 1차원값(단순히 0~최대값) 예에서 0~63(8x8)
uvec3 gl_GlobalInvocationID 전체의 ID값
gl_WorkGroupID * gl_WorkGroupSize + gl_LocalInvocationID을 계산해 놓은 값
예에서
(0~7) x  8 + (0~7) = ( 0 ~ 63)
(0~8) x  8 + (0~7) = ( 0 ~ 71)
(0~9) x  1 + (0~0) = ( 0 ~   9)
이거 테스트 안함

이렇게 어떤 Id에 해당하는 지를 구할 수 있다.

 

전체ID값의 1차원형은 지원해 주지 않는 것 같다. 이 계산은 독자들의 숙제로 남겨 두겠다.

 

 

Replies
Reply Write