Skip to content

Direct3D 11 Backend

Jeremiah van Oosten edited this page Oct 18, 2026 · 1 revision

Direct3D 11 backend

The Direct3D 11 backend (vgl/d3d11.h, C++ wrapper vgl/d3d11.hpp, CMake target vgl::d3d11, sources in backends/d3d11) draws a context's globe into the host's Direct3D 11 render targets, at feature level 11_0, on Windows. This page covers handing over the device and device context, the shaders compiled with fxc at build time, depth buffer formats and conventions, matrices and culling, the state it saves and restores, device removal, WARP, and the Direct3D 11 sample. Like the OpenGL backend, it is a consumer of the render interface in a library of its own, built on the public C API only, and draws the same globe with the same features and debug views.

Contents

Requirements

Item Requirement
Platform Windows 10 or later (the project's minimum)
Device ID3D11Device at D3D_FEATURE_LEVEL_11_0 or later, hardware or WARP, created by the host
Runtime libraries d3d11.lib only: no d3dcompiler_47.dll, no D3DX
Build The Windows SDK's fxc, found when vgl is configured

Building and linking

The backend is on by default on Windows (VGL_BACKEND_D3D11, a dependent option that is only offered when WIN32; see Building vgl). Link it next to the core:

find_package(vgl CONFIG REQUIRED)
target_link_libraries(my_app PRIVATE vgl::vgl vgl::d3d11)

The target links d3d11 publicly. With BUILD_SHARED_LIBS=ON the backend is a DLL (vgl-d3d11.dll). It has no pkg-config file. vcpkg enables it with the d3d11 feature and Conan with with_d3d11 (both on by default on Windows); see Installing vgl. vgl/d3d11.h includes <d3d11.h>; the backend itself compiles with NOMINMAX and WIN32_LEAN_AND_MEAN, which your code may want too.

Shaders compiled at build time

The shaders are HLSL for shader model 5.0 in backends/d3d11/terrain.hlsl, a port of the OpenGL backend's GLSL that also includes the shared atmosphere code (backends/common/atmosphere.glsl) after a few #defines that turn its GLSL subset into HLSL. CMake compiles them with fxc into headers of bytecode that the library embeds, so creating a backend compiles nothing and shader errors surface at build time.

Variant Profile Entry point Defines
terrain_vs vs_5_0 vs_main
terrain_vs_log vs_5_0 vs_main VGL_LOGARITHMIC_DEPTH=1
terrain_ps ps_5_0 ps_main
terrain_ps_log ps_5_0 ps_main VGL_LOGARITHMIC_DEPTH=1
sky_vs, sky_ps vs_5_0, ps_5_0 sky_vs_main, sky_ps_main
stars_vs, stars_ps vs_5_0, ps_5_0 stars_vs_main, stars_ps_main

Each is compiled with /O3 /WX into shaders/<name>.h in the build tree. CMake looks for fxc in WindowsSdkVerBinPath, the Visual Studio generator's target SDK, and the newest Windows Kits/10/bin/10.*/x64. If it finds none, it warns "fxc (the Windows SDK's HLSL compiler) not found; skipping the Direct3D 11 backend" and builds without it. Point the cache variable VGL_FXC at fxc.exe to choose one:

cmake --preset windows-msvc-static -DVGL_FXC="C:/Program Files (x86)/Windows Kits/10/bin/10.0.26100.0/x64/fxc.exe"

(The SDK version in that path is an example; use one you have installed.)

Quick start

#include <vgl/d3d11.hpp>
#include <vgl/vgl.hpp>

// The host's device and immediate context, at feature level 11_0.
auto info = vgl::init<vgl_d3d11_backend_create_info>();
info.depth_convention = VGL_DEPTH_CONVENTION_REVERSED_Z; // the natural one on Direct3D 11
info.device = device;                 // ID3D11Device*
info.device_context = immediate;      // ID3D11DeviceContext*
vgl::expected<vgl::d3d11_backend> backend = vgl::d3d11_backend::create(context, info);
if (!backend)
{
    std::fprintf(stderr, "backend: %s\n", vgl::result_name_get(backend.error()));
}

// Each frame, after context.update():
const float clear[4] = {0.02f, 0.02f, 0.05f, 1.0f};
immediate->ClearRenderTargetView(rtv, clear);
immediate->ClearDepthStencilView(dsv, D3D11_CLEAR_DEPTH, 0.0f, 0); // 0 for reversed-Z
immediate->OMSetRenderTargets(1, &rtv, dsv);
D3D11_VIEWPORT viewport{};
viewport.Width = static_cast<float>(width);
viewport.Height = static_cast<float>(height);
viewport.MaxDepth = 1.0f;
immediate->RSSetViewports(1, &viewport);

auto draw = vgl::init<vgl_d3d11_draw_info>();
draw.view_matrix = &view_matrix;             // optional, vgl_dmat4
draw.projection_matrix = &projection_matrix; // optional, depth 0 to 1
draw.features = VGL_DRAW_FEATURE_ATMOSPHERE;
static_cast<void>(backend->draw(&draw));
swap_chain->Present(1, 0);

The same in C:

vgl_d3d11_backend_create_info info = {0};
info.struct_size = sizeof(info);
info.device = device;
info.device_context = immediate;
vgl_d3d11_backend* backend = NULL;
if (vgl_d3d11_backend_create(context, &info, &backend) != VGL_RESULT_SUCCESS)
{
    /* the context's log says why */
}
/* each frame: */
vgl_d3d11_backend_draw(backend, NULL);
/* at the end, before the vgl context and the device: */
vgl_d3d11_backend_destroy(backend);

In C, define COBJMACROS before <d3d11.h> to call the COM interfaces through macros such as ID3D11DeviceContext_ClearRenderTargetView(...); the pointers are the same.

Device and device context handoff

vgl_d3d11_backend_create_info is a versioned structure: zero-initialize it, set struct_size, then fill in.

Field Meaning Default
struct_size sizeof(vgl_d3d11_backend_create_info) Required
depth_convention VGL_DEPTH_CONVENTION_LOGARITHMIC, _REVERSED_Z or _CONVENTIONAL Logarithmic (0)
device ID3D11Device*, feature level 11_0 or later Required
device_context ID3D11DeviceContext* to draw with, usually the immediate context Required
  • The backend holds a reference (AddRef) to both and releases them in vgl_d3d11_backend_destroy, so you may release your own references in any order, as long as the backend is destroyed before the vgl context.
  • Call every backend function on the thread that owns the device context. With the immediate context that is your render thread; the draw must not overlap vgl_context_update.
  • Creation checks the feature level, creates the shaders (from the embedded bytecode), input layouts, constant buffers, samplers and states, and tells the context it samples RGBA8. It allocates through the context's allocator and logs through the context's log.
Result Cause
VGL_RESULT_SUCCESS Created
VGL_RESULT_INVALID_ARGUMENT A null pointer, a small struct_size, no device or device context, or an unknown convention
VGL_RESULT_NOT_SUPPORTED Fields of a newer vgl set, or a feature level below 11_0
VGL_RESULT_BACKEND_ERROR Direct3D failed to create a shader or state; the log has the HRESULT
VGL_RESULT_OUT_OF_MEMORY Allocation failed

A context has one render-interface consumer at a time. Destroying the backend releases its objects and resets the context's render resources, so a backend created later receives them all again.

Drawing

vgl_d3d11_backend_draw(backend, &info) draws into the render target and depth buffer bound on the device context, with the viewport you set. Call it once per frame after vgl_context_update. It takes a frame from the context, applies its resources (releasing and creating buffers and textures), draws one indexed draw per tile with up to four imagery layers, then the stars and the sky if enabled, restores your state, and ends the frame. It does not clear or present. Without a view, it draws nothing.

vgl_d3d11_draw_info mirrors vgl_gl_draw_info field for field; info may be null for all defaults:

Field Meaning 0 or null selects
struct_size sizeof(vgl_d3d11_draw_info) Required when info is given
mirrored VGL_TRUE when the projection mirrors the image: cull the other side Not mirrored
view_matrix const vgl_dmat4*, the camera to draw from when it differs from the last update The context's view
projection_matrix const vgl_dmat4*, camera to clip space with depth 0 to 1 The context's view's projection
sun_direction Towards the sun along the ECEF axes Zero: light from the camera; with the atmosphere, a sun above the camera
logarithmic_far The logarithmic convention's far distance, metres 1.0e8
upload_budget Bytes of resources to upload this draw 16 MiB
features vgl_draw_feature bits Terrain and imagery only
debug_views vgl_debug_view bits Normal drawing
earth_rotation_angle Radians, for the stars 0
contour_interval Metres 500
cloud_height Metres above the ellipsoid 6000

Results are those of the OpenGL backend: VGL_RESULT_INVALID_ARGUMENT for non-finite or negative values and unknown bits, VGL_RESULT_NOT_SUPPORTED for fields of a newer vgl, VGL_RESULT_INVALID_OPERATION if a frame is open or the last device reset failed, VGL_RESULT_OUT_OF_MEMORY if the frame could not begin.

Per-frame values sit in one constant buffer and per-tile values in another, updated with D3D11_MAP_WRITE_DISCARD for each draw. Layers bind to shader resource slots 0 to 3, the mesh's samples (DXGI_FORMAT_R32G32_UINT) to slot 4 and the clouds to slot 5; sampler 0 is trilinear, anisotropic 8x, clamped, and sampler 1 serves the clouds. Imagery textures are DXGI_FORMAT_R8G8B8A8_UNORM (the backend asks for VGL_TEXTURE_FORMAT_RGBA8).

Features, debug views, the sea surface, the atmosphere, stars and clouds behave exactly as in the OpenGL backend; see OpenGL Backend and Atmosphere, Stars and Clouds. Stars and clouds are set with vgl_d3d11_backend_stars_set and vgl_d3d11_backend_clouds_set (stars_set and clouds_set on vgl::d3d11_backend).

Matrices, handedness and culling

  • Camera space is the view matrix's: right-handed like ECEF, looking along -z. The backend's logarithmic depth and lighting rely on it. Projections map depth to [0, 1], as Direct3D does.
  • vgl_dmat4 is column-major and multiplies column vectors. A row-major matrix that multiplies row vectors, as DirectXMath's XMMATRIX is, has the same memory layout, so you can copy one element for element. Use the right-handed functions (XMMatrixPerspectiveFovRH), or build the matrices yourself as the sample does.
  • Build the view matrix in double precision. Its translation is millions of metres; a float matrix (including any XMMATRIX) rounds it to about half a metre and the globe jitters. The projection can come from single precision.
  • Triangles are counter-clockwise from outside the ellipsoid; Direct3D tells front from back in clip space, before the viewport turns y down, so the backend's rasterizer treats counter-clockwise as front (FrontCounterClockwise = TRUE), with back faces culled. Set mirrored for a projection that flips the image.

A right-handed projection with depth from 0 to 1, as the sample builds it (column-major):

vgl_dmat4 perspective_zero_to_one(double fovy, double aspect, double near_plane, double far_plane)
{
    const double f = 1.0 / std::tan(fovy / 2.0);
    vgl_dmat4 m{};
    m.m[0] = f / aspect;
    m.m[5] = f;
    m.m[10] = far_plane / (near_plane - far_plane);
    m.m[11] = -1.0;
    m.m[14] = near_plane * far_plane / (near_plane - far_plane);
    return m;
}

For reversed-Z, the reversed infinite projection on the OpenGL Backend page works unchanged (it already maps depth to [0, 1]).

Depth buffer formats and conventions

Convention Depth format Clear depth Depth test Projection
Reversed-Z (recommended here) DXGI_FORMAT_D32_FLOAT 0 GREATER Reversed, near to 1, far (ideally infinite) to 0
Logarithmic (default) Any, for example DXGI_FORMAT_D24_UNORM_S8_UINT 1 LESS Conventional, depth 0 to 1; near and far do not matter. The shaders write SV_Depth and set the clip-space depth to match
Conventional Any 1 LESS Yours; small scenes only

The viewport's MinDepth and MaxDepth should be 0 and 1. The sky and stars draw at the far depth (0 for reversed-Z, 1 otherwise) with GREATER_EQUAL or LESS_EQUAL and no depth writes. The sample uses DXGI_FORMAT_D32_FLOAT for every convention and reversed-Z by default. For the logarithmic convention in your own HLSL, see the snippet on Render Interface.

State it saves and restores

When it draws, the backend saves this state of the device context and restores it before returning (requirement NFR-08), releasing the references the save took:

  • input assembler: the input layout, the primitive topology, vertex buffer 0 (with stride and offset), and the index buffer (with format and offset)
  • the vertex shader and its constant buffers 0 and 1
  • the pixel shader, its constant buffers 0 and 1, its shader resources 0 to 5 and its samplers 0 and 1
  • the rasterizer state
  • the depth-stencil state and stencil reference
  • the blend state, blend factor and sample mask

It draws with your render targets, viewports and scissor rectangles as they are, and with the stencil test off and blending off for the terrain (the sky and stars blend).

The sample's check verifies the restore with sentinel state around each draw.

Integrating with an existing renderer

  • Pass order. Clear, draw a background that should show through thin sky without writing depth, draw your opaque geometry with the same depth convention, call vgl_d3d11_backend_draw, then transparent geometry and UI. The reasoning is the same as for OpenGL (pass order).
  • Colour format. The backend writes display-encoded colour from sRGB-encoded imagery sampled as unorm. Draw into a DXGI_FORMAT_R8G8B8A8_UNORM render target view, as the sample's swap chain is, not an _SRGB view; otherwise the globe looks washed out.
  • MSAA. It draws into a multisampled target as into any other; resolve it yourself. With the logarithmic convention the pixel shader writes SV_Depth (one depth per pixel, no early depth test); reversed-Z avoids that.
  • Deferred contexts. The header says the device context to draw with is "usually the immediate one". The sample and the checks use the immediate context only; deferred contexts are untested.
  • Several views. Pass another camera in view_matrix and projection_matrix to draw the tiles selected for the context's view from it.

Device removal

When the device is removed or reset (DXGI_ERROR_DEVICE_REMOVED from Present, for example), create a new device and context and hand them over:

if (backend->device_reset(new_device, new_context) != vgl::result::success)
{
    // draws fail with VGL_RESULT_INVALID_OPERATION until a reset succeeds
}

vgl_d3d11_backend_device_reset(backend, device, device_context) forgets the old device's objects without using them again, takes references to the new device and context, recreates its shaders and states, and resets the context's resources, which upload again over the next frames. Stars and clouds are kept on the CPU and upload again. It fails with VGL_RESULT_NOT_SUPPORTED below feature level 11_0 and VGL_RESULT_BACKEND_ERROR if Direct3D cannot create a shader or state.

WARP

WARP, Direct3D's software rasterizer, is on every Windows machine and supports feature level 11_0, so the backend runs on machines without a GPU, in virtual machines and in CI. The atmosphere needs no textures or extra passes and runs on WARP too. The sample creates a hardware device and falls back to WARP, or uses WARP directly with --warp:

const D3D_FEATURE_LEVEL wanted = D3D_FEATURE_LEVEL_11_0;
HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, &wanted, 1,
                               D3D11_SDK_VERSION, &device, nullptr, &immediate);
if (FAILED(hr))
{
    hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_WARP, nullptr, 0, &wanted, 1,
                           D3D11_SDK_VERSION, &device, nullptr, &immediate);
}

The Direct3D 11 sample

samples/d3d11/main.cpp, executable vgl-d3d11: the globe in a plain Win32 window with a flip-model swap chain, the same navigation, panel and keys as the OpenGL sample, and the atmosphere, stars and clouds.

vgl-d3d11 [--data <directory> | --pack <uri>...] [--cache <directory> [--cache-budget <MiB>]]
          [--depth logarithmic|reversed|conventional] [--warp] [--check] [--save <directory>]
          [--stars <catalog>]
  • The default convention is reversed-Z, with a DXGI_FORMAT_D32_FLOAT depth buffer.
  • --warp draws with WARP; without it, the sample falls back to WARP when no hardware device is available.
  • --check runs without a window: it draws a few views offscreen with each depth convention, checks the images and that the conventions agree, checks that the backend restores the host's state, checks that it draws again after device_reset, and exits with 1 on failure. --save writes the checked images as PPM files.

The check is the CTest test d3d11, added whenever tests are built; it needs no display, since it draws offscreen on the hardware device or WARP. Data and common options are on Samples.

Troubleshooting

Symptom Likely cause
No vgl::d3d11 target, configure warns that fxc was not found Install the Windows SDK, or set VGL_FXC
Create fails with VGL_RESULT_NOT_SUPPORTED Feature level below 11_0 (a 10.x device)
Nothing drawn No view set, render target or viewport not bound, or depth cleared to the wrong value for the convention
Terrain missing or depth wrong with the logarithmic convention A left-handed view matrix: the backend expects right-handed camera space looking along -z
Jitter near the ground The view matrix was built in single precision
Colours washed out An _SRGB render target view

More on Troubleshooting.

See also

Render Interface, OpenGL Backend, Atmosphere, Stars and Clouds, Ogre Backend, Building vgl, Samples, and the header backends/d3d11/include/vgl/d3d11.h.

Clone this wiki locally