Direct3D 11
Before we write any code, it helps to know what we are talking to.
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
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
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
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 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
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
- The
device (ID3D11Device) creates things: buffers, textures, shaders, views and state objects. It does not draw, and it holds no current state. It can be called from any thread. - The
device context (ID3D11DeviceContext) uses things. It binds resources and state to the stages of the pipeline, issues draw calls, copies data and clears render targets. It is a state machine in exactly OpenGL's sense: whatever we bind stays bound until we bind something else. Theimmediate context , the one created with the device, sends its work straight to the GPU. Direct3D 11 can also createdeferred contexts that record command lists on other threads, which we will not need.
A third object completes the set. The D3D11_ prefix.
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
// 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
COM
Direct3D's objects are
- An object is reached through an
interface , a pointer to a table of functions. Every interface starts with the methods of IUnknown:QueryInterface asks an object for another of its interfaces, andAddRef andRelease count references to it. An object frees itself when its count drops to zero. - Interfaces are identified by 128-bit GUIDs. When a function can hand back more than one kind of object, we pass the GUID of the one we want. The swap chain's back buffer is fetched as a texture this way, with IID_ID3D11Texture2D, in Hello Window.
- Almost every method returns an HRESULT, a 32-bit status code that is negative on failure. The lessons check it where failure is plausible, such as creating the device or compiling a shader.
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.
opengl32.dll only exports a very old version of the API. Direct3D exports what it has: we link against d3d11.lib and Resources and views
One more kind of object deserves an early mention, because it has no direct OpenGL counterpart. A
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
- Direct3D 11 Graphics: Microsoft's programming guide and reference for every function and structure.
- Direct3D 11.3 Functional Specification: the detailed rules each stage follows, down to rasterization and filtering precision.
- Introduction to a device in Direct3D 11: devices, immediate and deferred contexts.
- Direct3D feature levels: what each level guarantees.
- LearnOpenGL's OpenGL chapter: the same introduction from OpenGL's side.
Exercises
- The device-creation call in
hello_window.asmpasses0for the parameter that receives the feature level actually obtained (the one beforeADDR context). Add a seconddwordlocal, pass its address there instead, and print it in hexadecimal withprint "%x\n", .... (11_0 is0xb000.) - Turn on the
debug layer by passing D3D11_CREATE_DEVICE_DEBUG as the flags argument (the second0after the driver type), then run the program under a debugger and watch its output window. Now delete the line that setsswap_desc.SampleDesc.Countand run it again: device creation fails, and the debug layer says why. (The debug layer needs Windows' optional "Graphics Tools" feature installed.) - After the render target view is created, the program no longer needs its own pointer to the back-buffer texture. Release it with
release backbufferright there and confirm the window still clears. Why does the view keep working?