비타Cpp

CPU와 GPU의 상호작용 - CPU/GPU 동기화 본문

DirectX12/메모

CPU와 GPU의 상호작용 - CPU/GPU 동기화

멍C 2021. 10. 1. 13:05

한 시스템에서 두 개의 처리 장치가 병렬로 실행되다 보니 여러 가지 동기화 문제가 발생한다.

이런 문제의 해결책 하나는 GPU가 명령 대기열의 명령들 중 특정 지점까지의 모든 명령을 다 처리할 때까지 CPU를 기다리게 하는 것이다. 대기열의 모든 명령을 처리하는 것을 가리켜 Flush(방출)이라고 한다. 이때 필요한 것이 Fence(울타리)라고 부르는 객체이다. Fence는 ID3D12Fence 인터페이스로 대표되며, GPU와 CPU의 동기화를 위한 수단으로 쓰인다. 다음은 Fence 객체를 생성하는 메서드이다.

 

HRESULT CreateFence(
  UINT64           InitialValue,
  D3D11_FENCE_FLAG Flags,
  REFIID           ReturnedInterface,
  void             **ppFence
);

//예:
ThrowIfFailed(md3dDevice->CreateFence(
0,
D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&mFence)));

Fence객체는 UINT64 값 하나를 관리 한다. 이 값은 그냥 시간상의 특정 울타리 지점을 식별하는 정수이다. 앞으로의 예제들은 처음(Fence 지점이 하나도 없는 상태)에는 이 값을 0으로 두고, 새 울타리 지점을 만들 때마다 이 값을 1씩 증가시킨다. 다음은 울타리를 이용해서 명령 대기열을 비우는 방법을 보여주는 코드이다.

 

UINT64 mCurrentFence = 0;

void D3DApp::FlushCommandQueue()
{
	//Advance the fence value to mark commands up to this fence point.
	mCurrentFence++;

	//Command 큐에 명령을 추가하여 새 Fence 포인트를 설정해야 한다.
	//GPU 타임라인에 있어서, Signal() 이전의 모든 명령이 처리될때까지 새 Fence 포인트가 설정되지 않기 때문이다.(무슨말이야!!)
	ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence));

	//GPU가 이 Fence 포인트까지 명령을 완료할때까지 기다린다.
	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가 Xgpu까지의 명령들을 처리한, 그리고 CPU가 ID3D12CommandQueue::Signal(fence, n+1)을 호출한 직후의 상황을 나타낸 것이다. 그 호출은 울타리 값을 n+1로 변경하는 명령을 대기열의 끝에 추가하는 효과를 낸다. 그러나 GPU가 Signal(fence, n+1) 명령 이전에 명령 대기열에 추가된 명령들을 모두 처리할 때까지는 mFence->GetCompletedValue()가 계속n을 돌려준다.

 

위의 코드는 GPU의 현재 명령들이 완료 되기 전까지 CPU는 놀게 된다. 이러한 방식은 좋은 방식은 아니다. 다행히 간단한 해결방법이 있지만 이는 다음에 수정하겠다.

Comments