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
feature_level: D3D_FEATURE_LEVEL = D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0;
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 C 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:
raster_desc: D3D11_RASTERIZER_DESC = {
.FillMode = D3D11_FILL_MODE.D3D11_FILL_SOLID,
.CullMode = D3D11_CULL_MODE.D3D11_CULL_NONE,
.FrontCounterClockwise = 1,
.DepthClipEnable = 1,
};
raster_state: *ID3D11RasterizerState = null;
ID3D11Device_CreateRasterizerState(device, raster_desc&, raster_state&);
ID3D11DeviceContext_RSSetState(context, raster_state);
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(...). Our programs are written in rin, which compiles to C, and C has no methods. The Windows SDK headers support C anyway: with COBJMACROS defined, each method gets a macro named after its interface, which takes the object as its first argument and calls through the table. Our includes/win32.rin turns those macros on before including the headers:
#define COBJMACROS
cinclude "windows.h"
cinclude "d3d11.h"
and then declares each method it uses, so that rin can check the argument types of every call:
ID3D11DeviceContext_ClearRenderTargetView: proc[external](This: *ID3D11DeviceContext, view: *ID3D11RenderTargetView, rgba: *const FLOAT) -> void = {}
So whenever you see ID3D11Device_CreateBuffer(device, ...) 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 call 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.rinpassesnullfor the parameter that receives the feature level actually obtained. Pass the address of a D3D_FEATURE_LEVEL variable instead and print it in hexadecimal. (11_0 is0xb000.) - Turn on the
debug layer by passing D3D11_CREATE_DEVICE_DEBUG as the flags argument (the0after the driver type), then run the program under a debugger and watch its output window. Now delete the line that setsSampleDesc.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
ID3D11Texture2D_Release right there and confirm the window still clears. Why does the view keep working?