The Command Queue

Understand why DX12 requires explicit command queues, create a direct queue for rendering, and learn why DXGI needs this queue before it can build a swap chain.

Early Draft Work in progress — GitHub links coming soon.

In the previous chapter we created the D3D12 device. The device is the factory for everything, but it cannot run GPU work by itself. To submit work to the GPU you need a command queue.

By the end of this chapter you will have:

Why Queues Are Explicit in DX12

In DX11 there was one implicit immediate context. You called draw commands on it and the driver batched and submitted them to the GPU behind the scenes. You had no control over when work was submitted, how it was parallelised, or how the CPU and GPU stayed synchronised.

DX12 removes all of that implicit behaviour. You create command queues yourself, record command lists, submit them explicitly, and manage synchronisation with fences. The trade-off is more code at startup and more responsibility everywhere, but also far more control — and far fewer driver-level surprises once you understand the rules.

Three queue types exist, ordered by capability:

TypeCan run
D3D12_COMMAND_LIST_TYPE_DIRECTDraw calls, compute dispatches, copies — everything
D3D12_COMMAND_LIST_TYPE_COMPUTECompute dispatches and copies
D3D12_COMMAND_LIST_TYPE_COPYCopy operations only

A narrower queue type can run in parallel with the direct queue on some hardware. Async compute (running a compute pass on the compute queue at the same time as a draw pass on the direct queue) is a real optimisation technique. We are not there yet. For now we need one direct queue and everything goes through it.

Creating the Queue

#include <d3d12.h>
#include <wrl/client.h>

D3D12_COMMAND_QUEUE_DESC desc = {};
desc.Type     = D3D12_COMMAND_LIST_TYPE_DIRECT;
desc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
desc.Flags    = D3D12_COMMAND_QUEUE_FLAG_NONE;
desc.NodeMask = 0;

Microsoft::WRL::ComPtr<ID3D12CommandQueue> queue;
device->CreateCommandQueue(&desc, IID_PPV_ARGS(&queue));

NodeMask is a multi-GPU field — zero means “use the default single-GPU node”. Priority can be NORMAL, HIGH, or GLOBAL_REALTIME. NORMAL is correct for rendering. GLOBAL_REALTIME requires administrator privileges and is for specialised workloads.

Flags can be D3D12_COMMAND_QUEUE_FLAG_DISABLE_GPU_TIMEOUT. That flag prevents Windows from killing the GPU process if a command list runs for more than a few seconds. Leave it off for now; the watchdog timer is a safety net that catches infinite loops.

Why DXGI Needs the Queue

The command queue is not just for recording and submitting render work. DXGI also requires a pointer to it when creating the swap chain.

This is a DX12-specific requirement. When you call Present, DXGI needs to know which command queue it should signal after it finishes copying the back buffer to the screen. That way the D3D12 runtime can maintain correct synchronisation between your render work and the presentation engine.

In DX11 DXGI handled this internally. In DX12 you make it explicit. The consequence is that the swap chain must be created after the queue, and both must be created before rendering begins.

Wrapping in a Class

Add CommandQueue.h and CommandQueue.cpp to the project:

dx12-sandbox/
    CMakeLists.txt
    src/
        main.cpp
        Window.cpp
        Window.h
        DXGIContext.cpp
        DXGIContext.h
        D3DDevice.cpp
        D3DDevice.h
        CommandQueue.cpp  ← new
        CommandQueue.h    ← new

CommandQueue.h

#pragma once

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <d3d12.h>
#include <wrl/client.h>

class CommandQueue
{
public:
    bool Init(ID3D12Device* device);

    ID3D12CommandQueue* Get() const { return queue_.Get(); }

private:
    Microsoft::WRL::ComPtr<ID3D12CommandQueue> queue_;
};

CommandQueue.cpp

#include "CommandQueue.h"

bool CommandQueue::Init(ID3D12Device* device)
{
    D3D12_COMMAND_QUEUE_DESC desc = {};
    desc.Type     = D3D12_COMMAND_LIST_TYPE_DIRECT;
    desc.Priority = D3D12_COMMAND_QUEUE_PRIORITY_NORMAL;
    desc.Flags    = D3D12_COMMAND_QUEUE_FLAG_NONE;
    desc.NodeMask = 0;

    return SUCCEEDED(device->CreateCommandQueue(&desc, IID_PPV_ARGS(&queue_)));
}

Update CMakeLists.txt

cmake_minimum_required(VERSION 3.24)

project(DX12Sandbox LANGUAGES CXX)

add_executable(DX12Sandbox WIN32
    src/main.cpp
    src/Window.cpp
    src/DXGIContext.cpp
    src/D3DDevice.cpp
    src/CommandQueue.cpp
)

target_include_directories(DX12Sandbox PRIVATE src)

target_compile_features(DX12Sandbox PRIVATE cxx_std_20)

target_link_libraries(DX12Sandbox PRIVATE
    dxgi
    d3d12
)

main.cpp

#include "Window.h"
#include "DXGIContext.h"
#include "D3DDevice.h"
#include "CommandQueue.h"

int RunRenderLoop()
{
    MSG msg = {};

    while (msg.message != WM_QUIT)
    {
        if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
        {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
        else
        {
            // Later: RenderFrame();
        }
    }

    return static_cast<int>(msg.wParam);
}

int WINAPI wWinMain(
    HINSTANCE hInstance,
    HINSTANCE,
    PWSTR,
    int nCmdShow)
{
    Window window;
    if (!window.Create(hInstance, 1280, 720, nCmdShow))
    {
        MessageBoxW(nullptr, L"Window creation failed.", L"Startup Error", MB_ICONERROR);
        return -1;
    }

    DXGIContext dxgi;
    if (!dxgi.Init(true))
    {
        MessageBoxW(nullptr, L"No suitable GPU found.", L"Startup Error", MB_ICONERROR);
        return -1;
    }

    D3DDevice d3d;
    if (!d3d.Init(dxgi.Adapter(), true))
    {
        MessageBoxW(nullptr, L"D3D12 device creation failed.", L"Startup Error", MB_ICONERROR);
        return -1;
    }

    CommandQueue queue;
    if (!queue.Init(d3d.Device()))
    {
        MessageBoxW(nullptr, L"Command queue creation failed.", L"Startup Error", MB_ICONERROR);
        return -1;
    }

    return RunRenderLoop();
}

Common Mistakes

If CreateCommandQueue returns E_INVALIDARG, double-check the Type field in the descriptor. An invalid enum value is the most common cause.

Do not use the same command queue from multiple threads without synchronisation. The queue itself is thread-safe for submission, but you must ensure command lists are fully closed before passing them to ExecuteCommandLists, and that two threads are not submitting to the same queue concurrently without coordination.

Quick Checkpoint

You are ready to move on if these ideas feel solid:

What’s Next

Next we build the swap chain. We now have all three pieces DXGI requires: the factory from DXGIContext, the HWND from Window, and the command queue we just created. The swap chain owns the back buffers and manages the flip from the rendered frame to the display.