Windows from Linux: MinGW cross-builds, runtime staging and Wine evidence

CNA snapshot 009d40f5  ·  Deep Dives › Cross-platform engineering  ·  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. Checked by reading the toolchain file, ThirdPartySDL.cmake, the CTest wiring, the Wine and Proton scripts, the Win32 validation plan and the workflow files at 009d40f5. No Wine, Proton or Windows run was executed for this page; the 389-test figure is CNA's own record.

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 or Direct2D 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 Win32 platform harness, 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

  1. Select the target. mingw-w64.cmake names the compilers and confines searches to the cross root.
  2. Build dependencies for the target. The vendored SDL3, SDL3_image and SDL3_mixer are configured, built and installed for Windows at configure time.
  3. Link a PE executable against GNU import libraries.
  4. Stage the runtime. Copy the SDL DLLs and whichever MinGW runtime DLLs the link strategy left dynamic beside the executable.
  5. 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:

HelperCopiesUsed for
cna_copy_sdl_runtimethe SDL3, SDL3_image and SDL3_mixer DLLs of the target installevery Windows executable that links SDL (skipped for the web and Android)
cna_copy_mingw_runtimelibwinpthread-1.dllMinGW test executables, which link libgcc and libstdc++ statically and so need only the threading DLL
cna_copy_mingw_cxx_runtimethe 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 DLLapplication 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: the standalone platform harness (standalone_tests/CMakeLists.txt) resolves windows through dynamic_cast across the platform interfaces, and GCC 13's mingw-w64 libstdc++.a also defines the inline std::type_info::operator== in tinfo.o, which a static link reports as a multiple definition. Those harness targets use the dynamic runtime and copy all five candidate DLLs. Renderer-specific runtime pieces (the Direct3D translators, Direct2D's Wine runtime) 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 rendererEmulatorGate bypass set for discovery and unit tests
DIRECTX9scripts/run-wine-dxvk9.shCNA_D3D9_SKIP_DXVK_GATE=1
DIRECTX11scripts/run-wine-dxvk.shCNA_D3D11_SKIP_DXVK_GATE=1
DIRECTX12scripts/run-wine-vkd3d.shCNA_D3D12_SKIP_VKD3D_GATE=1
DIRECT2D in the compiled setscripts/run-wine-direct2d.shCNA_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, chosen in this order: the default renderer decides for DIRECTX9, DIRECTX11 and DIRECTX12, the Direct2D launcher is used when DIRECT2D is anywhere in the compiled set, and a cross tree that matches none of these gets no emulator at all (CNA-GAP-059). The strict public-API compile check has its own copy of the same wiring for the three Direct3D defaults (Harnesses.cmake, cna_strict_xna_api_check).

Two registrations differ on purpose. The GDI examples are built in a cross tree but registered with CTest only when not cross-compiling (GDI examples CMake): a cross-built PE needs the developer's own Wine and display setup, so GDI's automatic target is native Windows. The DIRECT2D registrations run through run-wine-direct2d.sh with the DXVK gate bypassed, or through the pinned Proton launcher; the next sections explain what that does and does not prove.

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 harnessRuntime that implements the APIPrefix and wrapper
DIRECTX9DXVK d3d9~/.wine-cna-d3d11 by default (its own variable CNA_D3D9_WINEPREFIX); run-wine-dxvk9.sh
DIRECTX11DXVK d3d11 and dxgi~/.wine-cna-d3d11; run-wine-dxvk.sh
DIRECTX12vkd3d-proton d3d12/d3d12core~/.wine-cna-d3d12; run-wine-vkd3d.sh; swap-chain work through Proton in ~/.wine-cna-d3d12-protonrun (run-proton-vkd3d.sh)
DIRECT2DWine's built-in d2d1, d3d11 and dxgi; or a pinned Proton runtime~/.wine unless CNA_DIRECT2D_WINEPREFIX names another; run-wine-direct2d.sh or run-proton-direct2d.sh
GDIWine's gdi32no wrapper script; not registered in a cross tree
Win32 platform harnessWine's own user32/gdi32 (and a Direct3D probe on the produced window)a fresh prefix created by wineboot --init in CI

Direct2D deliberately does not borrow the D3D11 prefix: its wrapper's header records that a DXVK-only prefix can lack Wine's d2d1 runtime even where the normal Wine installation supports Direct2D. A result labelled only "ran under Wine" hides exactly the implementation the test was meant to validate.

 MinGW-w64 cross-build --> Windows PE test + staged DLLs
                                   |
                                   v
                 wrapper script (scripts/run-wine-*.sh, run-proton-*.sh)
                 selects prefix, DLL overrides, virtual display, logs
                                   |
                                   v
 dedicated Wine prefix --> runtime that implements the API
 (bitness, registry,       DXVK d3d9 / d3d11+dxgi, vkd3d-proton d3d12,
  installed runtimes)      Wine built-in d2d1 / gdi32
                                   |
                   +---------------+----------------+
                   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
Figure. The Windows-on-Linux engagement chain. A MinGW-built PE test with its staged DLLs is launched by a wrapper script that selects a dedicated Wine prefix, which decides which runtime (DXVK, vkd3d-proton or Wine's built-in DLLs) implements the API on top of the Linux driver and GPU. A behaviour or pixel verdict is credited only when the captured run log also contains the marker of the intended runtime; the Direct3D wrappers exit with status 3 when it is missing. Direct2D and GDI have no such marker, so for them the prefix and environment controls are the evidence.

Engagement is proven per route, not per Wine run

For the three Direct3D identities 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, and the Proton route for D3D12 swap chains, 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. Direct2D has no d2d1 marker: run-wine-direct2d.sh reuses the D3D11 launcher for its logging, and every Direct2D registration sets CNA_D3D11_SKIP_DXVK_GATE=1, so the log does not prove which Direct2D implementation ran. Its evidence is instead the environment, and the scripts make that environment explicit:

  • A known display. Direct2D presents to a real HWND, so run-direct2d-virtual-display.sh runs every invocation on an isolated Xvfb server with a private X authority and a known geometry and depth (it is re-entrant; CNA_DIRECT2D_USE_HOST_DISPLAY=1 opts out for interactive debugging).
  • A prefix created from nothing. run-direct2d-fresh-wine-suite.sh runs the whole Direct2D CTest label against a throwaway prefix whose only dependency is wineboot --init: no winetricks package, no native DLL override, no DXVK. Its header states the contract this proves, that Wine's built-in d2d1, d3d11 and dxgi are what gets exercised, so a pass cannot depend on a developer's accumulated prefix state.
  • A pinned Proton. The Proton route reads direct2d-proton-pin.txt, which lists Proton 9.0 (Beta) then Proton 8.0 in priority order, refuses to run without the pin file, and refuses the moving "Proton - Experimental" runtime unless CNA_DIRECT2D_PROTON_ALLOW_EXPERIMENTAL=1 is set for a one-off investigation that must not be quoted as evidence. Every run also publishes the runtime it used (ProtonDirectory, ProtonVersion, ProtonDistVersion, DxvkD3d11Path and DxvkVersion) to stderr and, with CNA_DIRECT2D_PROTON_IDENTITY_FILE, to a file. The pin logic itself (runtime selection, the Experimental refusal, that identity output) is recorded as verified against a prepared fake Steam tree (plan row D2D-122), and the tree holds no identity artifact from an installed pinned runtime, so this is a control that exists rather than a recorded Proton result.

The two Proton launchers do not share that discipline: run-proton-vkd3d.sh still defaults to the local Proton - Experimental install (overridable with CNA_D3D12_PROTON_DIR), so a Direct3D 12 swap-chain result names a runtime that Steam may upgrade underneath it. GDI has no wrapper and no marker at all. Controls such as the isolated display and the 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. Both Proton launchers hard-refuse any prefix path that resolves to ~/.wine; the D3D12 launcher's comment records the incident that motivated the guard, in which a Proton wine binary run without WINEPREFIX fell back to the developer's personal prefix, silently upgraded it to Proton's newer Wine, and hung in a Mono installation wizard for hours. Every Wine command in that launcher now names its prefix explicitly, and the one-time bootstrap of a fresh Proton prefix runs under timeout --kill-after (CNA_D3D12_PROTON_BOOTSTRAP_TIMEOUT, default 300 seconds), tearing down its own wineserver instead of leaving zombie process trees. 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. The number of prefixes is not an architecture metric; isolation of bitness, overrides and installed runtimes is the contract.

The Win32 platform harness under Wine

The only automatic Windows evidence at this snapshot is the win32-cross job of platform-ci.yml. It installs g++-mingw-w64-x86-64, Wine and Xvfb, configures tools/platform/standalone_tests with the MinGW toolchain file, creates a fresh prefix in the runner's temporary directory, runs cna_platform_tests.exe (the platform contract and Win32 suites) and then cna_win32_directx_probe.exe against a window the backend produced. It builds the platform module and its harness, not the engine; the full engine for Windows is built by hand or by the manual MSVC workflows.

A worked case: the test that asserted a screen size

CNA's Win32 validation plan (plan_win32_native_validation.md, finding WINNATIVE-F2) records why the Wine run and the native runs have to be read together. Three Win32 tests requested a 1024×768 client area and failed with sizes such as 964×518. Windows silently clamps a client area to what the monitor's work area can hold once the frame is added, so a test that names an absolute size and asserts it round-tripped is also asserting a minimum screen size. The controlled experiment was the same executable on the same machine: it passed on the 1920×1080 interactive desktop, failed in session 0 (whose window station reports 1024×768), and failed under Wine, whose prefix had a 1024×768 virtual desktop at 192 DPI. The fix, Win32TestDesktop.hpp, derives the requested size from SPI_GETWORKAREA and the measured frame while keeping the round-trip assertion exact; the plan reports the Wine run at 389 tests, 388 passed and 1 skipped afterwards (CNA's record; not re-run here). The lesson generalises: a translated or virtual desktop is a host with its own geometry and DPI, and a test that does not name its assumptions measures the host.

Native MSVC is a separate tier

Four workflows run on real Windows runners with MSVC, and all are manual (or bound to one branch), so none is an automatic merge gate:

  • win32-native in platform-ci.yml (workflow_dispatch only): the same harness built by a second compiler, on a real per-monitor-DPI desktop with real Direct3D 11 and 12 runtimes, shell dialogs and clipboard;
  • d3d-windows-ci.yml: DIRECTX11, DIRECTX12 and DIRECT2D built with MSVC and run against the real runtime DLLs, with the HLSL recompiled by a genuine compiler;
  • gdi-windows-ci.yml: the GDI suite, the only place GDI's CTests are registered (a cross tree does not register them), and a manual dispatch, so GDI has no automatic run at all;
  • content-pipeline-windows-ci.yml (pushes to one named branch plus dispatch): a HEADLESS content-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. Interactive IME is not on that list because there is nothing to establish: the Win32 backend reports ime false and handles no WM_IME_* message (CNA-GAP-058). MinGW plus Wine, native MSVC automation and a person using Windows hardware are three separate evidence tiers.

One more source sits between the last two and is not a workflow: plan_win32_native_validation.md records a manual campaign on a Windows 10 22H2 virtual machine (VirtualBox, MSVC 19.44, a VBoxSVGA virtual GPU) driven from Linux by tools/platform/windows_vm_*.sh. By that record the standalone platform harness ran 389 tests on the interactive desktop (386 passed, 3 skipped, none failed), the full CnaTests ran to completion (8,136 tests; 1,537 failures, of which 1,480 are one Direct3D 11 refusal the plan leaves unexplained and 44 are classed as Windows-only), keyboard, text, mouse and raw input were driven with SendInput (24 checks), the clipboard was checked against Notepad, DPI was measured at 100, 125, 150 and 200 percent, and a 300-second soak of a real application kept USER, GDI and thread counts flat. It also found defects that MinGW and Wine could not show: every example application failed to link with MSVC (WinMain), and the path-containment guard accepted a rooted path. These are CNA's own figures, not re-run here, and because the guest has a virtual GPU none of it is evidence about a physical Windows driver; the plan itself records physical-GPU validation as not done.

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 and 12 expose surfaces closer to what native applications use, yet they still execute 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.

TierEstablishesDoes not establish
Cross-build linksthe code compiles for Windows with MinGW-w64 and its symbols resolvethat the staged directory starts, or any behaviour
Staged executable runs under Winethe PE, its DLL set and the platform contract work on Wine's implementation of Windowswhich graphics runtime handled a device, unless a marker says so
Engaged translator with GPU readbackcall sequencing, lifetimes, shader compilation and pixel arithmetic through DXVK or vkd3d-proton on one named Vulkan driverWindows driver behaviour, DXGI presentation and tearing, genuine device removal
Manual native MSVC workflowa second compiler and the real Windows runtime DLLsphysical displays, multi-vendor drivers, interactive input
A person on Windows hardwareauthentic device loss, fullscreen transitions, real XNA 4.0 outside Wine(the remaining high-value check; not recorded at this snapshot)

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

  1. mingw-w64.cmake: the target selection and its i686 fallback.
  2. ThirdPartySDL.cmake: cna_configure_vendored_sdl, the prebuilt keys and the three runtime-copy helpers.
  3. UnitTests.cmake and Harnesses.cmake: the emulator chains and their gate bypasses.
  4. run-wine-direct2d.sh, run-direct2d-fresh-wine-suite.sh and run-proton-direct2d.sh: the Direct2D controls.
  5. run-proton-vkd3d.sh: the prefix guard, the bootstrap timeout and the incident behind them.
  6. platform-ci.yml: win32-cross and win32-native.

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

Tests and validation
Test architecture