KoreanFoodie's Study

4-2. Direct3D 기초 : CPU와 GPU의 상호작용 , 명령 대기열, CPU/GPU 동기화 본문

Game Dev/DirectX

4-2. Direct3D 기초 : CPU와 GPU의 상호작용 , 명령 대기열, CPU/GPU 동기화

GoldGiver 2021. 11. 17. 09:41

DirectX 12 3D 게임 프로그래밍의 바이블

'DirectX 12를 이용한 3D 게임 프로그래밍 입문'을 읽으며 내용을 정리하고 중요한 부분을 기록하는 글입니다.


4-2. Direct3D 기초 : CPU와 GPU의 상호작용 , 명령 대기열, CPU/GPU 동기화

 

알아 두어야 할 개념들 : 

 

명령 대기열과 명령 목록

CPU는 GPU와 상호작용하며 그래픽을 그려준다. 우리는 GPU와 CPU가 각각 쉬지 않고 열심히 일하기를 원한다. CPU/GPU 사이에는 동기화가 필요하지만, 동기화는 병렬성을 망칠 수 있다.

CPU는 GPU에게 명령을 전달하고, GPU는 명령대기열에서 명령을 꺼내 수행한다

즉, 명령 대기열이 꽉 차 CPU가 놀거나 명령 대기열이 비어 있어 GPU가 노는 상황은 방지해야 한다.

 

Direct3D 12에서 명령 대기열을 대표하는 인터페이스는  ID3D12CommandQueue이다. 이 인터페이스를 생성하려면 대기열을 서술하는 D3D12_COMMAND_QUEUE_DESC 구조체를 채운 후 ID3D12Device::CreateCommandQueue를 호출해야 한다.

Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_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)

주요 메서드 중 하나는 명령 목록에 있는 명령들을 대기열에 추가하는 ExecuteCommandLists 메서드이다.

void ID3D12CommandQueue::ExecuteCommandLists( 
  // Number of commands lists in the array
  UINT Count, 
  // Pointer to the first element in an array of command lists
  ID3D12CommandList *const *ppCommandLists);

 

실제 그래픽 작업을 위한 명령 목록은 이 인터페이스를 상속하는 ID3D12GraphicsCommandList라는 인터페이스는 명령들을 명력 목록에 추가하는 여러 메서드가 있다. 다음 코드는 뷰포트를 설정하고, 렌더 대상 뷰를 지우고, 그리기 호출을 실행하는 명령드을 추가한다.

// mCommandList pointer to ID3D12CommandList
mCommandList->RSSetViewports(1, &mScreenViewport);
mCommandList->ClearRenderTargetView(mBackBufferView, 
  Colors::LightSteelBlue, 0, nullptr);
mCommandList->DrawIndexedInstanced(36, 1, 0, 0, 0);

이 코드들은 명령들을 목록에 추가만하고, 나중에 ExecuteCommandLists를 호출해야 명령들이 명령 대기열에 추가되어, 이후 GPU가 명령들을 뽑아 실행한다.

명령들을 명령 목록에 다 추가한 후에는 ID3D12GraphicsCommandList::Close 메서드를 호출해서 명령들의 기록이 끝났음을 Direct3D에 알려 주어야 한다.

mCommandList->Close();

 

명령 목록에는 ID3D12CommandAllocator 형식의 메모리 할당자가 하나 연관된다. 명령 목록에 추가된 명령들은 이 할당자의 메모리에 저장된다. 할당자는 ID3D12Device의 다음의 메서드를 이용해서 생성한다.

HRESULT ID3D12Device::CreateCommandAllocator( 
  // 명령 목록의 종류. 
  // DIRECT는 GPU가 직접 실행. 
  // BUNDLE은 명령을 묶음 단위로 기록하는 최적화 수단을 제공. 예제에서는 사용 X
  D3D12_COMMAND_LIST_TYPE type,
  // 생성하고자 하는 ID3D12CommandAllocator 인터페이스의 COM ID
  REFIID riid,
  // 생성된 명령 할당자를 가리키는 포인터 (출력 매개변수)
  void **ppCommandAllocator);

 

명령 목록 역시 ID3D12Device로 생성한다.

HRESULT ID3D12Device::CreateCommandList( 
  // GPU가 하나인 시스템은 0. 여러 개일 때는 명령 목록과 연결시킬 물리적 GPU 어댑터 노드를 지정
  UINT nodeMask,
  // 명령 목록의 종류. DIRECT 또는 BUNDLE
  D3D12_COMMAND_LIST_TYPE type,
  // 생성된 명령 목록 할당자
  ID3D12CommandAllocator *pCommandAllocator,
  // 명령 목록의 초기 파이프라인 상태 지정
  ID3D12PipelineState *pInitialState,
  // ID3D12CommandList 인터페이스의 COM ID
  REFIID riid,
  // 생성된 명령 목록을 가리키는 포인터 (출력 매개변수)
  void **ppCommandList);

한 할당자를 여러 명령 목록에 연관시켜도 되지만, 명령들을 여러 명령 목록에 동시에 기록할 수는 없다. 현재 명령들을 추가하는 명령 목록을 제외한 모든 명령 목록은 닫혀 있어야 한다. 따라서 같은 할당자로 두 명령 목록을 연달아 생성하면 다음과 같은 오류가 발생한다.

D3D12 ERROR: ID3D12CommandList::{Create,Reset}CommandList: 
The command allocator is currently in-use by another command list.

ID3D12CommandQueue::ExecuteCommandList(C)를 호출한 후 ID3D12CommandList::Reset 메서드를 호출하면 C의 내부 메모리를 새로운 명령들을 기록하는 데 재사용할 수 있게 된다. Reset 메서드의 매개변수들의 의미는 ID3D12Device::CreateCommandList의 해당 매개변수들의 의미와 같다.

HRESULT ID3D12CommandList::Reset( 
  ID3D12CommandAllocator *pAllocator,
  ID3D12PipelineState *pInitialState);

명령 목록을 재설정해도 명령 대기열이 참조하는 명령들은 연관된 명령 할당자의 메모리에 여전히 남아 있으므로 상관이 없다.

 

하나의 프레임을 완성하는 데 필요한 렌더링 명령들을 모두 GPU에 제출한 후, 명령 할당자의 메모리를 다음 프레임을 위해 재사용하기 위해 ID3D12CommandAllocator::Reset 메서드를 사용한다.

HRESULT ID3D12CommandAllocator::Reset(void);

이 개념은 std::vector::clear을 호출하는 것과 비슷하다. 단, 명령 대기열이 할당자 안의 자료를 참조하고 있을 수도 있으므로, GPU가 명령 할당자에 담긴 모든 명령이 실행했음이 확실해지기전 까지는 명령 할당자를 재설정하지 말아야 한다.

 

 

CPU/GPU 동기화

 

Comments