티스토리 뷰
개요
이번 시간에는 DirectX12의 GPU 동기화에 대해서 알아보겠습니다.
DirectX12는 기존 버전의 DirectX나 OpenGL과는 다르게 CPU와 GPU의 동기화를 위해 별도의 명시적인 작업을 해주어야 합니다. 기존 DirectX에서는 관련 작업을 driver가 자동으로 해주어서 그래픽 프로그래머가 좀 더 간편하게 코딩할 수 있었지만, DirectX12부터는 프로그래머가 직접 컨트롤 할 수 있게 되어서 어플리케이션에 좀 더 알맞는 코딩을 할 수 있게 되었습니다. 하지만 프로그래머가 더 신경써야 할 부분이 많아지기는 했습니다.
어찌됐든 GPU 동기화는 DirectX12에서 꼭 필요한 요소입니다. GPU 동기화를 하지 않은 경우 CPU와 GPU가 코드로 의도한 순서대로 동작하지 않고, GPU에서 사용중인 리소스를 해제해버리는 이슈가 생길 수 있기 때문입니다.
동기화 구성 요소
GPU 동기화와 관련된 요소는 크게 4가지가 있습니다.
각각은 CommandAllocator, CommandList, CommandQueue와 Fence입니다.
CommandAllocator
GPU에서 실행할 명령(Command)들을 저장하는 메모리 공간입니다.
CommandList 객체와 연결된 상태에서 CommandList 객체로 GPU에 명령을 내리는 함수들을 호출하면 해당 메모리 공간에 Command들이 쌓이게 됩니다.
CommandList
CommandList는 GPU에 명령을 내리기 위해 사용되는 DirectX용 객체입니다.
GPU에 내릴 수 있는 명령의 종류로는 copy, compute, draw 등이 있습니다.
관련하여 매우 많은 함수들이 정의되어 있는데 그 중 일부는 아래와 같습니다.
commandList->CopyBufferRegion()
commandList->OMSetRenderTargets()
commandList->DrawIndexedInstanced()
CopyBufferRegion은 특정 영역에 값을 복사하는 함수이고
OMSetRenderTarget은 RenderTarget을 설정하는 것이고
DrawIndexedInstanced는 실제로 DrawCall을 하는 함수입니다.
모두 CommandList를 통해서 명령을 내릴 수 있습니다.
위 함수들을 호출하면 Command들이 연결된 CommandAllocator에 쌓이게 됩니다.
이전 버전의 DirectX에서는 CommandList에 명령들을 발행하면 GPU가 알아서 실행을 했었으나, DirectX12에서는 별도로 CommandQueue라는 것을 통해 CommmandList들을 실행해야 GPU에서 실행이 됩니다.
CommandQueue
CommandQueue는 GPU에서 실제로 실행할 명령들이 담긴 Queue입니다.
commandQueue->ExecuteCommandLists(commandList);
이런 식으로 함수를 호출하게 되면 그동안 commandList에 의해 쌓였던 명령들이 commandQueue에 쌓여서 GPU가 실행할 수 있게 됩니다.
comandList에 명령을 쌓았다고 바로 GPU가 실행하지 않고 별도로 commandQueue를 만든 이유는 명령을 "적재"하는 과정과 "실행"하는 과정을 분리하기 위함이라고 합니다.
CommandList는 명령을 적재하는 역할을 하고, ExecuteCommandLists를 함으로써 명령들이 실행되도록 합니다.
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
Fence는 CPU와 GPU를 동기화하기 위해 사용하는 객체입니다.
Fence가 CPU와 GPU를 동기화하는 원리는 Fence Value라는 하나의 값을 관리하며 해당 값이 특정 값이 되었는지 확인하여 코드를 진행시키거나 기다리게 하거나 하는 방식입니다. 예를 들어 CPU에서는 다음 프레임으로 넘기기 전에 Fence 값이 10이 될 때까지 기다리고, GPU에서는 현재 프레임에 관련된 모든 명령을 실행한 후에 Fence 값을 10으로 셋팅하는 방식으로 동기화를 맞추는 것입니다.
Fence를 통해 취할 수 있는 액션은 Signal과 Wait 두 가지 입니다.
Signal은 Fence Value가 특정 값이 되도록 셋팅하는 것이고, Wait는 Fence Value가 특정 값이 될 때까지 Wait하는 것입니다. CPU와 GPU 모두에서 Signal을 할 수도 있고, Wait를 할 수도 있습니다.
Signal - CPU
fence->Signal(fenceValue);
CPU에서 Signal을 하는 방법은 단순히 fence의 Signal 함수를 호출하는 것입니다.
Signal - GPU
commandQueue->Signal(fence, fenceValue);
GPU에서 Signal을 하는 방법은 CommandQueue의 Signal 함수를 호출하며 fence 객체를 넘기는 것입니다.
CommendQueue에 넘김으로써 GPU가 해당 Fence에 Signal을 하는 명령을 실행할 수 있도록 합니다.
Wait - CPU
CPU에서 Wait하는 방법은 구현하기 나름이지만 보통 사용하는 방법은 fence의 GetCompletedValue() 함수와 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 동안 기다리라는 함수입니다.
단순히 fenceValue가 특정 값이 될 때까지 기다리기 보다는 중간에 Event를 둬서 처리합니다.
특정 FenceValue가 되면 Event를 호출하도록 등록 -> 해당 Event가 호출될까지 대기하는 방식으로 처리됩니다.
위 함수들을 통해서 CPU에서 Wait하는 방법은 가장 아래쪽에 예시에서 설명드리도록 하겠습니다.
Wait - GPU
commandQueue->Wait(fence, fenceValue);
GPU에서 Wait하는 방법은 Signal과 마찬가지로 CommandQueue의 함수를 호출하며 fence 객체를 넘깁니다.
이렇게 함으로써 fence가 fenceValue 값이 될 때까지 Wait 다음에 추가된 명령들을 실행되지 않습니다.
CPU & GPU 동기화 예시
위에서 설명했던 요소들을 이용해 CPU와 GPU를 동기화 하는 간단한 예시를 설명드리겠습니다.
실제 DirectX12에서 사용하는 함수를 사용하지는 않았고 흐름 정도만 서술하였습니다.
1) commandAllocator->Reset() 함수를 호출합니다.
commandAllocator의 Reset 함수는 메모리 공간을 비우는 것입니다.
현재 GPU가 해당 메모리 공간을 사용중일 때는 Reset하면 안됩니다.
2) commandList->Reset(commandAllocator, pso) 함수를 호출합니다.
commandList의 Reset 함수는 commandList를 어떤 allocator와 연결할지 결정하고 commandList를 다시 명령을 쓸 수 있는 상태로 만드는 역할을 합니다.
3) commandList->SetRenderPipeline(), commandList->Draw() 등의 Rendering 관련 함수들을 호출합니다.
GPU에서 실행하게될 명령들이 적재됩니다.
4) commandList->Close() 함수를 호출해서 명령 적재가 종료되었다는 것을 알립니다.
지금까지 적재된 명령들이 commandQueue에 넘길 수 있는 "패킷 묶음"으로 묶이게 됩니다.
5) commandQueue->ExecuteCommandLists(commandList) 함수를 호출해서 commandList들을 commandQueue에 넘겨줍니다.
GPU에서는 해당 명령들을 순차적으로 실행할 수 있게됩니다.
6) commandQueue->Signal(fence, fenceValue) 함수를 호출해서 GPU에서 Signal을 하는 것을 예약해둡니다.
CommandQueue에 쌓인 모든 명령이 수행되고 나면 fence가 가진 값이 fenceValue로 셋팅되게 됩니다.
7) CPU에서는 다음 프레임을 그리는 명령을 GPU에 내리기 전에 fence 값이 fenceValue가 되기를 기다려야 하는 상황입니다.
먼저 fence->GetCompletedValue() 함수를 호출해서 기대하는 fenceValue가 되었는지 확인합니다.
8) 만약 기대한 fenceValue가 아닌 경우, GPU가 아직 이전 프레임을 그리고 있다는 의미이므로
fence->SetEventOnCompletion(fenceValue, fenceEvent)를 호출해서 fenceValue가 되면 fenceEvent가 발생하도록 합니다.
9) 바로 뒤에 fence->WaitForSingleObject(fenceEvent, waitTime) 함수를 호출해서 기대한 fenceEvent가 발생할 때까지 다음 코드를 실행하지 않고 대기를 합니다.
10) GPU에서 모든 명령을 실행하고 fence의 Signal이 발생하였다면 기대한 fenceValue가 되어서 fenceEvent가 발생할 것이고, CPU는 Wait를 끝마치고 다음 코드를 실행하게 됩니다. 다시 1)번으로 돌아갑니다.
지금까지 DirectX12에서의 CPU와 GPU 동기화에 대해서 알아보았습니다.
간단하게 사용한다면 어렵지 않지만 효율적으로 동기화를 하려면 많은 처리를 해주어야 할 것으로 보입니다.
'프로그래밍 > DirectX12' 카테고리의 다른 글
| DirectX12를 활용한 Deferred Shading (0) | 2025.07.22 |
|---|---|
| DirectX12를 활용한 Bloom 효과 (0) | 2025.03.30 |
| DirectX12를 활용한 Instanced Drawing (0) | 2025.03.03 |
| Microsoft PIX를 활용한 DirectX12 프로그램 디버깅 (0) | 2025.01.13 |
| DirectX12를 활용한 PostProcessing (0) | 2024.12.28 |
- Total
- Today
- Yesterday
- normalized device coordinate
- AABB
- 통계학
- 경우의 수
- 수학
- 루빅스큐브
- RL
- 유니티
- 참조 형식
- Unreal
- Unity
- 중복 조합
- CollisionDetection
- Mesh
- C#
- 중복 순열
- DirectX12
- 조합
- 순열
- 값 형식
- VTK
- opengl
- collision detection
- perspective projection
- MeshProcessing
- Scriptable Render Pipeline
- value type
- RubiksCube
- 최적화
- Mesh Processing
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 | 31 |