DIRECTX11 internals: lifetimes, recovery, readback and target finalisation

CNA snapshot c1c316b9  ·  Deep Dives › Renderers  ·  source links pinned to c1c316b9

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit c1c316b9; unless a sentence says otherwise, nothing here was built or executed. Read from the renderer sources and fixture registrations at c1c316b9; nothing was executed. A genuine DXGI device removal is not triggered by any CNA test; recovery is exercised through the deterministic simulation path.

DIRECTX11 takes its shaders, format and state tables from D3DCommon and lets the Direct3D 11 driver own command lists and resource transitions. This page describes the resource side of the renderer at this snapshot: device creation and the three lifetime groups, device recovery, compute and indirect draws, why it refuses the windowless HeadlessEXT request, back-buffer readback, how MRT sets and cube targets are finalised, and how render-target usage is honoured. It corrects several statements that earlier descriptions of the renderer made. It is for maintainers of the renderer and for anyone debugging lifetime, stale-texture or recovery reports on Windows.

What D3DCommon provides

D3DCommon holds one Shader Model 5 DXBC package of stock shaders (hlsl_shaders.hpp, compiled offline; see how it is built), the SurfaceFormat-to-DXGI_FORMAT and XNA-state mappings, constant-buffer layouts, vertex-declaration helpers, the presentation geometry and the program reflection used by ShaderEffect. DIRECTX11 classifies formats by asking the device: the 20 classic XNA formats with native BC1–BC3, and the CNA extension formats classified unsupported (texture formats). DIRECTX9 uses none of this; its D3DFORMAT world is separate.

Device creation and the three lifetime groups

DirectX11Renderer::CreateDeviceResources (DirectX11Renderer.cpp) negotiates feature levels 11_1, 11_0, 10_1 and 10_0 on a hardware driver, retries without 11_1 when a driver rejects the explicit request with E_INVALIDARG, and then refuses anything below 11_0, because the SM5 stock shaders need it. It probes whether the device really keeps the bytes it is given for three packed formats (B5G6R5, B5G5R5A1, B4G4R4A4); a device that reports B4G4R4A4 but does not store its texels makes SurfaceFormat::Bgra4444 unavailable with a warning rather than silently corrupt. Window resize and device loss affect different resources, so the renderer keeps three lifetime groups:

GroupObjectsRecreated on
DeviceID3D11Device, immediate context, the IDXGIFactory2 chain, the cached tearing-support flagdevice recovery only
Swap chainthe IDXGISwapChain1 (flip-discard, two buffers, never multisampled)device recovery only; a resize reuses the object with ResizeBuffers()
Window sizeback-buffer view, the optional multisampled colour texture, the depth texture and view, the viewportevery resize (released before ResizeBuffers(), recreated after) and every recovery

This division avoids both recreating the device on resize and forgetting the swap chain during recovery. Back-buffer MSAA is a separate multisampled texture resolved into the swap-chain buffer before each Present() and before readback. Every Direct3D 11 and DXGI interface is held in Microsoft::WRL::ComPtr, including under MinGW. DIRECTX11 asks only for a hardware driver and has no WARP path.

Device recovery: real code, simulated trigger

The renderer routes DXGI_ERROR_DEVICE_REMOVED and DXGI_ERROR_DEVICE_RESET from Present() and ResizeBuffers() into a lost state, log GetDeviceRemovedReason() and raise DeviceLost. Recovery then rebuilds everything: RecreateDeviceEXT releases every registered long-lived buffer, texture and render target, drops all caches and stock shaders, recreates device, swap chain and window-size resources and asks each registered resource to recreate itself, reporting how many failed. Registration follows SetContextRecoveryEnabled.

A genuine device removal cannot be triggered on the development loop. The recovery path is exercised deterministically through DebugSimulateContextLoss() and DebugRestoreContext() (the F9/F10 keys and the shared d3d_context_recovery_contract_test.cpp, DX-244), which raise Lost, Resetting and Reset around a real recreation. Proving that a real removal reaches this path is the native-hardware item CNA keeps open.

Compute, indirect draws and resource cardinality

At feature level 11_0 DIRECTX11 reports compute shaders with their storage-buffer path and GPU-buffer-driven indirect draws; the native pieces are D3D11ComputeShader.cpp, D3D11StorageTexture2D.cpp and D3D11IndirectBuffer.cpp. Direct3D 11 manages descriptors and resource transitions itself, so the renderer keeps no descriptor heaps or frame slots of its own: what it owns is caching, of input layouts, state objects and samplers (D3D11InputLayoutCache.cpp, D3D11StateObjectCache.cpp, D3D11SamplerCache.cpp). The shared descriptor_capacity_contract_test.cpp fixture, registered as DirectX11_DescriptorCapacityContract, checks the public half: a high-cardinality workload renders correctly.

HeadlessEXT is not a DIRECTX11 mode

SpriteFont, Model and parts of Texture2D.FromStream/SaveAsPng need a GraphicsDevice, so PresentationParameters has the CNA-only HeadlessEXT property (getHeadlessEXTProperty()/setHeadlessEXTProperty(), default false), and CNA_FORCE_HEADLESS_DEVICE_EXT applies it to named renderers from the environment. A device created that way opens no window and does not initialise the video subsystem. Among the GPU renderers only SDL_GPU honours it, with a renderer-owned colour target as its off-screen back buffer. DIRECTX11 creates its swap chain in its constructor and EasyGL’s context is bound to a window, so both throw from their constructors, which the property’s documentation calls the honest outcome. A DIRECTX11 device therefore always has a back buffer once construction has succeeded.

Back-buffer readback

DIRECTX11 overrides ReadBackbuffer: it resolves back-buffer MSAA, copies the back buffer into a staging texture and honours RowPitch. It maps a logical request through the presentation geometry (readback mapping) and throws NotSupportedException, by name, when there is no back buffer instead of returning a zero-filled frame as a success.

Finalising MRT sets

A single RenderTarget2D has a simple end-of-use rule: when it is unbound, ResolveAndGenerateMipsEXT() resolves the multisampled draw texture into the separate sampleable texture and, for a mipmapped target, regenerates the chain, so a later sampler reads the current image. The rule was once silently missing for an MRT set, because the single-target pointer could not represent several targets: the old code bound all views with OMSetRenderTargets() and finalised none when the set was replaced, and a game could draw into two MSAA targets and then sample stale single-sample textures without any error. The renderer now keeps a non-owning array and count for the active set: SetRenderTarget2D() and SetRenderTargets() first call FlushPendingMRTResolveEXT() (DX-143), which finalises every stored target and clears its entry before anything is bound. All three transitions are covered: MRT to back buffer, MRT to a single target, and MRT to another MRT set. CNA’s stock shaders write only the first target.

The proof has to read the sampleable textures of every target, because a repair that resolved only target zero, or that merely left the MSAA draw texture valid, would pass a weaker check. The shared MRT fixture (easygl_mrt_test.cpp, REMED-GFX-016/DX-224), registered for DIRECTX11 as DirectX11_MRT, does this with equivalent GLSL and HLSL ShaderEffect programs that write distinct outputs to targets 0 to 3: it covers ordered one-to-four-target rendering, independent colour-write masks, first-target depth ownership, true MRT MSAA with resolve, target-set replacement and producer-to-consumer sampling right after unbind. RenderTarget_MsaaFirstReadback and RenderTarget_ActiveMsaaReadback add the single-target rule that stale resolve data is never a valid answer. That MSAA MRT sets and mipmapped targets are finalised by the same helper is wired in source, and a fixture combines MRT with mipMap = true: leg L1 of bound_target_lifetime_test.cpp (Resource_BoundTargetLifetime, registered for DIRECTX11) destroys one slot of a bound multi-target set and requires the surviving target's mip chain to be regenerated.

Cube targets: MSAA, mip chains and the public finalisation edge

Direct3D 11 does not allow a multisampled cube resource or cube view. The MSAA colour resource of a RenderTargetCube is therefore a plain six-slice Texture2DMSArray used only as a render-target view, and a second, single-sample, cube-flagged resource receives ResolveSubresource() for the active face on unbind; the shader-resource view targets the resolved resource. The renderer resolves only the currently active face, and GetMultiSampleCount() reports the applied count.

Mip generation is left to the driver:

  • DIRECTX11 calls ID3D11DeviceContext::GenerateMips() on the shader-resource view; for a cube that view spans all six faces, so every face's chain is regenerated on the GPU (unchanged faces reproduce their previous levels).

An interface-default audit found a routing hole after these algorithms landed: early native smoke checks called BindAsRenderTargetFace() and UnbindAsRenderTarget() directly, while the public route never tracked a bound cube, so a multisampled cube's resolve texture stayed empty and a mipmapped cube's upper levels were never regenerated. The renderer now overrides the cube-face route, keep the active cube separately from single-target and MRT state, and call FlushPendingCubeResolveEXT() before any cube-to-cube, cube-to-2D, cube-to-MRT or cube-to-back-buffer transition (REMED-GFX-134): the outgoing cube's resolve and mip generation run before the new destination replaces native state. A single cube face in the plural binding path is delegated to the same route; cube faces inside a multi-target set remain an explicit refusal. Shared fixtures (RenderTargetCube_SampleAfterUnbind, RenderTargetCube_Usage, RenderTargetCube_MsaaFace, RenderTargetCube_GetDataContract) drive an asymmetric cube face through public binding and readback, so a skipped finalisation, wrong face or stale resolve fails visibly.

Render-target usage on persistent resources

Direct3D 11 binds render targets without a load action, so a resource keeps its contents across binds. The factory accepts the preserveContents flag and deliberately leave it unused; the visible policy lives in GraphicsDevice::SetRenderTargets, which, when the first bound target's usage is DiscardContents, clears it to opaque black plus whichever depth and stencil planes that attachment really has. PreserveContents and PlatformContents clear nothing, and the first descriptor decides for an MRT set. Rebinding an identical target set does nothing: GraphicsDevice::SetRenderTargets returns early when every target and cube face matches the current binding, as XNA does (rebinding is a no-op), before any resolve, discard clear or viewport reset. A genuine change of binding, including a different cube face, finalises the outgoing binding first and clears a DiscardContents target again. The shared RenderTarget_Usage, RenderTarget_DepthStencilUsage, RenderTargetCube_Usage and GraphicsDevice_OrderedClear fixtures, which render an asymmetric image and rebind with only a small marker so a discard masquerading as preservation cannot pass, are registered for DIRECTX11; DIRECTX9 registers the cube-usage, depth/stencil-usage and ordered-clear fixtures in its own list (DirectX9_RenderTargetCube_Usage, DirectX9_RenderTarget_DepthStencilUsage, DirectX9_GraphicsDevice_OrderedClear) but not the 2D RenderTarget_Usage fixture. The cross-renderer rules are on RenderTargetUsage.

Statements that no longer hold

Several claims made about this renderer in earlier descriptions are false at this snapshot, and are listed so that they are not reintroduced:

Earlier statementAt this snapshot
Replacing an MRT set leaves its multisampled targets unresolvedFlushPendingMRTResolveEXT() finalises every stored target before anything is bound (DX-143)
A cube bound through the public route is never resolved or mip-generatedthe cube-face route is tracked and flushed on every transition (REMED-GFX-134)
The DIRECTX11 swap chain ignores the requested depth formatthe default depth attachment follows it
A device without a back buffer returns a zero-filled readbackReadBackbuffer throws NotSupportedException by name

Read in this order

  1. DirectX11Renderer.cpp: device creation, EnsureSwapChainSize, recovery and the flush helpers.
  2. D3D11RenderTargets.cpp: resolve and mip generation for 2D and cube targets.
  3. modules/renderers/common/d3d: the stock shaders, format and state tables the renderer takes from D3DCommon.
  4. DirectXParityTests.cmake: the shared fixtures registered for DIRECTX11.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Maintainer workflow
Fix a renderer bug
Tests and validation
Test architecture: GPU tests