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:
- a
CommandQueueclass that owns anID3D12CommandQueue, - an understanding of why DX12 exposes queues explicitly,
- and the queue ready to hand to DXGI when we create the swap chain.
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:
| Type | Can run |
|---|---|
D3D12_COMMAND_LIST_TYPE_DIRECT | Draw calls, compute dispatches, copies — everything |
D3D12_COMMAND_LIST_TYPE_COMPUTE | Compute dispatches and copies |
D3D12_COMMAND_LIST_TYPE_COPY | Copy 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:
- DX12 has no implicit submission context. You create queues and submit explicitly.
- A direct queue can run draw calls, compute, and copies.
NodeMask = 0is correct for single-GPU systems.- DXGI needs a pointer to the command queue when building the swap chain, because it must signal that queue after presentation.
- The queue is the raw
ID3D12CommandQueue*; command lists submit work through it.
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.