The D3D12 Device

Create the D3D12 device from the DXGI adapter, enable the debug validation layer before device creation, pick a feature level, and wrap everything in a small class.

Early Draft Work in progress — GitHub links coming soon.

In the previous chapter we used DXGI to select a GPU adapter. That adapter object describes the hardware — its name, VRAM, and capabilities. The D3D12 device is the logical representation of that hardware inside your process. It is the object you use to create every other DX12 resource.

By the end of this chapter you will have:

What the Device Is (and Is Not)

The device does not record GPU commands. It does not submit work to the GPU. It does not own the GPU timeline. Those responsibilities belong to command lists and command queues.

The device’s job is creation and introspection:

Think of it as the factory for every other DX12 object. Almost every Create* call you will ever make belongs to ID3D12Device.

The Debug Layer

Before creating the device, you need to enable the D3D12 debug layer. The ordering is critical: the debug layer must be enabled before D3D12CreateDevice. Enabling it afterwards has no effect on the device you have already created.

The debug layer intercepts every DX12 call and validates it. It catches mismatched resource states, incorrect barrier transitions, invalid descriptor accesses, and dozens of other mistakes that otherwise produce silent corruption or a GPU hang. In a release build you leave it off — the validation overhead is real. In a development build it is indispensable.

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

Microsoft::WRL::ComPtr<ID3D12Debug> debug;
if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug))))
{
    debug->EnableDebugLayer();
}

D3D12GetDebugInterface returns the debug controller. If the Graphics Tools optional Windows feature is not installed, this call returns a failure code. Do not crash on that failure — just skip the layer and continue.

To install Graphics Tools: Settings → Apps → Optional Features → Add a feature → Graphics Tools.

Feature Levels

A D3D12 feature level declares the minimum set of GPU capabilities your program needs. If the chosen adapter cannot meet the level, D3D12CreateDevice fails cleanly rather than running on unsupported hardware.

Feature levelWhat it covers
D3D_FEATURE_LEVEL_11_0DX11-class hardware with a DX12 driver
D3D_FEATURE_LEVEL_12_0Tier 1 resource binding, hardware feature level 12
D3D_FEATURE_LEVEL_12_1Conservative rasterization, rasterizer-ordered views
D3D_FEATURE_LEVEL_12_2Mesh shaders, DirectX Raytracing tier 1.1

We will request D3D_FEATURE_LEVEL_12_0. It covers every DirectX 12–capable GPU shipped since roughly 2015 and gives us everything needed for a standard render loop.

Creating the Device

Microsoft::WRL::ComPtr<ID3D12Device> device;

HRESULT hr = D3D12CreateDevice(
    adapter,                    // IDXGIAdapter1* from DXGIContext
    D3D_FEATURE_LEVEL_12_0,
    IID_PPV_ARGS(&device)
);

if (FAILED(hr))
{
    return false;
}

Passing nullptr as the adapter tells DX12 to choose one itself. We do not do that here because we already selected the best adapter in the previous chapter.

Break on Errors in Debug Builds

When the debug layer is active, DX12 sends validation messages to a queue on the device. By default those messages just print to the debugger output. You can make the debug layer fire a breakpoint the moment it encounters an error, which is far easier to diagnose than scrolling through output text after the fact.

After creating the device, query for ID3D12InfoQueue:

#ifdef _DEBUG
Microsoft::WRL::ComPtr<ID3D12InfoQueue> infoQueue;
if (SUCCEEDED(device->QueryInterface(IID_PPV_ARGS(&infoQueue))))
{
    infoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, TRUE);
    infoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, TRUE);
}
#endif

CORRUPTION means the GPU is producing undefined behaviour. ERROR means you called the API incorrectly. Both are worth breaking on. WARNING can be noisy — leave it off to start.

Wrapping in a Class

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

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

D3DDevice.h

#pragma once

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

class D3DDevice
{
public:
    bool Init(IDXGIAdapter1* adapter, bool debugLayer);

    ID3D12Device* Device() const { return device_.Get(); }

private:
    static void EnableDebugLayer();
    void SetupInfoQueue();

    Microsoft::WRL::ComPtr<ID3D12Device> device_;
};

D3DDevice.cpp

#include "D3DDevice.h"

bool D3DDevice::Init(IDXGIAdapter1* adapter, bool debugLayer)
{
    if (debugLayer)
    {
        EnableDebugLayer();
    }

    if (FAILED(D3D12CreateDevice(adapter, D3D_FEATURE_LEVEL_12_0, IID_PPV_ARGS(&device_))))
    {
        return false;
    }

    if (debugLayer)
    {
        SetupInfoQueue();
    }

    return true;
}

void D3DDevice::EnableDebugLayer()
{
    Microsoft::WRL::ComPtr<ID3D12Debug> debug;
    if (SUCCEEDED(D3D12GetDebugInterface(IID_PPV_ARGS(&debug))))
    {
        debug->EnableDebugLayer();
    }
}

void D3DDevice::SetupInfoQueue()
{
#ifdef _DEBUG
    Microsoft::WRL::ComPtr<ID3D12InfoQueue> infoQueue;
    if (SUCCEEDED(device_->QueryInterface(IID_PPV_ARGS(&infoQueue))))
    {
        infoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_CORRUPTION, TRUE);
        infoQueue->SetBreakOnSeverity(D3D12_MESSAGE_SEVERITY_ERROR, TRUE);
    }
#endif
}

The two debug calls are deliberately split. EnableDebugLayer must run before D3D12CreateDevice. SetupInfoQueue can only run after, because the info queue is an interface on the device. Putting both calls in Init makes the ordering visible.

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
)

target_include_directories(DX12Sandbox PRIVATE src)

target_compile_features(DX12Sandbox PRIVATE cxx_std_20)

target_link_libraries(DX12Sandbox PRIVATE
    dxgi
    d3d12
)

d3d12 resolves to d3d12.lib from the Windows SDK automatically.

main.cpp

#include "Window.h"
#include "DXGIContext.h"
#include "D3DDevice.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;
    }

    return RunRenderLoop();
}

Common Mistakes

If D3D12CreateDevice fails with E_INVALIDARG, the adapter does not support the requested feature level. Lower to D3D_FEATURE_LEVEL_11_0 temporarily to confirm the adapter is at least recognised. If that also fails, the adapter has no DX12 driver support.

If the debug layer is not catching errors you expect, confirm that EnableDebugLayer is called before D3D12CreateDevice. The call order is strict.

If D3D12GetDebugInterface always returns failure, install the Graphics Tools optional feature. Without it, the debug runtime is not present.

If break-on-error is not firing, check that _DEBUG is defined. Visual Studio defines it automatically in Debug configurations. With CMake you may need to add it explicitly:

target_compile_definitions(DX12Sandbox PRIVATE $<$<CONFIG:Debug>:_DEBUG>)

Quick Checkpoint

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

What’s Next

Next we create the command queue — the channel through which recorded work is submitted to the GPU. In DX12 there is no implicit submission context. You create queues explicitly, and their type determines what operations they can run. A direct queue can run everything: draw calls, compute dispatches, and copies.