Win32 API programming with C - Getting started with Direct3D

Ciprian FusaCiprian Fusa
5 min read

Introduction

For the purpose of this article we will focus on Direct3D version 11 as Direct3D 12 represents a significant departure from the Direct3D 11 programming model.

Direct3D 11, a component of Microsoft's DirectX API, serves as a robust framework for developing graphics-heavy applications, such as game games and simulations, specifically for Windows environments. It offers developers a low-level GPU interface, facilitating the rendering of both 2D and 3D graphics.

We will look at creating a minimal Direct3D 11 application that clears the screen to a solid color. We start by including the necessary headers and defining some global variables:

#include <windows.h>
#include <d3d11.h>

#pragma comment (lib, "d3d11.lib")
#pragma comment (lib, "dxguid.lib")

// global variables
ID3D11Device* device;
ID3D11DeviceContext* deviceContext;
IDXGISwapChain* swapChain;
ID3D11RenderTargetView* renderTarget;

Device

A Direct3D device is responsible for the allocation and destruction of objects, rendering primitives, and interaction with both the graphics driver and hardware. In Direct3D 11, the functionality of a device is divided into two objects: the device object, tasked with resource creation, and the device-context object, which handles rendering tasks.

Every application requires at least one device. To establish a device, we use the D3D11CreateDeviceAndSwapChain function, selecting one of the hardware drivers available on our system by specifying the driver type with the D3D_DRIVER_TYPE flag.

Device context

A device context defines the environment for a device's operation, facilitating the configuration of pipeline state and the issuance of rendering commands with the device's resources. Direct3D 11 offers two kinds of device contexts: one for real-time rendering and another for batched, or deferred, rendering.

The immediate context executes renderings directly through the driver, and each device is equipped with a single immediate context capable of fetching data from the GPU.

Swap Chain

A swap chain consists of a series of buffers designated for presenting frames to the user. Whenever an application introduces a new frame for display, the foremost buffer in the swap chain replaces the currently displayed buffer. This action is commonly referred to as swapping or flipping.

Back buffer

The back buffer is a rectangle of memory that an application can directly write to. The back buffer is never directly displayed on the monitor.

Here is our swap chain definition, we use one back buffer that outputs to our window:

DXGI_SWAP_CHAIN_DESC getSwapChainDescription(HWND hWnd)
{
    DXGI_SWAP_CHAIN_DESC scd;
    ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));

    scd.BufferCount = 1;                          
    scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    scd.BufferDesc.Width = 800;
    scd.BufferDesc.Height = 600;
    scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    scd.OutputWindow = hWnd;
    scd.SampleDesc.Count = 4;
    scd.Windowed = TRUE;
    scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

    return scd;
}

Next we create a device, get a pointer to our back buffer and create a render target pointing to our back buffer.

// creates a device and a swap chain used for rendering
D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, NULL, 0,
    D3D11_SDK_VERSION, &swapchainDesc, &swapChain, &device, NULL, &deviceContext);

// pointer to swap chain's back buffer
ID3D11Texture2D* pBackBuffer;
swapChain->lpVtbl->GetBuffer(swapChain, 0, &IID_ID3D11Texture2D, (LPVOID*)&pBackBuffer);

// creates a render target pointing to the back buffer
device->lpVtbl->CreateRenderTargetView(device, pBackBuffer, NULL, &renderTarget);
pBackBuffer->lpVtbl->Release(pBackBuffer);

Graphics pipeline

The Direct3D 11 programmable pipeline is designed for generating graphics for real-time gaming applications. The following diagram shows the data flow from input to output through each of the programmable stages.

Next we use the device context to bind to the different stages of the graphics pipeline:

// bind viewport to the rasterizer stage
deviceContext->lpVtbl->RSSetViewports(deviceContext, 1, &viewport);
 // bind to output-merger stage
 deviceContext->lpVtbl->OMSetRenderTargets(deviceContext, 1, &backbuffer, NULL);

Now we can start rendering frames. The following code creates a window and initializes Direct3D 11 to clear the screen to a blue color each frame. It is a basic foundation for more complex Direct3D applications. From here, you can extend the application to handle input, load and display 3D models, implement lighting and shading, and much more.

#include <windows.h>
#include <d3d11.h>

#pragma comment (lib, "d3d11.lib")
#pragma comment (lib, "dxguid.lib")

// global variables
ID3D11Device* device;
ID3D11DeviceContext* deviceContext;
IDXGISwapChain* swapChain;
ID3D11RenderTargetView* renderTarget;

// function declarations
void InitD3D(HWND hWnd);
void CleanUp();
void RenderFrame();

LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
    HWND hWnd;
    WNDCLASSEX wc;

    ZeroMemory(&wc, sizeof(WNDCLASSEX));

    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WindowProc;
    wc.hInstance = hInstance;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)COLOR_WINDOW;
    wc.lpszClassName = "WindowClass";

    RegisterClassEx(&wc);

    hWnd = CreateWindowEx(0, "WindowClass", "Direct3D 11", WS_OVERLAPPEDWINDOW,
        300, 300, 800, 600, NULL, NULL, hInstance, NULL);

    ShowWindow(hWnd, nCmdShow);

    // Initialize Direct3D
    InitD3D(hWnd);

    // Main message loop
    MSG msg;
    while (TRUE) {
        if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
            TranslateMessage(&msg);
            DispatchMessage(&msg);

            if (msg.message == WM_QUIT)
                break;
        }
        else {
            RenderFrame();
        }
    }

    return msg.wParam;
}

// Window procedure function
LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
    switch (message) {
        case WM_DESTROY:
            CleanUp();
            PostQuitMessage(0);
            return 0;
    }

    return DefWindowProc(hWnd, message, wParam, lParam);
}

DXGI_SWAP_CHAIN_DESC getSwapChainDescription(HWND hWnd)
{
    DXGI_SWAP_CHAIN_DESC scd;
    ZeroMemory(&scd, sizeof(DXGI_SWAP_CHAIN_DESC));

    scd.BufferCount = 1;                          
    scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    scd.BufferDesc.Width = 800;
    scd.BufferDesc.Height = 600;
    scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
    scd.OutputWindow = hWnd;
    scd.SampleDesc.Count = 4;
    scd.Windowed = TRUE;
    scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;

    return scd;
}

D3D11_VIEWPORT getViewPort()
{
    D3D11_VIEWPORT viewport;
    ZeroMemory(&viewport, sizeof(D3D11_VIEWPORT));

    viewport.TopLeftX = 0;
    viewport.TopLeftY = 0;
    viewport.Width = 800;
    viewport.Height = 600;

    return viewport;
}

void InitD3D(HWND hWnd) {
    DXGI_SWAP_CHAIN_DESC swapchainDesc = getSwapChainDescription(hWnd);

#pragma region Input-assembler stage
    // creates a device and a swap chain used for rendering
    D3D11CreateDeviceAndSwapChain(NULL, D3D_DRIVER_TYPE_HARDWARE, NULL, 0, NULL, 0,
        D3D11_SDK_VERSION, &swapchainDesc, &swapChain, &device, NULL, &deviceContext);

    // pointer to swap chain's back buffer
    ID3D11Texture2D* pBackBuffer;
    swapChain->lpVtbl->GetBuffer(swapChain, 0, &IID_ID3D11Texture2D, (LPVOID*)&pBackBuffer);

    // creates a render target pointing to the back buffer
    device->lpVtbl->CreateRenderTargetView(device, pBackBuffer, NULL, &renderTarget);
    pBackBuffer->lpVtbl->Release(pBackBuffer);
#pragma endregion

#pragma region Rasterizer stage
    // bind viewport to the rasterizer stage
    D3D11_VIEWPORT viewport = getViewPort();
    deviceContext->lpVtbl->RSSetViewports(deviceContext, 1, &viewport);
#pragma endregion

#pragma region Output-Merger stage
    // bind to output-merger stage
    deviceContext->lpVtbl->OMSetRenderTargets(deviceContext, 1, &renderTarget, NULL);
#pragma endregion
}

void RenderFrame() {
    const FLOAT ClearColour[4] = { 0.0f, 0.2f, 0.4f, 1.0f };
    deviceContext->lpVtbl->ClearRenderTargetView(deviceContext, renderTarget, ClearColour);
    swapChain->lpVtbl->Present(swapChain, 0, 0);
}

void CleanUp() {
    renderTarget->lpVtbl->Release(renderTarget);
    swapChain->lpVtbl->Release(swapChain);
    device->lpVtbl->Release(device);
    deviceContext->lpVtbl->Release(deviceContext);
}

More info on Direct3D 11 graphics here: https://learn.microsoft.com/en-us/windows/win32/direct3d11/atoc-dx-graphics-direct3d-11

0
Subscribe to my newsletter

Read articles from Ciprian Fusa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Ciprian Fusa
Ciprian Fusa

I am a .NET Developer and Consultant with over 15 years of experience, I specialize in crafting scalable, high-performance applications that drive business growth and innovation.