개발/OpenGL / / 2022. 3. 10. 13:41

OpenGL 공부일지 - OpenGL Super Bible 쉐이더 스토리지 블록, 어토믹 카운터

반응형

Shader Storage Blocks

쉐이더 스토리지 블록으로 쉐이더에서 버퍼 객체를 쓰는 공간으로 쓸 수 있다. 유니폼 블록과 다른 점은 쉐이더가 쉐이더 스토리지에 쓸 수 있다는 점이다.

쉐이더 스토리지의 선언 방식은 buffer지시어를 사용한다. 패킹 레이아웃 지시어 std140에도, std430에도 지원한다. 

#version 450 core

struct my_structure
{
    int pea;
    int carrot;
    vec4 potato;
};

layout (binding = 0, std430) buffer my_storage_block
{
    vec4 foo;
    vec3 bar;
    int baz[24];
    my_structure veggies;
};

위와 같은 방식으로 쉐이더 스토리지 블록을 선언한다.

쉐이더 스토리지 블록 멤버는 일반 변수 참조하듯 참조할 수 있고, glBufferData 등으로 버퍼에 데이터를 전달할 수도 있다. 유니폼블록과 비슷하다.

유니폼에 비해 명시적인 크기의 상한선이 없고, std430의 새로운 패킹 규칙을 통해 더 효율적으로 패킹된다. 이는 유니폼 블록이 정렬이 더 세세하고 최소 크기가 작아서이다. 

#version 450 core
struct vertex
{
    vec4 position;
    vec3 color;
};

layout (binding = 0, std430) buffer my_vertices
{
	vertex vertices[];
};

uniform mat4 transform_matrix;

out VS_OUT
{
	vec3 color;
} vs_out;

void main(void)
{
    gl_Position = transform_matrix * vertices[gl_VertexID].position;
    vs_out.color = vertices[gl_VertexID].color;
}

위 코드는 버텍스 쉐이더를 일반 입력이 아닌 쉐이더 스토리지 블록으로 사용하는 예시이다.

쉐이더 스토리지 블록이 유니폼 블록에 비해 융통성이 있긴 하나 opengl에서 최적화하기 어려울 수도 있다.

Atomic Memory Operations

어토믹 연산은 메모리 읽기와 쓰기 중간에 인터럽트가 일어나지 않아 결과가 보장되는 것이다.

만약 쉐이더 호출이 m = m + 1 연산을 수행할 때, m이라는 동일한 메모리 위치의 값을 사용한다. 호출을 하면 m의 메모리 위치에 저장된 현재 값을 읽은 후 연산을 수행해서 1 증가시킨다.

 이러한 호출이 동시에 일어나면 값이 꼬일 수 있다. 2번 호출하였는데 동시에 실행하여 동일한 값을 읽고 +1의 결과만 저장되는 것이다. 이러한 문제 때문에 어토믹 연산이 필요하다. 

 

 어토믹 연산으로 한 호출의 읽기-수정-쓰기 사이클이 완료되어야 다른 호출이 동작하게 한다. 즉 각 호출을 직렬화 시켜 한 번에 하나씩만 수행한다.

위오 같은 종류의 어토믹 메모리 함수가 있다.

Synchronizing Access to Memory

쉐이더에서 버퍼를 읽는거는 언제든지 상관없지만, 쉐이더에서 버퍼 객체에 쓸 때는 쉐이더 스토리지 블록에 데이터를 쓰든 메모리에 쓰는 어토믹 연산을 명시적으로 호출하든 처리하지 않으면 문제가 발생할 수 있다.

  • Read-After-Write (RAW) 
    메모리 위치에 쓰고 읽기를 시도하는 경우 쓰기가 완료되기 전에 읽기가 수행될 염려가 있다.
  • Write-After-Write (WAW)
    처음 쓴 내용이 다음 쓴 내용으로 덮어지지 않고 남아있을 우려가 있다.
  • Write-After-Read (WAR)
    병렬 처리 시스템에서 스레드 실행시 다른 스레드의 쓰기 이후에 읽고 가져와야하는데, 두 번째 스레드가 이미 덮어 쓴 데이터를 가져올 수 있다.

memory barrier와 같은 방법으로 이러한 순서 조정 및 병렬화 문제를 피하려고 한다.

Using Barriers in Your Application

void glMemoryBarrier(GLbitfield barriers);

위와 같은 베리어 추가 함수를 이용하여 메모리 배리어를 설정한다.

인자를 통해 베리어 설정 범위를 결정한다. 

  • GL_SHADER_STORAGE_BARRIER_BIT : 베리어 이전 수행된 접근을 쉐이더가 데이터를 액세스하기 전에 모두 완료하도록 말한다.
  • GL_UNIFORM_BARRIER_BIT : 데이터를 사용한 메모리가 베리어 이후 유니폼 버퍼로 사용되기 위해 버퍼에 쓰는 쉐이더가 작업을 완료할 때까지 기다렸다가 쉐이더를 수행하라 명령.
  • GL_VERTEX_ATTRIB_ARRAY_BARRIER_BIT : 버텍스 속성으로 버텍스 데이터 원본 버퍼 사용 전, 쉐이더들의 작업 완료를 기다리라고 전달.

베리어 설정을 위한 위와 같은 옵션들이 있다. 중요한 것은 베리어가 적용되지 전에 갱신한 데이터를 믿을 수 없다는 것이다.

Using Barriers in Your Shaders

베리어로 쉐이더가 수행하는 메모리 액세스 순서를 조정한 것과 같이, 쉐이더에 베리어를 추가하면 opengl이 메모리 읽기 쓰기순서를 조정하지 못하게 설정하는 것도 된다. 

void memoryBarrier();

위는 GLSL의 메모리 베리어 함수이다. 호출시 기존 메모리 읽기 쓰기는 위 함수의 반환 전으로 모두 완료된다. 

함수 호출 시점으로부터 쓴 데이터는 읽어도 안전하다는 의미이다. 만약 이 베리어가 없다면 새로 쓴 메모리의 위치를 읽어도 이전 값이 반환될 수 있다.

 memoryBarrier는 더 세분화된 버전을 제공한다. 

 

Atomic Counters

어토믹 카운터는 여러 쉐이더 호출에 걸쳐 공유되는 스토리지를 표현하는 특별한 변수 타입이다. 이 스토리지는 버퍼 객체에 의해서 채워진다. GLSL의 함수들은 버퍼의 값을 증가 혹은 감소하는 연산이 최소단위 즉 어토믹하다. 

어토믹 단위이므로 두 개 이상의 호출을 실행하면 opengl에서는 차례대로 수행하지만, 연산의 수행순서는 보장되지 않는다. 

layout (binding = 0) uniform atomic_uint my_variable;

위와 같이 어토믹 카운터를 선언한다.

opengl에서는 어토믹 카운터 값을 저장할 버퍼를 바인딩할 수 있는 여러 바인딩 포인트를 제공한다. 또한 어토믹 카운터를 버퍼 객체의 특정 오프셋 위치에 저장할 수 있다. 인덱스 및 오프셋은 binding 및 offset 레이아웃 지시어로 설정 가능하고, 레이아웃 지시어는 어토믹 카운트를 유니폼 선언에서 설정한다. 

layout (binding = 3, offset = 8) uniform atomic_uint my_variable;

위 코드는 어토믹 카운터 바인딩 포인트 3에 바인딩되어있고, 버퍼 내 오프셋 8의 위치에 있다는 코드이다.

// Generate a buffer name
GLuint buf;
glGenBuffers(1, &buf);

// Bind it to the generic GL_ATOMIC_COUNTER_BUFFER target and
// initialize its storage
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buf);
glBufferData(GL_ATOMIC_COUNTER_BUFFER, 16 * sizeof(GLuint),
    NULL, GL_DYNAMIC_COPY);

// Now bind it to the fourth indexed atomic counter buffer target
glBindBufferBase(GL_ATOMIC_COUNTER_BUFFER, 3, buf);

위 코드는 어토믹 카운터에 스토리지를 제공하기 위해 버퍼 객체를 GL_ATOMIC_COUNTER_BUFFER 인덱스 바인딩 포인트에 바인딩한 코드이다.

// Bind our buffer to the generic atomic counter buffer
// binding point
glBindBuffer(GL_ATOMIC_COUNTER_BUFFER, buf);

// Method 1 - use glBufferSubData to reset an atomic counter
const GLuint zero = 0;
glBufferSubData(GL_ATOMIC_COUNTER_BUFFER, 2 * sizeof(GLuint),
	sizeof(GLuint), &zero);

// Method 2 - Map the buffer and write the value directly into it
GLuint * data =
	(GLuint *)glMapBufferRange(GL_ATOMIC_COUNTER_BUFFER,
	0, 16 * sizeof(GLuint),
	GL_MAP_WRITE_BIT |
	GL_MAP_INVALIDATE_RANGE_BIT);
data[2] = 0;
glUnmapBuffer(GL_ATOMIC_COUNTER_BUFFER);

// Method 3 - use glClearBufferSubData
glClearBufferSubData(GL_ATOMIC_COUNTER_BUFFER,
	GL_R32UI,
	2 * sizeof(GLuint),
	sizeof(GLuint),
	GL_RED_INTEGER, GL_UNSIGNED_INT,
	&zero);

쉐이더에서 어토믹 카운터를 사용하려면 리셋하는 것이 좋다. 위 코드를 보자.

method 1 : 리셋을 위해서 변수의 주소를 glBufferSubData에 넘긴다.

method 2 : glMapBufferRange로 버퍼를 매핑하여 직접 값을 쓴다.

method 3 : glClearBufferSubData를 사용하는 방법도 있다.

버퍼를 만들어 어토믹 카운터 버퍼 타깃에 바인딩 하였고, 쉐이더에 어토믹 카운터 유니폼을 선언하였다.

uint atomicCounterIncrement(atomic_uint c);

위 함수를 호출하여 어토믹 카운터를 증가시킨다. 

uint atomicCounterDecrement(atomic_uint c);

위 함수로 어토믹 카운터를 감소시킨다. 

uint atomicCounter(atomic_uint c);

위 함수로 어토믹 카운터의 값을 얻을 수 있다.

// eg5-31
#version 450 core

layout (binding = 0, offset = 0) uniform atomic_uint area;

void main(void)
{
	atomicCounterIncrement(area);
}

매 실행마다 어토믹 카운터를 증가시키는 프래그먼트 쉐이더이다. 이 것으로 렌더링되는 객체들의 화면 공간 영역을 어토믹 카운터를 사용해서 표시한다.

위 쉐이더는 out으로 선언된 출력이 없어, 프레임버퍼에 아무 데이터도 쓰지 않는다. 이를 실행하는 동안에는 프레임버퍼에 쓰기를 비활성화 하고자 할때는

glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);

위의 glColorMask의 인자로 false를 주고, 다시 활성화하려면 true를 준다.

어토믹 카운터를 다른 버퍼 타깃에 바인딩해서 쉐이더에서 값을 얻는 것이 가능하다.

// eg5-32
#version 450 core

layout (binding = 0) uniform area_block
{
	uint counter_value;
};

out vec4 color;

uniform float max_area;

void main(void)
{
    float brightness = clamp(float(counter_value) / max_area,
    0.0, 1.0);
    color = vec4(brightness, brightness, brightness, 1.0);
}

위 코드는 어토믹 카운터의 값으로 쉐이더의 수행을 제어하는 예제이다. 

위의 5-31 쉐이더를 실행하면 렌더링되는 지오메트리의 영역을 계산하고, 5-32에서는 해당 영역이 유니폼 버퍼 블록의 멤버로 선언된다.

위 코드와 같이 밝기를 계산하고, 렌더링하게 되면, 

객체가 뷰어에 가까이 있는 경우 영역이 크고 화면을 많이 차지하여 어토믹 카운트의 값이 클 것이다. 

반대로 뷰어에서 멀다면, 영역은 작고 어토믹 카운터는 작은 값일 것이다. 어토믹 카운터 값은 두 번째 쉐이더 유니폼 블록에 적용되어 렌더링하는 지오메트리의 밝기에 영향을 미친다.

Synchronizing Access to Atomic Counters

어토믹 카운터는 버퍼 객체의 위치를 나타낸다. 쉐이더 실행 시 어토믹 카운터의 값이 메모리에 쓰여지는데, 이는 메모리 연산의 형태이기 때문에 문제가 발생할 수 있다.

glMemoryBarrier(GL_ATOMIC_COUNTER_BARRIER_BIT);

위 함수는 어토믹 카운터에 대한 동기화된 접근을 지원한다. 버퍼 객체의 어토믹 카운터에 접근하고 쉐이더에서 그 버퍼를 갱신할 수 있도록 보장한다.

 memoryBarrier의 glsl버전인 memoryBarrierAtomicCounter를 사용하면 함수의 반환 전에 어토믹 카운터 연산의 완룔르 보장한다.

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유