Hello Triangle
Everything Direct3D draws starts out as points in 3D space, and everything it shows ends up as a grid of coloured pixels in a window. Turning one into the other is the job of the
Each stage does one narrow job, and does it for a great many vertices or pixels at once, which is why GPUs carry thousands of small cores. A few stages run programs we write ourselves. Those programs are
Here are Direct3D 11's stages in order. The ones in green run our shaders; the grey ones are fixed hardware we configure with state objects.
- The
input assembler reads our vertex data out of buffers and groups it intoprimitives : points, lines or triangles, as we ask. - The
vertex shader runs once per vertex. Its main duty is to say where that vertex lands, as a position inclip space . - Tessellation and the
geometry shader can add or reshape primitives. They are optional, and we leave them empty until the geometry shader chapter. - The
rasterizer clips primitives to the visible region and works out which pixels each one covers. - The
pixel shader runs once for every covered pixel and returns its colour. OpenGL calls this stage the fragment shader; it is where most of the interesting work in later chapters happens. - The
output merger does the depth and stencil tests and blending, then writes the result into the render target.
OpenGL ships default shaders for nothing in modern profiles, and neither does Direct3D: a vertex shader and a pixel shader are the minimum before anything is drawn. Let's write both.
Vertex input
The vertex shader has to say where each vertex lands, and it says it in
This picture is LearnOpenGL's and holds for Direct3D too, except in depth: Direct3D's z runs from 0 (near) to 1 (far), where OpenGL's runs from -1 to 1. That only starts to matter once we build a projection matrix in Coordinate Systems. The rasterizer then maps NDC to pixels with the
Three corners of a triangle, z left at 0, written as one flat array of floats:
align 4
triangle_vertices real4 -0.9, -0.5, 0.0
real4 -0.1, -0.5, 0.0
real4 -0.5, 0.5, 0.0
triangle_vertices_size equ $ - triangle_vertices
This array lives in our program's memory. The GPU wants it in its own, so we copy it into a
; rcx: bind flags, rdx: data, r8: size -> rax: the buffer
;
; An immutable buffer: filled once at creation, never written by the CPU again.
align 16
create_buffer proc bind:qword, data:qword, data_size:qword
local desc:D3D11_BUFFER_DESC, init:D3D11_SUBRESOURCE_DATA, buffer:qword
invoke mem_zero, ADDR desc, sizeof D3D11_BUFFER_DESC
mov eax, dword ptr data_size
mov desc.ByteWidth, eax
mov desc.Usage, D3D11_USAGE_IMMUTABLE
mov eax, dword ptr bind
mov desc.BindFlags, eax
invoke mem_zero, ADDR init, sizeof D3D11_SUBRESOURCE_DATA
mov rax, data
mov init.pSysMem, rax
vcall device, ID3D11Device_CreateBuffer, ADDR desc, ADDR init, ADDR buffer
mov rax, buffer
ret
create_buffer endp
This is the pattern for nearly every Direct3D object: fill a _DESC struct, call a
- BindFlags says what the buffer will be used for. D3D11_BIND_VERTEX_BUFFER here; the same function makes index buffers and, later, constant buffers.
- Usage says how often the data changes. It plays the part of OpenGL's GL_STATIC_DRAW and friends, but it is a promise the driver holds us to rather than a hint. D3D11_USAGE_IMMUTABLE means the data is given at creation and never written again, so the driver can put it wherever the GPU reads it fastest. Advanced Data uses D3D11_USAGE_DYNAMIC for a buffer rewritten every frame.
invoke create_buffer, D3D11_BIND_VERTEX_BUFFER, ADDR triangle_vertices, triangle_vertices_size
mov triangle_vb, rax
desc.ByteWidth is the address of desc plus the field's offset, and mov desc.ByteWidth, eax writes four bytes there. The descriptions come from d3d11.inc, generated from the Windows SDK's C headers, so every field sits where the C compiler would put it. A local starts out as whatever was on the stack, hence vcall device, ID3D11Device_CreateBuffer, ... calls a COM method: through the object's table of functions, with the object as the hidden first argument, where C++ would write device->CreateBuffer(...).
Vertex shader
The smallest useful vertex shader takes the position and hands it on. Ours lives in hello_triangle.hlsl, next to the program:
float4 vs_main(float3 position : POSITION) : SV_Position
{
return float4(position, 1.0);
}
An HLSL shader is an ordinary function, and we choose its name; vs_main is our convention for the vertex shader's entry point. The interesting parts are after the colons. These are
POSITIONis our own name for the input. Any name works; it only has to match the name we give the attribute when we describe the vertex layout below. It is how HLSL gets its inputs where GLSL writeslayout (location = 0) in vec3 aPos.SV_Positionis asystem value : a name the pipeline itself understands. Returning afloat4markedSV_Positionis what GLSL does by assigning gl_Position.
The fourth component, w, is set to 1. Why it exists, and what dividing by it does, comes with perspective in Coordinate Systems.
Compiling a shader
OpenGL compiles GLSL inside the driver while the program runs. Direct3D splits the job in two: a compiler that turns HLSL into bytecode, and the device, which turns bytecode into a shader object. The compiler is d3dcompiler.dll. It can also run ahead of time, as fxc does, but compiling at startup keeps each lesson to its source files and lets us edit a shader without a separate build step.
; rcx: path, rdx: source, r8: size, r9: entry point, [rsp+32]: target -> rax: the compiled code
align 16
compile_shader proc path:qword, source:qword, source_size:qword, entry:qword, target:qword
local bytecode:qword, errors:qword
mov bytecode, 0
mov errors, 0
invoke D3DCompile, source, source_size, path, 0, D3D_COMPILE_STANDARD_FILE_INCLUDE, entry, target, 0, 0, ADDR bytecode, ADDR errors
test eax, eax
jns compiled
vcall errors, ID3D10Blob_GetBufferPointer
print "ERROR::SHADER::COMPILATION_FAILED\n%s\n", rax
invoke ExitProcess, 1
compiled:
mov rax, bytecode
ret
compile_shader endp
We pass the source text, the file name (used only in error messages), the entry point and a vs_5_0 for a vertex shader at Direct3D 11's level. The result comes back in an ID3DBlob, a plain buffer of bytes. On failure the errors blob holds the compiler's messages, with file and line, which we print before giving up.
eax: negative means failure, so test eax, eax sets the sign flag and jns jumps past the error when it is clear. That is all C's FAILED() macro does. Eleven arguments are more than the four registers hold: Pixel shader
The pixel shader decides the colour of each pixel the triangle covers. Colours are four floats, red, green, blue and alpha, each from 0 to 1. Ours returns one orange for everything:
float4 ps_main() : SV_Target
{
return float4(1.0, 0.5, 0.2, 1.0);
}
SV_Target is the system value for "the colour written to render target 0". GLSL declares an out vec4 for this; HLSL marks the return value. Both entry points sit in the same file, so we compile it twice with different entry points and targets:
invoke compile_shader, path, source, source_size, "vs_main", "vs_5_0"
mov vs_code, rax
invoke compile_shader, path, source, source_size, "ps_main", "ps_5_0"
mov ps_code, rax
Shader objects
OpenGL links its shaders into one program object, and checks there that the outputs of one stage match the inputs of the next. Direct3D has no program object. Each stage is its own object, created from its bytecode, and bound to its own slot in the pipeline:
vcall vs_code, ID3D10Blob_GetBufferPointer
mov rbx, rax
vcall vs_code, ID3D10Blob_GetBufferSize
mov rsi, rax
vcall device, ID3D11Device_CreateVertexShader, rbx, rsi, 0, ADDR vertex_shader
The bytecode's address and size are kept in rbx and rsi, two of the registers a call must leave as it found them, so they are still there for the input layout below. The pixel shader is made the same way from ps_code:
vcall device, ID3D11Device_CreatePixelShader, rbx, rax, 0, ADDR pixel_shader
Mixing and matching is free: any vertex shader can be bound with any pixel shader, so long as what one outputs is what the other reads. Here the pixel shader reads nothing at all, so there is nothing to match yet.
Describing the vertices
The buffer is only bytes. The vertex shader wants a float3 called POSITION. Something has to say how one becomes the other: where each attribute starts in a vertex, what type it is, and which shader input it feeds. OpenGL says it with
; One attribute: three floats, read as POSITION by the vertex shader.
semantic_position byte "POSITION", 0
align 8
vertex_layout D3D11_INPUT_ELEMENT_DESC <offset semantic_position, 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0>
Each element names the semantic it feeds and the format of the data: DXGI_FORMAT_R32G32B32_FLOAT is three 32-bit floats. The structure is filled in where it is declared, its fields in order between angle brackets: the semantic's name (by address), semantic index 0, the format, vertex buffer slot 0, starting at byte 0 of the vertex, and per-vertex rather than per-instance data (that last one returns in Instancing).
As in LearnOpenGL's picture, the positions are tightly packed: each vertex is 12 bytes, and the position is at offset 0. Where OpenGL puts the stride into the attribute pointer, Direct3D gives it when the buffer is bound, as we will see in the render loop.
The layout is created against the vertex shader's bytecode:
; The layout is created against the compiled vertex shader, which is the one place D3D checks
; that the two agree.
vcall device, ID3D11Device_CreateInputLayout, ADDR vertex_layout, 1, rbx, rsi, ADDR input_layout
That is the step OpenGL's linker did for us. Misspell POSITION on either side and creation fails here, at startup, rather than drawing garbage. The layout does not mention any buffer; it only describes vertices. Which buffer they come from is decided when drawing, so one layout serves every mesh with the same vertex format.
The triangle we've all been waiting for
Everything is created. What remains is, every frame, to tell the pipeline what to use and to ask it to draw. A Direct3D
vcall context, ID3D11DeviceContext_IASetInputLayout, input_layout
vcall context, ID3D11DeviceContext_IASetPrimitiveTopology, D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST
vcall context, ID3D11DeviceContext_VSSetShader, vertex_shader, 0, 0
vcall context, ID3D11DeviceContext_PSSetShader, pixel_shader, 0, 0
vcall context, ID3D11DeviceContext_IASetVertexBuffers, 0, 1, ADDR triangle_vb, ADDR stride, ADDR offset_
vcall context, ID3D11DeviceContext_Draw, 3, 0
The prefixes give the stage each call configures: IA the input assembler, VS the vertex shader, PS the pixel shader, and further on RS for the rasterizer and OM for the output merger. The
This sits in the render loop from Hello Window, between clearing the back buffer and presenting it. And there it is:
The rectangle to its right is the next section's.
Index buffers
A rectangle is two triangles. Listed as triangles that is six vertices, two of them stored twice, because the triangles share a diagonal. On a rectangle that is a small waste; on a model where every vertex is shared by about six triangles, it is most of the data. The fix is to store each vertex once, and draw from a second list that says which vertices make each triangle. Those are
rectangle_vertices real4 0.1, -0.5, 0.0 ; bottom left
real4 0.9, -0.5, 0.0 ; bottom right
real4 0.9, 0.5, 0.0 ; top right
real4 0.1, 0.5, 0.0 ; top left
rectangle_vertices_size equ $ - rectangle_vertices
rectangle_indices dword 0, 1, 2, 0, 2, 3
rectangle_indices_size equ $ - rectangle_indices
Four vertices, six indices: triangle 0-1-2 is the bottom right half, 0-2-3 the top left. The index buffer is made by the same function with a different bind flag, and drawing swaps
invoke create_buffer, D3D11_BIND_INDEX_BUFFER, ADDR rectangle_indices, rectangle_indices_size
mov rectangle_ib, rax
vcall context, ID3D11DeviceContext_IASetVertexBuffers, 0, 1, ADDR rectangle_vb, ADDR stride, ADDR offset_
vcall context, ID3D11DeviceContext_IASetIndexBuffer, rectangle_ib, DXGI_FORMAT_R32_UINT, 0
vcall context, ID3D11DeviceContext_DrawIndexed, 6, 0, 0
DXGI_FORMAT_R32_UINT says each index is a 32-bit unsigned integer, matching the dword array; 16-bit indices would use DXGI_FORMAT_R16_UINT and take half the space.
The input layout is reused unchanged, since the rectangle's vertices have the same format as the triangle's. In OpenGL each shape would get its own VAO, recording its buffers alongside its layout. In Direct3D the layout and the buffers are bound separately, so switching shapes is just binding the other buffers.
Wireframe mode
To see the two triangles, draw only their edges. OpenGL has
mov raster_desc.FillMode, D3D11_FILL_WIREFRAME
vcall device, ID3D11Device_CreateRasterizerState, ADDR raster_desc, ADDR wire_state
and Tab chooses which one to bind each frame:
mov rax, raster_state
cmp wireframe, 0
je solid
mov rax, wire_state
solid:
vcall context, ID3D11DeviceContext_RSSetState, rax
The diagonal across the rectangle is where its two triangles meet. Making state objects once and only binding them in the loop is the Direct3D way throughout: the expensive checking happens at creation, and switching between states is cheap.
If you got here with both shapes on screen, you have done the hardest part of graphics programming: the first triangle. Everything from here on adds to this, one piece at a time. The full program is here, and its shaders here.
Additional resources
- Graphics pipeline: Microsoft's overview of every Direct3D 11 stage.
- Getting started with the input-assembler stage: buffers, input layouts and topologies in more depth.
- LearnOpenGL's Hello Triangle: the OpenGL version of this chapter, with its VBOs, VAOs and EBOs.
Exercises
- Draw two triangles side by side with a single
Draw call, by adding three more vertices to the array. - Draw the same two triangles from two separate vertex buffers, binding each in turn.
- Add a second pixel shader entry point,
ps_yellow, to the same.hlslfile, and draw one of the triangles with it. Only one morecompile_shader and one moreCreatePixelShader are needed.