Direct3D 11 - Constant Buffer Tutorial (C & Win32)
How to move an object on the screen using a transformation matrix and a D3D11 constant buffer.
Updated Jan 23, 2024

Constant buffers allow you to pass constant data, such as matrices or colors to shaders during runtime.

shader.hlsl

Add this to your shader:

cbuffer constants : register(b0) {
	row_major float4x4 model;
};
  • cbuffer declares a constant buffer.
  • register(b0) associates the buffer with a specific location in GPU memory. Later, we set it using VSSetConstantBuffers() before drawing so that the shader can access it during rendering.
  • The row_major qualifier is used to inform the shader that the matrix in memory is laid out in a row-major order. In HLSL, the default memory layout for matrices is column-major, signifying that the elements of each column are stored consecutively in memory. However, I want to specify and pass the model matrix as a basic C array, where the data is laid out in memory in a row-major order (rows are stored consecutively in memory):
{
    1.0f, 0.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f, 0.0f,
    1.0f, 0.0f, 0.0f, 1.0f
}

Multiply the vertex position by the model matrix:

VS_Output vs_main(VS_Input input) {
	VS_Output output;
	output.position = float4(input.position, 1.0f);
	output.position = mul(output.position, model); // here
	output.uv = input.uv;
	return output;
};

main.c

Create a struct that matches the constants buffer in the shader:

typedef struct { 
    F32 m[4][4]; 
} Matrix;

typedef struct {
    Matrix model;
} D3D11Constants;
  • Matrix represents a 4x4 matrix.
  • D3D11Constants specifies the constants that will be sent to shaders during rendering.

Create the constant buffer:

ID3D11Buffer *constant_buffer;

D3D11_BUFFER_DESC constant_buffer_desc = {
    .ByteWidth = sizeof(D3D11Constants),
    .Usage = D3D11_USAGE_DYNAMIC,
    .BindFlags = D3D11_BIND_CONSTANT_BUFFER,
    .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE,
};

HRESULT hr =
    ID3D11Device1_CreateBuffer(device, &constant_buffer_desc, NULL, &constant_buffer);
  • D3D11_BUFFER_DESC describes the buffer.
  • D3D11_USAGE_DYNAMIC indicates that the buffer will be frequently updated by the CPU.
  • D3D11_BIND_CONSTANT_BUFFER specifies that the buffer will be bound as a constant buffer.
  • D3D11_CPU_ACCESS_WRITE indicates that the CPU is allowed to update the buffer.

Set the constant buffer and map it before drawing:

ID3D11DeviceContext1_VSSetConstantBuffers(context, 0, 1, &constant_buffer);

D3D11_MAPPED_SUBRESOURCE mapped_subresource;
        
ID3D11DeviceContext1_Map(context, 
                            (ID3D11Resource *)constant_buffer, 
                            0, D3D11_MAP_WRITE_DISCARD, 
                            0, &mapped_subresource);
D3D11Constants *constants = (D3D11Constants *)mapped_subresource.pData;

constants->model = (Matrix){
    1.0f, 0.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f, 0.0f,
    1.0f, 0.0f, 0.0f, 1.0f
};

ID3D11DeviceContext1_Unmap(context, (ID3D11Resource *)constant_buffer, 0);
  • ID3D11DeviceContext1_VSSetConstantBuffers sets the vertex shader's constant buffers.
  • ID3D11DeviceContext1_Map maps the constant buffer for CPU access.
  • D3D11_MAP_WRITE_DISCARD mapping type indicates that the previous contents of the resource can be discarded.
  • D3D11Constants *constants = (D3D11Constants *)mapped_subresource.pData creates a pointer to the mapped data. The data in the constant buffer will be accessed through this pointer.
  • constants->model = (Matrix){ ... }; updates the data in the constant buffer.
  • ID3D11DeviceContext1_Unmap unmaps the constant buffer, allowing the GPU to access it again.

Result with an identity matrix:

constants->model = (Matrix){
    1.0f, 0.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f, 0.0f,
    0.0f, 0.0f, 0.0f, 1.0f
};

Moving everything 1 unit to the right:

constants->model = (Matrix){
    1.0f, 0.0f, 0.0f, 0.0f,
    0.0f, 1.0f, 0.0f, 0.0f,
    0.0f, 0.0f, 1.0f, 0.0f,
    1.0f, 0.0f, 0.0f, 1.0f
};

You can download this image from here.

Complete example

main.c

// cl main.c /Zi /nologo /link /SUBSYSTEM:windows

#define WIN32_LEAN_AND_MEAN
#define _USE_MATH_DEFINES
#define COBJMACROS

#include <stdio.h>
#include <stdint.h>
#include <windows.h>
#include <d3d11_1.h>
#include <dxgidebug.h>
#include <Dxgi1_3.h>
#include <assert.h>
#include <float.h>
#include <stddef.h>
#include <intrin.h>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

#define Assert(c) do { if (!(c)) __debugbreak(); } while (0)
#define AssertHR(hr) Assert(SUCCEEDED(hr))

#pragma comment (lib, "user32")
#pragma comment (lib, "d3d11")
#pragma comment (lib, "d3dcompiler")
#pragma comment (lib, "dxguid")
#pragma comment (lib, "gdi32")
#pragma comment (lib, "dxgi")

// Types.

typedef uint32_t U32;
typedef float    F32;

typedef struct { 
    F32 m[4][4]; 
} Matrix;

typedef struct {
    Matrix model;
} D3D11Constants;

// Globals.

ID3D11Buffer *constant_buffer;

U32 client_width;
U32 client_height;

ID3D11Device *device;
ID3D11DeviceContext *context;
ID3D11RenderTargetView *render_target_view;
ID3D11InputLayout *input_layout;
ID3D11SamplerState *sampler_state;
IDXGISwapChain1 *swap_chain;
ID3D11ShaderResourceView *shader_resource_view;
D3D11_VIEWPORT viewport;

LRESULT CALLBACK 
win_proc(HWND window, UINT msg, WPARAM w_param, LPARAM l_param) {
    switch(msg) {
        case WM_DESTROY: {
            PostQuitMessage(0);
        } break;
        case WM_KEYDOWN: {
            switch(w_param) {
                case 'O': {
                    DestroyWindow(window);
                } break;
            }
        } break;
        default: {
            return DefWindowProcW(window, msg, w_param, l_param);
        }
    }
    return 0;
}

int WINAPI 
WinMain(HINSTANCE instance, HINSTANCE p_instance, LPSTR cmd, int cmd_show) {
    
    // Window creation.
    
    WNDCLASSW wc = { 
        .lpszClassName = L"MyWindowClass",
        .lpfnWndProc = win_proc,
        .hInstance = instance,
        .hCursor = LoadCursor(NULL, IDC_CROSS),
    };
    ATOM atom = RegisterClassW(&wc);
    Assert(atom && "Failed to register a window");
    
    HWND window = CreateWindowW(wc.lpszClassName, 
                                L"Window", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
                                client_width, client_height,
                                NULL, NULL, instance, NULL);
    Assert(window && "Failed to create a window");
    ShowWindow(window, cmd_show);
    
    // Fullscreen.
    
    DWORD window_style = GetWindowLong(window, GWL_STYLE);
    MONITORINFO mi = { sizeof(mi) };
    if(GetMonitorInfo(MonitorFromWindow(window, MONITOR_DEFAULTTOPRIMARY), &mi)) {
        client_width = mi.rcMonitor.right - mi.rcMonitor.left;
        client_height = mi.rcMonitor.bottom - mi.rcMonitor.top;
        SetWindowLong(window, GWL_STYLE, window_style & ~WS_OVERLAPPEDWINDOW);
        SetWindowPos(window, HWND_TOP,
                     mi.rcMonitor.left, mi.rcMonitor.top,
                     client_width,
                     client_height,
                     SWP_NOOWNERZORDER | SWP_FRAMECHANGED);
    }
    
    // Viewport.
    
    viewport.Width = client_width;
    viewport.Height = client_height;
    viewport.MaxDepth = 1.0f;
    
    // Device & Context.
    
    UINT creation_flags = 0;
#ifndef NDEBUG
    creation_flags = D3D11_CREATE_DEVICE_DEBUG;
#endif
    
    D3D_FEATURE_LEVEL feature_levels[] = { D3D_FEATURE_LEVEL_11_0 };
    
    HRESULT hr = D3D11CreateDevice(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, creation_flags, feature_levels, ARRAYSIZE(feature_levels), D3D11_SDK_VERSION, &device, NULL, &context);
    
    AssertHR(hr);
    
    // Debug.
    
#ifndef NDEBUG
    {
        ID3D11InfoQueue *info;
        ID3D11Device_QueryInterface(device, &IID_ID3D11InfoQueue, (void**)&info);
        ID3D11InfoQueue_SetBreakOnSeverity(info, D3D11_MESSAGE_SEVERITY_CORRUPTION, TRUE);
        ID3D11InfoQueue_SetBreakOnSeverity(info, D3D11_MESSAGE_SEVERITY_ERROR, TRUE);
        ID3D11InfoQueue_Release(info);
    }
    {
        IDXGIInfoQueue *dxgi_info;
        hr = DXGIGetDebugInterface1(0, &IID_IDXGIInfoQueue, (void**)&dxgi_info);
        AssertHR(hr);
        IDXGIInfoQueue_SetBreakOnSeverity(dxgi_info, DXGI_DEBUG_ALL, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_CORRUPTION, TRUE);
        IDXGIInfoQueue_SetBreakOnSeverity(dxgi_info, DXGI_DEBUG_ALL, DXGI_INFO_QUEUE_MESSAGE_SEVERITY_ERROR, TRUE);
        IDXGIInfoQueue_Release(dxgi_info);
    }
#endif
    
    // Swap chain.
    
    IDXGIDevice *dxgi_device;
    hr = ID3D11Device_QueryInterface(device, &IID_IDXGIDevice, (void **)&dxgi_device);
    AssertHR(hr);
    
    IDXGIAdapter *dxgi_adapter;
    hr = IDXGIDevice_GetAdapter(dxgi_device, &dxgi_adapter);
    AssertHR(hr);
    
    IDXGIFactory2 *dxgi_factory;
    hr = IDXGIAdapter_GetParent(dxgi_adapter, &IID_IDXGIFactory2, (void **)&dxgi_factory);
    AssertHR(hr);
    
    DXGI_SWAP_CHAIN_DESC1 swap_chain_desc = { 
        .Width = 0,
        .Height = 0,
        .Format = DXGI_FORMAT_R8G8B8A8_UNORM,
        .SampleDesc.Count = 1,
        .SampleDesc.Quality = 0,
        .BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT,
        .BufferCount = 2,
        .Scaling = DXGI_SCALING_NONE,
        .SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD,
        .Flags = 0, 
    };
    
    hr = IDXGIFactory2_CreateSwapChainForHwnd(dxgi_factory, (IUnknown *)device, window, &swap_chain_desc, 0, 0, &swap_chain);
    AssertHR(hr);
    
    IDXGIFactory_MakeWindowAssociation(dxgi_factory, window, DXGI_MWA_NO_ALT_ENTER);
    
    IDXGIFactory2_Release(dxgi_factory);
    IDXGIAdapter_Release(dxgi_adapter);
    ID3D11Device_Release(dxgi_device);
    
    // Render target view.
    
    ID3D11Texture2D *back_buffer;
    hr = IDXGISwapChain1_GetBuffer(swap_chain, 0, &IID_ID3D11Texture2D, (void **)&back_buffer);
    AssertHR(hr);
    
    hr = ID3D11Device_CreateRenderTargetView(device, (ID3D11Resource *)back_buffer, NULL, &render_target_view);
    AssertHR(hr);
    
    ID3D11Texture2D_Release(back_buffer);
    
    // Shader.
    
    ID3D11VertexShader *vs;
    ID3D11PixelShader *ps;
    ID3D10Blob *vs_blob;
    ID3D10Blob *ps_blob;
    ID3D11InputLayout *il;
    ID3D11Buffer *buffer;
    
    hr = D3DCompileFromFile(L"shader.hlsl", NULL, NULL, "vs_main", "vs_5_0", NULL, NULL, &vs_blob, NULL);
    AssertHR(hr);
    
    hr = D3DCompileFromFile(L"shader.hlsl", NULL, NULL, "ps_main", "ps_5_0", NULL, NULL, &ps_blob, NULL);
    AssertHR(hr);
    
    hr = ID3D11Device1_CreateVertexShader(
                                          device, ID3D10Blob_GetBufferPointer(vs_blob), ID3D10Blob_GetBufferSize(vs_blob), 
                                          0, &vs);
    AssertHR(hr);
    
    hr = ID3D11Device1_CreatePixelShader(
                                         device, ID3D10Blob_GetBufferPointer(ps_blob), ID3D10Blob_GetBufferSize(ps_blob), 
                                         0, &ps);
    AssertHR(hr);
    
    // Input layout.
    
    D3D11_INPUT_ELEMENT_DESC
        input_element_desc[] = {
        {"POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0,
            D3D11_INPUT_PER_VERTEX_DATA, 0},
        {"UV", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT,
            D3D11_INPUT_PER_VERTEX_DATA, 0},
    };
    
    
    hr = ID3D11Device1_CreateInputLayout(device, input_element_desc, ARRAYSIZE(input_element_desc), ID3D10Blob_GetBufferPointer(vs_blob), ID3D10Blob_GetBufferSize(vs_blob), &input_layout);
    AssertHR(hr);
    
    // Mesh.
    
    F32 vertices[] = {
        -0.5f, -0.5f, 0.0f, 0.0f, 1.0f,
        -0.5f, 0.5f, 0.0f, 0.0f, 0.0f,
        0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
        -0.5f, -0.5f, 0.0f, 0.0f, 1.0f,
        0.5f, 0.5f, 0.0f, 1.0f, 0.0f,
        0.5f, -0.5f, 0.0f, 1.0f, 1.0f,
    };
    
    U32 stride = 5 * sizeof(F32);
    U32 num_vertices = sizeof(vertices) / stride;
    U32 offset = 0;
    
    D3D11_BUFFER_DESC buffer_desc = { 
        sizeof(vertices), 
        D3D11_USAGE_DEFAULT, 
        D3D11_BIND_VERTEX_BUFFER, 
        0, 0, 0 
    };
    
    D3D11_SUBRESOURCE_DATA initial_data = { vertices };
    
    ID3D11Device1_CreateBuffer(device, &buffer_desc, &initial_data, &buffer);
    
    // Texture.
    
    U32 image_width = 0;
    U32 image_height = 0;
    U32 image_channels = 0;
    U32 image_desired_channels = 4;
    
    unsigned char *image_data = stbi_load("image.png", &image_width, &image_height, &image_channels, image_desired_channels);
    Assert(image_data);
    
    U32 image_pitch = image_width * 4;
    
    D3D11_TEXTURE2D_DESC texture_desc = { 
        .Width = image_width,
        .Height = image_height,
        .MipLevels = 1,
        .ArraySize = 1,
        .Format = DXGI_FORMAT_R8G8B8A8_UNORM,
        .SampleDesc.Count = 1,
        .SampleDesc.Quality = 0,
        .Usage = D3D11_USAGE_IMMUTABLE,
        .BindFlags = D3D11_BIND_SHADER_RESOURCE,
    };
    
    D3D11_SUBRESOURCE_DATA texture_subresource_data = {
        .pSysMem = image_data,
        .SysMemPitch = image_pitch,
    };
    
    ID3D11Texture2D *texture;
    
    hr = ID3D11Device1_CreateTexture2D(device, &texture_desc, &texture_subresource_data, &texture);
    AssertHR(hr);
    
    free(image_data);
    
    hr = ID3D11Device1_CreateShaderResourceView(device, (ID3D11Resource *)texture, NULL, &shader_resource_view);
    AssertHR(hr);
    
    // Sampler.
    
    D3D11_SAMPLER_DESC sampler_desc = { 
        .Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR,
        .AddressU = D3D11_TEXTURE_ADDRESS_CLAMP,
        .AddressV = D3D11_TEXTURE_ADDRESS_CLAMP,
        .AddressW = D3D11_TEXTURE_ADDRESS_CLAMP,
        .MipLODBias = 0.0f,
        .MaxAnisotropy = 1,
        .ComparisonFunc = D3D11_COMPARISON_NEVER,
        .BorderColor[0] = 1.0f,
        .BorderColor[1] = 1.0f,
        .BorderColor[2] = 1.0f,
        .BorderColor[3] = 1.0f,
        .MinLOD = -FLT_MAX,
        .MaxLOD = FLT_MAX,
    };
    
    hr =  ID3D11Device1_CreateSamplerState(device, &sampler_desc, &sampler_state);
    AssertHR(hr);
    
    // Constant buffer.
    
    D3D11_BUFFER_DESC constant_buffer_desc = {
        .ByteWidth = sizeof(D3D11Constants),
        .Usage = D3D11_USAGE_DYNAMIC,
        .BindFlags = D3D11_BIND_CONSTANT_BUFFER,
        .CPUAccessFlags = D3D11_CPU_ACCESS_WRITE,
    };
    
    hr =
        ID3D11Device1_CreateBuffer(device, &constant_buffer_desc, NULL, &constant_buffer);
    AssertHR(hr);
    
    // Loop.
    
    for(;;) {
        MSG msg;
        if(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            if(msg.message == WM_QUIT) break;
            TranslateMessage(&msg);
            DispatchMessage(&msg);
            continue;
        }
        
        F32 color[] = { 0.2f, 0.2f, 0.2f, 1.0f};
        
        ID3D11DeviceContext1_ClearRenderTargetView(context, render_target_view, color);
        ID3D11DeviceContext1_RSSetViewports(context, 1, &viewport);
        ID3D11DeviceContext1_OMSetRenderTargets(context, 1, &render_target_view, NULL);
        
        ID3D11DeviceContext1_PSSetShaderResources(context, 0, 1, &shader_resource_view);
        ID3D11DeviceContext1_PSSetSamplers(context, 0, 1, &sampler_state);
        ID3D11DeviceContext1_VSSetConstantBuffers(context, 0, 1, &constant_buffer);
        
        ID3D11DeviceContext1_IASetInputLayout(context, input_layout);
        ID3D11DeviceContext1_VSSetShader(context, vs, 0, 0);
        ID3D11DeviceContext1_PSSetShader(context, ps, 0, 0);
        ID3D11DeviceContext1_IASetPrimitiveTopology(context, D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
        ID3D11DeviceContext1_IASetVertexBuffers(context, 0, 1, &buffer, &stride, &offset);
        
        // Constants.
        
        D3D11_MAPPED_SUBRESOURCE mapped_subresource;
        
        ID3D11DeviceContext1_Map(context, 
                                 (ID3D11Resource *)constant_buffer, 
                                 0, D3D11_MAP_WRITE_DISCARD, 
                                 0, &mapped_subresource);
        D3D11Constants *constants = (D3D11Constants *)mapped_subresource.pData;
        
        constants->model = (Matrix){
            1.0f, 0.0f, 0.0f, 0.0f,
            0.0f, 1.0f, 0.0f, 0.0f,
            0.0f, 0.0f, 1.0f, 0.0f,
            1.0f, 0.0f, 0.0f, 1.0f
        };
        
        ID3D11DeviceContext1_Unmap(context, (ID3D11Resource *)constant_buffer, 0);
        
        // Draw.
        
        ID3D11DeviceContext1_Draw(context, num_vertices, 0);
        
        // Swap.
        
        IDXGISwapChain1_Present(swap_chain, 1, 0); 
        
    }
    return 0;
}

shader.hlsl

cbuffer constants : register(b0) {
	row_major float4x4 model;
};

struct VS_Input {
	float3 position: POSITION;
	float2 uv: UV;
};

struct VS_Output {
	float4 position: SV_POSITION;
	float2 uv: UV;
};

/*
static matrix model = {
	{1,0,0,0},
	{0,1,0,0},
	{0,0,1,0},
	{1,0,0,1}
};
*/

VS_Output vs_main(VS_Input input) {
	VS_Output output;
	output.position = float4(input.position, 1.0f);
	output.position = mul(output.position, model);
	output.uv = input.uv;
	return output;
};

Texture2D my_texture;
SamplerState my_sampler;

float4 ps_main(VS_Output input): SV_TARGET {
	return my_texture.Sample(my_sampler, input.uv);
};

Video