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:
- a
D3DDeviceclass that owns anID3D12Device, - the debug validation layer enabled before device creation,
- break-on-error behaviour in debug builds,
- and the build linked against
d3d12.lib.
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:
- create command queues, command allocators, command lists,
- create GPU resources — textures, buffers, heaps,
- create descriptor heaps and root signatures,
- create pipeline state objects.
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 level | What it covers |
|---|---|
D3D_FEATURE_LEVEL_11_0 | DX11-class hardware with a DX12 driver |
D3D_FEATURE_LEVEL_12_0 | Tier 1 resource binding, hardware feature level 12 |
D3D_FEATURE_LEVEL_12_1 | Conservative rasterization, rasterizer-ordered views |
D3D_FEATURE_LEVEL_12_2 | Mesh 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:
- The device creates DX12 objects. It does not record or submit GPU work.
- The debug layer must be enabled before
D3D12CreateDevice. D3D_FEATURE_LEVEL_12_0covers all DX12-capable hardware since 2015.ID3D12InfoQueuelets the debug layer halt the program on API errors.d3d12.libmust be listed intarget_link_libraries.- Almost every DX12
Create*call requires a pointer to this device.
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.