DIRECTX8 (real IDirect3D8/IDirect3DDevice8, DXVK-delivered, fixed-function) Renderer — Completeness Status
DIRECTX8 is architecturally very different from every renderer in this family so far (DIRECTX1..DIRECTX7, all
DirectDraw-based): DirectDraw and Direct3D merge into a single API in DirectX 8, so this renderer has
no DirectDraw at all — a single IDirect3D8::CreateDevice call creates both the device and its
own swap chain, the same device-bring-up shape as this project's own DirectX9Renderer. Delivered
via DXVK 2.6.0's D8VK (Direct3DCreate8 resolved from DXVK's own d3d8.dll), not Wine's
built-in ddraw.dll/d3d8.dll — the same "Route B" pattern D3D9/D3D11/D3D12 already use in this
project, not the Wine-native pattern DIRECTX1..DIRECTX7 use. Scope is fixed-function 3D only, matching
DIRECTX1..DIRECTX7's own CPU-transform-and-submit shape — not real Shader Model 1.x programmable shaders
(a project-owner decision recorded in plan_dx8.md and dx8-spike/README.md, since real XNA
effects need ps_2_0+ regardless of SM1.x support).
This document covers what's specific to DIRECTX8. Unlike DIRECTX3..DIRECTX7 (each a mechanical port of
its predecessor with a documented delta), DIRECTX8's 2D layer, device bring-up, and readback path are
all genuinely new designs, not ports — see plan_dx8.md for the full design-decision record.
| # | Spike | What it proves | Result |
|---|---|---|---|
DX8-0a |
Direct3DCreate8 + IDirect3D8::CreateDevice via DXVK's installed d3d8.dll |
Whether real device creation works through DXVK | ✅ Works — DXVK log confirms genuine engagement ("D3D9DeviceEx", "operating in D3D8 compatibility mode") |
DX8-0b |
Clear(rect_count=0, rects=nullptr, TARGET|ZBUFFER|STENCIL, ...) |
Whether count=0 clears the WHOLE target (D3D9 convention) or nothing (the DIRECTX5-DIRECTX7 Clear2 gotcha) |
✅ Clears everything — exact (10,10,10) readback |
DX8-0c |
SetVertexShader(rawFvfValue) + DrawIndexedPrimitiveUP — Gouraud quad, no vertex buffer |
Whether the FVF-via-SetVertexShader idiom actually works | ✅ Works — real Gouraud interpolation confirmed |
DX8-0d |
DrawPrimitiveUP, far(blue)-then-near(red) overlapping triangles, D3DRS_ZENABLE=TRUE |
Whether real Z-test occlusion works | ✅ Works — exact red readback (near wins) |
DX8-0e |
CreateTexture + SetTexture + SetTextureStageState(D3DTSS_COLOROP, D3DTOP_MODULATE) |
Whether the modern stage-state texture mechanism works from the start (pre-empting DX7-0's own legacy-render-state rejection) |
✅ Works — exact solid-red readback, first try |
DX8-0f |
Stencil write (left-half quad) then test (full-screen quad) | Whether real stencil write+test works | ✅ Works — exact green/black after write, red/black after test |
DX8-0g |
Present() with a real window |
Whether the real DXVK-backed swap chain presents without error | ✅ Works — real Vulkan swapchain confirmed in the log |
One real bug found and fixed by the spike: D3DPRESENT_PARAMETERS.FullScreen_PresentationInterval
must be D3DPRESENT_INTERVAL_DEFAULT (not D3DPRESENT_INTERVAL_IMMEDIATE) in windowed mode, or
CreateDevice fails with D3DERR_INVALIDCALL — real DXVK/D3D8-compat validation, not a Wine bug.
See dx8-spike/README.md for the full record, including two further runtime bugs found only by the
full CTest suite (below).
- No DirectDraw. No
IDirectDrawX/IDirectDrawSurfaceXobject anywhere in this renderer — a singleIDirect3D8::CreateDevice(adapter, type, hFocusWindow, behaviorFlags, &presentParams, &device)call creates the device AND its own real swap chain. No manual "shadow backbuffer + identityBltto primary" trick this whole family needed sinceDX2-0. D3DTLVERTEX/D3DFVF_TLVERTEXare gone from the real headers. D3D8 introduced the generic FVF/vertex-declaration model — callers define their own struct matching the FVF byte layout. The FVF values (D3DFVF_XYZRHW/DIFFUSE/SPECULAR/TEX1) are unchanged, so this renderer'sDirectX8TLVertexstruct reproduces the exact same 32-byte layout the old macro implied.- Render state names changed from
D3DRENDERSTATE_*toD3DRS_*(the underlyingD3DRENDERSTATETYPEenum values are unchanged) — a naming-convention break, not a behavior one. Texture filter/address/anisotropy moved from per-device render states to per-stageD3DTSS_*texture-stage states (D3DTSS_MAGFILTER/MINFILTER/ADDRESSU/ADDRESSV/MAXANISOTROPY). DrawPrimitive/DrawIndexedPrimitiverequire a bound vertex buffer (SetStreamSource) — the CPU-memory-pointer submissionDIRECTX2..DIRECTX7all used isDrawPrimitiveUP/DrawIndexedPrimitiveUPinstead, which additionally require the FVF to be set viaSetVertexShader(fvfValue)first — passing a raw FVFDWORDdirectly as if it were a real vertex-shader handle (D3D9 later split this into a separateSetFVF(); a real, confirmed-working D3D8-only idiom).- No
GetRenderTargetData(a D3D9-only addition) — readback usesCreateImageSurface(a lockable system-memory surface) +CopyRectsfrom the real active surface instead. - No scaled-blit primitive at all. No
StretchRect(D3D9-only) andCopyRectsis same-size-copy only — a real architectural gapDX8-0didn't anticipate, discovered mid-implementation while reusingDIRECTX7's own letterboxing test. Solved with a logical-resolution render target: all rendering (Clear/3D draws/SpriteBatch) targets an internal, offscreenD3DUSAGE_RENDERTARGETtexture sized to the requested virtual/logical resolution;Present()then switches to the real swap chain back buffer, draws the logical texture as a single letterbox-scaled full-screen quad through the same fixed-function pipeline, calls the realPresent(), then restores the logical (or bound customRenderTarget2D) target for the next frame.GetViewportSize()/TransformWindowToLogical/TransformLogicalToWindowall use this logical resolution, matchingDIRECTX1..DIRECTX7's own letterbox convention — NOT D3D9's own simpler "swap chain always matches the window" choice. - 2D
SpriteBatchis a redesign, not a port.DIRECTX1..DIRECTX7all used DirectDraw'sBlt/BltFastfor a CPU-side pixel compositor. With no DirectDraw at all,DirectX8SpriteBatchRendererrenders real GPU-textured quads through the same fixed-function pipeline 3D geometry uses, with realD3DRS_ALPHABLENDENABLE/SRCBLEND/DESTBLENDstate — a genuine capability improvement (real GPU blending, real rotation/scale) overDIRECTX1..DIRECTX7's CPU-approximated-per-blend-mode-formula approach, matching the pattern every non-DirectDraw-based CNA renderer (D3D9/EasyGL/Vulkan) uses. - Real hardware blending, no preset-detection fallback.
DIRECTX2..DIRECTX7's CPU-emulatedApplyBlendStatedetects which of the 4 XNABlendStatepresets a caller's factors match and falls back to a hardcodedAlphaBlendformula for anything else (no real programmable blend hardware to fall back to).DIRECTX8'sApplyBlendStatesetsD3DRS_SRCBLEND/D3DRS_DESTBLENDdirectly from the caller's own factors — ANYBlendState, preset or custom, produces its own genuine hardware blend result. D3D8 has no configurable blend equation though (noD3DRS_BLENDOP— a D3D9 addition), so a customBlendStatewithOpaque's exact factors but a differentBlendFunctionis genuinely indistinguishable from realOpaqueon this hardware — a real, documented DirectX-8-era boundary, verified byDirectX8_Blend's own Check F. AnisotropicFilteringreportstrue— unlike every prior renderer in this family (DIRECTX2..DIRECTX7, all confirmed-absent on their shared software rasterizer),DIRECTX8runs on a real GPU via DXVK, so this is a genuine, distinct capability here.- No native mip chain, same as
DIRECTX2..DIRECTX7:Texture2D::SetData(level>0, ...)throws honestly (DirectX8TextureRenderer/DirectX8RenderTargetRenderer::UpdatePixelsLevel) rather than silently discarding the upload. - Everything else (CPU transform/clip pipeline — Sutherland-Hodgman near-plane clip, perspective
divide,
BasicEffectCPU lighting —VertexBuffer/IndexBufferplain CPU-side storage) is conceptually the same math asDIRECTX7's ownDirectX7ClipVertex/DirectX7ClipTriangleNearPlane/DirectX7ComputeVertexLighting, re-expressed against the newDirectX8TLVertexstruct andDrawIndexedPrimitiveUPsubmission shape — not a mechanical port, since the surrounding API shape differs too much.
The DX8-0 spike above only ever exercises raw D3D8 through a bare Win32 window with no SDL and a
single Present() per run. The full (SDL-based) CTest suite — every test calls Present() twice:
once explicitly in its own Draw(), once more automatically via the CNA framework's EndDraw() —
hit three further real bugs, none of them anticipated by the spike:
- A real
DirectX8Renderer::SetVirtualResolutionbug. The logical render target (texture + surface + depth-stencil) is created once at construction using whatevervirtualWidth/virtualHeightthe renderer happened to be constructed with (the SDL window's own initial/default size, e.g. 800x480) — butGraphicsDeviceManager's ownApplyChanges()callsSetVirtualResolutionafterward with the test's REAL preferred size (e.g. 64x64), and the original implementation just updated bookkeeping fields without recreating the underlying D3D8 resources. This leftReadBackbuffer'sCopyRectscall copying from an 800x480 real surface into a 64x64 destination — a genuine size mismatch,D3DERR_INVALIDCALL. Fixed by havingSetVirtualResolutionactually recreate the logical texture/surface/depth-stencil at the new size (and rebind it as the active render target if it was the active one). - Two environment-specific Wine/DXVK/AMD-RADV bugs, not code defects — both reproduced in
minimal, CNA-free spikes (
dx8_spike3_sdl_load_order.cpp/dx8_spike4_double_present.cpp) and fully documented indx8-spike/README.md's "Two further runtime bugs" section:- SDL3's own internal, dynamic
LoadLibrary("dxgi.dll")probe (for HDR/colorimetry detection, unrelated to whether the game uses Direct3D at all) crashes under this sandbox's Xvfb (no real monitor EDID) whendxgiis overridden to DXVK — corrupting Wine's monitor-enumeration state for the rest of the process. Fixed by givingDIRECTX8its own dedicated~/.wine-cna-dx8Wine prefix withd3d8/d3d9/d3d10/d3d10_1/d3d10core/d3d11native (D8VK genuinely needsd3d9native) butdxgideliberately left as Wine's own builtin. - A real AMD RADV driver bug: the SECOND consecutive
Present()call on this sandbox's GPU fails withVK_ERROR_SURFACE_LOST_KHR(fatal — Wine's X11 error handler aborts the process), 100% reproducible, unrelated to window resizing, timing, or message-pump gaps. Forcing DXVK onto the software (llvmpipe) Vulkan device instead avoids it entirely.scripts/run-wine-directx8.shsetsDXVK_FILTER_DEVICE_NAME=llvmpipeby default for this reason.
- SDL3's own internal, dynamic
- A real D3D8 XYZRHW half-texel alignment bug. D3D8's pre-transformed (
D3DFVF_XYZRHW) vertices place texel/pixel centers at integer coordinates, not integer+0.5 like modern APIs (the same classic D3D8/9 conventionD3D9SpriteBatchRenderercompensates for by baking a half-texel shift into its own orthographic projection matrix). SinceDirectX8SpriteBatchRenderer's quad is already in screen space with no such matrix, the same -0.5 shift is applied directly to the final screen-space quad corners instead (DirectX8SpriteBatchRenderer::DrawandPresent()'s own full-screen quad). Found viaDirectX8_SpriteBatch'sSpriteEffects::FlipHorizontallycheck (a 1:1 texel:pixel mapping, maximally sensitive to any half-texel misalignment) and confirmed to also fixDirectX8_AddressMode'sTextureAddressMode::Mirrorcheck, which failed for the same underlying reason.
20/20 DIRECTX8-labeled CTests pass: DirectX8_LegacyInterfaceDiscipline, DirectX8_Smoke,
DirectX8_TextureRenderTarget, DirectX8_SpriteBatch, DirectX8_Blend, DirectX8_AddressMode, DirectX8_SpriteFont,
DirectX8_GraphicsCapability, DirectX8_LogicalTransform, DirectX8_Device3DSmoke, DirectX8_VertexIndexBuffer,
DirectX8_ColoredPrimitives, DirectX8_IndexedPrimitives, DirectX8_ZTest, DirectX8_Texture3D, DirectX8_Clipping,
DirectX8_RemainingDefaults, DirectX8_Lighting, DirectX8_WireframeAniso, DirectX8_Stencil.
SupportsCapability(GraphicsCapability::WireFrame) reports true; unlike every prior renderer in
this family, AnisotropicFiltering also reports true (real GPU via DXVK — §2 above).
SupportsCapability(GraphicsCapability::DepthStencilBuffer) reports true.
Cross-renderer regression: GraphicsRendererCompileDefinitionsTest.ExactlyOneGraphicsRendererIsSelected,
GraphicsDeviceValidationTest.SetRenderTargets_*, and GraphicsDeviceCapabilityTest.* all pass
except the same 3 pre-existing, already-documented ungated-test-class failures DX2-84 found
(SupportsMultipleRenderTargets/SupportsOcclusionQuery/SupportsCustomEffects — that test file's
own header comment documents it as only ever building against a fully 3D-capable renderer) — zero new
regressions.
The legacy-interface-discipline check (scripts/check-directx8-legacy-interface-discipline.sh) forbids
IDirectDrawX/DDSURFACEDESC/DDSCAPS/D3DTLVERTEX/D3DVT_*/D3DRENDERSTATE_* anywhere in this
renderer's source — a real, automated proof this renderer genuinely has no DirectDraw and uses only
modern D3DRS_* render-state naming, not just claimed.
Same DirectX-8-era boundaries as documented in plan_dx8.md: no Shader Model 1.x programmable
shaders/CreateEffectRenderer (project-owner scope decision), no multitexture, no DXTn compressed
formats, no cube environment maps, no MRT (SetRenderTargets(count>1) throws), no occlusion query,
no native mip chain (SetData(level>0) throws).
plan_dx8.md— this renderer's own implementation plan and design-decision record.dx8-spike/README.md— the fullDX8-0spike record plus the two further runtime-bug investigations (§3 above).docs/directx7-renderer.md— the last DirectDraw-based renderer in this family;DIRECTX8diverges from it architecturally rather than porting it.plan_dxold.md— the roadmap this renderer is a row of.