Graphic Programming에서는 GPU와 CPU. 두 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이다.
다음은 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)));
#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
여기서 __uuidof(**(ppType))은 (**(ppType))의 COM Interface ID로 평가 된다.
보조함수 IID_PPV_ARGS_Helper는 ppType을 void**로 Cast한다.
Direct3D 12 API에는 생성하고자 하는 Interface의 COM ID와 void**를 받는 함수들이 많기 때문에
책에서 이 매크로를 선언하고, 책 내의 예시코드 전반적으로 사용되고 있다.
ExcuteCommandLists
void ID3D12CommandQueue::ExecuteCommandLists(
UINT NumCommandLists,
ID3D12CommandList * const *ppCommandLists
);
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12commandqueue-executecommandlists
ID3D12CommandQueue::ExecuteCommandLists (d3d12.h) - Win32 apps
Submits an array of command lists for execution.
docs.microsoft.com
선언으로 보면 ID3D12CommandList가 Command List를 담당하는 것 같지만,
실제 그래픽 작업을 위한 Command List는 이를 상속하는 ID3D12GraphicsCommandList라는 Interface가 담당한다.
HRESULT Close();
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12graphicscommandlist-close
ID3D12GraphicsCommandList::Close (d3d12.h) - Win32 apps
Indicates that recording to the command list has finished.
docs.microsoft.com
CreateCommandAllocator
Command List에는 ID3D12CommandAllocator가 하나 연관된다.
ExcuteCommandLists로 Command List를 제출하면 Command Queue는 Allocator에 담긴 Command들을 참조한다.
Command Memory Allocator는 다음 메서드를 이용해 생성한다.
HRESULT CreateCommandAllocator(
D3D12_COMMAND_LIST_TYPE type,
REFIID riid,
void **ppCommandAllocator
);
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-createcommandallocator
ID3D12Device::CreateCommandAllocator (d3d12.h) - Win32 apps
Creates a command allocator object.
docs.microsoft.com
D3D12_COMMAND_LIST_TYPE_DIRECT
D3D12_COMMAND_LIST_TYPE_BUNDLE
Bundle을 나타내는 Command List
Command List를 만드는 데에 CPU의 부담이 어느정도 따른다.
Bundle을 추가하면 Driver는 Rendering 도중에 실행이 최적화 되도록 Bundle의 Command들을 PreCompile한다.
Application이 특정 Command List들을 구축하는데 시간이 오래 걸린다면, 이를 고려할 필요가 있다.
즉, Bundle을 무조건 사용하지는 말아야 한다.
riid
ppCommandAllocator
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
} ;
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12device-createcommandallocator
ID3D12Device::CreateCommandAllocator (d3d12.h) - Win32 apps
Creates a command allocator object.
docs.microsoft.com
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
ID3D12Device::CreateCommandList - Win32 apps
Creates a command list.
docs.microsoft.com
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
ID3D12Device::GetNodeCount (d3d12.h) - Win32 apps
Reports the number of physical adapters (nodes) that are associated with this device.
docs.microsoft.com
type
pInitialState
Command List의 초기 Pipeline 상태를 지정한다.
Bundle이거나 초기화 목적으로 쓰이고 실제 그리기 명령이 없는 Command List의 경우 null을 지정한다.
자세한 내용은 나중에 추가로 정리 할 예정이다.
riid
ppCommandList
한 Allocator를 여러 Command List에 연관시켜도 되지만, Command를 여러 Command List에 동시에 기록할 수는 없다.
바꿔 말하면, 현재 Command를 추가하는 Command List를 제외한 모든 Command List들은 닫혀 있어야 한다.
그래야 Command List의 모든 Command가 Allocator 안에 인접해서 저장된다.
Command List를 Create 하거나 Reset하면 Command List가 열린 상태가 된다.
Reset
HRESULT ID3D12GraphicsCommandList::Reset(
ID3D12CommandAllocator *pAllocator,
ID3D12PipelineState *pInitialState
);
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12graphicscommandlist-reset
ID3D12GraphicsCommandList::Reset (d3d12.h) - Win32 apps
Resets a command list back to its initial state as if a new command list was just created.
docs.microsoft.com
HRESULT ID3D12CommandAllocator::Reset();
https://docs.microsoft.com/en-us/windows/win32/api/d3d12/nf-d3d12-id3d12commandallocator-reset
ID3D12CommandAllocator::Reset (d3d12.h) - Win32 apps
Indicates to re-use the memory that is associated with the command allocator.
docs.microsoft.com
CPU/GPU 동기화
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
ID3D12Device::CreateFence (d3d12.h) - Win32 apps
Creates a fence object.
docs.microsoft.com
Fence 객체는 UINT64 값 하나를 관리한다.
예를 들어, Fence가 하나도 없을 때에는 이 값을 0으로 둔다.
이를 아래 예시 코드를 보며 더 자세히 설명을 해보겠다.
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);
}
}
하지만 이것은 이상적인 해결책은 아니다.
하지만 충분히 간단한 해결책이므로 당분간은 이 방법을 이용한다.
Command Queue를 비우는 시점에는 제약이 거의 없다.
예를 들어 초기화를 위한 GPU Command들이 있다고 하자.
Resource hazard를 막기 위해 Direct3D는 Resource들에게 State를 부여한다.
임의의 State Transition를 Direct3D에게 보고하는 것은 전적으로 Application의 몫이다.
예를 들어, Texture Resource에 자료를 기록해야 할 때에는 그 Texture의 State를 Render Object State로 설정한다.
Application이 State Transition을 Direct3D에게 보고 함으로써, GPU는 Resource Hazard를 피하는데 필요한 조치를 할 수 있다.
Resource Transition을 보고하는 부담을 Application이 담당하는 것은 성능 때문이다.
전이 자원 장벽(Transition Resource Barrier)
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
D3D12_RESOURCE_BARRIER (d3d12.h) - Win32 apps
Describes a resource barrier (transition in resource use).
docs.microsoft.com
https://docs.microsoft.com/en-us/windows/win32/direct3d12/helper-structures-for-d3d12
Helper Structures for D3D12 - Win32 apps
These helper structures help initialize many of the Direct3D 12 structures, and are declared in d3dx12.h.
docs.microsoft.com
https://github.com/microsoft/DirectX-Graphics-Samples
microsoft/DirectX-Graphics-Samples
This repo contains the DirectX Graphics samples that demonstrate how to build graphics intensive applications on Windows. - microsoft/DirectX-Graphics-Samples
github.com
Command List를 이용한 Multi Thread 활용
Direct3D 12는 Multi Thread를 효율적으로 활용할 수 있도록 설계되었다.
물체가 많은 큰 장면을 다룰 때, 장면 전체를 하나의 Command List로 그리려 하면 구축하는데 CPU 시간이 오래 걸린다.
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를 사용하지 않는다.