Win32 API programming with C - Getting started with Direct3D
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
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.