Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
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
Archives
Today
Total
관리 메뉴

codingfarm

4. CPU와 GPU의 상호작용(Command List, Queue, Allocator) 본문

computer graphics/DX12 book

4. CPU와 GPU의 상호작용(Command List, Queue, Allocator)

scarecrow1992 2021. 1. 14. 00:16

그래픽 프로그래밍에는 CPU와 GPU 2개의 장치(프로세서)가 작동한다.

CPU : 로직제어

GPU : 화면표시

 

이들은 병렬로 작동하지만 동기화를 할 필요가 있다.

하지만 최적의 성능을 얻으려면

1. 최대한 둘다 바쁘게 돌아가야한다.

2. 동기화를 최소화 해야한다.

2가지 조건을 만족해야한다.

동기화는 한 처리 장치가 작업을 마칠 때까지 다른 한 처리 장치가 놀고 있어야 함을 의미하며,

따라서 성능에 바람직하지 않다. 즉, 동기화는 병렬성을 망친다.

 


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

GPU에는 명령 대기열(Command Queue)가 있고, CPU에는 명령 목록(Command List)가 있다.

CPU는 그리기 명령들이 담긴 Command List를 Direct3D 를 통해서 GPU의 Command Queue에 제출한다.

주의

명령 대기열에 명령들이 제출됐다 해도, 그 명령들을 GPU가 즉시 실행하는것은 아니다.

 

Command Queue가 비면 GPU는 놀게된다.

반대로 Command Queue가 꽉차면 GPU가 명령들을 처리해새 queue에 자리가 생길 때까지 CPU가 놀게된다.

두 상황 모두 바람직하지 않다.

게임 같은 고성능 응용 프로그램에서 목표는, 가용 하드웨어 자원을 최대한 활용할 수 있도록 CPU와 GPU 둘 다 쉬지않고 돌아가게 만드는 것이다.

 

 

 

 

 

이제 command list와 command queue를 생성하는 방법에 대해 알아보겠다.

 


디바이스(Device)

$\bullet$ Direct3D의 초기화는 Direct3D 12 Device(ID3D12Device)를 생성하는것으로 시작한다.

$\bullet$ device란 가상의 디스플레이 어댑터를 표현하는 객체이다.

일반적으로 디스플레이 어댑터는 물리적인 3차원 그래픽 하드웨어 장치(가령 그래픽카드)를 나타낸다.

하지만 하드웨어 그래픽 기능성을 흉내내는 디스플레이 어댑터(가령 WARP 어댑터)도 존재한다.

$\bullet$ ID3D12Device : Direct3D 12 에서 device를 대표하는 인터페이스다

본 포스팅에서 command allocator, queue, list를 만들기 위해 쓰인다.

 

Remark

$\bullet$ D3D12Device는 각 어댑터당 하나씩만 존재한다(singleton)

$\bullet$ 주어진 adapter를 위한 device가 현재 프로세스에 이미 존재한다면, D3D12CreateDevice 함수는 이미 존재하는 device를 반환한다.

$\bullet$ device가 removed state라면  D3D12CreateDevice 함수는 fail한다.

( ID3D12Device::GetDeviceRemovedReason HRESULT가 반환된다.)

$\bullet$ device간의 비교는 pointer가 아닌 LUID를 통해 이루어저야 한다.

 

D3D12를 지원하는 첫번째 device의 확보를 확실히하기 위해서는 아래의 코드를 사용하라.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void GetHardwareAdapter(IDXGIFactory4* pFactory, IDXGIAdapter1** ppAdapter)
{
    *ppAdapter = nullptr;
    for (UINT adapterIndex = 0; ; ++adapterIndex)
    {
        IDXGIAdapter1* pAdapter = nullptr;
        if (DXGI_ERROR_NOT_FOUND == pFactory->EnumAdapters1(adapterIndex, &pAdapter))
        {
            // No more adapters to enumerate.
            break;
        } 
 
        // Check to see if the adapter supports Direct3D 12, but don't create the
        // actual device yet.
        if (SUCCEEDED(D3D12CreateDevice(pAdapter, D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr)))
        {
            *ppAdapter = pAdapter;
            return;
        }
        pAdapter->Release();
    }
}
cs

 

 

용도

$\bullet$ command allocators, command lists, command queues, fences, resources, pipeline state objects, heaps, root signatures, samplers, and many resource views 를 만들기위해 사용된다.

$\bullet$ 기능 지원 점검에 사용된다.

 

device를 생성할때는 D3D12CreateDevice 함수를 사용한다.

1
2
3
4
5
6
HRESULT D3D12CreateDevice(
  IUnknown          *pAdapter,
  D3D_FEATURE_LEVEL MinimumFeatureLevel,
  REFIID            riid,
  void              **ppDevice
);
cs

 

parameters

pAdapter

$\bullet$ device를 만들때 사용하기 위한 디스플레이 어댑터를 포인트 한다.
$\bullet$ NULL 포인터를 지정하면 기본(primary) 디스플레이 어댑터가 쓰인다.
(이후의 포스팅은 모두 기본 어댑터를 사용한다.)
$\bullet$ 시스템의 모든 디스플레이 어댑터를 나열하는 방법은 ~~~(수정)을 참고할것.

Note  Don't mix the use of DXGI 1.0 (IDXGIFactory) and DXGI 1.1 (IDXGIFactory1) in an application. Use IDXGIFactory or IDXGIFactory1, but not both in an application.

 

MinimumFeatureLevel

$\bullet$ 응용 프로그램이 요구하는 최소 기능 수준

$\bullet$ 만일 디스프레이 어댑터가 이 매개변수에서의 수준을 지원하지 않으면 장치 생성이 실패한다.

$\bullet$ 이 책의 예제 프레임워크에서는 항상 D3D_FEATURE_LEVEL_11_0을 지정한다.

 

riid

$\bullet$ device interface를 위한 globally unique identifier (GUID)

$\bullet$ 생성하고자하는 ID3D12Deice 인터페이스의 COM ID.

$\bullet$ 이 매개변수와 ppDriveIID_PPV_ARGS 로 처리될 수 있다.

1
#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
cs

Direct3D 12 API에는 생성하고자 하는 인터페이스의 COM ID와 void** 를 마지막 매개변수로 받는 함수들이 많으므로  이 매크로함수가 자주 쓰일것이다.

 

ppDrive

$\bullet$생성된 device가 이 매개변수에 설정된다(출력 매개변수).

$\bullet$ S_FALSE : device 생성 실패

 

Return Value

타입 : HRESULT

Direct3D 12 Return Codes.중 하나를 return 한다.

 

함수 호출 예

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
ComPtr<IDXGIFactory4> factory;
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&factory)));
 
if (m_useWarpDevice)
{
    ComPtr<IDXGIAdapter> warpAdapter;
    ThrowIfFailed(factory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)));
 
    ThrowIfFailed(D3D12CreateDevice(
        warpAdapter.Get(),
        D3D_FEATURE_LEVEL_11_0,
        IID_PPV_ARGS(&m_device)
        ));
}
else
{
    ComPtr<IDXGIAdapter1> hardwareAdapter;
    GetHardwareAdapter(factory.Get(), &hardwareAdapter);
 
    ThrowIfFailed(D3D12CreateDevice(
        hardwareAdapter.Get(),
        D3D_FEATURE_LEVEL_11_0,
        IID_PPV_ARGS(&m_device)
        ));
}
cs

warp 어댑터의 존재 여부를 확인하여 하드웨어 어댑터와 WARP 어댑터 둘중 어느 장치를 포인트할지 나누어서 코드를 작성한다.

 

이후 포스팅에서는 아래와 같은 예제 코드를 사용할것이다.

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
#if defined(DEBUG) || defined(_DEBUG) 
// Enable the D3D12 debug layer.
{
ComPtr<ID3D12Debug> debugController;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
}
#endif
 
ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory)));
 
// Try to create hardware device.
HRESULT hardwareResult = D3D12CreateDevice(
    nullptr,             // default adapter
    D3D_FEATURE_LEVEL_11_0,
    IID_PPV_ARGS(&md3dDevice));
 
// Fallback to WARP device.
if(FAILED(hardwareResult))
{
    ComPtr<IDXGIAdapter> pWarpAdapter;
    ThrowIfFailed(mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter)));
 
    ThrowIfFailed(D3D12CreateDevice(
        pWarpAdapter.Get(),
        D3D_FEATURE_LEVEL_11_0,
        IID_PPV_ARGS(&md3dDevice)));
}
cs

 

디버그 모드 빌드를 위해 디브그층을 활성화했다.

첫 D3D12CreateDevice 호출이 실패하면 소프트웨어 어댑터인 WARP(Windows Advanced Rasterization Platform)를 나타내는 장치를 생성한다. Windows 7과 그 이전 버전에서 WARP 장치는 기능 수준 10.1 까지 지원하고, Windows 8의 WARP 장치는 기능 수준 11.1 까지 지원한다. WARP 어댑터에 대한 장치를 생성하려면 그 전에 다음과 같이 IDXGIFactory4의 EnumWarpAdapter 메서드를 호출해 주어야 한다. 이렇게 해야 디스플레이 어댑터 나열 시 WARP 어댑터가 나타난다.

1
2
3
4
5
Microsoft::WRL::ComPtr<IDXGIFactory4> mdxgiFactory;
ComPtr<IDXGIAdapter> pWarpAdapter;
 
CreateDXGIFactory1(IID_PPV_ARGS(&mdxgiFactory));
mdxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&pWarpAdapter));
cs

mdxgiFactory 객체는 교환 사슬을 생성하는 데에도 쓰인다(이는 교환 사슬이 Direct3D가 아니라 DXGI의 일부이기 때문이다.)

 


 

명령 대기열(Command Queue)

ID3D12CommandQueue : Direct3D 12 에서 command queue를 대표하는 인터페이스이다.

D3D12_COMMAND_QUEUE_DESC : command queue 인터페이스의 생성내용을 서술하기 위한 구조체

 

Command Queue의 생성은 device를 통해 이루어진다.

ID3D12Device::CreateCommandQueue : command queue를 생성하기 위해 호출하는 메서드

다음은 이후의 포스팅에서 예제 프로그램들이 Command Queue를 생성하는 방법을 보여주는 코드이다.

1
2
3
4
5
6
7
8
#define IID_PPV_ARGS(ppType) __uuidof(**(ppType)), IID_PPV_ARGS_Helper(ppType)
 
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
 
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
cs

IID_PPV_ARGS(ppType) : Direct3D 12 API에는 생성하고자 하는 인터페이스의 COM ID와 void** 를 마지막 매개변수로 받는 함수들이 많으므로 첫번째 줄과 같은 매크로함수가 자주 쓰일것이다.

D3D12_COMMAND_QUEUE_DESC 구조체에 만들고자 하는 command queue의 내용을 서술하고, 이를 참조하여 D3D12DEVICE를 통해 command queue를 생성하는 내용을 담고있다.

 

이 인터페이스의 주요 메서드중 하나인 ID3D12CommandQueue::ExecuteCommandLists는 아래코드처럼 Command List에 있는 명령들을 command queue에 추가하는 것이다.

즉, CPU에서 명령을 보내는것이 아닌 GPU에서 명령을 가저오는 관점이 정확할것이다.

1
2
3
4
void ExecuteCommandLists(
  UINT              NumCommandLists,
  ID3D12CommandList * const *ppCommandLists
);
cs

 


명령 할당자(Command Allocator)

명령 목록에 대해 보기전에 우선 명령할당자를 보겠다.

command list의 생성에 command allocator가 연관되기 때문이다.

command list에 추가된 명령들은 이 allocator의 메모리에 저장된다.

ID3D12CommandQueue::ExecuteCommandLists로 command list를 command queue에 제출하면, command queue는 그 allocator에 담긴 명령들을 참조한다. (데이터 복사되서 command queue에 들어가는것이 아니다!)

command allocator는 ID3D12Device의 메서드를 통해 생성한다.

 

1
2
3
4
5
HRESULT CreateCommandAllocator(
  D3D12_COMMAND_LIST_TYPE type,
  REFIID                  riid,
  void                    **ppCommandAllocator
);
cs
매개변수 기능
type $\bullet$ 이 할당자와 연관시킬 수 있는 명령 목록의 종류
흔히 아래의 2가지 값이 쓰인다.

- D3D12_COMMAND_LIST_TYPE_DIRECT
GPU가 직접 실행하는 command list
이후 포스팅에서 사용하게 될 값

- D3D12_COMMAND_LIST_TYPE_BUNDLE
아래 참조
riid 생성하고자 하는 ID3D12CommandAllocator 인터페이스의 COM ID
ppCommandAllocator 생성된 명령 할당자를 가리키는 포인터(출력 매개변수)

함수의 사용법은 아래와 같다.

1
2
3
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
 
ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
cs

 

D3D12_COMMAND_LIST_TYPE_BUNDLE

묶음(bundle)을 나타내는 command list

command list를 만드는데에는 CPU의 부담이 어느정도 따르기 때문에, Direct3D 12는 일련의 명령들을 소위 '묶음' 단위로도 기록할 수 있는 최적화 수단을 제공한다.

bundle을 추가하면 드라이버는 렌더링 도중에 실행이 최적화 되도록 bundle의 명령들을 전처리한다.

command list의 구축에 시간이 오래 걸린다면 이러한 bundle을 이용하 ㄴ최적화를 고려할 필요가 있다.

하지만 Direct3D 12의 그리기 API는 이미 아주 효율적이므로, 명령 묶음을 사용해야 하는 경우는 자주 생기지 않을것이다.

묶음은 성능상의 이득이 명백한 경우에만 사용해야한다. 즉, 무조건 bundle을 사용하지는 말아야 한다.

 

 


명령 목록(Command List)

$\bullet$ ID3D12CommandList : Direct3D 12 에서 command list를 대표하는 인터페이스이다.

$\bullet$ 실제 그래픽 작업을 위한 command list는 이 인터페이스를 상속하는 ID3D12GraphicsCommandList라는 인터페이스로 대표된다.

 

Command List의 생성은 ID3D12Device::CreateCommandList 메서드의 호출을 통해 이루어진다.

1
2
3
4
5
6
7
8
HRESULT CreateCommandList(
  UINT                    nodeMask,
  D3D12_COMMAND_LIST_TYPE type,
  ID3D12CommandAllocator  *pCommandAllocator,
  ID3D12PipelineState     *pInitialState,
  REFIID                  riid,
  void                    **ppCommandList
);
cs

 

매개변수 기능
nodeMask $\bullet$ GPU가 하나인 시스템에서는 0으로 설정한다.
$\bullet$ GPU가 여러개일 때에는 이 명령 목록과 연관시킬 물리적 GPU 어댑터 노드들을 지정하는 bitmask값을 설정한다.
이후 포스팅에서는 시스템에 GPU가 하나라고 가정하므로 0을 넣는다.
type $\bullet$ 명령 목록의 종류를 지정한다.
D3D12_COMMAND_LIST_TYPE_DIRECT
또는
D3D12_COMMAND_LIST_TYPE_BUNDLE
pCommandAllocator 생성된 command list에 연관시킬 allocator
2번재 매개변수 type은 allocator의 type과 일치해야한다.
pInitialState 명령 목록의 초기 파이프라인 상태를 지정한다.
bundle의 경우 그리고 그리기 명령은 없는 명령 목록의 경우에는 NULL을 지정해도 된다.
6장에서 보다 자세히 논의한다.
riid 생성하고자하는 command list에 해당하는 ID3D12CommandList 인터페이스의 COM ID.
ppCommandList 생성된 command list를 가리키는 포인터(출력 매개변수)

시스템에 있는 GPU 어댑터 노드 갯수는 ID3D12Device::GetNodeCount 메서드로 알아낼 수 있다.

 

$\bullet$ ID3D12CommandList 인터페이스는 별개의 함수를 통해 각 명령들을 command list에 추가한다.

아래 코드는 뷰포트를 설정하고, 렌더 대상 뷰를 지우고, 그리기 호출을 실행하는 명령들을 추가한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
 
ThrowIfFailed(md3dDevice->CreateCommandList(
    0,
    D3D12_COMMAND_LIST_TYPE_DIRECT,
    mDirectCmdListAlloc.Get(), // Associated command allocator
    nullptr,                   // Initial PipelineStateObject
    IID_PPV_ARGS(mCommandList.GetAddressOf())));
 
// 뷰포트 설정 명령 추가
mCommandList->RSSetViewports(1&mScreenViewport);
 
// 렌더 대상뷰를 지우는 명령 추가
mCommandList->ClearRenderTargetView(mBackBufferView, 
    Colors::LightSteelBlue, 0, nullptr);
 
// 그리기 호출을 실행하는 명령들을 
mCommandList->DrawIndexedInstanced(361000);
 
mCommandList->Close();
cs

한 할당자를 여러 명령 목록에 연관시켜도 되지만, 명령들을 여러 명령 목록에 동시에 기록할수는 없다.

현재 명령들을 추가하는 명령 목록을 제외한 command list는 닫혀있어야 한다.

즉, command list는 명령을 추가할때 이외에는 항상 닫혀있어야 한다.

이렇게 해야 한 command list의 모든 명령이 할당자 안에 인접해서 저장된다.

command list를 닫기 위해선 Close 메서드를 호출한다.

command list을 생성하거나 재설정하면 command list는 '열린'상태가 됨을 주의하라.

따라서 같은 할당자로 두 command list를 닫지않고 연달아 생성하면 에러가 발생한다.

 

 

아래 코드로 Command queue와 command allocator, command list를 생성하는 방법을 한번 더 확인하자.

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
Microsoft::WRL::ComPtr<ID3D12CommandQueue> mCommandQueue;
Microsoft::WRL::ComPtr<ID3D12CommandAllocator> mDirectCmdListAlloc;
Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> mCommandList;
 
void D3DApp::CreateCommandObjects() {
    D3D12_COMMAND_QUEUE_DESC queueDesc = {};
    queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
    queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
    ThrowIfFailed(md3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&mCommandQueue)));
 
    ThrowIfFailed(md3dDevice->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        IID_PPV_ARGS(mDirectCmdListAlloc.GetAddressOf())));
 
    ThrowIfFailed(md3dDevice->CreateCommandList(
        0,
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        mDirectCmdListAlloc.Get(), // Associated command allocator
        nullptr,                   // Initial PipelineStateObject
        IID_PPV_ARGS(mCommandList.GetAddressOf())));
 
    // Start off in a closed state.  This is because the first time we refer 
    // to the command list we will Reset it, and it needs to be closed before
    // calling Reset.
    mCommandList->Close();
}
cs

CreateCommandList 호출 시 파이프라인 상태 객체 매개변수에 널 포인터를 지정했음을 주목하라.

이번 장의 예제 프로그램에서는 그 어떤 그리기 명령도 제출하지 않으므로 유효한 파이프라인 상태 객체를 지정하지 않아도 된다. 파이프라인 상태 객체는 6장에서 논의한다.

 


재사용(reuse)

ID3D12CommandQueue::ExecuteCommandList(C)를 호출하여 CommandList 내의 명령들을 CommandQueue에 넣은 후 ID3D12CommandList::Reset 메서드를 호출하면 C의 내부 메모리를 새로운 명령들을 기록하는 데 재사용할 수 있게 된다.

1
2
3
4
HRESULT ID3D12CommandList::Reset(
  ID3D12CommandAllocator *pAllocator,
  ID3D12PipelineState    *pInitialState
);
cs

매개변수의 의미는 ID3D12Device::CreateCommandList의 매개변수들의 의미와 같다.

이 메서드는 주어진 명령 목록을 마치 처음 생성했을때와 같은 상태로 만든다.

이 메서드를 통해 command list를 해제하고 새로운 command list를 할당하는 번거로움 없이 명령 목록의 내부 메모리를 재사용할 수 있다.

앞서 말했듯이 command queue 내의 명령들은 command list의 명령을 복사하는것이 아닌 참조하는것이다.

하지만 reset함수는 명령들의 메모리 자체를 지우는것은 아니므로 Reset으로 command list를 재설정해도, command queue에 있는 명령들에는 영향이 미치지 않음을 주의하라

command queue가 참조하는 명령들은 연관된 command allocator의 메모리에 여전히 남아 있기 때문이다.

 

하나의 프레임을 완성하는 데 필요한 렌더링 명령들을 모두 GPU에 제출한 후에는, command allocator의 메모리를 다음 프레임을 위해 재사용 해야 할것이다. 이때 ID3D12CommandAllocator::Reset 메서드를 사용한다.

1
HRESULT ID3D12CommandAllocator::Reset();
cs

크기는 0이되지만 현재 용량(capacity)는 변하지 않는다는 점에서 std::vector::clear를 호출하는것과 비슷하다.

 

 

하지만, gpu가 명령을 관리하는 방법은 command queue내에 복사된 명령을 가지는 방식이 아니라 할당자 안의 자료를 참조하는 방법이다. 즉 command queue와 command list가 같은 allocator를 공유하고 있다고 볼 수 있으며, GPU가 명령 할당자에 담긴 모든 명령을 실행했음이 확실해지기 전까지는 명령 할당자를 재설정하지 말아야 한다.

이를 위한 구체적인 방법은 이후 포스팅에서 알아보겠다.

 

 


2. 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
);
cs

 

울타리 객체의 생성 이우헤 필요한 서술자들의 크기도 미리 조회해서 설정해둔다. 서술자 크기는 GPU마다 다를 수 있으므로로, 실행 시점에서 적절한 메서드를 호출해서 알아내야 한다. 나중에 서술자 크기가 필요할 때 바로 사용할 수 있도록, 크기들은 적절한 멤버 변수들에 저장해 둔다.

사용 예

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Microsoft::WRL::ComPtr<ID3D12Fence> mFence;
 
UINT mRtvDescriptorSize = 0;
UINT mDsvDescriptorSize = 0;
UINT mCbvSrvUavDescriptorSize = 0;
 
ThrowIfFailed(md3dDevice->CreateFence(0, D3D12_FENCE_FLAG_NONE,
    IID_PPV_ARGS(&mFence)));
 
mRtvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize
    (D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize
    (D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescriptorSize = md3dDevice->GetDescriptorHandleIncrementSize
    (D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
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, falsefalse, 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);
    }
}
cs

 

ID3D12Fence::GetCompletedValue

CreateEventEx function

 

정확하진 않지만 내가 이해한 코드는 아래와 같다.

1. 우선 FlushCommandQueue 함수를 호출하여 Fence를 Queue에 넣는다.

2. GetCompletedValue 호출시 GPU가 직전에 넣은 fence의 지점까지 command queue를 처리 하지 않았다면 cpu의 timeline은 if문 내부로 들어가게되며, WaitForSingleObject에 의해 GPU가 현재 울타리 지점에 도달할 때 까지 기다린다.

3. FlushCommandQueue함수의 호출이 끝난 시점에서 Command Queue는 비어있음이 보장된다. 예제 코드에서는 FlushCommandQueue 함수 뒤에 CommandList::Reset() 함수를 호출하여 할당자를 비운다.

 

디버깅 모드로 위 코드를 실행해본 결과

 

아래 그림은 위코드를 도식화 한것이다.

위 그림은 GPU가 $x_{gpu}$ 까지의 명령들을 처리하고, 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가 주도권을 가저 오지 않게끔 동기화한다.

 

 

 


정리

  • ID3D12Device::CreateCommandAllocator 함수로 메모리 할당자를 생성
  • ID3D12Device::CreateCommandList와 CreateCommandQueue 함수로 cmd list, cmd queue를 생성
    • cmd list생성시 앞서 만든 메모리 할당자를 넘김
    • 메모리 할당자와 cmd list의 종류는 일치해야함
    • 하나의 메모리 할당자에 대해 여러개의 cmd list를 만들 수 있음
  • cmd list에 명령을 추가하기 위한 다양한 함수가 존재함
    • ID3D12CommandList::RSSetViewports
    • ID3D12CommandList::ClearRenderTargetView
    • ID3D12CommandList::DrawIndexedInstanced
  • 현재 명령을 추가하고 잇는 cmd list 이외에 다른 cmd list는 다 닫혀 있어야 함
    • ID3D12GraphicCommandList::Close 함수로 닫을 수 있음
  • cmd queue에서 cmd list로부터 그리기 명령을 가저올 수 있음
    • ID3D12CommandQueue::ExecuteCommandList
  • cmd list에 새로운 명령들을 기록하게끔 재사용 할수 있게 해주는 함수 존재
    • ID3D12CommandList::Reset
    • 이 함 수호출시 주어진 cmd list를 처음 생성했을때와 같은 상태로 만듬
    • cmd list를 해제하고 새로운 cmd list를 할당하는 번거로움 없이 메모리의 재사용이 가능해짐
    • cmd queue에 있는 명령들에 영향을 주지 않음
  • cmd list와 cmd queue는 동일한 메모리를 참조하므로 동기화 문제가 발생할 수 있음
    • cmd queue가 읽기전에 cmd list에서 데이터를 수정 및 삭제하는 참사가 발생해선 안됨
    • 동기화를 위해 fence라는 수단이 제공됨
      • ID3D12Device::CreateFence로 생성
      • CPU가 cmd allocator상의 특정 지점에 fence를 새기면, GPU에서 해당 fence지점을 읽기전까지 cpu는 대기함

 

 

Comments