• 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
);

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

  • 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
);

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

  • 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
} ;

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

  • 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

 

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

  • 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

 

ID3D12CommandAllocator::Reset (d3d12.h) - Win32 apps

Indicates to re-use the memory that is associated with the command allocator.

docs.microsoft.com

  • 하지만 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

 

ID3D12Device::CreateFence (d3d12.h) - Win32 apps

Creates a fence object.

docs.microsoft.com

  • 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

 

D3D12_RESOURCE_BARRIER (d3d12.h) - Win32 apps

Describes a resource barrier (transition in resource use).

docs.microsoft.com

  • 이 책에서는 Direct3D 12의 구조체들에 여러 편의용 메서드들을 추가한 확장 버전을 사용한다.

    • 대부분의 구조체들은 이런 편의용 확장 버전들이 존재하며,
      이 책에서 사용하는 것들은 Microsoft 사이트에서 내려받을 수 있다.

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

  • 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를 적용 할 때 주의해야 할 점이 쳐가지 있다.

    1. Command List와 Command Allocator는 Free-Threaded 하지 않는다.

      보통의 경우 여러 Thread가 같은 Command List나 Allocator를 공유하지도 않고,
      그 메서드들을 동시에 호출하지도 않는다.

      따라서, 일반적으로 각 Thread는 각자 자신만의 Command List와 Allocator를 가지게 된다.

    2. Command Queue는 Free-Threaded하다.

      즉, 여러 Thread가 같은 Command Queue에 접근해서 그 메서드들을 동시에 호출할 수 있다.

      특히, Thread들이 각자 생성한 Command List를 동시에 Command Queue에 제출할 수 있다.

    3. 성능상의 이유로,
      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

+ Recent posts