Tutorial 3: Initializing DirectX 12

This tutorial will be the first introduction to working with DirectX 12. We will address how to initialize and shut down Direct3D as well as how to render to a window.


DirectX 12 Core Concepts

In this tutorial we are going to concentrate on the bare minimums to just clear the window to a specified color. This will allow us to learn some of the basic core concepts in DirectX 12. After going through this tutorial you should understand the use of the device, swap chain, back buffers, command lists, command queue, and fences. All of the information I provide here was obtained from the MSDN website which is the primary location for their documentation and I recommend reading up on all the functions and such there to help you get a deeper understanding of how everything works in DirectX 12.


Updated Framework

We are going to add another class to the framework which will handle all the Direct3D system functions. We will call the class D3DClass. I have updated the framework diagram below:

As you can see the D3DClass will be located inside the GraphicsClass. The previous tutorial mentioned that all new graphics related classes will be encapsulated in the GraphicsClass and that is why it is the best location for the new D3DClass. Now let's take a look at the changes made to the GraphicsClass:


Graphicsclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _GRAPHICSCLASS_H_
#define _GRAPHICSCLASS_H_

Here is the first change. We have taken out the include for windows.h and instead included the new d3dclass.h.

///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "d3dclass.h"

/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.1f;


////////////////////////////////////////////////////////////////////////////////
// Class name: GraphicsClass
////////////////////////////////////////////////////////////////////////////////
class GraphicsClass
{
public:
	GraphicsClass();
	GraphicsClass(const GraphicsClass&);
	~GraphicsClass();

	bool Initialize(int, int, HWND);
	void Shutdown();
	bool Frame();

private:
	bool Render();

private:

And the second change is the new private pointer to the D3DClass which we have called m_Direct3D. In case you were wondering I use the prefix m_ on all class variables. That way when I'm coding I can remember quickly which variables are members of the class and which are not.

	D3DClass* m_Direct3D;
};

#endif

Graphicsclass.cpp

If you'll remember from the previous tutorial this class was entirely empty with no code in it at all. Now that we have a D3DClass member we will start to fill out some code inside the GraphicsClass to initialize and shutdown the D3DClass object. We will also add calls to D3DClass::Render in the Render function so that we are now drawing to the window using Direct3D.

////////////////////////////////////////////////////////////////////////////////
// Filename: graphicsclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "graphicsclass.h"

So the very first change is in the class constructor. Here we initialize the pointer to null for safety reasons as we do with all class pointers.

GraphicsClass::GraphicsClass()
{
	m_Direct3D = 0;
}


GraphicsClass::GraphicsClass(const GraphicsClass& other)
{
}


GraphicsClass::~GraphicsClass()
{
}

The second change is in the Initialize function inside the GraphicsClass. Here we create the D3DClass object and then call the D3DClass Initialize function. We send this function the screen width, screen height, handle to the window, and the four global variables from the Graphicsclass.h file. The D3DClass will use all these variables to setup the Direct3D system. We'll go into more detail about that once we look at the d3dclass.cpp file.

bool GraphicsClass::Initialize(int screenHeight, int screenWidth, HWND hwnd)
{
	bool result;


	// Create the Direct3D object.
	m_Direct3D = new D3DClass;
	if(!m_Direct3D)
	{
		return false;
	}

	// Initialize the Direct3D object.
	result = m_Direct3D->Initialize(screenHeight, screenWidth, VSYNC_ENABLED, hwnd, FULL_SCREEN, SCREEN_DEPTH, SCREEN_NEAR);
	if(!result)
	{
		MessageBox(hwnd, L"Could not initialize Direct3D", L"Error", MB_OK);
		return false;
	}

	return true;
}

The next change is in the Shutdown function in the GraphicsClass. Shut down of all graphics objects occur here so we have placed the D3DClass shutdown in this function. Note that I check to see if the pointer was initialized or not. If it wasn't we can assume it was never set up and not try to shut it down. That is why it is important to set all the pointers to null in the class constructor. If it does find the pointer has been initialized then it will attempt to shut down the D3DClass and then clean up the pointer afterwards.

void GraphicsClass::Shutdown()
{
	// Release the Direct3D object.
	if(m_Direct3D)
	{
		m_Direct3D->Shutdown();
		delete m_Direct3D;
		m_Direct3D = 0;
	}

	return;
}

The Frame function has been updated so that it now calls the Render function each frame.

bool GraphicsClass::Frame()
{
	bool result;


	// Render the graphics scene.
	result = Render();
	if(!result)
	{
		return false;
	}

	return true;
}

The final change to this class is in the Render function. We call the m_Direct3D object to render the scene. For this tutorial all it's going to do is clear the screen to a grey color. But this is the first big step to getting DirectX 12 to work.

bool GraphicsClass::Render()
{
	bool result;


	// Use the Direct3D object to render the scene.
	result = m_Direct3D->Render();
	if(!result)
	{
		return false;
	}

	return true;
}

D3dclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: d3dclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _D3DCLASS_H_
#define _D3DCLASS_H_

First thing in the header is to specify the libraries to link when using this object module. The first library contains all the Direct3D functionality for setting up and drawing 3D graphics in DirectX 12. The second library contains tools to interface with the hardware on the computer to obtain information about the refresh rate of the monitor, the video card being used, and so forth. The third library contains functionality for compiling shaders which we will cover in the next tutorial.

/////////////
// LINKING //
/////////////
#pragma comment(lib, "d3d12.lib")
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3dcompiler.lib")

The next thing we do is include the headers for those libraries that we are linking to this object module.

//////////////
// INCLUDES //
//////////////
#include <d3d12.h>
#include <dxgi1_4.h>

The class definition for the D3DClass is kept as simple as possible here. It has the regular constructor, copy constructor, and destructor. Then more importantly it has the Initialize and Shutdown function. This will be what we are mainly focused on in this tutorial. Other than that I have a Render function which will draw the grey window to the screen.

////////////////////////////////////////////////////////////////////////////////
// Class name: D3DClass
////////////////////////////////////////////////////////////////////////////////
class D3DClass
{
public:
	D3DClass();
	D3DClass(const D3DClass&);
	~D3DClass();

	bool Initialize(int, int, HWND, bool, bool);
	void Shutdown();
	
	bool Render();

private:
	bool m_vsync_enabled;
	ID3D12Device* m_device;
	ID3D12CommandQueue* m_commandQueue;
	char m_videoCardDescription[128];
	IDXGISwapChain3* m_swapChain;
	ID3D12DescriptorHeap* m_renderTargetViewHeap;
	ID3D12Resource* m_backBufferRenderTarget[2];
	unsigned int m_bufferIndex;
	ID3D12CommandAllocator* m_commandAllocator;
	ID3D12GraphicsCommandList* m_commandList;
	ID3D12PipelineState* m_pipelineState;
	ID3D12Fence* m_fence;
	HANDLE m_fenceEvent;
	unsigned long long m_fenceValue;
};

#endif

D3dclass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: d3dclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "d3dclass.h"

So like most classes we begin with initializing all the member pointers to null in the class constructor. All pointers from the header file have all been accounted for here.

D3DClass::D3DClass()
{
	m_device = 0;
	m_commandQueue = 0;
	m_swapChain = 0;
	m_renderTargetViewHeap = 0;
	m_backBufferRenderTarget[0] = 0;
	m_backBufferRenderTarget[1] = 0;
	m_commandAllocator = 0;
	m_commandList = 0;
	m_pipelineState = 0;
	m_fence = 0;
	m_fenceEvent = 0;
}


D3DClass::D3DClass(const D3DClass& other)
{
}


D3DClass::~D3DClass()
{
}

The Initialize function is what does the setup for DirectX 12. I have placed all the code necessary in here as well as some extra stuff that will facilitate future tutorials.

The screenHeight and screenWidth variables that are given to this function are the height and width of the window we created in the SystemClass. Direct3D will use these to initialize and use the same window dimensions. The hwnd variable is a handle to the window. Direct3D will need this handle to access the window previously created. The fullscreen variable is whether we are running in windowed mode or fullscreen. Direct3D needs this as well for creating the window with the correct settings. The vsync variable indicates if we want Direct3D to render according to the users monitor refresh rate or to just go as fast as possible.

bool D3DClass::Initialize(int screenHeight, int screenWidth, HWND hwnd, bool vsync, bool fullscreen)
{
	D3D_FEATURE_LEVEL featureLevel;
	HRESULT result;
	D3D12_COMMAND_QUEUE_DESC commandQueueDesc;
	IDXGIFactory4* factory;
	IDXGIAdapter* adapter;
	IDXGIOutput* adapterOutput;
	unsigned int numModes, i, numerator, denominator, renderTargetViewDescriptorSize;
	unsigned long long stringLength;
	DXGI_MODE_DESC* displayModeList;
	DXGI_ADAPTER_DESC adapterDesc;
	int error;
	DXGI_SWAP_CHAIN_DESC swapChainDesc;
	IDXGISwapChain* swapChain;
	D3D12_DESCRIPTOR_HEAP_DESC renderTargetViewHeapDesc;
	D3D12_CPU_DESCRIPTOR_HANDLE renderTargetViewHandle;

	
	// Store the vsync setting.
	m_vsync_enabled = vsync;

The first thing we will be doing is creating the Direct3D device. This is our primary interface into DirectX.

To create the device it requires a parameter called the feature level. The feature level basically allows us to set what version of DirectX we will be using. As DirectX 12 is backwards compatible on a large number of video cards you are able to set the feature level according to the video card's capabilities. For example your video card may only have DirectX 10 capable hardware but the driver for your video card supports DirectX 12, therefore you could set the feature level to 10_0 and you can use DirectX 12 minus the features that aren't available on your video card.

	// Set the feature level to DirectX 12.1 to enable using all the DirectX 12 features.
	// Note: Not all cards support full DirectX 12, this feature level may need to be reduced on some cards to 12.0.
	featureLevel = D3D_FEATURE_LEVEL_12_1;

	// Create the Direct3D 12 device.
	result = D3D12CreateDevice(NULL, featureLevel, __uuidof(ID3D12Device), (void**)&m_device);
	if(FAILED(result))
	{
		MessageBox(hwnd, L"Could not create a DirectX 12.1 device.  The default video card does not support DirectX 12.1.", L"DirectX Device Failure", MB_OK);
		return false;
	}

Sometimes this call to create the device will fail if the primary video card is not compatible with DirectX 12. Some machines may have the primary card as a DirectX 11 video card and the secondary card as a DirectX 12 video card. Also some hybrid graphics cards work that way with the primary being the low power Intel card and the secondary being the high power Nvidia card. To get around this you will need to not use the default device and instead enumerate all the video cards in the machine and have the user choose which one to use and then specify that card when creating the device. Or if it fails you can use the adapter to get the next video card and attempt to create the device again. To set a video card other than the default one you need to send in the adapter for the non-default card as the first argument instead of using NULL.

After the device is created we then create the command queue. We use the command queue in DirectX 12 for executing our command lists. Basically each frame we put all our rendering into command lists and then pass those to the command queue to execute them on the GPU for us. You generally have one command queue per GPU. For this tutorial we set the node mask to 0 to specify just using a single GPU.

	// Initialize the description of the command queue.
	ZeroMemory(&commandQueueDesc, sizeof(commandQueueDesc));

	// Set up the description of the command queue.
	commandQueueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
	commandQueueDesc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
	commandQueueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
	commandQueueDesc.NodeMask = 0;

	// Create the command queue.
	result = m_device->CreateCommandQueue(&commandQueueDesc, __uuidof(ID3D12CommandQueue), (void**)&m_commandQueue);
	if(FAILED(result))
	{
		return false;
	}

Before we can initialize the swap chain we have to get the refresh rate from the video card/monitor. Each computer may be slightly different so we will need to query for that information. We query for the numerator and denominator values and then pass them to DirectX during the setup and it will calculate the proper refresh rate. If we don't do this and just set the refresh rate to a default value which may not exist on all computers then DirectX will respond by performing a buffer copy instead of a buffer flip which will degrade performance and give us annoying errors in the debug output.

	// Create a DirectX graphics interface factory.
	result = CreateDXGIFactory1(__uuidof(IDXGIFactory4), (void**)&factory);
	if(FAILED(result))
	{
		return false;
	}

	// Use the factory to create an adapter for the primary graphics interface (video card).
	result = factory->EnumAdapters(0, &adapter);
	if(FAILED(result))
	{
		return false;
	}

	// Enumerate the primary adapter output (monitor).
	result = adapter->EnumOutputs(0, &adapterOutput);
	if(FAILED(result))
	{
		return false;
	}

	// Get the number of modes that fit the DXGI_FORMAT_R8G8B8A8_UNORM display format for the adapter output (monitor).
	result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, NULL);
	if(FAILED(result))
	{
		return false;
	}

	// Create a list to hold all the possible display modes for this monitor/video card combination.
	displayModeList = new DXGI_MODE_DESC[numModes];
	if(!displayModeList)
	{
		return false;
	}

	// Now fill the display mode list structures.
	result = adapterOutput->GetDisplayModeList(DXGI_FORMAT_R8G8B8A8_UNORM, DXGI_ENUM_MODES_INTERLACED, &numModes, displayModeList);
	if(FAILED(result))
	{
		return false;
	}

	// Now go through all the display modes and find the one that matches the screen height and width.
	// When a match is found store the numerator and denominator of the refresh rate for that monitor.
	for(i=0; i<numModes; i++)
	{
		if(displayModeList[i].Height == (unsigned int)screenHeight)
		{
			if(displayModeList[i].Width == (unsigned int)screenWidth)
			{
				numerator = displayModeList[i].RefreshRate.Numerator;
				denominator = displayModeList[i].RefreshRate.Denominator;
			}
		}
	}

We now have the numerator and denominator for the refresh rate. The last thing we will retrieve using the adapter is the name of the video card and the video card memory.

	// Get the adapter (video card) description.
	result = adapter->GetDesc(&adapterDesc);
	if(FAILED(result))
	{
		return false;
	}

	// Store the dedicated video card memory in megabytes.
	m_videoCardMemory = (int)(adapterDesc.DedicatedVideoMemory / 1024 / 1024);

	// Convert the name of the video card to a character array and store it.
	error = wcstombs_s(&stringLength, m_videoCardDescription, 128, adapterDesc.Description, 128);
	if(error != 0)
	{
		return false;
	}

Now that we have stored the numerator and denominator for the refresh rate and the video card information we can release the structures and interfaces used to get that information. However we won't release the factory just yet as we need it to create the swap chain next.

	// Release the display mode list.
	delete [] displayModeList;
	displayModeList = 0;

	// Release the adapter output.
	adapterOutput->Release();
	adapterOutput = 0;

	// Release the adapter.
	adapter->Release();
	adapter = 0;

Now that we have the refresh rate from the system we can start creating the swap chain. The first thing we'll do is fill out the description of the swap chain. The swap chain is the two buffers to which the graphics will be drawn. Generally you use one of the back buffers, do all your drawing to it, and then swap it to the user's screen. And while that is being displayed you start drawing you next frame to the other back buffer. And you just keep swapping them each frame; that is why it is called a swap chain. Note you can have more than two buffers if you want, but for the tutorial we will stick to just using a double buffer system.

	// Initialize the swap chain description.
	ZeroMemory(&swapChainDesc, sizeof(swapChainDesc));

	// Set the swap chain to use double buffering.
	swapChainDesc.BufferCount = 2;

	// Set the height and width of the back buffers in the swap chain.
	swapChainDesc.BufferDesc.Height = screenHeight;
	swapChainDesc.BufferDesc.Width = screenWidth;

	// Set a regular 32-bit surface for the back buffers.
	swapChainDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;

	// Set the usage of the back buffers to be render target outputs.
	swapChainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;

	// Set the swap effect to discard the previous buffer contents after swapping.
	swapChainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;

	// Set the handle for the window to render to.
	swapChainDesc.OutputWindow = hwnd;

	// Set to full screen or windowed mode.
	if(fullscreen)
	{
		swapChainDesc.Windowed = false;
	}
	else
	{
		swapChainDesc.Windowed = true;
	}

The next part of the description of the swap chain is the refresh rate. The refresh rate is how many times a second it draws the back buffer to the screen. If vsync is set to true in our graphicsclass.h header then this will lock the refresh rate to the system settings (for example 60hz). That means it will only draw the screen 60 times a second (or higher if the system refresh rate is more than 60). However if we set vsync to false then it will draw the screen as many times a second as it can, but this can cause some visual artifacts.

	// Set the refresh rate of the back buffer.
	if(m_vsync_enabled)
	{
		swapChainDesc.BufferDesc.RefreshRate.Numerator = numerator;
		swapChainDesc.BufferDesc.RefreshRate.Denominator = denominator;
	}
	else
	{
		swapChainDesc.BufferDesc.RefreshRate.Numerator = 0;
		swapChainDesc.BufferDesc.RefreshRate.Denominator = 1;
	}

	// Turn multisampling off.
	swapChainDesc.SampleDesc.Count = 1;
	swapChainDesc.SampleDesc.Quality = 0;
	
	// Set the scan line ordering and scaling to unspecified.
	swapChainDesc.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
	swapChainDesc.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;

	// Don't set the advanced flags.
	swapChainDesc.Flags = 0;

Once the description is filled out we can create the swap chain next. And once the swap chain is created we then upgrade itts interface to a version 3 swap chain so we can access newer swap chain methods that aren't available in version 1.

Also note that when we create the swap chain we also send the command queue that will be associated with it. When we do that we are associating the swap chain and the back buffers to the GPU that the specified command queue is paired with. If you are rendering to multiple GPUs you can create swap chains and back buffers for each one to do more advanced things like alternate frame rendering.

	// Finally create the swap chain using the swap chain description.	
	result = factory->CreateSwapChain(m_commandQueue, &swapChainDesc, &swapChain);
	if(FAILED(result))
	{
		return false;
	}

	// Next upgrade the IDXGISwapChain to a IDXGISwapChain3 interface and store it in a private member variable named m_swapChain.
	// This will allow us to use the newer functionality such as getting the current back buffer index.
	result = swapChain->QueryInterface(__uuidof(IDXGISwapChain3), (void**)&m_swapChain);
	if(FAILED(result))
	{
		return false;
	}

	// Clear pointer to original swap chain interface since we are using version 3 instead (m_swapChain).
	swapChain = 0;

	// Release the factory now that the swap chain has been created.
	factory->Release();
	factory = 0;

Now that the swap chain is completely setup we can now setup the render target views for the two back buffers. Render target views allow the GPU to use the two back buffers as resources for rendering to. To create the views we first need to create a descriptor heap to hold the tow back buffer views in memory. Once we create the descriptor heap we can get a handle to the memory location in the heap and then create the view using the pointer to that memory location. This will be a common theme for all resource binding in DirectX 12.

	// Initialize the render target view heap description for the two back buffers.
	ZeroMemory(&renderTargetViewHeapDesc, sizeof(renderTargetViewHeapDesc));

	// Set the number of descriptors to two for our two back buffers.  Also set the heap tyupe to render target views.
	renderTargetViewHeapDesc.NumDescriptors = 2;
	renderTargetViewHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
	renderTargetViewHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;

	// Create the render target view heap for the back buffers.
	result = m_device->CreateDescriptorHeap(&renderTargetViewHeapDesc, __uuidof(ID3D12DescriptorHeap), (void**)&m_renderTargetViewHeap);
	if(FAILED(result))
	{
		return false;
	}

	// Get a handle to the starting memory location in the render target view heap to identify where the render target views will be located for the two back buffers.
	renderTargetViewHandle = m_renderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();

	// Get the size of the memory location for the render target view descriptors.
	renderTargetViewDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);

	// Get a pointer to the first back buffer from the swap chain.
	result = m_swapChain->GetBuffer(0, __uuidof(ID3D12Resource), (void**)&m_backBufferRenderTarget[0]);
	if(FAILED(result))
	{
		return false;
	}

	// Create a render target view for the first back buffer.
	m_device->CreateRenderTargetView(m_backBufferRenderTarget[0], NULL, renderTargetViewHandle);

	// Increment the view handle to the next descriptor location in the render target view heap.
	renderTargetViewHandle.ptr += renderTargetViewDescriptorSize;

	// Get a pointer to the second back buffer from the swap chain.
	result = m_swapChain->GetBuffer(1, __uuidof(ID3D12Resource), (void**)&m_backBufferRenderTarget[1]);
	if(FAILED(result))
	{
		return false;
	}

	// Create a render target view for the second back buffer.
	m_device->CreateRenderTargetView(m_backBufferRenderTarget[1], NULL, renderTargetViewHandle);

With the two render target views created for our two back buffers we will be able to use them for rendering. To start we need to get an index to which buffer is the current one to be drawing to.

	// Finally get the initial index to which buffer is the current back buffer.
	m_bufferIndex = m_swapChain->GetCurrentBackBufferIndex();

The next thing we create is the command allocator. The command allocator is going to be used for allocating memory for the list of commands that we send to the GPU each frame to render graphics.

	// Create a command allocator.
	result = m_device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, __uuidof(ID3D12CommandAllocator), (void**)&m_commandAllocator);
	if(FAILED(result))
	{
		return false;
	}

The next thing we are going to do is create a command list. Command lists are one of key the components to understand in DirectX 12. Basically you fill the command list with all your rendering commands each frame and then send it into the command queue to execute the command list. And when you get more advanced you will create multiple command lists and execute them in parallel to get more efficiency in rendering. However that is where it gets tricky as you need to manage resources like you would in any multi-threaded program and ensure the execution order and dependencies between threads is safely handled. But for simplicity reasons in this tutorial I will just create a single one here for the time being. In future tutorials this will be removed from the D3DClass as it belongs elsewhere.

	// Create a basic command list.
	result = m_device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, m_commandAllocator, NULL, __uuidof(ID3D12GraphicsCommandList), (void**)&m_commandList);
	if(FAILED(result))
	{
		return false;
	}

	// Initially we need to close the command list during initialization as it is created in a recording state.
	result = m_commandList->Close();
	if(FAILED(result))
	{
		return false;
	}

The final thing we are going to create is the fence. We use the fence as a signaling mechanism to notify us when the GPU is completely done rendering the command list that we submitted via the command queue. GPU and CPU synchronization is completely up to us to handle in DirectX 12, so fences become a very necessary tool.

	// Create a fence for GPU synchronization.
	result = m_device->CreateFence(0, D3D12_FENCE_FLAG_NONE, __uuidof(ID3D12Fence), (void**)&m_fence);
	if(FAILED(result))
	{
		return false;
	}
	
	// Create an event object for the fence.
	m_fenceEvent = CreateEventEx(NULL, FALSE, FALSE, EVENT_ALL_ACCESS);
	if(m_fenceEvent == NULL)
	{
		return false;
	}

	// Initialize the starting fence value. 
	m_fenceValue = 1;
	
	return true;
}

The Shutdown function will release and clean up all the pointers used in the Initialize function, it's pretty straight forward. However before doing that I put in a call to force the swap chain to go into windowed mode first before releasing any pointers. If this is not done and you try to release the swap chain in full screen mode it will throw some exceptions. So to avoid that happening we just always force windowed mode before shutting down Direct3D.

void D3DClass::Shutdown()
{
	int error;


	// Before shutting down set to windowed mode or when you release the swap chain it will throw an exception.
	if(m_swapChain)
	{
		m_swapChain->SetFullscreenState(false, NULL);
	}
		
	// Close the object handle to the fence event.
	error = CloseHandle(m_fenceEvent);
	if(error == 0)
	{
	}
		
	// Release the fence.
	if(m_fence)
	{
		m_fence->Release();
		m_fence = 0;
	}

	// Release the empty pipe line state.
	if(m_pipelineState)
	{
		m_pipelineState->Release();
		m_pipelineState = 0;
	}

	// Release the command list.
	if(m_commandList)
	{
		m_commandList->Release();
		m_commandList = 0;
	}

	// Release the command allocator.
	if(m_commandAllocator)
	{
		m_commandAllocator->Release();
		m_commandAllocator = 0;
	}

	// Release the back buffer render target views.
	if(m_backBufferRenderTarget[0])
	{
		m_backBufferRenderTarget[0]->Release();
		m_backBufferRenderTarget[0] = 0;
	}
	if(m_backBufferRenderTarget[1])
	{
		m_backBufferRenderTarget[1]->Release();
		m_backBufferRenderTarget[1] = 0;
	}
	
	// Release the render target view heap.
	if(m_renderTargetViewHeap)
	{
		m_renderTargetViewHeap->Release();
		m_renderTargetViewHeap = 0;
	}

	// Release the swap chain.
	if(m_swapChain)
	{
		m_swapChain->Release();
		m_swapChain = 0;
	}

	// Release the command queue.
	if(m_commandQueue)
	{
		m_commandQueue->Release();
		m_commandQueue = 0;
	}
	
	// Release the device.
	if(m_device)
	{
		m_device->Release();
		m_device = 0;
	}

	return;
}

The D3DClass::Render function for this tutorial is just going to clear the screen to grey. It is very simple to teach you the absolute minimums for rendering graphics.

bool D3DClass::Render()
{
	HRESULT result;
	D3D12_RESOURCE_BARRIER barrier;
	D3D12_CPU_DESCRIPTOR_HANDLE renderTargetViewHandle;
	unsigned int renderTargetViewDescriptorSize;
	float color[4];
	ID3D12CommandList* ppCommandLists[1];
	unsigned long long fenceToWaitFor;

The first step in rendering is that we reset both the command allocator and command list memory. You will notice here we use a pipeline that is currently NULL. This is because pipelines require shaders and extra setup that we will not be covering until the next tutorial.

	// Reset (re-use) the memory associated command allocator.
	result = m_commandAllocator->Reset();
	if(FAILED(result))
	{
		return false;
	}

	// Reset the command list, use empty pipeline state for now since there are no shaders and we are just clearing the screen.
	result = m_commandList->Reset(m_commandAllocator, m_pipelineState);
	if(FAILED(result))
	{
		return false;
	}

The second step is to use a resource barrier to synchronize/transition the next back buffer for rendering. We then set that as a step in the command list.

	// Record commands in the command list now.
	// Start by setting the resource barrier.
	barrier.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
	barrier.Transition.pResource = m_backBufferRenderTarget[m_bufferIndex];
	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_PRESENT;
	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET;
	barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
	barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
	m_commandList->ResourceBarrier(1, &barrier);

The third step is to get the back buffer view handle and then set the back buffer as the render target.

	// Get the render target view handle for the current back buffer.
	renderTargetViewHandle = m_renderTargetViewHeap->GetCPUDescriptorHandleForHeapStart();
	renderTargetViewDescriptorSize = m_device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
	if(m_bufferIndex == 1)
	{
		renderTargetViewHandle.ptr += renderTargetViewDescriptorSize;
	}

	// Set the back buffer as the render target.
	m_commandList->OMSetRenderTargets(1, &renderTargetViewHandle, FALSE, NULL);

In the fourth step we set the clear color to grey and clear the render target using that color and submit that to the command list.

	// Then set the color to clear the window to.
	color[0] = 0.5;
	color[1] = 0.5;
	color[2] = 0.5;
	color[3] = 1.0;
	m_commandList->ClearRenderTargetView(renderTargetViewHandle, color, 0, NULL);

And finally we then set the state of the back buffer to transition into a presenting state and store that in the command list.

	// Indicate that the back buffer will now be used to present.
	barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET;
	barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT;
	m_commandList->ResourceBarrier(1, &barrier);

Once we are done our rendering list we close the command list and then submit it to the command queue to execute that list for us.

	// Close the list of commands.
	result = m_commandList->Close();
	if(FAILED(result))
	{
		return false;
	}

	// Load the command list array (only one command list for now).
	ppCommandLists[0] = m_commandList;

	// Execute the list of commands.
	m_commandQueue->ExecuteCommandLists(1, ppCommandLists);

We then call the swap chain to present the rendered frame to the screen.

	// Finally present the back buffer to the screen since rendering is complete.
	if(m_vsync_enabled)
	{
		// Lock to screen refresh rate.
		result = m_swapChain->Present(1, 0);
		if (FAILED(result))
		{
			return false;
		}
	}
	else
	{
		// Present as fast as possible.
		result = m_swapChain->Present(0, 0);
		if (FAILED(result))
		{
			return false;
		}
	}

Then we setup the fence to synchronize and let us know when the GPU is complete rendering. For this tutorial we just wait infinitely until it's done this single command list. However you can get optimiziations by doing other processing while waiting for the GPU to finish.

	// Signal and increment the fence value.
	fenceToWaitFor = m_fenceValue;
	result = m_commandQueue->Signal(m_fence, fenceToWaitFor);
	if(FAILED(result))
	{
		return false;
	}
	m_fenceValue++;

	// Wait until the GPU is done rendering.
	if(m_fence->GetCompletedValue() < fenceToWaitFor)
	{
		result = m_fence->SetEventOnCompletion(fenceToWaitFor, m_fenceEvent);
		if(FAILED(result))
		{
			return false;
		}
		WaitForSingleObject(m_fenceEvent, INFINITE);
	}

For the next frame swap to the other back buffer using the alternating index.

	// Alternate the back buffer index back and forth between 0 and 1 each frame.
	m_bufferIndex == 0 ? m_bufferIndex = 1 : m_bufferIndex = 0;

	return true;
}

Summary

So now we are finally able to initialize and shut down Direct3D. Compiling and running the code will produce the same window as the last tutorial but Direct3D is initialized now and it clears the window to a grey color. Compiling and running the code will also show if your compiler is set up properly and if it can see the headers and libraries files from the Windows SDK.


To Do Exercises

1. Re-compile the code and run the program to ensure DirectX works, if not look at the steps from the first tutorial. Press the escape key to quit after the window displays.

2. Change the global in graphicsclass.h to full screen and re-compile/run.

3. Change the clear color in GraphicsClass::Render to yellow.

4. Write the video card name out to a text file.


Source Code

Source Code and Data Files: dx12tut03_src.zip

Executable: dx12tut03_exe.zip

Back to Tutorial Index