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 graphics pipeline: a chain of stages, each taking the previous one's output as its input. Roughly, the first half works out where things land on the screen, and the second half works out what colour every covered pixel becomes. In this chapter we walk the pipeline once, end to end, and get a triangle out of it.

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 shaders, and in Direct3D they are written in the High-Level Shading Language (HLSL), the counterpart of OpenGL's GLSL. The next chapter is about HLSL; here we only need two tiny shaders.

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.

Inputassembler Vertexshader Hull, domaintessellator Geometryshader Rasterizer Pixelshader Outputmerger Render target(the back buffer) optional stages, dashed

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 clip space. For now we skip every transformation and give the positions directly in the coordinates the screen uses at the end, normalized device coordinates (NDC): x runs from -1 on the left of the window to 1 on the right, y from -1 at the bottom to 1 at the top, and anything outside is clipped away.

Normalized device coordinates, x and y from -1 to 1 (LearnOpenGL's diagram)

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 viewport we give it, which here is simply the whole window.

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 buffer: a block of GPU memory that the pipeline can read from. In OpenGL that is a vertex buffer object, filled by glBufferData. In Direct3D a buffer is created in one call, with its contents passed in up front:

; 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 Create method on the device, get a pointer back. Two fields deserve a word.

invoke create_buffer, D3D11_BIND_VERTEX_BUFFER, ADDR triangle_vertices, triangle_vertices_size
mov triangle_vb, rax
A struct in assembly is only a layout: 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 mem_zero first. 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 semantics, names that tie a value to the world outside the function:

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 D3DCompile from 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 target: the kind of shader and the shader model, 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.

Like most Direct3D calls, D3DCompile returns an HRESULT in 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: invoke puts the rest on the stack, above the 32 bytes of shadow space every Win64 call reserves.

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 glVertexAttribPointer, and keeps the answer in a vertex array object. Direct3D says it with an input layout, built from a list of elements:

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

Three floats per vertex, tightly packed: stride 12 bytes, offset 0 (LearnOpenGL's diagram)

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 device context holds the pipeline's current state; each call below sets one piece of it, and stays set until changed.

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 primitive topology, the GL_TRIANGLES argument of glDrawArrays, is state here too: every three vertices make one triangle. IASetVertexBuffers binds our buffer to slot 0, with the stride of 12 bytes and an offset of 0, and Draw draws 3 vertices starting at the first.

This sits in the render loop from Hello Window, between clearing the back buffer and presenting it. And there it is:

The orange triangle, and the rectangle beside it

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 indices, and they go in an index buffer (OpenGL's element buffer object):

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 Draw for DrawIndexed:

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 glPolygonMode; Direct3D puts the fill mode in the rasterizer state, an immutable object made from a description like everything else. The lesson makes a second state for wireframe:

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 same shapes in wireframe; the rectangle's diagonal shows the two triangles

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.

The default rasterizer state in Direct3D culls clockwise triangles. These lessons instead set FrontCounterClockwise and turn culling off at startup, so triangles are listed counter-clockwise as in LearnOpenGL and nothing disappears unexpectedly. Face culling turns it back on.

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

Exercises

  1. Draw two triangles side by side with a single Draw call, by adding three more vertices to the array.
  2. Draw the same two triangles from two separate vertex buffers, binding each in turn.
  3. Add a second pixel shader entry point, ps_yellow, to the same .hlsl file, and draw one of the triangles with it. Only one more compile_shader and one more CreatePixelShader are needed.
Credit: this chapter follows LearnOpenGL's Hello Triangle by Joey de Vries, rewritten for Direct3D 11 and x64 assembly. LearnOpenGL's text and code are licensed CC BY-NC 4.0, and so is this page; the wording, code and screenshots here are new, and diagrams marked as LearnOpenGL's are its own.