(DirectX : Basic) 2. 장치 초기화

Posted by : at

Category : DirectX



DirectX를 사용하기 위해서 여러 클래스를 정의한다.
총 5개의 클래스

  1. Engine
  2. Device
  3. CommandQueue
  4. SwapChain
  5. DesciptorHeap

을 정의할 예정이고 모든 클래스를 관리하는 Engine클래스를 먼저 설명하겠다.


Engine

#pragma once

class Engine
{
public:

	// 초기화
	void Init(const WindowInfo& info);
	/*
	struct WindowInfo
	{
		HWND hwnd;		// 핸들
		int32 width
		int32 height;
		bool windowed;	// 창모드
	};
	*/

	// 렌더링
	void Render();

public:
	void RenderBegin();
	void RenderEnd();

	void ResizeWindow(int32 width, int32 height);

private:
	WindowInfo		_window;
	// 위도우 정보(크기, 핸들 등을 담는다)

	D3D12_VIEWPORT	_viewport = {};
	// Describes the dimensions of a viewport.
	// x, y, width, height 를 정의
	// _viewport = { 0, 0, static_cast<FLOAT>(info.width), static_cast<FLOAT>(info.height), 0.0f, 1.0f };
	
	D3D12_RECT		_scissorRect = {};
	// Just Rect
	// 역시 x, y, width, height 를 정의
	// 단, 랜더링을 어디서 부터할지를 scissorRect에서 정의한다.
	// _scissorRect = CD3DX12_RECT(0, 0, info.width, info.height);

	// 아래 네 가지 클래스로 DirectX를 컨트롤하게 되며
	// 네 클래스를 관리하는 것이 Engine 클래스 이다.
	shared_ptr<class Device> _device;
	shared_ptr<class CommandQueue> _cmdQueue;
	shared_ptr<class SwapChain> _swapChain;
	shared_ptr<class DescriptorHeap> _descHeap;
};
#include "pch.h"
#include "Engine.h"
#include "Device.h"
#include "CommandQueue.h"
#include "SwapChain.h"
#include "DescriptorHeap.h"

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);

	// 모든 클래스를 여기서 초기화 하며 필요한 정보를 넘긴다.
	_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);
}
void Engine::RenderBegin()
{
	// width, height 정보를 queue로 넘긴다.
	_cmdQueue->RenderBegin(&_viewport, &_scissorRect);
}

queue를 먼저 잠깐보자면

void CommandQueue::RenderBegin(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect) 
{
	// ... 
	// 명령리스트(일단 받아들이자)에 Viewport, Scissor Rect를 넘겨서 어디까지 그려달라 명령한다.
	_cmdList->RSSetViewports(1, vp); 
	_cmdList->RSSetScissorRects(1, rect);

Viewport, Scissor Rect가 궁금하다면 아래를 참조하자.

대략적 설명은.

  • Viewport : 렌더링을 할 렌더타겟(후면버퍼) 영역을 나타낸다.
  • ScissorRect : 렌더링에서 제거하지 않을 영역을 설정한다. ScissorRect에 포함되지 않는 영역은 렌더링(레스터라이저)에서 제거된다.
    • 참고) 레스터라이저 단계(rasterizer stage) : 래스터화 단계는 실시간 3D 그래픽을 표시 하기 위해 벡터 정보 (도형 또는 기본으로 구성)를 래스터 이미지 (픽셀)로 변환 합니다.

Device

#pragma once

// 인력 사무소
class Device
{
public:
	void Init();

	ComPtr<IDXGIFactory> GetDXGI() { return _dxgi; }
	ComPtr<ID3D12Device> GetDevice() { return _device; }

private:
	// ComPtr : DirectX(COM객체)를 사용하는 일종의 스마트 포인터라 이해하면 된다.

	ComPtr<ID3D12Debug>			_debugController;
	// 디버그 출력을 담당(크게 중요하지 않음 무시해도 됨.)

	ComPtr<IDXGIFactory>		_dxgi; 
	// 화면 관련 기능들, DirectX 공통기능 API라 생각
	// 전체화면, 사용가능한 그래픽카드 리스트 등
	// SwapChain제작도 담당

	ComPtr<ID3D12Device>		_device; 
	// 그래픽 카드 자체를 의미
	// 각종 객체 생성, GPU와 통신을 담당하는 녀석
};

init 함수는 위 변수를 생성하는 부분이라 쉬워서 생략한다.


CommandQueue

DirectX는 GPU에 실시간으로 명령을 보내는 것이 아니라
Queue를 통해서 GPU에게 명령을 요청해 두고 GPU는 queue에서 명령을 꺼내서 처리하는 형식이다.

어떻게 명령을 보내는지 간략하게 설명하자면

  1. Alloc을 통해서 명령을 보낼 메모리 공간을 GPU에 할당한다
  2. 명령을 list에 담는다
  3. queue를 통해서 명령을 보낸다.
class CommandQueue
{
	// ...

private:
	// CommandQueue : DX12에 등장
	// 외주를 요청할 때, 하나씩 요청하면 비효율적
	// [외주 목록]에 일감을 차곡차곡 기록했다가 한 방에 요청하는 것

	ComPtr<ID3D12CommandQueue>			_cmdQueue;
	// 일감을 보내는 녀석
	// (참고) _cmdQueue->ExecuteCommandLists(_countof(cmdListArr), cmdListArr);

	ComPtr<ID3D12CommandAllocator>		_cmdAlloc;
	// 일감이 쌓이는 메모리공간

	ComPtr<ID3D12GraphicsCommandList>	_cmdList;
	// 일감 리스트 목록

	// Fence : 울타리(?)
	// CPU / GPU 동기화를 위한 간단한 도구
	// CPU에서 외주준 데이터가 GPU가 작업완료 될때까지 대기한다.
	ComPtr<ID3D12Fence>					_fence;
	uint32								_fenceValue = 0;		// fence번호(CPU가 외주한 데이터가 몇번인지 기록)
	// 일감에 숫자를 지정해 자신이 명령한 일감이 맞는지 확인용
	HANDLE								_fenceEvent = INVALID_HANDLE_VALUE;
	// Event를 통해 GPU의 작업이 완료됐는지 통보 받을수 있음.

	// ...
};
  1. Alloc을 통해서 명령을 보낼 메모리 공간을 GPU에 할당한다
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를 통해 생성함을 주목.

	// ID3D12CommandQueue 생성
	device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&_cmdQueue));

	// ID3D12CommandAllocator 생성
	// - D3D12_COMMAND_LIST_TYPE_DIRECT : GPU가 직접 실행하는 명령 목록
	device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&_cmdAlloc));

	// ID3D12GraphicsCommandList 생성
	// GPU가 하나인 시스템에서는 0으로
	// DIRECT or BUNDLE
	// Allocator
	// 초기 상태 (그리기 명령은 nullptr 지정)
	device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, _cmdAlloc.Get(), nullptr, IID_PPV_ARGS(&_cmdList));

	// CommandList는 Close / Open 상태가 있는데
	// Open 상태에서 Command를 넣다가 Close한 다음 제출하는 개념
	_cmdList->Close();

	// CreateFence
	// - CPU와 GPU의 동기화 수단으로 쓰인다
	device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&_fence));
	_fenceEvent = ::CreateEvent(nullptr, FALSE, FALSE, nullptr);
}
void CommandQueue::RenderBegin(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect) 
{
	// 다시 명령을 보낼경우 alloc된 메모리와 list를 초기화 해줘야하며,
	// list에게는 다시 alloc된 메모리 공간정보를 알려줘야한다.
	_cmdAlloc->Reset(); 
	_cmdList->Reset(_cmdAlloc.Get(), nullptr);
	
	// ...
  1. 명령을 list에 담는다
void CommandQueue::RenderBegin(const D3D12_VIEWPORT* vp, const D3D12_RECT* rect) 
{
	// 명령 메모리 공간할당
	_cmdAlloc->Reset();

	// 명령을 담을 List를 준비
	_cmdList->Reset(_cmdAlloc.Get(), nullptr); 


	// barrier(장벽)을 두고 특정메모리 공간을 어떻게 쓸것인가를 알린다.
	D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition( 
		_swapChain->GetCurrentBackBufferResource().Get(), 
		D3D12_RESOURCE_STATE_PRESENT, 		 // (before) 현재 출력중인 화면 
		D3D12_RESOURCE_STATE_RENDER_TARGET); // (after) GPU가 작업중인 공간 
		// before(기존에)는 현재 출력중인 화면이였는데 after(현재)는 GPU가 작업할 공간으로 쓰자.
	// 메모리 공간 _swapChain->GetCurrentBackBufferResource().Get()을
	// 현재 D3D12_RESOURCE_STATE_PRESENT로 쓰고 있는데
	// D3D12_RESOURCE_STATE_RENDER_TARGET로 쓰게 해달라

	// (참고) ComPtr<ID3D12Resource> GetCurrentBackBufferResource() { return _renderTargets[_backBufferIndex]; }
	// (참고) ComPtr<ID3D12Resource>	_renderTargets[SWAP_CHAIN_BUFFER_COUNT];
	// _renderTargets는 ID3D12Resource임

	/*
	
	// (참고) RenderTarget은 아래와 같이 생성됨.

	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 목록 : [ ] [ ]
		device->CreateDescriptorHeap(&rtvDesc, IID_PPV_ARGS(&_rtvHeap));

		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]);
		}

	*/


	// 위 화면교체 명령을 바로 실행하는 것이 아니라 List에 추가 해 놓는 형태이다. 
	_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);
	// GPU에게 backBufferView 백버퍼에 그려달라고 요청
	// OMSetRenderTargets : Render-Target과 Depth-Stencil-View의 Merger (Output-Merger(OM))
	// Render-Target과 Depth-Stencil-View를 합쳐주는 부분인데 일단은 받아 들인다.

	// 어쨋든 중요한건 _cmdList에 명령을 담고 있는것을 주목해서 보자.
  1. queue를 통해서 명령을 보낸다.
void CommandQueue::RenderEnd() 
{ 
	D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition( 
		_swapChain->GetCurrentBackBufferResource().Get(), 
		D3D12_RESOURCE_STATE_RENDER_TARGET, 	// (before) GPU가 작업중인 공간 
		D3D12_RESOURCE_STATE_PRESENT); 			// (after) 화면 출력중인 화면 


	// List에 명령을 넣는다. 
	_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(); 
}

여기서 계속 헷갈리는거 정리

  • ComPtr<ID3D12DescriptorHeap> _rtvHeap; : ID3D12DescriptorHeap를 통해서 RenderTargetView를 생성
  • D3D12_CPU_DESCRIPTOR_HANDLE _rtvHandle[SWAP_CHAIN_BUFFER_COUNT]; : DescriptorHeap에서는 RenderTargetView의 HANDLE만을 관리
  • ComPtr<ID3D12Resource> _renderTargets[SWAP_CHAIN_BUFFER_COUNT]; : SwapChain에서 실질적 RenderTarget의 주소를 관리
  • SwapChain->Present(화면출력) 순서
    • CommandQueue::RenderBegin 호출되며 내부실행
      • D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition 를 통해 버퍼교체
      • D3D12_CPU_DESCRIPTOR_HANDLE backBufferView = _descHeap->GetBackBufferView(); HANDLE을 통해 RTV 그려질 부분 선언
      • _cmdList->ClearRenderTargetView(backBufferView, Colors::LightSteelBlue, 0, nullptr); 테스트 코드에선 파란색으로 그림

SwapChain

스왑체인은 더블버퍼링을 담당하는 부분이다. 더 자세한 설명은 다른 블로그 참조…

#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;		// Present를 호출시 이 핸들에 그려준다.
	sd.Windowed = info.windowed;
	sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; // 전면 후면 버퍼 교체 시 이전 프레임 정보 버림
	sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

	// swapchain은 dxgi를 통해서 만든다.
	dxgi->CreateSwapChain(cmdQueue.Get(), &sd, &_swapChain);

	// 스왑체인의 메모리 주소를 _renderTargets에 할당한다.
	for (int32 i = 0; i < SWAP_CHAIN_BUFFER_COUNT; i++)
		_swapChain->GetBuffer(i, IID_PPV_ARGS(&_renderTargets[i]));

	// 참고) ComPtr<ID3D12Resource>	_renderTargets[SWAP_CHAIN_BUFFER_COUNT];
}
void SwapChain::Present()
{
	// Present the frame.
	// 화면에 프리젠트(출력)해 달라
	_swapChain->Present(0, 0);
	// init에서 받은 핸들에 그리게 된다.
}

DescriptorHeap(Render-Target View)

Render-Target의 핸들이라 생각. Render-Target에 명령을 넣고 싶다면 여기를 만지면 되는데 현재는 사용되는 부분이 없기에 우선 설명은 생략한다.

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

	// Descriptor (DX12) = View (~DX11)
	// [서술자 힙]으로 RTV 생성
	// DX11의 RTV(RenderTargetView), DSV(DepthStencilView), 
	// CBV(ConstantBufferView), SRV(ShaderResourceView), UAV(UnorderedAccessView)를 전부 DescriptorHeap에서 관리

	_rtvHeapSize = device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

	// 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 목록 : [ ] [ ]
	// RTV를 배열로 생서해주세요
	device->CreateDescriptorHeap(&rtvDesc, IID_PPV_ARGS(&_rtvHeap));

	// RTV의 주소값을 줘
	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);
		// SwapChain에 있는 RTV 주소를 가져와 생성
		device->CreateRenderTargetView(swapChain->GetRenderTarget(i).Get(), nullptr, _rtvHandle[i]);
	}
}

About Taehyung Kim

안녕하세요? 8년차 현업 C++ 개발자 김태형이라고 합니다. 😁 C/C++을 사랑하며 다양한 사람과의 협업을 즐깁니다. ☕ 꾸준한 자기개발을 미덕이라 생각하며 노력중이며, 제가 얻은 지식을 홈페이지에 정리 중입니다. 좀 더 상세한 제 이력서 혹은 Private 프로젝트 접근 권한을 원하신다면 메일주세요. 😎

Star
Useful Links