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:
- a
SwapChainclass that owns theIDXGISwapChain3and both back buffer resources, - an understanding of the flip model and why DX12 requires it,
- tearing support checked and enabled if the display allows it,
- and back buffer pointers ready for render target view creation.
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:
DXGI_SWAP_EFFECT_FLIP_DISCARD— the GPU may discard the previous back buffer contents after the flip. Use this.DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL— the GPU preserves back buffer contents across flips. Costs more memory bandwidth for no benefit in a typical renderer.
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:
- Check whether the adapter and display support it.
- Add
DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARINGto the swap chain flags. - Pass
DXGI_PRESENT_ALLOW_TEARINGtoPresentinstead 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:
- The swap chain owns the back buffer textures. You write to one while the other is displayed.
- DX12 requires
DXGI_SWAP_EFFECT_FLIP_DISCARD. Sequential and non-flip modes are DX11 era. GetCurrentBackBufferIndexreturns which buffer to render to this frame.MakeWindowAssociationwithDXGI_MWA_NO_ALT_ENTERprevents DXGI from intercepting Alt+Enter.- Back buffer resources are owned by the swap chain; you only borrow the
ID3D12Resource*. - Tearing must be declared in both the swap chain flags and the
Presentcall — they must match.
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.