DirectX

2. CPU와 GPU의 상호작용

Awesome Red Tomato 2021. 12. 17. 22:39

그래픽 프로그래밍은 CPU와 GPU를 굴린다. 이들은 병렬로 작동하는데 최대한 본전을 뽑아먹으려면 동기화를 최소화 해야한다. 동기화란 한 처리 장치가 작업을 마칠 때 까지 다른 처리 장치가 놀고있어야 함을 의미하며, 따라서 성능에 바람직하지 않다.

 

1. 명령 대기열(Command Queue)과 명령 목록(Command List)

GPU에는 명령 대기열이 하나 있다. CPU는 그리기 명령들이 담긴 명령 목록을 제출한다. 중요한 점은 명령 목록을 제출했다고 바로 실행되는 것이 아니라는 것이다. 앞선 명령들을 GPU가 처리해야만 그 다음 명령 목록을 처리하기 시작하는다 이 명령을 처리하는 동안 CPU는 놀게된다. 그 반대로 명령 대기열이 비어있으면 GPU는 놀게된다. 용납할 수 없다. CPU고 GPU고 최대한 뼈빠지게 일하게 만들어야 하는 것이 목표다.

CommandQueue

 

ID3D12CommandQueue

D3D12에서 대표되는 명령대기열 인터페이스다. 이 인터페이스가 뭐하는 놈인지 어떻게 생겨먹은 놈인지를D3D12_COMMAND_QUEUE_DESC 로 서술해준다.

Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
  
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;

mDevice->CreateCommandQueue(	
	&queueDesc, 
	IID_PPV_ARGS(mCommandQueue.GetAddressOf()));

 

명령목록(명령 리스트)을 명령 대기열(명령 큐)에 넣으려면 ID3D12CommandQueue::ExcuteCommadLists() 메서드를 이용하면 된다. 다만 명령 큐에 넣었다고 즉시 실행되는 것은 아니다. 그저 은행업무(그래픽처리)를 위해 은행 창구 번호표를 뽑고 차례를 기다리게(큐 대기) 하는것 뿐이다.

 

이제 명령 리스트를 다 추가했으면 ID3D12GraphicsCommandList::Close() 메서드를 호출해 명령 기록이 끝났음을 D3D에게 알려주어야 한다.

 

 

ID3D12CommandAllocator

명령 리스트에는 메모리 할당자가 하나 연관된다. 명령 큐에 추가된 명령들은 이 할당자의 메모리에 저장된다. ExcuteCommandLists()로 명령을 제출하면, 명령 큐는 이 할당자에 담긴 명령을 참조한다. 명령 할당자는 다음과 같이 생성한다.

mDevice->CreateCommandAllocator(
	D3D12_COMMAND_LIST_TYPE_DIRECT,
	IID_PPV_ARGS(mCommandAllocator.GetAddressOf()));

 

ID3D12CommandList

명령 리스트도 ID3D12Device로 생성한다.

mDevice->CreateCommandList(
	0,
	D3D12_COMMAND_LIST_TYPE_DIRECT,
	mCommandAllocator.Get(), 
	nullptr,             
	IID_PPV_ARGS(mCommandList.GetAddressOf())));

명령 리스트를 추가, 생성, 재설정하면 명령 목록은 '열림' 상태가 되는데 꼭 닫아줘야 한다(CommandList::Close()).

 

 

하나의 프레임을 완성하는데 필요한 명령들을 모두 GPU에 제출하고 나면, 명령 할당자의 메모리를 다음 프레임을 위해 재사용해야 할 것이다. 이 때 ID3D12CommandAllocator::Reset() 메서드를 사용한다. 이 메서드는 용량은 그대로 내용만 비우는 것이다. 중요한 것은 명령 큐가 이 메모리의 자료를 참조하고 있을 수도 있으므로 GPU가 확실하게 처리하기 전까지는 리셋하면 안된다.

 

 

2. CPU/GPU 동기화

대기열의 명령을 모두(특정 지점까지) 처리하는 것을 '명령 대기열을 비운다' 또는 '방출한다(Flush)' 고 한다. 위 문제를 해결하기 위해 나온 수단이 울타리(Fence)라고 부르는 객체이다. 울타리는 ID3D12Fence 인터페이스가 대표적이다.

mDevice->CreateFence(
		0,
		D3D12_FENCE_FLAG_NONE,
		IID_PPV_ARGS(mFence.GetAddressOf()));

 

울타리 객체는 UINT64 값 하나를 관리한다. 이 값은 그냥 시간상의 특정 울타리 지점을 식별하는 정수이다. 이 책은 처음에는 이 값을 0으로 두고 새 울타리 지점을 만들 때마다 값을 1씩 증가시킨다.

UINT64 mCurrentFence = 0;
void D3DApp::FlushCommandQueue()
{
	++mCurrentFence;

	// 새 울타리 지점을 설정한다.
	ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));

	// GPU가 이 울타리 지점까지의 명령들을 완료할 때까지 기다린다.
	if (mFence->GetCompletedValue() < mCurrentFence)
	{
		HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS);

		// GPU가 현 울타리 지점까지 도달했으면 이벤트를 발동한다.
		ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle));

		// GPU가 현재 울타리 지점에 도달했음을 뜻하는 이벤트를 기다린다.
		WaitForSingleObject(eventHandle, INFINITE);
		CloseHandle(eventHandle);
	}
}

 

'DirectX' 카테고리의 다른 글

5. 프레임워크  (0) 2021.12.22
4. 타이머  (0) 2021.12.20
0. Direct3D 12 개요  (0) 2021.12.20
3. Direct3D의 초기화  (0) 2021.12.19
1. 자원 상태 전이  (0) 2021.12.19