오늘은 DirectX12 template의 예시 프로젝트에서 Rendering 부분.

즉, DirectX를 사용하는 부분에 대해 분석을 해볼 차례이다.

그 중 먼저 살펴볼 것은 DeviceResources이다.

 

저번 글에서 DeviceResources는 DirectX에서 제공하는 Device Resource들을 관리한다고 언급했다.

Document에서는 해당 코드를 소유한 Application에 장치 손실 또는 생성에 대한 알림을 받을 수 있는 Interface를 제공한다고 설명하고 있다.

https://docs.microsoft.com/ko-kr/windows/uwp/gaming/user-interface 

 

DirectX 게임 프로젝트 템플릿 - UWP applications

UWP(유니버설 Windows 플랫폼) 및 DirectX 게임을 만드는 템플릿에 대해 알아봅니다.

docs.microsoft.com

https://github.com/Microsoft/DirectXTK/wiki/DeviceResources

 

microsoft/DirectXTK

The DirectX Tool Kit (aka DirectXTK) is a collection of helper classes for writing DirectX 11.x code in C++ - microsoft/DirectXTK

github.com

github wiki에서는 device & swapchain, 선택적 깊이 버퍼에 대한 추상화를 제공한다고 설명하고 있다.

그 외에 설명으로는 단순히 'boilerplate'를 별도의 파일로 정리 한 것이라고 되어 있다.

여기서 boilerplate란, 최소한의 수정만을 거쳐 여러 곳에 필수적으로 사용되는 코드를 지칭한다.

https://en.wikipedia.org/wiki/Boilerplate_code

 

Boilerplate code - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Code that has to be included in many places with little or no alteration In computer programming, boilerplate code or just boilerplate are sections of code that have to be included in

en.wikipedia.org

그리하여 DeviceResources는 가능하면 건드리지 않는 것이 좋은 코드라는 것을 나름 긴 설명을 통해 깨달았다.

이제 본격적으로 코드를 파헤쳐보자.

#pragma once

namespace DX
{
	static const UINT c_frameCount = 3;		// Use triple buffering.

	// Controls all the DirectX device resources.
	class DeviceResources
	{
	public:
		DeviceResources(DXGI_FORMAT backBufferFormat = DXGI_FORMAT_B8G8R8A8_UNORM, DXGI_FORMAT depthBufferFormat = DXGI_FORMAT_D32_FLOAT);
		void SetWindow(Windows::UI::Core::CoreWindow^ window);
		void SetLogicalSize(Windows::Foundation::Size logicalSize);
		void SetCurrentOrientation(Windows::Graphics::Display::DisplayOrientations currentOrientation);
		void SetDpi(float dpi);
		void ValidateDevice();
		void Present();
		void WaitForGpu();

		// The size of the render target, in pixels.
		Windows::Foundation::Size	GetOutputSize() const				{ return m_outputSize; }

		// The size of the render target, in dips.
		Windows::Foundation::Size	GetLogicalSize() const				{ return m_logicalSize; }

		float						GetDpi() const						{ return m_effectiveDpi; }
		bool						IsDeviceRemoved() const				{ return m_deviceRemoved; }

		// D3D Accessors.
		ID3D12Device*				GetD3DDevice() const				{ return m_d3dDevice.Get(); }
		IDXGISwapChain3*			GetSwapChain() const				{ return m_swapChain.Get(); }
		ID3D12Resource*				GetRenderTarget() const				{ return m_renderTargets[m_currentFrame].Get(); }
		ID3D12Resource*				GetDepthStencil() const				{ return m_depthStencil.Get(); }
		ID3D12CommandQueue*			GetCommandQueue() const				{ return m_commandQueue.Get(); }
		ID3D12CommandAllocator*		GetCommandAllocator() const			{ return m_commandAllocators[m_currentFrame].Get(); }
		DXGI_FORMAT					GetBackBufferFormat() const			{ return m_backBufferFormat; }
		DXGI_FORMAT					GetDepthBufferFormat() const		{ return m_depthBufferFormat; }
		D3D12_VIEWPORT				GetScreenViewport() const			{ return m_screenViewport; }
		DirectX::XMFLOAT4X4			GetOrientationTransform3D() const	{ return m_orientationTransform3D; }
		UINT						GetCurrentFrameIndex() const		{ return m_currentFrame; }

		CD3DX12_CPU_DESCRIPTOR_HANDLE GetRenderTargetView() const
		{
			return CD3DX12_CPU_DESCRIPTOR_HANDLE(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(), m_currentFrame, m_rtvDescriptorSize);
		}
		CD3DX12_CPU_DESCRIPTOR_HANDLE GetDepthStencilView() const
		{
			return CD3DX12_CPU_DESCRIPTOR_HANDLE(m_dsvHeap->GetCPUDescriptorHandleForHeapStart());
		}

	private:
		void CreateDeviceIndependentResources();
		void CreateDeviceResources();
		void CreateWindowSizeDependentResources();
		void UpdateRenderTargetSize();
		void MoveToNextFrame();
		DXGI_MODE_ROTATION ComputeDisplayRotation();
		void GetHardwareAdapter(IDXGIAdapter1** ppAdapter);

		UINT											m_currentFrame;

		// Direct3D objects.
		Microsoft::WRL::ComPtr<ID3D12Device>			m_d3dDevice;
		Microsoft::WRL::ComPtr<IDXGIFactory4>			m_dxgiFactory;
		Microsoft::WRL::ComPtr<IDXGISwapChain3>			m_swapChain;
		Microsoft::WRL::ComPtr<ID3D12Resource>			m_renderTargets[c_frameCount];
		Microsoft::WRL::ComPtr<ID3D12Resource>			m_depthStencil;
		Microsoft::WRL::ComPtr<ID3D12DescriptorHeap>	m_rtvHeap;
		Microsoft::WRL::ComPtr<ID3D12DescriptorHeap>	m_dsvHeap;
		Microsoft::WRL::ComPtr<ID3D12CommandQueue>		m_commandQueue;
		Microsoft::WRL::ComPtr<ID3D12CommandAllocator>	m_commandAllocators[c_frameCount];
		DXGI_FORMAT										m_backBufferFormat;
		DXGI_FORMAT										m_depthBufferFormat;
		D3D12_VIEWPORT									m_screenViewport;
		UINT											m_rtvDescriptorSize;
		bool											m_deviceRemoved;

		// CPU/GPU Synchronization.
		Microsoft::WRL::ComPtr<ID3D12Fence>				m_fence;
		UINT64											m_fenceValues[c_frameCount];
		HANDLE											m_fenceEvent;

		// Cached reference to the Window.
		Platform::Agile<Windows::UI::Core::CoreWindow>	m_window;

		// Cached device properties.
		Windows::Foundation::Size						m_d3dRenderTargetSize;
		Windows::Foundation::Size						m_outputSize;
		Windows::Foundation::Size						m_logicalSize;
		Windows::Graphics::Display::DisplayOrientations	m_nativeOrientation;
		Windows::Graphics::Display::DisplayOrientations	m_currentOrientation;
		float											m_dpi;

		// This is the DPI that will be reported back to the app. It takes into account whether the app supports high resolution screens or not.
		float											m_effectiveDpi;

		// Transforms used for display orientation.
		DirectX::XMFLOAT4X4								m_orientationTransform3D;
	};
}

DeviceResources.h이다.

Method 중 'Get'으로 시작하거나 'Is'로 시작하는 method, 'Set'으로 시작하는 method들은 설명을 생략하겠다.

이들은 대체로 field들의 Getter, Setter 함수들이기에 값을 받아오거나 설정하는 기능으로만 기억하면 된다.

중요한 것은 이런 method가 아니라 그것이 가르키는 field가 무엇인지 아니겠는가?

 

#include "pch.h"
#include "DeviceResources.h"
#include "DirectXHelper.h"

using namespace DirectX;
using namespace Microsoft::WRL;
using namespace Windows::Foundation;
using namespace Windows::Graphics::Display;
using namespace Windows::UI::Core;
using namespace Windows::UI::Xaml::Controls;
using namespace Platform;

namespace DisplayMetrics
{
	// High resolution displays can require a lot of GPU and battery power to render.
	// High resolution phones, for example, may suffer from poor battery life if
	// games attempt to render at 60 frames per second at full fidelity.
	// The decision to render at full fidelity across all platforms and form factors
	// should be deliberate.
	static const bool SupportHighResolutions = false;

	// The default thresholds that define a "high resolution" display. If the thresholds
	// are exceeded and SupportHighResolutions is false, the dimensions will be scaled
	// by 50%.
	static const float DpiThreshold = 192.0f;		// 200% of standard desktop display.
	static const float WidthThreshold = 1920.0f;	// 1080p width.
	static const float HeightThreshold = 1080.0f;	// 1080p height.
};

// Constants used to calculate screen rotations.
namespace ScreenRotation
{
	// 0-degree Z-rotation
	static const XMFLOAT4X4 Rotation0(
		1.0f, 0.0f, 0.0f, 0.0f,
		0.0f, 1.0f, 0.0f, 0.0f,
		0.0f, 0.0f, 1.0f, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f
		);

	// 90-degree Z-rotation
	static const XMFLOAT4X4 Rotation90(
		0.0f, 1.0f, 0.0f, 0.0f,
		-1.0f, 0.0f, 0.0f, 0.0f,
		0.0f, 0.0f, 1.0f, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f
		);

	// 180-degree Z-rotation
	static const XMFLOAT4X4 Rotation180(
		-1.0f, 0.0f, 0.0f, 0.0f,
		0.0f, -1.0f, 0.0f, 0.0f,
		0.0f, 0.0f, 1.0f, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f
		);

	// 270-degree Z-rotation
	static const XMFLOAT4X4 Rotation270(
		0.0f, -1.0f, 0.0f, 0.0f,
		1.0f, 0.0f, 0.0f, 0.0f,
		0.0f, 0.0f, 1.0f, 0.0f,
		0.0f, 0.0f, 0.0f, 1.0f
		);
};

DeviceResources.cpp에서 header나 namespace 선언, 상수 선언 부분만 가져온 것이다.

 

먼저 DisplayMatrics namespace 안에 선언된 상수들을 살펴보자.

SupportHighResolutions는 High Resolution을 지원하는지를 판단하는 변수이다.

High Resolution은 많은 GPU resource와 전력을 소모하는 작업이다.

그렇기에 예시에서는 이를 지원하지 않는 것으로 결정 한 것 같다.

WidthThreshold, HeightThreshold는 화면의 너비와 높이 크기 제한을 결정한다.

DpiThreshold 역시 해상도의 제한을 결정하며, 192.0은 보통 데스크탑 화면의 200% 정도이다.

 

ScreenRotation은 4×4 Matrix 상에서 Z축을 기준으로 0, 90, 180, 360 회전 연산을 Matrix로 표현한 것이다.

 

이 중 다시 한번 낯선 것이 있다.

Header에 보면 [DirectXHelper.h]라는 header 파일이 보일 것이다.

DirectXHelper.h는 Microsoft에서 제공하는 DirectX 개발을 지원하는 코드이다.

#pragma once

#include <ppltasks.h>	// For create_task

namespace DX
{
	inline void ThrowIfFailed(HRESULT hr)
	{
		if (FAILED(hr))
		{
			// Set a breakpoint on this line to catch Win32 API errors.
			throw Platform::Exception::CreateException(hr);
		}
	}

	// Function that reads from a binary file asynchronously.
	inline Concurrency::task<std::vector<byte>> ReadDataAsync(const std::wstring& filename)
	{
		using namespace Windows::Storage;
		using namespace Concurrency;

		auto folder = Windows::ApplicationModel::Package::Current->InstalledLocation;

		return create_task(folder->GetFileAsync(Platform::StringReference(filename.c_str()))).then([](StorageFile^ file)
		{
			return FileIO::ReadBufferAsync(file);
		}).then([](Streams::IBuffer^ fileBuffer) -> std::vector<byte>
		{
			std::vector<byte> returnBuffer;
			returnBuffer.resize(fileBuffer->Length);
			Streams::DataReader::FromBuffer(fileBuffer)->ReadBytes(Platform::ArrayReference<byte>(returnBuffer.data(), fileBuffer->Length));
			return returnBuffer;
		});
	}

	// Converts a length in device-independent pixels (DIPs) to a length in physical pixels.
	inline float ConvertDipsToPixels(float dips, float dpi)
	{
		static const float dipsPerInch = 96.0f;
		return floorf(dips * dpi / dipsPerInch + 0.5f); // Round to nearest integer.
	}

	// Assign a name to the object to aid with debugging.
#if defined(_DEBUG)
	inline void SetName(ID3D12Object* pObject, LPCWSTR name)
	{
		pObject->SetName(name);
	}
#else
	inline void SetName(ID3D12Object*, LPCWSTR)
	{
	}
#endif
}

// Naming helper function for ComPtr<T>.
// Assigns the name of the variable as the name of the object.
#define NAME_D3D12_OBJECT(x) DX::SetName(x.Get(), L#x)

ThrowIfFailed는 DirectX Win32 API에서 반환된 오류 HRESULT 값을 Windows Runtime Exception으로 변환한다.

이를 이용해 DirectX Error를 Debug 하기 위한 중단점을 배치한다.

https://github.com/Microsoft/DirectXTK/wiki/ThrowIfFailed

 

microsoft/DirectXTK

The DirectX Tool Kit (aka DirectXTK) is a collection of helper classes for writing DirectX 11.x code in C++ - microsoft/DirectXTK

github.com

ReadDataAsync는 binary file을 비동기적으로 읽는 함수이다.

마지막으로 ConvertDipsToPixels는 DIP의 길이를 실제 Pixel의 길이로 변환하는 함수이다.

 

다음은 DeviceResources의 생성자 부분이다.

// Constructor for DeviceResources.
DX::DeviceResources::DeviceResources(DXGI_FORMAT backBufferFormat, DXGI_FORMAT depthBufferFormat) :
	m_currentFrame(0),
	m_screenViewport(),
	m_rtvDescriptorSize(0),
	m_fenceEvent(0),
	m_backBufferFormat(backBufferFormat),
	m_depthBufferFormat(depthBufferFormat),
	m_fenceValues{},
	m_d3dRenderTargetSize(),
	m_outputSize(),
	m_logicalSize(),
	m_nativeOrientation(DisplayOrientations::None),
	m_currentOrientation(DisplayOrientations::None),
	m_dpi(-1.0f),
	m_effectiveDpi(-1.0f),
	m_deviceRemoved(false)
{
	CreateDeviceIndependentResources();
	CreateDeviceResources();
}

// Configures resources that don't depend on the Direct3D device.
void DX::DeviceResources::CreateDeviceIndependentResources()
{
}

생성자에서는 크게 2가지 작업이 이루어지고 있다.

하나는 field의 초기화. 다른 하나는 Resources를 생성하고 있다.

Resource는 두가지로 분류된다. Direct3D device와 독립적인 것과, 연관된 것.

본 예시는 DirectX12를 사용하기 대문에 독립적인 부분인 CreateDeviceIndependentResources는 빈 함수이다.

 

그렇다면 남은 함수인 CreateDeviceResources를 보자.

// Configures the Direct3D device, and stores handles to it and the device context.
void DX::DeviceResources::CreateDeviceResources()
{
#if defined(_DEBUG)
	// If the project is in a debug build, enable debugging via SDK Layers.
	{
		ComPtr<ID3D12Debug> debugController;
		if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
		{
			debugController->EnableDebugLayer();
		}
	}
#endif

	DX::ThrowIfFailed(CreateDXGIFactory1(IID_PPV_ARGS(&m_dxgiFactory)));

	ComPtr<IDXGIAdapter1> adapter;
	GetHardwareAdapter(&adapter);

	// Create the Direct3D 12 API device object
	HRESULT hr = D3D12CreateDevice(
		adapter.Get(),					// The hardware adapter.
		D3D_FEATURE_LEVEL_11_0,			// Minimum feature level this app can support.
		IID_PPV_ARGS(&m_d3dDevice)		// Returns the Direct3D device created.
		);

#if defined(_DEBUG)
	if (FAILED(hr))
	{
		// If the initialization fails, fall back to the WARP device.
		// For more information on WARP, see: 
		// https://go.microsoft.com/fwlink/?LinkId=286690

		ComPtr<IDXGIAdapter> warpAdapter;
		DX::ThrowIfFailed(m_dxgiFactory->EnumWarpAdapter(IID_PPV_ARGS(&warpAdapter)));

		hr = D3D12CreateDevice(warpAdapter.Get(), D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&m_d3dDevice));
	}
#endif

	DX::ThrowIfFailed(hr);

	// Create the command queue.
	D3D12_COMMAND_QUEUE_DESC queueDesc = {};
	queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;

	DX::ThrowIfFailed(m_d3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_commandQueue)));
	NAME_D3D12_OBJECT(m_commandQueue);

	// Create descriptor heaps for render target views and depth stencil views.
	D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
	rtvHeapDesc.NumDescriptors = c_frameCount;
	rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	DX::ThrowIfFailed(m_d3dDevice->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_rtvHeap)));
	NAME_D3D12_OBJECT(m_rtvHeap);

	m_rtvDescriptorSize = m_d3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

	D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
	dsvHeapDesc.NumDescriptors = 1;
	dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
	dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	ThrowIfFailed(m_d3dDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(&m_dsvHeap)));
	NAME_D3D12_OBJECT(m_dsvHeap);

	for (UINT n = 0; n < c_frameCount; n++)
	{
		DX::ThrowIfFailed(
			m_d3dDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(&m_commandAllocators[n]))
			);
	}

	// Create synchronization objects.
	DX::ThrowIfFailed(m_d3dDevice->CreateFence(m_fenceValues[m_currentFrame], D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&m_fence)));
	m_fenceValues[m_currentFrame]++;

	m_fenceEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
	if (m_fenceEvent == nullptr)
	{
		DX::ThrowIfFailed(HRESULT_FROM_WIN32(GetLastError()));
	}
}

간단하게 설명을 하자면

1) DXGIFactory1을 생성하면서 실패 시 Error를 throw 한다.

2) 1)이 문제 없이 동작하면 Comptr<IDXGIAdapter1>에 GetHardwareAdapter의 반환 값을 저장한다.

GetHardWareAdpater는 Direct3D 12를 제공하는 사용 가능한 Hardware Adapter 중 첫번째를 반환한다.

// This method acquires the first available hardware adapter that supports Direct3D 12.
// If no such adapter can be found, *ppAdapter will be set to nullptr.
void DX::DeviceResources::GetHardwareAdapter(IDXGIAdapter1** ppAdapter)
{
	ComPtr<IDXGIAdapter1> adapter;
	*ppAdapter = nullptr;

	for (UINT adapterIndex = 0; DXGI_ERROR_NOT_FOUND != m_dxgiFactory->EnumAdapters1(adapterIndex, &adapter); adapterIndex++)
	{
		DXGI_ADAPTER_DESC1 desc;
		adapter->GetDesc1(&desc);

		if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
		{
			// Don't select the Basic Render Driver adapter.
			continue;
		}

		// Check to see if the adapter supports Direct3D 12, but don't create the
		// actual device yet.
		if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_0, _uuidof(ID3D12Device), nullptr)))
		{
			break;
		}
	}

	*ppAdapter = adapter.Detach();
}

3) 2)에서 저장한 HardwareAdapter와 이 Application이 제공 할 최소 Direct3D feature level, 생성 될 Direct3D device를 저장 할 변수를 넣어 Direct3D 12 API Device Object를 생성한다.

4) 3)에서 생성한 Device Object 값이 유효한지 검사한다.

5) command queue를 생성한다.

6) render target views와 depth stencil vies에 사용될 descriptor heap을 생성한다.

7) 동기화 오브젝트를 생성한다.

 

솔직히 적으면서도 정확히 어떤 의미인지 파악이 잘 되지 않는다.

하지만 DeviceResources를 살펴보면서 몇가지 느낀 점이 있다.

1. DeviceResources가 하고 있는 작업은 입문자가 따로 처리하기 복잡한 작업들이다.

2. DeviceResources는 따로 제공을 하는 파일이다.

3. DirectX와 관련된 작업들이기는 하나, Rendering과 직접 연관이 있는 것은 아니다. 편의성, 성능과 연관된 작업이다.

 

그렇기에 DeviceResources와 관련된 설명은 아래 링크로 대체한다.

https://github.com/Microsoft/DirectXTK/wiki/DeviceResources

 

microsoft/DirectXTK

The DirectX Tool Kit (aka DirectXTK) is a collection of helper classes for writing DirectX 11.x code in C++ - microsoft/DirectXTK

github.com

결국 DeviceResources도 이전에 살펴보았던 App과 마찬가지로 수정할 수 없는 부분이었다.

정확히는 d3d12.h, DirectXHelper.h와 더 비슷한 것 같다.

더불어 Rendering 부분에서 사용하는 StepTimer.h도 이와 비슷한 부류라 생각한다.

이들의 공통적은 Project 상에서 Common 폴더 밑에 존재한다는 점이다.

 

자세한 것은 살펴봐야 알겠지만 Template Project에서 directory에 따라 기능이 나뉘는 것 같다.

Common/*: Project 전반적으로 사용되는 코드나 함수들. 주로 Microsoft에서 편의성을 위해 제공되는 함수들이 포함됨

Content/*: 실질적으로 Rendering과 관련된 코드들. hlsl등도 여기에 포함 됨.

기타 파일들 : Rendering을 제외한 Windows Application과 관련된 코드. 혹은 이를 Rendering과 연결하는 코드.

 

다음에는 ExampleCreateionMain을 시작으로 Rendering 관련 부분들을 살펴보도록 하겠습니다.

+ Recent posts