Status (2026-07-21): all phases complete and verified. 19/19
DX5-labeled CTests pass, all green on the first run after the port. See §2 for the spike result, the phase tables below for the implementation, anddocs/dx5-backend.mdfor the full write-up.
- New backend:
CNA_GRAPHICS_BACKEND=DX5. No temporary-naming concern this time (unlikeDX3) —DX5is not already claimed by any shipping backend. - DX5 (1997) is the release where execute buffers disappear entirely.
IDirect3DDevice3only ever exposesDrawPrimitive/DrawIndexedPrimitive— matchingDX2/DX3's own already-proven choice of avoiding execute buffers, except now it's the only option the SDK itself offers, not a workaround for a broken path. - More invasive port than
DX3's.DX3upgraded only the topIDirectDrawobject to v2, keeping every surface at v1. DX5 needs every surface to be v4 (IDirectDrawSurface4,DDSURFACEDESC2,DDSCAPS2) —IDirectDraw4::CreateSurfacereturnsLPDIRECTDRAWSURFACE4directly, andIDirect3D3::CreateDevicerequires a v4 surface specifically. This touches every 2D-layer surface (primary, shadow-backbuffer, Z-buffer, textures, render targets), not just the top-level object. - 3D layer:
IDirect3D3/IDirect3DDevice3/IDirect3DViewport3. Texture object staysIDirect3DTexture2(confirmed noIDirect3DTexture3exists).DrawPrimitive/DrawIndexedPrimitive's vertex-type parameter changes from theD3DVERTEXTYPEenum (D3DVT_TLVERTEX) to aDWORDFVF bitmask (D3DFVF_TLVERTEX, confirmed to expand to the exact same bit layoutD3DTLVERTEXalready has) — same struct, same CPU transform/clip math, same Phase O9 CPU lighting, ported fromDX3, just resubmitted through the new parameter shape. - Real capability upgrade used, not just carried forward as a workaround:
IDirect3DViewport3::Clear2takes explicit color/z/stencil values —DX2-24/DX3had to work aroundIDirect3DViewport(2)::Clear()'s complete lack of such a parameter via a manualLock()-the-Z-buffer-and-write-raw-values hack. DX5 uses the realClear2()call instead — simpler, and no longer relies on an assumed-but-unverified raw Z-buffer memory layout. - New existence-gate spike (
DX5-0,dx5-spike/) confirmed all of the above genuinely works in this Wine environment — real Gouraud interpolation via the new FVF path, real Z-test occlusion, real texture sampling from a v4-owned surface, and a real, correctClear2(found and fixed two spike-authoring bugs along the way, neither a Wine limitation — seedx5-spike/README.md).
| Layer | Symbol(s) used | Introduced in | Never used here |
|---|---|---|---|
| DirectDraw (2D) | IDirectDraw4, IDirectDrawSurface4, DDSURFACEDESC2, DDSCAPS2 |
DX5 SDK | IDirectDraw/IDirectDraw2/IDirectDraw3 (skipped, DX3 already used v2 — DX5 goes straight to v4, the real DX5-SDK-introduced revision), IDirectDrawSurface/IDirectDrawSurface2/IDirectDrawSurface3, IDirectDraw7+ |
| Direct3D device/object | IDirect3D3, IDirect3DDevice3 |
DX5 SDK (skips interface number "3" landing anywhere but DX5 — DX3 SDK used interface number "2"; DX5 is where execute buffers were dropped and the interface number jumped to "3", not to match the SDK version number, which doesn't happen until DX7) | IDirect3D/IDirect3DDevice (v1, execute-buffer only), IDirect3D2/IDirect3DDevice2 (DX3, still real but superseded here), IDirect3D7+ |
| Viewport | IDirect3DViewport3 |
DX5 SDK | IDirect3DViewport/IDirect3DViewport2 |
| Texture | IDirect3DTexture2 |
DX3 SDK (unchanged — no IDirect3DTexture3 exists at all, confirmed by grep across the real MinGW headers) |
IDirect3DTexture (v1) |
| Vertex format | D3DTLVERTEX (unchanged struct) |
DX2 SDK (d3dtypes.h) |
D3DVERTEX/D3DLVERTEX |
| Vertex-type selector | DWORD FVF bitmask (D3DFVF_TLVERTEX) |
DX5 SDK | The old D3DVERTEXTYPE enum (D3DVT_TLVERTEX) — IDirect3DDevice3::DrawPrimitive's parameter type genuinely changed from the enum to a DWORD, confirmed by inspecting both vtables side by side |
| Draw call | IDirect3DDevice3::DrawPrimitive/DrawIndexedPrimitive |
DX5 SDK (execute buffers removed entirely as of this device revision) | IDirect3DDevice::Execute/IDirect3DDevice2::DrawPrimitive (superseded) |
| Depth/color clear | IDirect3DViewport3::Clear2 (explicit color/z/stencil values) |
DX5 SDK | IDirect3DViewport(2)::Clear() (no value parameter at all — DX2-24's finding) |
On the "IDirect3D3 for DirectX 5" naming: this was double-checked (not assumed) against the
real MinGW headers before writing this plan, prompted by a direct question about whether
IDirect3D3 "belongs" to DirectX 3 by its number. It does not — IDirect3D2/IDirect3DDevice2
(interface number "2") is the genuine DX3-SDK interface (DX3's own plan already established
this); IDirect3D3/IDirect3DDevice3 (interface number "3") is the next revision, introduced by
the DX5 SDK (DirectX 4 was never released on Windows). The interface-revision-number-to-SDK-
version-number offset that begins at DX3 (SDK 3 → interface 2) grows to two at DX5 (SDK 5 →
interface 3), and Microsoft doesn't re-align them until DX7 (SDK 7 → interface 7, from then on
matching exactly through D3D8/D3D9/etc).
Confirmed present in this environment's MinGW-w64 headers before writing this plan:
IID_IDirectDraw4, IID_IDirect3D3, IID_IDirect3DDevice3, IID_IDirect3DViewport3,
IID_IDirect3DVertexBuffer (exists, but genuinely unneeded here — see decision 8), the full
IDirectDraw4/IDirect3DDevice3/IDirect3DViewport3 vtables, DDSURFACEDESC2/DDSCAPS2's field
layouts (confirmed DDSCAPS2.dwCaps is field-position-compatible with DDSCAPS.dwCaps, and
DDSURFACEDESC2 dropped the old top-level dwZBufferBitDepth/DDSD_ZBUFFERBITDEPTH in favor of
describing Z-buffer depth via ddpfPixelFormat's DDPF_ZBUFFER flag instead — a real, spike-
verified field-migration detail, not assumed).
| # | Spike | What it proves | Result |
|---|---|---|---|
DX5-0a |
ddV1->QueryInterface(IID_IDirectDraw4, &dd4), then CreateSurface(DDSURFACEDESC2)/Lock/Unlock via v4 |
Whether IDirectDraw4 is a real, fully-functional 2D surface layer |
✅ Works |
DX5-0b |
dd4->QueryInterface(IID_IDirect3D3, &d3d3), d3d3->CreateDevice(IID_IDirect3DRGBDevice, v4surface, &device, nullptr) |
Whether the v4-surface-rooted Direct3D device chain works | ✅ Works |
DX5-0c |
IDirect3DViewport3 creation, SetViewport2 (same D3DVIEWPORT2 struct as DX2/DX3) |
Whether viewport setup is unchanged | ✅ Works |
DX5-0d |
device->DrawPrimitive(D3DPT_TRIANGLELIST, D3DFVF_TLVERTEX, verts, 3, 0) |
Whether FVF-based submission of the same D3DTLVERTEX struct still Gouraud-interpolates correctly |
✅ Works |
DX5-0e |
DrawIndexedPrimitive + real D3DZB_TRUE/D3DCMP_LESS Z-test, two overlapping quads |
Whether depth-test occlusion is real through the new FVF path | ✅ Works — exact (255,0,0) readback |
DX5-0f |
Real 2×2 texture via IDirectDrawSurface4+IDirect3DTexture2 (unchanged interface, obtained from a v4 surface, GetHandle called via a QueryInterface'd IDirect3DDevice2 view of the device3 object), sampled via DrawIndexedPrimitive |
Whether texture sampling still works when the owning surface is v4 and the device is v3 | ✅ Works — both sampled corners exact |
DX5-0g |
IDirect3DViewport3::Clear2(1, &fullRect, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER, color, z, stencil), then a z=0.8 quad against a Clear2 z=0.75 |
Whether Clear2 is a real, working replacement for the DX2/DX3 manual-Z-buffer-Lock workaround |
✅ Works — exact requested clear color read back, z=0.8 quad correctly Z-rejected |
Two real bugs found in the spike itself, not in Wine/Direct3D5 (see dx5-spike/README.md for
the full detail): Clear2's count=0, rects=nullptr does not mean "clear the whole surface"
(clears nothing, silently) — an explicit D3DRECT{0,0,w,h} with count=1 is required, same
convention the old Clear() already used; and sampling the exact screen-center pixel of a
diagonally-split full-viewport quad lands on the triangle seam and reads a rasterization-edge
value, not either triangle's true interior color.
Net effect: DX5's entire 3D layer (CPU transform/clip, D3DTLVERTEX packing, lighting) is
architecturally identical to DX3's already-proven one, needing only the FVF-parameter swap and
a real Clear2-based clear. Phase Q1 is unblocked.
-
Platform gate, same as
DX1/DX2/DX3. Same Windows-native-or-MinGW-cross-compileFATAL_ERRORgate. -
2D layer: every surface upgrades to v4 (
IDirectDrawSurface4,DDSURFACEDESC2,DDSCAPS2), not just the topIDirectDrawobject (unlikeDX3's decision 2, which kept surfaces at v1).Impl::ddisLPDIRECTDRAW4(upgraded viaQueryInterfaceimmediately afterDirectDrawCreate, releasing the v1 pointer, same pattern asDX3);Impl::primary,backBuffer,zbuffer, and everyDx5TextureBackend/Dx5RenderTargetBackend's owned surface becomeLPDIRECTDRAWSURFACE4. Field-level code (dwFlags,dwWidth,dwHeight,ddsCaps.dwCaps,lpSurface,lPitch) is otherwise unchanged —DDSURFACEDESC2/DDSCAPS2are field-position-compatible supersets for every field this backend actually uses. Z-buffer creation dropsDDSD_ZBUFFERBITDEPTH/dwZBufferBitDepth(removed fromDDSURFACEDESC2's top level) in favor ofddpfPixelFormat.dwFlags = DDPF_ZBUFFER+ddpfPixelFormat.dwZBufferBitDepth = 16(spike-confirmed,DX5-0a's own follow-up finding while building the Z-buffer surface forDX5-0b). One more real signature change, found by the compiler while porting, not by the spike (a pure interface-signature difference, not a runtime behavior difference — no new spike was needed for it):IDirectDrawSurface4::Unlocktakes anLPRECT(conventionallynullptr, meaning "the whole surface"), not theLPVOIDlocked-memory pointerIDirectDrawSurface/IDirectDrawSurface2/IDirectDrawSurface3::Unlockall take — everysurface->Unlock(desc.lpSurface)call ported fromDX3had to becomesurface->Unlock(nullptr). -
3D device bring-up:
IDirect3D3/IDirect3DDevice3/IDirect3DViewport3, verbatim port ofDX3's bring-up sequence otherwise.IDirect3D3::CreateDevicetakes an extra trailingIUnknown* outerparameter vs.IDirect3D2::CreateDevice(alwaysnullptrhere, spike- confirmed sufficient).IDirect3DViewport3::SetViewport2takes the sameD3DVIEWPORT2struct asDX3'sIDirect3DViewport2::SetViewport2— no new viewport struct exists. -
CPU transform/clip pipeline,
D3DTLVERTEXpacking, and Phase O9 CPU lighting: verbatim port ofDX3's own (itself a verbatim port ofDX2's post-Phase-O9 code), with exactly one change: everyDrawPrimitive/DrawIndexedPrimitivecall site passesD3DFVF_TLVERTEX(aDWORD) instead ofD3DVT_TLVERTEX(aD3DVERTEXTYPEenum value) as the vertex-type parameter — spike-confirmed (DX5-0d/DX5-0e) to render identically, sinceD3DFVF_TLVERTEX's bit layout matchesD3DTLVERTEX's actual memory layout exactly. -
Real
Clear2-based depth clearing, replacingDX2/DX3's manual Z-bufferLock()workaround.ClearDepthroutes throughviewport->Clear2(1, &fullRect, D3DCLEAR_ZBUFFER, 0, depth, 0)— a real, direct API call with an explicit depth value, not an assumed raw Z-buffer memory layout.fullRectis always{0,0,activeWidth,activeHeight}. Spike-confirmed gotcha, must not be reintroduced:count=0, rects=nullptrclears nothing at all (silently) — always pass a real rect withcount=1.Clear(r,g,b,a)'s own COLOR clear stays exactly asDX2/DX3already do it (a direct 2DLock()+fill onActiveSurface(), unrelated to Direct3D) — deliberately not switched toClear2, since a custom-boundRenderTarget2Dhas no Direct3D device/viewport of its own toClear2against (decision 4 already scopes 3D drawing, and therefore any realClear2call, to the default backbuffer only).ClearColorAndDepthis therefore stillClear(r,g,b,a)+ClearDepth(depth), matchingDX2/DX3's own structure — onlyClearDepth's own internal mechanism changed.ClearStencil/ClearDepthAndStencil/ClearColorAndStencil/ClearColorDepthAndStencilare unchanged fromDX2/DX3(no real stencil buffer exists until DX6, decision 8 — the stencil value is still silently ignored, not routed throughClear2either). -
Texture creation:
IDirectDrawSurface4-owned,IDirect3DTexture2-typed (unchanged interface),GetHandlecalled via aQueryInterface'dIDirect3DDevice2view of thedevice3object.IDirect3DTexture2::GetHandleonly ever accepted anIDirect3DDevice2*parameter (never gained an overload forIDirect3DDevice3*) — spike-confirmed (DX5-0f) that querying anIDirect3DDevice2interface off the samedevice3object works correctly (real historical COM aggregation), rather than assuming this would fail and needing a different texture-handle mechanism. -
VertexBuffer/IndexBuffer: plain CPU-side storage, unchanged fromDX3. A realIDirect3DVertexBufferobject exists at this DirectX era (confirmed present in the headers,IID_IDirect3DVertexBuffer,IDirect3D3::CreateVertexBuffer), but this backend's CPU-transform architecture (decision 4) never needs one — the CPU-sidestd::vector<uint8_t>storageDX2/DX3already use is reused verbatim, matching their own decision 8/7 reasoning: the transform happens on the CPU regardless of what GPU-side vertex-buffer object exists at this DirectX era. -
Lighting/fog/multitexture/envMap/skinning/stencil/MRT/instancing/occlusion-query/volume-cube textures/custom-effects: identical boundary to
DX3/DX2post-Phase-O9. Fixed-function lighting is real for the two normal-bearing vertex strides (32/52); fog, multitexture, environment mapping, and skinning remain accepted-and-ignored; the rest are genuinely unavailable at this DirectX era, same asDX2/DX3. -
32-bit surfaces only,
DirectSound/DirectInput/DirectPlayout of scope, header containment, CMake integration shape — identical toplan_dx1.md/plan_dx2.md/plan_dx3.md's equivalent decisions, ported without change. -
CMake integration: add
"DX5"toCNA_GRAPHICS_BACKEND'sSTRINGSproperty + aCNA_BACKEND_DX5option; acna_backend_graphics_dx5static library target undersrc/CNA/Internal/Backends/Dx5/, same Windows-onlyFATAL_ERRORgate. Link set:ddraw+dxguid+SDL3::SDL3— identical toDX1/DX2/DX3. -
Testing:
scripts/run-wine-dx5.sh, modeled onscripts/run-wine-dx3.sh— same~/.wine-cna-dx1prefix is reusable (confirmed by this plan's own spike, which ran against it). -
No execute-buffer code anywhere in this backend. Same grep-based discipline CTest as
DX1-1/DX2-1/DX30-9(DX5-1), adapted to allowIDirectDraw4/IDirectDrawSurface4(this backend's own real, intended v4 surface) while still forbiddingIDirectDraw7+/IDirectDrawSurface[23567]+ (DX5 is v4-only, not v3 and not v7+) and the literal execute-buffer surface. -
Full CTest suite is a straight rename/port of
DX3's own 19DX3-labeled CTests toDX5-labeled equivalents, plus updated expectations anywhereClear2's real depth/color values are now directly verifiable (whereDX2/DX3's own tests could only verify the Z-test behavior, not the exact clear color, due to the manual-Lock workaround's own limitations).
DX5-0(existence-gate spike, §2) — done, unblocks everything else.- Phase Q1 (CMake integration + skeleton) — same shape as
DX30-1..DX30-6. - Phase Q2 (2D layer: port from
Dx3GraphicsBackend, upgraded to v4 throughout, decision 2). - Phase Q3 (3D device bring-up: verbatim port from
Dx3GraphicsBackend, upgraded toIDirect3D3/IDirect3DDevice3/IDirect3DViewport3, decision 3, plus realClear2-based clearing, decision 5). - Phase Q4 (CPU transform/clip pipeline + Phase-O9 lighting: verbatim port, FVF parameter swap, decision 4).
- Phase Q5 (
VertexBuffer/IndexBufferbackends: verbatim port, decision 7). - Phase Q6 (state mapping: verbatim port).
- Phase Q7 (remaining
IGraphicsBackenddefaults: verbatim port). - Phase Q8 (tests +
docs/dx5-backend.md).
For every task: build the affected target (-DCNA_GRAPHICS_BACKEND=DX5, MinGW cross-compile), run
the relevant CTest through scripts/run-wine-dx5.sh, and do not mark a task ✅ without both
actually passing.
| # | Task | Status | Notes |
|---|---|---|---|
DX5-1 |
Add "DX5" to CNA_GRAPHICS_BACKEND's STRINGS property + CNA_BACKEND_DX5 option; extend the Windows-only FATAL_ERROR gate; add the execute-buffer-discipline grep CTest (design decision 12) |
✅ | Dx5_ExecuteBufferDiscipline passing. |
DX5-2 |
cna_backend_graphics_dx5 static library target; confirm minimal link set empirically |
✅ | Same link set as DX1/DX2/DX3 (SDL3::SDL3 ddraw dxguid). |
DX5-3 |
include/CNA/Internal/Backends/Dx5/Dx5GraphicsBackend.hpp + src/CNA/Internal/Backends/Dx5/Dx5GraphicsBackend.cpp: port of Dx3GraphicsBackend's files, upgraded to v4/IDirect3D3 throughout |
✅ | |
DX5-4 |
Factory dispatch for DX5 in CreateGraphicsBackend() |
✅ | |
DX5-5 |
scripts/run-wine-dx5.sh (design decision 11) |
✅ | Reuses ~/.wine-cna-dx1 prefix. |
DX5-6 |
Confirm CnaTests/the new MinGW test binaries link cleanly against the new backend target under cross-compilation |
✅ | cmake-build-dx5 configures/builds clean (MinGW cross, Release, ccache). Proactively added the matching Dx5 entry to every CNA_BACKEND_* registry file found via grep -rl "CNA_BACKEND_DX3\b" (the exact lesson DX30-83 recorded) before the first build attempt. One real signature difference the compiler itself caught (not proactively known, not spiked separately since it's a pure interface-signature difference): IDirectDrawSurface4::Unlock takes LPRECT (conventionally nullptr), not the LPVOID locked-memory pointer IDirectDrawSurface/2/3::Unlock all take — every ported surface->Unlock(desc.lpSurface) call had to become surface->Unlock(nullptr) (4 call sites). Also initially forgot to add include(cmake/Tests/Dx5Tests.cmake) to the root CMakeLists.txt (caught immediately by "no rule to make target" on the first test build attempt). |
| # | Task | Status | Notes |
|---|---|---|---|
DX5-10 |
Device/window bring-up: DirectDrawCreate → QueryInterface(IID_IDirectDraw4) upgrade (decision 2) → SetCooperativeLevel/primary surface/shadow-backbuffer/Clear/Present, all via LPDIRECTDRAWSURFACE4/DDSURFACEDESC2 |
✅ | |
DX5-11 |
Texture/render-target backends, LPDIRECTDRAWSURFACE4-owned |
✅ | |
DX5-12 |
SpriteBatch CPU compositor, all rotation/scale/flip/blend/sampling paths |
✅ | Port unchanged in logic, re-typed to v4. |
DX5-13 |
SpriteFont |
✅ | Port unchanged in logic, re-typed to v4. |
DX5-14 |
TransformWindowToLogical/TransformLogicalToWindow/letterbox present math |
✅ | Port unchanged from DX3. |
DX5-15 |
Renamed 2D CTests passing, pixel-verified | ✅ |
| # | Task | Status | Notes |
|---|---|---|---|
DX5-20..DX5-25 |
IDirect3D3/IDirect3DDevice3/viewport/Z-buffer bring-up (decisions 2/3), Dx5_Device3DSmoke CTest |
✅ | Z-buffer creation uses DDPF_ZBUFFER/dwZBufferBitDepth on ddpfPixelFormat (decision 2's spike-found field migration), not the old top-level field. |
DX5-26 |
Real Clear2-based ClearDepth (decision 5), replacing DX2/DX3's manual Z-buffer Lock()/FillZBuffer16 workaround (removed as dead code) |
✅ | Always passes an explicit D3DRECT{0,0,w,h} with count=1 — the spike-found count=0 gotcha, deliberately never reintroduced. Clear(r,g,b,a)'s own color clear is deliberately unchanged (still the 2D Lock()+fill on ActiveSurface()) — see decision 5's own narrower-scope note for why. ClearStencil/ClearDepthAndStencil/ClearColorAndStencil/ClearColorDepthAndStencil are likewise unchanged (still no real stencil buffer at this era). |
| # | Task | Status | Notes |
|---|---|---|---|
DX5-30..DX5-39 |
CPU clip/transform math, D3DTLVERTEX packing (unchanged), DrawColoredPrimitives/DrawIndexedColoredPrimitives/DrawPrimitivesEx/DrawIndexedPrimitivesEx, depth-test/texture/clipping CTests, all submitting via D3DFVF_TLVERTEX instead of D3DVT_TLVERTEX (decision 4) |
✅ | Verbatim port of DX30-30..DX30-39/DX2-30..DX2-39. |
DX5-40 |
CPU-side BasicEffect lighting (Phase O9's Dx2ComputeVertexLighting, specular via D3DRENDERSTATE_SPECULARENABLE) |
✅ | Verbatim port of DX30-40/DX2-91..DX2-96's implementation. Dx5_Lighting CTest (4/4, same checks). |
| # | Task | Status | Notes |
|---|---|---|---|
DX5-50, DX5-51 |
Dx5VertexBufferBackend/Dx5IndexBufferBackend, SetData/GetData round-trip test |
✅ | Verbatim port of DX30-50/DX30-51. |
| # | Task | Status | Notes |
|---|---|---|---|
DX5-60..DX5-64 |
ApplyDepthStencilState/ApplyRasterizerState/ApplyBlendState/ApplySamplerState, SetDepthTestEnabled/SetBlendEnabled/SetDepthWriteEnabled |
✅ | Verbatim port of DX30-60..DX30-64. WireFrame reports true (inherited finding); AnisotropicFiltering stays false (inherited finding). |
| # | Task | Status | Notes |
|---|---|---|---|
DX5-70..DX5-76 |
Occlusion query/volume-cube-textures/custom-effects/instancing/remaining clears/DebugSimulateContextLoss |
✅ | Verbatim port of DX30-70..DX30-76. |
| # | Task | Status | Notes |
|---|---|---|---|
DX5-80 |
Full renamed 2D+3D CTest suite passing | ✅ | 19/19 DX5-labeled CTests. |
DX5-81 |
docs/dx5-backend.md: mirror docs/dx3-backend.md's structure |
✅ | |
DX5-82 |
Update CMakeLists.txt's CNA_GRAPHICS_BACKEND STRINGS docstring, README.md, and plan_dxold.md's DX5 row |
✅ | |
DX5-83 |
Full DX5-labeled CTest suite regression + targeted cross-backend test re-run (mirroring DX30-83's scope decision) |
✅ |
Identical to plan_dx3.md's own Boundaries section: execute-buffer Direct3D (permanently
excluded, doesn't exist at this device revision anyway); fog (lighting itself is real for
normal-bearing strides); multitexture/env-mapping/skinning (accepted-and-ignored); stencil
operations (no real stencil buffer/ops confirmed usable until later — D3DRENDERSTATE-level
stencil support exists in the header but was not part of this plan's scope to verify, matching
DX2/DX3's own identical stencil boundary); IDirectDraw7+/IDirectDrawSurface[23567]+
features; DirectSound/DirectInput/DirectPlay; MRT/instancing/occlusion query/volume-cube
textures/custom programmable effects; real vertex-buffer GPU objects (decision 7 — genuinely
unneeded, not a gap); real Windows/macOS hardware verification (MinGW cross-compile + Wine on
Linux only, same caveat every Route-B CNA backend carries).
plan_dxold.md— the roadmap this plan is row 5 of.plan_dx3.md,docs/dx3-backend.md— the backend this plan ports (2D layer upgraded further to v4; 3D layer upgraded toIDirect3D3/FVF but otherwise identical, including Phase O9 lighting/WireFrame).plan_dx2.md,docs/dx2-backend.md— the original source of the CPU transform/clip pipeline and Phase O9 lighting math, two generations back.dx5-spike/README.md— the fullDX5-0spike record, including the two spike-authoring bugs found and fixed (Clear2's rect requirement, diagonal-seam sample point).