티스토리 뷰

개요

이번 시간에는 DirectX12GPU 동기화에 대해서 알아보겠습니다.

 

DirectX12는 기존 버전의 DirectX나 OpenGL과는 다르게 CPU와 GPU의 동기화를 위해 별도의 명시적인 작업을 해주어야 합니다. 기존 DirectX에서는 관련 작업을 driver가 자동으로 해주어서 그래픽 프로그래머가 좀 더 간편하게 코딩할 수 있었지만, DirectX12부터는 프로그래머가 직접 컨트롤 할 수 있게 되어서 어플리케이션에 좀 더 알맞는 코딩을 할 수 있게 되었습니다. 하지만 프로그래머가 더 신경써야 할 부분이 많아지기는 했습니다.

 

어찌됐든 GPU 동기화는 DirectX12에서 꼭 필요한 요소입니다. GPU 동기화를 하지 않은 경우 CPU와 GPU가 코드로 의도한 순서대로 동작하지 않고, GPU에서 사용중인 리소스를 해제해버리는 이슈가 생길 수 있기 때문입니다.

동기화 구성 요소

GPU 동기화와 관련된 요소는 크게 3가지가 있습니다.

각각은 CommandList, CommandQueue와  Fence입니다.

CommandList

1) CommandList는 GPU에 명령을 내리기 위해 사용되는 DirectX용 객체입니다.

GPU에 내릴 수 있는 명령의 종류로는 copy, compute, draw 등이 있습니다.

관련하여 매우 많은 함수들이 정의되어 있는데 그 중 일부는 아래와 같습니다.
commandList->CopyBufferRegion()

commandList->OMSetRenderTargets()

commandList->DrawIndexedInstanced()

 

CopyBufferRegion은 특정 영역에 값을 복사하는 함수이고

OMSetRenderTargetRenderTarget을 설정하는 것이고

DrawIndexedInstanced는 실제로 DrawCall을 하는 함수입니다.

모두 CommandList를 통해서 명령을 내릴 수 있습니다.

 

이전 버전의 DirectX에서는 CommandList에 명령들을 발행하면 GPU가 알아서 실행을 했었으나, DirectX12에서는 별도로 CommandQueue라는 것을 통해 CommmandList들을 실행해야 GPU에서 실행이 됩니다.

CommandQueue

2) CommandQueue는 GPU에서 실제로 실행할 명령들이 담긴 Queue입니다.

commandQueue->ExecuteCommandLists(commandList);

이런 식으로 함수를 호출하게 되면 그동안 commandList에 쌓였던 명령들이 commandQueue에 쌓여서 GPU가 실행할 수 있게 됩니다.

comandList에 명령을 쌓았다고 바로 GPU가 실행하지 않고 별도로 commandQueue를 만든 이유는 명령을 "적재"하는 과정과 "실행"하는 과정을 분리하기 위함이라고 합니다. 이렇게 함으로써 프로그래머는 좀 더 유연하게 명령을 다룰 수 있게 됩니다.

 

CommandQueue의 타입은 Copy, Compute, Direct 3가지 입니다.

각 타입별로 실행할 수 있는 Command들이 다릅니다.

Copy 타입은 "Copy 관련 Command"만 처리할 수 있습니다.

Compute 타입은 "Copy 관련 Command + 계산(dispatch) 관련 Command"들을 처리할 수 있습니다.

Direct 타입은 "Copy 관련 Command  + 계산 관련 Command + Draw 관련 Command"들을 처리할 수 있습니다.

Direct 타입의 CommandQueue가 가장 많은 Command를 지원하기 때문에 Direct 타입의 CommandQueue만 사용해도 됩니다. 하지만 이렇게 구현을 하면 Command를 병렬처리하는 이점을 얻을 수 없기 때문에 성능이 중요한 경우 여러 타입의 CommandQueue를 사용하는 것이 좋다고 합니다. 하지만 대신 CommandQueue를 여러 개 사용하게되면 동기화가 중요한 경우 코딩의 복잡도가 올라가기는 할 것입니다.

Fence

3) Fence는 CPU와 GPU를 동기화하기 위해 사용하는 객체입니다.

FenceCPU와 GPU를 동기화하는 원리는 Fence Value라는 하나의 값을 관리하며 해당 값이 특정 값이 되었는지 확인하여 코드를 진행시키거나 기다리게 하거나 하는 방식입니다. 예를 들어 CPU에서는 다음 프레임으로 넘기기 전에 Fence 값이 10이 될 때까지 기다리고, GPU에서는 현재 프레임에 관련된 모든 명령을 실행한 후에 Fence 값을 10으로 셋팅하는 방식으로 동기화를 맞추는 것입니다.

 

Fence를 통해 취할 수 있는 액션은 SignalWait 두 가지 입니다.

Signal은 Fence Value가 특정 값이 되도록 셋팅하는 것이고, Wait는 Fence Value가 특정 값이 될 때까지 Wait하는 것입니다. CPU와 GPU 모두에서 Signal을 할 수도 있고, Wait를 할 수도 있습니다.

Signal

fence->Signal(fenceValue);

CPU에서 Signal을 하는 방법은 단순히 fenceSignal 함수를 호출하는 것입니다.

 

commandQueue->Signal(fence, fenceValue);

GPU에서 Signal을 하는 방법은 CommandQueueSignal 함수를 호출하며 fence 객체를 넘기는 것입니다.

CommendQueue에 넘김으로써 GPU가 해당 FenceSignal을 하는 명령을 실행할 수 있도록 합니다.

Wait

CPU에서 Wait하는 방법은 구현하기 나름이지만 보통 사용하는 방법은 fenceGetCompletedValue() 함수SetEventOnCompletion(fenceValue, fenceEvent) 함수 그리고 WaitForSingleObject(fenceEvent, waitTime) 함수를 사용하는 방식입니다.

fence->GetCompletedValue() 함수는 현재 fence에 설정된 값을 리턴합니다.

종전에 fence value 20으로 signal이 되었다면 fence->GetCompletedValue() 함수가 리턴하는 값도 20입니다.

fence->SetEventOnCompletion(fenceValue, fenceEvent) 함수는 fence에 셋팅된 값이 fenceValue가 되면, fenceEvent를 발생시키라는 의미의 함수입니다.

WaitForSingleObject(fenceEvent, waitTime) 함수는 fenceEvent가 발생될 때까지 최대 waitTime 동안 기다리라는 함수입니다. 

위 함수들을 통해서 CPU에서 Wait하는 방법은 가장 아래쪽에 예시에서 설명드리도록 하겠습니다.

 

commandQueue->Wait(fence, fenceValue);

GPU에서 Wait하는 방법은 Signal과 마찬가지로 CommandQueue의 함수를 호출하며 fence 객체를 넘깁니다.

이렇게 함으로써 fencefenceValue 값이 될 때까지 Wait 다음에 추가된 명령들을 실행되지 않습니다.

CPU & GPU 동기화 예시

위에서 설명했던 요소들을 이용해 CPU와 GPU를 동기화 하는 간단한 예시를 설명드리겠습니다.

실제 DirectX12에서 사용하는 함수를 사용하지는 않았고 흐름 정도만 서술하였습니다.

1) commandList->SetRenderPipeline(), commandList->Draw() 등의 Rendering 관련 함수들을 호출, GPU에서 실행하게될 명령들이 적재됨

2) commandQueue->ExecuteCommandLists(commandList) 함수를 호출해서 commandList들을 commandQueue에 옮겨둠, GPU에서는 해당 명령들을 순차적으로 실행할 수 있게됨

3) commandQueue->Signal(fence, fenceValue) 함수를 호출해서 GPU에서 Signal을 하는 것을 예약해둠, CommandQueue에 쌓인 모든 명령이 수행되고 나면 fence가 가진 값이 fenceValue로 셋팅되게 됨

4) CPU에서는 다음 프레임을 그리기 전에 fence 값이 fenceValue가 되기를 기다려야 하는 상황, 먼저 fence->GetCompletedValue() 함수를 호출해서 기대하는 fenceValue가 되었는지 확인

5) 만약 기대한 fenceValue가 아닌 경우, fence->SetEventOnCompletion(fenceValue, fenceEvent)를 호출해서 fenceValue가 되면 fenceEvent가 발생하도록 함

6) 바로 뒤에 fence->WaitForSingleObject(fenceEvent, waitTime) 함수를 호출해서 기대한 fenceValue가 될 때까지 다음 코드를 실행하지 않고 대기를 함

7) GPU에서 모든 명령을 실행하고 fenceSignal이 발생하였다면 기대한 fenceValue가 되어서 fenceEvent가 발생할 것이고, CPU는 Wait를 끝마치고 다음 코드를 실행하게 됨, 다시 1)번으로 돌아감

 

지금까지 DirectX12에서의 CPU와 GPU 동기화에 대해서 알아보았습니다.

간단하게 사용한다면 어렵지 않지만 효율적으로 동기화를 하려면 많은 처리를 해주어야 할 것으로 보입니다.

 

 

 

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/11   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함