Direct3D 12 Initialization

In this post, we will describe how to initialize DirectX 12.

The steps that you need to follow are:

– Create the device
– Query descriptor sizes
– Check MSAA quality level support
– Create command queue
– Create swap chain
– Create descriptor heaps
– Resize the back buffer and create a render target view to the back buffer
– Create the depth/stencil buffer and its depth/stencil view
– Set viewport and scissor rectangles.

Assumptions

In our case, we use Windows 10 (obviously) and Visual Studio 2015 Community Edition. This code assumes you have a graphics card that supports DirectX 12 (GTX 680 in our case) and we also use the following code for error checking purposes.

#if defined(DEBUG) || defined(_DEBUG)
#define ASSERT(condition) \
assert(condition);
#else
#define ASSERT(condition) (condition)
#endif

#ifndef CHECK_HR
#define CHECK_HR(x) \
{ \
const HRESULT hr__ = (x); \
if (FAILED(hr__)) { \
const std::wstring wfn = StringUtils::AnsiToWString(__FILE__); \
_com_error err(hr__); \
const std::wstring msg = err.ErrorMessage(); \
const std::wstring outputMsg = L"Failed in " + wfn + L" line " + std::to_wstring(__LINE__) + L" error: " + msg; \
MessageBox(0, outputMsg.c_str(), 0, 0); \
abort(); \
} \
}
#endif

Create the device

The device represents a display adapter. It could be the graphics card or a software display adapter that emulates 3D hardware functionality (WARP).
Its Direct3D structure is ID3D12Device and we use D3D12CreateDevice() function.

#if defined(DEBUG) || defined(_DEBUG)
    // Enable the D3D12 debug layer.
    {
	Microsoft::WRL::ComPtr<ID3D12Debug> debugController;
	CHECK_HR(D3D12GetDebugInterface(IID_PPV_ARGS(debugController.GetAddressOf())));
	debugController->EnableDebugLayer();
    }
#endif

    // Create device
    // mD3dDevice is a ComPtr<ID3D12Device>
    // mDxgiFactory is a ComPtr<IDXGIFactory4>
    CHECK_HR(CreateDXGIFactory1(IID_PPV_ARGS(mDxgiFactory.GetAddressOf())));
    CHECK_HR(D3D12CreateDevice(nullptr, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(mD3dDevice.GetAddressOf())));

As you can see, we enabled debug layer for debug mode builds only. Direct3D will enable extra debugging and send debug messages to the VC++ output window.

Query descriptor sizes

Shader resources (textures, buffers, etc.) are not bound directly to the shader pipeline but through a descriptor.
A descriptor is a small object that contains information about one resource (usage, subrange, shader to bind, etc.).
Descriptors are grouped together to form descriptor tables (descriptor table start + descriptor table size).
Descriptor tables are stored in a descriptor heap. Descriptor tables are not a memory allocation but an offset and length into a descriptor heap.

IC771755

As descriptor size varies by hardware, then we need to query and store them. We use ID3D12Device::GetDescriptorHandleIncrementSize() function for this. This value is typically used to increment a handle into a descriptor array by the correct amount.

// These member variables are uint32_t
mRtvDescSize = mD3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
mDsvDescSize = mD3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
mCbvSrvUavDescSize = mD3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV);
mSamplerDescSize = mD3dDevice->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER);

Check MSAA quality level support

Direct3D supports an antialiasing technique called multisampling, which shares some computational information across subpixels making it less expensive (but less accurate) than techniques like supersampling.
For example, if we are using 4X multisampling (4 subpixels per pixel), multisampling uses a back buffer and depth buffer 4X bigger than the screen resolution and it computes image color once per pixel, at the pixel center and shares that color information with its subpixels based on visibility (the depth/stencil test is evaluated per subpixel) and coverage (does the subpixel center lie inside or outside the polygon?). We use ID3D12Device::CheckFeatureSupport() function.

// Check 4X MSAA quality support for our back buffer format.
D3D12_FEATURE_DATA_MULTISAMPLE_QUALITY_LEVELS msQualityLevels = {};
msQualityLevels.Format = mBackBufferFormat;
msQualityLevels.SampleCount = 4U;
msQualityLevels.Flags = D3D12_MULTISAMPLE_QUALITY_LEVELS_FLAG_NONE;
msQualityLevels.NumQualityLevels = 0U;
CHECK_HR(mD3dDevice->CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS, &msQualityLevels, sizeof(msQualityLevels)));
m4xMsaaQuality = msQualityLevels.NumQualityLevels;
ASSERT(m4xMsaaQuality > 0U &&"Unexpected MSAA quality level");

As you can see, we used 4X MSAA because all DirectX11 capable devices support it with all render target formats. Then the returned quality should always be greater than zero.

Create command queue

In D3D12 the concept of a command queue is the API representation of a rough sequence of work submitted by the application. Barriers and other techniques allow this work to be executed in a pipeline or out of order, but the application only sees a single completion timeline.
The D3D12 device has methods to create and retrieve command queues of different types and priorities. Most applications should use the default command queues because these allow for shared usage by other components. Applications with additional concurrency requirements can create additional queues. Queues are specified by the command list type that they consume.
In Direct3D 12, the command queue is represented by the ID3D12CommandQueue and you can use ID3D12Device::CreateCommandQueue() function to create it.

// mCmdQueue is a ComPtr<ID3D12CommandQueue>
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
CHECK_HR(mD3dDevice->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(mCmdQueue.GetAddressOf())));

The CPU submits commands to the queue through the Direct3D API using command lists. It is important to understand that once a set of commands have been submitted to the command queue, they are not immediately executed by the GPU. They sit in the queue until the GPU is ready to process them, as the GPU is likely busy processing previously inserted commands.
If the command queue gets empty, the GPU will idle because it does not have any work to do; on the other hand, if the command queue gets too full, the CPU will at some point have to idle while the GPU catches up. Both of these situations are undesirable; for high-performance applications like games, the goal is to keep both CPU and GPU busy to take full advantage of the hardware resources available.

A command list for graphics is represented by the ID3D12GraphicsCommandList which inherits from the ID3D12CommandList interface.
Associated with a command list is a memory backing class called an ID3D12CommandAllocator. As commands are recorded in the command list, they will actually be stored in the associated command allocator. When a command list is executed via ID3D12CommandQueue::ExecuteCommandLists(), the command queue will reference the commands in the allocator. A command allocator is created using ID3D12Device::CreateCommandAllocator() function.

// mCmdListAlloc is a ComPtr<ID3D12CommandAllocator>
CHECK_HR(mD3dDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(mCmdListAlloc.GetAddressOf())));

Command lists are created using ID3D12Device::CreateCommandList() function.

// mCmdList is a ComPtr<ID3D12CommandList>
CHECK_HR(mD3dDevice->CreateCommandList(0U, D3D12_COMMAND_LIST_TYPE_DIRECT, mCmdListAlloc.Get(), nullptr, IID_PPV_ARGS(mCmdList.GetAddressOf())));

Create swap chain

To avoid flickering in animation, it is best to draw an entire frame of animation into an off-screen texture called the back buffer. Once the entire scene has been drawn to the back buffer for the given frame of animation, it is presented to the screen as one full frame; in this way, the viewer does not watch as the frame gets drawn (the viewer only sees complete frames). To implement this, two texture buffers are maintained by the hardware, one called the front buffer and a second called the back buffer. The front buffer stores the image data currently being displayed on the monitor, while the next frame of animation is being drawn to the back buffer. After the frame has been drawn to the back buffer, the roles of the back buffer and front buffer are reversed: the back buffer becomes the front buffer, and the front buffer becomes the back buffer for the next frame of animation. Swapping the roles of the back and front buffers is called presenting. Presenting is an efficient operation, as the pointer to the current front buffer and the pointer to the current back buffer just need to be swapped. The front and back buffers compose a swap chain. In Direct3D, a swap chain is represented by the IDXGISwapChain interface. This interface stores the front and back buffer textures.
Using two buffers is called double buffering. You can use IDXGIFactory::CreateSwapChain() function.

DXGI_SWAP_CHAIN_DESC sd = {};
sd.BufferDesc.Width = mClientWidth;
sd.BufferDesc.Height = mClientHeight;
sd.BufferDesc.RefreshRate.Numerator = 60U;
sd.BufferDesc.RefreshRate.Denominator = 1U;
sd.BufferDesc.Format = mBackBufferFormat;
sd.BufferDesc.ScanlineOrdering = DXGI_MODE_SCANLINE_ORDER_UNSPECIFIED;
sd.BufferDesc.Scaling = DXGI_MODE_SCALING_UNSPECIFIED;
sd.SampleDesc.Count = 1U;
sd.SampleDesc.Quality = 0U;
sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
sd.BufferCount = SWAP_CHAIN_BUFFER_COUNT;
sd.OutputWindow = mMainWnd;
sd.Windowed = true;
sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
sd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

// Note: Swap chain uses queue to perform flush.
// mSwapChain is a ComPtr<IDXGISwapChain>
CHECK_HR(mDxgiFactory->CreateSwapChain(mCmdQueue.Get(), sd, mSwapChain.GetAddressOf()));

Create descriptor heaps

We need to create the descriptor heaps to store the descriptors our app needs. A descriptor heap is represented by the ID3D12DescriptorHeap interface. A heap is created with the ID3D12Device::CreateDescriptorHeap() function. As we are only initializing Direct3D in this post, we only need a heap for storing swap chain buffers (render target views) and another for storing one depth stencil view. You can use the following code:

// mRtvHeap is a ComPtr<ID3D12DescriptorHeap>
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = SWAP_CHAIN_BUFFER_COUNT;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
rtvHeapDesc.NodeMask = 0;
CHECK_HR(mD3dDevice->CreateDescriptorHeap(&rtvHeapDesc, IID_PPV_ARGS(mRtvHeap.GetAddressOf())));

// mDsvHeap is a ComPt&amp;lt;ID3D12DescriptorHeap&amp;gt;;
D3D12_DESCRIPTOR_HEAP_DESC dsvHeapDesc = {};
dsvHeapDesc.NumDescriptors = 1;
dsvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_DSV;
dsvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
dsvHeapDesc.NodeMask = 0;
CHECK_HR(mD3dDevice->CreateDescriptorHeap(&dsvHeapDesc, IID_PPV_ARGS(mDsvHeap.GetAddressOf())));

Resize the back buffer and create a render target view to the back buffer

We do not bind a resource to a pipeline stage directly. Instead, we need to create a descriptor to the resource and bind the descriptor to the pipeline stage. In particular, to bind the back buffer to the output merger stage of the pipeline (so Direct3D can render on it), we need to create a render target view to the back buffer.
First, we need to get the buffer resources which are stored in the swap chain, through IDXGISwapChain::GetBuffer() function. Second, to create the render target view, we use the ID3D12Device::CreateRenderTargetView function.

// mBackBufferFormat is a DXGI_FORMAT
CHECK_HR(mSwapChain->ResizeBuffers(SWAP_CHAIN_BUFFER_COUNT, mClientWidth, mClientHeight, mBackBufferFormat, DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH));

// mSwapChainBuffer is an array of SWAP_CHAIN_BUFFER_COUNT ComPtr<ID3D12Resource>
CD3DX12_CPU_DESCRIPTOR_HANDLE rtvHeapHandle(mRtvHeap->GetCPUDescriptorHandleForHeapStart());

for (uint32_t i = 0U; i < SWAP_CHAIN_BUFFER_COUNT; ++i) {
    CHECK_HR(mSwapChain->GetBuffer(i, IID_PPV_ARGS(mSwapChainBuffer[i].GetAddressOf())));
    mD3dDevice->CreateRenderTargetView(mSwapChainBuffer[i].Get(), nullptr, rtvHeapHandle);
    rtvHeapHandle.Offset(1U, mRtvDescSize);
}

Create the depth/stencil buffer and its depth/stencil view

The depth buffer is an example of a texture that does not contain image data, but rather depth information about a particular pixel. The possible depth values range from 0.0 to 1.0, where 0.0 denotes the closest an object in the view frustum can be to the viewer and 1.0 denotes the farthest an object in the view frustum can be from the viewer.
There is a one-to-one correspondence between each element in the depth buffer and each pixel in the back buffer. So if the back buffer had a resolution of 800×600, there could be 800×600 depth entries.
Depth buffering works by computing a depth value of each pixel and performing a depth test. The depth test compares the depths of pixels competing to be written to a particular pixel location on the back buffer. The pixel with the depth value closest to the viewer wins, and that is the pixel that gets written to the back buffer. This makes sense because the pixel closest to the viewer obscures the pixels behind it.
As depth/stencil buffer is just a 2D texture, that is a GPU resource, we need to create it by filling out a D3D12_RESOURCE_DESC structure describing it and then calling ID3D12Device::CreateCommitedResource() method. GPU resources live in heaps, which are essentially blocks of GPU memory with certain properties. The ID3D12Device::CreateCommittedResource() function creates and commits a resource to a particular heap with the properties we specify.

// Create the depth/stencil buffer and view.
D3D12_RESOURCE_DESC depthStencilDesc;
depthStencilDesc.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D;
depthStencilDesc.Alignment = 0U;
depthStencilDesc.Width = mClientWidth;
depthStencilDesc.Height = mClientHeight;
depthStencilDesc.DepthOrArraySize = 1U;
depthStencilDesc.MipLevels = 1U;
depthStencilDesc.Format = mDepthStencilFormat;
depthStencilDesc.SampleDesc.Count = U;
depthStencilDesc.SampleDesc.Quality = 0U;
depthStencilDesc.Layout = D3D12_TEXTURE_LAYOUT_UNKNOWN;
depthStencilDesc.Flags = D3D12_RESOURCE_FLAG_ALLOW_DEPTH_STENCIL;

// mDepthStencilBuffer is a ComPtr<ID3D12Resource>
D3D12_CLEAR_VALUE optClear;
optClear.Format = mDepthStencilFormat;
optClear.DepthStencil.Depth = 1.0f;
optClear.DepthStencil.Stencil = 0U;
CD3DX12_HEAP_PROPERTIES heapProps(D3D12_HEAP_TYPE_DEFAULT);
CHECK_HR(mD3dDevice->CreateCommittedResource(
		&heapProps,
		D3D12_HEAP_FLAG_NONE,
		&depthStencilDesc,
		D3D12_RESOURCE_STATE_COMMON,
		&optClear,
		IID_PPV_ARGS(mDepthStencilBuffer.GetAddressOf())));

mD3dDevice->CreateDepthStencilView(mDepthStencilBuffer.Get(), nullptr, mDsvHeap->GetCPUDescriptorHandleForHeapStart());

Set viewport and scissor rectangles

Usually, we like to draw the 3D scene to the entire back buffer, where the back buffer size corresponds to the entire screen (full-screen mode) or the entire client area of a window. However, sometimes we only want to draw the 3D scene into a subrectangle of the back buffer. This subrectangle we draw into is called the viewport, and it is described by the D3D12_VIEWPORT structure.

We can define a scissor rectangle about the back buffer such that pixels outside this rectangle are culled (not rasterized to the back buffer). A scissor rectangle is defined by a D3D12_RECT.

// mScreenViewport is a D3D12_VIEWPORT
// mScissorRect is a D3D12_RECT
mScreenViewport.TopLeftX = 0.0f;
mScreenViewport.TopLeftY = 0.0f;
mScreenViewport.Width = (float)mClientWidth;
mScreenViewport.Height = (float)mClientHeight;
mScreenViewport.MinDepth = 0.0f;
mScreenViewport.MaxDepth = 1.0f;

mScissorRect = { 0, 0, mClientWidth, mClientHeight };

This concludes the steps that we need to basically initialize Direct3D 12. If you get any problems, you can check MSDN documentation and also VC++ output window for error messages.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s