기본 지식
1.Direct3D 개요
Direct3D는 윈도우의 제작사인 마이크로소프트와 여러 그래픽카드 제조사들의 직간접적인 협력으로 개발된 윈도우에서의 GPU 그래픽 인터페이스이다. C++의 API로 제공된다.
2.COM 객체 ★
COM(Component Object Model)는 특정 소프트웨어의 컴포넌트에 대해 프로그래밍 언어 독립성과 하위 호환성을 가능하게하는 기술이다.
DirectX에서 이러한 COM 객체는 C++ 클래스로 제공되니 그냥 한 객체라고 생각하고 사용해도 무방하다.
DriectX에서 COM 객체들은 대문자 I로 시작하는 클래스로 제공되는데 (ex/ ID3D12GraphicsCommandList) 이러한 객체들은 Microsoft::WRL::COMPtr<>이라는 전용 스마트 포인터 클래스를 사용해서 관리해야한다.
ex) COMPtr<ID3D12GraphicsCommandList> mCommandList;
COMPtr의 주된 기능들은 다음과 같다.
Get 메소드 : COMPtr이 가르키고 있는 COM 객체의 주소를 돌려준다. COM객체를 어떤 함수의 입력으로 사용할때 매개변수에 주로 넣어준다.
ex) 반환값 타입: ID3D12GraphicsCommandList*
Reset 메소드: COMPtr을 nullptr로 만들고 기존에 가르키던 COM 객체의 참조를 1줄인다.
GetAddressOf 메소드 : COMPtr의 멤버중 실제로 COM객체를 가르키는 역할을 하는 포인터 멤버의 주소를 돌려준다. 함수에서 COM객체를 생성하고 COMPtr에 그 값을 넣어줄 때 매개변수에 넣어주는 경우 가끔 쓰인다.
ex) 반환값 타입: ID3D12GraphicsCommandList**
함수로부터 COMPtr에 COM객체를 받을때 GetAddressOf 메소드를 사용한다고 되어있지만 실제로 저 함수는 가끔 특정한 상황에만 쓰이고 대부분의 경우 IID_PPV_ARGS 매크로를 이용해서 출력을 COMPtr에 받아야 한다.
함수가 출력용 포인터의 매개변수로 다음과 같이 COM객체의 포인터의 주소를 직접적으로 받는 경우에는 GetAddressOf메소드로 매개변수를 입력해주면 된다.
ID3D12GraphicsCommandList** output
하지만 함수가 출력용 포인터의 매개변수대신 다음과 같은 두개의 매개변수를 요구할 때는 IID_PPV_ARGS라는 매크로를 사용해야한다.
REFIID riid //COM객체 고유의 ID이다.
void **outputCOMObject //출력용으로 사용할 COM객체 포인터의 주소(void형)이다.
해당 매크로를 다음과 같이 사용하면 COMPtr이 가르키는 객체를 저 두 매개변수로 적절히 변형시켜준다.
IID_PPV_ARGS의 정의는 다음과 같다.
#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
여기서 매크로 매개변수인 ppType에는 출력을 받을 COMPtr객체를 mCP라고 했을때
&mCP
또는
mCP.GetAddressOf를 사용하면 된다.
즉
IID_PPV_ARGS(&mCP)
또는
IID_PPV_ARGS(mCP. GetAddressOf)
형식으로 매크로를 사용한다.
이때 둘의 차이는 &로 직접 COMPtr의 주소를 넘겨주는 경우 Reset이 자동으로 호출 되어 기존에 COMPtr이 가르키던 COM객체에 대한 참조가 1줄어들지만 GetAdressOf는 그렇지않다는 점이다. 빈상태의 COMPtr에 대해서는 똑같이 처리된다고 봐도 무방하다.
__uuidof는 적절한 COM ID로 **(ppType)을 변환해주고 IID_PPV_ARGS_Helper는 ppType을 void**로 적절히 변환해주어 위의 두 매개변수에 적절하게 COMPtr객체를 변환해주게 된다.
3.텍스처
텍스처는 gpu자원으로 사용되는 특정 원소의 n차 배열이라고 보면 된다.
주로 2차원 텍스처가 사용되는데 화면을 나타내는 픽셀들의 1920*1080짜리 텍스처가 그 예이다.
이때 원소들은 해당 픽셀들의 색정보를 가지는 RGBA값들을 가지게 된다.
물론 깊이버퍼등 화면외의 용도도 다양하게 존재한다.
텍스처의 원소들은 텍셀이라고 부른다.
이때 텍스처가 화면을 나타내는 경우 원소들을 픽셀이라고 부르기도 한다.
조직에 따라 이 둘을 섞어 사용하는 경우도 있다고 한다.
텍스처의 원소타입은 DXGI_FORMAT 열거형으로 표현된다.
가끔 원소타입을 무형식으로 지정하는 경우도 있는데 그 자원(텍스처)를 가르키는 서술자에서 그 타입을 지정해주어야한다. (서술자는 이후에 서술)
4.교환 사슬 ★
실시간 렌더링 기법에서는 여러개의 화면 텍스처를 사용해서 실시간 화면들을 렌더링한다.
대표적인게 이중 버퍼링인데 두개의 화면 텍스처를 사용한다.
이때 화면이 다 렌더링된 한 텍스처가 실제 모니터에 출력되는 동안 나머지 한 텍스처에 렌더링을 진행하고 그 텍스처에 렌더링이 끝나면 둘의 역할을 바꾼다. 이런 식으로 반복하여 매 프레임을 그려나간다.
이때 모니터에 출력되는 텍스처를 전면버퍼라 하며 뒤에서 렌더링 되는 텍스처를 후면버퍼라고 한다.
DirectX에서는 이러한 절차를 교환사슬이라는 인터페이스로 관리해준다
IDXGISwapChain이라는 클래스로 제공되는데 다음과 같은 역할들을 가진다.
1. n중 버퍼링에 사용될 n개의 텍스처 자원들을 생성하고 관리한다.
2. present 메소드로 교환사슬 생성시 연관시켰던 명령 대기열에 후면버퍼와 전면버퍼를 전환하여 모니터에 출력하는 명령을 제출한다.
3.새로운 크기의 자원을 생성해서 다시 관리하는 reSize메소드등 여러가지 편의 기능을 제공한다.
4. 등등
5.깊이 버퍼링 ★
파이프라이닝에서 텍스처에 화면을 그릴 때 화면이 그려지는 텍스처(render target)외에 하나의 텍스처를 더 사용한다. 바로 깊이와 스텐실값을 가지는 깊이/스텐실버퍼이다. 여기서 스텐실은 그림자,반투명등에 사용되는 기능인데 이는 이후에 설명하고 깊이에 대해서 설명한다.
파이프 라이닝에서는 여러가지 기하구조들을 하나씩 렌더링하여 그 기하구조가 화면상에 나타나는 픽셀 부분들을 추출해내 한 픽셀씩 render target 텍스처에 그린다. 이때 그 픽셀이 담당하는 공간상에 여러 기하구조들이 존재한다면 제일 앞에 있는 기하구조의 특정 점면이 그 뒤의 점면들을 가릴 것이다. 이때 제일 앞의 점면만 최종적으로 픽셀에 그려지게 해야하는데 이때 깊이 버퍼가 사용된다.
한 기하구조의 픽셀이 화면에 그려질때 그 점면이 얼마나 시점에 가까웠는지, 즉 깊이값을 깊이버퍼의 해당 픽셀 위치부분에 기록한다. 이후 똑같은 픽셀 위치에 다른 점면이 그려지려고 할때 기존 깊이버퍼값과 현재 점면의 깊이 값을 비교하여 더 가깝다면 rentarget 픽셀과 깊이버퍼의 픽셀을 갱신하고 아니라면 넘어간다. 이런식으로 더 깊은 점면만을 그리게하여 화면을 그려나간다.
6.자원 서술자 ★
gpu에서 사용 되는 자원(대표적으로 텍스처)들은 서술자로 관리해야한다. 서술자는 특정 자원을 가르키면서 동시에 자원의 역할등 여러정보를 서술해주는 역할도 한다. 서술자는 자원이 render target용인지 깊이 스텐실용인지들의 역할을 가르켜야한다. 또 자원의 원소가 무형식으로 지정된 경우 그 형식을 지정해줄 필요도 있다.
서술자들은 서술자 힙이라는 서술자 배열로 관리된다.
서술자힙~(핸들) - 서술자 - 자원
이 셋은 이러한 관계로 서로 연동된다.
n개의 서술자를 가지는 서술자 힙은 n개의 핸들을 가지는데 각 핸들이 각 서술자를 가르킨다. 즉 서술자 힙의 n번째 핸들에 n번째 서술자가 등록되어 사용된다.
이들의 관계를 만드는 과정은 다음과 같다.
- 서술자힙을 생성한다.(핸들은 자동 생성) / 자원을 생성한다.(직접생성하거나 교환사슬등이 생성해준다.)
- 서술자 힙의 n번째 핸들과 자원을 매개변수로 서술자를 생성한다.
- 이제 서술자 힙의 n번째 핸들로 해당 자원을 컨트롤 할 수 있다.
7.초과 표본화, 다중 표본화
초과, 다중 표본화는 보다 부드러운 픽셀을 렌더링하는 기법이다.
초과 표본화
화면이 10x10이라고 할때 100개의 픽셀에 그냥 기하구조를 렌더링하면 픽셀이 구불구불한 계단현상이 나타난다.
이때 한 픽셀들을 2x2, 즉 4개의 픽셀로 나누어 4배의 넓이로 화면을 잡고 여기에 기하구조를 렌더링한다. 그 후 그 4개의 픽셀들의 색깔의 평균을 내어 10x10픽셀의 색깔을 정하는 기법이다.
여기서 한 픽셀을 부분 픽셀로 나누는 갯수를 더 늘릴 수록 정교한 표본화가 된다.
다중 표본화
초과 표본화는 렌더링연산이 배로 들기에 성능이 좋지않다. 따라서 절충안으로 다중 표본화를 사용한다.
다중 표본화는 초과 표본화와 달리 각 부분픽셀에 모든 렌더 연산을 수행하지않는다. 렌더 연산중 일부는 원래 픽셀당 한번만 계산하고 가시성, 포괄도등의 일부 연산만 각 부분픽셀에 계산한 뒤 이 값들을 적절히 계산하여 최종 픽셀의 색깔을 계산해낸다.
8. 기능수준
기능 수준은 현재 하드웨어, 환경이 directX의 기능을 얼마나 지원하는지를 나타내는 개념이다.
D3D_FEATURE_LEVEL이라는 열거형이 이를 대표한다.
ex)
enum D3D_FEATURE_LEVEL
{
.......
D3D_FEATURE_LEVEL_10_0,
D3D_FEATURE_LEVEL_10_1,
D3D_FEATURE_LEVEL_11_0,
D3D_FEATURE_LEVEL_11_1
}D3D_FEATURE_LEVEL;
프로그램의 높은 호환성을 바란다면 현재 환경의 기능수준을 파악 후 기능수준별로 적절한 기능을 사용하도록 해야한다.
10. DXGI ★
DXGI는 Direct3D와 함께 쓰이는 API이다. Ditect3D외 2D등에도 같이 쓰일 수 있는 기능들은 주로 이곳에서 지원한다. swapChain등이 그 예이다.
DXGI의 핵심 인터페이스는 IDXGIFactory로 대부분의 인터페이스를 생성한다.
11. 기능 지원 점검
ID3D12Device::CheckFeatureSupport 메서드는 기능 수준, 다중 표본화 지원 여부(+ 몇 레벨까지 지원하는지)등 여러 기능 지원을 점검 할 수 있는 메서드다.
124p 참고
12. 상주성
상주성은 특정 자원이 GPU 메모리에 올라가있는지 아닌지를 나타내는 말이다.
GPU메모리는 그렇게 여유로운 편이 아니기에 한동안 쓰지않는 자원은 내려주고 계속 쓰게 될 자원은 그대로 올려두는 등 세밀한 상주성 관리가 필요하다.
DirectX의 여러 함수로 이러한 상주성을 우리가 직접 관리할 수 있다.
12. 디버깅 ★
Direct3D의 여러 함수들은 HRESULT 형식의 구조체를 반환한다. 이 구조체는 함수의 처리결과를 담고있다. 함수가 정상적으로 처리됬는지, 처리되지 않았다면 오류에 대한 여러 정보도 포함되어있다.
이때 이 HRESULT에 대한 ThrowIfFailed라는 매크로가 있다.
이 매크로에 HRESULT를 넣어서 사용하면 해당 HRESULT가 오류를 나타낼때 오류가 발생한 구문과 위치를 포함한 여러 오류 정보들을 담은 DxException이라는 예외를 throw한다.
따라서 HRESULT를 리턴하는 ABC()라는 함수가 있을때
ThrowIfFailed(ABC())이와 같이 함수를 사용하면 함수 결과로 오류 발생시 DxException를 throw해준다.
그리고 그러한 식으로 디버깅한 부분을 프로그램 바깥에서 try로 묶고 catch에서 DxException을 받아 사용하면 적절한 디버깅을 사용할 수 있다.
CPU GPU 상호작용
1.명령 대기열, 명령 목록, 명령 할당자 ★
CPU와 GPU의 상호작용은 CPU에서 GPU가 처리할 명령들을 제작하여 GPU에게 제출하면 GPU는 그러한 명령을 처리하는 식으로 이루어진다. 이때 CPU가 GPU로 명령을 제출하는 과정은 명령 대기열, 명령 목록, 명령 할당자들을 이용하여 이루어진다.
명령 목록
우선 명령 목록은 말그대로 GPU가 처리할 명령어들이 순서대로 담긴 명령어 리스트이다.
ID3D12CommandList로 대표된다.( 우리는 ID3D12GraphicsCommandList라는 업그레이드 버전을 사용한다.)
명령 할당자
명령 목록속 명령들은 명령 할당자가 관리하는 메모리 공간에 실제로 존재하게 된다.
ID3D12CommandAllocator로 대표된다.
명령 대기열
명령 대기열에 들어간 명령목록들은 순서대로 GPU에 제출된다.
ID3D12CommandQueue로 대표된다.
위 세개의 사용을 정리하면
- 특정 그래픽 카드(어뎁터)에 대한 명령 대기열, 할당자를 생성한다.
- 생성한 할당자를 매개변수로 명령 목록을 생성한다.
- 생성한 명령 목록에 처리할 명령어들을 넣는다.
- 명령목록이 완성되면 닫는다.
- 명령 대기열의 ExecuteCOmmandLists메소드로 명령목록을 명령 대기열에 제출한다.
- (그러면 자동으로 명령 대기열의 명령어들이 GPU에 들어가 처리된다.)
- 명령 대기열에 명령목록을 제출했으면 명령 목록을 reset메소드로 비워준다.
- gpu가 명령 대기열의 해당 할당자의 모든 명령을 다 처리했다면 명령 할당자를 reset 메소드로 비워준다.
위 순서에서 1,2번이 초기화 3~8번이 매 프레임의 명령어 처리 루프가 된다.
여기서 주목할 점은 명령 할당자는 그 속의 명령어들이 모두 처리된 후 리셋 되어야하지만 명령 목록은 명령 대기열에 제출만 되면 바로 리셋해도 된다는 점이다. 명령 목록이 명령 대기열에 제출한 후 바로 리셋을 해도 명령 할당자의 공간에 명령어들이 계속 남아있고 명령대기열에서는 그 명령어들을 계속 추적할 수 있기에 명령 목록은 제출 후 바로 리셋해도 되는 것이다. 하지만 명령 할당자는 모든 명령이 처리 되기 전엔 리셋하면 안된다.
2.CPU와 GPU의 동기화 ★
CPU와 GPU는 병렬로 동작하기에 서로 동기화가 필요하다. 가장 기본적인 동기화 방법은 한 프로세서가 다른 프로세서가 어느 특정 부분까지 처리할때 까지 기다려 주는 것이다.
DitectX에서는 fence라는 기능으로 명령 대기열의 특정 명령어에 도달시 CPU에서 이벤트를 주거나 Fence의 멤버값을 자동으로 수정해주어 CPU에서 그 명령어 도달 시점까지 기다리게 하는 것이 가능하다.
대표적인 방법은 어느 특정 cpu지점에서 명령 대기열에 Fence이벤트를 발생시키는 명령어를 제출하고 그 fence이벤트가 발생할때 까지 대기하면 그 시점의 모든 명령 대기열 명령어들이 다 처리 될 때 까지 기다리는 것이 가능하다.
명령 대기열의 Signal이라는 메소드를 fence와 int값을 매개변수로 사용하면 그 명령어가 실행시 fence에 등록된 이벤트를 발생시키고 int값으로 fence의 멤버값을 수정해주는 명령어가 추가된다.
3. 자원 상태 전이 ★
GPU의 자원에는 서술자와는 별개로 자원자체에도 자원의 상태라는 개념이 존재한다. 자원의 상태는 render target, present같은 것이 존재한다. 특정 자원을 사용하기전 그 용도에 맞게 상태를 전이해주어야한다.
명령 목록에 ResourceBarrier 메소드를 이용하여 특정 자원의 상태를 전이하는 명령어를 추가할 수 있다.
4. CPU 다중 스레드 활용
그래픽 프로그래밍에서는 CPU가 GPU의 명령어들을 작성하고 제출해주어야한다. CPU의 다중스레드를 활용하면 이러한 처리의 효율을 높일 수 있다. 이때 명령 할당자는 여러 스레드에서 동시에 공유하여도 문제가 없지만 명령 목록과 명령 할당자는 자유 스레드 모형을 따르지 않으므로(여러 스레드에서 동시에 사용하면 문제가 생김) 각 스레드별로 따로 만들어 주어야한다.
그리고 성능상의 문제로 프로그램 초기화 시 동시에 기록 될 수 있는 명령목록의 최대개수를 명시해주어야 한다.
참고서적
DirectX 12를 이용한 3D 게임 프로그래밍 입문
'그래픽스 > DirectX12' 카테고리의 다른 글
[Directx12][5장]렌더링 파이프라인 (0) | 2023.03.21 |
---|---|
[Directx12][4장][2]Direct3D 기본 구조 (0) | 2023.03.21 |
[Directx12][3장]기초 수학 - 변환 및 관련 라이브러리 (0) | 2023.03.21 |
[Directx12][2장]기초 수학 - 행렬 및 관련 라이브러리 (0) | 2023.03.21 |
[Directx12][1장]기초 수학 - 벡터 및 관련 라이브러리 (0) | 2023.03.21 |