Post

8. 장치 초기화

인프런 Rookiss C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈 게임 수학과 DirectX12을 듣고 리뷰한 내용입니다.

DirectX12 초기화에 대한 내용을 간단하게 리뷰하는 내용이다. 자세한 코드보다 어떤 흐름으로 동작하고 있으며, 각 개념들이 무엇을 의미하는지에 대해서 정리하는 것이 목표다.

DirectX12 개발을 위해 개발 편리한 기능들을 제공하고 있다.

DirectX 12 개발을 위해 Microsoft에서 제공하는 다양한 샘플 코드 및 유틸리티가 있다. 아래와 같은 오픈소스를 참고하면 개발을 더욱 편리하게 진행할 수 있다.

DirectX Graphics Samples
DirectXTex

구조 뼈대

📦Client
┣ 📜Client.cpp
┣ 📜Client.h
┣ 📜Game.cpp
┣ 📜Game.h
┣ 📜pch.cpp
┣ 📜pch.h
📦Engine
┣ 📜CommandQueue.cpp
┣ 📜CommandQueue.h
┣ 📜d3dx12.h
┣ 📜DescriptorHeap.cpp
┣ 📜DescriptorHeap.h
┣ 📜Device.cpp
┣ 📜Device.h
┣ 📜Engine.cpp
┣ 📜Engine.h
┣ 📜Engine.vcxproj
┣ 📜Engine.vcxproj.filters
┣ 📜Engine.vcxproj.user
┣ 📜EnginePch.cpp
┣ 📜EnginePch.h
┣ 📜pch.cpp
┣ 📜pch.h
┣ 📜SwapChain.cpp
┗ 📜SwapChain.h

Client와 Engine을 분리하여 설계하였으며, Engine에서 빌드된 파일을 활용하여 Client 프로그램이 동작하도록 구성되어 있다.

각 파일별 기능 설명

Client

Client.h, Client.cpp파일에서는 Window프로그램을 다루고 있다. Window프로그램 설정과 Main()함수에서 우리가 만든 Game을 실행시키는 기능을 하고 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include "pch.h"
#include "Game.h"
#include "Engine.h"

void Game::Init(const WindowInfo& info)
{
    GEngine->Init(info);
}

void Game::Update()
{
    GEngine->Render();
}

Game.h, Game.cpp파일에서는 Engine을 Init, Update하는 내용이다. Engine에 이루지는 내용들이 앞으로 다루게 된 내용이니, 이곳에 집중하는 것이 좋을 것 같다.

Client에서는 전반적으로 윈도우 프로그램에 Engine을 올려 프로그램이 실행될 수 있도록 하는 기능이 구현되어 있다.

Engine

Engine 클래스

Engine.cpp - Init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Engine::Init(const WindowInfo& info)
{
    _window = info;
    ResizeWindow(info.width, info.height);

    _viewport = { 0, 0, static_cast<FLOAT>(info.width), static_cast<FLOAT>(info.height), 0.0f, 1.0f }; //
    _scissorRect = CD3DX12_RECT(0, 0, info.width, info.height); // CD3DX12_RECT - d3x12.h에 정의되어 있는 구조체

    _device = make_shared<Device>();
    _cmdQueue = make_shared<CommandQueue>();
    _swapChain = make_shared<SwapChain>();
    _descHeap = make_shared<DescriptorHeap>   
    _device->Init();
    _cmdQueue->Init(_device->GetDevice(), _swapChain, _descHeap);
    _swapChain->Init(info, _device->GetDXGI(), _cmdQueue->GetCmdQueue());
    _descHeap->Init(_device->GetDevice(), _swapChain);
}
  • Device : 각종 객체를 생성하는 용도
  • CommandQueue : 일감을 하나씩 처리하면 비효율적이기 때문에, 일감 쌓아두고 한 번에 요청하도록 함. 커맨더 패턴 : 큐에 쌓아두고 한 번에 처리하는 방식
  • SwapChain : 전면 버퍼와 후면 버퍼를 교환하기 위한 역할을 담당한다. 전면 버퍼는 화면에 보이는 장면, 후면 버퍼는 다음 장면이라고 생각하면 된다. 전면 버퍼와 후면 버퍼를 바꿔가면서 화면에 출력된다.
  • DescrptorHeap : 일종의 기안서로 일감을 맡길 때 해당 일감에 대한 정보를 같이 넘겨주게 된다.
Engine.cpp - Render, RenderBegin, RenderEnd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Engine::Render()
{
	RenderBegin();
	// TODO: 여기에 렌더링 코드를 추가합니다.
	RenderEnd();
}

void Engine::RenderBegin()
{
	_cmdQueue->RenderBegin(&_viewport, &_scissorRect);
}

void Engine::RenderEnd()
{
	_cmdQueue->RenderEnd(&_viewport, &_scissorRect);
}

Render함수는 매 프레임 그려지면서, RenderBegin과 RenderEnd사이에 여러 물체를 그리게 된다.

Device 클래스

Device.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#pragma once

// 인력 사무소 : 각종 객체 생성을 하는 역할
class Device
{
public:
	void Init();

	ComPtr<IDXGIFactory> GetDXGI() { return _dxgi; }
	ComPtr<ID3D12Device> GetDevice() { return _device; }
private:
	// COM(Component Object Model) : 
	// - DX의 프로그래밍 언어 독립성과 하위 호황성을 가능하게 하는 기술
	// - COM 객체(COM 인퍼페이스)를 사용. 세부사항은 우리한테 숨겨짐
	// - ComPtr : 일종의 스마트 포인터
	// Comptr : 그래픽 카드랑 통신할 때 사용하는 객체.
	ComPtr<ID3D12Debug> _debugController;
	ComPtr<IDXGIFactory> _dxgi;		// 화면 관련 기능들.
	ComPtr<ID3D12Device> _device;	// 각종 객체 생성.
};
  • CPU (중앙처리장치, Central Processing Unit)
    • 고급 연산 처리, 논리 연산 및 제어
  • GPU (그래픽처리장치, Graphics Processing Unit)
    • 대량의 단순 연산을 병렬로 처리하는 데 특화
    • CPU는 여러 가지 작업을 순차적으로 처리하고, GPU는 많은 단순 연산을 동시에 처리하는 데 최적화됨
  • GPU와 CPU의 통신 문제
    • CPU와 GPU는 직접 연결되지 않고, 특정 API를 통해 통신해야 함
    • GPU 제조사(NVIDIA, AMD, Intel)마다 아키텍처(설계 방식)가 다름
    • 따라서 게임을 만들 때 GPU에 맞춰 개별적으로 개발하면 비효율적임

즉, DirectX를 사용하면 개발자가 특정 GPU 제조사에 의존하지 않고 그래픽을 구현할 수 있음. OpenGL, Vulkan 등이 있음.

Device.cpp - Init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "pch.h"
#include "Device.h"

void Device::Init()
{
#ifdef _DEBUG
	::D3D12GetDebugInterface(IID_PPV_ARGS(&_debugController));
	_debugController->EnableDebugLayer();
#endif

	::CreateDXGIFactory(IID_PPV_ARGS(&_dxgi));
	::D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&_device));
}

함수역할주요 기능
CreateDXGIFactoryDXGI 팩토리 생성어댑터(그래픽 카드) 열거, 스왑 체인 생성
D3D12CreateDeviceDirect3D 12 디바이스 생성GPU 제어, 그래픽 리소스 생성 및 렌더링
_debugController 디버그 메세지를 띄워주는 기능.

SwapChain 클래스

SwapChain.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#pragma once

class SwapChain
{
public:
	void Init(const WindowInfo& info, ComPtr<IDXGIFactory> dxgi, ComPtr<ID3D12CommandQueue> cmdQueue);

	void Present();
	void SwapIndex();

	ComPtr<IDXGISwapChain> GetSwapChain() { return _swapChain; }
	ComPtr<ID3D12Resource> GetRenderTarget(int32 index) { return _renderTargets[index]; }

	uint32 GetCurrentBackBufferIndex() { return _backBufferIndex; }
	ComPtr<ID3D12Resource> GetCurrentBackBufferResource() { return _renderTargets[_backBufferIndex]; }

private:
	ComPtr<IDXGISwapChain>	 _swapChain;
	ComPtr<ID3D12Resource>	 _renderTargets[SWAP_CHAIN_BUFFER_COUNT];
	uint32					 _backBufferIndex = 0;  // 0 - 1 바뀌면서 왔다갔다 함.	
};


게임에서 매 프레임마다 새로운 화면을 그려야 하므로, 렌더링된 결과를 저장하고 표시하는 버퍼(Buffer)가 필요하다. 만약 단 하나의 버퍼만 사용하면, 새로운 프레임을 그리는 도중 기존 화면이 덮어씌워져 화면이 깜빡이거나 깨지는 현상(Flickering, Tearing)이 발생할 수 있다. 이를 해결하기 위해 더블 버퍼링(Double Buffering)SwapChain이 사용된다.

  • 더블 버퍼링이란?
    • 두 개의 버퍼를 번갈아 가며 사용하여 화면을 부드럽게 출력하는 방식
    • 하나의 버퍼(Front Buffer)는 현재 화면에 표시되고, 다른 버퍼(Back Buffer)는 다음 프레임을 준비
    • 백 버퍼가 완성되면 두 버퍼를 교체(Swap)하여 끊김 없이 화면을 갱신
  • SwapChain이란?
    • DirectX에서 더블 버퍼링 또는 트리플 버퍼링을 구현하는 객체
    • GPU에서 렌더링한 프레임을 화면에 출력하는 역할
    • 여러 개의 버퍼를 관리하며, 화면을 부드럽게 갱신하도록 돕는 버퍼 교환 시스템
SwapChain.cpp
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
35
36
37
38
39
40
41
#include "pch.h"
#include "SwapChain.h"

void SwapChain::Init(const WindowInfo& info, ComPtr<IDXGIFactory> dxgi, ComPtr<ID3D12CommandQueue> cmdQueue)
{
	// 이전에 만든 정보 날린다
	_swapChain.Reset();

	DXGI_SWAP_CHAIN_DESC sd;
	sd.BufferDesc.Width = static_cast<uint32>(info.width); // 버퍼의 해상도 너비
	sd.BufferDesc.Height = static_cast<uint32>(info.height); // 버퍼의 해상도 높이
	sd.BufferDesc.RefreshRate.Numerator = 60; // 화면 갱신 비율
	sd.BufferDesc.RefreshRate.Denominator = 1; // 화면 갱신 비율
	sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; // 버퍼의 디스플레이 형식
	sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
	sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
	sd.SampleDesc.Count = 1; // 멀티 샘플링 OFF
	sd.SampleDesc.Quality = 0;
	sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; // 후면 버퍼에 렌더링할 것 
	sd.BufferCount = SWAP_CHAIN_BUFFER_COUNT; // 전면+후면 버퍼
	sd.OutputWindow = info.hwnd;
	sd.Windowed = info.windowed;
	sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; // 전면 후면 버퍼 교체 시 이전 프레임 정보 버림
	sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

	// IDXGIFactory에서 SwapChain을 생성하는 것을 알 수 있음.
	dxgi->CreateSwapChain(cmdQueue.Get(), &sd, &_swapChain);

	for (int32 i = 0; i < SWAP_CHAIN_BUFFER_COUNT; i++)
		_swapChain->GetBuffer(i, IID_PPV_ARGS(&_renderTargets[i])); // swapchain을 이용해 _renderTargets을 저장.
}
void SwapChain::Present()
{
	// Present the frame.	화면 그림.
	_swapChain->Present(0, 0);
}

void SwapChain::SwapIndex()	// 0 - 1 바뀌면서 왔다갔다 함.
{
	_backBufferIndex = (_backBufferIndex + 1) % SWAP_CHAIN_BUFFER_COUNT;
}

DescriptorHeap 클래스

DescriptorHeap.cpp
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
35
36
37
38
#include "pch.h"
#include "DescriptorHeap.h"
#include "SwapChain.h"

void DescriptorHeap::Init(ComPtr<ID3D12Device> device, shared_ptr<SwapChain> swapChain)
{
	_swapChain = swapChain;
	
	_rtvHeapSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

	D3D12_DESCRIPTOR_HEAP_DESC rtvDesc;
	rtvDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	rtvDesc.NumDescriptors = SWAP_CHAIN_BUFFER_COUNT;
	rtvDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	rtvDesc.NodeMask = 0;

	// 같은 종류의 데이터끼리 배열로 관리
	// RTV 목록 : [ ] [ ] - rendertarget
	// _rtvHeap 배열
	device->CreateDescriptorHeap(&rtvDesc, IID_PPV_ARGS(&_rtvHeap));

	// D3D12_CPU_DESCRIPTOR_HANDLE
	// - CPU에서 접근 가능한 디스크립터 힙의 시작 주소를 나타내는 구조체

	// 시작 주소값 배열을 첫번째 요소를 받음.
	D3D12_CPU_DESCRIPTOR_HANDLE rtvHeapBegin = _rtvHeap->GetCPUDescriptorHandleForHeapStart();
	
	for (int i = 0; i < SWAP_CHAIN_BUFFER_COUNT; i++)
	{
		_rtvHandle[i] = CD3DX12_CPU_DESCRIPTOR_HANDLE(rtvHeapBegin, i * _rtvHeapSize);
		device->CreateRenderTargetView(swapChain->GetRenderTarget(i).Get(), nullptr, _rtvHandle[i]);
	}
}

D3D12_CPU_DESCRIPTOR_HANDLE DescriptorHeap::GetBackBufferView()
{
	return GetRTV(_swapChain->GetCurrentBackBufferIndex());
}

DirectX 12에서 ID3D12ResourceGPU가 사용할 데이터(버퍼, 텍스처 등)를 나타내는 객체이다. 하지만, GPU는 이 리소스가 어떤 용도로 사용되는지 정확히 알아야 한다.

📌 DirectX 11 vs DirectX 12의 차이점

DirectX 11DirectX 12
View 개념 사용Descriptor Heap 개념 사용
개별 객체마다 View 생성CreateDescriptorHeap을 통해 관리
RenderTargetView (RTV), DepthStencilView (DSV) 등 개별 생성하나의 힙에서 RTV, DSV, CBV, SRV, UAV 등을 관리

📌 대표적인 Descriptor의 종류

Descriptor Type설명
Render Target View (RTV)프레임 버퍼에 출력할 때 사용
Depth Stencil View (DSV)깊이(Depth) 및 스텐실(Stencil) 정보 저장
Constant Buffer View (CBV)쉐이더에서 상수 버퍼를 사용할 때 사용
Shader Resource View (SRV)쉐이더에서 텍스처 및 기타 데이터를 읽을 때 사용
Unordered Access View (UAV)읽기/쓰기 가능 리소스 (예: 컴퓨트 쉐이더)

📌 Descriptor Heap의 역할
✅ 여러 개의 Descriptor(서술자)을 한 번에 저장하고 관리
✅ CPU가 GPU에게 리소스 정보를 전달하는 “기안서” 역할
CreateDescriptorHeap을 사용하여 생성

CommandQueue 클래스

CommandQueue.h
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
#pragma once
class SwapChain;
class DescriptorHeap;

class CommandQueue
{
public:
	~CommandQueue();

	void Init(ComPtr<ID3D12Device> device, shared_ptr<SwapChain> swapChain, shared_ptr<DescriptorHeap> descHeap);
	void WaitSync();

	void RenderBegin(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect);
	void RenderEnd(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect);

	ComPtr<ID3D12CommandQueue> GetCmdQueue() { return _cmdQueue; }

private:
	ComPtr<ID3D12CommandQueue> 		_cmdQueue;
	ComPtr<ID3D12CommandAllocator> 		_cmdAlloc;
	ComPtr<ID3D12GraphicsCommandList> 	_cmdList;

	ComPtr<ID3D12Fence>			_fence;
	uint32					_fenceValue = 0;
	HANDLE					_fenceEvent = INVALID_HANDLE_VALUE;

	shared_ptr<SwapChain>			_swapChain;
	shared_ptr<DescriptorHeap>		_descHeap;

};
  • CPU ↔ GPU 동시 작업
    • CPU는 GPU에게 작업(명령)을 요청하고, GPU는 이를 처리하여 화면을 렌더링한다.
    • CPU가 외주를 주는 동안에도 계속 작업을 진행 가능하다.
  • 하나씩 요청하면 비효율적 → 여러 개의 명령을 모아 한 번에 전달하는 것이 더 효율적이다.
  • CPU가 명령을 모아서 보내고, GPU가 하나씩 처리하는 방식으로 성능 최적화 가능하다.
객체역할비유
ID3D12GraphicsCommandList명령을 작성“해야 할 작업 리스트” 작성
ID3D12CommandAllocator명령을 저장할 공간 제공“작업 리스트를 넣을 노트”
ID3D12CommandQueueGPU에 전달하여 실행“작업 리스트를 외주 업체(GPU)에게 전달”

커맨더 큐가 외주에게 일감을 주기 위한 명령큐이다.
까다로운 점은, GPU에게 무언가를 해라 하면, 바로 함.
하지만 지금은 뒤늦게 작업하기 때문에, 바로 작업을 하지 않는다.

방식은 효율적이지만, 기존 방식과 다르기 때문에 적응이 어렵다.
이런 저런 요청 중에 swapchain, descritor등을 다 들고 있음.

  • Command QueueGPU에게 일감을 주기 위한 명령 큐 역할을 한다.
  • 기존에는 GPU가 즉시 명령을 실행했지만, DX12에서는 Queue에 저장 후 순차적으로 실행한다.
  • 이러한 방식은 효율적이지만, 기존 방식과 다르기 때문에 적응이 어렵다.
  • SwapChain, Descriptor 등 다양한 요청을 함께 관리해야 한다.
  • 쉽게 말해, GPU에게 줄 외주 일감 목록이며, 이름 그대로 명령을 큐(Queue) 형태로 관리하는 구조이다.
CommandQueue.cpp - Init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void CommandQueue::Init(ComPtr<ID3D12Device> device, shared_ptr<SwapChain> swapChain, shared_ptr<DescriptorHeap> descHeap)
{
	_swapChain = swapChain;
	_descHeap = descHeap;

	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;

	device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&_cmdQueue)); 

	device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&_cmdAlloc));

	device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, _cmdAlloc.Get(), nullptr, IID_PPV_ARGS(&_cmdList));

	_cmdList->Close();

	device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&_fence));
	_fenceEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
}
CommandQueue.cpp - RenderBegin, RenderEnd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void CommandQueue::RenderBegin(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect)
{
	_cmdAlloc->Reset();
	_cmdList->Reset(_cmdAlloc.Get(), nullptr);

	D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
		_swapChain->GetCurrentBackBufferResource().Get(),
		D3D12_RESOURCE_STATE_PRESENT, // 화면 출력
		D3D12_RESOURCE_STATE_RENDER_TARGET); // 외주 결과물

	_cmdList->ResourceBarrier(1, &barrier);

	// Set the viewport and scissor rect.  This needs to be reset whenever the command list is reset.
	_cmdList->RSSetViewports(1, vp);
	_cmdList->RSSetScissorRects(1, rect);

	// Specify the buffers we are going to render to.
	D3D12_CPU_DESCRIPTOR_HANDLE backBufferView = _descHeap->GetBackBufferView();
	_cmdList->ClearRenderTargetView(backBufferView, Colors::LightSteelBlue, 0, nullptr);
	_cmdList->OMSetRenderTargets(1, &backBufferView, FALSE, nullptr);
}
  • Command Allocator & Command List 초기화
    • _cmdAlloc->Reset(); → 기존 명령 초기화.
    • _cmdList->Reset(_cmdAlloc.Get(), nullptr); → 새로운 명령을 받을 준비.
  • 백 버퍼(Back Buffer) 상태 변경
    • D3D12_RESOURCE_BARRIER를 사용하여 PRESENT → RENDER_TARGET 상태로 변경.
  • 뷰포트(Viewport) & 시저(Rect) 설정
    • _cmdList->RSSetViewports(1, vp); → 화면 크기 지정.
    • _cmdList->RSSetScissorRects(1, rect); → 그릴 영역 지정.
  • 렌더 타겟 설정 & 초기화
    • _descHeap->GetBackBufferView(); → 백 버퍼 핸들 가져오기.
    • _cmdList->ClearRenderTargetView(..., Colors::LightSteelBlue, ...); → 화면 초기화 (배경색: LightSteelBlue).
    • _cmdList->OMSetRenderTargets(1, &backBufferView, FALSE, nullptr); → 랜더 타겟 설정.
CommandQueue.cpp - RenderEnd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void CommandQueue::RenderEnd(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect)
{
	D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
		_swapChain->GetCurrentBackBufferResource().Get(),
		D3D12_RESOURCE_STATE_RENDER_TARGET, // 외주 결과물
		D3D12_RESOURCE_STATE_PRESENT); // 화면 출력

	_cmdList->ResourceBarrier(1, &barrier);
	_cmdList->Close();

	ID3D12CommandList* cmdListArr[] = { _cmdList.Get() };
	_cmdQueue->ExecuteCommandLists(_countof(cmdListArr), cmdListArr); 

	_swapChain->Present();

	// Wait until frame commands are complete.  This waiting is inefficient and is
	// done for simplicity.  Later we will show how to organize our rendering code
	// so we do not have to wait per frame.
	WaitSync();

	_swapChain->SwapIndex();
}
  • 백 버퍼 상태 변경 (Render → Present)
    • D3D12_RESOURCE_BARRIER를 사용하여
      D3D12_RESOURCE_STATE_RENDER_TARGET → D3D12_RESOURCE_STATE_PRESENT
      (렌더링 완료된 프레임을 화면에 출력할 수 있도록 변경).
  • Command List 실행
    • _cmdList->Close(); → 명령 리스트 작성 완료.
    • _cmdQueue->ExecuteCommandLists(...); → GPU가 명령을 실행하도록 전달.
  • 프레임 출력 & 동기화
    • _swapChain->Present(); → 현재 프레임을 화면에 출력.
    • WaitSync(); → GPU & CPU 동기화 (비효율적이지만 기본적인 구현).
  • SwapChain 인덱스 변경
    • _swapChain->SwapIndex(); → 다음 프레임을 위한 백 버퍼 인덱스 변경.
CommandQueue.cpp - WaitSync
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
35
36
37
38
39
40
41
42
43
#include "pch.h"
#include "CommandQueue.h"
#include "SwapChain.h"
#include "DescriptorHeap.h"

CommandQueue::~CommandQueue()
{
	::CloseHandle(_fenceEvent);
	// 동적 할당했을 때 delete를 해주는 게 정석인 것처럼
	// 이벤트를 사용할 뒤에는 CloseHandle을 해주는 것이 정석
}

void CommandQueue::Init(ComPtr<ID3D12Device> device, shared_ptr<SwapChain> swapChain, shared_ptr<DescriptorHeap> descHeap)
{
	...

	device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&_fence));
	_fenceEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);

}

void CommandQueue::WaitSync()
{
	// cpu가 gpu를 기다리는 것
	// fence의 일감이 모두 완료될 때까지 기다리는 것
	// Advance the fence value to mark commands up to this fence point.
	_fenceValue++;

	// 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().
	_cmdQueue->Signal(_fence.Get(), _fenceValue);

	// Wait until the GPU has completed commands up to this fence point.
	if (_fence->GetCompletedValue() < _fenceValue)
	{
		// Fire event when GPU hits current fence.  
		_fence->SetEventOnCompletion(_fenceValue, _fenceEvent);

		// Wait until the GPU hits current fence event is fired.
		::WaitForSingleObject(_fenceEvent, INFINITE);
	}
}
  • Fence(펜스)란?
    • GPU와 CPU의 작업 타이밍을 맞추기 위한 동기화 도구.
    • GPU가 모든 명령을 완료할 때까지 CPU가 대기하도록 만듦.
  • WaitSync() 동작 과정
    • _fenceValue++ → 새로운 펜스 값을 설정.
    • _cmdQueue->Signal(_fence.Get(), _fenceValue);
      → GPU가 이 펜스 값까지 모든 명령을 완료하면 신호를 보냄.
    • _fence->SetEventOnCompletion(_fenceValue, _fenceEvent);
      → GPU가 해당 펜스 값을 완료하면 이벤트를 발생시킴.
    • WaitForSingleObject(_fenceEvent, INFINITE);
      GPU가 작업을 완료할 때까지 CPU가 대기.
  • 정리
    • GPU가 끝나야 CPU가 새로운 작업을 전달할 수 있기 때문에 펜스를 사용해 동기화.
    • WaitSync()CPU가 GPU의 작업 완료를 기다리는 역할을 수행.
    • CloseHandle(_fenceEvent); → 동적 할당한 이벤트 핸들은 해제해야 함 (메모리 관리).
  • 결론
    GPU와 CPU의 작업 속도가 다르므로, Fence를 사용해 동기화를 맞추고 CPU가 GPU의 작업 완료를 기다리는 방식으로 동작한다.
This post is licensed under CC BY 4.0 by the author.