DirectX 12 Guide — Modern Graphics Programming on Windows
In this tutorial, you'll learn about DirectX 12 Guide. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
DirectX 12 is Microsoft's low-level graphics API that provides explicit control over GPU resources, command submission, and pipeline state, enabling console-grade performance optimization for Windows and Xbox platforms.
What You'll Learn & Why It Matters
In this tutorial, you will learn DirectX 12 programming from initialization to rendering. You will create a D3D12 device, manage command allocators and lists, set up root signatures and pipeline state objects, manage descriptor heaps, and render geometry.
Real-world use: AAA games on Windows and Xbox use DirectX 12 for maximum hardware utilization. Game Development studios targeting Xbox Series X|S rely on DirectX 12's DirectStorage and DirectML extensions for fast asset streaming and Machine Learning inference.
Prerequisites
- GPU Architecture (previous)
- C++17 or newer
- Windows development experience
Learning Path
flowchart LR A[GPU Architecture] --> B[DirectX 12 Guide] B --> C[Vulkan Guide] B --> D[Compute Shaders] B --> E[Ray Tracing Basics] C --> F[Real-Time GI] B:::current classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
Device Initialization
DirectX 12 requires creating a device, command queue, and swapchain before any rendering:
#include <d3d12.h>
#include <dxgi1_6.h>
#include <wrl.h>
#include <iostream>
using Microsoft::WRL::ComPtr;
class D3D12Renderer {
public:
ComPtr<ID3D12Device> device;
ComPtr<ID3D12CommandQueue> commandQueue;
ComPtr<IDXGISwapChain3> swapChain;
ComPtr<ID3D12CommandAllocator> commandAllocator;
ComPtr<ID3D12GraphicsCommandList> commandList;
static const int FrameCount = 3;
UINT frameIndex = 0;
void initDevice(HWND hwnd, int width, int height) {
// Enable debug layer in debug builds
#ifdef _DEBUG
ComPtr<ID3D12Debug> debugController;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debugController))))
debugController->EnableDebugLayer();
#endif
ComPtr<IDXGIFactory7> factory;
CreateDXGIFactory1(IID_PPV_ARGS(&factory));
// Find adapter with highest dedicated video memory
ComPtr<IDXGIAdapter1> adapter;
SIZE_T maxDedicatedMemory = 0;
for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; i++) {
DXGI_ADAPTER_DESC1 desc;
adapter->GetDesc1(&desc);
if (desc.DedicatedVideoMemory > maxDedicatedMemory &&
SUCCEEDED(D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_1,
__uuidof(ID3D12Device), nullptr))) {
maxDedicatedMemory = desc.DedicatedVideoMemory;
break;
}
}
D3D12CreateDevice(adapter.Get(), D3D_FEATURE_LEVEL_12_1, IID_PPV_ARGS(&device));
// Create command queue
D3D12_COMMAND_QUEUE_DESC queueDesc{};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
queueDesc.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;
device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&commandQueue));
// Create swapchain
DXGI_SWAP_CHAIN_DESC1 swapchainDesc{};
swapchainDesc.BufferCount = FrameCount;
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;
ComPtr<IDXGISwapChain1> tempSwapChain;
factory->CreateSwapChainForHwnd(commandQueue.Get(), hwnd, &swapchainDesc,
nullptr, nullptr, &tempSwapChain);
tempSwapChain.As(&swapChain);
frameIndex = swapChain->GetCurrentBackBufferIndex();
// Create command allocator and list
device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT,
IID_PPV_ARGS(&commandAllocator));
device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT,
commandAllocator.Get(), nullptr,
IID_PPV_ARGS(&commandList));
std::cout << "DirectX 12 device created (Feature Level 12.1)" << std::endl;
}
};
Root Signatures and Pipeline State
DirectX 12 requires a root signature (defining shader resources) and a pipeline state object (PSO, all fixed-function state):
void createRootSignature(ComPtr<ID3D12Device> device,
ComPtr<ID3D12RootSignature>& rootSignature) {
D3D12_ROOT_SIGNATURE_FLAGS rootSignatureFlags =
D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT |
D3D12_ROOT_SIGNATURE_FLAG_DENY_HULL_SHADER_ROOT_ACCESS |
D3D12_ROOT_SIGNATURE_FLAG_DENY_DOMAIN_SHADER_ROOT_ACCESS |
D3D12_ROOT_SIGNATURE_FLAG_DENY_GEOMETRY_SHADER_ROOT_ACCESS;
CD3DX12_ROOT_PARAMETER1 rootParameters[1];
rootParameters[0].InitAsConstants(4, 0, 0, D3D12_SHADER_VISIBILITY_VERTEX);
CD3DX12_VERSIONED_ROOT_SIGNATURE_DESC rootSignatureDesc;
rootSignatureDesc.Init_1_1(1, rootParameters, 0, nullptr, rootSignatureFlags);
ComPtr<ID3DBlob> signatureBlob, errorBlob;
D3D12SerializeVersionedRootSignature(&rootSignatureDesc, &signatureBlob, &errorBlob);
device->CreateRootSignature(0, signatureBlob->GetBufferPointer(),
signatureBlob->GetBufferSize(),
IID_PPV_ARGS(&rootSignature));
}
void createPipelineState(ComPtr<ID3D12Device> device,
ComPtr<ID3D12RootSignature> rootSignature,
const wchar_t* vertexShaderPath,
const wchar_t* pixelShaderPath,
ComPtr<ID3D12PipelineState>& pso) {
// Compile shaders
ComPtr<ID3DBlob> vertexShader, pixelShader;
D3DCompileFromFile(vertexShaderPath, nullptr, nullptr, "VSMain", "vs_5_1", 0, 0,
&vertexShader, nullptr);
D3DCompileFromFile(pixelShaderPath, nullptr, nullptr, "PSMain", "ps_5_1", 0, 0,
&pixelShader, nullptr);
D3D12_INPUT_ELEMENT_DESC inputLayout[] = {
{"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
{"COLOR", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, 12,
D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, 0},
};
D3D12_GRAPHICS_PIPELINE_STATE_DESC psoDesc{};
psoDesc.pRootSignature = rootSignature.Get();
psoDesc.VS = CD3DX12_SHADER_BYTECODE(vertexShader.Get());
psoDesc.PS = CD3DX12_SHADER_BYTECODE(pixelShader.Get());
psoDesc.InputLayout = {inputLayout, 2};
psoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE;
psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM;
psoDesc.NumRenderTargets = 1;
D3D12_RASTERIZER_DESC& rasterizer = psoDesc.RasterizerState;
rasterizer.FillMode = D3D12_FILL_MODE_SOLID;
rasterizer.CullMode = D3D12_CULL_MODE_BACK;
rasterizer.FrontCounterClockwise = false;
rasterizer.DepthClipEnable = true;
D3D12_BLEND_DESC& blend = psoDesc.BlendState;
blend.RenderTarget[0].RenderTargetWriteMask = D3D12_COLOR_WRITE_ENABLE_ALL;
psoDesc.SampleDesc.Count = 1;
psoDesc.SampleMask = UINT_MAX;
device->CreateGraphicsPipelineState(&psoDesc, IID_PPV_ARGS(&pso));
}
Recording and Executing Commands
void recordAndSubmit(ComPtr<ID3D12GraphicsCommandList> commandList,
ComPtr<ID3D12CommandAllocator> commandAllocator,
ComPtr<ID3D12Resource> renderTarget,
ComPtr<ID3D12PipelineState> pso,
D3D12_CPU_DESCRIPTOR_HANDLE rtvHandle,
ComPtr<ID3D12CommandQueue> commandQueue) {
commandAllocator->Reset();
commandList->Reset(commandAllocator.Get(), pso.Get());
// Set viewport and scissor rect
D3D12_VIEWPORT viewport = {0.0f, 0.0f, 800.0f, 600.0f, 0.0f, 1.0f};
D3D12_RECT scissorRect = {0, 0, 800, 600};
commandList->RSSetViewports(1, &viewport);
commandList->RSSetScissorRects(1, &scissorRect);
// Transition to render target
D3D12_RESOURCE_BARRIER barrier = CD3DX12_RESOURCE_BARRIER::Transition(
renderTarget.Get(), D3D12_RESOURCE_STATE_PRESENT,
D3D12_RESOURCE_STATE_RENDER_TARGET);
commandList->ResourceBarrier(1, &barrier);
commandList->OMSetRenderTargets(1, &rtvHandle, FALSE, nullptr);
// Clear
float clearColor[] = {0.1f, 0.1f, 0.2f, 1.0f};
commandList->ClearRenderTargetView(rtvHandle, clearColor, 0, nullptr);
// Draw triangle
commandList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
commandList->DrawInstanced(3, 1, 0, 0);
// Transition to present
barrier = CD3DX12_RESOURCE_BARRIER::Transition(
renderTarget.Get(), D3D12_RESOURCE_STATE_RENDER_TARGET,
D3D12_RESOURCE_STATE_PRESENT);
commandList->ResourceBarrier(1, &barrier);
commandList->Close();
ID3D12CommandList* commandLists[] = {commandList.Get()};
commandQueue->ExecuteCommandLists(1, commandLists);
}
flowchart TD A[Create Device & Queue] --> B[Create Swapchain] B --> C[Create RTV Descriptor Heap] C --> D[Create Root Signature & PSO] D --> E[Record Command List] E -->|ExecuteCommandLists| F[GPU Renders] F -->|Present| G[DXGI Flip Model] G --> H[Wait for GPU with Fence] H --> E
Shaders for DirectX 12
// Vertex Shader
struct VSInput {
float3 position : POSITION;
float4 color : COLOR;
};
struct VSOutput {
float4 position : SV_POSITION;
float4 color : COLOR;
};
VSOutput VSMain(VSInput input) {
VSOutput output;
output.position = float4(input.position, 1.0);
output.color = input.color;
return output;
}
// Pixel Shader
float4 PSMain(VSOutput input) : SV_TARGET {
return input.color;
}
Common Errors & Mistakes
1. Resource State Tracking Errors
Mistake: Using a resource in a state it is not currently in (e.g., reading a buffer that still has write pending), causing GPU hangs.
Fix: Track resource states with barriers. Transition to D3D12_RESOURCE_STATE_RENDER_TARGET before rendering and to D3D12_RESOURCE_STATE_PRESENT before presenting.
2. Fence Synchronization
Mistake: Not waiting for the GPU to finish before reusing resources (command allocators, back buffers), causing data races.
Fix: Use fences to track GPU progress. Wait for the fence value matching the current frame index before reusing that frame's resources.
3. Command Allocator Reset Without Wait
Mistake: Resetting a command allocator while the GPU is still executing commands recorded in it.
Fix: Track which command allocators are in flight. Only reset allocators whose associated fence value has been signaled.
4. Descriptor Heap Overflow
Mistake: Running out of descriptor heap space for textures, samplers, or render target views.
Fix: Pre-allocate descriptor heaps with sufficient capacity. Use descriptor tables instead of root descriptors when many resources are needed.
Practice Questions
Question 1
What is a command allocator and how does it differ from a command list?
Show answer
A command allocator manages the underlying memory for command list recording. Multiple command lists can be recorded from one allocator, but they cannot be executed simultaneously. The allocator must be reset when all associated command lists have finished execution.Question 2
What is a root signature and why is it needed?
Show answer
A root signature defines the resources (constants, descriptors, tables) that shaders access. It must match between the pipeline state object and the command list. It is validated at PSO creation time, not at draw time.Question 3
What is the purpose of a fence in DirectX 12?
Show answer
A fence is a synchronization primitive that signals when the GPU has reached a certain point in execution. The CPU can wait for fence values to know when resources are safe to reuse, enabling multi-frame buffering without data races.Challenge
Extend the DirectX 12 triangle to render a textured quad with vertex and index buffers, a constant buffer for transform, and a depth buffer. Implement a simple camera rotation and measure GPU timestamps.
FAQ
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Author: DodaTech | Last updated: June 23, 2026
DodaTech tutorials are built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — security tools used by millions worldwide.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro