개요
GPU는 그래픽 렌더링 뿐 아니라 다양한 병렬 처리에 사용될 수 있다. 그러한 처리에 사용되는 것이 계산 셰이더 이다.
계산 셰이더는 렌더링 파이프라인에 포함되지않은 별개의 셰이더로 GPU를 이용하여 여러 병렬 계산을 할 수 있도록 해주는 셰이더이다.
계산 셰이더는 여러개의 스레드 그룹을 3차원 격자형태로 수행한다. 각 스레드 그룹은 여러개의 스레드를 3차원 격자형태로 수행한다.
하드웨어와 스레드
GPU는 여러개의 다중처리기로 구성되어있다.
다중처리기는 여러개의 코어로 구성되어있다.
한 스레드는 한 코어에 의해 수행되며 한 스레드 그룹은 한 다중처리기에 의해 수행된다.
이때 다중처리기는 NVIDIA의 경우 32개, AMD는 64개의 코어로 구성되므로 한 스레드그룹에는 이의 배수에 해당하는 스레드를 가지게 하는것이 효율적이다.
다중처리기의 개수 x 2개 이상의 스레드 그룹을 사용하는것이 병렬성을 최대로 높일 수 있는데 이는 스레드 그룹이 텍스처 읽기등의 작업으로 쉬는시간이 생길 수 있고 이때 다중처리기는 다른 스레드 그룹을 처리하는 식으로 유휴시간을 활용할 수 있기에 적어도 한 다중처리기당 두개의 스레드그룹이 할당 될 수 있도록 해야하는 것이 좋기 때문이다.
스레드 그룹
계산 셰이더를 수행하는 그래픽 명령은 다음 함수로 등록 할 수 있다.
void ID3D12GraphicsCommandList::Dispatch(
UINT ThreadGroupCountX,
UINT ThreadGroupCountY,
UINT ThreadGroupCountZ
);
이는 매개 변수의 XYZ길이의 격자로 스레드그룹들을 배치하여 수행하는 명령을 등록한다.
각 스레드 그룹에서 스레드들의 배치는 계산셰이더의 셰이더 함수위에 다음과 같이 명시하여 정할 수 있다.
[numthreads(X,Y,Z)]
void CS(...)
{
...
}
정리하면 계산셰이더에서 명시된 XYZ격자로 스레드들을 가진 스레드그룹들이 dispatch에 명시된 XYZ격자로 수행된다.
스레드 식별 시스템 값
계산 셰이더의 함수 매개변수로는 여러 의미소로 구성된 시스템 입력들이 있다. 이들은 각 스레드를 나타내는 ID값들인데 다음과 같은 것들이 있다.
SV_GroupID : 현재 스레드가 속한 스레드 그룹의 고유 ID로 스레드 그룹 격자에서의 3차원 인덱스이다.
SV_GroupThreadID : 현재 스레드에 대한 스레드 그룹안에서의 고유 ID로 격자에서의 3차원 인덱스이다.
SV_DispatchThreadID : 현재 스레드에 대한 전체 수행에서의 고유 ID로 전체 격자에서의 3차원 인덱스이다.
(3x2의 스레드 그룹과 각 스레드 그룹이 10x10이면 (0,0,0)~(29,19,0) 범위의 ID들이 할당된다.)
SV_GroupIndex : SV_GroupThreadID의 1차원 버전이다.
계산 셰이더는 각 스레드들이 사용하는 자료구조의 형태에 따라 스레드 ID를 적절히 인덱싱에 사용하여 각 자료 원소에 대해 병렬처리를 하는식으로 구현되는 것이 일반적이다.
공유 메모리
계산 셰이더에서 한 스레드 그룹안에서 공유하여 사용할 수 있는 메모리를 생성할 수 있다. 배열 형태로 생성하여 스레드 ID를 활용하여 인덱싱하여 사용하는 것이 일반적이다. 이는 다중처리기에 할당되는 하드웨어 캐시로 접근 속도가 매우 빠르다. 이는 텍스처에 직접 접근하여 데이터를 읽는 속도보다 빠르기에 캐싱용도로 주로 사용된다.
대표적인 사용 예시는 한 스레드가 여러 텍셀을 읽어 처리하고 이웃 스레드들 끼리 읽는 텍셀이 겹칠 때 각자 하나의 텍셀만 읽고 공유메모리에 저장한 뒤 서로 다른 텍셀들은 공유메모리로 읽어 속도를 향상시키는 방법이 있다.
이때 각 스레드들은 다른 스레드가 자신의 텍셀을 공유 메모리에 저장할때 까지 기다려야하는데 이는 계산 셰이더 내에서 GroupMemoryBarrierWuthGroupSunc()함수를 이용하여 동기화 할 수 있다.
이 함수를 호출하면 그룹 스레드내의 다른 스레드들이 모두 그 함수에 도달할때까지 대기하게 해준다.
계산 셰이더의 사용
계산 셰이더는 D3D12_COMPUTE_PIPLINE_STATE_DESC에 컴파일 된 계산 셰이더 바이트등을 채운 뒤 Device 객체의 CreateComputePipelineState 메소드를 사용하여 계산 PSO를 만들어 사용할 수 있다.
이 PSO를 커맨드 리스트에서 등록하고 루트 매개변수등의 세팅을 한뒤 dispatch를 이용하면 계산셰이더가 수행된다.
계산 셰이더의 입출력
계산 셰이더에는 주로 텍스처나 구조적 버퍼 자원등이 입출력 자원으로 사용된다.
텍스처를 입력하는 것은 기존에 사용해오던 것과 동일하다. srv로 셰이더 매개변수에 등록해주면 된다. 이렇게 입력된 텍스처에는 기존같이 텍스처 표본 추출로 값을 추출하거나 그냥 정수 인덱싱으로 특정 원소의 값을 읽을 수 있다.
텍스처를 입출력 매개변수로 넣기 위해서는 자원을 생성할때 ua플래그를 넣어 생성하고 uav로 뷰를 만들어 셰이더 매개변수에 등록해주면 된다. 이러한 텍스처에 정수 인덱싱으로 특정 원소에 값을 넣을 수 있다.
구조적 버퍼 자원은 셰이더에서 정의한 구조체를 배열로 접근할 수 있는 자원이다. 정점 버퍼를 만들때 했듯이 입력하고 싶은 구조체 배열로 default buffer를 만들어 srv로 입력하면 된다.
출력은 마찬가지로 자원을 만들때 ua플래그만 넣어 만든뒤 uav로 입력하면 된다.
또한 추가 버퍼와 소비 버퍼라는 특별한 구조적 버퍼 자원도 있는데 소비 버퍼에는 원소를 하나씩 뺄 수 있는 Consume메소드가 존재하고 추가 버퍼에는 원소를 넣을 수 있는 Append메소드가 존재한다. 이는 처리 및 저장에 인덱스나 순서가 중요하지않은 로직에서 사용하면 유용하다.
이 출력 결과는 출력 자원에 저장되어 있는데 cpu에서 읽고 싶다면 readback버퍼를 만든뒤 옮겨담고 mapping으로 cpu자료구조에 매핑시켜 가져오면 된다.
구조적 버퍼자원이 출력일땐 주로 위 방법으로 데이터를 가져오고 텍스처가 출력일땐 그 텍스처를 바로 렌더링 하는데 사용하는 것이 일반적이다.
흐리기
계산 셰이더의 대표적인 사용 예시는 흐리기이다. 텍스처를 입력받아 한 텍셀을 주변 텍셀들의 가중 평균으로 만들어 갱신하면 흐려진 텍스처를 만들 수 있다. 이때 가중평균의 가중치가 흐리기의 시그니처가 된다. 또한 가중치 방식에 따라 mxn범위의 흐리기를 할때 mxn범위를 가중평균 하는 것이 아닌 1xm으로 수평 흐리기를 한후 mx1로 수직흐리기를 하는 방식으로 분리하여 흐리기가 가능한 경우도 있다.
실시간 렌더링에서 흐리기를 적용할때는 다음 단계를 사용한다.
- 후면버퍼에 장면을 그린다.
- 후면버퍼를 입력으로 하여 흐려진 텍스처를 그린다.
- 흐려진 텍스처를 입힌 사각형을 화면 전체에 채우고 텍스처를 그대로 출력하는 셰이더로 후면버퍼에 장면을 그린다.
참고서적
DirectX 12를 이용한 3D 게임 프로그래밍 입문
'그래픽스 > DirectX12' 카테고리의 다른 글
[Directx12][15장]1인칭 카메라 & 동적 색인화 (0) | 2023.03.21 |
---|---|
[Directx12][14장]테셀레이션 (0) | 2023.03.21 |
[Directx12][12장]기하셰이더 (0) | 2023.03.21 |
[Directx12][11장]스텐실 (0) | 2023.03.21 |
[Directx12][10장]혼합 (0) | 2023.03.21 |