Skip to content

Instantly share code, notes, and snippets.

@d7samurai
Last active March 24, 2025 03:25
Show Gist options
  • Save d7samurai/a8dc490c54a714f8385f512d95278e0b to your computer and use it in GitHub Desktop.
Save d7samurai/a8dc490c54a714f8385f512d95278e0b to your computer and use it in GitHub Desktop.
Minimal D3D11 bonus material: simple 2D rendering

Minimal D3D11 bonus material: simple 2D rendering

image

Plain 2D rendering in D3D11, without the distracting feature set of a complete sprite renderer and allowing arbitrarily placed triangle vertices with absolute pixel coordinate positioning (there's really no need for the sometimes confusing complication of a full-on projection matrix pipeline for UI and 2D games). Like the original Minimal D3D11, this one uses a canonical 1:1 TRIANGLELIST vertex buffer with input layout, so no fancy manual custom buffer fetching and in-shader instanced quad expansion here. As usual: complete, runnable single-function app. ~160 LOC. No modern C++, OOP or (other) obscuring cruft.

Still not plain enough? Here's an even simpler variant (no texturing or alpha blending).

As with all of these demos, remember to keep gpu.hlsl in the working directory of the executable.

#pragma comment(lib, "user32")
#pragma comment(lib, "d3d11")
#pragma comment(lib, "d3dcompiler")
///////////////////////////////////////////////////////////////////////////////////////////////////
#include <windows.h>
#include <d3d11.h>
#include <d3dcompiler.h>
///////////////////////////////////////////////////////////////////////////////////////////////////
#define TEXTURE_WIDTH 17
#define TEXTURE_HEIGHT 25
unsigned long long texturedata[] = // skull texture. 17 x 25 pixels, 8 bits per pixel (grayscale) for compactness
{
0x0000000000000000, 0x0000000000000000, 0xdd00000000000000, 0x00000000b2e0dede, 0xe1dc000000000000, 0x00afaeaeaec8c8c8,
0xc7e3000000000000, 0x89b09999c3ddddc3, 0xc7dd0000000000ae, 0x9b99afb0c6dbddc6, 0xc400000000af899b, 0xb0aeaec6c6dedcc6,
0xdd0000008a9c99af, 0xadadaec4dedcdcc4, 0x0000599b9aaeafaf, 0xadadc6c6ddddc6c6, 0x005c9a9aafafadae, 0xadacc6c3e0ddc600,
0x5c9d5ab0b0aeadad, 0xaddedcdcc6ae0000, 0x9f5ac4c4c7c6adad, 0x895e89ae5d00005c, 0x5b56578dc6aeaeb0, 0x18185c3300005a5a,
0x1818198cae8c1716, 0x195a330000305b31, 0x171617af5a331718, 0x8ec60000315a5b30, 0x165b8b8a58321718, 0xe000005b8c5a3132,
0x8a17dd8d5c188db1, 0x000089ad8a5a1658, 0x3219da888eaf8b5b, 0x00323034898b8d5b, 0x16aeae1719590000, 0x00301717af8d3089,
0xad5a32dc00000000, 0x005a1959ad8c8bb0, 0x8d198a0000000000, 0x58318b17e217dc5a, 0x8a8a000000000000, 0x5b3230185a5b2f18,
0xae00000000000033, 0xb02fdc30de18ddaf, 0x000000000000335a, 0xb0e1aeb1afafae00, 0x0000000000005daf, 0xadafb1afb0000000,
0x0000000000005b5c, 0x898b8b0000000000, 0x0000000000005a5c, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
};
///////////////////////////////////////////////////////////////////////////////////////////////////
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
WNDCLASSA wndclass = { 0, DefWindowProcA, 0, 0, 0, 0, 0, 0, 0, "d7" };
RegisterClassA(&wndclass);
HWND window = CreateWindowExA(0, "d7", 0, 0x91000000, 0, 0, 0, 0, 0, 0, 0, 0);
///////////////////////////////////////////////////////////////////////////////////////////////
D3D_FEATURE_LEVEL featurelevels[] = { D3D_FEATURE_LEVEL_11_0 };
DXGI_SWAP_CHAIN_DESC swapchaindesc = {};
swapchaindesc.BufferDesc.Format = DXGI_FORMAT_B8G8R8A8_UNORM; // non-srgb for simplicity here. see other minimal gists for srgb setup
swapchaindesc.SampleDesc.Count = 1;
swapchaindesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
swapchaindesc.BufferCount = 2;
swapchaindesc.OutputWindow = window;
swapchaindesc.Windowed = TRUE;
swapchaindesc.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD;
IDXGISwapChain* swapchain;
ID3D11Device* device;
ID3D11DeviceContext* devicecontext;
D3D11CreateDeviceAndSwapChain(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT, featurelevels, ARRAYSIZE(featurelevels), D3D11_SDK_VERSION, &swapchaindesc, &swapchain, &device, nullptr, &devicecontext);
swapchain->GetDesc(&swapchaindesc); // get actual dimensions
///////////////////////////////////////////////////////////////////////////////////////////////
ID3D11Texture2D* framebuffer;
swapchain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&framebuffer); // get the swapchain's buffer
ID3D11RenderTargetView* framebufferRTV;
device->CreateRenderTargetView(framebuffer, nullptr, &framebufferRTV); // and make it a render target [view]
///////////////////////////////////////////////////////////////////////////////////////////////
ID3DBlob* vertexshaderCSO;
D3DCompileFromFile(L"gpu.hlsl", 0, 0, "VsMain", "vs_5_0", 0, 0, &vertexshaderCSO, 0);
ID3D11VertexShader* vertexshader;
device->CreateVertexShader(vertexshaderCSO->GetBufferPointer(), vertexshaderCSO->GetBufferSize(), 0, &vertexshader);
D3D11_INPUT_ELEMENT_DESC inputelementdesc[] = // maps to vertexdesc struct in gpu.hlsl via semantic names ("POS", "TEX", "COL")
{
{ "POS", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // float2 position (x, y)
{ "TEX", 0, DXGI_FORMAT_R32G32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // float2 texcoord (u, v)
{ "COL", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 0, D3D11_APPEND_ALIGNED_ELEMENT, D3D11_INPUT_PER_VERTEX_DATA, 0 }, // float4 color (r, g, b, a)
};
ID3D11InputLayout* inputlayout;
device->CreateInputLayout(inputelementdesc, ARRAYSIZE(inputelementdesc), vertexshaderCSO->GetBufferPointer(), vertexshaderCSO->GetBufferSize(), &inputlayout);
///////////////////////////////////////////////////////////////////////////////////////////////
ID3DBlob* pixelshaderCSO;
D3DCompileFromFile(L"gpu.hlsl", 0, 0, "PsMain", "ps_5_0", 0, 0, &pixelshaderCSO, 0);
ID3D11PixelShader* pixelshader;
device->CreatePixelShader(pixelshaderCSO->GetBufferPointer(), pixelshaderCSO->GetBufferSize(), 0, &pixelshader);
///////////////////////////////////////////////////////////////////////////////////////////////
D3D11_RASTERIZER_DESC rasterizerdesc = { D3D11_FILL_SOLID, D3D11_CULL_NONE };
ID3D11RasterizerState* rasterizerstate;
device->CreateRasterizerState(&rasterizerdesc, &rasterizerstate);
///////////////////////////////////////////////////////////////////////////////////////////////
D3D11_SAMPLER_DESC samplerdesc = { D3D11_FILTER_MIN_MAG_MIP_POINT, D3D11_TEXTURE_ADDRESS_WRAP, D3D11_TEXTURE_ADDRESS_WRAP, D3D11_TEXTURE_ADDRESS_WRAP };
ID3D11SamplerState* samplerstate;
device->CreateSamplerState(&samplerdesc, &samplerstate);
///////////////////////////////////////////////////////////////////////////////////////////////
D3D11_BLEND_DESC blenddesc = { FALSE, FALSE, { TRUE, D3D11_BLEND_SRC_ALPHA , D3D11_BLEND_INV_SRC_ALPHA, D3D11_BLEND_OP_ADD, D3D11_BLEND_ZERO, D3D11_BLEND_ZERO, D3D11_BLEND_OP_ADD, D3D11_COLOR_WRITE_ENABLE_ALL } };
ID3D11BlendState* blendstate;
device->CreateBlendState(&blenddesc, &blendstate);
///////////////////////////////////////////////////////////////////////////////////////////////
float constants[2] = { 2.0f / swapchaindesc.BufferDesc.Width, -2.0f / swapchaindesc.BufferDesc.Height }; // precalc for simple screen coordinate transform in shader (instead of full-on projection matrix)
D3D11_BUFFER_DESC constantbufferdesc = {};
constantbufferdesc.ByteWidth = sizeof(constants) + 0xf & 0xfffffff0;
constantbufferdesc.Usage = D3D11_USAGE_IMMUTABLE;
constantbufferdesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
D3D11_SUBRESOURCE_DATA constantbufferSRD = { constants };
ID3D11Buffer* constantbuffer;
device->CreateBuffer(&constantbufferdesc, &constantbufferSRD, &constantbuffer);
///////////////////////////////////////////////////////////////////////////////////////////////
D3D11_TEXTURE2D_DESC texturedesc = {};
texturedesc.Width = TEXTURE_WIDTH;
texturedesc.Height = TEXTURE_HEIGHT;
texturedesc.MipLevels = 1;
texturedesc.ArraySize = 1;
texturedesc.Format = DXGI_FORMAT_R8_UNORM; // 8 bits per pixel
texturedesc.SampleDesc.Count = 1;
texturedesc.Usage = D3D11_USAGE_IMMUTABLE;
texturedesc.BindFlags = D3D11_BIND_SHADER_RESOURCE;
D3D11_SUBRESOURCE_DATA textureSRD = {};
textureSRD.pSysMem = texturedata;
textureSRD.SysMemPitch = TEXTURE_WIDTH * 1; // 1 byte per pixel (R8)
ID3D11Texture2D* texture;
device->CreateTexture2D(&texturedesc, &textureSRD, &texture);
ID3D11ShaderResourceView* textureSRV;
device->CreateShaderResourceView(texture, nullptr, &textureSRV);
///////////////////////////////////////////////////////////////////////////////////////////////
#define MAX_VERTICES 1024 // arbitrary limit
struct vertexdesc { float x, y, u, v, r, g, b, a; }; // float2 position, float2 texcoord, float4 color
///////////////////////////////////////////////////////////////////////////////////////////////
D3D11_BUFFER_DESC vertexbufferdesc = {};
vertexbufferdesc.ByteWidth = sizeof(vertexdesc) * MAX_VERTICES;
vertexbufferdesc.Usage = D3D11_USAGE_DYNAMIC; // updated every frame
vertexbufferdesc.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
vertexbufferdesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
ID3D11Buffer* vertexbuffer;
device->CreateBuffer(&vertexbufferdesc, nullptr, &vertexbuffer);
///////////////////////////////////////////////////////////////////////////////////////////////
FLOAT clearcolor[4] = { 0.1725f, 0.1725f, 0.1725f, 1.0f }; // RGBA
D3D11_VIEWPORT viewport = { 0, 0, (float)swapchaindesc.BufferDesc.Width, (float)swapchaindesc.BufferDesc.Height, 0, 1 };
UINT stride = sizeof(vertexdesc);
UINT offset = 0;
///////////////////////////////////////////////////////////////////////////////////////////////
vertexdesc* vertexbatch = (vertexdesc*)HeapAlloc(GetProcessHeap(), 0, sizeof(vertexdesc) * MAX_VERTICES); // per frame vertex batch (cpu-local buffer)
///////////////////////////////////////////////////////////////////////////////////////////////
while (true)
{
MSG msg;
while (PeekMessageA(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (msg.message == WM_KEYDOWN) return 0; // PRESS ANY KEY TO EXIT
DispatchMessageA(&msg);
}
///////////////////////////////////////////////////////////////////////////////////////////
int vertexcount = 0; // start a new vertex batch every frame
// triangle 1:
{
// vertex 1
vertexdesc vertex;
vertex.x = 100; // screen coordinates in pixels (xy)
vertex.y = 100;
vertex.u = 0.0f; // texture coordinates (uv)
vertex.v = 0.0f;
vertex.r = 1.0f; // color (rgba)
vertex.g = 1.0f;
vertex.b = 1.0f;
vertex.a = 1.0f;
vertexbatch[vertexcount++] = vertex; // add vertex to batch
// vertex 2
vertexbatch[vertexcount++] = { 252, 300, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f }; // x, y, u, v, r, g, b, a
// vertex 3
vertexbatch[vertexcount++] = { 100, 300, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
}
// triangle 2:
vertexbatch[vertexcount++] = { 100, 100, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f };
vertexbatch[vertexcount++] = { 252, 100, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f };
vertexbatch[vertexcount++] = { 252, 300, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f };
// triangle 3:
vertexbatch[vertexcount++] = { 296, 104, 0.0f, 0.0f, 0.803f, 0.000f, 0.290f, 1.0f };
vertexbatch[vertexcount++] = { 448, 304, 1.0f, 1.0f, 0.952f, 0.439f, 0.105f, 1.0f };
vertexbatch[vertexcount++] = { 296, 304, 0.0f, 1.0f, 0.988f, 0.721f, 0.023f, 1.0f };
// triangle 4:
vertexbatch[vertexcount++] = { 304, 96, 0.0f, 0.0f, 0.011f, 0.698f, 0.286f, 1.0f };
vertexbatch[vertexcount++] = { 456, 96, 1.0f, 0.0f, 0.000f, 0.541f, 0.819f, 1.0f };
vertexbatch[vertexcount++] = { 456, 296, 1.0f, 1.0f, 0.392f, 0.376f, 0.670f, 1.0f };
///////////////////////////////////////////////////////////////////////////////////////////
D3D11_MAPPED_SUBRESOURCE vertexbufferMSR;
devicecontext->Map(vertexbuffer, 0, D3D11_MAP_WRITE_DISCARD, 0, &vertexbufferMSR);
{
memcpy(vertexbufferMSR.pData, vertexbatch, sizeof(vertexdesc) * vertexcount); // send vertex batch to gpu
}
devicecontext->Unmap(vertexbuffer, 0);
///////////////////////////////////////////////////////////////////////////////////////////
devicecontext->ClearRenderTargetView(framebufferRTV, clearcolor);
devicecontext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST); // 3 vertices per triangle
devicecontext->IASetInputLayout(inputlayout);
devicecontext->IASetVertexBuffers(0, 1, &vertexbuffer, &stride, &offset);
devicecontext->VSSetShader(vertexshader, nullptr, 0);
devicecontext->VSSetConstantBuffers(0, 1, &constantbuffer);
devicecontext->RSSetViewports(1, &viewport);
devicecontext->RSSetState(rasterizerstate);
devicecontext->PSSetShader(pixelshader, nullptr, 0);
devicecontext->PSSetShaderResources(0, 1, &textureSRV);
devicecontext->PSSetSamplers(0, 1, &samplerstate);
devicecontext->OMSetRenderTargets(1, &framebufferRTV, nullptr);
devicecontext->OMSetBlendState(blendstate, nullptr, 0xffffffff);
///////////////////////////////////////////////////////////////////////////////////////////
devicecontext->Draw(vertexcount, 0);
///////////////////////////////////////////////////////////////////////////////////////////
swapchain->Present(1, 0);
}
}
cbuffer constants : register(b0)
{
float2 rn_screensize; // 2 / width, -2 / height
}
struct vertexdesc
{
float2 position : POS;
float2 texcoord : TEX;
float4 color : COL;
};
struct pixeldesc
{
float4 position : SV_POSITION;
float2 texcoord : TEX;
float4 color : COL;
};
Texture2D mytexture : register(t0);
SamplerState mysampler : register(s0);
pixeldesc VsMain(vertexdesc vertex)
{
pixeldesc output;
output.position = float4(vertex.position * rn_screensize - float2(1, -1), 0, 1);
output.texcoord = vertex.texcoord;
output.color = vertex.color;
return output;
}
float4 PsMain(pixeldesc pixel) : SV_TARGET
{
return float4(mytexture.Sample(mysampler, pixel.texcoord).rrr * pixel.color.rgb, pixel.color.a); // .rrr because texture is red channel only (DXGI_FORMAT_R8_UNORM)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment