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 January 23, 2024

Table of contents

Introduction

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

Leave a comment

You can use Markdown to format your comment.