일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- two pointer
- MYSQL
- 그래프
- Hash
- DP
- Dijkstra
- 다익스트라
- String
- 스토어드 프로시저
- Brute Force
- binary search
- 이진탐색
- union find
- SQL
- Stored Procedure
- Trie
- Two Points
- Today
- Total
codingfarm
5. Fence 본문
0. CPU/GPU 동기화
한 시스템에서 두 개의 처리 장치가 병렬로 실행되다 보니 여러 가지 동기화 문제가 발생한다.
대표적인 동기화 문제는 이전에 제시했듯이 CPU 측에서 ExecuteCommandList 메서드로 command list의 내용을 제출했을때, GPU가 command queue 내에서 아직 처리하지 않은 명령의 내용을 그대로 덮어쓰는 현상이 발생 할수도 있다.
이를 해결하기 위한 해결책중 하나는 Fence를 이용하는 것이다.
Fence
docs.microsoft.com/ko-kr/windows/win32/direct3d12/fence-based-resource-management
GPU가 command queue의 명령들 중 특정 지점까지의 모든 명령을 다 방출(flust) 혹은 처리할 때까지 CPU를 기다리게 하는것이다.
ID3D12Fence : Direct3d 12에서 fence를 대표하는 인터페이스, CPU와 GPU의 호환을 위해 쓰인다.
ID3D12Device::CreateFence : fence 객체를 생성하는 메서드
1 2 3 4 5 6 |
HRESULT CreateFence( UINT64 InitialValue, D3D12_FENCE_FLAGS Flags, REFIID riid, void **ppFence ); Colored by Color Scripter |
cs |
울타리 객체의 생성 이우헤 필요한 서술자들의 크기도 미리 조회해서 설정해둔다. 서술자 크기는 GPU마다 다를 수 있으므로로, 실행 시점에서 적절한 메서드를 호출해서 알아내야 한다. 나중에 서술자 크기가 필요할 때 바로 사용할 수 있도록, 크기들은 적절한 멤버 변수들에 저장해 둔다.
사용 예
1 2 3 4 |
Microsoft::WRL::ComPtr<ID3D12Fence> mFence; ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&mFence))); |
cs |
매개변수 | 기능 |
InitialValue | fence의 초기화 값(initial value)이다. |
Flags | A combination of D3D12_FENCE_FLAGS-typed values that are combined by using a bitwise OR operation. The resulting value specifies options for the fence. |
riid | 생성하고자 하는 ID3D12Fence 인터페이스의 COM ID |
ppFence | 생성된 명령 할당자를 가리키는 포인터(출력 매개변수) |
fence 객체는 UINT64 값 하나를 관리한다. 이 값은 그냥 시간상의 특정 울타리 지점을 식별하는 정수이다.
이 책의 예제들은 처음(울타리 지점이 하나도 없는 상태)에는 이 값을 0으로 두고, 새 울타리 지점을 만들 때마다 이 값을 1씩 증가시킨다.
다음은 울타리를 이용해서 명령 대기열을 비우는 방법을 보여주는 코드이다.
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 32 33 34 |
UINT64 mCurrentFence = 0; void D3DApp::FlushCommandQueue() { // Advance the fence value to mark commands up to this fence point. // 현재 울타리 지점까지의 명령들을 표시하도록 울타리 값을 전진시킨다. mCurrentFence++; // Add an instruction to the command queue to set a new fence point. Because we // are on the GPU timeline, the new fence point won't be set until the GPU finishes // processing all the commands prior to this Signal(). /* 새 울타리 지점을 설정하는 명령(Signal)을 명령 대기열에 추가한다. 지금 우리는 GPU 시간선(timeline) 상에 있으므로, 새 울타리 지점은 GPU가 이 Signal() 명령까지의 모든 명령을 처리하기 전까지는 설정되지 않는다. */ ThrowIfFailed(mCommandQueue->Signal(mFence.Get(), mCurrentFence)); // Wait until the GPU has completed commands up to this fence point. // GPU가 이 울타리 지점까지의 명령들을 완료할 때까지 기다린다. if(mFence->GetCompletedValue() < mCurrentFence) { HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS); // Fire event when GPU hits current fence. // GPU가 현재 울타리 지점에 도달했으면 이벤트를 발동한다. ThrowIfFailed(mFence->SetEventOnCompletion(mCurrentFence, eventHandle)); // Wait until the GPU hits current fence event is fired. // GPU가 현재 울타리 지점에 도달했음을 뜻하는 이벤트를 기다린다. WaitForSingleObject(eventHandle, INFINITE); CloseHandle(eventHandle); } } Colored by Color Scripter |
cs |
16번째 줄 : mCommandQueue에서 제일 마지막에 들어간 command가 저장된 지점에 mFence를 식별자 mCurrentFence와 함께 박는다.
21번째 줄 : mFence->GetCompletedValue() 함수를 통해 현재 mCommandQueue 내에서 제일 최근에 도달한 fence의 식별자를 반환한다. 그리고 mCurrentFence는 CPU가 mCommandQueue에 마지막으로 박은 fence의 식별자를 상징한다.
- mFence->GetCompletedValue() < mCurrentFence
- CPU가 더 빠르므로 GPU의 작업을 기다려야함
- mFence->GetCompletedValue() == mCurrentFence
- GPU가 더 빠르므로 CPU가 안기다려도 됨
위 코드의 작동 프로세스는 아래와 같다.
1. mCurrentFence++;
정수값을 하나 증가시킨다.
이 값은 새롭게 설정될 펜스의 값, 그리고 queue 가 도달해야할 목표 값이 된다,
2. mCommandQueue->Signal(mFence.Get(), mCurrentFence));
cmd queue가 fence에 특정 값을 update한다. 그리고 cmd queue에 signal을 추가한다.
3. if(mFence->GetCompletedValue() < mCurrentFence)
GetCompletedValue는 fence의 현재 값을 반환한다.
Signal 함수 호출 ~ GetCompletedValue 함수 호출 사이의 매우짧은 순간동안 GPU가 Signal 함수에 의해 update된 값 지점까지 도달하지 못할경우 true가 되는 조건문
4. HANDLE eventHandle = CreateEventEx(nullptr, false, false, EVENT_ALL_ACCESS)
이벤트를 생성한다. 추후 이 이벤트는 이전에 queue가 signal을 새긴 지점에 도달할 때 발동되는 용도로 쓰인다.
5. mFence->SetEventOnCompletion(mCurrentFence, eventHandle)
펜스의 특정 지점에 이벤트를 등록한다. 이제 GPU가 여기서 등록한 지점에 도달하면 event가 발생하게된다.
6. WaitForSingleObject(eventHandle, INFINITE)
이전에 등록했던 이벤트가 발생할때까지 대기
ID3D12Fence::GetCompletedValue
정확하진 않지만 내가 이해한 코드는 아래와 같다.
1. FlushCommandQueue 함수를 호출하여 Fence를 Queue에 넣는다.
2. GetCompletedValue 호출시 GPU가 직전에 넣은 fence의 지점까지 command queue를 처리 하지 않았다면 cpu의 timeline은 if문 내부로 들어가게되며, WaitForSingleObject에 의해 GPU가 현재 울타리 지점에 도달할 때 까지 기다린다.
3. FlushCommandQueue함수의 호출이 끝난 시점에서 Command Queue는 비어있음이 보장된다. 예제 코드에서는 FlushCommandQueue 함수 뒤에 CommandList::Reset() 함수를 호출하여 할당자를 비운다.
디버깅 모드로 위 코드를 실행해본 결과
아래 그림은 위코드를 도식화 한것이다.
위 그림은 GPU가 xgpu 까지의 명령들을 처리하고, CPU가 ID3D12CommandQueue::Signal(fence, n+1)을 호출한 직후의 상황을 나타낸 것이다. 그 호출은 울타리 값을 n+1로 변경하는 명령을 대기열의 끝에 추가하는 효과를 낸다. 그러나 GPU가 Signal(fence, n+1) 명령 이전에 명령 대기열에 추가된 명령들을 모두 처리할 때 까지는 mFence->GetCompletedValue()가 계속 n을 돌려준다.
이 방법은 이상적인 해결책이 아니다. GPU의 작업이 끝날 때까지 CPU가 기다려야 하기 때문이다.
해결책은 7장에서 배울것이다.
명령대기열을 flush하는 시점에는 제약이 없다.(한 프레임에서 딱 한번만 비워야 하는 것은 아니다.)
가령 초기화를 위한 GPU 명령들이 있다면, 먼저 그 초기화 명령들을 비운 후에 주 렌더링 루프로 진입하면 될 것이다.
이러한 명령 대기열 flush 동기화 기법을 통해 앞에서 제시했던 "GPU가 명령 할당자에 담긴 명령을 실행하기 전에 CPU가 명령 할당자를 재설정하는 문제"를 해결할 수 있다.
사실 Fence의 작동방식에 대해서는 잘 이해를 못하겠다.
우선 내가 개념은 아래와 같다.
가령 Device에서 command list와 command queue를 막 만들었을때의 상태는 아래와 같을것이다.
이제 CPU에서 데이터를 작성해 나간다.
위의 지점에서 CPU는 데이터를 쓰기를 중단하고 이제 GPU가 명령들을 실행해 나간다.
GPU가 열심히 값을 읽는중이다... 그런데 아직 CPU가 작성한곳 까지 처리를 못했다.
이런 상황에서 GPU가 처리를 중단하고 CPU가 다시 사용권을 가저간다면? 그리고 GPU가 아직 읽지 않은 데이터를 담은 allocator 내의 영역에 다른 데이터를 작성한다면? 프로그램은 우리가 원하는대로 작동하지 않게 될것이다.
그러므로 펜스를 새겨, GPU가 펜스 지점까지 값을 읽을때 까지는 CPU가 주도권을 가저 오지 않게끔 동기화한다.
'computer graphics > DirectX12' 카테고리의 다른 글
12. 다중표본화(multisampling) (0) | 2021.07.04 |
---|---|
8. 교환사슬과 더블버퍼링 (0) | 2021.07.04 |
7. COM (0) | 2021.07.04 |
6. 자원, 서술자 (0) | 2021.07.04 |
2. COM (0) | 2021.07.04 |