(사실 이해했는지 모르겠다. 어렴풋이 느껴질 뿐... 그래도 그 느낌을 정리한다.)
1. Compute Shader
- 픽셀 작업을 위해 연속된 메모리 + 병렬계산을 하는데 특화된 GPU를, 좀 더 다양한 방법으로 사용하는 것이다.
(GPU 프로그래밍을 하는 느낌이다. CS는 스레드 하나가 어떻게 작동할지 정하는 느낌)
- GPU에서 돌아갈 스레드들이 모인 스레드 그룹(thread group)으로 grid를 만들고, 그것이 GPU위에서 돌아간다.
- 스레드 그룹은 구성 스레드가 접근 가능한 공유 메모리를 가진다. (원하는 작업 -> 그룹 하나)
- 그리고 어디서 많이 들었던 SIMD은 하드웨어별로 정해진 WARP라는 단위로 스레드가 묶여서 처리된다.
- GPU 화면을 그리는데 집중하는 다른 그래픽스 파이프라인과 다르게, 얘는 어떤 작업/계산에 집중하기 위해 만들어진 친구다. 그래서 계산 결과를 시스템 메모리에 가져와서 사용할 수 도 or 아니면 그냥 렌더링 작업에 그대로 사용할 수 도 있다.
- 연속된 데이터를 스레드가 나뉘어서 비슷한 작업을 한다는 건데, 스레드는 작업할 데이터가 어디 있는지 어떻게 아나...
(나름 흐름을 좀 살리고 싶어서 이리저리 글의 순서를 바꾸다 보니 설명이 아래에 있는데)
- ID3D12GraphicsCommandList ::Dispatch를 이용해서 스레드를 생성을 하고, 그것들이 Compute Shader의 인자로 들어가는, 총 생성된 스레드 개수에 맞게 SV_DispatchThreadID에 고유 ID가 들어가게 된다. 스레드 그룹 별 스레드 ID는 SV_GroupTheradID, 스레드 그룹에 붙는 ID는 SV_GroupID이다.
(잘은 모르지만, 데이터 입력 보다 데이터 처리속도가 빨라서 stall이 자주 일어난다고 한다. 그래서 최소 GPU 멀티 프로세서 개수 2배의 스레드 그룹을 두는 게 바람직하다고 한다.)
- 스레드 그룹의 개수, 스레드 개수, 스레드 그룹의 개념적인 Dimension 등을 고려한 색인이 정의가 되고, 그 색인으로 데이터에 접근을 해서 Compute Shader가 작동을 하도록 하는 것이다.
2. CS의 입력과 출력
- 일단 입력은 Texture와 Buffer를 받는다.
- 텍스쳐는 Graphics PSO랑 똑같이 Shader Resource View를 Heap에 얹어서, Table로 받는다.
(버퍼도 Vertex나 Index Buffer는 ID3D12GraphicsCommandList::IASetVertex(Index)Buffers으로 view를 등록하고)
(Constant Buffer는 함수 이름만 살짝 바꿔서 ID3D12GraphicsCommandList::SetComputeRootConstantBufferView 으로 view를 등록한다. )
- (CS 자체가 출력을 하는 느낌은 아닌데, ) Read - Write 가 가능한 자원을 만들어서 거기에 CS가 작성을 하여 사용하는 느낌이다. Shader에선 RW 접두사를 붙인 타입이 그 역할을 한다.
- 얘는 입력과 다르게 UAV (Unordered Access View)로 만들어서 CS와 연결해줘야 한다.
- 뜬금 구조적 버퍼 (Structured Buffer)도 사용한다. (입력, 출력 둘 다.)
- Shader 내에서 구조체를 배열로서 이용할 수 있게 하는 친구이다. App에서 초기화하는 모양새를 보면 그냥 Vertex Buffer를 만드는 것과 같다.
- 이 친구로 입력만 받는다면? 레지스터 슬롯은 t0~이고, 출력까지 하고 싶다면(RW뭐시기) u0 ~ 레지스터 슬롯을 가진다.
- 그리고 ID3D12Device::CreateCommittedResource 할 때, D3D12_RESOURCE_DESC의 flag와 D3D12_RESOURCE_STATES 을 Unordered로 해야 한다.
New Resource Types - Win32 apps | Microsoft Learn
3. Compute PSO
- 얘는 Compute PSO를 따로 가진다. (Graphics Pipeline State Object 가 아니라)
- 구조체 멤버 구성은 같은 것도 있고, 다른 점도 있다.
- 일단 매개변수 용 데이터를 올리는, RootSignature의 형식은 (Graphics PSO와) 같다.
- 쉐이더는 CS를 등록한다. (VS, GS, PS가 아니라)
4. CS에서 매개변수 데이터에 접근하기
(매개변수라고 해봤자 Texture와 Buffer이지만, 그래도 일반적인 표현을 쓰고 싶어서.. ^^:;)
- 교재처럼 RWTexture2D<타입> gOutput의 경우를 예로 들면, 1번 항목에서 막 스레드니 뭐니 해가지고 작업물을 쪼개서 작업을 한다고 했는데, Texture의 하나의 Texel에 접근하는 방법은 두 가지가 있다.
- 일단 [] (integer index)로 Texel에 접근하는 방법이다.
- sementic 은 SV_DispatchThreadID이고 이 친구로 스레드 그룹에 스레드가 담당하는 Texel의 색인을 uint3 값으로 가지고 오게 된다. (예를 들면 int3 dispatchThreadID : SV_DispatchThreadID라면 -> gOutput[dispatchThreadID.xy] )
- 하나의 스레드마다 하나의 텍셀이 호출된다면? 잘 작동하는 것이다.
- 두 번째는 SamplerState를 이용해서 SampleLevel 함수로 Texel에 접근하는 방법이다.
- 우리 생각보다 엄청 똑똑하게 Rasterize를 하는 GPU에서 픽셀 개수에 맞는 Miplevel을 정해주지만, Compute Shader는 Rasterize 단계가 아니라서, mipmap level을 매개변수로 정해주어야 한다.
- 그리고 Texel을 추출하는 uv index와 mip level이 정의역이 [0.f, 1.f] 구간으로 정규화되어 있어서, CB 등으로 알맞게 정규화해서 CS로 넘겨주어야 한다.
5. 스레드 그룹 공유 메모리
- 스레드 그룹마다 공유 메모리를 가질 수 있도록 CS에서 선언할 수 있는데, groupshared를 붙여서 선언한다.(Storage_Class 한정자 Variable Syntax - Win32 apps | Microsoft Learn )
- 스레드끼리, 서로의 작업물을 공유하는 데에 특화된 친구다. SV_GroupThreadID을 이용하면, 스레드에 알맞은 데이터 영역을 찾아서 갈 것이다.
(가용한 공유 메모리의 크기보다, 스레드 그룹이 사용하는 공유 데이터 크기가 절반 이하여야 stall이 일어나지 않고, GPU의 병렬성이 제대로 발휘될 수 있다고 한다. - 32KB)
- GroupMemoryBarrierWithGroupSync function을 이용해서 CS 로직 내에서 스레드들이 해당 해당 흐름까지 작업이 완료되는 것을 기다릴 수 있다.
6. App에서 해줘야 하는 것
- App에서는 ID3D12GraphicsCommandList::Dispatch를 이용해서, 스레드 그룹을 생성한다.
(x, y, z 축을 개념적으로 가진다)
- D3D12_COMPUTE_PIPELINE_STATE_DESC을 이용해서 Compute PSO를 만들어준다.
- CS에서 출력에 사용하는 UAV를 만들기 위해서 D3D12_RESOURCE_DESC를 채워주는데, (다른 View들과 다른 점은)
Flags = D3D12_RESOURCE_FLAG_ALLOW_UNORDERED_ACCSS으로 만들어줘야 한다. (올라가는 Heap은 같다.)
- 하지만 이 데이터를 받는 Root Signature, 그러니까 Table에서는 Entry를 정의할 때, D3D12_DESCRIPTOR_RANGE_TYPE_UAV를 해줘야 알맞게, 쉐이더로 넘어가게 된다.
7. 시스템 메모리에 CS 결과 가져오기
- 3번 글에서 CS가 데이터를 출력한다고 했는데, (바로 화면에 찍는 거 이외에) 메모리에 올려서 보는 방법이다.
- D3D12_HEAP_TYPE_READBACK 기능을 하는 Heap 위에 Buffer를 만들고, ID3D12GraphicsCommandList::CopyResource를 이용해서 그 값을 GPU에서 가져와서 그 Buffer에 올리게 된다.
(타입과 크기가 같아야 한다.)
8. Shader에서 사용하는 스트림 버퍼
- GPU 특성상 병렬로 작업이 이뤄지고, 뭐가 먼저 끝날지 알 수 없다.
- 그리고 뭐가 먼저 끝나던지 상관이 없는 작업의 경우 사용하기 좋은 구조적 스트림 버퍼를 제공한다.
- AppendStructuredBuffer와 ConsumeStructuredBuffer이다.
- Particle을 흩날리게 할 때 많이 쓰인다. App에서 냅다 ConsumeStructuredBuffer에 데이터를 넣어주면, 스레드가 하나씩 데이터를 빼서, 계산이 끝나는 대로 그냥 AppendStructuredBuffer에 넣어주고 그걸 화면에 그려주면 되는 것이다.
(여기서도 스레드 하나에 Buffer Data 하나가 할당되는 것은 보장된다.)
9. 언급할 만한 예제에서 사용한 방법(구조)
- 화면에 Blur를 먹이는데, 다 그려진 BackBuffer를 화면에 뿌리기 전에 Compute Shader에서 후처리를 해주는 것이다.
- 일단 교재는 정확한(?) Blur를 먹이지 않고 적당히 흐린 모습만 나오면 된다고 타협을 보았다.
- 스레드 개수의 한계로 256 픽셀마다 쪼개서 Blur를 처리하고,
- 성능을 위해서 BlurRadius 만큼 주변의 픽셀을 다 참조하는 것이 아니라, 가로 -> 세로 순으로 처리를 하여서 참조 횟수를 줄였다. (가로 세로 각각 다른 CS를 이용하여 처리하였다.)
- 그리고 쉐이더 내부에서도 무거운 연산인 Sample 대신에 groupshared 메모리를 이용해서, 텍셀 값을 가볍게 가져오면서 Blur도 그 위에서 바로 하였다.
- 약간 헷갈리는 GroupThreadID, DispatchThreadID로 다시 한번 더 정리하면,
- 입력으로 온 Texture2D를 SV_DispatchThreadID로 샘플링해와서, 스레드 그룹의 공유메모리에 SV_GroupThreadID로 캐싱을 한다. (DispatchThreadID는 총 생성된 스레드에서 자신이 몇 번째인지, GroupThreadID는 그룹 내에서 몇 번째인지)
- 그리고 CS 내에서 Blur를 먹인 다음에 그 결과를 GroupThreadID으로 가져와서 다시 RWTexture2D에 DispatchThreadID으로 저장한다.
10. 연습 문제
(클릭하면 커집니다.)
연습 문제 1 - Vector 연산하기 (결과 + CS)
- App에서 하는 일은 하나의 함수에 다 넣었다. - VectorPractice()
- 테스트한 벡터
std::array<VectorPrac_t, 64> vectors = {
VectorPrac_t(0.f, 0.f, 0.f),
VectorPrac_t(0.610f, 1.472f, 2.630f),
VectorPrac_t(0.353f, 1.090f, 2.551f),
VectorPrac_t(0.407f, 1.187f, 2.025f),
VectorPrac_t(0.056f, 1.502f, 2.381f),
VectorPrac_t(0.224f, 1.964f, 2.705f),
VectorPrac_t(1.589f, 2.426f, 3.703f),
VectorPrac_t(1.757f, 2.193f, 3.935f),
VectorPrac_t(1.502f, 2.244f, 3.231f),
VectorPrac_t(1.f, 0.f, 0.f),
VectorPrac_t(1.128f, 2.570f, 3.188f),
VectorPrac_t(1.292f, 2.855f, 3.475f),
VectorPrac_t(1.219f, 2.330f, 3.373f),
VectorPrac_t(1.253f, 2.494f, 3.990f),
VectorPrac_t(2.177f, 3.124f, 4.672f),
VectorPrac_t(2.243f, 3.399f, 4.464f),
VectorPrac_t(2.374f, 3.153f, 4.291f),
VectorPrac_t(2.509f, 3.742f, 4.253f),
VectorPrac_t(2.608f, 3.916f, 4.185f),
VectorPrac_t(3.469f, 4.514f, 5.680f),
VectorPrac_t(3.182f, 4.804f, 5.320f),
VectorPrac_t(3.636f, 4.361f, 5.402f),
VectorPrac_t(2.f, 0.f, 0.f),
VectorPrac_t(3.843f, 4.425f, 5.164f),
VectorPrac_t(3.520f, 4.292f, 5.334f),
VectorPrac_t(3.798f, 4.824f, 5.337f),
VectorPrac_t(4.568f, 5.710f, 6.102f),
VectorPrac_t(4.619f, 5.663f, 6.585f),
VectorPrac_t(4.169f, 5.485f, 6.480f),
VectorPrac_t(4.620f, 5.535f, 6.114f),
VectorPrac_t(4.538f, 5.191f, 6.609f),
VectorPrac_t(4.452f, 5.347f, 6.318f),
VectorPrac_t(4.341f, 5.208f, 6.388f),
VectorPrac_t(3.f, 0.f, 0.f),
VectorPrac_t(4.401f, 5.549f, 6.215f),
VectorPrac_t(4.428f, 5.697f, 6.074f),
VectorPrac_t(4.114f, 5.556f, 6.111f),
VectorPrac_t(5.122f, 6.247f, 0.484f),
VectorPrac_t(5.428f, 6.335f, 0.543f),
VectorPrac_t(5.442f, 6.224f, 0.457f),
VectorPrac_t(5.548f, 6.712f, 0.078f),
VectorPrac_t(5.411f, 6.366f, 0.337f),
VectorPrac_t(5.153f, 6.370f, 0.153f),
VectorPrac_t(5.370f, 6.539f, 0.138f),
VectorPrac_t(6.167f, 7.312f, 3.155f),
VectorPrac_t(4.f, 0.f, 0.f),
VectorPrac_t(6.318f, 7.204f, 3.238f),
VectorPrac_t(6.264f, 7.203f, 3.382f),
VectorPrac_t(6.105f, 7.326f, 3.266f),
VectorPrac_t(6.260f, 7.103f, 3.123f),
VectorPrac_t(6.349f, 7.270f, 3.319f),
VectorPrac_t(6.267f, 7.305f, 3.101f),
VectorPrac_t(6.166f, 7.095f, 3.320f),
VectorPrac_t(6.155f, 7.108f, 3.108f),
VectorPrac_t(6.084f, 7.202f, 3.076f),
VectorPrac_t(0.305f, 0.106f, 9.204f),
VectorPrac_t(0.081f, 0.184f, 9.083f),
VectorPrac_t(5.f, 0.f, 0.f),
VectorPrac_t(0.148f, 0.299f, 9.114f),
VectorPrac_t(0.098f, 0.109f, 9.090f),
VectorPrac_t(0.198f, 0.062f, 9.066f),
VectorPrac_t(0.201f, 0.150f, 9.014f),
VectorPrac_t(0.091f, 0.155f, 9.067f),
VectorPrac_t(0.029f, 0.143f, 9.069f)
};
연습 문제 2
연습 문제 3 - (고치는 중)
- Unordered Buffer를 어떻게 쉐이더로 넘겨주는지 모르겠다...
- Counter Resource는 뭔 개소리고, 그냥 CreateUnorderedAccessView는 StructuredBuffer와 맞지 않는지, CreateRootSignature를 걸면 그냥 E_OUTOFMEMORY을 뱉으면서 터져 버린다....
- 그냥 ConsumeStructuredBuffer 이나 AppendStructuredBuffer 같은 스트림 버퍼는 CS에서 작동하지 않는다고 한다....?
- ConsumeStructuredBuffer - Win32 apps | Microsoft Learn 교재 이거 뭐야 ^^;; 빡치네
- 일단 다음문제 다 풀고 쉐이더를 옮겨서 시도하던가 해봐야겠다.
연습 문제 4
- 타깃 픽셀과의 위치 관계 + 가우시안 분포만을 이용해서 처리하는 Spatial Blur 이외에, 다른 가중치를 넣어서 처리하는 Bilateral Blur를 연습하는 문제이다.
- 나는 간단하게 타겟 픽셀과 색깔 차이를 고려한 Intensity Blur(?) Color Blur(?)를 시도했다. (이렇게 하면, Edge가 뭉개지지 않는다고 한다.)
- CS 코드 (원래 예제 Blur 코드에 색 가중치의 역을 추가해주었다.)
연습 문제 5
- Wave 방정식을 CPU가 아니라 Compute Shader를 이용하여 GPU에서 작업하는 예제
- GPU를 이용한 오른쪽이 훨씬 빠르다.
- 예제에서 수정한 부분
(Resource Barrier Transition Error 가 계속 떠가지고 Update 부분에 몰아서 처리하기로 했다.)
연습 문제 6
- 소벨(sobel) 연산자로, 소벨 필터를 연습하는 문제다.
- 예제를 따라갈 때 Warning이 2개가 뜬다.
- COMMAND_LIST_DRAW_VERTEX_BUFFER_NOT_SET : 인풋 레이아웃이 설정이 안 되어 있다는 경고다.
- 해당 경고를 뱉는 PSO에 이렇게, 깡통 Input Layout을 설정해 주면 warning이 없어진다.
- CLEARRENDERTARGETVIEW_MISMATCHINGCLEARVALUE : 이거는... Render target 용 텍스쳐 리소스를 만들 때,
CreateCommittedResource()에 D3D12_CLEAR_VALUE 값을 넘겨주지 않아서 생긴다.
- 요렇게 대충 만들어서 넣어주면 warning이 없어진다.
11. 메모용
-
책 : DirectX 12를 이용한 3D 게임 프로그래밍 입문
'Programming > D3D12' 카테고리의 다른 글
[책공부] Dynamic Indexing / FPS Camera + chap 15 연습 문제 (0) | 2024.02.19 |
---|---|
[책공부] Tessellation + chap 14 연습 문제 (0) | 2024.02.14 |
[책공부] Geometry Shader + chap 12 연습 문제 (0) | 2024.01.30 |
[책공부] Stencil 예제 + chap 11 연습 문제 (0) | 2024.01.23 |
[책공부] Blend 예제 + chap 10 연습 문제 (0) | 2024.01.16 |