Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

Chapter 26 DirectX 1–3 and free-direct

Four identities meet here, but only three are Microsoft-era rungs. DIRECTX1, DIRECTX2, and DIRECTX3 use historical DirectDraw/Direct3D interfaces on Windows. FREEDIRECT is a portable 2D renderer over the separate free-direct project and is not an implementation of the DIRECTX3 selector.

Identity Native surface 3D route RT / MRT / query
DIRECTX1 DirectDraw v1 unavailable; base fallback rejects off-screen surface / no / no
DIRECTX2 DirectDraw and Direct3D v2 fixed-function DrawPrimitive yes / no / no
DIRECTX3 DirectDraw v2 and Direct3D v2 fixed-function DrawPrimitive yes / no / no
FREEDIRECT free-direct surface API unavailable; base fallback rejects yes / no / no

26.1 DIRECTX1: before Direct3D existed

DirectX 1 shipped DirectDraw but no Direct3D. CNA’s renderer is consequently a 2D implementation, not an unfinished 3D renderer. Its off-screen surfaces provide RenderTarget2D; MRT and occlusion queries do not exist at this API level.

The extended primitive methods are inherited. Their base route reaches colored rendering, and this 2D family rejects that request. This is the correct visible failure for a permanent API ceiling. Reporting successful effect-aware 3D would be more misleading than throwing.

Build and execution are Windows-shaped. The repository can cross-compile with mingw-w64 and exercise the binary under Wine’s DirectDraw implementation, but that proves a compatibility layer as well as CNA. Native historical drivers remain a different environment claim.

26.2 DIRECTX2: fixed function without execute buffers

Direct3D 2 is often associated with execute buffers, yet CNA submits immediate DrawPrimitive calls. A spike established that Wine’s implementation accepts the route and provides the transform, lighting, texture, depth, and raster behavior needed by the bounded renderer.

Both extended draw entry points are native fixed-function adapters. CNA’s modern draw packet is reduced to texture, matrix, lighting, fog, alpha, and state concepts available in the old pipeline. Custom programmable effects are outside the API’s design. 2D targets work; multiple attachments and queries do not.

26.3 DIRECTX3: a small port, not a renamed free-direct

This renderer uses the DirectDraw 2 and Direct3D 2 interface generation available with the DirectX 3 SDK surface. It landed under a temporary DX30 name because the old DIRECTX3 selector was still occupied by what is now FREEDIRECT. The identity cleanup separated historical Microsoft DirectX from the portable sibling project.

The measured source delta from the DirectX 2 port was small, roughly 3.7 percent, but that number describes lineage rather than semantic equivalence. Interface acquisition, presentation, and platform behavior still belong to a separate family and build target. Its draw and target feature shape matches the preceding rung: native fixed-function draws, one 2D target, no MRT, and no occlusion queries.

26.4 Why the first three rungs matter

The sequence isolates what the API itself added. DirectX 1 can compose surfaces but cannot express 3D. DirectX 2 introduces an immediate fixed-function route sufficient for CNA’s bounded 3D mapping. DirectX 3 changes the surrounding SDK/interface generation without turning that fixed pipeline into a programmable one. The same CNA call surface therefore reveals a historical API boundary instead of flattening every old renderer into one generic legacy label.

26.5 FREEDIRECT: a separate portable 2D branch

FREEDIRECT is CNA’s second 2D-only renderer, and its odd one out architecturally: it does not use SDL3’s 2D API at all, instead fronting IDirectDraw / IDirectDrawSurface-shaped calls against free-direct, the narrow DirectDraw reimplementation introduced in Chapter 2 and covered as its own project later in this book. This chapter covers only what is specific to CNA’s own renderer layer on top of it; this book’s free-direct chapter covers the library itself.

26.6 An unanticipated architectural constraint

free-direct’s IDirectDrawSurface::Lock() never exposes a writable pointer for the primary surface—only for offscreen ones. Instead of treating the primary surface as a render target directly, this renderer owns an internal, always-lockable “shadow backbuffer”: every Clear() and every SpriteBatch draw targets that shadow surface, and Present() is a single identity Blt() copying it onto the primary surface. This design makes the renderer possible.

26.7 A capability beyond SDL_RENDERER

TextureAddressMode::Wrap and Mirror — the two features Chapter 30 named as formally BLOCKED on SDL_RENDERER — have implemented sampling paths here. This renderer’s CPU compositor already samples per-source-pixel for any non-identity draw, so wrap/mirror addressing costs nothing extra to add. FREEDIRECT also implements separate Opaque, AlphaBlend, NonPremultiplied, and Additive formulas, in contrast to the SOFTWARE renderer’s single collapsed baseline formula.

26.7.1 Address modes beyond SDL_RENDERER

The fixture from Chapter 30 draws a two-texel red/green texture through a four-texel source rectangle onto four destination pixels. With point filtering, each destination sample is unambiguous. FREEDIRECT reads back all four red-channel values for each address mode:

1 Texture2D tex(dev, 2, 1);
2 std::vector<Color> px = {Color(255, 0, 0, 255), Color(0, 255, 0, 255)}; // texel 0 = red, 1 = green
3
4 SamplerState point;
5 point.setFilterProperty(TextureFilter::Point);
6 point.setAddressUProperty(TextureAddressMode::Wrap); // or Mirror, or Clamp
7 point.setAddressVProperty(TextureAddressMode::Wrap);
8
9 spriteBatch_->Begin(SpriteSortMode::Deferred, BlendState::AlphaBlend, &point, nullptr, nullptr);
10 spriteBatch_->Draw(tex, Rectangle(0, 0, 4, 1), Rectangle(0, 0, 4, 1), Color::White);
11 spriteBatch_->End();
12 // Read back R at destination x = 0,1,2,3:

Wrap yields (255, 0, 255, 0), or red/green twice. Mirror yields (255, 0, 0, 255), and Clamp yields (255, 0, 0, 0). SDL Renderer provides only the clamp-shaped result through this draw path. FREEDIRECT can implement the three index rules inside its existing per-source-pixel CPU loop; SDL’s texture-blit route exposes no equivalent sampler control.

26.8 Corrected FREEDIRECT defects

The renderer audit corrected several independent faults:

  • Clear() wrote alpha 255 through its color-fill route; locked-pixel writes now preserve all four requested channels.

  • the general CPU blend path omitted source-alpha multiplication;

  • CreateOcclusionQuery() threw instead of using the project’s null-safe query;

  • blend-preset detection compared factors but ignored BlendFunction, so a custom subtract state could be mistaken for a preset; and

  • display-free tests were unnecessarily registered as requiring X11.

The current regressions cover the behavior, while historical test-count bookkeeping is not used as a capability claim.

26.9 RenderTarget2D: the same shadow-backbuffer trick

RenderTarget2D reuses the shadow-backbuffer design once per target. The private render-target renderer owns an always-lockable offscreen surface, defined (like FreeDirectTextureRenderer) entirely inside FreeDirectRenderer.cpp so <ddraw.h> never leaks into a header. Binding one is a pure redirect: Clear() and ReadBackbuffer() both route through an internal Impl::ActiveSurface() indirection that resolves to whichever surface is currently bound, while Present() always targets the shadow backbuffer from §26.6 regardless of what is bound at the time — an intentional asymmetry, since only the shadow backbuffer is the only surface copied to the primary.

freedirect_texture_rendertarget_test.cpp verifies this bind-redirect concretely rather than by code inspection alone, using two distinct, deliberately different clear colors so a readback that returned either the wrong surface’s color or a stale value would fail visibly:

1 dev.Clear(Color(20, 40, 60, 255)); // shadow backbuffer’s own color, established first
2
3 auto rt = std::make_unique<RenderTarget2D>(dev, 8, 8);
4 dev.SetRenderTarget(rt.get());
5 dev.Clear(Color(210, 30, 90, 255)); // the render target’s own, different color
6 // GetBackBufferData() now reads back (210,30,90) -- the render target’s surface, not the shadow’s.
7
8 dev.SetRenderTarget(nullptr);
9 // GetBackBufferData() over the same region now reads back (20,40,60) again --
10 // unbinding really redirects Clear()/ReadBackbuffer() back to Impl::backBuffer.

Two further checks round out the same test. RenderTargetUsage::DiscardContents auto-clearing to black on rebind needed zero FREEDIRECT-specific code at all — it falls straight out of shared GraphicsDevice.cpp logic once bind/Clear/read were wired correctly, the same "comes for free" shape Chapter 14 documents for other renderers. This is also a CNA/FNA identity difference: CNA repeats that clear on an identical currently-bound target, while FNA treats the redundant call as a no-op. The persistent DirectDraw surface makes Preserve and Platform retain color in source, but the test proves only the Discard-to-black case; presentation usage is ignored. SetRenderTargets() with two or more bindings throws, honestly: IDirectDrawSurface has exactly one active surface at a time, the identical single-active-surface conclusion Chapter 30 reached for SDL_RENDERER for an unrelated reason (that renderer’s SDL2 render target API, not DirectDraw’s own surface model) — two structurally different renderers landing on the same MRT answer for two different underlying reasons.

The same test confirms the size boundary: a 5000×5000 Texture2D construction throws. free-direct::CreateSurface enforces a fixed 4096×4096 cap, and the renderer propagates that exception.

26.10 Public depth/stencil clear

The ordinary colour hook locks and fills the complete active surface with all four requested channels, and target-redirection tests prove that “active” changes between the shadow backbuffer and a RenderTarget2D. Depth and stencil follow two different layers. GraphicsDevice sees SupportsDepthStencil()==false, strips both flags, and returns silently when no target bit remains — the same depthless-target rule current FNA uses. freedirect_no3d_test.cpp separately invokes all six renderer combination hooks and proves their ThrowNo3D errors precisely because those hooks are unreachable through the public mask. Treating either result as evidence for the other would conflate shared and renderer contracts; §19.4.2 keeps them separate.

26.11 Known limitations at the pin

No 3D pipeline exists, matching free-direct’s own explicit “Direct3D not implemented” stance; 8-bit palette surfaces and GetDC / SetPalette-style GDI-adjacent calls are unsupported, since XNA itself has no palette-texture concept to begin with; mip levels above zero throw, since IDirectDrawSurface has no native mip chain; and SetPresentationMode() is honestly downgraded from an earlier full-pass mark to a documented partial: it stores the requested presentation mode but never changes the actual physical output scale and always presents in letterbox. Closing this limitation requires a change in free-direct, not only CNA.

The same stored-versus-applied distinction affects two ordinary XNA properties: Dx3GraphicsRenderer overrides neither SetViewport() nor SetScissorRect(), and it has no rasterizer-state hook. Both values therefore round-trip through GraphicsDevice without changing the 2D DirectDraw/SDL output. The SDL renderer that free-direct owns privately does not rescue this path: CNA never routes these graphics-state setters into it (unlike the separate public input-discovery route below). There is no dedicated negative pixel test, so this is a source-proven permanent 2D boundary rather than an executed claim.

There is a related resize defect that is easier to miss because the new logical surfaces are created correctly. Once the first Present() has configured free-direct’s internal renderer, a later SetVirtualResolution() replaces CNA’s primary and shadow surfaces but cannot reset the library’s private logicalPresentationSet_ flag. The next frame therefore uploads the new-sized primary texture into the old logical-presentation rectangle. CNA’s input-transform methods do recompute their scale from the new logical dimensions, but the public input path does not call them: it discovers free-direct’s associated SDL renderer first, as the section below explains. That renderer retains the same old logical-presentation rectangle as physical output, so public input and presentation remain tied to the old coordinate canvas while the game’s newly allocated surfaces use the new size. Closing that three-way mismatch needs free-direct to reapply logical presentation when the primary changes, not an unilateral CNA-side approximation.

DirectSound / DirectPlay networking are explicitly out of scope for this renderer by the project owner’s own instruction — CNA’s own audio and networking go through the ordinary renderer-agnostic paths from Chapter 45 and Chapter 51 of this book, not through free-direct at all.

26.12 The transform math is sound, but public input takes another route

The renderer implements both TransformWindowToLogical() and its inverse from the physical SDL_Window size. For a logical size (Lw,Lh) and physical window size (Pw,Ph), they recompute the same centered-letterbox transform that free-direct uses:

s=min(Pw/Lw,Ph/Lh),ox=(PwLws)/2,oy=(PhLhs)/2.

A logical point maps to (xs+ox,ys+oy); a mouse/window point maps back through ((xox)/s,(yoy)/s). Thus the black bars introduced by the otherwise unavoidable letterbox mode are accounted for explicitly, rather than being accidentally treated as game pixels. The calculation intentionally queries the live physical window every time it is used: the primary/shadow surfaces retain the requested logical resolution, while window managers may choose a quite different actual window size.

Those two correct methods are nevertheless shadowed during normal public input. Both public callers first ask SDL_GetRenderer(window) for an associated renderer and use SDL’s own logical-coordinate conversion when one exists. Only a renderer-less window proceeds to the renderer registry. free-direct creates an SDL renderer on this same window during SetCooperativeLevel(), so SDL’s first tier wins even though Dx3GraphicsRenderer::GetRendererInternal() returns nullptr. CNA can still reach the renderer through the registry; this accessor does not return it.

This route has two time-dependent consequences. Before the first Present(), free-direct has created the renderer but has not yet called SDL_SetRenderLogicalPresentation; public input therefore passes through at window scale. The first presentation installs the hardcoded letterbox mapping, after which SDL’s own conversion correctly follows the displayed output. A later logical resize hits the stale logicalPresentationSet_ problem above: both output and public input retain the old SDL mapping, while the new game surfaces no longer share that coordinate size.

The dedicated Dx3_LogicalTransform test avoids a fragile fixed-size expectation for that reason. It asks the running window for its physical dimensions, requests a 64×64 logical canvas, then verifies four properties: logical-to-window-to-logical round trips preserve five points (corners and centre); logical (32,32) becomes the physical geometric centre; equal horizontal and vertical logical steps have equal physical length; and that measured length is exactly min(Pw/64,Ph/64). These checks prove both the centering and the absence of silent anisotropic stretch on the active display, including the environment where an attempted SDL_SetWindowSize() was not honoured by the window manager. They do not prove the public route above: the test obtains FreeDirectRenderer& and invokes both methods directly, bypassing Mouse::SetPosition(), SdlInputBridge, and the SDL-renderer-first branch. Chapter 19, §19.2.1, places that proof boundary in the full renderer matrix.

26.13 The fixed 32-bit surface decision

FreeDirectRenderer.cpp creates the primary, shadow backbuffer, render targets, and Texture2D surfaces at 32 bits per pixel. It has no palette/8-bit route. This makes two limitations in free-direct’s docs/directdraw-limitations.md unreachable from CNA:

The discarded bit-depth parameter.

IDirectDraw::SetDisplayMode’s dwBPP parameter is silently discarded — free-direct’s own CreateSurface always allocates a 32bpp primary regardless of the requested depth. CNA’s CreateSurfaces() always requests 32, so its request and the library result agree.

The mixed-depth blit silent-no-op.

DirectDrawSurfaceImpl’s BlitFrom method implements same-depth 8-on-8 and 32-on-32 copies; a mixed pair copies nothing. CNA creates only 32bpp surfaces, so every renderer-issued blit follows the 32-on-32 path.

XNA’s SurfaceFormat has no 8-bit indexed-color member, so this fixed direct-color choice fits CNA’s public texture model. Direct users of free-direct that request 8-bit surfaces or mixed-depth blits remain exposed to the underlying limitations.

PresentationParameters cannot change that decision. The FREEDIRECT factory reads the window, virtual size, and presentation mode, but discards BackBufferFormat, DepthStencilFormat, and IsFullScreen. Shared device code may still ask SDL to change the window, while free-direct remains at DDSCL_NORMAL; there is no exclusive DirectDraw transition. The primary and shadow buffers stay fixed 32-bpp, and no depth attachment exists. Thus the public fields can round-trip while the native answer remains unchanged, exactly as the cross-renderer matrix in §19.2.5 records.

26.13.1 Why raw factors do not identify a preset

The blend-preset defect becomes visible with concrete factor values. BlendState.cpp constructs BlendState::AlphaBlend with two color factors: ColorSourceBlend is Blend::One, and ColorDestinationBlend is Blend::InverseSourceAlpha. A hypothetical custom state sharing exactly those two factors but pairing them with BlendFunction::Subtract instead of the preset’s own (default) Add computes a different result whenever the destination pixel is not fully black: 𝑠𝑟𝑐+𝑑𝑠𝑡×(1𝑠𝑟𝑐𝐴) versus 𝑠𝑟𝑐𝑑𝑠𝑡×(1𝑠𝑟𝑐𝐴). They coincide only when the destination term is zero, as it is for BlendState::Opaque; AlphaBlend’s nonzero destination factor exposes the mismatch over existing color. A fixture that draws only onto cleared black can therefore miss it. Preset matching now compares both BlendFunction fields as well as the raw factors.

At the pin, the renderer plan records no open BLOCKED decisions. That plan status does not broaden the scoped execution evidence above.

Type at least three characters. Results are ranked by how often and where the words occur.