Direct3D evidence: MinGW cross-builds, Wine translators and native Windows

CNA snapshot 009d40f5  ·  Deep Dives › Renderers  ·  source links pinned to 009d40f5

✓

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 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Read from CMake registrations, wrapper scripts and the workflow file at 009d40f5; no Wine session, Windows run or test was executed for this page, and no pass counts are claimed.

CNA's three Direct3D renderers, DIRECTX9, DIRECTX11 and DIRECTX12, are developed mostly on Linux: cross-compiled with MinGW-w64 and run under Wine, with Direct3D translated to Vulkan on a real GPU. This page explains that loop precisely: which translator serves which renderer, the gates that stop a silent fall-back to Wine's own Direct3D, how the CTest registrations run each renderer, what the shared DirectX parity inventory contains, and what the native-Windows workflow adds. It is for anyone who reads a Direct3D test result, adds a Direct3D fixture, or has to decide how much a green Wine run proves.

Three identities, one development loop

All three identities are Windows-only: RendererSelection.cmake stops the configure step with a FATAL_ERROR unless CMAKE_SYSTEM_NAME is Windows, which means a native MSVC or MinGW build on Windows or a cross-build from Linux with cmake/toolchains/mingw-w64.cmake. The step-by-step commands are in Tutorial 103; this page is about what the resulting runs mean.

IdentityStock shadersShared codeTranslator on the Linux loopWrapper script
DIRECTX9Microsoft's XNA stock-effect .fx sources compiled offline to SM2 bytecode (see DIRECTX9 fidelity)none: D3D9 has its own format and state tablesDXVK (Direct3D 9 to Vulkan)run-wine-dxvk9.sh
DIRECTX11one shared Shader Model 5 DXBC package (hlsl_shaders.hpp)common/d3d (D3DCommon)DXVK (Direct3D 11 to Vulkan)run-wine-dxvk.sh
DIRECTX12the same SM5 package, plus pipeline-state and root-signature cachesD3DCommonvkd3d-proton (Direct3D 12 to Vulkan), in a separate prefix; Proton for swap-chain workrun-wine-vkd3d.sh, run-proton-vkd3d.sh

All three create render targets, MRT sets and occlusion queries through their native APIs (on DIRECTX9 the query is created only when CreateQuery(D3DQUERYTYPE_OCCLUSION, nullptr) reports support). Their capability answers are in the capability matrix.

What the cross-build adds

Each family's examples directory registers tests only when that identity is the configured renderer and CNA_BUILD_TESTS is on. The cna_directx11_test and cna_directx12_test macros (DirectX11 examples CMake, DirectX12 examples CMake) link every test executable statically against the MinGW C and C++ runtimes (-static-libgcc -static-libstdc++), add -Wl,--allow-multiple-definition, and copy the MinGW and SDL runtime DLLs next to the executable (cna_copy_mingw_runtime, cna_copy_sdl_runtime in ThirdPartySDL.cmake), so a test .exe runs in a Wine prefix without an installed toolchain. When CMAKE_CROSSCOMPILING is true the registered command is the wrapper script with the executable as its argument; a native Windows build registers the executable itself.

The shipped stock shaders are also produced by this loop, but offline: compile_shaders_hlsl.py cross-builds a tiny D3DCompile() tool, runs it once per .hlsl file through the DXVK wrapper and embeds the DXBC as C++ arrays. The script is not part of any CMake target; it is rerun by hand after a shader edit, and the SHADERS list inside it (every entry compiled as vs_5_0 or ps_5_0) is the authority for what ships.

Three translators, deliberately separate prefixes

DIRECTX11 runs in the prefix ~/.wine-cna-d3d11 (override with CNA_D3D11_WINEPREFIX) prepared with dxvk-setup install. DIRECTX9 uses the same physical prefix by default, because DXVK's installer also redirects d3d9.dll, but reads its own variable, CNA_D3D9_WINEPREFIX, so the two renderers' test infrastructure never share an environment contract by accident. Three DIRECTX9 registrations (DirectX9_ConstantTable, DirectX9_EffectRenderer and DirectX9_SpriteBatch_CustomEffect) pin a different prefix, ~/.wine-cna-d3d9-spike, because only it holds Microsoft's real d3dcompiler_47.dll: the default prefix has just Wine's builtin one, which cannot compile Shader Model 2/3 sources. The two effect tests open a device and keep the DXVK gate; the constant-table test never creates one and opts out. DIRECTX12 runs in ~/.wine-cna-d3d12 (CNA_D3D12_WINEPREFIX) with vkd3d-proton's d3d12.dll and d3d12core.dll installed as native overrides.

Plain Wine is enough for the Direct3D 12 device, queues, heaps, command lists and fences, but not for a swap chain. CreateSwapChainForHwnd crashes with a null read inside Wine's own dxgi.dll: the system dxgi.dll belongs to one Wine build and cannot hand a Direct3D 12 command queue to vkd3d-proton's separately overridden d3d12.dll, whose matching dxgi.dll is ABI-compatible only with Proton's own DLL set. The cause is external to CNA. run-proton-vkd3d.sh therefore launches through Proton's own proton run in a Proton-managed prefix, where the header of that script records CreateSwapChainForHwnd returning S_OK in the swap-chain diagnostic, which then runs a ten-frame clear-and-present loop with a different colour each frame (CNA's records report it succeeding; not re-run here). Proton does not reliably forward a child process's stdout, which is why that diagnostic writes its result to a file. The routine suite avoids the window entirely (below).

A fourth wrapper, run-wine-vkd3d-headless.sh, starts a private Xvfb server per invocation for cross-compiled SDL_GPU/Direct3D 12 tests: Wine and vkd3d need a virtual X server to enumerate the Vulkan-backed DXGI adapter even though no swap chain is created.

Authenticity gates: proving the translator actually ran

If a DLL override is missing, Wine silently uses its own WineD3D implementation. The test still runs, still produces plausible pixels, and proves nothing about DXVK or vkd3d-proton, let alone about Direct3D. The wrappers therefore inspect the run's own log before reporting success:

WrapperRequired log lineOn failureDeliberate opt-outs
run-wine-dxvk.sh (DIRECTX11)DXVK: v2.6 or DXVK: 2.6.0 (official and packaged builds print different forms; both are accepted)exit status 3CNA_D3D11_SKIP_DXVK_GATE=1 for an executable that never opens a device (the pure mapping-table test DirectX11_Common); CNA_D3D11_ALLOW_WINED3D=1 for a deliberate one-off WineD3D diagnostic, never for normal runs
run-wine-dxvk9.sh (DIRECTX9)a DXVK: <major>.<minor> line (the pattern has no optional v, so the official DXVK: v2.6 spelling that the DIRECTX11 wrapper accepts would be reported as a missing marker)exit status 3CNA_D3D9_SKIP_DXVK_GATE=1, CNA_D3D9_ALLOW_WINED3D=1
run-wine-vkd3d.sh (DIRECTX12)vkd3d-proton - applicationVersion: x.y.zexit status 3CNA_D3D12_SKIP_VKD3D_GATE=1

The DIRECTX11 wrapper's DXVK gate is itself tested without Wine (the DIRECTX9 and DIRECTX12 wrappers carry their own copies of the gate, and no fixture exercises them): CTest DirectX11_DxvkGate, registered only in a DIRECTX11 tree, runs test-run-wine-dxvk-gate.sh, which puts a stub wine on PATH that prints each marker form (and none) and checks the wrapper's verdict, so a regression in the gate cannot hide behind a green suite. CNA_D3D11_VIRTUAL_DESKTOP (a Wine desktop specification such as CNA,1280x1024) runs the executable inside Wine's own desktop for hosts where a plain Xvfb screen is not exposed as a Windows display; a batch trampoline returns the viewer's stdout and, for an ordinary run, its exact exit status. A --capture run is judged differently: a newly written, size-stable PNG counts as success (exit 0) whatever the viewer returned, a cna-gltf-viewer: diagnostic line gives exit 1, and no completion within 30 seconds gives 124.

⚠

A marker proves which translator handled the run. It does not prove Direct3D behaviour on a Windows driver: DXVK and vkd3d-proton implement Direct3D on top of one Vulkan driver, and on the DIRECTX9 loop even D3DCAPS9 is DXVK's synthesized capability set, not what an XNA-era Direct3D 9 driver would report.

How CTest runs each renderer

  • Renderer-local programs. DirectX11_Smoke and DirectX12_Smoke were narrowed (DX-243) to native integration that the renderer-neutral corpus cannot observe: native cache identity and keying, immediate-context binds, command submission, resource-state tracking and device recreation. Public pixels, resources, effects, queries, presentation and state semantics moved into the parity inventory below. The older, much larger smoke executables that earlier descriptions of these renderers quote check counts for no longer exist in that form. DIRECTX9 keeps a broad DirectX9_Smoke plus about fifty focused registrations in its examples CMake, including D3D9_XNA_Diff.
  • The shared DirectX parity inventory. DirectXParityTests.cmake (DX-242) is one authoritative list of 265 fixtures, registered as DirectX11_<name> in a DIRECTX11 tree and DirectX12_<name> in a DIRECTX12 tree. 176 of the sources are EasyGL fixtures and 84 are renderer-neutral graphics examples, so the Direct3D renderers are held to the same public-API assertions as the GL family. Only three are renderer exceptions, each with a mandatory written reason: Common and Pbr_VertexColor (D3D11 only) and DescriptorAllocator (D3D12 only). The macro refuses a fixture listed twice and fails the configure, naming the fixture and the path, when a cross-module source has moved.
  • How a DIRECTX12 fixture runs on the Linux loop. When cross-compiling, every fixture is registered with CNA_FORCE_HEADLESS_DEVICE_EXT=DIRECTX12, which creates the device as PresentationParameters::HeadlessEXT (no window, no swap chain, an implicit off-screen back buffer), unless it is marked DIRECTX12_NO_HEADLESS (the allocator probe) or DIRECTX12_PROTON. The three Proton fixtures, BackbufferResize, RealWindowResize and ViewportResetAfterResize, need a real window and run through run-proton-vkd3d.sh. A native Windows build runs every fixture windowed, exactly as DIRECTX11 does.
  • Two fixtures need SDL. RealWindowResize and ViewportResetAfterResize reach into an SDL_Window to cross-check what CNA reported; with CNA_ENABLE_SDL=OFF they are skipped at configure time with a status message instead of breaking the build.

Registration is not execution. Report ctest -N and the executed results for the exact configuration; the site does not quote a pass count for these suites, and nothing was run for this page.

How a Direct3D pixel check is made discriminating

The Direct3D 11 bring-up built its 3D evidence as a ladder of GPU-readback checks, each designed so that the failure it targets cannot pass by accident. The ladder lived in the old, large DirectX11_Smoke; at this snapshot the same ideas are carried by the shared fixtures of the parity inventory. The design rules are what is worth reusing:

  • Change the pixel, then read it. Clear to a known colour, draw an opaque primitive over the same region, and require the new colour. A stale clear, an unbound shader or a draw that silently did nothing cannot pass; the first rung did this with the stride-16 VertexPositionColor path through both indexed and non-indexed calls, which exercises input layout, shaders, constant buffer, topology, rasterisation and readback together.
  • Separate the terms of a product. A textured draw that samples one known texel exactly, and a vertex colour multiplied through a white texture, isolate sampling from vertex-colour modulation.
  • Use exact controls where arithmetic is exact, inequalities where it is not. A lit draw's unlit control is byte-exact, while the lit result is only required to differ from both the unlit result and the clear colour: that proves the lighting branch ran without pretending a hand-derived floating-point GPU result is stable.
  • Choose inputs that falsify a specific mistake. Fog is drawn with the flag off (exact vertex colour) and at the fog end (exact fog colour); an environment-map check constrains the reflection to one cube face; a skinned check supplies an identity bone instead of an all-zero default matrix, which would collapse the geometry and hide a packing error. The shared BasicEffect_Fog, BasicEffect_LitVertexColor, EnvironmentMapEffect_Golden and SkinnedEffect_IdentityBones fixtures carry these cases to both DXGI renderers.

A ladder like this supports the specific paths it names. It is not a licence to extrapolate that an untested state combination must also be correct.

What the native Windows workflow adds

d3d-windows-ci.yml is a workflow_dispatch-only (manual) job on windows-latest with a matrix of DIRECTX11, DIRECTX12 and DIRECT2D. It builds with MSVC, a different compiler from the MinGW g++ every other Direct3D result comes from, and runs ctest -L <renderer> against the real Windows runtime DLLs with no Wine involved. The Direct3D legs also run verify_hlsl_shaders_native_msvc.py, which rebuilds the shader tool with cl.exe and recompiles every checked-in .hlsl against a genuine D3DCOMPILER_47.dll. It deliberately does not byte-compare the output with the embedded DXBC, because a different compiler build may emit different but equivalent bytecode; the signal is that real Windows still accepts the HLSL. The workflow's own header scopes it as a native-runtime regression gate, not a physical-display, tearing, device-removal or driver-parity result, and states that the full CnaTests GoogleTest suite is not built on Windows because several test files call POSIX ::setenv(). No workflow names DIRECTX9.

The POSIX-environment rule behind a Windows CnaTests

The header of the native workflow still gives POSIX ::setenv() in test files as the reason it does not build CnaTests. That reason belongs to an earlier state of the tree, and it is also a rule with a gate of its own. MinGW-w64, like MSVC, has neither setenv nor unsetenv, so a single call in any translation unit that reaches a Windows build stops CnaTests.exe from compiling. The repair recorded in docs/cnatests-mingw-setenv-proposal.md replaced the call sites with System::Environment::SetEnvironmentVariable(name, value), where an empty value means unset. At this snapshot 40 CNA source files use that call.

Eleven new POSIX calls had appeared by 2026-08, so the rule is now enforced as a text check. check_no_posix_setenv.py scans every .cpp and .hpp under modules/, tools/ and tests/, skips comment lines and third-party trees, and fails on any setenv( or unsetenv( call. The script's point is to fail on a Linux build long before anyone attempts a Windows one. It is registered as the CTest CNAEXT_NoPosixSetenv (label CnaExt) in graphics-ext/examples/CMakeLists.txt, in every tree with tests enabled once a Python 3 interpreter has been found, whatever renderer is selected.

At 009d40f5 the gate does not pass. Run read-only against the snapshot tree for this page, it reports twelve calls. One is in the Wayland platform itself: WaylandPlatform.cpp unsets XDG_ACTIVATION_TOKEN after consuming it. The other eleven are in four Wayland test files that set up a private compositor environment. None of that code is compiled for a Windows target, so the Windows build the rule protects is not affected. The gate, however, matches text, not targets, so an unfiltered ctest in a test-enabled tree reports CNAEXT_NoPosixSetenv as failed (see Known Issues). A red result from this test therefore says "a POSIX environment call exists somewhere in the tree", not "the Windows build is broken".

What each tier proves

TierProvesDoes not prove
Source exists, cross-build linksthe code compiles for Windows with MinGW-w64 and the symbols resolveany behaviour
Gated Wine run with GPU readbackcall sequencing, resource lifetimes, HLSL compilation and pixel arithmetic through DXVK or vkd3d-proton on one Vulkan driver; results come from GPU readback, not from an S_OKWindows driver behaviour, DXGI presentation and tearing, a genuine device removal
Forced-headless DIRECTX12 fixturethe draw, resource and readback paths against an implicit off-screen back bufferswap-chain creation, Present(), window resize (only the three Proton fixtures and the manual diagnostic touch those)
XNA oracle (D3D9_XNA_Diff)byte-exact agreement with real XNA 4.0 on the committed scenes when the test passes, with both sides running on the same DXVK layer and GPU (CNA's records report all 39 matching; the last dated report covers 31 - see the oracle records)anything the scenes do not exercise; native Windows; CI
Manual native Windows workflowMSVC build, off-screen suites on real Windows runtime DLLs, HLSL accepted by a real D3DCOMPILER_47.dllphysical display, multi-vendor drivers, device removal

CNA's own tracking keeps these items open for both modern renderers: real DXGI presentation and tearing, a genuine device-removed recovery (the detection code and a deterministic simulated recovery exist; see device recovery), MSVC-versus-MinGW ABI parity beyond the manual job, and multi-vendor drivers. For DIRECTX9, the plan item for real Windows hardware is the only one marked as needing a human; its text calls the oracle re-run on Windows the definitive result.

Debug layers and diagnostics

  • DIRECTX11 requests D3D11_CREATE_DEVICE_DEBUG in non-NDEBUG builds; CNA_D3D11_DEBUG_LAYER=1 forces it on in any build type and 0 forces it off. When the SDK debug component is missing (DXGI_ERROR_SDK_COMPONENT_MISSING) device creation retries without it. Informational messages are filtered at the info queue and the rest are drained into the shared debug-layer log after every Present().
  • DIRECTX12 captures its policy once per device from the environment (D3D12Configuration.cpp): CNA_D3D12_ADAPTER=hardware|warp (hardware never falls back to WARP), CNA_D3D12_DEBUG_LAYER, CNA_D3D12_GPU_VALIDATION (which implies the debug layer) and CNA_D3D12_DRED for device-removed breadcrumbs. An invalid value is logged and the safe default is used; a recovered device is recreated under the same captured policy.
  • CNA_D3D12_DESCRIPTOR_TRACE prints the descriptor allocators' real counters (see descriptor heaps).

Read in this order

  1. DirectXParityTests.cmake: the fixture inventory, its exceptions and the cross-compile environment rules.
  2. run-wine-dxvk.sh and run-wine-vkd3d.sh: the gates and prefix variables.
  3. run-proton-vkd3d.sh: the Direct3D 12 swap-chain root cause.
  4. d3d-windows-ci.yml: what the native job runs and what it disclaims.
  5. DirectX9 examples CMake and the oracle README: the fidelity renderer's own registrations; the method is on Using the XNA oracle as evidence.

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

Tests and validation
Test architecture: GPU tests