Tutorial 104: The Historical DirectX Ladder

CNA Tutorials  ·  Renderers

What you’ll learn

  • What each rung of CNA's DirectX ladder actually talks to, from DirectDraw v1 to Direct3D 10.
  • The naming trap: today's DIRECTX3 is not the renderer that used to be called DX3.
  • Where the permanent wall is — and why it is shaders, not 2D versus 3D.
  • How to build and run a rung, and what a run under Wine does and does not establish.

Before you startTutorial 103: Direct3D 11 and 12 on Windows sets up the MinGW-w64 cross-compile and Wine workflow every renderer on this page shares.

CNA carries renderers for DIRECTX1, DIRECTX2, DIRECTX3, DIRECTX5, DIRECTX6, DIRECTX7, DIRECTX8 and DIRECTX10, alongside the modern DIRECTX9, DIRECTX11 and DIRECTX12. (There is no DIRECTX4: Microsoft never released DirectX 4.)

These are not emulations or shims. Each rung includes the genuine Windows headers and drives the genuine COM interfaces of its own generation — IDirectDraw v1 for DIRECTX1, IDirectDrawSurface4 and IDirect3DDevice3 for DIRECTX5, ID3D10Device for DIRECTX10. What each rung can render is decided by what that generation of DirectX genuinely offered.

The naming trap. Today's DIRECTX3 is real DirectX 3: DirectDraw v2 plus Direct3D v2. It is not the renderer previously documented as DX3, which fronted the ../free-direct sibling reimplementation — that renderer still exists and is now called FREEDIRECT. The two are unrelated implementations that briefly shared a name.

The ladder, rung by rung

ValueWhat it drivesThe step it adds
DIRECTX1IDirectDraw / IDirectDrawSurface, v1 interfaces onlyThe starting point. DirectX 1 (1995) shipped no Direct3D at all.
DIRECTX2DirectDraw v1 plus IDirect3D2 / IDirect3DDevice2The first real 3D pipeline: DrawPrimitive / DrawIndexedPrimitive, a z-buffer, one texture.
DIRECTX3The same 3D layer, with the DirectDraw object upgraded to IDirectDraw2Exactly one delta from DIRECTX2: a QueryInterface to the v2 DirectDraw object.
DIRECTX5IDirectDraw4 / IDirectDrawSurface4 plus IDirect3D3 / IDirect3DDevice3Everything moves to v4 surfaces and DDSURFACEDESC2; vertices are submitted through the FVF bitmask; Clear2 replaces a manual z-buffer lock.
DIRECTX6The same interfaces as DIRECTX5No new COM interface at all — DirectX 6 added render states. The deliverable is a real stencil buffer: a combined depth-plus-stencil surface and real stencil render states.
DIRECTX7IDirectDraw7 / IDirect3D7, created with DirectDrawCreateExThe viewport object disappears entirely — SetViewport and Clear become direct device methods — and textures bind straight to a stage with no handle indirection.
DIRECTX8IDirect3D8 / IDirect3DDevice8, no DirectDraw at allDirectDraw and Direct3D merge. One CreateDevice call creates the device and its swap chain — the whole shadow-backbuffer arrangement the earlier rungs need disappears.
DIRECTX10ID3D10Device with real vs_4_0 / ps_4_0 HLSLThe fixed-function pipeline is gone. Every draw — 2D included — is a compiled shader pair, and state is real state objects rather than per-call render states.

Reading down that table, three genuine architectural boundaries stand out, and they are where the interesting behaviour changes: the arrival of DrawPrimitive at DIRECTX2, the disappearance of DirectDraw at DIRECTX8, and the disappearance of fixed function at DIRECTX10.

The wall is shaders, not 3D

It is tempting to assume the old rungs are “2D only”. They are not — DIRECTX2 onwards have real geometry, real depth-test occlusion and real texture sampling. The actual boundary is different, and it is absolute:

A custom ShaderEffect can never run below DIRECTX9. XNA 4.0 was built on Direct3D 9 and Shader Model 2.0; its Reach profile targets SM 2.0 and HiDef targets SM 3.0. DIRECTX1 through DIRECTX7 have no programmable shader stage at any level, and CNA's DIRECTX8 renderer is deliberately scoped to fixed function because Shader Model 1.x cannot host an SM 2.0 effect either. This is a property of the hardware era, not a gap someone forgot to close.

What the fixed-function rungs can do is reconstruct the stock effects, because CNA hands renderers a structured description of a draw — matrices, directional lights, material colours, textures, fog, alpha test, bone transforms and the flags that select the variant — rather than opaque shader bytecode. BasicEffect was designed to mirror the fixed-function pipeline in the first place, so the mapping is direct.

DIRECTX10 is the odd one out at the far end: it has real HLSL, but at vs_4_0/ps_4_0, and its scope in CNA is deliberately bounded as a first version rather than being a fidelity target.

DIRECTX1 is a declared stub for 3D

DIRECTX1's entire 3D pipeline throws. Every 3D entry point — depth and stencil clears, SetDepthTestEnabled, vertex and index buffer creation, both Draw*Primitives families — raises a clear DIRECTX1 (DirectDraw v1) does not support 3D message, and the optional resource factories (CreateTexture3D, CreateTextureCube, CreateRenderTargetCube, CreateOcclusionQuery, CreateEffectRenderer) return nullptr.

That is not a shortfall against DirectX 1; it is an accurate reflection of it. There was no Direct3D COM interface in 1995 to call. What DIRECTX1 does deliver is a complete 2D renderer: real textures and render targets, the full SpriteBatch surface including rotation, scale, tint, flip and a custom transform, all four blend modes, Wrap/Mirror/Clamp addressing, Point/Linear filtering, SpriteFont text, and correct window-to-logical mapping under letterbox scaling.

If you want a 3D game to run on a 2D-only rung rather than abort, set the unsupported-call policy described in Tutorial 101:

device.SetUnsupported3DGraphicsCallBehavior(
    CNA::Unsupported3DGraphicsCallBehavior::WarnAndStub);

Why the DirectDraw rungs carry a CPU rasteriser

IDirectDrawSurface::Blt has never supported rotation, in any version of DirectX. So every DirectDraw-era rung — DIRECTX1 through DIRECTX7 for their 2D path — needs a CPU compositor: a two-triangle edge-function rasteriser that implements rotation, scaling, blending and sampling in software, with an identity fast path that falls back to a straight BltFast copy when nothing needs transforming.

Build these in Release before judging performance. A CPU-rasterising renderer is exactly the kind of code an unoptimised build destroys. This was found the hard way on DIRECTX1: a build directory with an empty CMAKE_BUILD_TYPE made the renderer look far slower than it is. Correctness is unaffected — pixel assertions do not care about optimisation — but a performance impression from a Debug build is meaningless.

From DIRECTX8 onward the compositor disappears, because there is a real swap chain and hardware-accelerated textured triangles to batch sprites into.

Building and running a rung

Every rung shares the Windows gate and the MinGW-w64 cross-compile route from Tutorial 103:

cmake -S . -B build-dx7 \
      -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake \
      -DCNA_GRAPHICS_RENDERER=DIRECTX7 \
      -DCMAKE_BUILD_TYPE=Release \
      -DCNA_BUILD_TESTS=ON
cmake --build build-dx7 -j3

# Each rung has its own Wine wrapper script
scripts/run-wine-directx7.sh build-dx7/examples/directx7_smoke_test.exe

The wrappers are per-rung: run-wine-directx1.sh, run-wine-directx2.sh, run-wine-directx3.sh, run-wine-directx5.sh, run-wine-directx6.sh, run-wine-directx7.sh, run-wine-directx8.sh, run-wine-directx10.sh. DIRECTX1 additionally ships scripts/check-directx1-v1-only.sh, which enforces that the renderer never reaches for a v2-or-later DirectDraw interface.

Two rungs use a translation layer rather than Wine's own DirectDraw implementation: DIRECTX8 goes through DXVK's D8VK d3d8.dll, and DIRECTX10 goes through Wine's thin builtin d3d10.dll forwarding to DXVK's d3d10core.dll plus DXVK's dxgi.dll. The rest run on Wine's built-in ddraw.

Two Windows renderers that are not on the ladder

ValueWhat it isExtra requirement
GDIClassic Win32 GDI with private CPU 2D rasterisation. 2D-only; a custom ShaderEffect throws. It advertises a 2D stencil-mask extension without claiming a depth attachment or a 3D pipeline.The Windows gate only.
GLIDE3dfx Glide 3.x, dynamically loaded from glide3x.dll.A 32-bit (i686) toolchain. Glide's application ABI carries the render-window handle in a 32-bit value, so configuring a 64-bit target is a FATAL_ERROR. Use cmake/toolchains/mingw-w64-i686.cmake.

DIRECT2D is also on the Windows gate; it is a modern 2D-only renderer rather than part of the historical sequence.

What a ladder result means

Every renderer on this page is developed and verified through MinGW cross-compilation plus Wine on Linux — not on real Windows hardware, and not on period hardware. Wine's DirectDraw and DXVK's D3D8/D3D10 paths are themselves reimplementations, so a passing result establishes that CNA drives the API correctly, not that a 1997 graphics card would agree.

These renderers are also explicitly not held to the DIRECTX9 oracle bar. DIRECTX9 is the renderer that matches all 39 scenes of CNA's XNA 4.0 oracle corpus at tolerance 0. The ladder rungs are retro and alternative renderers, validated by their own pixel checks — interesting for authenticity and completeness, not for XNA fidelity.

46 renderer identities are not 46 equally complete renderers, and the ladder is where that shows most. Choose a rung because you want that API generation specifically. If you just want a Windows build, DIRECTX11 is the ordinary answer, and DIRECTX9 is the answer for XNA fidelity.

Where to go next