Command Allocators and Command Lists

Understand the allocator and list model for recording GPU work, create one allocator per back buffer, create a reusable command list, and learn the reset pattern used every frame.

Early Draft Work in progress — GitHub links coming soon.

In the previous chapter we created the descriptor heap with render target views for each back buffer. We can now describe where we will write pixels. What we still cannot do is record any GPU commands.

In DX12, recording commands requires two objects working together: a command allocator and a command list.

By the end of this chapter you will have:

The Two-Object Model

Command allocator — the backing memory pool where recorded commands are stored. When you record a draw call or a resource barrier, the binary encoding of that command is written into memory owned by the allocator. The allocator cannot be reset while the GPU is still reading from it.

Command list — the interface you call to record commands. A command list does not own memory; it writes into the allocator. One command list can be reused across frames by resetting it each frame with a different allocator.

The split exists for efficiency. Allocators can be reclaimed and pooled. A command list is a thin recording context you keep alive.

Why One Allocator Per Back Buffer

Resetting an allocator while the GPU is still executing commands from it is undefined behaviour. The GPU and CPU run concurrently. If the CPU is recording frame N+1 while the GPU is still processing frame N, both frames share the allocator’s memory and corrupt each other.

The safe approach: give each back buffer its own allocator. With double buffering and a fence that ensures the GPU has finished with frame N before the CPU recycles that frame’s allocator, there is no overlap.

Frame 0 → allocator[0] → GPU processes → allocator[0].Reset() is safe
Frame 1 → allocator[1] → GPU processes → allocator[1].Reset() is safe
Frame 2 → allocator[0] again (GPU finished frame 0)

Creating Allocators

Microsoft::WRL::ComPtr<ID3D12CommandAllocator> allocators[FrameCount];

for (UINT i = 0; i < FrameCount; ++i)
{
    device->CreateCommandAllocator(
        D3D12_COMMAND_LIST_TYPE_DIRECT,
        IID_PPV_ARGS(&allocators[i]));
}

The type must match the type of commands you will record into the list — DIRECT here because we will be recording draw commands and resource barriers.

Creating the Command List

A command list is created with an initial allocator and starts in the open (recording) state:

Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> commandList;

device->CreateCommandList(
    0,                                        // NodeMask
    D3D12_COMMAND_LIST_TYPE_DIRECT,
    allocators[0].Get(),                      // initial allocator
    nullptr,                                  // initial pipeline state
    IID_PPV_ARGS(&commandList));

commandList->Close();

The second nullptr argument is the initial pipeline state object (PSO). We pass nullptr because we have no PSO yet. Passing nullptr is valid and tells DX12 the list is not bound to any pipeline at creation time.

The Close() call immediately after creation is essential. The list starts open. Reset can only be called on a closed list. If you call Reset on an open list, the debug layer will report an error. Closing immediately puts the list into the known state we need for the reset pattern.

The Reset Pattern

At the start of each frame, before recording any commands:

UINT frameIndex = swapChain.CurrentBackBufferIndex();

allocators[frameIndex]->Reset();
commandList->Reset(allocators[frameIndex].Get(), nullptr);

allocator->Reset() discards all previously recorded commands and reclaims the memory. This is safe only after the GPU has finished executing those commands — fence synchronisation (next chapter) guarantees this.

commandList->Reset(allocator, pso) rebinds the list to the new allocator and re-opens it for recording. The second argument is the initial PSO; nullptr again for now.

After Reset, the list is open and you can record commands into it.

Closing the List

When you are done recording, close the list before submitting it:

commandList->Close();

ID3D12CommandList* lists[] = { commandList.Get() };
commandQueue->ExecuteCommandLists(1, lists);

ExecuteCommandLists takes an array of ID3D12CommandList* pointers (the base interface for ID3D12GraphicsCommandList). The list must be closed before this call — submitting an open list is an error.

After ExecuteCommandLists, the list is closed and the commands are queued on the GPU. The allocator’s memory is now live on the GPU side. Do not reset the allocator until the GPU signals completion.

Wrapping in a Class

The cleanest home for these objects is a small struct that holds one allocator per frame and the shared command list. The command list is shared — only one allocator drives it at any given time, and we switch via Reset.

Add FrameResources.h and FrameResources.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
        CommandQueue.h
        SwapChain.cpp
        SwapChain.h
        RTVHeap.cpp
        RTVHeap.h
        FrameResources.cpp  ← new
        FrameResources.h    ← new

FrameResources.h

#pragma once

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

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

    bool ResetFor(UINT frameIndex);

    ID3D12GraphicsCommandList* CommandList() const { return commandList_.Get(); }

private:
    Microsoft::WRL::ComPtr<ID3D12CommandAllocator>    allocators_[FrameCount];
    Microsoft::WRL::ComPtr<ID3D12GraphicsCommandList> commandList_;
};

FrameResources.cpp

#include "FrameResources.h"

bool FrameResources::Init(ID3D12Device* device)
{
    for (UINT i = 0; i < FrameCount; ++i)
    {
        if (FAILED(device->CreateCommandAllocator(
                D3D12_COMMAND_LIST_TYPE_DIRECT,
                IID_PPV_ARGS(&allocators_[i]))))
        {
            return false;
        }
    }

    if (FAILED(device->CreateCommandList(
            0,
            D3D12_COMMAND_LIST_TYPE_DIRECT,
            allocators_[0].Get(),
            nullptr,
            IID_PPV_ARGS(&commandList_))))
    {
        return false;
    }

    commandList_->Close();

    return true;
}

bool FrameResources::ResetFor(UINT frameIndex)
{
    if (FAILED(allocators_[frameIndex]->Reset()))
    {
        return false;
    }

    if (FAILED(commandList_->Reset(allocators_[frameIndex].Get(), nullptr)))
    {
        return false;
    }

    return true;
}

ResetFor takes the frame index so it picks the right allocator. Checking the HRESULT here is useful during development: if you accidentally reset an allocator that the GPU is still using, the debug layer will return an error rather than silently corrupting memory.

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
    src/SwapChain.cpp
    src/RTVHeap.cpp
    src/FrameResources.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"
#include "SwapChain.h"
#include "RTVHeap.h"
#include "FrameResources.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;
    }

    SwapChain swapChain;
    if (!swapChain.Init(dxgi.Factory(), queue.Get(), window.Handle(),
                        window.Width(), window.Height()))
    {
        MessageBoxW(nullptr, L"Swap chain creation failed.", L"Startup Error", MB_ICONERROR);
        return -1;
    }

    RTVHeap rtvHeap;
    if (!rtvHeap.Init(d3d.Device(), swapChain))
    {
        MessageBoxW(nullptr, L"RTV heap creation failed.", L"Startup Error", MB_ICONERROR);
        return -1;
    }

    FrameResources frames;
    if (!frames.Init(d3d.Device()))
    {
        MessageBoxW(nullptr, L"Frame resources creation failed.", L"Startup Error", MB_ICONERROR);
        return -1;
    }

    return RunRenderLoop();
}

Common Mistakes

If the debug layer reports “allocator is being reset while the GPU is still executing”, you are calling allocator->Reset() before the fence has confirmed the GPU finished. The next chapter adds the fence that prevents this.

If commandList->Reset() fails with E_FAIL, the list is probably still open from a previous frame — you forgot to call Close() before submitting, or you called Reset on an already-open list.

If you call Reset on the command list with the wrong allocator index, commands for the wrong frame will be overwritten. Tie the allocator index to GetCurrentBackBufferIndex() consistently.

Quick Checkpoint

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

What’s Next

Next we write the first render loop. We will record a resource barrier to transition the back buffer from present to render target, clear it to a colour, transition it back, and present it. We will also add the fence that keeps CPU and GPU in sync — without it the allocator reset would corrupt in-flight frames.