Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

Chapter 28 Modern Direct3D

Four CNA identities occupy the modern Direct3D line. Three broad implementations share one development loop; DIRECTX10 is a deliberately narrower bridge between them: cross-compiled from this project’s own Debian machine using the same MinGW-w64 toolchain Chapter 4’s Windows cross-build section already introduced, then run and verified under Wine with GPU execution — D3D9 and D3D11 through DXVK, D3D12 through a different translation layer, vkd3d-proton, in its own separate Wine prefix. Native-Windows CI now covers DIRECTX11 and DIRECTX12; local Wine evidence remains a distinct tier and DIRECTX9/DIRECTX10 have no equivalent CI job.

Identity Shader model Extended route RT / MRT / query
DIRECTX9 D3D9 / XNA-native effects native yes / yes / yes
DIRECTX10 two embedded SM4 shaders inherited colored downgrade yes / yes / no
DIRECTX11 SM5 shared generated package native yes / yes / yes
DIRECTX12 SM5 plus PSO/root-signature caches native yes / yes / yes

28.1 D3D9: zero-tolerance XNA oracle comparisons

D3D9 targets pixel-for-pixel agreement with XNA 4.0 on its measured corpus. It uses Microsoft’s vendored XNA stock-effect HLSL and compiles it through d3dcompiler_47.dll; it does not substitute a reimplemented shader package.

28.1.1 Oracle: XNA 4.0 under Wine

A dedicated tool, tools/xna-oracle/, runs XNA 4.0 under Wine using the GAC assemblies and the in-prefix csc.exe. It renders each declarative scene through XNA and CNA’s D3D9 renderer, then compares the images at zero tolerance. Both sides use the same DXVK layer on the same GPU, isolating CNA behavior from differences between graphics stacks. The pinned corpus contains 39 scene files. Every retained comparison is an exact match under this Wine/DXVK configuration; the result does not cover paths absent from that corpus.

28.1.2 Current contract and evidence

Full device lifecycle via plain Direct3DCreate9 (not D3D9Ex, a deliberate choice), with DeviceLost / DeviceResetting / DeviceReset handling; all five XNA stock effects, with 61 of 66 compiled shader variants byte-identical to Microsoft’s own shipped bytecode (the remaining five differ only by HLSL compiler version, and are separately compared through the oracle); an oracle- and mutation-verified SpriteEffect.fx, including the half-pixel offset Direct3D 9 is notorious for — caught before it could become a false-positive “closed” claim, because the offset’s absence is invisible on a 1×1 test texture and only shows up at a larger size; and GraphicsProfile.Reach / .HiDef enforcement through a D3DCAPS9 query — the only audited CNA renderer where the distinction reaches a native capability query, as Chapter 12 already noted. Render-target sampling and the PreferPerPixelLighting/specular gaps described in Chapter 15 were corrected. The specular repair produced zero-tolerance frame matches for four of five PixelLighting bytecode variants via the oracle. The fifth, an untextured vertex-lit variant, is unreachable at the pin because the required position-only vertex layout is missing on the measured stock-effect paths.

D3D9’s lost-device lifecycle is reachable through two framework-reserved keys: after ordinary input processing, non-repeated F9 enters Lost and raises DeviceLost; F10 performs the reset, bracketing it with DeviceResetting and DeviceReset. The smoke suite checks exact event counts, that rendering fails while lost, exact pixels after recovery, and release/recreation of default-pool resources. This strong deterministic debug path should not be confused with two other public extensions: D3D9’s SetStringMarkerEXT() throws, and SetContextRecoveryEnabled() changes the shared Texture2D shadow policy before its D3D9 hook throws. Section 19.5.1 compares all fourteen products in the inherited audit cohort and calls out that partial-state outcome.

28.1.3 Coverage limits

Every SurfaceFormat besides Color remains unsupported (a shared, cross-renderer limitation from Chapter 14, not a D3D9-specific one). Texture3D has no oracle-scene coverage, but it has a capability-gated sub-volume SetData()/GetData() byte round trip: D3D9_Smoke creates a 4×4×4 volume only when D3DCAPS9::MaxVolumeExtent is nonzero, writes 32 distinct RGBA bytes into the off-origin 2×2×2 box at (1,1,1), and requires an exact readback of that same box. The test proves D3D9’s LockBox() row- and slice-pitch copy in both directions; it does not prove an oracle rendering path or automatic mip generation. OcclusionQuery is not built for this renderer at all, the same gap D3D12 carries; and SpriteSortMode.Immediate / .Texture were confirmed not viable as deterministic oracle scenes at all — Immediate is not something a raster diff can observe, and Texture sorts by an implementation-defined GetHashCode(), which no two implementations are obligated to agree on. The caveat that applies to every result in this section: D3DCAPS9 itself is synthesized by DXVK on this dev loop, not reported by XNA-era Windows driver hardware — native-Windows hardware verification remains open, as stated above.

28.1.4 D3D9 render-target limits and evidence

The D3D9 smoke test establishes render-target storage and recovery. An 8x8 target with Depth24Stencil8 is bound, cleared through the public device, and copied with D3D9’s required GetRenderTargetData()–system-memory-surface–LockRect() route; its own surface must contain the exact requested color. A cube-face target has the same proof for one selected face, followed by an independent back-buffer clear/readback after unbind.

MSAA is capability-gated by IDirect3D9::CheckDeviceMultiSampleType(), not assumed from the requested count. When 4x is available, a multisampled target is cleared and unbound; D3D9’s StretchRect() resolve must make its separate sampleable texture read exactly (200,210,220,255). The assertion reads the resolved result, not the multisample surface. One naming trap matters here: this is an internal device-capability check in the creation path, not GraphicsDevice::SupportsCapability. D3D9 never overrides that public method, so MultiSampleAntiAliasing still reports true even when CheckDeviceMultiSampleType() rejects every count above one. The same mismatch affects AnisotropicFiltering: sampler creation uses D3DTEXF_ANISOTROPIC and D3DSAMP_MAXANISOTROPY, but neither the public query nor the setter consults and clamps to D3DCAPS9::MaxAnisotropy.

Multiple render targets are bounded by D3DCAPS9::NumSimultaneousRTs: two targets receive one exact-color clear, unbinding restores the back buffer, and requesting one more target than the device reports throws rather than silently truncating. This proves D3D9’s multi-attachment binding and clear semantics. It does not separately prove a shader with several fragment outputs, so the result should not be generalized into a G-buffer draw claim.

28.1.5 One oracle scene

The oracle’s own scene format is a small, declarative text file, not a compiled asset or a piece of C++. The simplest scene in the corpus, colored3d.scene, fits in eleven lines:

1 width=256
2 height=256
3 profile=HiDef
4 clearcolor=100,149,237,255
5 vertexcolor=true
6 lighting=false
7 primitive=TriangleList
8 vertex=-0.6,-0.6,0,255,0,0,255
9 vertex=0.0,0.7,0,0,255,0,255
10 vertex=0.6,-0.6,0,0,0,255,255

With vertexcolor=true and lighting=false, BasicEffect draws one red/green/blue-cornered triangle under identity transforms over Color.CornflowerBlue, RGBA (100,149,237,255). XNA and CNA render the scene independently. Matching corners establish vertex-color plumbing; matching interior pixels also exercise Gouraud interpolation. The more elaborate fog_gradient_quad (Chapter 22’s own negative-FogEnd finding), uses the identical file format with a handful of additional fog* keys — the same declarative scheme scales from this eleven-line triangle through the corpus’s 39 scenes.

28.1.6 Two defects exposed by the oracle

The first was a sort-mode Z-clipping defect: zFarPlane=1 mapped a nonzero SpriteBatch layer depth outside Direct3D 9’s own valid clip-space Z range, silently clipping away any sprite with a nonzero layer depth entirely — fixed by using zFarPlane=-1 instead. The second was shared across renderers: GraphicsProfile never reached the device because Game’s GraphicsDevice was eagerly default-constructed (hardcoded to Reach) before GraphicsDeviceManager even existed to apply a game’s requested profile — meaning a game setting GraphicsProfile = HiDef; ApplyChanges(); had no path to the live device at all. This is a shared, cross-renderer bug, only discovered while building D3D9’s own tests, because D3D9 is the one renderer where the profile actually matters enough to notice.

28.1.7 Why the zFarPlane sign hid nonzero-depth sprites

The sign controls clip-space placement. SpriteBatch’s orthographic projection maps a layerDepth value in [0,1] (XNA’s own documented range: 0 = front, 1 = back) onto Direct3D 9’s own native clip-space Z range, which is [0,1]not the [1,1] range OpenGL-family APIs use. Constructing that projection with zFarPlane=1 produces a near/far mapping where any layerDepth strictly greater than what the incorrect matrix treats as its own valid far bound gets clipped by the GPU’s fixed-function Z-clip test without an error. The default layerDepth=0 was unaffected, so the defect appeared only with nonzero depth ordering. The zFarPlane=-1 matrix maps the full documented [0,1] layerDepth range maps inside Direct3D 9’s own valid clip volume instead of partly outside it.

28.2 Three D3D11 resource-lifetime groups

Window resize and device-removed recovery affect different resource sets. D3D11 separates them into three lifetime groups:

Device lifetime.

ID3D11Device, ID3D11DeviceContext, the IDXGIFactory2 chain, and a cached allowTearingSupported_ capability flag — created once at startup, torn down only by full device-removed recovery.

Swap-chain lifetime.

The IDXGISwapChain1 object itself — also created once at startup (and recreated only alongside the device, on recovery), but a plain resize deliberately reuses the same object via IDXGISwapChain1::ResizeBuffers(…) rather than destroying and recalling CreateSwapChainForHwnd.

Window-size lifetime.

The back-buffer ID3D11RenderTargetView, the depth-stencil texture and its ID3D11DepthStencilView, and the viewport — unbound and released before ResizeBuffers, then recreated after it, on every resize and on device-removed recovery alike.

Resize releases and recreates only the window-size group, with one ResizeBuffers() call on the existing swapchain. Device removal rebuilds all three groups. This division avoids both recreating the device on resize and omitting the swapchain from recovery.

All D3D11, DXGI, and D3D12 interfaces use Microsoft::WRL::ComPtr<T>. A MinGW-w64/Wine spike exercised device creation, .As(), GetParent(), and the ReleaseAndGetAddressOf() repopulation needed by resize before that ownership model was adopted.

28.3 DIRECTX10: a bounded MVP with one unsafe default

DIRECTX10 uses Wine’s D3D10/D3D10.1 forwarding path into DXVK’s d3d10core and DXGI. Its two embedded shaders provide colored 2D and 3D submission, render targets, and MRT, but it deliberately leaves effect creation, occlusion queries, Texture3D, TextureCube, and cube targets at their base null implementations.

The sharpest boundary is not a throw. The renderer does not override either effect-aware extended primitive method, so the base class calls its colored draw implementation after discarding GpuDrawParams. A BasicEffect-shaped call can therefore produce pixels while silently losing texture, lighting, fog, skinning, PBR, instancing, or custom-effect intent. This is the sole unconditional inherited colored downgrade among all 46 renderer families.

That behavior makes capability checks necessary but insufficient. ThreeD can be true because colored geometry is implemented while effect semantics remain absent. A porter should treat DIRECTX10 as an explicit MVP target and verify the exact draw vocabulary used by the game rather than infer parity from a successful triangle.

28.4 D3D11: native Windows implementation

D3D11 is CNA’s first renderer built directly on Direct3D 11, verified in the same MinGW-w64-plus-Wine-plus-DXVK loop as D3D9. Its covered path includes an ID3D11Device / ID3D11DeviceContext pipeline: buffers, textures, and render targets (including MSAA and multiple render targets), state objects, all ten stock HLSL shader variants across the five stock effects, SpriteBatch, and — unique to this renderer and D3D12 — a runtime-D3DCompile()-backed ShaderEffect path for custom shaders, already introduced in Chapter 17. Pixel assertions use GPU readback, so they establish more than successful API return codes.

Device-removed recovery has detection-only evidence: the check is wired, but the Wine/DXVK campaign did not trigger device removal, so full recovery remains unobserved.

An early build exposed a static-link cycle when the linker could not resolve Effect::Apply(): the D3D11 SpriteBatch renderer calls back into Effect::Apply(), which lives in the main CNA library, which itself depends on the renderer). MinGW’s single-pass archive resolution never revisited the already-searched libCNA.a to satisfy it. The fix was an explicit repeated target_link_libraries(… CNA) for the D3D11/D3D12 targets, using CMake’s documented support for a repeated static-library dependency. The failure reproduced on the base revision and was not introduced by the D3D11 work.

28.4.1 The public presentation-mode call reaches D3D11 and D3D12, but neither scales yet

Chapter 6’s CNAEXT PresentationMode is not lost before it reaches these renderers: the public manager/device forwarding path passes the selected enum to each renderer. But the final handlers are currently intentional no-ops. D3D11 explicitly discards mode; D3D12 has the same empty handler. They create and present their swap chain at the SDL window’s pixel size, not a separately rendered logical surface which could be letterboxed, cropped, or stretched.

Their SetVirtualResolution(width,height) methods only retain the supplied integers. On D3D11, GetViewportSize() still queries the window’s physical pixel size; on D3D12, the stored virtual dimensions are returned to the caller but do not create a scaling pass. Consequently setting Letterbox, Overscan, Stretch, NativeBackBuffer, or FixedHeightDynamicWidth produces no different presentation geometry on either native Direct3D renderer today. This is a renderer limitation, not an XNA-compatibility promise: the enum itself is a CNA extension, and there is no test that could honestly pixel-prove a mode-specific result until a logical render target plus final composite/scaling path exists.

28.4.2 One PresentInterval::Two, two opposite Direct3D mistakes

The three renderers do not share one presentation-interval policy. D3D9 preserves CNA’s 0/1/2 conversion in D3DPRESENT_PARAMETERS: Immediate becomes D3DPRESENT_INTERVAL_IMMEDIATE, Two becomes D3DPRESENT_INTERVAL_TWO, and everything else becomes D3DPRESENT_INTERVAL_ONE. A runtime change marks presentation state dirty and immediately calls the same device-reset path used for a resize. This is the only CNA renderer in the Direct3D family whose source requests a literal native two-retrace interval.

It is not yet a safe implementation. Microsoft’s D3D9 contract says interval Two must be present in D3DCAPS9::PresentationIntervals, and windowed mode supports only Default, Immediate, and One.11 1 https://learn.microsoft.com/en-us/windows/win32/direct3d9/d3dpresent CNA checks neither condition. Its ordinary game window is windowed, so requesting Two can make IDirect3DDevice9::Reset() return failure. The renderer logs that result, restores its previous dimensions, leaves presentationDirty_ true, and retries at each later Present(); the public PresentationParameters still reports Two. No dedicated test exercises this route, and DXVK’s synthesized capability structure would not replace the still-needed real-Windows check anyway.

D3D11 and D3D12 make the opposite mistake. Both reduce the construction argument and every later setter call to vsyncEnabled_ = interval > 0; their Present() then passes only sync interval 1 or 0 to DXGI. DXGI itself accepts 1 through 4 and defines them as waiting for at least the corresponding number of vertical blanks,22 2 https://learn.microsoft.com/en-us/windows/win32/api/dxgi/nf-dxgi-idxgiswapchain-present so nothing in the native API forces CNA to collapse Two. The real windowed smoke paths prove that default presentation succeeds, but the D3D plans explicitly say the no-VSync/tearing branch is untested; no located test distinguishes sync interval 2 either.

The useful porting conclusion is deliberately narrow. D3D9 has the correct representation but incomplete validation and failure reporting; D3D11/12 have robust per-present selection but an unnecessarily Boolean representation. A successful reset or a stored PresentInterval::Two is not yet evidence of half-refresh pacing on any of the three. The cross-renderer table in §19.2.4 places these results beside the other eleven products in that audit cohort.

28.4.3 Only D3D9 consumes the presentation format/fullscreen hook

D3D9 likewise stands apart for the other three presentation fields. It maps the requested backbuffer enum to D3DFORMAT (with a required Color-specific A8B8G8R8-to-A8R8G8B8 display substitution), maps None/Depth16/Depth24/Depth24Stencil8 to no attachment, D16, D24X8, and D24S8 respectively, and writes D3DPRESENT_PARAMETERS.Windowed = !IsFullScreen. A later manager reset reaches UpdatePresentationFormatEXT() and immediately resets the native device. The smoke suite observes a real D24S8 surface after manager-driven setup, so this is more than a stored field.

D3D11 and D3D12 ignore all three renderer arguments. Their windowed swapchains are fixed R8G8B8A8 UNORM and their default depth attachments fixed D24S8, even when public state says Depth24 or None. Shared SDL code may still enlarge the containing window for fullscreen. Both CreateSwapChainForHwnd calls omit a fullscreen descriptor, and neither renderer calls DXGI SetFullscreenState; their exclusiveFullscreen_ fields remain false. D3D12’s HeadlessEXT route is a third case: no window means no swapchain and no default depth attachment at all. It can render only into explicitly-created targets.

These distinctions prevent two tempting overclaims. D3D9’s enum mapping is not a guarantee that every mapped format is a legal display mode; failed native reset can leave the previous attachments active while public state has already changed. And an SDL fullscreen window on D3D11/12 is not DXGI exclusive fullscreen. The inherited fourteen-product comparison and test boundary are in §19.2.5.

28.4.4 Evidence ladder for D3D11 3D draws

The D3D11 3D checks form a progressively discriminating GPU-readback ladder. The first uses the stride-16 VertexPositionColor path: a known blue clear was read back, an NDC-space opaque-red triangle was then drawn through both indexed and non-indexed calls, and the same region had to become red. That proves the input layout, shaders, constant buffer, primitive topology, rasterization, and readback path together; a stale clear or an unbound shader cannot pass it accidentally.

The next rung separated texture sampling from vertex-color modulation. A stride-20 textured draw sampled a known texel exactly, through both indexed forms, while the stride-24 variant multiplied a known vertex color through a white texture and read back that exact color. The stride-32 lighting path then exercises the per-light constant buffer: its unlit control is byte-exact, while its lit control must differ from both the unlit result and the clear color. That deliberately avoids pretending a hand-derived GPU floating-point result is more stable than it is, while still distinguishing execution of the lighting branch.

Later tests applied the same discipline to features whose happy paths look deceptively similar. Fog draws the same geometry with the flag off (exact vertex red) and at the fog end (exact fog green); an environment-map fixture constrains the reflection to the cube’s -Z face; and a skinned fixture supplies an identity bone instead of an all-zero default matrix. These fixtures falsify different failure modes. At the pin, the D3D11 smoke executable contains 147 checks exercised through Wine and DXVK GPU execution. The result supports the specific paths named here; it is not a license to extrapolate that any arbitrary, untested D3D11 state combination must therefore be correct.

28.4.5 Viewport/scissor: three viewport paths, but only two scissor paths

D3D9 and D3D11 take the simplest possible immediate route. Their public setters call the corresponding native viewport and scissor setters; their rasterizer mapping separately controls scissor enable. Neither SpriteBatch implementation replaces those native rectangles, and both derive their 2D projection size from the currently-bound native viewport. D3D11 Smoke Check O reads the exact native viewport, depth range, and scissor back through the corresponding native getters. That is strong native-state evidence but still one layer short of a public behavioral pixel test because the check invokes renderer methods directly.

D3D9 implements both states, but its evidence label needed correction. The renderer plan now calls viewport and scissor “oracle-proven.” A direct recount/search of the current 39-scene tools/xna-oracle/scenes corpus finds cull-mode scenes but no viewport or scissor scene, and the D3D9 smoke program’s resize check proves a full-size post-reset viewport, not a custom sub-region. The safest current claim is therefore native-source implementation plus broad D3D9 draw coverage, not a zero-tolerance XNA comparison for these two states.

D3D12 now splits the two states. Its SetViewport() override stores rectangle and depth range, and every ordinary, extended, instanced, and SpriteBatch command list obtains that value through GetEffectiveViewportEXT(). The getter clamps depth to [0,1] and falls back to the current full target only when no positive custom rectangle is active. SpriteBatch also uses the same effective width and height as its projection basis, so coordinates remain local to the sub-viewport rather than being transformed twice. A native off-screen GPU check distinguishes inside/outside pixels, two custom rectangles in successive draws, depth-range behavior, and the full-target reset on a target switch. Scissor remains the negative half: there is still no rectangle hook, scissorTestEnable is not part of the PSO, and every draw installs a full-target D3D12_RECT. The complete product comparison is §19.4.3.

28.4.6 Clear: exact native bits do not imply one shared public contract

D3D11 immediately clears every bound RTV and selects the native depth and stencil clear bits independently when a DSV exists. D3D12 mirrors that selection with synchronous command lists. Both have strong direct-renderer proof: stencil-plane bytes are read back from the GPU, changed to a second value, and depth is distinguished by whether one identical triangle passes. Those tests bypass GraphicsDevice’s public route, but the route now derives depth and stencil availability independently from the bound 2D/cube attachment instead of inferring stencil from every non-None depth enum. D3D12 still requires a bound colour resource before its combination helper can record any command list; once one is bound, however, a requested depth/stencil aspect with no DSV is deliberately a legal no-op, matching D3D11.

D3D9’s native IDirect3DDevice9::Clear bits are likewise immediate and individually selected, but their final gate consults depthStencilFormatOrdinal_, the presentation format captured for the backbuffer. Binding a differently-formatted 2D or cube target never updates that field. A target can therefore lose a depth clear when the backbuffer has no depth, or receive a stencil bit selected because the backbuffer has one. The smoke test’s seven public combinations all use a D24S8 backbuffer and mostly check colour preservation, so they cannot reveal this mismatch. See §19.4.2 for the corresponding thirteen-product contrast within that audit cohort.

28.5 MRT finalization across all bound targets

An ordinary single RenderTarget2D owns a simple end-of-use rule. When it is unbound, its ResolveAndGenerateMipsEXT() helper asks D3D11 to resolve the multisampled draw attachment into the separate sampleable texture. If the target has a mip chain, it then calls GenerateMips() on the shader-resource view. Thus the texture a later sprite samples is the resolved, current image rather than the still-multisampled attachment or an old mip level.

That rule was once silently absent for an N>1 MRT bind. The single-target pointer could not represent several targets, so the old SetRenderTargets() path bound all their RTVs with OMSetRenderTargets() but did not call any target’s finalization when the set was replaced. A game could clear or draw to two MSAA targets successfully, then sample stale single-sample resolve textures without an exception. The repair keeps a non-owning array plus a count for the active MRT set. Both SetRenderTargets() and SetRenderTarget2D() first run FlushPendingMRTResolveEXT(), which visits every stored target, invokes its ResolveAndGenerateMipsEXT(), clears the array entry, and only then binds the next target or restores the back buffer. This matters for all three transitions: MRT to back buffer, MRT directly to one target, and MRT to a different MRT set.

D3D11_Smoke makes the error observable. It binds two distinct 8×8, 4x-MSAA targets, clears the shared MRT pass to (200,30,40,255), unbinds it, and reads both targets’ sampleable textures directly. Both must contain that exact color. A repair that resolved only target zero, or that merely left the MSAA draw attachment valid, fails the second readback. A second case binds another two-target set, clears it to (5,6,7,255), and switches straight to an unrelated single target rather than unbinding to null; its first prior target must still read the new value. This distinguishes all target transitions from a narrower null-unbind repair.

The shared helper also calls GenerateMips(), but the scope of its proof should remain honest. The suite separately reads an 8×8 single-target mipmapped render target’s 4×4 and 2×2 levels after a solid (200,90,10,255) clear, proving that the normal unbind path creates non-garbage downstream levels. The combination of N>1 MRT and mipMap=true uses the same helper and is therefore wired, but is not independently pixel-tested yet. That is a verification boundary, not a claim that the two mechanisms are magically proven together.

28.6 D3D12: shared foundations and split verification

D3D12 reuses D3D11’s HLSL/DXBC bytecode, format/state mappings, and constant-buffer layouts through D3DCommon. The implemented route includes a device, queues, descriptor heaps, command lists, fences, per-resource barriers, pipeline-state objects, root signatures, per-slot samplers, buffers, 2D/cube/3D textures, render targets with MRT, mip generation and device-queried MSAA, occlusion queries, all ten stock shader variants, SpriteBatch, runtime-compiled sprite shaders, and device-removed recovery.

Evidence.  The routine D3D12 CTest suite is entirely off-screen. A separate manual diagnostic, launched through Proton, establishes swapchain creation and Present() through a window. That manual result is not part of the routine automated suite because the Proton bootstrap is too heavy for this development loop.

28.6.1 Backbuffer readback is not part of the shared D3D11 foundation

The repeated phrase “GPU readback” in this chapter names several different mechanisms. D3D11 has a ReadBackbuffer() override: it always copies backBufferTexture_ into a staging texture, honors RowPitch, and backs the public GraphicsDevice::GetBackBufferData() route with exact windowed clear/draw tests. It does not redirect to a currently bound render target.

D3D12 has no corresponding override. A public backbuffer call reaches IGraphicsRenderer’s default runtime_error on a windowed device, while HeadlessEXT has no swapchain to read in the first place. The D3D12 smoke suite’s many exact GPU pixel assertions call its working render-target-resource readback helpers directly; the shared public SpriteBatch example below likewise reads the off-screen render target, not GetBackBufferData(). Those results prove the named draw/resource path but cannot be quoted as evidence for a public backbuffer bridge that does not exist. The complete renderer matrix and FNA contrast are in §19.2.6.

28.6.2 Two frames in flight and blocking call sites

Unlike the driver-managed D3D11 model, D3D12 makes the command allocator lifetime explicit. CNA therefore creates exactly two ID3D12CommandAllocator objects (kFramesInFlight = 2), one reusable graphics command list, one shared ID3D12Fence, and one remembered fence value per allocator slot. The essential operation, SignalAndWaitForFrameEXT(frameIndex), deliberately has a slightly counter-intuitive ordering: it signals a new, monotonically increasing value for the frame just submitted, then waits only for the previous value recorded for that same slot before allowing its allocator to be reused. Thus a call for slot 0 may submit value v2 while waiting for older v0; it must not wait for v2 itself, because doing so would erase the overlap the two slots exist to permit.

The smoke test invokes the primitive for slots 0, 1, and 0 again, verifies that v0 < v1 < v2, and, on the second use of slot 0, requires GetCompletedValue() to have reached v0. It intentionally does not assert that v2 has completed on return: ID3D12CommandQueue::Signal() is asynchronous, so that would be a race, not a correctness condition. A separate explicit event wait establishes eventual completion. The test also places 48 64-MiB GPU copies ahead of the old fence and comparing that wait with an already-complete control wait; the loaded wait is measurably slower, establishing that WaitForSingleObject() supplies back-pressure.

The reusable two-slot primitive is directly tested, but the current ordinary clear/draw helper uses ExecuteCommandListAndWaitEXT(), its intentionally simpler synchronous sibling: it submits a closed list, signals a fresh fence value, and waits for that value before returning. Present() likewise uses that synchronous helper for its explicit back-buffer transition before calling IDXGISwapChain3::Present(). Consequently a “Two frames in flight” therefore describes tested infrastructure and the allocator-reuse rule; it does not claim that current ordinary CNA drawing overlaps two CPU frames. The off-screen test paths retain a deterministic readback boundary.

Render-target, depth-stencil, shader-resource, and sampler descriptors come from four separate heaps; their current allocators are fixed-capacity, monotonically advancing bump allocators, not a general free-list. A destroyed resource does not return its descriptor slot during the device’s lifetime, and exhaustion throws a named std::runtime_error instead of silently aliasing another resource’s view. The capacities in the live renderer — 64 RTV, 8 DSV, 64 CBV/SRV/UAV, and 16 sampler descriptors — are therefore engineering limits of this implementation, not Direct3D 12 limits. Destroying many transient resources can therefore exhaust a descriptor heap even when GPU memory has been released; callers cannot assume unlimited descriptor churn during one device lifetime.

28.6.3 External Wine/vkd3d architecture mismatch

Swap-chain creation under plain Wine (as opposed to a Proton-managed launch) crashes outright, reproduced twice with a full symbolized backtrace pointing to a null-pointer read inside Wine’s own dxgi.dll. The cause is external to CNA: a mismatch between Debian’s own system dxgi.dll and vkd3d-proton’s separately overridden d3d12.dll — two DLLs that were never meant to be paired this way. Once launched through Proton, which supplies the matched DLL pair expected by vkd3d-proton, swapchain creation and a ten-frame clear-and-present loop through a window succeed in two runs.

28.6.4 Explicitly corrected claims

Earlier D3D12 documentation listed runtime state objects, sixteen-slot sampler state, public 2D and cube render targets including MRT, Texture3D, and TextureCube::GetData() as open. They are implemented at the pin. The live tracking file remains the authority for revisions after this edition.

28.7 One public ShaderEffect API, two Direct3D contracts

Chapter 17 draws an important boundary: D3D11 and D3D12 compile custom HLSL at runtime, but their implementations are SpriteBatch custom shader facilities, not general replacements for XNA’s compiled Effect. The two renderers accept the same constructor strings, compile the same main entry points as vs_5_0 and ps_5_0, and expose the same public setters. Under that shared surface, both require one exact 32-byte vertex:

Semantic Byte offset Required HLSL type
POSITION0 0 float2: pixel-space X and Y
TEXCOORD0 8 float2: texture U and V
COLOR0 16 float4: sprite tint

This is SpriteBatch’s own vertex, not an inferred convention. D3D11 creates a three-element ID3D11InputLayout from the compiled vertex blob; D3D12 embeds the same three elements in the custom effect’s pipeline-state object. A shader declaring float3 POSITION, a tangent, or a game-specific stride therefore cannot be made compatible by supplying a matching VertexDeclaration: unlike EasyGL’s separately tested general-3D path, neither Direct3D renderer reads GpuDrawParams::customEffectRenderer from its 3D draw dispatch.

28.7.1 The names are documentation; the byte offsets are the API

Both implementations store public uniform writes in the same 128-byte block. The caller’s name argument is deliberately ignored. What selects a value is which setter was called:

Bytes Writer Meaning
0–15 renderer only vpSize; SpriteBatch writes width and height before binding the effect.
16–79 SetUniformMat4 one 64-byte matrix, regardless of the supplied name.
80–95 SetUniformVec2/3/4 one overlapping vector slot; a later vector setter overwrites the components it supplies.
96–99 SetUniformFloat/Int one scalar slot. The integer overload stores a floating-point conversion, not integer bits.
100–127 none reserved padding in the current implementation.

There are two further consequences that the common public surface does not make obvious. SetUniformFloatArray() and SetUniformVec2Array() inherit IEffectRenderer’s no-op defaults on both renderers. So do all three SetTexture() renderer hooks. The sprite’s primary resource still works because SpriteBatch itself binds its texture and sampler to t0/s0; a second 2D texture, cube map, or volume texture set through ShaderEffect does not. Effect authors should therefore treat the layout above as a tiny fixed ABI, not as name-reflected HLSL constants.

28.7.2 Where D3D11 and D3D12 stop being the same

D3D11 can bind the compiled vertex shader, pixel shader, input layout, and dynamic constant buffer as separate context state. Every Bind() performs a write-discard map, copies the 128 bytes, and attaches the buffer to b0 for both shader stages. The native map mode is D3D11_MAP_WRITE_DISCARD.

D3D12 has no equivalent independent shader bind. Its effect constructor must build a complete ID3D12PipelineState up front, using the same cached root-signature shape as the stock sprite path: one CBV at b0, one SRV table at t0, and one sampler table at s0. The 128-byte constant buffer lives in a persistently mapped upload resource; Bind() only copies the current values, while D3D12SpriteBatchRenderer::FlushBatch() binds the resource and the prebuilt PSO inside its command list.

That PSO also makes D3D12’s boundary stricter and directly visible in source: it is compiled for one DXGI_FORMAT_R8G8B8A8_UNORM, single-sample color target, with blending, depth, and stencil disabled and culling set to none. Those choices match the custom-effect pixel test. They do not form a dynamic promise that the same object can be reused for an MRT set, an MSAA target, another color format, or caller-selected pipeline state; those dimensions would require separate PSOs and a cache key that the current effect renderer does not build.

28.7.3 Shared D3D11/D3D12 pixel-test example

The two smoke tests use byte-for-byte equivalent HLSL to turn a solid red texture into exact cyan. The vertex shader’s vpSize field is at byte zero because the renderer fills it; the four padding vectors advance the next public vector slot to byte 80:

1 const char* vertexHlsl = R"(
2 struct VSIn { float2 pos : POSITION0;
3 float2 uv : TEXCOORD0;
4 float4 col : COLOR0; };
5 struct VSOut { float4 pos : SV_Position;
6 float2 uv : TEXCOORD0;
7 float4 col : TEXCOORD1; };
8 cbuffer CB : register(b0) {
9 float4 vpSize;
10 float4 pad1[4];
11 float4 uColor;
12 float4 uFloat0;
13 };
14 VSOut main(VSIn input) {
15 VSOut output;
16 float2 ndc = (input.pos / vpSize.xy) * 2.0 - 1.0;
17 output.pos = float4(ndc.x, -ndc.y, 0.0, 1.0);
18 output.uv = input.uv;
19 output.col = input.col;
20 return output;
21 })";
22
23 const char* pixelHlsl = R"(
24 Texture2D texSampler : register(t0);
25 SamplerState texSamplerSampler : register(s0);
26 struct PSIn { float4 pos : SV_Position;
27 float2 uv : TEXCOORD0;
28 float4 col : TEXCOORD1; };
29 float4 main(PSIn input) : SV_Target {
30 float4 source = texSampler.Sample(texSamplerSampler, input.uv);
31 return float4(1.0 - source.rgb, 1.0);
32 })";
33
34 ShaderEffect invert(device, vertexHlsl, pixelHlsl);
35 if (!invert.IsEffectValid()) {
36 throw std::runtime_error("custom HLSL did not compile");
37 }
38
39 SamplerState pointClamp = SamplerState::PointClamp;
40 SpriteBatch batch(device);
41 batch.Begin(SpriteSortMode::Deferred, BlendState::Opaque,
42 &pointClamp, nullptr, nullptr, &invert);
43 batch.Draw(redTexture, destination, Color::White);
44 batch.End();

D3D11 runs this through a windowed Wine/DXVK device and reads the sprite’s top-left region. D3D12 runs the same public GraphicsDevice/SpriteBatch sequence against a HeadlessEXT off-screen device, then reads the render target. In both cases solid (255,0,0,255) must become exactly (0,255,255,255). That result proves more than successful compilation: the fixed input layout, automatic viewport slot, b0 constant-buffer/root binding, t0/s0 resource path, custom pixel shader, and GPU readback all participated in one observable public-API draw.

28.8 Persistent Direct3D resources implement the shared usage contract

All three Direct3D factories receive the preserve Boolean for 2D and cube targets but need not translate it into a native load action: their resources persist across binding. Shared code supplies the visible policy. DiscardContents clears black plus each available depth and stencil aspect; PreserveContents and PlatformContents do not. The first descriptor controls the same policy for MRT. A redundant bind is not a no-op: native resolve/finalization can run before the resource is rebound, and a discard target receives its three-aspect clear again.

Public pixel fixtures establish this policy. The shared cube-usage fixture renders an asymmetric face, rebinds it and adds only a small marker, so a discard masquerading as preservation cannot pass. Parallel depth and stencil fixtures preserve an occluder or stamp across a bind cycle, while pass-boundary and ordered-clear tests separate usage policy from explicit clear chronology. D3D9 and D3D11 register these tests in their routine renderer suites; D3D12 cross-builds the same public fixtures, while its headless/direct smoke coverage supplies the local GPU mechanisms. See §19.4.1.

28.9 Cube-target MSAA, mip generation, and public finalization

Shared D3DCommon tables do not imply identical resource algorithms. D3D11 and D3D12 use parallel designs for RenderTargetCube MSAA but different mip-generation routes. An interface-default audit also exposed a public routing hole after those mechanisms landed: early smoke checks invoked BindAsRenderTargetFace() and UnbindAsRenderTarget() directly, while the public route never finalized the outgoing cube. That defect is now closed. The following subsections describe both the renderer algorithms and the tracking edge that connects them to the public device route.

28.9.1 RenderTargetCube MSAA

Neither D3D11_RESOURCE_MISC_TEXTURECUBE nor a D3D11_SRV_DIMENSION_TEXTURECUBE view can ever be multisampled, and D3D12’s equivalent D3D12_SRV_DIMENSION_TEXTURECUBE has no multisampled variant either — so on both renderers, the MSAA color resource becomes a plain, non-cube six-slice Texture2DMSArray used only as a render-target view, while a second, single-sample, cube-flagged resource receives ResolveSubresource() on unbind. The shader-resource view targets the resolved resource. D3D12 additionally requires two explicit resource-barrier transitions (RESOLVE_SOURCE on the color resource, RESOLVE_DEST on the resolve target) that D3D11 does not need at all — its context-level ResolveSubresource() call handles the equivalent state transition implicitly. Both renderers resolve only the currently active face on unbind, the same one-face-at-a-time convention their mip-chain handling follows. On both renderers, an 8×8 cube face requested at 4x MSAA is cleared, resolved, and read back exactly; GetMultiSampleCount() must also report four.

28.9.2 Mip-chain generation: one native GPU call versus an explicit CPU round trip

After drawing one cube face, D3D11 calls ID3D11DeviceContext::GenerateMips() on a shader-resource view spanning all six faces. The GPU regenerates every face’s chain; unchanged level-zero faces reproduce their prior mips. D3D12 has no corresponding convenience operation. Its GenerateMipsEXT() reads the active face’s preceding level through ReadbackSubresourceRGBA8(), box-filters it on the CPU, and uploads the next level with UploadSubresourceRGBA8(). It repeats this round trip for each level of that face. Both routes produce the required chain, but their synchronization and cost differ substantially. Shared bytecode and state mappings do not imply a shared resource algorithm.

28.9.3 How renderer tracking closes the public finalization edge

D3D11 and D3D12 now override the cube-face route and retain the active cube separately from the single-2D and MRT state. Before any cube-to-cube, cube-to-2D, cube-to-MRT or cube-to-backbuffer transition, FlushPendingCubeResolveEXT() clears that tracking pointer and calls the outgoing cube’s UnbindAsRenderTarget(). That ordering is important: the callback performs the active-face MSAA resolve and mip generation before the new destination replaces native state. The plural normalized-descriptor path recognizes a single cube face and delegates to this same route; cube faces in a multi-target set remain an explicit renderer rejection rather than being silently treated as 2D targets.

The original direct smoke checks still prove the two low-level algorithms described above, but they are no longer the only evidence. Shared GraphicsDevice fixtures now drive an asymmetric rendered cube face through public binding and readback, rebind it for the usage test, and isolate preserved multisample content across two faces. Those fixtures make a skipped finalization, wrong face/subresource, stale resolve or false preservation visibly fail. Thus the former caution remains a useful account of how the hole escaped an implementation-local smoke test, not a current feature limitation.

28.10 HeadlessEXT: routine off-screen D3D12 coverage

SpriteFont, Model, and part of the Texture2D.FromStream/SaveAsPng route require a GraphicsDevice. That constructor formerly created an SDL window for every renderer except HEADLESS and SOFTWARE. On D3D12, window creation also required the Proton-managed swapchain route, which was too expensive for the routine plain-Wine test loop.

PresentationParameters therefore gained the CNA-only HeadlessEXT property:

1 CNAEXT bool getHeadlessEXTProperty() const;
2 CNAEXT void setHeadlessEXTProperty(bool value);

The property defaults to false; only D3D12 honors it at the pin. D3D11 always creates a swapchain and EasyGL requires a window-bound GL context, so both reject it. A headless D3D12 device renders only to explicit targets and cannot present. This supports routine tests as well as off-screen applications such as server-side rendering and thumbnail generation.

28.10.1 Four D3D12 defects exposed by headless GraphicsDevice tests

The first routine public-device tests exposed four corrected D3D12 defects:

  • SpriteBatch ignored sampler filter and address-mode updates, inheriting state from a prior 3D draw;

  • render targets failed to bind their depth-stencil views;

  • the six combined color/depth/stencil clear variants threw; and

  • SetDepthTestEnabled, SetDepthWriteEnabled, and SetBlendEnabled threw when the Model route called them.

All four are fixed at the pin. Their discovery also shows why direct renderer smoke tests and public GraphicsDevice fixtures are complementary: the former had not traversed these shared-framework call paths.

Type at least three characters. Results are ranked by how often and where the words occur.