In the previous chapter we packaged the Win32 window into a Window class. That class stores the HWND and the current client size, and it leaves a comment where the swap chain resize code will eventually go.
Now we start filling in the DX12 side. Before we create a D3D12 device or record a single GPU command, we need DXGI. DXGI is the layer that connects DirectX to the OS, the display hardware, and the window on screen.
By the end of this chapter you will have:
- a
DXGIContextclass that creates a factory and picks a GPU adapter, - the factory available for swap chain creation in a later chapter,
- a working build with
dxgi.liblinked, - and a clear mental model of what DXGI is responsible for and what it is not.
Why DXGI Exists
When Microsoft shipped DirectX 10, they had a problem. Every major version of DirectX had reinvented the same things independently: enumerating graphics cards, choosing display modes, managing the swap of rendered frames onto the screen. Each version did it slightly differently. Applications had to handle all of it themselves.
DXGI — DirectX Graphics Infrastructure — was the answer. It extracted the shared infrastructure into a single versioned layer that DirectX 10, 11, and 12 all sit on top of.
The split is clean:
- DXGI handles anything that touches the OS or the display: adapter enumeration, output (monitor) enumeration, swap chains, full-screen transitions, and the pixel formats used across the system.
- D3D12 handles the GPU side: devices, command queues, command lists, resources, shaders, pipelines, and render passes.
A useful way to think about it: DXGI is about where pixels end up and how they get to the screen. D3D12 is about producing those pixels.
For a DX12 program you will touch DXGI in two places:
- Startup: enumerate adapters, pick the right GPU.
- Presentation: create a swap chain, call
Presentonce per frame.
Everything else is D3D12.
DXGI Versions
DXGI ships as a series of versioned header files. Each version adds new interfaces built on top of the previous ones:
| Header | Key addition |
|---|---|
dxgi.h | Original (DX10 era) |
dxgi1_2.h | Stereo, CreateSwapChainForHwnd |
dxgi1_3.h | Waitable swap chains, overlays |
dxgi1_4.h | IDXGIFactory4, required for DX12 device wrapping |
dxgi1_5.h | HDR, variable refresh rate queries |
dxgi1_6.h | EnumAdapterByGpuPreference for laptop GPU selection |
We will include <dxgi1_6.h>. That pulls in every version below it automatically. You only need to include the highest version you use.
We will use IDXGIFactory4 for the factory. That is the minimum version that supports CreateSwapChainForHwnd, which is how DX12 programs create a swap chain. IDXGIFactory6 adds the GPU-preference enumeration, which we will use when picking an adapter, so we will query for it at runtime if available.
A Note on COM
DXGI is a COM API. COM (Component Object Model) is the Windows object system behind DirectX, DXGI, WIC, and much of the Windows SDK. Every DXGI object is a COM interface: a pointer to a table of function pointers, with AddRef and Release for reference counting.
You almost never call AddRef and Release directly in modern C++. Instead you use Microsoft::WRL::ComPtr<T>, a smart pointer included with the Windows SDK:
#include <wrl/client.h>
Microsoft::WRL::ComPtr<IDXGIFactory4> factory;
ComPtr calls Release when it goes out of scope, just like std::unique_ptr calls delete. It also supports Get() to retrieve the raw pointer and GetAddressOf() or & when passing to functions that write a new pointer into an output parameter.
The macro IID_PPV_ARGS combines the interface ID and the output pointer address in a single call:
CreateDXGIFactory2(0, IID_PPV_ARGS(&factory));
// equivalent to:
CreateDXGIFactory2(0, __uuidof(IDXGIFactory4), reinterpret_cast<void**>(&factory));
You will see IID_PPV_ARGS everywhere in DX12 code.
Creating the Factory
The DXGI factory is the root object. You cannot create adapters, swap chains, or outputs without one. There is one global factory per process; you create it once at startup.
#include <dxgi1_6.h>
#include <wrl/client.h>
Microsoft::WRL::ComPtr<IDXGIFactory4> factory;
UINT flags = 0;
// Set DXGI_CREATE_FACTORY_DEBUG to enable the DXGI validation layer.
HRESULT hr = CreateDXGIFactory2(flags, IID_PPV_ARGS(&factory));
CreateDXGIFactory2 is the current function for creating a factory. The older CreateDXGIFactory exists but does not accept the debug flag and returns an older interface. Always use CreateDXGIFactory2.
The DXGI_CREATE_FACTORY_DEBUG flag tells DXGI to enable its own validation output, separate from D3D12’s debug layer. In development builds you want both. For now we will accept a bool debugLayer parameter and set the flag accordingly.
Enumerating Adapters
An adapter represents a physical GPU. On a desktop with a discrete graphics card there is usually one hardware adapter and one software adapter. On a laptop with both integrated and discrete graphics there can be two hardware adapters.
You enumerate them in a loop:
Microsoft::WRL::ComPtr<IDXGIAdapter1> candidate;
for (UINT i = 0; factory->EnumAdapters1(i, &candidate) != DXGI_ERROR_NOT_FOUND; ++i)
{
DXGI_ADAPTER_DESC1 desc = {};
candidate->GetDesc1(&desc);
// desc.Description — wide-character name, e.g. "NVIDIA GeForce RTX 4070"
// desc.VendorId — 0x10DE = NVIDIA, 0x1002 = AMD, 0x8086 = Intel
// desc.DedicatedVideoMemory — VRAM in bytes
// desc.Flags — DXGI_ADAPTER_FLAG_SOFTWARE for the software renderer
}
EnumAdapters1 returns DXGI_ERROR_NOT_FOUND when the list is exhausted. Each call writes a ComPtr<IDXGIAdapter1> and increments the index.
Skipping the Software Adapter
Windows always includes a software renderer called WARP (Windows Advanced Rasterization Platform). WARP is useful for debugging and for machines without a GPU, but it is not what you want for a game. You can identify it with the DXGI_ADAPTER_FLAG_SOFTWARE flag:
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
continue;
}
Picking by VRAM
When multiple hardware adapters are present, the simplest heuristic is to pick the one with the most dedicated video memory. Dedicated VRAM usually means the discrete GPU:
if (desc.DedicatedVideoMemory > bestVram)
{
bestVram = desc.DedicatedVideoMemory;
adapter = candidate;
}
This heuristic is not perfect — on a machine with two equally matched GPUs or on eGPU setups it may not pick the “right” one — but it is reliable enough for a sandbox and easy to revisit later.
Using IDXGIFactory6 for GPU Preference
If IDXGIFactory6 is available (Windows 10 1803 and later), you can ask DXGI to sort adapters by preference directly:
Microsoft::WRL::ComPtr<IDXGIFactory6> factory6;
if (SUCCEEDED(factory->QueryInterface(IID_PPV_ARGS(&factory6))))
{
for (UINT i = 0;
factory6->EnumAdapterByGpuPreference(
i,
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
IID_PPV_ARGS(&candidate)) != DXGI_ERROR_NOT_FOUND;
++i)
{
DXGI_ADAPTER_DESC1 desc = {};
candidate->GetDesc1(&desc);
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
continue;
}
adapter = candidate;
break; // first result with DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE is the best
}
}
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE puts the discrete GPU first. DXGI_GPU_PREFERENCE_MINIMUM_POWER puts the integrated GPU first. This matters on laptops where the wrong choice can drain the battery or give unexpected performance.
We will query for IDXGIFactory6 and fall back to the VRAM heuristic if it is not available.
Wrapping in a Class
Add two files to the project:
dx12-sandbox/
CMakeLists.txt
src/
main.cpp
Window.cpp
Window.h
DXGIContext.cpp ← new
DXGIContext.h ← new
DXGIContext.h
#pragma once
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <dxgi1_6.h>
#include <wrl/client.h>
class DXGIContext
{
public:
bool Init(bool debugLayer);
IDXGIFactory4* Factory() const { return factory_.Get(); }
IDXGIAdapter1* Adapter() const { return adapter_.Get(); }
private:
bool SelectAdapter();
Microsoft::WRL::ComPtr<IDXGIFactory4> factory_;
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter_;
};
Factory() and Adapter() return raw pointers because DXGI functions expect raw COM pointers. The ComPtr objects own the lifetime; callers must not store or release these raw pointers independently.
DXGIContext.cpp
#include "DXGIContext.h"
bool DXGIContext::Init(bool debugLayer)
{
UINT flags = debugLayer ? DXGI_CREATE_FACTORY_DEBUG : 0;
if (FAILED(CreateDXGIFactory2(flags, IID_PPV_ARGS(&factory_))))
{
return false;
}
return SelectAdapter();
}
bool DXGIContext::SelectAdapter()
{
Microsoft::WRL::ComPtr<IDXGIAdapter1> candidate;
// Prefer IDXGIFactory6 which can sort by GPU preference.
Microsoft::WRL::ComPtr<IDXGIFactory6> factory6;
if (SUCCEEDED(factory_->QueryInterface(IID_PPV_ARGS(&factory6))))
{
for (UINT i = 0;
factory6->EnumAdapterByGpuPreference(
i,
DXGI_GPU_PREFERENCE_HIGH_PERFORMANCE,
IID_PPV_ARGS(&candidate)) != DXGI_ERROR_NOT_FOUND;
++i)
{
DXGI_ADAPTER_DESC1 desc = {};
candidate->GetDesc1(&desc);
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
continue;
}
adapter_ = candidate;
return true;
}
}
// Fall back to VRAM heuristic when IDXGIFactory6 is unavailable.
SIZE_T bestVram = 0;
for (UINT i = 0; factory_->EnumAdapters1(i, &candidate) != DXGI_ERROR_NOT_FOUND; ++i)
{
DXGI_ADAPTER_DESC1 desc = {};
candidate->GetDesc1(&desc);
if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
{
continue;
}
if (desc.DedicatedVideoMemory > bestVram)
{
bestVram = desc.DedicatedVideoMemory;
adapter_ = candidate;
}
}
return adapter_ != nullptr;
}
SelectAdapter tries IDXGIFactory6 first. The QueryInterface call asks the factory COM object “do you also implement IDXGIFactory6?”. If yes, factory6 is set and SUCCEEDED returns true. If the runtime is older, the call returns E_NOINTERFACE and we fall back to the loop.
Update CMakeLists.txt
DXGI is a system library. You do not download it; it ships with Windows and the Windows SDK. But you must link against it:
cmake_minimum_required(VERSION 3.24)
project(DX12Sandbox LANGUAGES CXX)
add_executable(DX12Sandbox WIN32
src/main.cpp
src/Window.cpp
src/DXGIContext.cpp
)
target_include_directories(DX12Sandbox PRIVATE src)
target_compile_features(DX12Sandbox PRIVATE cxx_std_20)
target_link_libraries(DX12Sandbox PRIVATE
dxgi
)
dxgi resolves to dxgi.lib from the Windows SDK automatically. Later we will add d3d12 and dxguid when the device arrives.
main.cpp
Add the DXGI context next to the window:
#include "Window.h"
#include "DXGIContext.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;
}
return RunRenderLoop();
}
true passes the debug flag to DXGIContext::Init. Later you will want to tie this to a compile-time flag so release builds do not pay the validation overhead.
What You Can Inspect Now
Before moving on, it is worth printing the adapter description to confirm you selected the right GPU. Add this just after dxgi.Init succeeds:
DXGI_ADAPTER_DESC1 desc = {};
dxgi.Adapter()->GetDesc1(&desc);
OutputDebugStringW(desc.Description);
OutputDebugStringW(L"\n");
OutputDebugStringW sends output to the Visual Studio Output window. You should see your GPU name — something like NVIDIA GeForce RTX 4070 or AMD Radeon RX 7900 XTX. If you see Microsoft Basic Render Driver it means no hardware adapter was found and the software fallback was used.
DXGI_FORMAT
One last thing worth noting even though we are not using it yet: DXGI_FORMAT is a DXGI enumeration that appears throughout D3D12. Every texture, every render target, every swap chain buffer has a format. Common ones:
| Format | Use |
|---|---|
DXGI_FORMAT_R8G8B8A8_UNORM | Standard 8-bit RGBA, the default swap chain format |
DXGI_FORMAT_R16G16B16A16_FLOAT | HDR render targets |
DXGI_FORMAT_D32_FLOAT | 32-bit depth buffer |
DXGI_FORMAT_R32G32B32_FLOAT | Vertex position data |
You will see DXGI_FORMAT in every resource description and pipeline state in D3D12. It lives in DXGI rather than D3D12 because the format system is shared with DXGI’s swap chain and output APIs.
Common Mistakes
If CreateDXGIFactory2 fails, check that dxgi.lib is listed in target_link_libraries. Missing this link produces an unresolved external symbol error at link time, not a compile error.
If SelectAdapter returns nullptr, check for WARP. Run this loop without the software-adapter skip to confirm at least one adapter exists:
Microsoft::WRL::ComPtr<IDXGIAdapter1> adapter;
for (UINT i = 0; factory_->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i)
{
DXGI_ADAPTER_DESC1 desc = {};
adapter->GetDesc1(&desc);
OutputDebugStringW(desc.Description);
OutputDebugStringW(L"\n");
}
If you only see Microsoft Basic Render Driver in that output, the machine does not have a hardware GPU visible to DXGI. This can happen inside virtual machines without GPU passthrough.
If QueryInterface for IDXGIFactory6 fails and you expected it to succeed, make sure you are targeting Windows 10 1803 or later in your project manifest and SDK version.
Quick Checkpoint
You are ready to move on if these ideas feel solid:
- DXGI handles adapter enumeration, swap chains, and display output. D3D12 handles rendering.
- The factory is the root DXGI object; every other DXGI object comes from it.
CreateDXGIFactory2is the correct function for DX12 programs.IDXGIFactory6::EnumAdapterByGpuPreferenceis the cleanest way to find the best GPU.- The software adapter (
DXGI_ADAPTER_FLAG_SOFTWARE) should be skipped in most cases. ComPtr<T>manages COM object lifetimes;IID_PPV_ARGScombines interface ID and output pointer.DXGI_FORMATis a shared enumeration that will appear in almost every D3D12 API call.- The factory is passed to swap chain creation later; we do not build the swap chain until after the D3D12 device exists.
What’s Next
Next we create the D3D12 device — the logical representation of the GPU. The device is created with D3D12CreateDevice, and it needs the adapter we selected here. After the device exists we can create a command queue, and after the command queue exists we can build the swap chain by passing both the queue and Window::Handle() to DXGI’s CreateSwapChainForHwnd.