Direct3D란?
Direct3D는 응용 프로그램에서
GPU를 제어하고 프로그래밍하는 데 쓰이는 저수준 그래픽 API입니다.
COM(Component Object Model)이란?
COM은 DirectX의
프로그래밍 언어 독립성과 하위 호환성을 가능하게 하는 기술입니다. COM객체를 COM 인터페이스라고 합니다. COM 객체를 스마트포인터처럼 이용하기
위해서는 ComPtr 클래스를 이용해야 합니다. ComPtr은 wrl.h 헤더파일을 불러줘야 합니다. COM은 new 생성자를 이용할 필요가 없으며, delete 대신 COM 클래스 내의 Release 함수를 불러야 합니다. 하지만, ComPtr를 이용하면 자동으로 Release를 호출합니다.
Sawp Chain과 Presenting
만약에, 매 프레임마다 게임을 로딩한다면, 재미보다 짜증을 느낄 것입니다. Swap Chain은 이것을 줄이기
위해, Front Buffer와 Back Buffer를 이용하여
현재 화면은 Front Buffer에 나타내고 다음 프레임의 화면을 Back
Buffer에 두는 것을 Swap Chain을 형성한다고 합니다. 그리고 Front Buffer에서 Back Buffer로 전환하는 것을 Presenting이라고 합니다. 이렇게 함으로써, 효율적으로 화면에 텍스쳐를 그림을 그릴 수 있게
됩니다. 이와 같은 방법을 Double Buffering 이라고
합니다.
Depth Buffering
Depth Buffer는 각 픽셀의 Depth 정보를 담습니다. Depth는 0.0일 때 최대한 절두체에 가깝게 표현합니다. 1.0일 때에는 최대한
멀리 있게 표현합니다. Depth Buffering은 Z-Buffering에
의해 구현됩니다. 자세한 방법은 Z-Buffering을 개인적으로
해석하고, 구현해봄으로써 설명하겠습니다.
Resource와 Descriptor(=View)
리소스는 컴퓨터가 렌더링 등에 사용할 수 있는 자원을 말합니다. 3D게임
개발에서는 메모리, GPU, CPU등이 대표적입니다. Draw명령을
제출하기 위해서는 이 리소스중에 Draw호출에 사용할 리소스들을 렌더링 파이프라인에 Binding 해줘야 합니다. 하지만,
이 리소스들이 직접적으로 Binding되는 것이 아닙니다.
Descriptor를 통해 Binding됩니다.
Descriptor는 리소스들을 GPU에게 전달하기 위한 자료구조입니다. GPU는 리소스 descriptor를 통해서 리소스의 실제 자료에
접근하며, 그 리소스를 사용하는 데 필요한 정보 역시 리소스 Descriptor로부터
얻습니다. GPU 리소스는 범용적인 메모리 조각이기 때문에, 똑
같은 리소스를 각각의 서로 다른 렌더링 파이프라인에서 사용될 수 있습니다. 왜냐하면, 리소스 자체로는 어떤 곳에 쓰이고 있는지, 어떤 형식인지에 대해
알려주지 않기 때문입니다. 따라서, Descriptor로부터
리소스의 사용법을 알 수 있고, 무형식의 리소스를 구체적으로 명시할 수 있도록 도와줍니다. 이후에 View라는 단어도 나오는데 이는 Descriptor와 같은 의미입니다.
Descriptor를
담을 수 있는 배열이 있는데 이것을 Descriptor heap이라고 합니다. 이 힙에는 같은 종류의 descriptor들이 저장됩니다. 또한, 한 종류의 descriptor에
대해 다수의 힙을 둘 수 있습니다. 하나의 자원을 참조하는 descriptor가
하나뿐이어야 하는 것은 아닙니다. 예를 들어 한 리소스의 여러 부분 영역을 여러 descriptor가 참조할 수 있습니다. 또한, 앞에서 언급했듯이 하나의 리소스가 렌더링 파이프라인의 여러 단계에서 binding할
수 있는데, 각 단계마다 개별적인 descriptor가 필요합니다.
Descriptor생성은 프로그램의 초기화 시점에서 하는 것이 형식
점검과 유효성 검증을 통해 더 좋은 효율성을 얻습니다.
DXGI(DirectX Graphics Infrastructure)란?
DXGI는 Direct3D와 함께 쓰이는 API입니다. 여기에는 여러 그래픽 API에
공통적인 그래픽 관련 작업들이 존재합니다. 예를 들어, 앞서
배운 SwapChain의 API도 여기에 속해있습니다. 또한, 우리가 게임을 시작해 옵션에 들어가면 창모드와 전체화면 설정을
볼 수 있을 것입니다. 이 기능 또한 DXGI에서 그래픽
설정을 통해 구현할 수 있습니다.
DXGI에는 핵심적인 인터페이스가 있습니다. IDXGIFactory라는
인터페이스인데 이름부터 공장 찍어내듯이 그래픽을 찍어낼 것 같군요. IDXGI에는 IDXGISwapChain 인터페이스 생성과 디스플레이 어댑터 열거가 있습니다.
SwapChain은 앞서 배웠기 때문에 무엇인가 알 수 있겠지만 디스플레이 어댑터 열거라는 것이 걸리는 군요.
디스플레이 어댑터는
물리적인 하드웨어 장치를 의미합니다. 쉽게 말해 그래픽 카드입니다. 하지만, 이것을 흉내 내는 소프트웨어 디스플레이 어댑터도 존재합니다. 하나의
시스템에 여러 개의 어댑터가 있을 수 있는데 한 예로, 알파고의 GPU는
100개가 넘었죠……
CPU와 GPU의 상호작용
-
Command Queue와 Command List
n 그래픽 프로그래밍에서는
CPU와 GPU에 의해 작동합니다. 둘은 병렬로 작동하기 때문에 동기화가 필요합니다. 동기화란, 한 처리 장치가 작업을 마칠 때까지 다른 한 처리 장치가 놀고 있어야 함을 의미합니다. 하지만, 최적의 성능을 위해서는 하나라도 멈춰서는 안됩니다.
n GPU에는 명령 대기열(Command Queue)이 하나 있습니다. CPU는 그리기 명령들이 담긴 명령 목록(Command List)을
Direct3D API를 통해서 GPU의 대기열에 제출합니다. 대기열에 명령이 추가되면 GPU가 이 명령들을 뽑아 실행합니다. 하지만, 대기열에 명령이 추가된다 하더라도 그 명령을 바로 실행하는
것은 아닙니다.
n 명령 대기열을
대표하는 인터페이스는 ID3D12CommandQueue입니다.
n 명령 목록을
대표하는 인터페이스는 ID3D12GraphicsCommandList입니다. 이 인터페이스에는 명령들을 목록에 추가하는 다양한 함수들이 있습니다. 하지만, 이 명령들을 대기열에 추가하기 위해서는 ExcuteCommandLists를
호출해야만 합니다. 호출하지 않으면 그저 목록에 추가만 할 뿐이기 때문입니다. 하지만, 명령 목록은 현재 명령들을 추가하는 명령 목록 이외에는
모두 닫혀있어야 합니다. 명령들을 여러 명령 목록에 동시에 기록할 수는 없기 때문입니다. 따라서, 명령을 대기열에 추가한 뒤 Close함수를 통해 닫아줘야 합니다.
n 명령 목록에는
메모리 할당자가 부여됩니다. 추가된 명령들은 이 할당자의 메모리에 저장이 됩니다. 대기열이 명령을 실행할 때 이 할당자를 참조하기 때문에 명령이 실행되기 전까지는 재설정해서는 안됩니다. 명령이 실행되기 전에 재설정 시 대기열은 실행해야 할 명령을 실행하지 않고,
변경된 명령을 실행하게 될 것입니다. 컴파일러가 막아주겠지만 유의해야 합니다. 할당자는 ID3D12CommandAllocator형식의 인터페이스가
존재합니다.
-
Fence
n Fence는 GPU가 대기열의 지정된 지점까지의 모든 명령을 처리할
때까지 CPU를 기다리게 하는 것입니다. ID3D12Fence 인터페이스가
대표적입니다. 이것이 GPU와 CPU의 동기화 방법 중 하나입니다.
-
Transition Resource Barrier
n GPU가 리소스에 데이터를 다 기록하지 않았거나, 기록을 아예 시작하지도
않은 상태에서 리소스의 데이터를 읽으려 하면 문제가 생기는 상황을 리소스 하자드라고 합니다. 줄여서
리자드라 부르겠습니다. (사실 책에서는 자원 위험 상황이라고 하지만 뭔가 어감이 이상해서 이렇게 부르겠습니다.)
n 자원 상태
전이는 리자드를 해결하기 위해, Direct3D가 리소스들에게 상태를 부여하는 것을 의미합니다. 예를 들어 텍스처 리소스에 데이터를 기록해야 할 때에는 텍스처의 상태를 Render
Target 상태로 설정합니다. 이후 텍스처의 데이터를 읽어야 할 때가 되면 상태를 셰이더
리소스 상태로 변경합니다. 프로그램이 상태 전이를 Direct3D에게
보고함으로써, GPU는 리자드를 피하는 데 필요한 조치를 할 수 있습니다.
n Transition Resource Barrier는 GPU에게 리소스의
상태가 전이됨을 알려주는 하나의 명령입니다. 배리어 덕분에 GPU는
이후의 명령들을 실행할 때 리자드를 피하는 데 필요한 단계들을 밟을 수 있게 됩니다.
Direct3D의 초기화
1.
D3D12CreateDevice함수를 이용해서
ID3D12Device를 생성한다.
A.
초기화 과정은 길지만, 프로그램이 실행 시 한
번만 초기화하면 됩니다.
B.
2.
ID3D12Fence 객체를 생성하고 Descriptor들의 크기를 얻는다.
3.
4X MSAA* 품질 수준 지원 여부를 점검한다.
4.
Command Queue와 Command List Allocator 그리고 Command List를
생성한다.
5.
Swap Chain을 Descript하고 생성한다.
6.
응용 프로그램에 필요한 Descriptor Heap들을
생성한다.
7.
후면 버퍼의 크기를 설정하고, 후면 버퍼에 대한
Render Target View를 생성한다.
8.
Depth and Stencil 버퍼를 생성하고, 그와 연관된 Depth and Stencil View를 생성한다.
9.
뷰포트와 Scissor Rectangle들을
설정한다.
성능 타이머
성능타이머의
시간 측정 단위는 Tick의 개수입니다. 언리얼 엔진을 다루어
보신 분이라면 익숙할 단어네요. 틱 수 단위의 현재 시간을 얻을 때에는 QueryPerformanceCounter 함수를 사용하고, 초 단위
시간을 얻기 위해서는 QueryPerformanceFrequency 함수를 사용합니다. 두 함수 모두 자료형이 LARGE_INTEGER*인 매개변수로부터
값을 반환합니다.
mSecondsPerCount = 1.0 / (double)
countsPerSec (1sec/초당 틱 수 = 틱당 초 수)를 보아하니 주파수와 주기율의 관계를 보는 듯 합니다. 틱당 초 수에
틱 수를 곱하면 초 단위 시간이 도출됩니다.
valueInSecs = valueInSecs * mSecondsPerCount
하지만, 중요한 것은 항상 성능 타이머가 돌려준 실제 값들 자체가
아니라 측정한 두 시간 값 사이의 상대적 차이입니다. 따라서, 두
개의 값의 차에 틱당 초 수를 곱해줌으로써 우리는 어떤 작업을 했을 때 걸린 시간을 얻을 수 있습니다.
시간
측정에 관해서는 꼭 교재를 참고해주시길 바랍니다. 이 부분은 수학적인 부분도 많고, 코드와 그림도 많이 첨부되어 있어 요약한 내용으로는 부족한 부분이 많아 이해하기 힘들 것입니다. 내용을 요약하려니 요약하기가 어려운 파트였던 것 같습니다.
이상으로 4장을 마치겠습니다.
4.5절에서 Win32프로그래밍 지식이 전제조건이라 하는데 이것도 공부를 해야 할 것 같군요
ㅠㅠ