-
Graphic Programming에서는 GPU와 CPU. 두 Processor가 작동한다는 점을 이해할 필요가 있다.
-
이 둘은 병렬로 작동하지만, 종종 동기화가 필요하다.
-
최적의 성능을 위해서는 둘 모두 최대한 바쁘게 돌아가게 만들어야 하며, 동기화를 최소화 해야 한다.
-
동기화는 한 Processor의 작업이 마칠 때까지 다른 Processor는 놀아야 한다는 것을 의미한다.
-
-
한마디로, 동기화는 병렬성을 망친다.
명렬 대기열(Command Queue)과 명령 목록(Command List)
-
CPU는 Command List를 Direct3D API를 통해 GPU의 Command Queue에 제출한다.
-
하지만 "Queue"에서 알 수 있듯이,
Command Queue에 넘어갔다고 GPU가 그 즉시 실행하는 것은 아니다. -
Command들은 GPU가 처리할 준비가 되어야 비로서 실행되기 시작한다.
-
즉, GPU가 이전에 제출된 Command를 처리하는 동안에는 Queue에 남아 있다는 것이다.
-
-
Command Queue가 비면 GPU는 놀게 되고, 꽉 차면 Queue에 자리가 생길 때까지 CPU가 놀게 된다.
-
게임 같은 High-Quality Application에서는 가용 Hardware Resource를 최대한 쓰는 것이 목표다.
-
즉, CPU도 GPU도 항상 쉬지 않고 일하게 만들어야 한다.
-
-
Direct3D 12에서 Command Queue를 담당하는 Interface는 ID3D12CommandQueue이다.
-
이를 생성하기 위해서는 D3D12_COMMAND_QUEUE_DESC 구조체를 채운 후 ID3D12Device::CreateCommandQueue를 호출해야 한다.
-
-
다음은 Command Queue를 채우는 방식을 보여주는 예시 코드이다.
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMNAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
-
위 예시 코드에서 사용된 보조 메크로 IID_PPV_ARGS는 다음과 같이 정의되어 있다.
#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
-
여기서 __uuidof(**(ppType))은 (**(ppType))의 COM Interface ID로 평가 된다.
-
위 예에서의 ID는 ID3D12CommandQueue이다.
-
-
보조함수 IID_PPV_ARGS_Helper는 ppType을 void**로 Cast한다.
-
Direct3D 12 API에는 생성하고자 하는 Interface의 COM ID와 void**를 받는 함수들이 많기 때문에
책에서 이 매크로를 선언하고, 책 내의 예시코드 전반적으로 사용되고 있다.
ExcuteCommandLists
-
ExcuteCommandLists는 Command List에 있는 Command들을 Queue에 추가하는 메서드이다.
void ID3D12CommandQueue::ExecuteCommandLists(
UINT NumCommandLists,
ID3D12CommandList * const *ppCommandLists
);
-
선언으로 보면 ID3D12CommandList가 Command List를 담당하는 것 같지만,
실제 그래픽 작업을 위한 Command List는 이를 상속하는 ID3D12GraphicsCommandList라는 Interface가 담당한다.
-
HRESULT Close();
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12graphicscommandlist-close
-
ExcuteCommandLists로 Command List를 제출하기 전에
반드시 이 메서드를 이용해 Command List를 닫아야 함을 기억하자.
CreateCommandAllocator
-
Command List에는 ID3D12CommandAllocator가 하나 연관된다.
-
Command List에 추가된 Command들은 이 Allocator의 메모리에 저장된다.
-
-
ExcuteCommandLists로 Command List를 제출하면 Command Queue는 Allocator에 담긴 Command들을 참조한다.
-
Command Memory Allocator는 다음 메서드를 이용해 생성한다.
HRESULT CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE type,
REFIID riid,
void **ppCommandAllocator
);
-
type은 이 Allocator와 연관시킬 수 있는 Command List 종류이다.
-
책에서 주로 쓰이는 Type은 다음과 같다.
D3D12_COMMAND_LIST_TYPE_DIRECT
-
GPU가 직접 실행 시킬 수 있는 Command List
-
지금까지 설명한 Command List들이 이에 포함됨.
D3D12_COMMAND_LIST_TYPE_BUNDLE
-
Bundle을 나타내는 Command List
-
Command List를 만드는 데에 CPU의 부담이 어느정도 따른다.
-
때문에 Direct3D 12는 일련의 Command들을 Bundle 단위로 기록할 수 있는 최적화 수단을 제공한다.
-
-
Bundle을 추가하면 Driver는 Rendering 도중에 실행이 최적화 되도록 Bundle의 Command들을 PreCompile한다.
-
Application이 특정 Command List들을 구축하는데 시간이 오래 걸린다면, 이를 고려할 필요가 있다.
-
단, Direct3D 12 API는 이미 아주 효율적이라 이런 경우가 자주 생기지 않는다.
-
때문에 Bundle 사용이 성능상 이득을 가져오는 명백한 경우에만 사용한다.
-
-
즉, Bundle을 무조건 사용하지는 말아야 한다.
riid
-
생성하고자 하는 ID3D12CommandAllocator의 COM ID
ppCommandAllocator
-
생성된 Command Allocator를 가리키는 포인터
-
출력되는 매개변수
typedef enum D3D12_COMMAND_LIST_TYPE {
D3D12_COMMAND_LIST_TYPE_DIRECT,
D3D12_COMMAND_LIST_TYPE_BUNDLE,
D3D12_COMMAND_LIST_TYPE_COMPUTE,
D3D12_COMMAND_LIST_TYPE_COPY,
D3D12_COMMAND_LIST_TYPE_VIDEO_DECODE,
D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS,
D3D12_COMMAND_LIST_TYPE_VIDEO_ENCODE
} ;
CreateCommandList
HRESULT ID3D12Device::CreateCommandList(
UINT nodeMask,
D3D12_COMMAND_LIST_TYPE type,
ID3D12CommandAllocator *pCommandAllocator,
ID3D12PipelineState *pInitialState,
REFIID riid,
void **ppCommandList
);
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-createcommandlist
nodeMask
-
GPU가 여러개일 때에는 이 Command List와 연관시킬
물리적 GPU Adpater Node들을 지정하는 비트마스크 값을 설정한다.
-
GPU가 하나인 겨우에는 0으로 설정하면 된다.
-
System의 GPU Adapter node 개수는 다음 메서드로 알아낼 수 있다.
UINT ID3D12DeviceGetNodeCount();
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-getnodecount
type
-
Command List의 종류
-
pCommandAllocator
-
생성된 Command List와 연관시킬 Allocator
-
Command Allocator의 type은 Command List의 type와 일치해야 한다.
-
pInitialState
-
Command List의 초기 Pipeline 상태를 지정한다.
-
Bundle이거나 초기화 목적으로 쓰이고 실제 그리기 명령이 없는 Command List의 경우 null을 지정한다.
-
자세한 내용은 나중에 추가로 정리 할 예정이다.
riid
-
생성하고자 하는 Command List에 해당하는 ID3D12CommandList Interface의 COM ID
ppCommandList
-
생성된 Command List를 가르키는 포인터
-
출력 매개변수
-
한 Allocator를 여러 Command List에 연관시켜도 되지만,
Command를 여러 Command List에 동시에 기록할 수는 없다.-
바꿔 말하면, 현재 Command를 추가하는 Command List를 제외한 모든 Command List들은 닫혀 있어야 한다.
-
그래야 Command List의 모든 Command가 Allocator 안에 인접해서 저장된다.
-
-
Command List를 Create 하거나 Reset하면 Command List가 열린 상태가 된다.
-
때문에 같은 Allocator로 두 Command List를 연달아 Create하면 오류가 발생한다.
-
Reset
HRESULT ID3D12GraphicsCommandList::Reset(
ID3D12CommandAllocator *pAllocator,
ID3D12PipelineState *pInitialState
);
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12graphicscommandlist-reset
-
Reset 함수는 Command List뿐만 아니라 Command Allocator에서도 제공한다.
-
예를 들어, 하나의 Frame을 완성하는데 필요한 Rendering 명령들을 모두 GPU에 제출 한 후에는
Command Allocator의 Memory를 다음 Frame을 위해 재사용해야 할 것이다.
-
ID3D12CommandAllocator::Reset 메서드는 이 때 사용된다.
-
HRESULT ID3D12CommandAllocator::Reset();
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12commandallocator-reset
-
하지만 Command Queue가 Allocator 안의 자료를 참조하고 있을수도 있다.
-
때문에 GPU가 Command Allocator에 담긴 모든 Command를 실행했음이 확실해지기 전까지
Command Allocator를 재설정하지 말아야 한다.
-
CPU/GPU 동기화
-
한 System에서 두 개의 Processor가 병렬로 실행되다 보니 여러 동기화문제가 발생한다.
-
이에 대한 해결법 중 하나는 GPU가 Command Queue의 Command들 중
특정 지점까지의 모든 Command를 다 처리할 때까지 CPU를 기다리게 하는 것이다.
-
-
Queue의 모든 Command를 처리하는 것을 Flush라고 한다.
-
이 때 필요한 것은 Fence라는 객체이다.
-
Fence는 ID3D12Fence Interface로 구현하며 GPU와 CPU의 동기화를 위한 수단으로 쓰인다.
-
Fence를 생성하는 메서드는 다음과 같다.
-
HRESULT ID3D12Device::CreateFence(
UINT64 InitialValue,
D3D12_FENCE_FLAGS Flags,
REFIID riid,
void **ppFence
);
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-createfence
-
Fence 객체는 UINT64 값 하나를 관리한다.
-
이는 그냥 시간상의 특정 Fence 지점을 식별하는 정수이다.
-
-
예를 들어, Fence가 하나도 없을 때에는 이 값을 0으로 둔다.
-
새 Fence를 만들 때에는 이 값이 1씩 증가한다.
-
-
이를 아래 예시 코드를 보며 더 자세히 설명을 해보겠다.
UINT64 mCurrentFence = 0;
void D3DApp::FlushCommandQueue()
{
// 현재 Fence 지점까지의 명령들을 표시하도록 Fence 값을 전진시킨다.
mCurrentFence++;
// 새 Fence 지점을 설정하는 Signal을 Command Queue에 추가한다.
// 지금 우리는 GPU timeline상에 있으므로 새 Fence 지점은
// GPU가 이 Signal() Command까지의 모든 명령을 처리하기 전까지는 설정되지 않는다.
ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));
// GPU가 이 Fence 지점까지의 Command들을 완료할 때까지 기다린다.
if(mFence->GetCompletedValue() < mCurrentFence)
{
HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);
// GPU가 현재 Fence 지점에 도달했으면 이벤트를 발동한다.
ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));
// GPU가 현재 Fence 지점에 도달했음을 뜻하는 이벤트를 기다린다.
WaitForSingleObject(eventHandle, INFINITE);
CloseHandle(eventHandle);
}
}
-
하지만 이것은 이상적인 해결책은 아니다.
-
GPU의 작업이 끝날 때까지 CPU가 기다려야하기 때문이다.
-
-
하지만 충분히 간단한 해결책이므로 당분간은 이 방법을 이용한다.
-
Command Queue를 비우는 시점에는 제약이 거의 없다.
-
특히 한 Frame에서 1번만 비워야 하는 것도 아니다.
-
-
예를 들어 초기화를 위한 GPU Command들이 있다고 하자.
-
먼저 그 Command가 실행되었음이 확실해진 후 Command Allocator를 재설정 하려면
Command Queue를 비운 후 Command Allocator를 Reset하면 된다.
-
-
Resource hazard를 막기 위해 Direct3D는 Resource들에게 State를 부여한다.
-
새로 생성된 Resource는 Default State로 시작한다.
-
-
임의의 State Transition를 Direct3D에게 보고하는 것은 전적으로 Application의 몫이다.
-
덕분에 GPU는 State를 전이하고 Resourece Hazard를 방지하는데 필요한 일들을 자유롭게 진행할 수 있다.
-
-
예를 들어, Texture Resource에 자료를 기록해야 할 때에는 그 Texture의 State를 Render Object State로 설정한다.
-
이후 Texture의 자료를 읽어야 할 때가 되면 State를 Shader Resource State로 Transition 한다.
-
-
Application이 State Transition을 Direct3D에게 보고 함으로써,
GPU는 Resource Hazard를 피하는데 필요한 조치를 할 수 있다.-
쓰기 연산이 완료되길 기다린 후 읽기를 시도 하는 등
-
-
Resource Transition을 보고하는 부담을 Application이 담당하는 것은 성능 때문이다.
-
GPU 입장에서는 Transition이 언제 발생하는지 몰라 항상 자동으로 추적하게 해야 한다.
-
Programmer는 이러한 Transition이 언제 일어아는지 미리 알고 있기에 필요할 때 추적할 수 있다.
-
전이 자원 장벽(Transition Resource Barrier)
-
Resource State Transition은 Transition Resource Barrier들의 Array를 설정해서 지정한다.
-
Array를한번의 API 호출로 여러 개의 Resource를 Transition 할 수 있다.
-
-
코드 상에서는 Resource Barrier는 D3D12_RESOURCE_DESC 구조체로 서술된다.
typedef struct D3D12_RESOURCE_BARRIER {
D3D12_RESOURCE_BARRIER_TYPE Type;
D3D12_RESOURCE_BARRIER_FLAGS Flags;
union {
D3D12_RESOURCE_TRANSITION_BARRIER Transition;
D3D12_RESOURCE_ALIASING_BARRIER Aliasing;
D3D12_RESOURCE_UAV_BARRIER UAV;
};
} D3D12_RESOURCE_BARRIER;
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/ns-d3d12-d3d12_resource_barrier
-
이 책에서는 Direct3D 12의 구조체들에 여러 편의용 메서드들을 추가한 확장 버전을 사용한다.
-
대부분의 구조체들은 이런 편의용 확장 버전들이 존재하며,
이 책에서 사용하는 것들은 Microsoft 사이트에서 내려받을 수 있다.
-
https://docs.microsoft.com/en-us/windows/win32/direct3d12/helper-structures-for-d3d12
https://github.com/microsoft/DirectX-Graphics-Samples
-
Transition Resource Barrier는 GPU에게 Resource의 State가 Transition됨을 알려주는
하나의 Command라고 생각할 수 있다.-
Comand를 통해 Resource State Transition를 알려주는 덕분에,
GPU는 이후 Command들을 실행 할 때 Resource Hazard를 피하는데 필요한 단계를 밟을 수 있다.
-
Command List를 이용한 Multi Thread 활용
-
Direct3D 12는 Multi Thread를 효율적으로 활용할 수 있도록 설계되었다.
-
Command List의 서러계는 Direct3D가 Multi Thread 적용의 장점을 취하는 대표적인 한 방법이다.
-
-
물체가 많은 큰 장면을 다룰 때,
장면 전체를 하나의 Command List로 그리려 하면 구축하는데 CPU 시간이 오래 걸린다.-
이에 대한 자명한 해결책은 여러 개의 Command List를 병렬로 구축하는 것이다.
-
-
Command List 구축에 Multi Thread를 적용 할 때 주의해야 할 점이 쳐가지 있다.
-
Command List와 Command Allocator는 Free-Threaded 하지 않는다.
보통의 경우 여러 Thread가 같은 Command List나 Allocator를 공유하지도 않고,
그 메서드들을 동시에 호출하지도 않는다.따라서, 일반적으로 각 Thread는 각자 자신만의 Command List와 Allocator를 가지게 된다.
-
Command Queue는 Free-Threaded하다.
즉, 여러 Thread가 같은 Command Queue에 접근해서 그 메서드들을 동시에 호출할 수 있다.
특히, Thread들이 각자 생성한 Command List를 동시에 Command Queue에 제출할 수 있다.
-
성능상의 이유로,
Application은 동시에 기록할 수 있는
Command List의 최대 개수를 반드시 초기화 시점에서 설정해야 한다.
-
-
단순함을 위해, 이 책에서는 Multi Thread를 사용하지 않는다.
-
하지만 System Resource 사용을 극대화 하려면 Application이
반드시 Multi Thread를 이용해 Multi Core의 장점을 최대한 활용할 필요가 있다.
-
'내용정리 > DirectX12' 카테고리의 다른 글
20.07.25 개발일지 (0) | 2020.07.25 |
---|---|
20.07.24 일지 (0) | 2020.07.24 |
08. Direct3D의 초기화 - 기본지식 2 (0) | 2020.07.24 |
09. 렌더링 파이프라인 2 (1) | 2020.07.14 |
09. 렌더링 파이프라인 1 (0) | 2020.07.14 |