Programming/D3D12

[책공부] FrameResource 예제 ( + Descriptor Table)

Dorasima 2024. 1. 4. 15:46

(사실 이해했는지 모르겠다. 어렴풋이 느껴질 뿐... 그래도 그 느낌을 정리한다.)

 

1. FrameResource를 이용한 기본 도형 그리기 예제(까지)가 연습시켜주는 기술

 

1_1 어뎁터 초기화 단계

(박스 예제에서 한 초기화 이후에 뭐가 없는 듯)

더보기

- 창(window) 생성

- 어뎁터 (D3DDevice) 생성

- GPU와 CPU 동기화를 위한 Fence 생성

- GPU와 CPU의 [데이터 전송을 위한 Descriptor View]의 Handle Size 초기화

    (아마 요걸로 데이터 블록(아마도 테이블?)을 점프하면서 데이터를 읽을 것 같다.)

- Command Queue, Command Allocator, Command List 생성

    (멀티 스레딩을 위해 새로 바뀐 GPU에게 렌더링 동작을 요청하는 방법)

- Swap Chain에서 Render Target 얻기

- Render Target 의 정보를 GPU와 연결할 Descriptor (View)

- Render Target 의 정보를 GPU와 연결할 Descriptor 를 또 (GUID로 구분하는) 공간인 Descriptor  heap에 연결

- GPU에서 사용할 Depth - Stencil의 정보를 가지는  Descriptor (View)

- GPU에서 사용할 Depth - Stencil의 정보를 가지는  Descriptor 를 또 (GUID로 구분하는) 공간인 Descriptor  heap에 연결

- Viewport 설정

 

 

1_2. FrameResource 아이디어와 그것을 사용하기 위한 준비

- CPU가 빠를 수도, GPU가 빠를 수도 있다. 둘 중에 뭐가 먼저 끝날지 모른다.

- 하지만, CPU에서 GPU에 작업을 내리는 것이다. 결국 App 단에서 Command List에  명령을 담는 것, CPU가 작성한 Buffer를 읽는 것이 먼저다.

(박스 예제는 매 프레임 (Update -> Draw)  할 때 fence를 확인하면서 GPU 작업이 끝날 때 까지, App 을 멈췄다.)

- 이번 예제에서는  CPU가 GPU가 끝날때 까지 기다리지 않아도 렌더링 명령을 하도록하는 FrameResource라는 구조를 사용한다.

- GPU의 작업이 끝나지 않아도, CPU가 미리미리 Update 로직을 실행하도록 + Command List를 채우도록 여러개의 FrameResource를 두는 것이다.

-  GPU가 작업을 하는동안 App(CPU)단에서 바꾸면 안되는건 Buffer와 Command Allocator 일 것이다. 

- 그리고 혹시 GPU가 느릴 때를 대비한 fence 값을 가지고 있으면 된다.

(Buffer : GPU가 Rendering 을 하는데 사용하는 데이터)

(CommandAllocator : GPU에 Command List 를 통해 명령을 내릴때, CPU에게 할당된 메모리)

- Circular Array 처럼 작동하면서 매 프레임 마다 현재 타겟인 FrameResource를 얻는다.

(예제에서는 3개의 FrameResource를 사용하였다.)

- 그러면 Update 함수와 Draw 함수는, 해당 FrameResource에서 Buffer를 얻어서 값을 갱신하고, Allocator를 이용해서 Command List를 채운다.

(이게 기본 아이디어다.)

 

 

1_3. Root Signature 와 Buffer Descriptor(View) Heap 다시 정리

- CPU에서 Contant Buffer 값을 업데이트 하고, 어떻게 GPU에 넘겨주는지 간단하게 정리하고 간다.

 

(업로드할 CB를 나타내는 struct type의 크기를 256로 align 한 값으로)

- ID3D12Device::CreateCommittedResource 으로  Buffer(ID3D12Resource)와 그와 연결된 Heap을 만든다.

- 예제에서 사용한 방법은 Upload Buffer와 Upload Heap을 이용한 것이고, 인자로 들어가는 속성 값은 다음과 같다.

 

(D3D12_HEAP_PROPERTIES에서 D3D12_HEAP_TYPE은 D3D12_HEAP_TYPE_UPLOAD 으로)

( D3D12_RESOURCE_DESC 에서 D3D12_RESOURCE 는 D3D12_RESOURCE_DIMENSION_BUFFER로 설정한다)

(d3dx12.h 에 있는 CD3DX12_HEAP_PROPERTIES / CD3DX12_RESOURCE_DESC 을 사용하면 편하다.)

 

- Upload Heap이면 CPU에서 Buffer에 접근해서 값을 변경할 수 있기 때문이다.

- 예제에서 Buffer에 접근하는 방법은, ID3D12Resource::Map을 이용해서 포인터를 얻을 수 있고,

- 그 포인터로 Buffer의 값을 Update 하는 것이다.

(FrameResource가 가지고 있는 Buffer (Upload Buffer와 Upload Heap가 연결되어 있는 것)를 어떻게 만들어야하고, 얼마나 크게 만들어야 하는지는 일단 나중으로 미룬다. 흐름 끊기지 않게 하기 위해)

- 이제 CPU 가 Buffer 를 업데이트 할 수 있으니, 이걸 GPU에게 넘겨줘야 한다.

이전 글 에서 간단하게, GPU가 Rendering Pipe Line에서 필요한 정보나 데이터는 PSO에 담겨 넘어가는 방법이 있고, PSO에 들어가지 않는 데이터(Vertex Buffer, Index Buffer, Constant Buffer(?) 등)는 다른 방법으로 넣는다고 했다.

- Constant Buffer는 Root Signature와 Descriptor(View) Heap을 통해 넘어간다고 했고, 테이블로 등록이 되어있다고 했다.

(Root Signature는 PSO 설정 값으로 들어간다. 어떤 느낌이지...)

 

(잠깐 구조 흐름과 이유를 정리하고 가면)

(지금이라도... descriptor 보다 짧으니 view라고 쓰겠다.)

- view를 사용하는 이유 : 어떤 데이터를 GPU에게 넘길 때, 이 데이터에 대한 검증은 CPU에서 모두 끝내야 한다. GPU는 냅다 그림을 그리는데 집중을 하는 것이 좋다.

(view 는 하드웨어 마다 크기가 달라서, 제공 함수로 크기를 구해서 사용해야 한다.)

- view heap 의 개념 : d3d12에서 CPU에서도 GPU 에서도 view에 접근할 수 있게 제공하는 개념이다. heap은 view 들이 위치한...어떤... 블록? 느낌이다. (아래 handle 개념과 합치면, 연속되었다는 느낌도 받을 수 있다.)

- view handle 의 개념 : view heap에서 view를 찾기 위한... 어떤 포인터 느낌쓰이다. heap 안에서 점프를 하면서, 원하는 view에 위치에 접근을 할 수 있다.

- Root Signature의 개념 : PSO당 하나 존재한다. (PSO도 한번에 하나만 등록 가능하다.) Shader가 데이터를 받을 때, 그 데이터의 타입을 GPU에게 알려주는 역할을 한다.

(view로 들어갈 데이터의 타입과 방법을 Shader에게 알려주기만 하는 친구고 view와 연결은 하지 않는다.)

 

Shader Visible Descriptor Heaps - Win32 apps | Microsoft Learn

 

Shader Visible Descriptor Heaps - Win32 apps

Shader visible descriptor heaps, are descriptor heaps that can be referenced by shaders through descriptor tables.

learn.microsoft.com

- Shader (Visible) Descriptor Heap 은 Descriptor Table에 의해 참조가 될 수 있다고 나온다. 

(2가지 View Heap이 참조 될 수 있는데, Type이 D3D12_SRV_UAV_CBV_DESCRIPTOR_HEAP과 D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER인데, 예제에서는 전자만 사용한다.)

(이 Heap이 Table에 묶이고, Shader에서 참조하려면 D3D12_DESCRIPTOR_HEAP_DESC에 flags가 D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE을 해줘야 한다.)

(머... 크신 뜻이 있겠지만)

- 어떤 Buffer 가 있고, 그 Buffer에 대한 Descriptor(View)  가 있고, 그 View들이 모여있는 View Heap이 있고, View Heap을 Shader로 하여금 또 Table로 받을 수 있게 도와주는 Root Signature를 사용한다... (그걸 PSO에 맴버로 넣는다.)

 

-  예제에서 어떻게 했는지 보면

-  사용할 Buffer의 개수를 (NumDescriptors) 속성으로 넣어주고, ID3D12Device::CreateDescriptorHeap으로 view heap을 만들어준다.

- 그리고 어떤 Contant buffer를 가리키는 view를 view heap에 만드는 건 ID3D12Device::CreateConstantBufferView를 이용해서 만든다.

(들어가는 인자로는 Buffer의 GPU_VIRTUAL_ADDRESS와, View를 저장할 위치인 View Handle 값이다)

(도우미 구조체를 사용해서 CD3DX12_CPU_DESCRIPTOR_HANDLE 쉽게 접근 할 수 있다.)

-  Root Signature는 View Heap을 Table 형식으로 받도록 타입을 정의했다.

- Shader에게 view를 넘겨주는 것은 이제  D3D12GraphicsCommandList::SetGraphicsRootDescriptorTable에서 해준다. 

(인자로는 PSO의 속성 값으로 설정해 놓은 Root Signature의 Table 번호, 진짜 view가 있는 view handle을 넘겨준다.)

(Root Signature를 생성할 때 여러 인자가 있는데, 그중에는 쉐이더에 작성하는 레지스터 번호도 있으니 잘 지정해서 넣어줘야 한다.

 

루트 서명 만들기 - Win32 apps | Microsoft Learn

 

루트 서명 만들기 - Win32 apps

루트 서명은 중첩 구조를 포함하는 복합 데이터 구조입니다.

learn.microsoft.com

 

 

1_4. RenderItem 아이디어와  DrawIndexedInstanced을 사용하기

- 예제가 학생으로 하여금 똑같은 Vertex Buffer와 Index Buffer로 여러번 다른위치에 랜더링 하는 것을 연습시킨다.

 

- 연습의 목적은 다음이 아닐까 생각해본다.

- 첫번째, 도형을 한번씩만 저장해서 메모리를 아낀다. (모든 Vertex와 Index를 하나의 버퍼에 저장한다.)

- 두번째, 도형이 그려질 때 마다 다른 WorldMat이 Shader에 넘어가야 한다.

- 세번째, 점은 엄청 많다. 위치 변환은 GPU에서 하는게 상식처럼 느껴진다.

- 네번째, WorldMat은 CPU에서 갱신할 수 있고, 그것을 적절한 방법으로 Rendering Pipe Line에 Bind 한다.

- 다섯번째,  (레지스터 번호 관련) Object 마다 넘겨야 하는 값이 있고, 공통으로 사용해서 Frame에 한번만 넘겨줘야 하는 값이 있는데, 그것을 연습시킨다. 

 

- SetGraphicsRootDescriptorTable와 DrawIndexedInstanced 을 사용하게 했고, 그걸 쉽게 하도록 Render Item 구조체를 사용하도록 하였다.

- Render Item이 가지는 값은

- WorldMat : Shader 로 넘어가서, Item이 그려질 Transform을 나타낸다.

- NumFrameDirty : Item의 속성이 바뀌었을 때 값이 설정 된다. 0 상태가 바뀌지 않은 상태이다. 만약 상태가 바뀌었다면, App에서 Frame Resource의 개수의 값을 지정한다. 왜냐하면 Frame이 지나가면서 존재하는 모든 Frame Resource 안에 있는 값이 새로운 Item의 값으로 갱신이 되어야 하기 때문이다. 

- ObjCBIndex : 현재 View Heap 에

- Geometry 정보 : Vertex Buffer와 Index Buffer와 그 View를 얻을 수 있는 포인터다. 여러 Item이 하나의 Geometry를 가지고 있을 수 있다.

- Primitive Type : 그냥 Triangle 쓴다.

- DrawIndexedInstanced 의 인자들 : IndexCount, StartIndexLocation, BaseVertexLocation을 가지고 있다.

 

1_5. Initialize 와 Update 와 Draw 단계 간단 정리

1_5_1. Initialize

- 기본 모체 도형(종류)을 생성한다.

- 도형을 하나의 버퍼로 합친다. (Vertex Buffer, Index Buffer)

- 위 과정에서 필요한 인자들을 이용해서 Render Item을 정의한다.

(Render Item이 진짜 내가 그리고 싶은 도형의 개수와 초기 위치)

- Frame Resource 을 원하는 만큼 만든다.

(넘어가는 인자는 Render Item의 개수와 Frame 공통으로 쓰이는 Buffer 개수가 필요하다.)

(Frame Resource 내부적으로 인자에 맞게 Buffer가 생성이 된다.)

- Constant Buffer용 View Heap을 만든다.

(2개 만든다. Object Constant Buffer View Heap, Frame Constant Buffer View Heap)

- Frame Resource의 Buffer를 Buffer View로 View Heap에 연결 한다.

- Shader에서 구분해서, View Heap을 받을 수 있도록 Root Signature를 Table로 만들어서, View Heap을 하나씩 넣어준다.

- PSO를 만들어 준다. 

 

1_5_2. Update

- 1_2에서 언급한 것 처럼 현재 Frame Resource를 얻는다.

- 혹시 모를 Fence를 체크하고

- Object Contants Buffer와 Frame Constants Buffer를 업데이트 해준다.

 

1_5_3. Draw

- 1_2에서 언급한 것 처럼 현재 Frame Resource의 Command Allocator를 초기화 한다.

(이전 글 처럼 기본적인 것을 Command List에 넣어주고)

- 일단 Frame 공통 Buffer를 넘겨주는데, SetGraphicsRootDescriptorTable에서 공통 Frame View를 가지고있는 테이블 번호로 View heap을 지정하고, 현재 FrameResource Index에 맞는 view를 view heap에서 찾아 파이프라인에 연결해준다.

- 그리고 Object 별로 Buffer를 넘겨주는데, 마찬가지로 SetGraphicsRootDescriptorTable에서 Object view heap로 테이블 번호를 지정하고, 현재 FrameResource의 Index와 + Render Item의 ObjCBIndex 을 이용해 구한 Handle 값을 이용해서 view를 view heap에서 찾아 파이프라인에 연결해준다.

- DrawIndexedInstanced 에도 Render Item에 지정해 놓은 값을 넣어줘서 렌더링 요청을 한다.

(이전 글 처럼 Command Queue를 마무리 해주고)

- Frame Resource Index를 하나 늘려주고,

- 작업이 끝나면 GPU에게 fence 하나 늘리도록 요청한다.

 

 

 

 

2. 아직 잘 모르는 것

- 다행히 예제에서 연습한건 얼추 감을 잡았다.

- 근데 아직 써보지 않은 기능들, 속성들이 너무 많다.

- 그냥... 다음 챕터를 보는 수 밖에...