Windows from Linux: MinGW cross-builds, runtime staging and Wine evidence
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. Checked by reading the toolchain file, ThirdPartySDL.cmake, the CTest wiring, the Wine scripts, the Windows validation plans and the workflow files at c1c316b9. No Wine or Windows run was executed for this page; the Windows runs quoted are CNA's own records.
Most of CNA's Windows engineering happens on Linux: CMake selects a MinGW-w64 target, builds SDL for that target, links a PE executable, stages the DLLs it needs, and teaches CTest to run it through a Wine wrapper that must prove the intended Direct3D runtime was really used. Every one of those steps has its own failure mode, so a successful compiler probe is not yet a runnable Windows program and a green Wine run is not yet a Windows result. This page covers the whole route: the toolchain file, target-specific SDL, runtime staging, CTest emulator wiring, which translation runtime owns each renderer, prefix hygiene, the native MSVC tier and what translated execution cannot prove. The Direct3D-specific gates and fixture inventory are on Direct3D evidence and Wine; the step-by-step recipe is Tutorial 103.
The route in five independent steps
- Select the target.
mingw-w64.cmakenames the compilers and confines searches to the cross root. - Build dependencies for the target. The vendored SDL3, SDL3_image and SDL3_mixer are configured, built and installed for Windows at configure time.
- Link a PE executable against GNU import libraries.
- Stage the runtime. Copy the SDL DLLs and whichever MinGW runtime DLLs the link strategy left dynamic beside the executable.
- Run it through the right wrapper in the right Wine prefix, and require the log marker of the intended runtime.
A failure at step 4 does not stop the build: it shows up only when the executable is started in a clean prefix. A failure at step 5 can be invisible: Wine can satisfy a Direct3D call with its own implementation and still draw a plausible picture. The rest of the page is about closing those two gaps.
cmake -S . -B build \
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \
-DCNA_GRAPHICS_RENDERER=DIRECTX11
cmake --build build --target CnaTests cna_test_directx11_smoke
ctest --test-dir build -R '^DirectX11_(DxvkGate|Smoke)$' --output-on-failure # DirectX11_Smoke runs through scripts/run-wine-dxvk.sh
The commands are the shape the CMake logic reads as valid (the build directory name is illustrative); they were not executed for this page, and the D3D11 run additionally needs a Wine prefix with DXVK installed, as described below.
The toolchain file
The file sets CMAKE_SYSTEM_NAME to Windows and CMAKE_SYSTEM_PROCESSOR to x86_64, then looks for x86_64-w64-mingw32-gcc. If it is found the triple is x86_64-w64-mingw32; otherwise the file silently falls back to i686-w64-mingw32. It sets the C, C++ and resource compilers (windres) from the triple, points CMAKE_FIND_ROOT_PATH at /usr/<triple>, keeps program lookup on the build host (CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) and confines libraries, headers and packages to the cross root (ONLY), so build-time tools stay native while nothing the target links can come from the Linux host.
The fallback is not a self-consistent 32-bit identity: CMAKE_SYSTEM_PROCESSOR stays x86_64 after the i686 compiler is chosen. It rescues compiler discovery on a host that only has the 32-bit toolchain, but anything keyed by processor (the persistent SDL install below, ABI-sensitive checks) is then labelled for the wrong architecture, so it must not be treated as a 32-bit lane. The tree has exactly two toolchain files, this one and ios.cmake; Android uses the NDK's own toolchain file and the web uses emcmake. The per-route summary is on configuring CNA.
SDL is built for the target, never borrowed from the host
Before renderer selection, the root build calls cna_configure_vendored_sdl() (ThirdPartySDL.cmake), which runs a nested configure, build and install of SDL3, SDL3_image and SDL3_mixer and forwards the parent's toolchain. A MinGW tree therefore receives Windows DLLs and import libraries instead of accidentally linking the Linux host's SDL, and SDL is built as a shared library for Windows (static only for the web and iOS).
The install is persistent and keyed so that routes cannot overwrite each other: .sdl-prebuilt-<system>-<processor> for native and cross builds (for example .sdl-prebuilt-Windows-x86_64 beside .sdl-prebuilt-Linux-x86_64), with the Apple deployment target and a Wayland suffix added where they change the objects, and two separate Emscripten roots for single-threaded and pthread builds. A fingerprint of the SDL sources, patches and configure arguments triggers a rebuild when any of them changes. The consequence for a maintainer is simple: a cache produced under one ABI must never be renamed into another's place; delete the exact root CMake reports and let it be regenerated under the intended toolchain. CNA_MAX_VENDORED_BUILD_JOBS (default 2) bounds the parallelism of these configure-time builds.
Runtime staging is part of a successful build
A PE file linked against an import library still needs the DLL at run time, and Windows (and Wine) look beside the executable first; there is no RPATH. CNA's post-build helpers in ThirdPartySDL.cmake therefore copy the runtime next to each Windows target:
| Helper | Copies | Used for |
|---|---|---|
cna_copy_sdl_runtime | the SDL3, SDL3_image and SDL3_mixer DLLs of the target install | every Windows executable that links SDL (skipped for the web and Android) |
cna_copy_mingw_runtime | libwinpthread-1.dll | MinGW test executables, which link libgcc and libstdc++ statically and so need only the threading DLL |
cna_copy_mingw_cxx_runtime | the shared unwinder (libgcc_s_seh-1.dll, libgcc_s_dw2-1.dll or libgcc_s_sjlj-1.dll, whichever the toolchain's exception model provides), libstdc++-6.dll, then the threading DLL | application targets that deliberately use the dynamic C++ runtime, such as cna_demo_2d in a MinGW build |
Each helper asks the selected compiler where the file lives (-print-file-name=) and, for the threading DLL, falls back to the compiler's bin directory. When nothing is found the result is a configure-time WARNING that the executable may not run on a machine without MinGW, not a build error. Verification must therefore start or inspect the staged directory; "target built" says nothing about step 4.
Why two link strategies exist is recorded in the tree. Test executables link the C and C++ runtimes statically so a test .exe runs in a prefix with no toolchain installed. Code with a large RTTI graph cannot always do that: application targets such as cna_demo_2d keep the dynamic C++ runtime and copy the GCC and libstdc++ DLLs next to themselves with cna_copy_mingw_cxx_runtime, which ThirdPartySDL.cmake keeps separate from the threading-DLL copy test targets need. Renderer-specific runtime pieces (the Direct3D translators) are not staged by any of these helpers; they come from the Wine prefix.
CTest has to know that a PE file is not a host executable
GoogleTest discovery executes CnaTests.exe to list its cases. Without help, CTest on Linux would try to run the PE file natively and discovery would fail before a single test ran. UnitTests.cmake therefore sets CMake's CROSSCOMPILING_EMULATOR property on CnaTests when cross-compiling, chosen by the default renderer:
| Default renderer | Emulator | Gate bypass set for discovery and unit tests |
|---|---|---|
DIRECTX9 | scripts/run-wine-dxvk9.sh | CNA_D3D9_SKIP_DXVK_GATE=1 |
DIRECTX11 | scripts/run-wine-dxvk.sh | CNA_D3D11_SKIP_DXVK_GATE=1 |
The bypass is narrow and deliberate: a bare --gtest_list_tests or a device-free unit test creates no Direct3D device and so can never print a translator marker, and an engagement gate would reject it for the wrong reason. Renderer smoke and fixture registrations instead put the wrapper into their own registered command without the bypass, because those executables do create a device and must prove the runtime engaged. A target carries one emulator property, so the default renderer decides: DIRECTX9 and DIRECTX11 each get their wrapper, and a cross tree with any other default gets no emulator at all, even when a Direct3D renderer is also in its compiled set (CNA-GAP-059). The strict public-API compile check has its own copy of the same wiring for the two Direct3D defaults (Harnesses.cmake, cna_strict_xna_api_check).
Which runtime owns each renderer under Wine
On the Linux loop a renderer name does not identify the code that executed it. The same CNA test executable can pass through Wine's own built-in DLLs, a DXVK component or vkd3d-proton, depending on the prefix it runs in:
| Renderer or harness | Runtime that implements the API | Prefix and wrapper |
|---|---|---|
DIRECTX9 | DXVK d3d9 | ~/.wine-cna-d3d11 by default (its own variable CNA_D3D9_WINEPREFIX); run-wine-dxvk9.sh |
DIRECTX11 | DXVK d3d11 and dxgi | ~/.wine-cna-d3d11; run-wine-dxvk.sh |
SDL_GPU cross tests (SDL’s Direct3D 12 driver) | vkd3d-proton d3d12/d3d12core | ~/.wine-cna-d3d12 (CNA_D3D12_WINEPREFIX); run-wine-vkd3d-headless.sh, which starts a private Xvfb and calls run-wine-vkd3d.sh |
MinGW-w64 cross-build --> Windows PE test + staged DLLs
|
v
wrapper script (scripts/run-wine-*.sh)
selects prefix, DLL overrides, virtual display, logs
|
v
dedicated Wine prefix --> runtime that implements the API
(bitness, registry, DXVK d3d9 / d3d11+dxgi,
installed runtimes) vkd3d-proton d3d12 (SDL_GPU's driver)
|
+---------------+----------------+
v v
Linux Vulkan/GL driver + GPU whole run log captured
| |
v v
pixels read back route-specific marker present?
| no -> exit status 3 (fail closed)
+---------------+----------------+
v
behaviour or pixel verdict credited
Engagement is proven per route, not per Wine run
For the two Direct3D identities, and for SDL_GPU’s Direct3D 12 driver, the wrappers fail closed: the DXVK launchers require a DXVK: version line in the run's own log, the vkd3d launcher requires vkd3d-proton - applicationVersion:, and a missing marker ends the run with exit status 3 and a message naming the likely silent fallback. The allow-WineD3D switches exist for deliberate one-off diagnostics and change the oracle rather than producing an equivalent pass. The gate itself is tested without Wine by a stub-based CTest. Those details are on Direct3D evidence and Wine. The transferable design is small: identify an implementation-specific positive marker, capture the whole child run, and fail when the marker is absent.
The engagement method is strong where it exists and absent elsewhere: a marker proves which translator handled the run, not how a Windows driver behaves. Controls such as an isolated display and a fresh prefix describe the conditions under which a result holds; they are not renderer behaviour on genuine Windows.
Prefix hygiene is part of the evidence
A Wine prefix is mutable system state: DLL overrides, bitness, registry entries, installed runtimes and first-run initialisation all live in it, and reusing a personal prefix for an automated experiment can change it irreversibly. The scripts encode that lesson: each wrapper names its prefix through its own variable (CNA_D3D11_WINEPREFIX, CNA_D3D9_WINEPREFIX, CNA_D3D12_WINEPREFIX), so two routes never share an environment contract by accident. Special-purpose prefixes also hold Microsoft's d3dcompiler_47.dll and a real 32-bit XNA 4.0 installation for the XNA oracle; how that oracle keeps both sides on one engaged DXVK path is on using the XNA oracle as evidence. Wine also runs a Microsoft build tool for the content pipeline: BuildXact invokes the real XactBld3 with /WINDOWS or /XBOX360 through a launcher named by CNA_XACTBLD_LAUNCHER (the tool itself by CNA_XACTBLD or SetXactCompilerEXT) and maps its paths with winepath (BuildXact.cpp); CNA records XactBld3 9.28.1886.0 building two unchanged XNA sample projects under Wine, while the Xbox 360 switch is checked only as an argument. The number of prefixes is not an architecture metric; isolation of bitness, overrides and installed runtimes is the contract.
Native MSVC is a separate tier
Three workflows run on real Windows runners with MSVC, and all are manual (or bound to one branch), so none is an automatic merge gate:
sdl3-windowsinplatform-ci.yml(workflow_dispatchonly): CNA’s SDL3 platform built with MSVC, its contract, window and process-exit entries, and the SDL3 window suites on SDL’swindowsvideo driver;d3d-windows-ci.yml:DIRECTX11built with MSVC and run against the real runtime DLLs, with the HLSL recompiled by a genuine compiler (CNA records a run on 2026-10-06 that passed all 300 registered CTest entries);content-pipeline-windows-ci.yml(pushes to one named branch plus dispatch): aHEADLESScontent-pipeline build that deliberately runs under a Unicode path and checks deterministic rebuild hashes.
The native MSVC settings that the first such builds needed (/utf-8, /bigobj, NOMINMAX) and the configure-time copy of the SDL DLLs into the build root are described on configuring CNA: native MSVC. A cloud runner, even with WARP or a real runtime, does not establish every raw input device, multi-monitor or fullscreen transitions, or behaviour on a physical GPU. MinGW plus Wine, native MSVC automation and a person using Windows hardware are three separate evidence tiers.
One more source is not a workflow: plan_windows11_directx.md records a native MSVC and physical-GPU baseline for DIRECTX11 on one Windows 11 laptop with an Intel Iris Xe GPU, with the Direct3D debug layer enabled and application runs of three CNA-based programs; it is one machine and one GPU vendor, and it is CNA’s record, not re-run here.
What translated execution cannot prove
Under DXVK, legacy D3DCAPS9 fields are synthesised from a modern Vulkan device. CNA's code can be shown to query and interpret those fields correctly, and its output can match real XNA 4.0 through the same engaged DXVK path, but the returned capability numbers are not observations of a period Windows driver. Direct3D 11 exposes surfaces closer to what native applications use, yet it still executes through translation. Hardware-backed runs also have to name the adapter: a Vulkan software implementation such as llvmpipe must never be mistaken for GPU evidence.
| Tier | Establishes | Does not establish |
|---|---|---|
| Cross-build links | the code compiles for Windows with MinGW-w64 and its symbols resolve | that the staged directory starts, or any behaviour |
| Staged executable runs under Wine | the PE, its DLL set and the platform contract work on Wine's implementation of Windows | which graphics runtime handled a device, unless a marker says so |
| Engaged translator with GPU readback | call sequencing, lifetimes, shader compilation and pixel arithmetic through DXVK (or, for SDL_GPU, vkd3d-proton) on one named Vulkan driver | Windows driver behaviour, DXGI presentation and tearing, genuine device removal |
| Manual native MSVC workflow | a second compiler and the real Windows runtime DLLs | physical displays, multi-vendor drivers, interactive input |
| A person on Windows hardware | authentic device loss, fullscreen transitions, real XNA 4.0 outside Wine | (recorded only for DIRECTX11 on one Intel laptop; the remaining high-value check for everything else) |
The strongest precise wording for the Direct3D 9 oracle result is therefore "pixel-verified against XNA through the same engaged DXVK path on the recorded GPU", not "validated on Windows hardware"; the engagement gates are what make the first claim trustworthy, by ensuring it is not a result from the wrong runtime. The platform-level picture is on the cross-platform contract.
Read in this order
mingw-w64.cmake: the target selection and its i686 fallback.ThirdPartySDL.cmake:cna_configure_vendored_sdl, the prebuilt keys and the three runtime-copy helpers.UnitTests.cmakeandHarnesses.cmake: the emulator chains and their gate bypasses.run-wine-dxvk.shandrun-wine-vkd3d.sh: the separate prefixes and the fail-closed engagement gates.platform-ci.yml: the manual native-MSVCsdl3-windowsjob.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Platforms: Windows · Tutorial 103: Direct3D on Windows · Building CNA
- Architecture
- Platform architecture
- Internals
- SDL3 platform internals
- Maintainer workflow
- Using the XNA oracle as evidence
- Tests and validation
- Test architecture