Direct3D 11

Before we write any code, it helps to know what we are talking to. Direct3D 11 is Microsoft's 3D graphics API: a set of functions and objects through which a program hands geometry, images and small GPU programs to the graphics card, and gets pictures back. It is one part of DirectX, the family of Windows multimedia APIs, and it runs on Windows (and, in a close variant, on Xbox). In this chapter we look at what kind of thing Direct3D is, how it is organised, and where it differs in spirit from OpenGL, which is what LearnOpenGL teaches.

OpenGL is a specification. The Khronos Group writes down what every function must do, and each GPU vendor ships its own library that implements it. Direct3D is organised differently. Microsoft writes the API and ships the part your program links against: a runtime, d3d11.dll, that comes with Windows. Below the runtime sits the driver, written by the GPU vendor (NVIDIA, AMD, Intel and others), which turns what the runtime asks for into commands for that particular chip. The runtime validates our calls, tracks resources and talks to the driver through an interface Microsoft also defines.

our programhello_window.exe Direct3D 11 runtime and DXGId3d11.dll, dxgi.dll: part of Windows GPU driverfrom the GPU's vendor the GPU written by Microsoft written by the vendor

This split has practical consequences. The API behaves the same on every card because there is one runtime, and Microsoft runs conformance tests that a driver must pass before Windows will certify it. When something does go wrong on one machine and not another, the driver is still the usual suspect, so updating graphics drivers remains good advice. For reference material there are two sources: Microsoft's online documentation, which describes every function and structure, and the Direct3D 11 functional specification, which Microsoft has published and which spells out exactly what each stage must compute. Both are linked at the end of this chapter.

Direct3D 11 and its neighbours

Direct3D has had many versions, and two turning points matter for us. Up to Direct3D 9, the API offered a fixed-function pipeline: lighting, texturing and fog were built in, switched on and tuned with settings, and the programmer had little say in how they were computed. That is the equivalent of OpenGL's old immediate mode. Direct3D 10 removed all of it. From then on, nothing is drawn unless we supply the shaders, exactly as in core-profile OpenGL. Direct3D 11 kept that model and added tessellation, compute shaders and multithreaded command recording.

The second turning point is Direct3D 12. It is lower level again: the program manages GPU memory, synchronisation between CPU and GPU, and the recording and submission of command lists itself. That buys control and performance, at the price of a great deal of code before the first triangle. Direct3D 11 still does those jobs for us. The runtime and driver decide where resources live, notice when a texture we are about to read is still being written, and keep the CPU from overwriting data the GPU has not finished with.

That makes Direct3D 11 the counterpart of OpenGL 3.3 core, the version LearnOpenGL uses: modern in the sense that matters (programmable shaders, buffers, render targets, no fixed function), and still forgiving enough to learn on. Everything we build here carries over to Direct3D 12 or Vulkan; only the bookkeeping around it grows.

Feature levels

One Direct3D 11 runtime has to work on GPUs of very different ages. It handles this with feature levels: named sets of hardware capabilities, from D3D_FEATURE_LEVEL_9_1 for very old hardware up through D3D_FEATURE_LEVEL_11_0 and beyond. When we create a device we list the levels we are prepared to accept, and get back the highest one the hardware supports. Our lessons ask for exactly one:

mov feature_level, D3D_FEATURE_LEVEL_11_0

A list of one is a single dword, a local of the procedure that creates the device, and its address is what device creation is given. Level 11_0 guarantees shader model 5.0, which is what our shaders are compiled for, along with every feature the chapters use. Any GPU made in the last decade or so has it. If a machine does not, device creation fails at startup with a clear message rather than misbehaving later.

Optional features

OpenGL grows through extensions: a vendor ships a new feature under a name like GL_ARB_something, programs check whether the driver advertises it, and successful extensions are folded into later versions. Direct3D has no such mechanism. New features arrive with new versions of the API (11.1, 11.2 and so on), as new interfaces such as ID3D11Device1 that a program asks for at runtime. Capabilities that vary within a feature level, such as whether a given texture format can be rendered to or blended, are queried with CheckFeatureSupport and CheckFormatSupport. Vendors that want to expose something beyond the API do it through their own libraries, outside Direct3D.

For our purposes none of this comes up: feature level 11_0 covers every chapter, and we never need to ask.

The device and the context

OpenGL presents itself as one big state machine, the context: a collection of current settings that every drawing call reads. We change a setting, and the next draw uses it. Direct3D 11 splits that role between two objects.

A third object completes the set. The swap chain (IDXGISwapChain) owns the images that are shown in our window, and its Present method puts the latest one on screen. It belongs to DXGI, the DirectX Graphics Infrastructure, a lower layer shared by Direct3D 10, 11 and 12 that deals with GPUs, monitors, swap chains and pixel formats. This is why formats are spelled DXGI_FORMAT_R8G8B8A8_UNORM rather than with a D3D11_ prefix.

deviceID3D11Device buffers, textures, views,shaders, state objectsmade once, at startup device contextID3D11DeviceContext swap chainIDXGISwapChain pipeline stages, GPUdraws into the back buffer Create... ...Set... Draw Present

Keeping creation and use apart is the single most useful thing to know about the shape of a Direct3D program. All the expensive work, checking that a description makes sense and turning it into something the hardware understands, happens when an object is created. Drawing then only binds finished objects, which is cheap. Every lesson follows that order: create everything before the render loop, and inside the loop only bind and draw.

Objects

OpenGL refers to its objects by integer names. We generate a name, bind it to a target on the context, and then the functions that configure that target edit whichever object is bound there. Roughly:

// OpenGL: edit an object by binding it first
glGenSomething(1, &id);
glBindSomething(GL_SOME_TARGET, id);
glSomethingParameter(GL_SOME_TARGET, GL_OPTION, value);   // edits whatever is bound
glBindSomething(GL_SOME_TARGET, 0);

Direct3D does without the bind-to-edit step. An object is described completely in a plain structure, a _DESC, and created from that description in one call; we get back a pointer to the finished object. Here is a real one from the lessons, the rasterizer state that the Hello Triangle chapter discusses:

invoke mem_zero, ADDR raster_desc, sizeof D3D11_RASTERIZER_DESC
mov raster_desc.FillMode, D3D11_FILL_SOLID
mov raster_desc.CullMode, D3D11_CULL_NONE
mov raster_desc.FrontCounterClockwise, 1
mov raster_desc.DepthClipEnable, 1
vcall device, ID3D11Device_CreateRasterizerState, ADDR raster_desc, ADDR raster_state
vcall context, ID3D11DeviceContext_RSSetState, raster_state

The description is a local structure, zeroed first so that every field we don't set means "the default", then filled in field by field. The device creates; the context binds. State objects such as this one are immutable: to draw with a different setting we create a second object up front and bind that one instead, never edit the first. Resources (buffers and textures) are different in that their contents can be updated, but their size, format and intended use are fixed at creation too.

COM

Direct3D's objects are COM objects, the Component Object Model that most Windows system APIs use. For us that means three things.

In C++ a COM method call looks like an ordinary method call, device->CreateBuffer(...). Assembly has no methods, but it doesn't need them: what an interface pointer points to is laid out exactly. The object starts with a pointer to its table, the table is an array of 8-byte function addresses, and method number N is at byte offset 8 × N. Calling a method is loading the table, loading the address at the method's offset, and calling it with the object as the first argument, in rcx. This project's includes/macros.asm does that in one macro:

; A COM object is a pointer to a pointer to its vtable, an array of function pointers. Method N is
; at byte offset 8 * N, and d3d11.inc names every offset: ID3D11Device_CreateBuffer is 24. The
; object itself is always the hidden first argument, `this`, in rcx.
;
;     vcall device, ID3D11Device_CreateBuffer, ADDR desc, ADDR init, ADDR buffer
; ...
vcall macro object:req, method:req, args:VARARG
    mov r11, object
    mov r10, [r11]
    invoke qword ptr [r10 + method], r11, args
endm

The offsets come from includes/d3d11.inc, which is generated from the Windows SDK's own headers, so ID3D11Device_CreateBuffer is simply a constant, 24: the fourth entry, right after IUnknown's three. The same file holds every structure, constant and GUID the lessons use. So whenever you see vcall device, ID3D11Device_CreateBuffer, ... in these chapters, read it as device->CreateBuffer(...). The names are long, but they have one advantage: every call says exactly which interface it belongs to.

No function loader is needed. OpenGL programs on Windows have to look up most GL functions at runtime (LearnOpenGL uses GLAD for this) because the system's opengl32.dll only exports a very old version of the API. Direct3D exports what it has: we link against d3d11.lib and invoke D3D11CreateDeviceAndSwapChain directly, and every other function is a method on an object it returns, reached with vcall.

Resources and views

One more kind of object deserves an early mention, because it has no direct OpenGL counterpart. A resource is a block of GPU memory: a buffer or a texture. The pipeline does not use resources directly; it uses views of them, which say how a stage should interpret the memory. A render target view lets the output merger draw into a texture, a shader resource view lets a shader read it, and a depth-stencil view lets it serve as a depth buffer. The very first program creates a render target view of the swap chain's back buffer so that it can clear it, and Textures creates shader resource views so that shaders can sample images. Views come into their own in Framebuffers, where one texture is drawn into through one view and read through another.

Let's get started

That is the shape of the API: a runtime from Microsoft over a driver from the vendor, a device that creates immutable, fully described objects, a context that binds them and draws, and a swap chain that puts the result on screen, all reached through COM interfaces. If not all of it has settled yet, it will once we use it; every one of these objects appears in the next two programs. Next we set up the tools that build those programs.

Additional resources

Exercises

  1. The device-creation call in hello_window.asm passes 0 for the parameter that receives the feature level actually obtained (the one before ADDR context). Add a second dword local, pass its address there instead, and print it in hexadecimal with print "%x\n", .... (11_0 is 0xb000.)
  2. Turn on the debug layer by passing D3D11_CREATE_DEVICE_DEBUG as the flags argument (the second 0 after the driver type), then run the program under a debugger and watch its output window. Now delete the line that sets swap_desc.SampleDesc.Count and run it again: device creation fails, and the debug layer says why. (The debug layer needs Windows' optional "Graphics Tools" feature installed.)
  3. After the render target view is created, the program no longer needs its own pointer to the back-buffer texture. Release it with release backbuffer right there and confirm the window still clears. Why does the view keep working?
Credit: this chapter follows LearnOpenGL's OpenGL 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.