In order to use DirectX 12 you will need to create certain data structures that correspond to parts of the GPU, let's review each:
Factory
Factories are the entry point to the DirectX 12 API. They can be used to find adapters which can then create devices and other important data structures.
You can also create a Debug Controller alongside the factory which can enable API usage validation, though this should only be used in `debug builds.
- Reference: IDXGIFactory7
// 👋 Declare DirectX 12 Handles
IDXGIFactory4* factory;
ID3D12Debug1* debugController;
// 🏭 Create Factory
UINT dxgiFactoryFlags = 0;
#if defined(_DEBUG)
// 🐛 Create a Debug Controller to track errors
ID3D12Debug* dc;
ThrowIfFailed(D3D12GetDebugInterface(IID_PPV_ARGS(&dc)));
ThrowIfFailed(dc->QueryInterface(IID_PPV_ARGS(&debugController)));
debugController->EnableDebugLayer();
debugController->SetEnableGPUBasedValidation(true);
dxgiFactoryFlags |= DXGI_CREATE_FACTORY_DEBUG;
dc->Release();
dc = nullptr;
#endif
HRESULT result = CreateDXGIFactory2(dxgiFactoryFlags, IID_PPV_ARGS(&factory));Adapter
An Adapter provides information on the physical properties of a given DirectX device. You can query your current GPU's name, manufacturer, how much memory it has, and much more.
There's 2 categories for adapters, software and hardware adapters. Microsoft Windows always includes a software based DirectX implementation that can be used in the event of there being no dedicated hardware such as a discrete or integrated GPU.
- Reference: IDXGIAdapter4
// 👋 Declare Handles
IDXGIAdapter1* adapter;
// 🔌 Create Adapter
for (UINT adapterIndex = 0;
DXGI_ERROR_NOT_FOUND != factory->EnumAdapters1(adapterIndex, &adapter);
++adapterIndex)
{
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);
// ❌ Don't select the Basic Render Driver adapter.
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
continue;
}
// ✔️ Check if the adapter supports Direct3D 12, and use that for the rest
// of the application
if (SUCCEEDED(D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_0,
_uuidof(ID3D12Device), nullptr)))
{
break;
}
// ❌ Else we won't use this iteration's adapter, so release it
adapter->Release();
}Device
A Device allows you to create key data structures such as command queues, allocators, resources like pipelines, buffers, buffer views, shader blobs, heaps, and synchronization primitives.
- Reference: IDXGIDevice4
// 👋 Declare Handles
ID3D12Device* device;
// 💻 Create Device
ID3D12Device* pDev = nullptr;
ThrowIfFailed(D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_0,
IID_PPV_ARGS(&device)));A Debug Device allows you to rely on DirectX 12's debug mode. It can be difficult to keep track of data structures created with DirectX. With this you'll be able to prevent data leaks or verify if you're using the API correctly.
- Reference: ID3D12DebugDevice
// 👋 Declare Handles
ID3D12DebugDevice* debugDevice;
#if defined(_DEBUG)
// 💻 Get debug device
ThrowIfFailed(device->QueryInterface(&debugDevice));
#endifCommand Queue
A Command Queue allows you to submit groups of commands, known as command lists, together to execute in order, thus allowing a GPU to stay busy and optimize its execution speed. These commands can be to specify a buffer needs a barrier, to wait for an upload to the GPU to finish, to execute the raster or compute pipeline, and much more.
- Reference: ID3D12CommandQueue
// 👋 Declare Handles
ID3D12CommandQueue* commandQueue;
// 📦 Create Command Queue
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
ThrowIfFailed(device->CreateCommandQueue(&queueDesc,
IID_PPV_ARGS(&commandQueue)));Command Allocator
A Command Allocator allows you to create command lists where you can define the functions you want the GPU to execute for that allocator.
- Reference: ID3D12CommandAllocator
// 👋 Declare Handles
ID3D12CommandAllocator* commandAllocator;
// 🎅 Create Command Allocator
ThrowIfFailed(device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(&commandAllocator)));Synchronization
DirectX 12 features a number of synchronization primitives that can help the driver know how resources will be used in the future, know when tasks have been completed by the GPU, etc.
A Fence lets your program know when certain tasks have been executed by the GPU, be it uploads to GPU exclusive memory, or when you've finished presenting to the screen.
- Reference: ID3D12Fence1
// 👋 Declare handles
UINT frameIndex;
HANDLE fenceEvent;
ID3D12Fence* fence;
UINT64 fenceValue;
// 🚧 Create fence
ThrowIfFailed(device->CreateFence(0, D3D12_FENCE_FLAG_NONE,
IID_PPV_ARGS(&fence)));A Barrier lets the driver know how a resource should be used in upcoming commands. This can be useful if say, you're writing to a texture, and you want to copy that texture to another texture (such as the swapchain's render attachment).
// 👋 Declare handles
ID3D12GraphicsCommandList* commandList;
// 🔮 Create Barrier
D3D12_RESOURCE_BARRIER barrier = {};
result.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION;
result.Flags = D3D12_RESOURCE_BARRIER_FLAG_NONE;
barrier.Transition.pResource = texResource;
barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE;
barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_UNORDERED_ACCESS;
barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES;
commandList->ResourceBarrier(1, &barrier);Swapchain
Swapchains handle swapping and allocating back buffers to display what you're rendering to a given window.
- Reference: IDXGISwapChain4
// 💾 Declare Data
unsigned width = 640;
unsigned height = 640;
// 👋 Declare Handles
static const UINT backbufferCount = 2;
UINT currentBuffer;
ID3D12DescriptorHeap* renderTargetViewHeap;
ID3D12Resource* renderTargets[backbufferCount];
UINT rtvDescriptorSize;
// ⛓️ Swapchain
IDXGISwapChain3* swapchain;
D3D12_VIEWPORT viewport;
D3D12_RECT surfaceSize;
surfaceSize.left = 0;
surfaceSize.top = 0;
surfaceSize.right = static_cast<LONG>(width);
surfaceSize.bottom = static_cast<LONG>(height);
viewport.TopLeftX = 0.0f;
viewport.TopLeftY = 0.0f;
viewport.Width = static_cast<float>(width);
viewport.Height = static_cast<float>(height);
viewport.MinDepth = .1f;
viewport.MaxDepth = 1000.f;
if (swapchain != nullptr)
{
// Create Render Target Attachments from swapchain
swapchain->ResizeBuffers(backbufferCount, width, height,
DXGI_FORMAT_R8G8B8A8_UNORM, 0);
}
else
{
// ⛓️ Create swapchain
DXGI_SWAP_CHAIN_DESC1 swapchainDesc = {};
swapchainDesc.BufferCount = backbufferCount;
swapchainDesc.Width = width;
swapchainDesc.Height = height;
swapchainDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
swapchainDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapchainDesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
swapchainDesc.SampleDesc.Count = 1;
IDXGISwapChain1* newSwapchain =
xgfx::createSwapchain(window, factory, commandQueue, &swapchainDesc);
HRESULT swapchainSupport = swapchain->QueryInterface(
__uuidof(IDXGISwapChain3), (void**)&newSwapchain);
if (SUCCEEDED(swapchainSupport))
{
swapchain = (IDXGISwapChain3*)newSwapchain;
}
}
frameIndex = swapchain->GetCurrentBackBufferIndex();
// Describe and create a render target view (RTV) descriptor heap.
D3D12_DESCRIPTOR_HEAP_DESC rtvHeapDesc = {};
rtvHeapDesc.NumDescriptors = backbufferCount;
rtvHeapDesc.Type = D3D12_DESCRIPTOR_HEAP_TYPE_RTV;
rtvHeapDesc.Flags = D3D12_DESCRIPTOR_HEAP_FLAG_NONE;
ThrowIfFailed(device->CreateDescriptorHeap(
&rtvHeapDesc, IID_PPV_ARGS(&renderTargetViewHeap)));
rtvDescriptorSize =
device->GetDescriptorHandleIncrementSize(D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
// 🎞️ Create frame resources
D3D12_CPU_DESCRIPTOR_HANDLE
rtvHandle(renderTargetViewHeap->GetCPUDescriptorHandleForHeapStart());
// Create a RTV for each frame.
for (UINT n = 0; n < backbufferCount; n++)
{
ThrowIfFailed(swapchain->GetBuffer(n, IID_PPV_ARGS(&renderTargets[n])));
device->CreateRenderTargetView(renderTargets[n], nullptr, rtvHandle);
rtvHandle.ptr += (1 * rtvDescriptorSize);
}
Alain Galvan