Tutorial 103: Direct3D 9 and SDL_GPU on Windows

CNA Tutorials  ·  Renderers  ·  CNA snapshot 3b43bd86

ℹ

What you’ll learn

  • Which two renderers a Windows build uses: DIRECTX9, the XNA-fidelity renderer, and SDL_GPU, the Windows default on SDL’s Direct3D 12 driver.
  • Cross-compiling both from Linux with MinGW-w64.
  • Running and testing the resulting .exe under Wine — DXVK for Direct3D 9, vkd3d-proton for Direct3D 12 — including the guards that stop a silent fallback.
  • Exactly what Wine verification proves, what the manual native-Windows workflow adds, and what each renderer reports.

Before you start — Tutorial 72: Choosing a Renderer covers renderer selection and the platform gates this tutorial works within.

A Windows build of CNA has two renderers to choose from. DIRECTX9 is real Direct3D 9 — the API XNA 4.0 itself ran on — and the renderer CNA records as pixel-exact against the genuine XNA runtime; it is hard-gated to Windows. SDL_GPU is CNA’s modern native path and the default when you name no renderer: CNA maps XNA 4.0 onto SDL3’s GPU API, and on Windows SDL drives its Direct3D 12 driver underneath. Both are developed mostly on Linux, cross-compiled with MinGW-w64 and run under Wine. In this snapshot that route is manual, and the native-Windows MSVC workflow is manual-dispatch only, which is why this tutorial distinguishes what a Wine result proves from native-hardware coverage.

⚠

The Windows default is not qualified on Windows yet. SDL_GPU is validated on Linux (SDL’s Vulkan driver) and, cross-compiled, under Wine with vkd3d-proton (its Direct3D 12 driver). CNA’s handoff lists a native Windows run on Direct3D 12 as not done. If you ship on Windows today, test both renderers on your own hardware; DIRECTX9 is the alternative with the longer Wine record.

The Windows gate is hard — for DIRECTX9

DIRECTX9 refuses to configure unless CMAKE_SYSTEM_NAME is Windows. Attempting it on a native Linux or macOS configure is a FATAL_ERROR, not a warning, and the message points you straight at the cross-compilation toolchain:

CNA: DIRECTX9 renderer only builds when targeting Windows. Either build
natively on Windows, or cross-compile from Linux with
-DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake

The reason is simply that d3d9.h does not exist anywhere else. No CMake dependency is fetched for this renderer — the headers and import libraries (d3d9, d3dcompiler) come from the Windows SDK under MSVC, or from MinGW-w64's own copies when cross-compiling. SDL_GPU has no such gate: it builds wherever SDL3 does, and on Windows it fetches SDL_shadercross (with SPIRV-Cross) by default to translate its SPIR-V stock shaders for SDL’s Direct3D 12 driver. On the platform side a Windows target uses the SDL3 platform, which reaches Windows through SDL’s own windows video driver and hands DIRECTX9 the window’s HWND; iOS accepts neither.

Cross-compiling from Linux

sudo apt install mingw-w64

# Direct3D 9 (the XNA-fidelity renderer)
cmake -S . -B build-dx9 \
      -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \
      -DCNA_GRAPHICS_RENDERER=DIRECTX9 \
      -DCNA_BUILD_TESTS=ON
cmake --build build-dx9 --parallel

# SDL_GPU, the Windows default (SDL picks its Direct3D 12 driver at run time)
cmake -S . -B build-win-sdlgpu \
      -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \
      -DCNA_GRAPHICS_RENDERER=SDL_GPU \
      -DCNA_BUILD_TESTS=ON
cmake --build build-win-sdlgpu --parallel
⚠

The renderer name changed. D3D9 is not an accepted CNA_GRAPHICS_RENDERER value any more — use DIRECTX9. The CTest label moved with it, so ctest -L D3D9 matches nothing; the label is DIRECTX9. SDL_GPU’s own tests carry the label SdlGpu.

The two renderers’ example and test executables land at the root of the build directory (the smoke tests are cna_test_directx9_smoke and cna_test_sdlgpu_smoke). The DIRECTX9 test executables are linked statically against the MinGW C and C++ runtimes and get the MinGW and SDL runtime DLLs copied next to them, so a test .exe runs in a Wine prefix without an installed toolchain. Both need cmake --build against a real target. CNA itself is an interface umbrella library with no sources and is not buildable; build CnaTests, a demo target, or the whole build directory.

The translation layers

A cross-compiled .exe runs on the development machine under Wine, with its Direct3D calls translated to Vulkan on the real GPU. Each Direct3D generation has its own translator, its own Wine prefix and its own wrapper script:

RendererTranslatorWine prefix (override)Wrapper script
DIRECTX9DXVK (Direct3D 9 to Vulkan)~/.wine-cna-d3d11 (CNA_D3D9_WINEPREFIX)scripts/run-wine-dxvk9.sh
SDL_GPU on SDL’s Direct3D 12 drivervkd3d-proton (Direct3D 12 to Vulkan)~/.wine-cna-d3d12 (CNA_D3D12_WINEPREFIX)scripts/run-wine-vkd3d-headless.sh, which calls run-wine-vkd3d.sh
# DXVK, for DIRECTX9: the default prefix is one that dxvk-setup prepared
sudo apt-get install -y dxvk-wine64
WINEPREFIX=~/.wine-cna-d3d11 wineboot --init
WINEPREFIX=~/.wine-cna-d3d11 dxvk-setup install

scripts/run-wine-dxvk9.sh build-dx9/cna_test_directx9_smoke.exe

# vkd3d-proton, for SDL_GPU: a separate prefix whose d3d12/d3d12core DLLs
# are vkd3d-proton's, registered as native overrides
scripts/run-wine-vkd3d-headless.sh build-win-sdlgpu/cna_test_sdlgpu_smoke.exe

The prefixes are kept apart on purpose: DXVK and vkd3d-proton install different native-DLL overrides, and sharing one prefix would mean re-registering them between runs. Which DXVK the Direct3D 9 prefix carries matters too: CNA records that its compiled-effects test hangs or aborts in DXVK 2.6.0’s device teardown (Debian’s package) and passes with DXVK 3.1.1. The SDL_GPU wrapper starts a private Xvfb server for every run and selects SDL’s dummy video driver and its direct3d12 GPU driver, so nothing appears on your display: Wine and vkd3d need a virtual X server to enumerate the Vulkan-backed adapter even though no swap chain is created.

The guards that stop a silent fallback

This is the detail worth copying into your own workflow. If the DLL override is not in place, Wine quietly falls back to its own built-in WineD3D implementation. Your test still runs, still produces plausible pixels, and proves nothing about the path you meant to exercise.

The wrapper scripts therefore assert on the run's own log output before reporting success: run-wine-dxvk9.sh requires a DXVK: <version> line, and run-wine-vkd3d.sh requires vkd3d-proton’s applicationVersion line. If the line is missing, the script exits 3 rather than passing.

Environment variableEffect
CNA_D3D9_WINEPREFIX, CNA_D3D12_WINEPREFIXOverride the Wine prefix the wrapper uses.
CNA_D3D9_SKIP_DXVK_GATE=1Opt a binary out of the DXVK check — correct only for a test that legitimately never opens a Direct3D 9 device, such as a pure mapping-table unit test.
CNA_D3D9_ALLOW_WINED3D=1Bypass the DXVK check for a deliberate one-off WineD3D diagnostic run; never for normal runs.
CNA_D3D12_SKIP_VKD3D_GATE=1Opt a binary that never creates a device out of the vkd3d-proton check.

CTest wires the wrappers in automatically when you cross-compile, so ctest --test-dir build-dx9 -L DIRECTX9 and ctest --test-dir build-win-sdlgpu -L SdlGpu run every registered test through the guarded path rather than bare wine. A marker proves which translator handled the run; read the log line, not just the exit status.

What Wine verification proves, and what it does not

Running under DXVK or vkd3d-proton on a real GPU is a genuinely strong test: it exercises real call sequencing, real resource lifetimes, real shader compilation and real pixel maths, and the results come back from actual GPU readback rather than from an API returning S_OK. It is far more than a compile check.

⚠

It is not real-Windows verification, and CNA does not claim otherwise. A Wine run does not establish any of the following:

  • Real DXGI presentation and tearing behaviour.
  • Real device-lost / device-removed recovery — a genuine loss or removal cannot be triggered on the development loop.
  • Windows driver behaviour: on the DIRECTX9 loop even D3DCAPS9 is DXVK's synthesized capability set, and a vkd3d-proton run proves SDL’s Direct3D 12 driver against vkd3d-proton, not against a Windows driver.
  • MSVC-versus-MinGW ABI parity.
  • Multi-vendor driver behaviour — every Wine result comes from one Vulkan driver through one translator.

There is one native-Windows MSVC workflow for these renderers in this snapshot: d3d-windows-ci.yml (windows-latest). It is manual-dispatch only (workflow_dispatch) — it does not run on pushes or pull requests — and it has two legs, both built with MSVC and run with no Wine involved. The DIRECTX9 leg builds the renderer’s own CTest executables and runs ctest -L DIRECTX9 against the real Windows Direct3D 9 runtime. The SDL_GPU leg builds a focused smoke set and runs it with SDL’s windows video driver and direct3d12 GPU driver on whatever adapter the hosted runner offers, failing unless every program exits 0. The workflow scopes itself as a native-runtime regression gate, not a substitute for swap-chain, device-lost or driver-parity verification on a machine with a display. CNA’s handoff records the SDL_GPU leg as not run yet and no current real-Windows hardware validation. If you are shipping on Windows, budget for your own hardware verification pass.

What SupportsCapability() says on Windows

GraphicsCapability has 17 members. DIRECTX9 has no SupportsCapability() override at all and inherits the permissive base answers; SDL_GPU has its own switch with no permissive default arm, so an unknown enumerator answers false. The table shows each renderer’s own answer on a Windows build:

CapabilityDIRECTX9SDL_GPU (Windows build)
ThreeD, AnisotropicFiltering, WireFrame, Texture3D, Instancing, AdditiveBlendingtrue (inherited default)true
DepthStencilBuffer, StencilBuffertrue (inherited default)from the depth formats the device offers
MultiSampleAntiAliasingtrue (inherited default)from the sample counts the device offers
MultipleRenderTargetstrue, bounded by the graphics profile (Reach 1, HiDef 4)true
OcclusionQuerytrue; the query object is created only when the device passes Direct3D 9’s own support probefalse (SDL’s GPU API has no occlusion queries)
CustomEffectstrue (HLSL, compiled at run time with D3DCompile; applied to SpriteBatch draws, while 3D draws use the stock effects)false: ShaderEffect is compiled out of Windows builds
MultiStreamVertexInputfalsetrue
CompiledEffectsonly with -DCNA_DIRECTX9_COMPILED_EFFECTS=ONonly with -DCNA_SDL_GPU_COMPILED_EFFECTS=ON
FloatRenderTargets, HalfFloatRenderTargetsfalse (Color-only render targets)from the render-target formats the device can create
⚠

DIRECTX9 is the one renderer that does not override SupportsCapability(): its answers are the inherited base defaults, so nothing in them was authored for that renderer. Do not design a feature-detection path around those answers — probe the real operation instead, or query the richer capability profile that Tutorial 101 describes.

The compiled-effects switches (both default OFF) enable XNA Effect Framework bytecode through MojoShader; a default configure reports CompiledEffects as false on both. Positive answers are still a hint rather than proof that a particular draw path executes. For a richer, queryable picture — features, numeric limits and per-format usage — use the RendererCapabilityProfile API described in Tutorial 101.

A gap no capability answer reflects: binding a RenderTargetCube face inside a multiple-render-target set is refused by DIRECTX9 with NotSupportedException (a single cube face bound alone works); SDL_GPU binds a face in any slot of the set. On SDL_GPU, BlendState.MultiSampleMask on a multisampled target is applied only when it keeps every sample or none, and any other mask is refused with NotSupportedException; RendererFeature::MultiSampleMask reports that as Restricted. DIRECTX9 accepts only Color render targets, while it classifies Texture2D, cube and volume formats natively against the device.

If pixel-exactness is the goal: DIRECTX9

SDL_GPU is CNA’s modern path, but it is not the fidelity renderer. CNA's XNA oracle corpus is 39 scenes (256×256, HiDef) with reference PNGs captured from the genuine Microsoft XNA 4.0 runtime — run under Wine with DXVK on Linux, not on native Windows. DIRECTX9 is the only renderer recorded in the repository as matching all 39 at tolerance 0: the CTest D3D9_XNA_Diff renders every scene through a cross-built DIRECTX9 binary under Wine + DXVK and fails on any single-channel difference. The last consolidated dated divergence report covers 31 scenes (later per-scene notes record the rest), the run has not been repeated on native Windows hardware, and none of this runs in CI. That is a statement about DIRECTX9 over DXVK, not about Direct3D 9 on every native driver.

Pick SDL_GPU for the modern Windows path (and the default), remembering that it compiles ShaderEffect out on Windows and has no occlusion queries; pick DIRECTX9 when bit-identical XNA 4.0 output is the requirement, or when your game needs occlusion queries or custom HLSL SpriteBatch shaders on Windows (custom 3D shaders on Windows mean compiled XNA effects, behind either renderer’s compiled-effects option). Both draw SpriteBatch 2D as well as 3D.

Where to go next