DXGI: Factory and Adapter

Why DXGI exists, how it separates presentation from rendering, and how to create a factory, enumerate GPU adapters, and pick the right hardware device.

Early Draft Work in progress — GitHub links coming soon.

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:

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:

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:

  1. Startup: enumerate adapters, pick the right GPU.
  2. Presentation: create a swap chain, call Present once 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:

HeaderKey addition
dxgi.hOriginal (DX10 era)
dxgi1_2.hStereo, CreateSwapChainForHwnd
dxgi1_3.hWaitable swap chains, overlays
dxgi1_4.hIDXGIFactory4, required for DX12 device wrapping
dxgi1_5.hHDR, variable refresh rate queries
dxgi1_6.hEnumAdapterByGpuPreference 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:

FormatUse
DXGI_FORMAT_R8G8B8A8_UNORMStandard 8-bit RGBA, the default swap chain format
DXGI_FORMAT_R16G16B16A16_FLOATHDR render targets
DXGI_FORMAT_D32_FLOAT32-bit depth buffer
DXGI_FORMAT_R32G32B32_FLOATVertex 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:

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.