The Swap Chain

Create the DXGI swap chain using the factory, command queue, and window handle. Understand double buffering, the flip model, tearing support, and how to retrieve back buffer pointers.

Early Draft Work in progress — GitHub links coming soon.

In the previous chapter we created the command queue. We now have the three things DXGI requires to build a swap chain: the factory from DXGIContext, the HWND from Window, and the command queue from CommandQueue.

By the end of this chapter you will have:

What the Swap Chain Does

The swap chain owns a small set of textures called back buffers. Your renderer writes to the current back buffer. When the frame is ready, Present flips the back buffer to the screen while the other buffer becomes the new render target.

This is double buffering. With two buffers, the display always has a complete frame to show while the GPU is writing the next one. Triple buffering adds a third, allowing the GPU to keep working even when the display is mid-refresh.

We will use two buffers throughout this series. It is enough to avoid tearing and is simpler to synchronise.

The Flip Model

DX12 only supports the flip presentation model. There is no DXGI_SWAP_EFFECT_DISCARD or DXGI_SWAP_EFFECT_SEQUENTIAL as in older DX11 code. You must use one of:

Use DXGI_SWAP_EFFECT_FLIP_DISCARD. Never write code that reads from the back buffer after it has been presented.

Tearing and Variable Refresh Rate

On displays that support variable refresh rate (G-Sync, FreeSync), you can present at any time without synchronising to the display refresh cycle. This eliminates frame pacing jitter at the cost of visible tearing if frames arrive at an irregular rate. Most games expose a “V-Sync off” option that uses tearing.

To use tearing:

  1. Check whether the adapter and display support it.
  2. Add DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING to the swap chain flags.
  3. Pass DXGI_PRESENT_ALLOW_TEARING to Present instead of syncing to the refresh.

We will check for tearing support at creation time and store whether it is available. The render loop chapter will use the flag when presenting.

BOOL tearingSupported = FALSE;

Microsoft::WRL::ComPtr<IDXGIFactory5> factory5;
if (SUCCEEDED(factory->QueryInterface(IID_PPV_ARGS(&factory5))))
{
    factory5->CheckFeatureSupport(
        DXGI_FEATURE_PRESENT_ALLOW_TEARING,
        &tearingSupported,
        sizeof(tearingSupported));
}

Describing the Swap Chain

DXGI_SWAP_CHAIN_DESC1 desc = {};
desc.Width       = static_cast<UINT>(width);
desc.Height      = static_cast<UINT>(height);
desc.Format      = DXGI_FORMAT_R8G8B8A8_UNORM;
desc.BufferCount = FrameCount;
desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
desc.SwapEffect  = DXGI_SWAP_EFFECT_FLIP_DISCARD;
desc.SampleDesc  = { 1, 0 };
desc.Flags       = tearingSupported ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0;

SampleDesc = { 1, 0 } means no multisampling. The flip model does not support multisample swap chains — you resolve MSAA to a non-multisampled texture before presenting.

DXGI_FORMAT_R8G8B8A8_UNORM is the standard format for an 8-bit-per-channel RGBA swap chain. It is the most broadly supported and is correct for SDR output. HDR swap chains use DXGI_FORMAT_R16G16B16A16_FLOAT or DXGI_FORMAT_R10G10B10A2_UNORM, which require additional display checks.

FrameCount is a constant we define once and use everywhere. Two buffers is double buffering:

static constexpr UINT FrameCount = 2;

Creating the Swap Chain

CreateSwapChainForHwnd takes the factory, the command queue, the HWND, the descriptor, and two optional arguments for full-screen output and restricted output:

Microsoft::WRL::ComPtr<IDXGISwapChain1> swapChain1;

factory->CreateSwapChainForHwnd(
    queue,                  // ID3D12CommandQueue*
    hwnd,                   // HWND from Window::Handle()
    &desc,
    nullptr,                // no full-screen desc
    nullptr,                // no output restriction
    &swapChain1
);

The function returns an IDXGISwapChain1. We need IDXGISwapChain3 for GetCurrentBackBufferIndex, which tells us which back buffer to render to each frame. Cast it:

Microsoft::WRL::ComPtr<IDXGISwapChain3> swapChain;
swapChain1.As(&swapChain);

ComPtr::As is the typed QueryInterface. It sets the target ComPtr and returns an HRESULT.

After creation, prevent DXGI from handling Alt+Enter itself — it tries to go full screen using the old exclusive mode, which conflicts with the flip model:

factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);

Getting the Back Buffers

The swap chain owns the back buffer textures. Retrieve a pointer to each one:

Microsoft::WRL::ComPtr<ID3D12Resource> backBuffers[FrameCount];

for (UINT i = 0; i < FrameCount; ++i)
{
    swapChain->GetBuffer(i, IID_PPV_ARGS(&backBuffers[i]));
}

These ID3D12Resource pointers are what we will create render target views for in the next chapter. The swap chain owns the resource lifetime — do not release them before destroying the swap chain.

Wrapping in a Class

Add SwapChain.h and SwapChain.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   ← new
        SwapChain.h     ← new

SwapChain.h

#pragma once

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

static constexpr UINT FrameCount = 2;

class SwapChain
{
public:
    bool Init(IDXGIFactory4* factory,
              ID3D12CommandQueue* queue,
              HWND hwnd,
              int width, int height);

    IDXGISwapChain3*  Get()                       const { return swapChain_.Get(); }
    ID3D12Resource*   BackBuffer(UINT index)       const { return backBuffers_[index].Get(); }
    UINT              CurrentBackBufferIndex()     const { return swapChain_->GetCurrentBackBufferIndex(); }
    bool              TearingSupported()           const { return tearingSupported_; }

private:
    static bool CheckTearingSupport(IDXGIFactory4* factory);

    Microsoft::WRL::ComPtr<IDXGISwapChain3>  swapChain_;
    Microsoft::WRL::ComPtr<ID3D12Resource>   backBuffers_[FrameCount];
    bool                                     tearingSupported_ = false;
};

SwapChain.cpp

#include "SwapChain.h"

bool SwapChain::Init(IDXGIFactory4* factory,
                     ID3D12CommandQueue* queue,
                     HWND hwnd,
                     int width, int height)
{
    tearingSupported_ = CheckTearingSupport(factory);

    DXGI_SWAP_CHAIN_DESC1 desc = {};
    desc.Width       = static_cast<UINT>(width);
    desc.Height      = static_cast<UINT>(height);
    desc.Format      = DXGI_FORMAT_R8G8B8A8_UNORM;
    desc.BufferCount = FrameCount;
    desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    desc.SwapEffect  = DXGI_SWAP_EFFECT_FLIP_DISCARD;
    desc.SampleDesc  = { 1, 0 };
    desc.Flags       = tearingSupported_ ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0;

    Microsoft::WRL::ComPtr<IDXGISwapChain1> swapChain1;
    if (FAILED(factory->CreateSwapChainForHwnd(
            queue, hwnd, &desc, nullptr, nullptr, &swapChain1)))
    {
        return false;
    }

    factory->MakeWindowAssociation(hwnd, DXGI_MWA_NO_ALT_ENTER);

    if (FAILED(swapChain1.As(&swapChain_)))
    {
        return false;
    }

    for (UINT i = 0; i < FrameCount; ++i)
    {
        if (FAILED(swapChain_->GetBuffer(i, IID_PPV_ARGS(&backBuffers_[i]))))
        {
            return false;
        }
    }

    return true;
}

bool SwapChain::CheckTearingSupport(IDXGIFactory4* factory)
{
    BOOL supported = FALSE;

    Microsoft::WRL::ComPtr<IDXGIFactory5> factory5;
    if (SUCCEEDED(factory->QueryInterface(IID_PPV_ARGS(&factory5))))
    {
        factory5->CheckFeatureSupport(
            DXGI_FEATURE_PRESENT_ALLOW_TEARING,
            &supported,
            sizeof(supported));
    }

    return supported == TRUE;
}

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
)

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"

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;
    }

    return RunRenderLoop();
}

Common Mistakes

If CreateSwapChainForHwnd returns DXGI_ERROR_INVALID_CALL, the most common cause is a null command queue or null HWND. Check that the window was fully created before initialising the swap chain, and that the command queue was successfully created before being passed here.

If the window appears to flicker white before the first frame, you may be missing MakeWindowAssociation. DXGI tries to paint the window while it owns it.

If you add DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING to the flags but present without DXGI_PRESENT_ALLOW_TEARING, the debug layer will report an error. The flag and the present mode must match.

If the back buffer format does not match what you use when creating render target views in the next chapter, validation will report a format mismatch. Keep DXGI_FORMAT_R8G8B8A8_UNORM consistent across both.

Quick Checkpoint

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

What’s Next

Next we create descriptor heaps and render target views. The swap chain gave us ID3D12Resource pointers for the back buffers, but the GPU cannot write to a resource directly — it needs a descriptor that describes the format and dimensionality of the render target. A small descriptor heap will hold one render target view per back buffer.