Skip to content

Initialize D3D12 components

Tim Chang edited this page Jan 28, 2018 · 1 revision

Lesson 3 : Initialize D3D12 components

After we created a basic window, now we want to initialize D3D components. There is a new class should be create in order to keep the code clean and easy to read. Then we will also create a helper file for some utility functions.

Create a helper header

Now, we add a header named Helper.h used to implement some utility function. And we try to add a exception catcher function as below.

#pragma once

inline void ThrowIfFailed(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw std::exception();
    }
}

Create a new class

First, we create a header and a cpp files for thie new class, name it as Sample. There should be two files, one is Sample.h and the other one is Sample.cpp.

Declare public and private members and functions

We go to Sample.h header, declare those member and functions like below.

#pragma once

#include "stdafx.h"
#include "Helper.h"

// Note that while ComPtr is used to manage the lifetime of resources on the CPU,
// it has no understanding of the lifetime of resources on the GPU. Apps must account
// for the GPU lifetime of resources to avoid destroying objects that may still be
// referenced by the GPU.
// An example of this can be found in the class method: OnDestroy().
using Microsoft::WRL::ComPtr;

class Sample
{
public:
	Sample(UINT width, UINT height, std::wstring name);

	void OnInit(); // D3D components initialization
	void OnUpdate();	// Update frame-based values.
	void OnRender();	// Render the scene.
	void OnDestroy();

	// Accessors.
	UINT GetWidth() const { return m_width; }
	UINT GetHeight() const { return m_height; }
	const WCHAR* GetTitle() const { return m_title.c_str(); }

	void SetParentHWND(const HWND *hwnd) { m_hwnd = hwnd; }

private:
	UINT m_width;
	UINT m_height;

	std::wstring m_title;

	const HWND *m_hwnd;

	// Adapter info.
	bool m_useWarpDevice;

	static const UINT FrameCount = 2;

	// Pipeline objects.
	ComPtr<IDXGISwapChain3> m_swapChain;
	ComPtr<ID3D12Device> m_device;
	ComPtr<ID3D12Resource> m_renderTargets[FrameCount];
	ComPtr<ID3D12CommandAllocator> m_commandAllocator;
	ComPtr<ID3D12CommandQueue> m_commandQueue;
	ComPtr<ID3D12DescriptorHeap> m_rtvHeap;
	ComPtr<ID3D12PipelineState> m_pipelineState;
	ComPtr<ID3D12GraphicsCommandList> m_commandList;
	UINT m_rtvDescriptorSize;

	// Synchronization objects.
	UINT m_frameIndex;
	HANDLE m_fenceEvent;
	ComPtr<ID3D12Fence> m_fence;
	UINT64 m_fenceValue;

	void LoadPipeline();
	void LoadAssets();
	void PopulateCommandList();
	void WaitForPreviousFrame();
	void GetHardwareAdapter(IDXGIFactory2* pFactory, IDXGIAdapter1** ppAdapter);
};

Implement the Sample class

Before start to implement this class, we have to know the basic for D3D components initialization. For components, we can seperate 4 parts.

OnInit

Debug Layer

For this part, we seperate it as two parts again. One is LoadPipeline, and the other one is LoadAssets. In LoadPipeline, we will enable debug layer. In easy, debug layer is a feature that you can easy to know what kind of error or warning your D3D app hits, and you can easy to fix them. Then it is only enabled in debug mode. For more detail, you can refer the MSDN for Debug Layer.

UINT dxgiFactoryFlags = 0;
#if defined(_DEBUG)
	// Enalbe the debug layer (requires the Graphics Tools "optional feature").
	// NOTE: Enabling the debug layer after device creation will invalidation the active device.
	{
		ComPtr<ID3D12Debug> debugController;
		if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
		{
			debugController->EnableDebugLayer();

			// Enable additional debug layers.
			dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
		}
	}
#endif

ComPtr<IDXGIFactory4> factory;
ThrowIfFailed(CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory)));

Get Hardware Adapter

Next, we will try to get the hardware adapter or the software adapter by GetHardwareAdapter function, and create a device for D3D. You can see there is a flag(m_useWarpDevice). If it is true, we will get a software adapter. Or we will get a hardware adapter. For this example, we won't use this flag and the default value is false, so we will get a hardware adapter and create a device.

void Sample::GetHardwareAdapter(IDXGIFactory2* pFactory, IDXGIAdapter1** ppAdapter)
{
	ComPtr<IDXGIAdapter1> adapter;
	*ppAdapter = nullptr;

	for (UINT adapterIndex = 0; DXGI_ERROR_NOT_FOUND != pFactory->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.
			// If you want a software adapter, pass in "/warp" on the command line.
			continue;
		}

		if (SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_11_1, __uuidof(ID3D12Device), nullptr)))
		{
			break;
		}
	}

	*ppAdapter = adapter.Detach();
}

OnInit

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

Create the command queue

Different previous DirectX version, DirectX 12 let developers can communicate with GPU. You can think about this queue will help you sort every command you want to do in GPU, and it can help you avoid to hit unexpected synchronization. For detail, please see [Command Queuee] on MSDN.

// Describe and create the command queue.
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;

ThrowIfFailed(m_device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&m_commandQueue)))

Create the swap chain

Swap chain is used to back buffer switch and rotation. For example, if the screen status is switch from window mode to full-screen mode, swap chain will resize the buffer size and the target. For detail, you can see Swap Chain on MSDN.

// Describe and create the swap chain.
DXGI_SWAP_CHAIN_DESC1 swapChainDesc = {};
swapChainDesc.BufferCount = 2U;
swapChainDesc.Width = 800;
swapChainDesc.Height = 600;
swapChainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapChainDesc.SampleDesc.Count = 1;

ComPtr<IDXGISwapChain1> swapChain;
ThrowIfFailed(factory->CreateSwapChainForHwnd(
	m_commandQueue.Get(),
	*m_hwnd,
	&swapChainDesc,
	nullptr,
	nullptr,
	&swapChain
));

// This sample does not support fullscreen transition.
ThrowIfFailed(factory->MakeWindowAssociation(*m_hwnd, DXGI_MWA_NO_ALT_ENTER));

ThrowIfFailed(swapChain.As(&m_swapChain));
m_frameIndex = m_swapChain->GetCurrentBackBufferIndex();

Create descriptor heaps

Descriptor heaps is used to store all of memory resources. If you want to change the texture of objects, you have to place the memory of texture on the heap. There are some types of descriptor heaps, in our case, we want to create a render target view (RTV) descriptor heap. For detail, refer Descriptor Heaps on MSDN.

// Create descriptor heaps.
{
	// Describe and create a render target view (RTV) descriptor heap.
	D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
	rtvHeapDesc.NumDescriptors = 2U;
	rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
	ThrowIfFailed(m_device->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(&m_rtvHeap)));

	m_rtvDescriptorSize = m_device->GetDescriptorHandleIncrementSiz(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
}

Create frame resources

Then, we will create RTV for each frame.

// Create frame resources.
{
	CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart());

	// Create a RTV for each frame.
	for (UINT n = 0; n < 2U; n++)
	{
		ThrowIfFailed(m_swapChain->GetBuffer(n, IID_PPV_ARGS(&m_renderTargets[n])));
		m_device->CreateRenderTargetView(m_renderTargets[n].Get(), nullptr, rtvHandle);
		rtvHandle.Offset(1, m_rtvDescriptorSize);
	}
    
	ThrowIfFailed(m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(m_commandAllocator)));
}

Create the command list

We create a command list first, then we will add some commands for execute after.

// Create the command list.
ThrowIfFailed(m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator.Ge(), nullptr, IID_PPV_ARGS(&m_commandList)));

// Command lists are created in the recording state, but there is nothing
// to record yet. The main loop expects it to be closed, so close it now.
ThrowIfFailed(m_commandList->Close());

Create synchronization objects.

In GPU pipeline, it can deal with a lot of actions like Copy, Compute and 3D Drawing. And these actions are ran synchronously. So you can consider the fence is a queue which stores these actions. For detail, please see Synchronization and Multi-Engine on MSDN.

OnUpdate

In this function, we will have a lot of rotation, scales, transitions computing. This function will be called each frame. But now we don't do anything here.

OnRender

In this function, we will create some commands for command list. Different with OnUpdate, this function will REALLY do some actions. Like drawing, rotating, scaling and transiting commands. This function will be also called each frame.

Indicate back buffer for RTV

Point out which back buffer of that we want to used for drawing RTV.

// Indicate that the back buffer will be used as a render target.
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTarget[m_frameIndex].Get(), D3D12_RESOURCE_STATE_PRESENT,D3D12_RESOURCE_STATES::D3D12_RESOURCE_STATE_RENDER_TARGET));

CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHandle(m_rtvHeap->GetCPUDescriptorHandleForHeapStart(),m_frameIndex, m_rtvDescriptorSize);

Add action in command

For this example, we will try to clean the view as clear color. Also you can do other actions in this part.

// Record commands.
const float clearColor[] = { 0.0f, 0.2f, 0.4f, 1.0f };
m_commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);

Indicate the present buffer

After adding commands, we will indicate which back buffer we want to present.

// Indicate that the back buffer will now be used to present.
m_commandList->ResourceBarrier(1, &CD3DX12_RESOURCE_BARRIER::Transition(m_renderTargets[m_frameIndex].Get(), D3D12_RESOURCE_STATES::D3D12_RESOURCE_STATE_RENDER_TARGET, D3D12_RESOURCE_STATE_PRESENT));

Execute the command list

Then we will execute the command list.

// Execute the command list.
ID3D12CommandList* ppCommandLists[] = { m_commandList.Get() };
m_commandQueue->ExecuteCommandLists(_countof(ppCommandLists), ppCommandLists);

OnDestroy

This will be called before the app closed. You can destructor every resource or memory you used here.

CloseHandle(m_fenceEvent);

Add this class into main process

Initialize the Sample

We have to new this Sample class in the main process, and call OnInit and OnDestroy in the main function.

Sample* sample = new Sample(800, 600, L"D3D12Basic");

// ---- more ----

// Initialize
sample->OnInit();

// ---- more ----

sample->OnDestroy();

Call OnUpdate and OnRender

Switch to WindowProc function, we will call these two functions when the message is WM_PAINT.

case WM_PAINT:
    if (sample)
    {
    	sample->OnUpdate();
    	sample->OnRender();
    }
    return 0;

Consluion

In this article, we introduced and explained all of parts almost. Not every code shows above. Please see the completed code to know entire example.

Result

After press F5, you should see the window likes below.