비타Cpp

CPU와 GPU의 상호작용 - Command Queue, Comand List 본문

DirectX12/메모

CPU와 GPU의 상호작용 - Command Queue, Comand List

멍C 2021. 9. 27. 19:08

그래픽 프로그래밍에서는 두 가지 처리장치가 작동한다. 하나는 GPU이고 또 하나는 CPU이다. 이들은 병렬로 작동 하지만, 종종 동기화가 필요하다. 하지만 최적의 성능을 위해서는 동기화를 최소화하여야 한다. 동기화는 한 처리 장치가 작업을 마칠 때까지 다른 한 장치가 놀고 있어야 함을 의미하며, 성능에 바람직하지 않다.

 

CPU에는 명령 대기열(Command Queue)가 하나 있다. CPU는 그리기 명령들이 담긴 명령 목록(Command List)을 Direct3D API를 통해서 그 대기열에 제출한다.

여기서 중요한 점은, 일단 Command List의 명령들을 제출했다고 해도, 그 명령들이 GPU에서 즉시 실행하는 것은 아니라는 점이다. GPU가 처리할 준비가 되어있어야 비로소 실행되기 시작한다. 즉, GPU가 이전 명령으로 바쁘게 돌아가는 동안 명령들은 Queue에 남아 있게 된다.

 

Command Queue가 비어버리면 GPU는 처리할 명령이 없어 놀게 된다. 반대로 Queue가 꽉 차 버리면 GPU가 명령을 처리할 때까지 Command List의 명령들은 제출되지 않는다. 두 가지 모두 바람직하지 않은 상황이며, 게임 같은 고성능 응용 프로그램에서는 가용 자원을 최대한 활용하도록, CPU와 GPU 모두 쉬지 않고 돌아가게 만드는 게 좋다.

 

Direct3D 12에서 Command Queue를 대표하는 인터페이스는 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)

__uuidof(**(ppType))은 (**(ppType))의 COM 인터페이스 ID로 평가되는데, 앞의 예에서 그 ID는 ID3D12CommandQueue이다. 보조 매크로 IID_PPV_ARGS는 ppType을 **void로 캐스팅한다.

 

*참고 : UUID 

https://docs.microsoft.com/ko-kr/cpp/windows/attributes/uuid-cpp-attributes?view=msvc-160 

 

uuid(C++ 특성)

자세한 정보: uuid (c + + 특성)

docs.microsoft.com

 

이 인터페이스의 주요 메서드중 하나는 Command List에 있는 명령들을 Queue에 추가하는 ExcuteCommandLists 메서드다.

 

void ExecuteCommandLists(
  //배열에 있는 명령 목록들의 개수
  UINT              NumCommandLists,
  //명령 목록들의 배열의 첫 원소를 가리키는 포인터
  ID3D12CommandList * const *ppCommandLists
);

Command Queue는 배열의 첫 원소부터 차례로 실행된다.

 

위의 메서드의 선언에서 보이듯이 Command List를 대표하는 인터페이스는 ID3D12CommandList이다. 그러나 실제 그래픽 작업을 위한 Command List는 이 인터페이스를 상속하는 ID3D12GraphicsCommandList라는 인터페이스로 대표된다. ID3D12GraphicsCommandList 인터페이스에는 명령들을 Command List에 추가하는 여러 메서드가 있다.

 

다음 예는 뷰포트를 설정하고, 렌더 타겟 뷰를 지우고, 그리기 호출을 실행하는 명령들을 추가한다.

mCommandList->RSSetViewports(1, &mScreenViewport);
	
mCommandList->ClearRenderTargetView(CurrentBackBufferView(), 
   	Colors::LightSteelBlue, 0, nullptr);

mCommandList->DrawIndexedInstanced(
	mBoxGeo->DrawArgs["box"].IndexCount,
	1, 0, 0, 0);

이 메서드들의 이름을 보면 왠지 명령들이 즉시 실행될 것 같지만, 실제로는 명령들을 Command List에 추가하기만 한다. 나중에 ExcuteCommandLists를 호출해야 비로소 명령들이 Command Queue에 추가된다. 그러면 GPU가 명령들을 뽑아서 실행한다. 명령들을 Command List에 다 추가했으면, ID3D12GraphicsCommandList::Close 메서드를 호출해서 명령 기록이 끝났음을 Direct3D에게 알려주어야 한다.

//명령들의 기록을 마친다.
ThrowIfFailed(mCommandList->Close());

ID3D12CommandQueue::ExcuteCommandLists로 Command List를 제출하기 전에 반드시 이 메서드를 이용해서 Command List를 Close해야함을 기억하자.

 

Command List에는 ID3D12CommandAllocator 형식의 메모리 할당자가 하나 연관된다. Command List에 추가된 명령들은 이 할당자의 메모리에 저장된다. ID3D12CommandQueue::ExcuteCommandLists로 명령 목록을 제출하면, Command Queue는 그 할당자에 담긴 명령들을 참조한다. 명령 메모리 할당자는 ID3D12Device의 다음과 같은 메서드를 이용하여 생성한다.

HRESULT CreateCommandAllocator(
  D3D12_COMMAND_LIST_TYPE type,
  REFIID                  riid,
  void                    **ppCommandAllocator
);

1. type: 이 할당자와 연관시킬 수 있는 명령 목록의 종류.

2. riid: 생성하고자 하는 ID3D12CommandAllocator 인터페이스의 COM ID.

3. ppCommandAllocator: 생성된 명령 할당자를 가리키는 포인터(출력매개 변수)

 

Command List 역시 ID3D12Device로 생성한다. 해당 메서드는 다음과 같다.

HRESULT CreateCommandList(
  UINT                    nodeMask,
  D3D12_COMMAND_LIST_TYPE type,
  ID3D12CommandAllocator  *pCommandAllocator,
  ID3D12PipelineState     *pInitialState,
  REFIID                  riid,
  void                    **ppCommandList
);

1. nodeMask: GPU가 하나인 시스템에서는 0으로 설정하면 된다. GPU가 여러 개일 때에는 이 Command List와 연관시킬 물리적 GPU 어댑터 노드들을 지정하는 비트 마스크 값을 설정한다.

2. type: Command List의 종류.

3. pCommandAllocator: 생성된 Command List에 연관시킬 할당자. 그 명령 할당자의 type은 Command List의 type과 일치해야 한다.

4. pInitialState: 명령 목록의 초기 파이프라인 상태를 지정한다.

5. riid: 생성하고자 하는 Command List에 해당하는 ID3D12CommandList 인터페이스의 COM ID.

6. ppCommandList: 생성된 Command List를 가리키는 포인터(출력 매개변수)

 

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

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

이 메서드는 주어진 Command List를 마치 처음 생성했을 때와 같은 상태로 만든다. 이 메서드를 이용하면 Command List를 해제하고 새로이 할당하는 번거로움 없이 Command List 내부 메모리를 재사용할 수 있다. 이렇게 Command List를 재설정해도 Command Queue가 참조하는 명령에는 아무런 영향을 미치지 않는다. 해당 명령들은 명령 할당자의 메모리에 여전히 남아 있기 때문이다.

 

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

HRESULT ID3D12CommandAllocator::Reset(void);

명령 할당자는 GPU가 해당 할당자의 명령을 실행했음이 확실해지기 전까지는 재설정해서는 안된다.

Comments