free-direct and free-api beneath the FREEDIRECT renderer

CNA snapshot 009d40f5  ·  Deep Dives › Sibling libraries  ·  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. CNA facts were read at 009d40f5; free-direct was read at develop @ 934f72ff and free-api at develop @ 53d7a312 (both 2026-07-18), which CNA does not pin. Status-tag counts were measured from the headers at that commit. No library, renderer test or game was built or run.

CNA's 2D-only FREEDIRECT renderer draws through two sibling libraries that were written for other programs: free-direct, a narrow DirectDraw, DirectSound and DirectPlay reimplementation over SDL3, and free-api, a minimal Win32 layer beneath it. This page explains how the three layers connect in CNA snapshot 009d40f5, what each library actually implements and how honestly it reports that, the macro hazard that shapes CNA's renderer code, which limitations of the renderer come from the libraries, and how to state evidence about a build that uses them. It is for anyone selecting, debugging or maintaining FREEDIRECT, and for anyone deciding whether a legacy DirectDraw program needs these libraries or CNA.

Revisions and how frozen they are

Both libraries were read at the tip of their develop branches: free-direct 934f72ff and free-api 53d7a312, both committed on 2026-07-18. Neither local clone has a commit on any ref after that day, so descriptions of the two libraries written since then are still about the same code. That stability is a fact about the libraries, not a CNA guarantee: CNA pins neither. RendererSelection.cmake adds whatever sits at ../free-direct when FREEDIRECT is selected, and nothing records which commit a build used. The consumer lock files that name free-direct, those of cna-template (pinned to a CNA commit of 2026-08-11) and cna-c-template, both record 934f72ff, the same tip, and neither names free-api at all, because free-direct brings it in; the lock files of cna-street and living-room-simulator do not mention free-direct.

The CNA-side evidence is narrower than for most renderers. No CNA workflow clones free-direct, so no CI job builds FREEDIRECT. Its 21 tests in the family's examples CMake (FreeDirect_Smoke, FreeDirect_ResizeTransaction, FreeDirect_SpriteBatch, FreeDirect_Blend, FreeDirect_AddressMode, FreeDirect_Unsupported3DBehavior and the texture, render-target and back-buffer contract cases) register only when CNA_BUILD_TESTS is on, the host is neither Windows nor Emscripten, and FREEDIRECT is the selected renderer. They run with SDL_VIDEODRIVER=dummy and read pixels back through the renderer's own shadow back buffer, so they need no display. None was run for this page. A runtime result about this renderer is reproducible only if it names the CNA commit and both library commits.

The dependency chain

  game code:  GraphicsDevice, SpriteBatch, Texture2D, RenderTarget2D   (XNA-shaped, 2D)
                 |
                 v
  cna_renderer_freedirect  (FreeDirectRenderer.cpp; <ddraw.h> only behind a pimpl)
                 |  PRIVATE: SDL3::SDL3, free-direct
                 v
  free-direct      <ddraw.h> <dsound.h> <dplay.h>   (CNA uses DirectDraw only)
                 |  PUBLIC: free-api::free-api      PRIVATE: SDL3, SDL3_image, SDL3_mixer
                 v
  free-api         <windows.h> and 22 other Win32-named headers
                 |
                 v
  SDL3 targets     already created by CNA's vendored-SDL setup (one SDL build for all)
Figure. From game code down to SDL3 when FREEDIRECT is selected. The CNA renderer target links free-direct and SDL3 privately and includes free-direct's ddraw.h only inside its implementation file. free-direct links free-api publicly, so free-api's headers reach every consumer of free-direct. Both libraries use the SDL3, SDL3_image and SDL3_mixer targets CNA has already created, so the chain shares one SDL build. CNA uses only the DirectDraw part of free-direct.

CNA adds only free-direct. free-direct's own CMake adds ../free-api when no free-api target exists yet, forcing FREE_API_BUILD_TESTS off first, and links free-api::free-api PUBLIC. CNA's CMake mentions free-api only in comments, yet with FREEDIRECT selected free-api is compiled, linked and its headers are reachable from CNA's renderer translation unit. The two games the libraries were written for, free-eggbert and planetblupi, consume them differently: each adds ../free-api explicitly first and then ../free-direct. That asymmetry is harmless because of the if(NOT TARGET free-api) guard, and it shows that the chain is shared infrastructure, not machinery built for one CNA renderer. The renderer's CMake links free-direct by its literal lowercase target name because free-direct defines no namespaced alias (freedirect/CMakeLists.txt), and it declares REQUIRES_PLATFORM SDL3: the renderer needs CNA's SDL3 platform, whose window it hands to free-direct.

Names, and what FREEDIRECT is not

The identity was renamed to FREEDIRECT on 2026-08-04; before that it was DX3 (spelled with a longer DirectX 3 name in later text). The old name survives in task identifiers (the DX3- prefix in plan_freedirect.md), in the README, in comments and test titles under modules/renderers/freedirect and modules/graphics, and in the CMake macro cna_directx3_test; all of those mean this renderer. It is also a trap, because the name was handed on: the same day, it went to a real DirectX 3 renderer of a legacy DirectX family (built with MinGW and run under Wine), which was itself retired on 2026-09-17. Its C ABI value 25 is permanently reserved and no renderer answers to the old name today (docs/freedirect-renderer.md, docs/removed-renderers.md). Two further confusions are worth ruling out. FREEDIRECT is not one of the Windows-only Direct3D renderers (DIRECTX9, DIRECTX11, DIRECTX12): those need the Windows SDK or a MinGW cross-compile, while FREEDIRECT is deliberately left out of that gate and builds natively on Linux with an ordinary compiler. And although free-direct uses SDL3 inside its .cpp files, FREEDIRECT is not the SDL_RENDERER family: it never calls SDL's 2D texture API, only DirectDraw-shaped COM calls.

What free-direct implements

free-direct (C++20, MIT) reimplements a narrow, game-driven subset of 1990s DirectX, 2D only, driven by the call sites of two games (Speedy Blupi and Planet Blupi) rather than by API coverage; its non-goals are full DirectX compatibility, hardware-accurate emulation, any Direct3D pipeline (there is no Direct3D code) and APIs the target games do not call. It has three parts:

  • DirectDraw, the most developed: IDirectDraw, IDirectDrawSurface, IDirectDrawPalette, IDirectDrawClipper, with CPU pixel buffers uploaded to SDL textures on Flip, Blt/BltFast, Lock/Unlock, palettes and colour keys where the games need them, and an option (FREE_DIRECT_ENABLE_INDEXED_TEXTURES, default on) for GPU-side palette lookup of 8-bit surfaces.
  • DirectSound: IDirectSound and IDirectSoundBuffer playing static PCM buffers through an SDL3 audio stream.
  • DirectPlay: IDirectPlay/IDirectPlay2A with real session, player and message state over an in-process loopback transport; real UDP hosting and LAN discovery only with FREE_DIRECT_ENABLE_ENET (default off). It talks only to other free-direct processes, never to Microsoft's DirectPlay wire protocol.

Every public method carries a Doxygen status tag, which makes the coverage countable. At 934f72ff the three headers carry 95 tags:

HeaderIMPLEMENTEDPARTIALSTUB
ddraw.h24127
dsound.h2042
dplay.h1277
Total562316

(Counted with git show 934f72ff:include/<header> | grep -o 'Status: [A-Z_]*' | sort | uniq -c.) A STUB does not always fail. QueryInterface stubs return DDERR_UNSUPPORTED, but IDirectDrawSurface::IsLost() always returns DD_OK ("not lost") and Restore() always succeeds, because surfaces are kept resident. A caller of free-direct therefore cannot observe surface loss; on the SDL-backed implementation there is none to observe, and CNA's renderer never asks.

The public headers are global-namespace, DirectX-shaped C++ and leak no backend: a test script, tests/check_header_hygiene.sh, fails if any SDL3, SDL3_net or ENet identifier appears under include/. free-direct's own tests are off by default (FREE_DIRECT_BUILD_TESTS), so a CNA build compiles none of them; its suite of hand-rolled executables covers DirectDraw, DirectSound (including a forced no-driver process), DirectPlay, compile-only header smoke tests and, with ENet on, an ENet DirectPlay test. The suite is split by transport on purpose: its README warns that running the unfiltered suite against an ENet-enabled build fails a loopback-oriented DirectPlay test by design.

What free-api implements

free-api (C++20, MIT, a static library) reimplements the subset of the roughly 1998 Win32 API that free-eggbert and planetblupi call, over SDL3; its README says plainly that it is neither Wine nor a general Win32 SDK. Its public surface is 23 flat, Win32-named C headers (windows.h is the umbrella) of extern "C" WINAPI declarations, each tagged with a status, and it never exposes SDL types: an HWND is internally an SDL_Window* cast to void*. Behind it are a message queue that turns SDL events into Win32 MSGs, window creation over SDL_Window, input translation, a GDI subset, file and path shims, timers, MCI/MIDI music through vendored TinySoundFont and TinyMidiLoader, joystick input and string resources. It needs SDL3, SDL3_image and SDL3_mixer targets; only when it is asked to find a system SDL itself (FREE_API_USE_SYSTEM_SDL3) does it enforce version floors of 3.4.0, 3.4.0 and 3.2.0. Its tests are on when it is built alone and off when it is a subdirectory.

One header is deliberately not Win32. free_api_bridge.h declares three functions, FreeApiCreateSurfaceDC, FreeApiDestroySurfaceDC and FreeApiSetWindowFullscreen, that exist only so free-direct can draw into and manage the window free-api owns without free-api exposing SDL. It is the single authoritative declaration of that seam; consumers used to redeclare the three signatures themselves.

The fopen macro and CNA's containment rule

free-api's windows.h ends its file-access section with

#undef fopen
#define fopen free_api_fopen

so that Planet Blupi's backslash paths such as "data\\config.def" work on POSIX without editing the game. free_api_fopen normalises the path before calling the real fopen: it drops a leading drive letter, turns backslashes into slashes and strips every leading /, then retries with an upper-cased file name on failure (FreeApi::Internal::NormalizeFilesystemPath in src/internal/FreeApiPath.cpp). The header calls this a deliberate, narrowly scoped exception to its own no-global-macros rule. For any other code the effect is surprising: every translation unit that includes <windows.h>, directly or through free-direct's <ddraw.h> (which includes it), silently has its fopen calls rewritten, and an absolute POSIX path loses its leading slash and is opened relative to the working directory.

CNA contains this at the header boundary. FreeDirectRenderer.hpp does not include <ddraw.h>, and says why: leaking the macro through a renderer header would rewrite fopen in every CNA source file that happens to include it. All free-direct use lives in FreeDirectRenderer.cpp behind a FreeDirectRenderer::Impl pimpl, so the macro reaches exactly one translation unit. When changing this renderer, keep <ddraw.h>, <windows.h> and any type from them out of every header, and do not call fopen with a path you expect to be taken literally inside that .cpp.

Beyond fopen: what else reaches the renderer's translation units

The fopen macro is the loudest part of free-api's surface, not the only part. Two mechanisms carry the rest into CNA, and both stop at the renderer target.

Include directories. free-api links an INTERFACE target, freeapi_compat_headers (alias free-api::compat_headers), publicly. That target contributes free-api's flat include/ directory and, on every host except Windows, include_non_windows/. free-direct links free-api::free-api and its own include/ publicly, and CNA links free-direct privately (freedirect/CMakeLists.txt). Both translation units of the renderer, FreeDirectRenderer.cpp and FreeDirectRendererDescriptor.cpp, therefore compile with those directories as ordinary include directories, searched before the compiler's system directories. Nothing propagates past CNA's private link to other modules or to games. A #include <io.h>, <direct.h> or <sys/timeb.h> added to either file resolves to free-api's header. The directory include_non_windows/ holds two case forwarders, Windows.h and WinUser.h, so that Win32-cased includes work on a case-sensitive file system, and one real implementation, sys/timeb.h. Its ftime() fills struct timeb from std::chrono::system_clock and always sets timezone and dstflag to 0; free-eggbert's timing code is the reason it exists.

Macros. Once <ddraw.h> has pulled in free-api's windows.h, the rest of the translation unit also sees the following macros:

  • byte defined as BYTE. It comes from rpcndr.h, which windows.h includes unconditionally, so a std::byte written after the include becomes std::BYTE.
  • About twenty Win32 function names mapped to their ANSI A variants, or to W variants when UNICODE is defined. Among them are MessageBox, GetObject, LoadImage, PeekMessage, DeleteFile and CreateDirectory. A CNA header that declares a type such as CNA::Devices::MessageBox and is first included after <ddraw.h> would therefore silently declare MessageBoxA.
  • Function-like macros such as RGB, ZeroMemory, CopyMemory, LOWORD, HIWORD and MAKELONG, and the object-like TRUE, FALSE and MAX_PATH. Unlike the Windows SDK header, free-api's defines no min or max macro, so standard algorithms used after the include are unaffected.

At 009d40f5, FreeDirectRenderer.cpp includes its three CNA headers first, then <ddraw.h>, then Vector3.hpp, two sharp-runtime exception headers and standard library headers. It uses none of the listed names outside comments. This was checked by reading the source; nothing was compiled, and no CI job builds FREEDIRECT. The containment rule above therefore extends in three ways. Put new CNA includes before <ddraw.h>. Keep Win32-named system headers out of both files. Treat every name in the list as reserved in that file.

free-api's windows.h also lists what it deliberately does not provide: real Win32 resources, common dialogs, real GDI drawing, palettes, DIB sections, brushes, icons, cursors, fonts, real Unicode APIs, security descriptors and parent/child window semantics. The free-api facts were read at 53d7a312 and the free-direct facts at 934f72ff.

What the renderer takes from free-direct, and what it works around

CNA's adapter uses DirectDraw only. DirectSound and DirectPlay stay out by design: CNA's audio and networking are its own. Bring-up runs DirectDrawCreate, then SetCooperativeLevel with CNA's own SDL_Window* passed as the HWND (free-direct casts it back, so there is never a second window), then SetDisplayMode and the primary CreateSurface. Three properties of free-direct shaped the rest, and each is recorded in CNA's FreeDirect status document:

  • The primary surface cannot be locked for writing. Lock() on free-direct's primary never exposes a writable pointer, so CNA never renders into it. Clear() and every sprite go to an always-lockable off-screen shadow back buffer sized to the logical resolution, and Present() is one identity Blt() onto the primary (also described on graphics backends).
  • Colour fill ignores alpha. free-direct's fill path writes an alpha of 255 unconditionally, so CNA clears by locking the surface and writing all four channels itself instead of using a colour-fill blit.
  • Presentation is fixed. free-direct's presenter always letterboxes, so SetPresentationMode() is stored but cannot change physical output, and CNA's status document records that a virtual-resolution change after the first Present() keeps presenting at the old physical scale. Both need a free-direct change, which CNA's plan puts out of scope; the per-family presentation behaviour is on presentation and back-buffer readback.

Everything above the surfaces is CNA's own CPU work: the SpriteBatch compositor (a BltFast-style fast path for untransformed sprites and an edge-function rasteriser for rotated, scaled or transformed ones), four distinct blend presets plus a custom fallback, bilinear filtering and Wrap/Mirror/Clamp addressing. Limits follow from the DirectDraw model: no mip levels on a surface (level-1+ SetData throws), no palette textures, no depth, stencil or MSAA, and every 3D entry point throws by default or warn-and-stubs when Unsupported3DGraphicsCallBehavior asks for it. For tests the renderer exposes CNAEXT hooks: FreeDirectResizeFailurePointEXT injects a failure at one stage of a resize (shadow back-buffer creation or validation, display-mode binding, primary creation or validation, surface-set commit), and FreeDirectTestHooksEXT reports each native acquisition and release of the DirectDraw object, primary and shadow buffer, which FreeDirect_ResizeTransaction uses to prove a failed resize leaves the old surface set intact. Platform verification is Linux only; CNA's plan leaves Windows and macOS parity unverified.

Status matrix

SurfaceStatus at this snapshot
CNA FREEDIRECT rendererOne of the 25 public identities and one of the seven 2D-only ones, declared Experimental; capability answers are qualified per feature (ThreeD false) on the 2D-only renderers
free-directA separate, narrow compatibility library, unchanged since 2026-07-18 and not pinned by CNA (only the two template repositories' lock files record its revision); CNA uses its DirectDraw part
free-apiA transitive host layer beneath free-direct, not a renderer; reaches CNA only through free-direct's PUBLIC link
Legacy applicationsMust verify the subset they actually call, their data layout and their timing against the status tags; nothing is implied by the header shape

Which layer owns a dependency

The distinction decides where a porting problem belongs. A game written against XNA-shaped 2D APIs can select FREEDIRECT and never see a COM interface; for it, free-direct is an implementation detail of one renderer that takes the same XNA-shaped calls as SDL_RENDERER. That is API-level interchangeability only: FREEDIRECT composites on the CPU, is declared Experimental where SDL_RENDERER is Production, and no test compares the two renderers' output pixel for pixel, so treat a switch as a new configuration to re-verify. A legacy program that itself calls IDirectDrawSurface::BltFast needs free-direct directly, and CNA adds nothing for it. The Blupi ports show how that decision is made from evidence: free-direct's scope was set by counting each game's real call sites, and a count is meaningful only with its population named (porting case studies: call-site-driven scope). Neither library should be described as a general compatibility layer, and a statement that "FREEDIRECT works" is evidence only for the CNA, free-direct and free-api commits it was produced with.

Source reading order

  1. RendererSelection.cmake (the ../free-direct check and the Windows gate that leaves FreeDirect out), then freedirect/CMakeLists.txt.
  2. FreeDirectRenderer.hpp: the containment comment and the CNAEXT test hooks.
  3. FreeDirectRenderer.cpp: bring-up, the shadow back buffer, FillSurfaceColor and the compositor.
  4. free-direct include/ddraw.h (status tags), src/directdraw/DirectDrawSurface.cpp; free-api include/windows.h (the fopen section) and include/free_api_bridge.h.
  5. docs/freedirect-renderer.md: CNA's own completeness table and known limitations.

The user-facing view of this renderer is on Graphics renderers: 2D-only renderers and Tutorial 72; the ecosystem-level summary of both libraries is The CNA ecosystem: free-direct and free-api.

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

Maintainer workflow
Fix a renderer bug
Tests and validation
Test architecture