Skip to content

DirectX 12 Guide — Modern Graphics Programming on Windows

DodaTech Updated 2026-06-23 6 min read

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

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

What is the difference between DirectX 11 and DirectX 12?

DirectX 11 is a high-level API with driver-managed resource tracking, similar to OpenGL. DirectX 12 is low-level and explicit: the developer manages memory barriers, resource state, and synchronization for maximum performance.

Can DirectX 12 run on Windows 10?

Yes, DirectX 12 is supported on Windows 10 (version 1809+) and Windows 11. Feature Level 12_0 requires NVIDIA Kepler+ or AMD GCN+, while 12_1 adds conservative rasterization and rasterizer-ordered views.

What is DirectStorage?

DirectStorage is a DirectX 12 extension for fast NVMe SSD-to-GPU asset streaming. It bypasses the CPU and system memory for decompression and transfer, reducing load times from minutes to seconds.


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