Latest update (2026-07-14): Phase DX13 (D3D12 functional completion) is fully closed —
DX-116throughDX-123, all 8 rows, real GPU-facing proof throughout. D3D12 now has realPresent()/back-buffer rendering, a real render-target backend (RenderTarget2D/RenderTargetCube/ MRT), a real runtime-settableBlendState/DepthStencilState/RasterizerState→ PSO system, real per-slot dynamicSamplerState(16 slots), a realD3D12OcclusionQueryBackend(closing Phase DX15'sDX-147D3D12 half too), a realD3D12EffectBackend(customShaderEffect), a realD3D12Texture3DBackend, and realD3D12TextureCubeBackend::GetData()readback.D3D12_Smokegrew from 80/80 (Phase DX12's own closing number) to 125/125 checks. Phase DX14 (D3D11 verification hardening) is now also fully closed — all 7 rows (DX-124–DX-130),D3D11_Smokegrew to 120/120 checks (then 135/135 afterDX-136added below). Phase DX15 (remaining EasyGL-parity gaps) is now also fully closed — 18 of 18 rows, real GPU proof throughout:D3D11_Smokefinished at 135/135 checks,D3D12_Smokeat 189/189 checks. Full Phase DX13/DX14/DX15 detail is in those phases' own sections below (search "Phase DX13"/"Phase DX14"/ "Phase DX15"); the paragraphs immediately following this one are the original Phase DX1–DX12 closing summary, kept for history.Status (2026-07-14): BOTH backends' full software/logic layer are done and Wine-verified. D3D11: Phase DX1 through Phase DX11 are ALL closed — every task except
DX-90/DX-91(real-Windows hardware,needs_human, no such machine available in this dev environment). A real, feature-complete-per-this-plan backend: 6 CTest binaries, 96+ checks, all passing through Wine+DXVK on a real GPU, including a working (Wine-verified) swap chain/Present(). D3D12: Phase DX12 (DX-100throughDX-113andDX-115) is now ALSO fully closed — every task exceptDX-114(real-Windows hardware,needs_human, same constraint as D3D11'sDX-90). All 10 stock shader variants draw real, GPU-pixel-verified triangles off-screen through Wine+vkd3d-proton (D3D12_SmokeCTest, 80/80 checks), including a realD3D12SpriteBatchBackend, real device-removed recovery, and mutation-tested discriminating power. Update, same day: the swap-chain crash is fixed.CreateSwapChainForHwndnow genuinely returnsS_OKwhen launched through a real, correctly-bootstrapped Proton runtime (scripts/run-proton-vkd3d.sh, new this session) instead of the isolated hand-built prefixDX-100/DX-102originally used — confirms the earlier diagnosis was right (vkd3d-proton needs its matcheddxgi.dll, not justd3d12.dll) and that this was never a CNA bug. Update, same day:DX-116(realPresent()/back-buffer rendering) is also closed. D3D12 can now genuinely put pixels on a real screen — a real 10-frameClear()+Present()loop throughscripts/run-proton-vkd3d.sh's real Proton launch succeeds with no throw/crash, reproduced twice. This is a manual diagnostic, not a CTest (Proton's bootstrap launch is too heavy for routine CTest runs) — full tearing/vsync/exclusive-fullscreen/resize verification staysDX-114's job on real Windows hardware. Phase DX13 progress, same day:DX-117(real render targets/MRT/RenderTargetCube),DX-118(real runtime-settable BlendState/DepthStencilState/ RasterizerState → PSO state),DX-119(real per-slotSamplerStatevia a genuineTextureAddressMode::Wrap-vs-Clampdiscriminating pixel proof), andDX-120(D3D12OcclusionQueryBackend, closing Phase DX15's ownDX-147D3D12 half too) are now ALSO closed —D3D12_Smokegrew from 80 to 114/114 checks across these five tasks.DX-120itself found and fixed a real, non-obvious bug along the way:BeginQuery/EndQuerymust share one command-list submission with the draw(s) they bracket (a Vulkan/vkd3d-proton constraint this backend's own per-draw self-submission architecture didn't satisfy) — everyDrawXxx()method now brackets its own recording when an occlusion query is active, via a newSetActiveOcclusionQueryEXT().DX-121(D3D12EffectBackend, customShaderEffectsupport) is now ALSO closed — real runtimeD3DCompile()builds a real PSO+constant buffer, pixel-verified (D3D12_Smokegrew to 119/119 checks).D3D12SpriteBatchBackend's ownSpriteBatch::Begin(effect)wiring was implemented for real too, but is honestly NOT independently CTest-proven: it needs a realGraphicsDevice, whose constructor unconditionally creates a real window for any non- Headless/Software backend — the same crash-prone pathDX-100/DX-102already found for D3D12 outside a Proton-managed launch (confirmed by readingGraphicsDevice.cppdirectly, not assumed).DX-122(D3D12Texture3DBackend) is now ALSO closed — a real byte-exact sub-volume upload/ readback round-trip (off-center offset, per-Z-slice colors),D3D12_Smokegrew to 122/122 checks. D3D12 still lacksDX-123'sTextureCube::GetData()real readback plusDX-133's own follow-up (wiringD3D12SpriteBatchBackend's stored-but-inertSetSamplerFilter/SetSamplerAddressModetoDX-119's new system, now unblocked). Phase DX13 (D3D12 functional completion,DX-116–DX-122closed,DX-123remains), Phase DX14 (D3D11 verification hardening), and Phase DX15 (remaining EasyGL-parity gaps, a full feature-matrix sweep covering both backends) were added 2026-07-14 and are now authorized for implementation (project-owner go-ahead given the same day). See the dedicated milestone paragraphs below (search "also closed") for each phase's own real proof, or jump straight todocs/d3d11-backend.md/docs/d3d12-backend.mdfor the current-state summaries anddocs/graphics-backend-feature-matrix.mdfor the full row-by-row comparison.DX-114remains the only open, needs_human task from Phase DX1–DX12; Phase DX13/DX14/DX15 are now authorized and actively being worked through, task by task, in the order each phase's own intro documents. Full phase-by-phase history follows below.DX-100's own real spike (2026-07-14) found the D3D12 device/queue/fence/ command-list path genuinely works locally via Wine+vkd3d-proton (DXR 1.1/SM 6.8 negotiated, more capable than D3D11's own DXVK path), but swap-chain creation specifically crashes/fails under the tested setup — see that row's own Notes for the full evidence and the recommended off-screen-proof workaround forDX-101onward.DX-101(D3D12 CMake wiring) also closed 2026-07-14 — realcmake-build-d3d12MinGW cross-build,CNA_GRAPHICS_BACKEND=D3D12links onlyd3d12+dxgi(perDX-100's confirmed minimum), and a newD3D12GraphicsBackendskeleton (all 21IGraphicsBackendpure virtuals, honest throws) — no real D3D12 API calls yet, that starts atDX-102.
DX-102/DX-103/DX-104/DX-105also closed 2026-07-14 (device-lifetime resources are now real):ID3D12Device(real feature-level retry loop,12_1negotiated on the first try),ID3D12CommandQueue, 3 real descriptor heaps (RTV/DSV/CBV_SRV_UAV, bump-allocated), 2 real per-frameID3D12CommandAllocators + 1 reusedID3D12GraphicsCommandList(kFramesInFlight = 2, matching Vulkan's own constant), and a real sharedID3D12Fencewith a genuineSignalAndWaitForFrameEXTback-pressure primitive. NewD3D12_SmokeCTest (18/18 checks) proves all of it end-to-end through Wine+vkd3d-proton, gated by a realvkd3d-proton - applicationVersion: ...log-line check (scripts/run-wine-vkd3d.sh, mirrorsDX-85's own DXVK gate). Swap-chain creation itself remains the one real, unresolved gap — implemented for real (CreateSwapChainResources(), productionFLIP_DISCARD), but a dedicated, non-CTest, by-hand diagnostic (examples/d3d12_swapchain_diag.cpp) reproduced the exact crashDX-100's raw spike found, this time with a full symbolized backtrace: a null-pointer read inside Wine's owndxgi.dll(d3d12_swapchain_init→vkd3d_instance_get_vk_instance(instance=0)) — seeDX-102's own row for the full evidence. The primary CTest suite is deliberately off-screen-only (window = nullptr) so it never touches this crash path; real swap-chain verification staysDX-114's job on real Windows hardware.
DX-106/DX-107/DX-108also closed 2026-07-14 (barriers, PSOs, root signatures — the pipeline-plumbing layer aboveDX-102–105's device-lifetime resources): a realD3D12ResourceStateTracker(per-resourceD3D12_RESOURCE_STATESmap, emits a transition barrier only when the requested state genuinely differs from the tracked one, proven against a real throwaway committed buffer — a real barrier fires on a genuine state change, no barrier fires on a repeat request for the same state); a realD3D12RootSignatureCache(keyed by(numCbvs, numSrvs, numSamplers)binding shape, root CBVs + a static-sampler-backed SRV table,D3D12SerializeVersioned RootSignature+CreateRootSignature, real cache-hit/miss proven by pointer identity); and a realD3D12PipelineStateCache(one PSO per full(shader variant, stride, blend 6-tuple, depth 3-tuple, rasterizer 2-tuple, RTV/DSV format)key — the "PSO explosion" tradeoff this task's own row explicitly flagged, accepted as the correct first-implementation strategy) — the first realID3D12PipelineStatethis backend has ever created, forcolored3d/stride-16 against a real root signature, viaCreateGraphicsPipelineStatethrough Wine+vkd3d-proton on the real GPU.D3DVertexFormatHelpergrew a D3D12 counterpart of its stride-keyed input-element lookup (InputElementsForStrideD3D12) since D3D12 bakes the input layout directly into the PSO desc (no separateID3D11InputLayout-equivalent object exists in D3D12).D3D12_SmokeCTest grew from 18 to 31/31 checks (Checks G/H/I), all passing via a realctest --test-dir cmake-build-d3d12 -R D3D12run. Stencil state and scissor-enable are a documented, honest gap in the first PSO desc — not yet part of the cache key.
DX-109/DX-110also closed (🟨, honest partial) 2026-07-14 — real GPU resources and a real device-removed recovery path now exist.D3D12VertexBufferBackend/D3D12IndexBufferBackend(D3D12Buffers.hpp/.cpp) andD3D12TextureBackend(D3D12Textures.hpp/.cpp) each go through an explicitD3D12_HEAP_TYPE_DEFAULTGPU-resident resource + a freshD3D12_HEAP_TYPE_UPLOADstaging resource per upload +CopyBufferRegion/CopyTextureRegion, withD3D12ResourceStateTracker(DX-106) driving every transition — real, byte-exact round-trip proof via aD3D12_HEAP_TYPE_ READBACKbuffer (the off-screen-safe D3D12 equivalent of D3D11's ownDX-28staging-readback proof).CreateIndexBuffer32()was explicitly overridden and tested from the start (D3D11's own Phase DX5 fork found and fixed the exact bug of forgetting this override, which silently aliases every 32-bit request to a 16-bit buffer — not repeated here). Render targets and cube/3D textures were deliberately triaged out of this pass (honest 🟨, not silently dropped) since buffers + 2D textures areDX-111's actual prerequisite.RecreateDeviceEXT()genuinely tears down and rebuilds everyDX-102–105device-lifetime resource; real GPU work (a fresh command-list submission, a brand-new vertex buffer's upload+readback) is proven working through the recreated objects afterward — the real removal trigger itself remains untestable on this dev loop, the same honestDX-90-class gap D3D11's ownDX-27has. A real bug was caught in this task's own test: an early Check L draft asserted pointer inequality (GetDeviceEXT() != deviceBefore) as proof of recreation and genuinely failed on a real run —Reset()+recreate legally reusing a freed address is not a sign recreation silently no-op'd; fixed to assert real functional proof instead (fresh work actually succeeds through the recreated device), same "verify, don't assume" discipline this whole plan has followed throughout.D3D12_SmokeCTest grew from 31 to 48/48 checks (Checks J/K/L), all passing via a realctest --test-dir cmake-build-d3d12 -R D3D12run (3 consecutive runs, 48/48 every time).Clear()/Present()/draw calls remain honest throws —DX-111(porting D3D11's own Phase DX8 shader/effect variant set) is the next concrete step toward a real draw.
DX-111(🟨,colored3donly) also closed 2026-07-14 — the first real triangle this D3D12 backend has ever drawn, off-screen and pixel-verified.Clear()/DrawColoredPrimitives()/DrawIndexedColoredPrimitives()are real: a newBindOffscreenColorTargetEXT()NOXNA helper lets a caller-owned/trackedID3D12Resource+RTV stand in for the still-unusable swap chain (DX-100) and the still-deferred publicD3D12RenderTargetBackend(DX-109's own gap); draws get/create a real(2,0,0)root signature (DX-108) andcolored3d/stride-16 PSO (DX-107), populate realD3DPerDrawConstants/D3DFogConstantsinto persistently-mappedD3D12_HEAP_TYPE_UPLOADconstant buffers, and record a realOMSetRenderTargets/viewport/vertex-and-index-buffer/root-CBV/DrawInstanced(orDrawIndexedInstanced) sequence. A real, silent-failure bug was found and fixed, not assumed away: the PSO's default state (depth test enabled with no DSV bound, plus XNA's realCullCounterClockwiseFacedefault) made the very first draw attempt return success but paint nothing — no debug layer exists on this Wine+vkd3d-proton dev loop to report why; root-caused by bisecting (disabling depth had no effect;CullMode::Nonefixed it) — the test triangle's real winding was silently back-face-culled after D3D's NDC→screen-space Y-flip.D3D12_Smokegrew 48→51/51 checks (Check M), reusing D3D11's own Check P discipline (before/after-Clear()pixel-region readback) for both the non-indexed and indexed draw paths. Honest, explicit scope boundary: onlycolored3d(1 of 10 stock variants) is real —textured3d/lit_textured3d/alpha_test3d/dual_texture3d/env_map3d/skinned3d/sprite2d/instanced3d/colored_textured3dare all stillDrawPrimitivesEx-unwired follow-up work, per this row's own "one variant done right beats ten done shallow" triage allowance — see that row's own Notes for the full breakdown.DX-112(SpriteBatch) was consequently NOT attempted (its own directive requiressprite2dto land first).
DX-111extended further, same day (2026-07-14):textured3d,colored_textured3d,lit_textured3d, andalpha_test3dare now also real and pixel-verified — 5 of 10 stock variants done. NewDrawPrimitivesEx()/DrawIndexedPrimitivesEx()overrides onD3D12GraphicsBackendreplicate D3D11's own priority-chain dispatch (alpha-test > lit-textured (stride 32) > colored/ textured/colored_textured bundle by stride) —dual_texture3d/env_map3d/skinned3dexplicitly throw a named "not yet implemented" rather than silently falling through to the wrong shader. Texture binding uses a real SRV descriptor table:D3D12TextureBackend's own SRV (already allocated in the shared CBV/SRV/UAV heap at texture-creation time,DX-109) is bound directly as the 1-descriptor table base — no separate descriptor-copy step needed for a single texture. Root signatures:alpha_test3dneeds its own(1,1,1)shape (single combinedPerDrawcbuffer, no separateFogParams);textured3d/colored_textured3d/lit_textured3dall share one(2,1,1)shape and therefore one cached root-signature object — this corrected a stale speculative claim inD3D12RootSignatureCache.hpp's own doc comment (written ahead ofDX-111actually landinglit_textured3d), which had guessed(2,0,0)for it; the real HLSL (lit_textured3d.frag.hlsl) was read this time, not assumed, and does declare its ownt0/s0texture binding just liketextured3d. No new bugs found — all 5 new checks (Checks N/O/P) passed on the first real Wine+vkd3d-proton run.D3D12_Smokegrew 51→61/61 checks: textured3d/colored_textured3d get the same exact-color/exact-vertex-color-tint proof D3D11's own Check Q used;lit_textured3d's unlit branch is byte-exact, its lit branch is proven to genuinely differ from both the unlit result and the background (real Blinn-Phong, not CPU-replicated);alpha_test3d'sclip()is proven to genuinely discard on a failing pixel and draw the exact color (including a non-255 alpha byte) on a passing one. Still honestly deferred:dual_texture3d/env_map3d/skinned3d/sprite2d/instanced3d(5 of 10 variants) andDX-112(SpriteBatch, needssprite2dfirst) — real, scoped follow-up work, not silently dropped.
DX-111extended again, same day (2026-07-14):dual_texture3dandsprite2dare now also real and pixel-verified — 7 of 10 stock variants done — andDX-112(SpriteBatch) is now real too.env_map3d/skinned3d/instanced3d(3 of 10 variants) remain honestly deferred. Two real, non-obvious dev-loop bugs were found and fixed while landing this slice, neither a CNA logic mistake caught by inspection — both only surfaced by actually running the real pixel-readback proof and getting a wrong answer:
- Multi-descriptor root tables sample as all-zero under this machine's Wine+vkd3d-proton setup.
dual_texture3dneeds 2 SRVs (t0/t1); the first attempt built ONED3D12_DESCRIPTOR_TABLEroot parameter withNumDescriptors=2, populated via a fresh per-drawCopyDescriptorsSimpleinto a contiguous bump-allocated heap-slot pair — every CPU-side detail was independently verified correct (heap offsets incrementing by exactly one descriptor stride, both copies targeting sane, correctly-typed source/destination handles) and it STILL sampled as pure(0,0,0,0)on the real GPU. Fixed by switchingD3D12RootSignatureCacheto build N separate single-descriptor tables (one root parameter per texture register, t0/t1/...) instead of one shared multi-descriptor range — each texture's own permanent SRV (created once, at texture-construction time) is now bound directly to its own root parameter, exactly mirroring the already-proven single-SRV path, with no per-draw descriptor copy needed at all (simpler code, not just a workaround). Documented inD3D12RootSignatureCache.hpp's own doc comment as an empirical dev-loop finding, explicitly flagged for re-verification onDX-90/DX-114's real-Windows pass rather than asserted as universal D3D12 behavior.- D3D rasterizes at pixel CENTERS (
UV = (px+0.5)/width), notpx/width. The firstsprite2dpixel-readback test used a 2-texel-wide (1 red, 1 blue) source texture and checked render-target pixels whose naively-assumed UV (px/width) landed exactly on each texel's center — the real, pixel-center-adjusted UV was off by half a source-texel's worth of distance, so the LINEAR-filtered static sampler blended in ~1.5% of the neighboring texel ((197,50,33)/(33,80,217)instead of the exact(200,50,30)/(30,80,220)) — a genuine GPU sampling behavior, not a D3D12SpriteBatchBackend bug. Fixed by widening the test texture to 4 texels (2 red, 2 blue) so the readback points sit well inside a same-colored pair — blending red with red (or blue with blue) is exact regardless of the blend weight, sidestepping the need to solve for a perfectly pixel-center-aligned coordinate.
dual_texture3d's own layout:D3DPerDrawConstants@b0 (shared shape) + a dedicatedFogParams@b2 (t0/s0, t1/s1 already occupy the "next free slot" a single-texture variant would use for fog, perDX-13-hlsl's own note) — b1 goes unused by this shader but the root signature still declares it (3 contiguous CBV slots), bound to a harmless dummy address rather than left unset. NewD3D12SpriteBatchBackend(D3D12SpriteBatch.hpp/.cpp) mirrorsD3D11SpriteBatchBackend's exact destination/source-rect/origin/rotation/SpriteEffects-flip quad-building formula (CPU-side XNA geometry math, unchanged), submitted through a dedicated hand-built PSO/input-layout (NOT through the shared, stride-keyedD3D12PipelineStateCache/D3DVertexFormatHelper— sprite2d's 32-byteSprite2DVertexcollides with stride-32's existingVertexPositionNormalTexturemeaning, the same real collision risk D3D11's ownDX-70fork already flagged and worked around) but a shared(1,1,1)-shape root signature (genuinely reused fromalpha_test3d's own object — a root signature only describes binding slots, not cbuffer contents). Honest scope gaps, documented inD3D12SpriteBatch.hpp's own header comment, not silently dropped:SpriteBatch::Begin(effect)(D3D11's ownDX-71) throws a named error if actually flushed, since no D3D12 equivalent ofD3D11EffectBackend/DX-58exists yet;SetSamplerFilter/SetSamplerAddressMode(D3D11's ownDX-72) are stored but not yet behaviorally real, since no D3D12 dynamic-sampler-state system (a Phase-DX7 equivalent) exists yet either — the root signature's static sampler stays fixed at LINEAR/WRAP.D3D12_Smokegrew 61→68/68 checks (Checks Q/R):dual_texture3d's exact 2-texture-multiply-and-tint result, non-indexed and indexed;D3D12SpriteBatchBackend::Draw()'s exact quad placement and a genuineSpriteEffects::FlipHorizontallyproof (left/right sampling demonstrably swaps, not just "a draw call succeeded"). D3D11 rebuilt clean throughout (only sharedD3DVertexFormatHelper/doc-comment files touched, no D3D11 runtime code). Still honestly deferred:env_map3d(needs a newD3D12TextureCubeBackend, not yet built),skinned3d(needs a 3rd root CBV forD3DBoneConstants),instanced3d(needs a 2-stream instanced input layout) — real, scoped follow-up work, not silently dropped.
DX-111finished, same day (2026-07-14):skinned3dandinstanced3dare now also real and pixel-verified — 9 of 10 stock variants done.skinned3dneeded no new caching infrastructure — the existing(numCbvs,numSrvs,numSamplers)-keyedD3D12RootSignatureCache/D3D12PipelineStateCachealready generalize to its(3,1,1)shape (PerDraw@b0+BoneBlock@b1+ skinned3d's ownFogParams-equivalent@b2, stride 52), andD3DVertexFormatHelper:: InputElementsForStrideD3D12already covered stride 52 fromDX-107— this task only needed two new persistent constant buffers (GetOrCreateBoneConstantBufferEXT()/GetOrCreateSkinnedExtraConstantBufferEXT()) and a direct, unmodified port ofD3D11GraphicsBackend::DrawPrimitivesExImpl's ownneedsSkinnedCPU-side field population, including its already-established finding thatGpuDrawParams::boneTransformsis a straightmemcpy, not a per-matrix transpose.instanced3dneeded its own hand-built PSO/input-layout (newGetOrCreateInstancedPsoEXT()), the same reasonsprite2ddid — a genuinely different 2-input-slot layout (POSITION0per-vertex @ slot 0,INSTANCEWORLD0-3per-instance @ slot 1)D3DVertexFormatHelper's single-stream helper can't express — plus a newDrawInstancedPrimitivesEx()override (the first one this backend has ever had;IGraphicsBackend's own default just throws) using a(1,0,0)root-signature shape (PerDraw@b0only —instanced3d.frag.hlslis genuinely textureless, confirmed by reading the real HLSL). No new bugs found — both new checks (Checks S/T) passed on the first real Wine+vkd3d-proton run, reusing the exact vertex/GpuDrawParamsfixtures D3D11's own equivalentCheck V/Check Walready established.D3D12_Smokegrew 68→72/72 checks. D3D11 rebuilt clean throughout (no sharedD3DCommonfiles touched this time — D3D12-only changes).DX-111closed for real 2026-07-14:env_map3dlanded — 10 of 10 stock shader variants now real across every CNA graphics backend, D3D12 included. Needed a newD3D12TextureCubeBackend(D3D12TextureCube.hpp/.cpp) — a real 6-faceID3D12Resourcewith the same explicit upload-heap-staging disciplineD3D12TextureBackendalready established, per-faceSetData()uploads,D3D12_SRV_DIMENSION_TEXTURECUBESRV (GetData()left at the interface's own no-op default — a genuine, honest scope gap since this task's own pixel proof reads the render target back, not the cube texture). Root signature/PSO: reuses the exact(3,2,2)shapedual_texture3dalready created and cached — no new object.D3D12_Smokegrew 72→74/74 checks (Check U), the same geometrically-constrained-reflection methodology D3D11's ownDX-66used. Two real, unrelated bugs found and fixed along the way: the RTV descriptor heap's fixed bump-allocator capacity (DX-103, 8 slots) was genuinely exhausted by the growing CTest suite the moment this task's own render target was allocated — raised to 32; andRecreateDeviceEXT()(DX-110) was never resettingboneConstantBuffer_/skinnedExtraConstantBuffer_/instancedPso_despite them being just as device-tied as every other cached buffer — fixed in the same pass. D3D11 rebuilt clean throughout (no sharedD3DCommonfiles touched). The next open Phase DX12 step isDX-113(test-suite formalization) orDX-115(docs) —DX-112(SpriteBatch) was already closed againstsprite2d, and every one ofDX-101–DX-111is now ✅ exceptDX-109's own honest 🟨 (render targets/cube/3D textures beyond whatDX-111itself needed were deliberately triaged out).
DX-113(test-suite formalization) also closed 2026-07-14 — an audit-and-close pass, not a from-scratch suite:DX-102–111's own landing forks had already built 74 genuine off-screen pixel-readback checks incrementally; this task audited that coverage against D3D11's ownDX-80–85methodology and closed exactly 2 real gaps — a dedicated fog on/off discrimination test (D3D12_SmokeCheck V, same fixture as D3D11's own Check AC, same underlying DXBC) and theD3D12ResourceStateTracker's own documented-but-untested throw-on-untracked-resource contract (Checks G8–G10). A real mutation test on Check M (DrawColoredPrimitives's non-indexed path,VertexColorEnabledflipped1.0f→0.0f) failed exactly the one check it should have (M1, 79/80) and left the indexed M2 path untouched, confirmed via a real rebuild+run, then reverted and reconfirmed 80/80. Two gaps left honestly open, not silently claimed: D3D12 has noD3D11StateObjectCache-equivalent yet (blend/depth/rasterizer is a hardcoded PSO simplification, not a settable object — DX-82-equivalent testing genuinely isn't applicable until that exists), and Check E's fence back-pressure proof covers the value-ordering/wait-completion contract for real but doesn't independently measure a genuine CPU stall under real GPU load (no intervening GPU work is issued to force one). The only open Phase DX12 work is nowDX-115(docs) —DX-114staysneeds_human(real Windows hardware).The paragraphs immediately below are kept as the original, blow-by-blow session history (Phase DX1 → DX2 → DX4's core →
DX-80, 2026-07-13) — historically accurate at the time each was written, not stale placeholders.Status: Phase DX1 + Phase DX2 + Phase DX4's core +
DX-80all closed 2026-07-13, authorized and completed the same day.D3D11GraphicsBackendis real code now, not just a plan:include/CNA/Internal/Backends/D3D11/D3D11GraphicsBackend.hpp/.cppimplement everyIGraphicsBackendpure virtual (real device/swap-chain/back-buffer/clear/present/readback; honest "not yet implemented" throws for buffers/textures/draws/SpriteBatch, each naming its own future phase). Headline proof, not a simulated/assumed result:ctest -R D3D11→D3D11_Smokepasses 3/3 checks running through DXVK 2.6.0 on a real GPU (AMD Radeon 780M, RADV driver) — feature level negotiated to11_1, tearing capable, debug layer enabled, and — the actual point —Clear()+GetBackBufferData()round-trips the exact clear color for two different colors/ regions in the same run.CNA_GRAPHICS_BACKEND=D3D11is a real, working CMake option (D3D11-only — noD3D12scaffolding, per design decision 9). Along the way, buildingCNA/a real test executable under this cross-target surfaced and fixed 4 genuine pre-existing MinGW-portability bugs unrelated to D3D11 itself (2 in siblingsharp-runtime, 2 incna_graphics— seeDX-15's own row for specifics); the fullCnaTestssuite's own portability gap (~10 test files calling POSIX-only::setenv()) is now also fixed —CnaTestsbuilds and runs under MinGW (DX-15, closed 2026-07-14). Untouched/known gaps, honestly recorded row-by-row rather than silently claimed: the 5 comboClear*variants, window resize (DX-29), device-lost recovery (DX-27's detection code exists but was never triggered), and the debug-layer-missing fallback path (DX-21) are all real, implemented-but-unexercised gaps. Seeprograms.md§9 for the DXVK install commands this all built on.Phase DX3's mapping tables also closed 2026-07-13 (
DX-11-fmt/DX-12-state/DX-16-vtx):D3DCommonis a real shared static library now (cna_backend_graphics_d3dcommon), with the fullSurfaceFormat/DepthFormat→DXGI_FORMATtable, theBlend/BlendFunction/CompareFunction/CullMode/FillMode/TextureAddressMode/TextureFilter→D3D11_*table (its own "D3D11/D3D12 values are identical" claim independently verified against both real SDK headers on this machine, not assumed), and the stride-keyedD3D11_INPUT_ELEMENT_DESCvertex-layout helper. A newD3D11_CommonCTest (23/23 checks, pure-function, no GPU needed) verifies all of it, mutation-tested for real discriminating power.
DX-13-hlsl(HLSL shader porting) closed 2026-07-13 — all 20 files (10 variants × vertex/ pixel stage) ported line-by-line from the Vulkan GLSL source intosrc/CNA/Internal/Backends/D3DCommon/shaders/*.hlsl, hand-reviewed against the GLSL. Matrix convention:row_majorcbuffer matrices +mul(v, M)everywhere GLSL readsM * v(standard XNA/HLSL idiom). Two genuine HLSL-vs-GLSL deviations found and handled: a hand-writtenInverseTranspose3x3()helper (HLSL has no built-ininverse()), and an explicit Y-negation insprite2d.vert.hlslthat the GLSL source doesn't need (D3D/Vulkan NDC-convention difference — see that row's own notes).
DX-14-compilealso closed 2026-07-13 — and it's the real compiler-verification proofDX-13-hlslwas still missing. A standalonehlsl_compiler_tool.exe(cross-built via this project's own MinGW-w64 toolchain, links onlyd3dcompiler) calls realD3DCompile()on each of the 20.hlslfiles throughscripts/run-wine-dxvk.sh(DX-3's Wine+DXVK harness). All 20 compiled cleanly on the first real run, zero HLSL bugs found — confirmsDX-13-hlsl's hand-port was correct, not just plausible-looking. Output DXBC bytes (each independently confirmed to start with the realDXBCmagic) are embedded insrc/CNA/Internal/Backends/D3DCommon/shaders/ hlsl_shaders.hppviacompile_shaders_hlsl.py, checked in likespirv_shaders.hpp, same manual-regen convention (no CMake build-target wiring, matching the SPIR-V precedent).
DX-15-embedalso closed 2026-07-13 — Phase DX3 is now fully closed. A newD3DCommonshader cache (D3DShaderCache.hpp/.cpp) wireshlsl_shaders.hpp's DXBC bytes into realdevice->CreateVertexShader()/CreatePixelShader()calls, one pair perDX-13-hlslstock variant, returningWRL::ComPtrs (design decision 10).D3D11_Smoke(examples/ d3d11_smoke_test.cpp) grew a real Check D that creates all 20 shader objects through the same live device the existing Clear()/Present() checks already use — 13/13 checks pass (up from 3/3), each of the 10 variants confirmed to produce non-null vertex+pixel shader objects, not just compiler-accepted bytes.D3D11CTest total is now 36/36 checks (D3D11_Smoke13 +D3D11_Common23), verified viactest --test-dir cmake-build-d3d11 -R D3D11. Deliberately narrow scope, per this row's own boundary: no constant buffers, no input-layout binding, no draw calls — that's Phase DX8/DX-32, which now has a ready-to-call shader cache to build on. Authorized 2026-07-13 by the project owner to continue autonomously through Phase DX5 → DX6 → DX7 → DX8 → DX9 → DX10 → DX11, and Phase DX12 (D3D12) afterward if time/context allow — this is no longer gated per-phase the way the banner text below used to require;DX-90's real-Windows checklist stays explicitlyneeds_human(no such machine available here).Phase DX5 (vertex/index buffers + input layout,
DX-30/DX-31/DX-32) also closed 2026-07-13.D3D11VertexBufferBackend/D3D11IndexBufferBackend(D3D11Buffers.hpp/.cpp) are realD3D11_USAGE_DYNAMICbuffers updated viaMap/Unmap, withSetDataOptions::Discard/NoOverwrite/Nonemapped toD3D11_MAP_WRITE_DISCARD/_NO_OVERWRITE/_DISCARDrespectively. Found and fixed a real pre-existing gap along the way:D3D11GraphicsBackendonly ever declared/implemented the 16-bit index-buffer factory, silently inheritingIGraphicsBackend's ownCreateIndexBuffer32default (which just delegates toCreateIndexBuffer16) — any caller asking for a 32-bit index buffer was silently handed a 16-bit one. Now overridden for real.D3D11InputLayoutCache(D3D11-local, notD3DCommon—ID3D11InputLayouthas no D3D12 equivalent object, design decision 4's boundary) wiresDX-16-vtx's stride tables +DX-15-embed's vertex-shader bytecode into real, cachedCreateInputLayout()calls. Real proof, not assumed:d3d11_smoke_test.cppgrew Checks E/F/G — a vertex buffer and both index buffer widths round-trip exact bytes through a genuine GPU write (Map) + read (CopyResourceto a staging buffer +Map(READ), the same techniqueDX-28'sReadBackbuffer()already uses), and the input layout cache both creates a realID3D11InputLayoutfor two established strides and proves real caching (identical pointer on a repeat request) — 18/18 smoke checks pass (up from 13/13),D3D11CTest total now 41/41 checks (D3D11_Smoke18 +D3D11_Common23), verified viactest --test-dir cmake-build-d3d11 -R D3D11. Scope boundary honored: no constant buffers, no draw calls —DrawColoredPrimitives/DrawIndexedColoredPrimitivesstill correctly throw "not yet implemented" (Phase DX8).Phase DX6 (textures/render targets,
DX-40–DX-47) also closed 2026-07-13 — all 8 rows, including the 2 gap rows found in the prior post-hoc review.D3D11TextureBackend/D3D11TextureCubeBackend/D3D11Texture3DBackend(D3D11Textures.hpp/.cpp) andD3D11RenderTargetBackend/D3D11RenderTargetCubeBackend(D3D11RenderTargets.hpp/.cpp) are real. Required a genuine, load-bearing fix, not just new classes:Clear()/ClearColorAndDepth/ etc. were hardcoded to the back buffer's own RTV/DSV since Phase DX4 (nothing else existed to bind yet) — now routed throughcurrentColorRTVs_/currentDSV_tracking so a bound custom render target (or an MRT set) is what actually gets cleared, matching every other backend's "clear the active target" semantics. MSAA (DX-45) is real, device-queried (CheckMultisampleQualityLevels, never assumed) — this machine's RADV/DXVK GPU genuinely grants the requested 4x. MRT (DX-46) is one realOMSetRenderTargetscall binding up to 8 targets — simpler than Vulkan's own MRT-proxy approach, a genuine D3D11-immediate-binding-model advantage. Occlusion queries (DX-47) are a realID3D11Query(D3D11_QUERY_OCCLUSION). Real proof, not assumed:d3d11_smoke_test.cppgrew Checks H–N (texture/cube/3D-texture round-trip, render-target clear+readback+unbind-restores- backbuffer, MSAA clear+resolve, sampler cache, occlusion query, 2-target MRT clear) — 29/29 smoke checks pass (up from 18/18),D3D11CTest total now 52/52 checks (D3D11_Smoke29 +D3D11_Common23), verified viactest --test-dir cmake-build-d3d11 -R D3D11. Scope boundary honored: no constant buffers, no draw calls, no shader texture-sampling wiring —DrawColoredPrimitives/etc. still correctly throw "not yet implemented" (Phase DX8, next).Phase DX7 (state objects,
DX-50–DX-53) also closed 2026-07-13 — all 4 rows.D3D11BlendStateCache/D3D11DepthStencilStateCache/D3D11RasterizerStateCache(D3D11StateObjectCache.hpp/.cpp) create and cache realID3D11BlendState/ID3D11DepthStencilState/ID3D11RasterizerStateobjects fromApplyBlendState()/ApplyDepthStencilState()/ApplyRasterizerState()'s raw XNA-ordinal parameters, viaDX-12-stateplus a newD3DStateMapping::StencilOperationToD3D11(sharedD3DCommon, design decision 4). A real, documented unit-convention finding: XNA'sRasterizerState.DepthBiasfloat is the same "r"-scaled convention this project's Vulkan/EasyGL backends already feed unscaled intovkCmdSetDepthBias/glPolygonOffset(Task 767) — D3D11's ownDepthBiasfield is the identical convention butINT, notFLOAT, so this task rounds rather than truncates. Also implementedSetBlendFactor()/SetReferenceStencil()(Task 870/319's standalone-immediate-effect device properties) andSetViewport()/SetScissorRect()(directRSSetViewports()/RSSetScissorRects()calls, no caching needed — single-slot device state, not object-creating). Real proof, not assumed:d3d11_smoke_test.cppgrew Check O — cache identity/distinctness for all 3 state types,Apply*State()'s real bind confirmed viaOMGetBlendState()/OMGetDepthStencilState()/RSGetState(),SetBlendFactor()/SetReferenceStencil()'s standalone re-bind, andSetViewport()/SetScissorRect()'s exact round-trip viaRSGetViewports()/RSGetScissorRects()— 42/42 smoke checks pass (up from 29/29),D3D11CTest total now 65/65 checks (D3D11_Smoke42 +D3D11_Common23), verified viactest --test-dir cmake-build-d3d11 -R D3D11. Scope boundary honored, same as Phases DX5/DX6: no constant buffers, no draw calls — actual blended/stencil-tested/culled pixel output needs a real draw call, Phase DX8, next.Phase DX8's foundational tasks (
DX-60/DX-60a/DX-61) closed 2026-07-13 — this backend rendered its first real, pixel-verified triangle. New header-onlyD3DConstantBuffers.hpp(D3DCommon) definesD3DPerDrawConstants/D3DFogConstants/D3DLightingConstants/D3DBoneConstants, every field offsetstatic_assert-verified against the real, already- compiler-verified HLSLcbufferdeclarations (DX-13-hlsl/DX-14-compile) — not re-derived from the plan's own prose.D3D11GraphicsBackend::DrawColoredPrimitives()/DrawIndexedColoredPrimitives()(previously honest throws) now do a realcolored3ddraw: cached shaders (DX-15-embed) + cached input layout (DX-32) + two lazily-created persistent constant buffers, updated viaMap/Unmapeach draw, bound and issued through a realDraw()/DrawIndexed()— stride-16 (VertexPositionColor) only; other strides still throw a clear, named-successor error. A real, independent bug found and fixed getting the first pixel test to pass:DX-46'sSetRenderTargets(nullptr, 0)never restored the back buffer after a prior MRT bind (only the single-target path'scurrentCustomRT_tracking triggered the restore), leaving the device context bound to render-target views a test'sunique_ptrs had already destroyed — seeDX-46's own row. Real proof: newD3D11_SmokeCheck P clears to a known blue, reads back a fixed region (confirms blue), draws a real NDC-covering triangle with solid vertex color, reads back the same region again (confirms red) — for both the indexed and non-indexed draw paths. 44/44 smoke checks pass (up from 42/42),D3D11CTest total now 67/67 checks (D3D11_Smoke44 +D3D11_Common23), verified viactest --test-dir cmake-build-d3d11 -R D3D11.D3DLightingConstants/D3DBoneConstantsare defined but not yet wired into any draw call (that'sDX-63/DX-67) — deliberately scoped this way so the layout is settled once, ahead of the variants that need it. Remaining Phase DX8 rows (DX-62–DX-69,DX-58) — the other 9 shader variants, fog wiring, and customShaderEffect— are next.
DX-62/DX-63/DX-64(andDX-69's fog wiring for those 5 variants) also closed 2026-07-13. NewDrawPrimitivesEx()/DrawIndexedPrimitivesEx()overrides (sharing oneDrawPrimitivesExImplhelper) give this backend its first real,GpuDrawParams-driven effect dispatch —textured3d/colored_textured3d(stride 20/24,D3DPerDrawConstants/D3DFogConstants),lit_textured3d(stride 32, first real consumer ofD3DLightingConstants— full Blinn-Phong), andalpha_test3d(a new dedicatedD3DAlphaTestConstantsstruct, since its HLSL cbuffer shape genuinely differs fromD3DPerDrawConstants) all now draw for real, priority-ordered exactly likeVulkanGraphicsBackend::DrawPrimitivesEx(alpha-test > dual-tex > env-map > skinned > lit-texturedstride-selected colored/textured bundle) —
dual_texture3d/env_map3d/skinned3dthrow named "not yet implemented, see DX-65/66/67" errors rather than silently falling through to the wrong shader. Real GPU proof, zero bugs found on the first run: new Checks Q/R/S —textured3dsamples an exact known texture color (proven through both the indexed and non-indexed entry points, confirming they share one real code path);colored_textured3dmultiplies an exact vertex color through a white texture;lit_textured3d's unlit branch is byte-exact, its lit branch is proven to genuinely execute (differs from both the unlit result and the background, real Blinn- Phong math not byte-replicated on the CPU side);alpha_test3d'sclip()is proven to genuinely discard a failing pixel (background survives untouched) and to draw the exact texture color including its own non-255 alpha byte on a passing one. 51/51 smoke checks pass (up from 44/44),D3D11CTest total now 74/74 checks (D3D11_Smoke51 +D3D11_Common23), verified viactest --test-dir cmake-build-d3d11 -R D3D11.DX-69fog wiring for these 5 variants is real (the C++ side now populates fog fields fromGpuDrawParamsfor all of them) but not yet exercised by a dedicated fog-on/fog-off pixel check — Checks Q/R/S all draw withfogEnabled=false— an honest, open gap for Phase DX10, not a false claim;DX-69stays 🟨 (open) for the still- unimplementeddual_texture3d/env_map3d/skinned3d/sprite2d/instanced3dvariants. Remaining Phase DX8 rows (DX-65–DX-68,DX-58) are next.Phase DX8 is now fully closed (2026-07-13) — every stock 3D shader variant + custom
ShaderEffectthis backend set out to support actually draws for real.DX-65/DX-66/DX-67landdual_texture3d(two real SRVs/samplers,t0/s0+t1/s1),env_map3d(a realTextureCubeSRV, reflection direction constrained by test geometry to land deep inside one distinctly-colored cube face — not just "the call succeeded"), andskinned3d(a genuinely-populatedD3DBoneConstantsbuffer via a straightmemcpyfromGpuDrawParams::boneTransforms— confirmed not a row/column-major transpose bug by tracingSkinnedEffect::SetBoneTransforms()'s ownMatrix::ToColumnMajor()call).DX-68lands realDrawInstancedPrimitivesEx()(a new fixedINSTANCEWORLD0–3instanced input layout,context_->DrawIndexedInstanced()) forinstanced3d;sprite2dis deliberately left unwired into any draw dispatch — its real home isSpriteBatch(Phase DX9, not started), notGpuDrawParams-driven 3D dispatch.DX-69(fog) is now real for all 8 fog-capable variants,sprite2d/instanced3dgenuinely have no fog cbuffer to wire.DX-58adds a newD3D11EffectBackend(runtimeD3DCompile(),d3dcompilernow linked into the main backend target for real —objdump -pconfirms a realD3DCOMPILER_47.dllimport) mirroringVulkanEffectBackend's own fixed-slot uniform convention; itsBind()/uniform path is independently GPU-proven but not yet wired into an actualSpriteBatchdraw loop (samePhase DX9dependency assprite2d). Real GPU proof, zero bugs found in the 4 new-variant checks (one real pre-existing bug found and fixed along the way, in the DX-60/61 foundational work — see that row): new Checks T/U/V/W/X. 58/58 smoke checks pass (up from 51/51),D3D11CTest total now 81/81 checks (D3D11_Smoke58 +D3D11_Common23), verified viactest --test-dir cmake-build-d3d11 -R D3D11.Phase DX9 (SpriteBatch,
DX-70/DX-71/DX-72) also closed 2026-07-13 — D3D11 now has a real SpriteBatch, resolving both of Phase DX8's own deferred dependencies (sprite2d's draw path andDX-58's end-to-end integration) in the same task. NewD3D11SpriteBatchBackend(D3D11SpriteBatch.hpp/.cpp), structurally mirroringEasyGLSpriteBatchBackend's immediate- flush-per-texture-change quad batcher rather thanVulkanSpriteBatchBackend's frame-end-snapshot design (D3D11's context is already immediate-mode, same as GL). One real, deliberate improvement over Vulkan's own precedent:SetTransformMatrix()genuinely works here (Vulkan leaves it a silent no-op) — applied on the CPU, per vertex, viaVector2::Transform(), sincesprite2d.vert.hlsl's real contract has no projection-matrix uniform to fold it into GPU-side.DX-71reusesDX-58'sD3D11EffectBackenddirectly, addingSetViewportSizeEXT()to fill thevpSizeslot that class's own header comment had already reserved.DX-72needed no new implementation at all —D3D11SamplerCache(DX-44) already handles Wrap/Mirror for real; this row is purely the verification that it does. Tested through the real public API (Microsoft::Xna::Framework::Graphics::SpriteBatch+Texture2D, not the raw backend interface) — 6 new checks (Y/Z/AA), every one passing on the first real Wine+DXVK run on the real GPU, including two deliberately discriminating probe pixels for Wrap/Mirror (chosen so a broken Clamp-fallback would read a genuinely different, wrong color, not just "a color"). 64/64 smoke checks pass (up from 58/58),D3D11CTest total now 87/87 checks (D3D11_Smoke64 +D3D11_Common23), verified viactest --test-dir cmake-build-d3d11 -R D3D11.Phase DX10 (
DX-81–DX-85) also closed 2026-07-13 (DX-90/DX-91stay explicitlyneeds_human/best-effort, unchanged — no real Windows machine available here).DX-81's pixel- test coverage was audited row-by-row against Phase DX8/DX9's already-landed Checks P–AA and found genuinely complete; the one open question it raised (whether D3D11's clip-space Z range needs a Vulkan-Task 899-style adjustment) resolved to "no" — D3D11's native[0,1]Z range is the same DirectX-convention range the HLSL shaders already inherited from their Vulkan GLSL source.DX-82added 4 new real CTest entries (D3D11_BlendState_{Opaque,AlphaBlend},D3D11_DepthStencilState_ StencilEnable,D3D11_RasterizerState_CullMode) by reusing the exact backend-agnosticeasygl_*_test.cppsources Vulkan already reuses verbatim — genuine pixel-behavior proof (blend math, stencil gating, winding-order culling), not just Phase DX7's narrower object-creation bar; all 4 passed on the first real run.DX-83closedDX-29's long-flagged "implemented but never exercised" resize gap for real: a new Check AB resizes 64×64→96×80 via the publicGraphicsDeviceManagerAPI and confirms DXVK's own presenter log shows the new buffer size, with a correct post-resizeClear()+readback.DX-84ran a real mutation-test pass onDX-61'scolored3dtriangle check — a first mutation (reversed WVP multiply order) was a genuine, honest false negative (Check P uses identity matrices, so multiply order is a no-op there), and a second mutation (VertexColorEnabledflipped off) reproduced the exact predicted failure before a verified revert.DX-85hardenedscripts/run-wine-dxvk.shitself to automatically assert aDXVK: <version>marker appeared in every run's output — directly answering the project owner's own flagged concern ("pouhé spuštění pod Wine nestačí") — with a real, live-caught false-positive along the way (D3D11_Commonlegitimately never opens a device, so it needed its own narrowly- scopedCNA_D3D11_SKIP_DXVK_GATE=1opt-out, wired via CTestENVIRONMENT, not a weakened gate).D3D11CTest total now 6 tests, 92 embedded smoke/common checks + 10 more assertions across the 4 new state-object tests (D3D11_Smoke69 +D3D11_Common23 + 4 new tests), all verified via a realctest --test-dir cmake-build-d3d11 -R D3D11run — the last 2 ofD3D11_Smoke's checks (Check AC, added 2026-07-14) closeDX-69's own honestly-flagged "fog wired but not exercised by a dedicated on/off pixel test" gap. Phase DX11 (docs) is the only phase left before Direct3D 12 can be considered.Phase DX11 (docs,
DX-95–DX-98) also closed 2026-07-14 — every phase of this plan is now closed except the two explicitly-deferred real-Windows-hardware rows and Phase DX12 itself. Newdocs/d3d11-backend.md(mirrorsdocs/software-backend.md/docs/headless-backend.md's own structure), a realD3D11column added to every applicable table indocs/graphics-backend-feature-matrix.md(honestly mixing ✅/🟨/⬜ per row rather than blanket-✅ing anything the underlying code merely supports), a new "Build (Windows cross-compilation — D3D11 backend)" section + "Tested Compilers" row inREADME.md, and confirmation thatNEXT.mdhas cross-referenced this plan since Phase DX1. This is the actual milestone: Phase DX1 through DX11 are all ✅ — real device/swap-chain/back-buffer, a sharedD3DCommonmapping/shader/ constant-buffer core, all 10 stock HLSL shader variants + customShaderEffect, real vertex/index buffers, textures/render targets (MSAA/MRT/occlusion queries), cached state objects, a real SpriteBatch, and a cross-cutting test suite (mutation-verified, DXVK-engagement-gated) — 6 CTest binaries, 96+ checks, all passing through Wine+DXVK on a real GPU. What's left:DX-90/DX-91(real Windows hardware —needs_human, no such machine available here) and Phase DX12 (D3D12, separately authorized by the project owner to start "later if time allows," not yet begun).Direct3D 11 is the actual near-term target; Direct3D 12 is written up in full but authorized to follow once D3D11 is substantially complete — see "Why D3D11 first, D3D12 later" below.
Status legend: ✅ implemented and verified against its stated acceptance criteria; 🟨 code or documentation exists but has not met those criteria; ⬜ not implemented.
XNA 4.0 itself was a thin, XNA-flavoured wrapper over Direct3D 9 (Windows) / Direct3D 9 (Xbox 360),
and FNA (this project's own authoritative reference, /rv/data/library/github.com/FNA-XNA/FNA)
runs on top of a modern Direct3D on Windows via MojoShader/FAudio-era tooling in its own history.
A native Direct3D backend is therefore not "one more backend for coverage's sake" — it is the
closest thing CNA can have to an XNA-authentic Windows reference implementation, useful for:
- Cross-checking CNA's own rendering behavior against something closer to XNA's original Windows execution environment than EasyGL/Vulkan/Bgfx/WebGPU/SDL_Renderer.
- A dependency-free Windows path: CNA already reaches D3D11/D3D12 indirectly today, since
BGFXcan select a D3D11 or D3D12 renderer internally on Windows — but that's bgfx's own abstraction, not CNA's. A native backend removes the bgfx dependency for Windows users who want it, and gives this project full control over the exact Direct3D calls being made (matchesCLAUDE.md's own "preserve XNA-style APIs... using modern C++23 internals" mandate more directly than routing through a third abstraction layer). - Broad hardware reach: even old/integrated Windows GPUs have mature, stable D3D11 drivers.
D3D11 is recommended first, D3D12 explicitly later, for concrete reasons — not just "easier":
- D3D11 keeps an XNA-shaped immediate-context/device model (
ID3D11Device/ID3D11DeviceContext) close to howIGraphicsBackendalready works for every existing backend — no command lists/queues, no descriptor heaps, no manual barriers, no explicit frame-in-flight synchronization, no device-lost recovery machinery to design from scratch. D3D12 needs all of the above (command queues, descriptor heaps, resource barriers, pipeline state objects, multi-frame-in-flight synchronization, device-removed recovery) — real, substantial engineering effort with limited additional value right now, sinceVULKANalready gives this project an explicit, modern, low-level GPU backend, andBGFXcan already select D3D12 internally on Windows if someone specifically wants it. - D3D12 only earns its own native backend if/when the project wants: maximum Windows performance, DXR ray tracing, fine-grained modern-D3D feature access, a direct Vulkan-vs-D3D12 comparison point, or independence from bgfx for advanced Windows rendering specifically. None of those are active project goals today — flagged here as the real trigger condition for Phase DX12, not assumed to already apply.
This project has already proven that a Direct3D-adjacent backend can be developed on Linux and
verified on real Windows only at the end — see README.md's own "Tested Compilers" table:
SDL_RENDERER is "✅ verified building + full test suite under Wine" when cross-compiled with the
existing cmake/toolchains/mingw-w64.cmake toolchain file. D3D11/D3D12 extend that exact, already-
working pattern one step further:
Debian 13 (this repo's actual dev machine)
│
├── Linux native builds (existing, unaffected)
│ ├── EASYGL / VULKAN / BGFX / WEBGPU / SDL_RENDERER / HEADLESS / SOFTWARE
│
└── Windows cross-build (cmake/toolchains/mingw-w64.cmake, already exists)
└── D3D11 now / D3D12 only after future authorization
├── compile: MinGW-w64 (x86_64-w64-mingw32-{gcc,g++})
├── local dev-loop test: Wine + DXVK (D3D11→Vulkan) / Wine + vkd3d-proton (D3D12→Vulkan)
└── final verification: Windows CI runner or a real/VM Windows machine
DXVK translates D3D11 calls to Vulkan and runs under Wine, which means the backend's own logic (device/swap-chain setup, resource lifecycle, draw calls, state translation, shader execution, pixel-readback correctness) can be developed and pixel-tested on this Debian machine, in the same CLion/CMake workflow already used for every other backend — without ever touching a Windows machine for the bulk of the work. D3D12's equivalent path (Wine + vkd3d-proton) is less mature and not yet proven in this project — treated as a real risk to validate early in Phase DX12, not assumed to work by analogy with D3D11's DXVK path.
What Wine+DXVK cannot prove, and must be verified on real Windows/Windows CI before either
backend is called done (mirrors this project's existing "Wine proves the logic, not real-hardware
parity" discipline already documented for SDL_RENDERER):
- Real DXGI swap-chain behavior (present modes, tearing flags, fullscreen transitions).
- Device-lost/device-removed handling on a real driver.
- Actual Intel/AMD/NVIDIA driver quirks and the D3D11 debug layer's real validation warnings.
- WARP software-rasterizer fallback behavior.
- MSVC-vs-MinGW ABI/toolchain differences (this project already builds with MSVC 2022, clang-cl,
and MinGW-w64 for
SDL_RENDERER— seeREADME.md; D3D11/D3D12 should eventually match on at least MinGW-w64 + MSVC).
- Two separate compile-time backends, not one dial.
CNA_GRAPHICS_BACKEND=D3D11andCNA_GRAPHICS_BACKEND=D3D12are two distinct values, each producing its own static library target (cna_backend_graphics_d3d11/cna_backend_graphics_d3d12), exactly matching the existingSDL_RENDERER/EASYGL/BGFX/VULKAN/WEBGPU/HEADLESS/SOFTWAREpattern inCMakeLists.txt(CNA_GRAPHICS_BACKENDcache variable + oneelseif()block per backend). This is not aHEADLESS-style single-binary runtime mode dial — D3D11 and D3D12 are different APIs with different device/resource/command models; there is no sane single binary that "is" both. - Windows-only, hard-gated at configure time. Unlike
BGFX's existing platform check (amessage(WARNING ...)for "primarily tested on Linux" — bgfx can still attempt other platforms),D3D11/D3D12genuinely cannot build anywhere but Windows (native or MinGW/MSVC cross-compile) —d3d11.h/d3d12.h/dxgi.hdo not exist elsewhere. CMake must reject the combination withmessage(FATAL_ERROR ...)whenCNA_GRAPHICS_BACKENDisD3D11/D3D12andCMAKE_SYSTEM_NAMEis notWindows, with an error message that tells the user to either target Windows directly or cross-compile viacmake/toolchains/mingw-w64.cmake(which already exists and is already proven forSDL_RENDERER). - No external package dependency beyond the OS/toolchain-provided Direct3D libraries — and only
the libraries actually needed at link time, not a preemptive blanket set. Unlike
VULKAN(find_package(Vulkan REQUIRED)) orBGFX(FetchContentofbgfx.cmake), D3D11/D3D12 need no extra fetched dependency — every import library either backend could conceivably need (d3d11,dxgi,dxguid,d3dcompiler, plusd3d12for D3D12) is provided by both MSVC's Windows SDK and MinGW-w64's own packaged headers/libs;target_link_libraries()names them directly, nofind_package/FetchContentstep needed. But do not link all of them into the main runtime backend target by default —DX-1should empirically confirm the actual minimum linkable set beforeDX-12writes it into CMake, not assume the full list:dxguidmainly provides out-of-line storage for certain GUID symbols referenced as external objects in older-style code; with modern headers and__uuidof(...)it is frequently unnecessary. Do not link it intocna_backend_graphics_d3d11preemptively —DX-1determines, on this project's actual toolchain, whether anything it uses genuinely needs it.d3dcompileris not a runtime dependency of the main backend at all, matching this plan's own "offline-compiled bytecode, not runtimeD3DCompile()" stance (design decision 5) — linking it intocna_backend_graphics_d3d11by default would silently reintroduce the exactd3dcompiler_47.dllruntime dependency design decision 5 explicitly avoids. It belongs only on whatever target actually callsD3DCompile()— the offline shader-compile tool (DX-14-compile) and/or the later runtime custom-ShaderEffectpath (DX-58) — never oncna_backend_graphics_d3d11/cna_backend_graphics_d3d12themselves.- Likely real minimum for the main backend target:
d3d11,dxgi(D3D11);d3d12,dxgi(D3D12) — confirm this empirically inDX-1/DX-100rather than treating it as decided here. (MinGW-w64's D3D11 header completeness should still be spot-checked early — seeDX-1— since some newer/rarely-used interfaces are occasionally thinner than the real Windows SDK's.)
- A genuinely shared
D3DCommoncore, scoped to what is actually common — not a shared device or backend base class. D3D11 (ID3D11Device/immediateID3D11DeviceContext) and D3D12 (command queues/lists, descriptor heaps, explicit barriers, pipeline state objects, fences) have fundamentally different resource and command-submission models, so there is no sharedD3DGraphicsDevicebase class and no attempt to unify their draw-call paths. What genuinely is shared, and lives ininclude/CNA/Internal/Backends/D3DCommon/+src/CNA/Internal/Backends/D3DCommon/(a small static library,cna_backend_graphics_d3dcommon, linked by bothcna_backend_graphics_d3d11andcna_backend_graphics_d3d12— one layer more specific than the existing backend-agnosticcna_backend_graphics_commonINTERFACE library):DXGI_FORMATmapping tables for XNASurfaceFormat/DepthFormat(DX-11).- XNA state-enum →
D3D*_BLEND/D3D*_COMPARISON_FUNC/D3D*_CULL_MODE/D3D*_FILL_MODE/D3D*_TEXTURE_ADDRESS_MODE/D3D*_FILTERmapping tables (DX-12). Historically the D3D11 and D3D12 enum values for most of these are numerically identical (D3D12 reused D3D11's constants for many state enums) — verify this directly against the actual SDK headers before relying on it (DX-12's own acceptance criterion), do not assume by reputation. - HLSL shader sources (one
.hlslfile per stock-effect variant) plus the offline-compile tooling that turns them into embeddable bytecode (DX-13–DX-15) — this directly mirrors the project's own already-proven Vulkan precedent (src/CNA/Internal/Backends/Vulkan/shaders/ compile_shaders.py→spirv_shaders.hpp, GLSL→SPIR-V, checked-in generated header, no runtime shader compiler dependency). D3D12 genuinely can consume Shader Model 5 DXBC bytecode compiled from the same HLSL source and the same offline compiler (fxc/D3DCompile) as D3D11 — so one shared HLSL source tree and one shared compile step can bootstrap both backends, stated carefully: this is exactly right and sufficient for D3D11, but for D3D12 it is a compatible starting point, not a promise of the final shader system — DXIL/dxc, newer shader models, a modern root-signature-driven binding workflow, and (eventually) ray-tracing shaders are all legitimate future D3D12-specific upgrades this plan should not be read as ruling out. See Phase DX12's own notes (DX-107/DX-111). - The stride-keyed vertex-format-inference convention this project already uses on
WebGPU/Software (
DX-16) — reused, not reinvented.
- HLSL shader strategy: offline-compiled bytecode, not runtime
D3DCompile. Mirrors the ChatGPT-conversation research this plan is based on (see the project owner's own notes) and this project's existing Vulkan precedent exactly:.hlslsources compiled to.cso-equivalent DXBC bytecode ahead of time (CI or a manualcompile_shaders_hlsl.pystep, run on a real Windows machine or viafxc/d3dcompiler_47.dllunder Wine if that proves reliable —DX-14decides which), embedded as a checked-in generated C++ header (src/CNA/Internal/Backends/D3DCommon/shaders/hlsl_shaders.hpp, mirroringspirv_shaders.hppbyte-for-byte in spirit). RuntimeD3DCompile()is explicitly not the v1 path — it adds ad3dcompiler_47.dllruntime dependency and, per the project owner's own research notes, has known extra friction under Wine/DXVK specifically. A runtime-compile path for hand-authored customShaderEffectHLSL sources (mirroringIEffectBackend::CompileProgram()'s contract) is a separate, later, explicitly optional task (DX-58) — do not conflate it with the stock-effect shader set. - Full effect parity is the actual target for D3D11, not a reduced v1 subset. Unlike
SOFTWARE's deliberately narrow first version (no lighting/fog in v1, seeplan_software.mddesign decision 6), D3D11 is meant to be a faithful, full-fidelity Windows reference backend — itsGpuDrawParamsconsumption should aim at the same feature depth EasyGL/Vulkan/Bgfx already have (per-light diffuse+specular lighting, fog,AlphaTestEffect/DualTextureEffect/EnvironmentMapEffect/SkinnedEffect, not justBasicEffect's unlit subset). The 10 existing Vulkan GLSL shader pairs (colored3d,textured3d,colored_textured3d,lit_textured3d,alpha_test3d,dual_texture3d,env_map3d,skinned3d,sprite2d,instanced3d— seesrc/CNA/Internal/Backends/Vulkan/shaders/) are the direct 1:1 HLSL port target list, not a reference to reinvent from scratch. It is still fine to land these incrementally (Phase DX8 is ordered cheapest/most-foundational first), just not to declare the backend "done" at aBasicEffect-only subset the way Software's v1 legitimately did. - Native window handle via SDL3's Win32 property, not a new windowing abstraction. SDL3
already exposes the real
HWNDthroughSDL_GetPointerProperty(SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL)(confirmed present in the vendoredthird_party/SDL/include/SDL3/SDL_video.h) — this is the DXGI swap chain'sCreateSwapChainForHwndtarget. No new SDL subsystem or windowing code is needed; this is a same-shape lookup to whatVulkanGraphicsBackendalready does forSDL_Vulkan_CreateSurface. D3D11/D3D12follow the existing#ifdef CNA_BACKEND_HEADLESS-style per-backend guard convention whereverGraphicsDevice.cppbranches by backend — e.g. the existing#ifdef CNA_BACKEND_EASYGL/CNA_BACKEND_VULKANsites. Since these two backends do need a real window (unlikeHEADLESS/SOFTWARE), most such sites need no new branch at all — call this out explicitly per site rather than assuming.- D3D12 tasks in this plan (Phase DX12) are written in full but explicitly NOT authorized —
including its CMake scaffolding, not just its implementation. Same convention as
plan_software.md's Phase S9: a concrete, scoped-out task list the project owner can approve later, one task or one batch at a time — not a vague "someday" note. Do not start Phase DX12 without an explicit go-ahead, even after Phase DX1–DX11 (D3D11) is fully done. This is deliberately stricter than it might need to be: Phase DX2 does not pre-add"D3D12"toCNA_GRAPHICS_BACKEND'sSTRINGS/option flags "since it's cheap" —-DCNA_GRAPHICS_BACKEND=D3D12should not even be a recognized value untilDX-101(Phase DX12's own first task) explicitly adds it, so there is no inert-but-present D3D12 scaffolding sitting in the tree ahead of authorization. - COM object lifetime: one decided convention before Phase DX2 lands, not per-file
improvisation.
Microsoft::WRL::ComPtr<T>is the obvious default on real Windows/MSVC, but its availability and ergonomics under MinGW-w64 — this project's actual primary Debian dev-loop compiler (Phase DX1) — are not something to assume without checking (DX-6). Options:Microsoft::WRL::ComPtr(Windows-only, MinGW support varies by version/headers actually present);wil::com_ptr(an extra vendored dependency); a small project-localCNA::ComPtr<T>(mirrors this project's own "add a minimal stub rather than a new dependency" philosophy, seeCLAUDE.md's SharpRuntime-extension rule); or rawIUnknown::Release()wrapped in whatever RAII pattern this codebase already uses elsewhere. Resolved byDX-6(2026-07-13):Microsoft::WRL::ComPtr<T>—wrl/client.his present in this MinGW-w64 install and a real spike (device creation,.As(),GetParent, and specifically theReleaseAndGetAddressOf()re-population pattern below) compiled, linked against justd3d11/dxgi, and ran correctly end-to-end under Wine on this machine — no project-local type needed. Whichever is chosen, everyID3D11*/IDXGI*/ID3D12*interface pointer anywhere inD3D11GraphicsBackend/D3D12GraphicsBackend/D3DCommonuses it — no bareRelease()call sites. Without this, leaks on resize, double-releases, and forgotten releases on a partially-failed initialization are the expected failure mode for a hand-rolled D3D backend, not a hypothetical risk. SeeDX-6's own acceptance checklist for the exact method surface required if the project-local option is chosen (move semantics,GetAddressOf()vs.ReleaseAndGetAddressOf(),Attach/Detach, aQueryInterfacehelper) — each maps to a specific, real bug class in hand-rolled COM wrappers, not an optional nice-to-have. - Three independent resource-lifetime groups, not two — the swap chain object itself is its own
group, separate from both the device and the window-size-dependent views. A plain resize must
touch only the narrowest group that actually needs it:
- Device lifetime (
DX-20/DX-21/DX-22):ID3D11Device,ID3D11DeviceContext, theIDXGIFactory2chain, andallowTearingSupported_(the OS/driver capability, design decision 13) — created once, torn down only on device-removed recovery (DX-27). - Swap-chain lifetime (
DX-23): theIDXGISwapChain1object itself — created once at startup (and recreated on device-removed recovery), but a plain resize reuses the same object viaIDXGISwapChain1::ResizeBuffers(...); it does not callCreateSwapChainForHwndagain. - Window-size lifetime (
DX-24): back-bufferID3D11RenderTargetView, depth-stencil texture/ID3D11DepthStencilView, viewport — unbound and released beforeResizeBuffers, recreated after it, on every resize (DX-29) and on device-removed recovery. Device-removed recovery (DX-27/DX-90) tears down and recreates all three groups; a plain resize (DX-29) touches only the third group, plus aResizeBufferscall on the second — it must never re-run the first group's factory/tearing-capability query, and it must never destroy and recreate theIDXGISwapChain1object itself. A flat, undivided "create everything" function (or a two-group split that conflates the swap-chain object with its window-size-dependent views, an earlier draft of this plan's own mistake) is exactly what makes resize and device-loss recovery fragile in a hand-rolled D3D backend.
- Device lifetime (
- D3D11 device creation degrades gracefully on debug-layer/feature-level negotiation — but the
accepted minimum feature level is a hard, explicit policy, not silently whatever came back. A
build that only ever ran under Wine+DXVK (which does not require the real D3D11 SDK debug layer
to be installed) must not implicitly assume every real Windows machine has it —
DX-21retries device creation withoutD3D11_CREATE_DEVICE_DEBUGonDXGI_ERROR_SDK_COMPONENT_MISSINGand logs"D3D11 debug layer unavailable; retrying without it."rather than failing outright. LikewiseDX-20requests feature levels as a fallback list (11_1→11_0→10_1→10_0, with a retry-without-11_1 path for drivers that reject an explicit 11.1 request withE_INVALIDARG) rather than a single hardcodedD3D_FEATURE_LEVEL_11_0request. But acceptance is not the same as negotiation: Phase DX8's stock shader set is compiled as Shader Model 5 (vs_5_0/ps_5_0, design decision 5), which requires feature level 11.0+ — generatingvs_4_0/ps_4_0fallback variants to support a 10.x device is explicitly out of scope, not worth the added shader-variant complexity for this project. So:DX-20negotiates broadly, but construction must reject (a clear, specific diagnostic error — e.g. "GPU reports feature level 10.1; CNA's D3D11 backend requires 11.0+") anything the negotiation returns belowD3D_FEATURE_LEVEL_11_0, at construction time, not as a confusing, deferredCreateVertexShaderfailure once Phase DX8 first tries to use the device. - Tearing is two separate booleans — an OS/driver capability and a CNA policy — never
conflated into one flag.
allowTearingSupported_(fromDX-22'sIDXGIFactory5:: CheckFeatureSupportquery) records only what the system can do; a second, independentallowTearingRequested_records whether CNA actually wants tearing-capable presentation for the currentPresentationParameters/GraphicsDeviceManagerconfiguration. Both the swap-chain creation flags (DX-23) and the per-Present()flag computation (DX-26) gate on the conjunction of both — a swap chain should not unconditionally become tearing-capable just because the hardware supports it, if CNA/the game never asked for that mode. - Constant-buffer layout is a single, explicit, project-wide policy — not decided per shader.
Every GPU-side constant-buffer POD struct (
DX-60/DX-60a) commits to one matrix-layout convention (eitherrow_majordeclared consistently in every HLSLcbufferplus a matching C++-side layout, or CPU-side transposition before upload — pick one, document it once inD3DCommon, and never mix the two across shader variants) and must satisfysizeof(StructName) % 16 == 0(static_asserted) — D3D11 requires a constant buffer'sByteWidthto be a multiple of 16 bytes, so this is a hard correctness requirement, not a style preference.
- Phase DX1 (Windows cross-build dev loop) unblocks everything else — nothing in this plan can
be built, let alone tested, without a working MinGW-w64 + Wine + DXVK loop on this Debian
machine.
cmake/toolchains/mingw-w64.cmakealready exists and is already proven forSDL_RENDERER; this phase is mostly about confirming it also covers D3D11's extra headers/libs and standing up the Wine/DXVK side, which is new. - Phase DX2 (CMake integration + skeleton) depends only on Phase DX1's toolchain being
confirmed working — it wires
D3D11only intoCNA_GRAPHICS_BACKENDexactly like every other backend (noD3D12scaffolding this early, see that phase's own intro and design decision 9), with a real (if minimal)D3D11GraphicsBackendthat at least compiles and links againstCnaTests. - Phase DX3 (
D3DCommonshared core) should land early, before the D3D11-specific phases that consume it (format/state mapping tables, shader compile pipeline, vertex-format helper) — but individual pieces can be built just-in-time per consuming task rather than all up front, mirroring howplan_software.mdlet Phase S2/S3 interleave. - Phase DX4 (device, swap chain, back buffer) is the heart of the backend — get a real
Clear()/Present()/GetBackBufferData()round trip pixel-verified (mirrorsSOFTWARE-10throughSOFTWARE-14's own "prove the framebuffer is real" bar) before anything else. - Phases DX5–DX9 (buffers, textures/render targets, state objects, shaders/effects, SpriteBatch) build on Phase DX4's device/swap-chain foundation and can interleave somewhat, but Phase DX8 (shaders/effects) needs Phase DX5/DX6/DX7 far enough along to actually issue a real draw call.
- Phase DX10 (tests) — per this project's own convention (
CLAUDE.md), add test coverage in the same task that implements each capability, not bolted on afterward. This phase names the cross-cutting suites, not "when to start testing." - Phase DX11 (docs) — write
docs/d3d11-backend.mdas capabilities land, not all at the end, mirroringplan_software.md's own Phase S8 discipline. - Phase DX12 (D3D12) — do not start without explicit authorization (design decision 9).
For every task: build the affected target(s) (Windows cross-build, since this backend cannot build on Linux natively — see design decision 2), run the relevant tests under Wine+DXVK, and do not mark a task ✅ without both. Tasks whose acceptance criteria genuinely require real Windows/Windows CI (flagged individually) cannot be marked ✅ from Wine-only verification — mark 🟨 and say so plainly.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-1 | Confirm cmake/toolchains/mingw-w64.cmake (already used for SDL_RENDERER, see README.md) resolves <d3d11.h>/<dxgi.h> (and <d3dcompiler.h> separately, for the shader-tooling target only — design decision 3) from the mingw-w64 apt package on this machine, and empirically determine the actual minimum link-library set — do not assume dxguid is needed just because it's available |
✅ | Closed 2026-07-13. Confirmed via a real throwaway spike (D3D11CreateDevice with the DX-20 feature-level fallback array, IDXGIDevice→IDXGIAdapter→IDXGIFactory2→IDXGIFactory5 chain, IDXGIFactory5::CheckFeatureSupport tearing query, DXGI_SWAP_CHAIN_DESC1 construction — i.e. real production code shapes, not a trivial include-only check). This machine already has mingw-w64/g++-mingw-w64-x86-64 installed (per README.md's existing SDL_RENDERER precedent) with d3d11.h/dxgi.h/dxgi1_5.h/d3dcompiler.h all present under /usr/x86_64-w64-mingw32/include, and libd3d11.a/libdxgi.a/libdxguid.a/libd3dcompiler.a all present under /usr/x86_64-w64-mingw32/lib. Confirmed minimum link set: d3d11 + dxgi only — the spike (using __uuidof, QueryInterface, GetParent, IID_PPV_ARGS) compiled and linked cleanly (x86_64-w64-mingw32-g++ -std=c++23 spike.cpp -o spike.exe -ld3d11 -ldxgi) with no dxguid needed at all — confirms this plan's own suspicion (design decision 3) that modern MinGW-w64 headers resolve these GUIDs without the separate library. d3dcompiler was verified to link and work in complete isolation (a separate D3DCompile()-calling spike linked only -ld3dcompiler), confirming it's safe to keep off the main backend target per design decision 3. The resulting .exe was then run under Wine (this machine's real desktop :0 session, no DXVK installed yet — vanilla WineD3D) and every call in the spike genuinely succeeded at runtime (D3D11CreateDevice hr=0, the full DXGI factory chain, and CheckFeatureSupport reporting allowTearing=1) — a stronger result than DX-1 strictly required, and a preview of DX-4's own check once DXVK is installed. |
| DX-2 | Install and configure a dedicated Wine prefix + DXVK for D3D11 testing (WINEPREFIX=~/.wine-cna-d3d11, DXVK's d3d11.dll/dxgi.dll installed into it), following the project owner's own researched steps |
✅ | Closed 2026-07-13 — the project owner installed dxvk-wine64 (sudo apt-get install -y dxvk-wine64, which pulled in the dxvk meta-package and dxvk-wine32:i386 too). The dxvk meta-package ships dxvk-setup(1), a Debian-specific convenience tool this plan didn't originally know about — used instead of manually symlinking DLLs: WINEPREFIX=~/.wine-cna-d3d11 wineboot --init then WINEPREFIX=~/.wine-cna-d3d11 dxvk-setup install. Verified for real: system32/d3d11.dll/dxgi.dll are now symlinks straight to /usr/lib/dxvk/wine64/{d3d11,dxgi}.dll.so, and the registry's HKCU\Software\Wine\DllOverrides shows d3d11/dxgi = native (Debian's DXVK integration loads the .so directly as a "native" module rather than the typical Windows-release DXVK zip's override-to-builtin approach — a real, worth-documenting packaging difference). Full install commands recorded in programs.md §10, not just here. |
| DX-3 | scripts/run-wine-dxvk.sh wrapper (mirrors the existing scripts/run-all-backend-smoke-tests.sh convention): sets WINEPREFIX, optional DXVK_HUD/DXVK_LOG_LEVEL, execs wine "$1" (not wine64 — see Notes) |
✅ | Closed 2026-07-13. scripts/run-wine-dxvk.sh written and verified: CNA_D3D11_WINEPREFIX (defaults to ~/.wine-cna-d3d11), fails fast with a clear message if the prefix isn't initialized yet, honors caller-set DXVK_LOG_PATH/DXVK_LOG_LEVEL/DXVK_HUD, exec wine "$@". Confirmed real environment difference: there is no separate wine64 command on this Debian's Wine 10.0 packaging — only wine, which auto-detects PE32 vs. PE32+ (wine64: command not found, but wine spike.exe ran the PE32+ binary correctly) — the script and this plan's own wording were both corrected to wine. |
| DX-4 | Prove the loop end-to-end with a minimal non-CNA smoke program: create a bare ID3D11Device+swap chain, clear to a known color, run under wine with DXVK installed, confirm no crash/error in DXVK_LOG_LEVEL=info output and confirm DXVK is actually the thing that ran (see Notes) |
✅ | Closed 2026-07-13, all 3 verification methods from this task's own Notes passed. Ran DX-1's own spike binary through scripts/run-wine-dxvk.sh (DX-3) against the now-configured ~/.wine-cna-d3d11 prefix: (1) DXVK log files genuinely created (spike_d3d11.log, spike_dxgi.log in DXVK_LOG_PATH); (2) log content unambiguously identifies real DXVK — "DXVK: 2.6.0", "Build: x86_64 gcc 14.0.0", and a real GPU, not a software fallback: "AMD Radeon 780M (RADV PHOENIX)" via the radv 25.0.7 Vulkan driver (llvmpipe was explicitly skipped: "warn: Skipping CPU adapter: llvmpipe"); (3) feature-level negotiation genuinely worked end-to-end — "Using feature level D3D_FEATURE_LEVEL_11_1" (max supported 12_1), comfortably clearing this plan's own DX-20/design-decision-12 minimum of 11_0. Every application-level call in the spike (D3D11CreateDevice, IDXGIDevice→IDXGIFactory5 chain, CheckFeatureSupport tearing query, which reported allowTearing=1) returned hr=0. This is a strictly stronger result than DX-1's own earlier vanilla-Wine run of the same binary (which used WineD3D, not DXVK) — direct proof this environment's Wine+DXVK loop is real and load-bearing, not just "ran without crashing." |
| DX-5 | Document the CLion CMake-profile setup the project owner described (separate Windows-D3D11-MinGW profile, custom "run" step invoking scripts/run-wine-dxvk.sh) |
✅ | Closed 2026-07-13 (documentation-only, no new CMake logic — Phase DX2 itself is still unauthorized, so there's no D3D11 target to actually build yet; this records the profile shape for whoever sets it up once Phase DX2 lands). CLion CMake profile (Settings → Build, Execution, Deployment → CMake → +): name it Windows-D3D11-MinGW; leave the Toolchain as the default Linux one (the cross-compilation happens via the CMake toolchain file, not CLion's own toolchain selector); CMake options: -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake -DCNA_GRAPHICS_BACKEND=D3D11 -DCNA_BUILD_TESTS=ON; build directory e.g. cmake-build-d3d11-mingw. Run configuration: CLion cannot directly execute a cross-compiled Windows .exe, so add a separate Shell Script run configuration (not a plain CMake Application one) pointing at scripts/run-wine-dxvk.sh (DX-3) with the built target's .exe path as its argument (e.g. $CMakeCurrentBuildDir$/<target>.exe) — this is exactly the wrapper DX-3 built and DX-4 proved works. |
| DX-6 | Decide the COM object lifetime convention (design decision 10) before any Phase DX2 code lands: spike whether Microsoft::WRL::ComPtr<T> builds cleanly against this project's actual MinGW-w64 toolchain (cmake/toolchains/mingw-w64.cmake); if not (or if it's awkward), implement a small project-local CNA::ComPtr<T> in D3DCommon instead |
✅ | Closed 2026-07-13 — Microsoft::WRL::ComPtr<T> chosen; no project-local CNA::ComPtr<T> needed. wrl/client.h is present in this MinGW-w64 install (/usr/x86_64-w64-mingw32/include/wrl/client.h). A real spike (ComPtr<ID3D11Device>/ComPtr<ID3D11DeviceContext> via D3D11CreateDevice(..., device.GetAddressOf(), ..., context.GetAddressOf()), device.As(&dxgiDevice), dxgiDevice->GetParent(IID_PPV_ARGS(&adapter)), and — specifically exercising this task's own flagged leak pattern — re-creating the device into an already-populated ComPtr via device.ReleaseAndGetAddressOf()) compiled and linked cleanly against just d3d11/dxgi (no dxguid, consistent with DX-1's finding), and ran correctly end-to-end under Wine on this machine: D3D11CreateDevice hr=0, .As(IDXGIDevice) hr=0, GetParent(adapter) hr=0, and the ReleaseAndGetAddressOf()-based re-creation also hr=0 — confirming this exact leak-prone pattern behaves correctly through WRL::ComPtr. One real, generic (non-WRL-specific) build wrinkle found along the way: a plain dynamically-linked MinGW build needs libgcc_s_seh-1.dll/libstdc++-6.dll present at runtime (STATUS_DLL_NOT_FOUND under Wine otherwise) — already solved by this project's own existing CMakeLists.txt (target_link_options(CnaTests PRIVATE -static-libgcc -static-libstdc++) + cna_copy_mingw_runtime(CnaTests)), so no new build-system work is implied, just confirms the existing mechanism is the right one to keep relying on. DX-6a's custom-ComPtr unit tests are therefore not needed — WRL::ComPtr is Microsoft's own, already-tested type. |
| DX-6a | CNA::ComPtr<T> is built (DX-6), add dedicated unit tests... |
✅ | Closed as not-applicable, 2026-07-13 — DX-6 chose Microsoft::WRL::ComPtr<T>, which needs no project-local test suite (already tested upstream, exactly the skip condition this row's own original Notes anticipated). |
This phase is D3D11-only — no D3D12 CMake wiring, option flag, or STRINGS entry lands here,
not even as inert scaffolding. Adding D3D12 to CNA_GRAPHICS_BACKEND early would let a curious
-DCNA_GRAPHICS_BACKEND=D3D12 configure "successfully" into a target with no real implementation
behind it, muddying design decision 9's "nothing D3D12 happens without explicit authorization" line.
All of D3D12's CMake plumbing — the STRINGS/option-flag addition, the FATAL_ERROR guard
extension, the cna_backend_graphics_d3d12 target, and its factory dispatch — is Phase DX12's own
first task (DX-101), together with the rest of that phase.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-10 | Add "D3D11" (only) to CNA_GRAPHICS_BACKEND's CMake STRINGS property and a matching CNA_BACKEND_D3D11 option flag, following the exact existing pattern (CMakeLists.txt lines ~94–139) |
✅ | Closed 2026-07-13. D3D12 deliberately not added (design decision 9) — CNA_GRAPHICS_BACKEND=D3D12 is still not a recognized value. |
| DX-11 | FATAL_ERROR guard: reject CNA_GRAPHICS_BACKEND = D3D11 when CMAKE_SYSTEM_NAME is not Windows, with a message pointing at cmake/toolchains/mingw-w64.cmake (design decision 2) |
✅ | Closed 2026-07-13. Not yet independently re-verified by actually attempting a non-Windows configure (would need to temporarily fake CMAKE_SYSTEM_NAME, low value) — the guard's condition is simple and directly mirrors the existing BGFX check's own proven pattern. |
| DX-12 | cna_backend_graphics_d3dcommon static library target + cna_backend_graphics_d3d11 target (elseif(CNA_GRAPHICS_BACKEND STREQUAL "D3D11") block, mirroring every existing backend's own block) |
✅ | Closed 2026-07-13. cna_backend_graphics_d3d11 links exactly d3d11 dxgi — DX-1's confirmed minimum set — plus SDL3::SDL3 (needed for SDL_GetWindowSizeInPixels/SDL_GetPointerProperty/SDL_Log, not originally itemized in this row but obviously required, matching every other windowed backend's own link line). No dxguid, no d3dcompiler, no cna_backend_graphics_d3dcommon yet (design decision 4's shared core has no consumer until Phase DX3 — not created prematurely). Real build proof: cmake --build cmake-build-d3d11 --target cna_backend_graphics_d3d11 succeeds cleanly. |
| DX-13 | include/CNA/Internal/Backends/D3D11/D3D11GraphicsBackend.hpp + src/CNA/Internal/Backends/D3D11/D3D11GraphicsBackend.cpp: a class implementing every IGraphicsBackend pure virtual — real where Phase DX2 can make it real (construction/teardown), honest stubs elsewhere until later phases replace them (mirrors HEADLESS-3/SOFTWARE-3's own bar: CnaTests must link cleanly against it even before most methods are real) |
✅ | Closed 2026-07-13, and substantially more real than the bar this row asked for — Phase DX4's device/swap-chain/back-buffer/clear/present/readback work landed in the same pass (see DX-20–DX-29 below) rather than being deferred as stubs, since the two phases turned out cheap to do together once D3D11CreateDevice was reachable. Every ID3D11*/IDXGI* member uses Microsoft::WRL::ComPtr<T> (DX-6's resolved choice) from the first line. Stubs that remain honest "not yet implemented" throws, each naming its own future phase: CreateTexture (DX6), CreateSpriteBatch (DX9), CreateVertexBuffer/CreateIndexBuffer16 (DX5), DrawColoredPrimitives/DrawIndexedColoredPrimitives (DX8). SetDepthTestEnabled/SetBlendEnabled/SetDepthWriteEnabled are real no-ops (not throws) since GraphicsDevice applies default state unconditionally on construction — matches this row's own "must not crash normal construction" bar; real state-object application is Phase DX7's job. |
| DX-14 | CreateGraphicsBackend() factory dispatch for D3D11 |
✅ | Closed 2026-07-13. One-line return std::make_unique<D3D11::D3D11GraphicsBackend>(args); — no D3D12 branch (DX-10's own scoping). |
| DX-15 | First real build: Windows cross-build via cmake/toolchains/mingw-w64.cmake -DCNA_GRAPHICS_BACKEND=D3D11, confirm CNA and CnaTests targets both link |
✅ | Fully closed 2026-07-14 — CnaTests now builds, links, and runs. CNA was already closed 2026-07-13 (see prior note, preserved below). The remaining CnaTests blocker — roughly 10 test files calling POSIX-only ::setenv() directly — is now fixed: all call sites (Microsoft/Xna/Framework/Audio/{AudioCategory,AudioEngine,Cue,DynamicSoundEffectInstance,Microphone,SoundBank,SoundEffectInstance,SoundEffect,WaveBank}Tests.cpp, FrameworkDispatcherTests.cpp, Graphics/Texture2DTests.cpp) now call System::Environment::SetEnvironmentVariable(name, value) (sharp-runtime, already cross-platform — Windows SetEnvironmentVariableA/POSIX ::setenv internally) instead of ::setenv directly, #includeing System/Environment.hpp where missing. Real build proof: cmake --build cmake-build-d3d11 --target CnaTests → exit 0, Built target CnaTests, CnaTests.exe (65 MB) produced. Real execution proof, not just linking: WINEPREFIX=~/.wine-cna-d3d11 wine ./CnaTests.exe --gtest_list_tests enumerates the full suite; a real filtered run (--gtest_filter="CnaInput*:BoundaryAndOutOfRange/AlphaTestReferenceScalingTest.*") genuinely executes through a live D3D11 device ([D3D11] Backend initialised (800x480), feature level 0xb100, debug layer enabled, tearing supported) and reports [==========] 30 tests from 8 test suites ran. / [ PASSED ] 30 tests. — a real Wine+DXVK run, not assumed from the build alone. Full-suite execution (thousands of tests, long wall-clock under Wine) not attempted here — out of this row's own scope, which only asks that both targets build and link; the filtered run above already proves the binary is genuinely executable end-to-end, not merely linkable. Original 2026-07-13 CNA-closure note, preserved: cmake --build cmake-build-d3d11 --target CNA succeeds cleanly (full XNA API surface, not just the backend). A small standalone executable (examples/d3d11_smoke_test.cpp, see DX-80) also builds, links, and runs correctly end-to-end through Wine+DXVK. Getting there surfaced 4 real, pre-existing gaps, all unrelated to D3D11 itself (nobody had actually linked this cross-target recently): (1) sibling sharp-runtime's Console.cpp/HttpClientHandler.cpp unconditionally #define NOMINMAX, colliding with MinGW-w64's own <bits/os_defines.h> under -Werror — fixed (guarded with #ifndef, matching Environment.cpp's own already-correct sibling pattern in the same repo); (2) sharp-runtime's Process.cpp has two -Werror=unused-parameter hits in its Windows-unsupported stub bodies — fixed ((void)param;); (3) cna_graphics' own ContentManager.cpp guards Video/FFmpeg support with !defined(__EMSCRIPTEN__) && !defined(__ANDROID__), missing !defined(__MINGW32__) even though CMakeLists.txt's own CNA_FFMPEG_AVAILABLE computation already excludes MINGW — fixed (guard extended); (4) cna_net_two_process_harness (a POSIX-only helper executable, <sys/resource.h>) was built unconditionally under CNA_ENABLE_NET AND CNA_BUILD_TESTS even though its own consumer test is already excluded on WIN32 — fixed (matching exclusion added to the executable's own guard). |
Everything in this phase lives under include/CNA/Internal/Backends/D3DCommon/ +
src/CNA/Internal/Backends/D3DCommon/, linked by both D3D11 and D3D12 (design decision 4). Nothing
here depends on which of the two consumes it first — build each piece just-in-time for whichever
Phase DX4–DX9 task actually needs it, per the "Active execution order" note above.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-11-fmt | D3DFormatMapping.hpp/.cpp: XNA SurfaceFormat/DepthFormat → DXGI_FORMAT (e.g. Color→DXGI_FORMAT_R8G8B8A8_UNORM, Depth24Stencil8→DXGI_FORMAT_D24_UNORM_S8_UINT) |
✅ | Closed 2026-07-13. Full SurfaceFormat enum covered (27 values) plus DepthFormat (4 values, Depth24 correctly falling back to the same DXGI_FORMAT_D24_UNORM_S8_UINT as Depth24Stencil8 since D3D11 has no pure-24-bit-depth format — matches Vulkan's own documented fallback). Cross-checked against Vulkan's own hardcoded format choices where Vulkan actually has one (VK_FORMAT_R8G8B8A8_UNORM for Color, the same D24_UNORM_S8_UINT-family depth fallback) — Vulkan itself doesn't implement a full per-SurfaceFormat table (it hardcodes Color/RGBA8 almost everywhere), so most of this table's non-Color entries are this project's own first real SurfaceFormat→native-format mapping, not a re-derivation of an existing one. 6 dedicated pixel/logic checks in D3D11_Common (examples/d3d11_common_test.cpp), verified genuinely discriminating via a live mutation test (temporarily broke CullModeToD3D11, confirmed the exact expected check failed 22/23, reverted, reconfirmed 23/23). |
| DX-12-state | D3DStateMapping.hpp/.cpp: XNA Blend/BlendFunction/CompareFunction/CullMode/FillMode/TextureAddressMode/TextureFilter → D3D11_*/D3D12_* equivalents |
✅ | Closed 2026-07-13 — the "verify, don't assume" instruction was followed for real. Directly inspected both /usr/x86_64-w64-mingw32/include/d3d11.h and d3d12.h on this machine: D3D11_BLEND/D3D12_BLEND (D3D12 is a strict superset, every D3D11 value matches numerically), D3D11_BLEND_OP/D3D12_BLEND_OP, D3D11_COMPARISON_FUNC/D3D12_COMPARISON_FUNC, D3D11_CULL_MODE/D3D12_CULL_MODE, D3D11_FILL_MODE/D3D12_FILL_MODE, D3D11_TEXTURE_ADDRESS_MODE/D3D12_TEXTURE_ADDRESS_MODE, and D3D11_FILTER/D3D12_FILTER (including its non-trivial bitmask-encoded values) all confirmed numerically identical — the header's own doc comment states this as a verified fact with the exact verification method, not an assumption. CullMode mapping specifically derived (not guessed) from this project's own Vulkan precedent: D3D11's native FrontCounterClockwise=FALSE default already matches D3D's own clockwise-is-front convention (no override needed, unlike Vulkan which must explicitly set frontFace=CLOCKWISE), so CullClockwiseFace→D3D11_CULL_FRONT/CullCounterClockwiseFace→D3D11_CULL_BACK follows the identical logic Vulkan's own "Task 870 empirical finding" already established. 13 dedicated checks in D3D11_Common, including the specific CullMode pair (the one genuinely non-obvious mapping in this whole table) — mutation-tested as described in DX-11-fmt's row. |
| DX-13-hlsl | Port the 10 existing Vulkan GLSL shader pairs to HLSL source (colored3d, textured3d, colored_textured3d, lit_textured3d, alpha_test3d, dual_texture3d, env_map3d, skinned3d, sprite2d, instanced3d) into src/CNA/Internal/Backends/D3DCommon/shaders/*.hlsl, one file per stage per variant |
✅ | Ported 2026-07-13, all 20 files (10 variants × vs/ps) written and hand-reviewed line-by-line against the GLSL source, and now compiler-verified for real — DX-14-compile's own real D3DCompile() run (via a Wine+DXVK-executed compiler tool) compiled all 20 files cleanly on the first pass with zero HLSL errors, upgrading this row from "hand-reviewed, not compiled" to a genuinely closed state; see DX-14-compile's own row for that proof. Only the 10 named variants were ported; colored3d_legacy/alpha_test_colored3d/dual_texture_colored3d (extra GLSL files not in this row's variant list) were deliberately left unported — investigated and found to be Vulkan-only legacy/superseded pipeline aliases, not required by any of the 10 named variants. Matrix convention (design decision 14) chosen: row_major cbuffer matrices (XNA's own row-major CPU-side Matrix uploads byte-for-byte unchanged, same as the existing Vulkan/EasyGL/Bgfx backends already do) + mul(v, M) everywhere a GLSL source line reads M * v — this is the standard XNA/HLSL idiom (matches MonoGame's own DX11 HLSL effect sources) and was applied as one consistent, mechanical translation rule across all 20 files, including the two-matrix skinMat/Mvp chain in skinned3d.vert.hlsl and the 4-column-vector world-matrix reconstruction in instanced3d.vert.hlsl (both independently re-derived by hand to confirm the rule still produces the identical result under matrix chaining / vector-to-matrix construction, not just single M*v lines). cbuffer/register scheme: register(b0) = per-draw constants (mirrors each shader's GLSL push_constant block byte-for-byte, field order preserved), register(b1)/register(b2) = the secondary/tertiary UBOs each variant's GLSL source declares at set=0 binding=1/binding=2 (fog, LitLightParams, BoneBlock, EnvMapParams) — chosen so DX-60/DX-60a's upcoming explicit GPU-packed structs have a stable, already-proven-correct field-grouping target to formalize, not a placeholder invented from scratch. Textures/samplers: t0/s0 for GLSL binding=0, t1/s1 for a second sampler (dual_texture3d's 2nd texture, env_map3d's cube map). Two genuine, documented HLSL-vs-GLSL deviations found and handled, not silently glossed over: (1) HLSL has no built-in inverse() — lit_textured3d.vert.hlsl/env_map3d.vert.hlsl each define a small local InverseTranspose3x3() helper (cofactor-matrix-over-determinant, algebraically simplified so it computes transpose(inverse(m)) directly in one step, verified by hand-derivation, not just transliterated) rather than sharing an include (no HLSL include wiring exists yet, and design decision 5 didn't ask for one); (2) sprite2d.vert.hlsl's direct pixel→NDC formula needs an explicit Y-negation not present in the GLSL source — the 3D shaders' Vulkan-only pos.y = -pos.y flip has no D3D11 equivalent (D3D's NDC already matches XNA's own convention), but sprite2d bypasses the projection matrix entirely with a raw linear formula, and D3D's NDC is Y-up (like OpenGL) vs. Vulkan's native Y-down rasterization — so the D3D11 version needs its own, differently-signed correction to avoid rendering sprites upside down (documented in-file, independently re-derived from D3D/Vulkan NDC semantics, not copied from the Vulkan flip). Open item for whoever lands Phase DX5's instanced input layout: instanced3d.vert.hlsl declares its per-instance world-matrix input as 4 rows using the project-local semantic names INSTANCEWORLD0–INSTANCEWORLD3 (no established convention existed yet in D3DVertexFormatHelper.hpp, which only covers the 5 non-instanced strides) — the eventual D3D11_INPUT_ELEMENT_DESC array for this instanced layout must use these exact semantic name/index pairs to match this shader's input signature. |
| DX-14-compile | compile_shaders_hlsl.py (mirrors compile_shaders.py's role exactly): offline HLSL→DXBC compile step (design decision 5), generating hlsl_shaders.hpp with byte arrays, checked in like spirv_shaders.hpp |
✅ | Closed 2026-07-13 — real, not simulated. Built hlsl_compiler_tool.cpp (a tiny standalone D3DCompile()-calling .exe, src/CNA/Internal/Backends/D3DCommon/shaders/), cross-built with the project's own x86_64-w64-mingw32-g++ (statically linked -static-libgcc -static-libstdc++, links only d3dcompiler — objdump -p confirms the resulting .exe imports only D3DCOMPILER_47.dll/KERNEL32.dll/msvcrt.dll, no mingw runtime DLL dependency). compile_shaders_hlsl.py (this task's namesake, mirroring compile_shaders.py's read→compile→emit-header shape) builds that tool, then runs it once per .hlsl file through scripts/run-wine-dxvk.sh (DX-3's own established Wine+DXVK harness — same one D3D11_Smoke/D3D11_Common use), each producing genuine DXBC bytecode. All 20 shaders (DX-13-hlsl's full set) compiled cleanly on the first real run — zero HLSL bugs found, confirming DX-13-hlsl's hand-port was correct; every output blob's first 4 bytes were independently verified to be the DXBC magic (44 58 42 43), e.g. colored3d.vert.hlsl → 3508 bytes, skinned3d.vert.hlsl → 58352 bytes (the largest, from the 3-branch bone-blend if chain over a Bones[72] cbuffer array — plausible size, not a red flag; compiled with only D3DCOMPILE_ENABLE_STRICTNESS/-O3, no errors or warnings). Output: src/CNA/Internal/Backends/D3DCommon/shaders/hlsl_shaders.hpp (952KB, 20 static constexpr uint8_t[] arrays in CNA::Internal::Backends::D3DCommon::Shaders), checked in like spirv_shaders.hpp. Not wired into any CMake build target — same manual-regen convention compile_shaders.py/spirv_shaders.hpp already use (verified: no CMake reference to compile_shaders.py exists either), so this doesn't add wine/mingw as a normal-build dependency. |
| DX-15-embed | Wire hlsl_shaders.hpp's byte arrays into D3D11's CreateVertexShader/CreatePixelShader calls (Phase DX8 consumes this directly) |
✅ | Closed 2026-07-13 — real device-facing proof, not a scaffold. New D3DCommon module D3DShaderCache.hpp/.cpp (D3DShaderVariant enum + CreateVertexShaderForVariant/CreatePixelShaderForVariant/GetVertexShaderBytecode/GetPixelShaderBytecode) covers all 10 DX-13-hlsl variants, returning Microsoft::WRL::ComPtr<ID3D11VertexShader>/ComPtr<ID3D11PixelShader> (design decision 10) or a null ComPtr on failure — callers check, no throw. Added D3D11GraphicsBackend::GetDeviceEXT() (NOXNA) so tests/D3DCommon callers can reach the real ID3D11Device* without duplicating the backend's own device-creation path. examples/d3d11_smoke_test.cpp grew Check D: for each of the 10 variants, creates both shader objects through the same live device the existing Clear()/Present() checks (A–C) already use, and asserts both are non-null — 13/13 checks pass (up from 3/3), confirmed via a real ctest --test-dir cmake-build-d3d11 -R D3D11 run (D3D11_Smoke 13/13, D3D11_Common 23/23, 36/36 total), not assumed from DX-14-compile's compiler-only proof. GetVertexShaderBytecode/GetPixelShaderBytecode are unused by any caller yet — deliberately exposed now (not deferred) since D3D11_CreateInputLayout needs a vertex shader's raw bytecode/input-signature, not just the shader object, and Phase DX5's input-layout cache (DX-32) will need this exact accessor. Scope boundary honored: no constant buffers, no input-layout binding, no IShaderBackend/draw-call wiring touched — D3D11GraphicsBackend's own DrawColoredPrimitives/etc. still correctly throw "not yet implemented", unchanged; this task only proves+exposes the DXBC→D3D11-shader-object path for Phase DX8 to consume. |
| DX-16-vtx | D3DVertexFormatHelper.hpp: stride-keyed vertex layout inference (16/20/24/32/52-byte strides), mirroring VulkanVertexFormatHelper.hpp's own convention, emitting a D3D11_INPUT_ELEMENT_DESC[]/D3D12_INPUT_ELEMENT_DESC[] array per stride |
✅ | Closed 2026-07-13. All 5 established strides implemented, byte offsets read directly from VertexPositionColor/VertexPositionTexture/VertexPositionColorTexture/VertexPositionNormalTexture/VertexPositionNormalTextureSkinned's own real getVertexDeclarationStatic() C++ source (not re-derived/guessed), using this project's established HLSL semantic-name convention (POSITION/COLOR/TEXCOORD/NORMAL/BLENDWEIGHT/BLENDINDICES). D3D12_INPUT_ELEMENT_DESC variant not yet written — D3D11_INPUT_ELEMENT_DESC's layout is identical in shape (design decision 4's own verified claim covers the enum values used inside it, e.g. DXGI_FORMAT, which are shared as-is; the struct itself would need its own small D3D12 overload when Phase DX12 needs it, not written speculatively now). 4 dedicated checks in D3D11_Common (strides 16/24/52 plus an unrecognized-stride negative case), mutation-verified alongside DX-11-fmt/DX-12-state's own checks. |
Split into three independent resource-lifetime groups per design decision 11 — device
(DX-20–DX-22), swap chain (DX-23), and window-size views (DX-24) — specifically so
a plain resize (DX-29) touches only the narrowest group it actually needs, and device-removed
recovery (DX-27) is the only path that touches all three.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-20 | Device-lifetime resource: ID3D11Device/ID3D11DeviceContext creation via D3D11CreateDevice (not the combined ...AndSwapChain entry point — device creation is deliberately separated from swap-chain creation, design decision 11), requesting feature levels as a fallback array {11_1, 11_0, 10_1, 10_0}; if the call returns E_INVALIDARG retry once with 11_1 dropped from the array (some drivers reject an explicit 11.1 request outright), D3D11_SDK_VERSION |
✅ | Closed 2026-07-13. Real end-to-end proof via examples/d3d11_smoke_test.cpp (D3D11_Smoke CTest) running through DXVK 2.6.0 on a real GPU (AMD Radeon 780M/RADV): negotiated feature level 0xb100 = D3D_FEATURE_LEVEL_11_1 (the first array entry succeeded directly — the E_INVALIDARG/drop-11_1 fallback branch exists in code but was never actually exercised on this machine/driver, an honest gap, not a false claim). |
| DX-21 | Device-lifetime resource: debug layer as best-effort — attempt the DX-20 call with flags | D3D11_CREATE_DEVICE_DEBUG first; if it returns DXGI_ERROR_SDK_COMPONENT_MISSING, retry the exact same call with that flag cleared and log "D3D11 debug layer unavailable; retrying without it." (design decision 12) — the debug layer must never be a hard requirement for the backend to construct |
🟨 | The direct-success path is real and proven (D3D11_Smoke reports debug layer = enabled — DXVK's Wine integration provides whatever D3D11_CREATE_DEVICE_DEBUG needs on this machine, no fallback triggered). The actual DXGI_ERROR_SDK_COMPONENT_MISSING retry-without-debug-layer branch has never fired — this machine's environment happens to always satisfy the debug-layer request, so this path is implemented but genuinely untested. DX-90's real-Windows checklist (a machine without the D3D11 SDK debug layer installed) is the real test for this, exactly as this row's own Notes already anticipated. |
| DX-22 | Device-lifetime resource: obtain the modern DXGI factory chain and query tearing support before the swap chain is created — done once, at device-creation time, never repeated on a plain resize: IDXGIDevice (QueryInterface off the ID3D11Device) → IDXGIAdapter (GetParent) → IDXGIFactory2 (GetParent) → QueryInterface<IDXGIFactory5> → CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, ...); store the result as allowTearingSupported_ — a capability, distinct from the separate allowTearingRequested_ policy flag (design decision 13) |
✅ | Closed 2026-07-13. D3D11_Smoke reports tearing = capable — the full factory chain and IDXGIFactory5::CheckFeatureSupport genuinely succeeded under DXVK/RADV on this machine. |
| DX-23 | Swap-chain-lifetime resource: the IDXGISwapChain1 object itself — modern creation via a DXGI_SWAP_CHAIN_DESC1 (Format = DXGI_FORMAT_R8G8B8A8_UNORM, mapping to XNA SurfaceFormat::Color via DX-11-fmt's own table; BufferCount=2, SwapEffect=DXGI_SWAP_EFFECT_FLIP_DISCARD, SampleDesc.Count=1, Flags = (allowTearingSupported_ && allowTearingRequested_) ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0 — design decision 13) sized from PresentationParameters, then IDXGIFactory2::CreateSwapChainForHwnd targeting the real HWND from SDL_PROP_WINDOW_WIN32_HWND_POINTER (design decision 7). Created once at startup (and on device-removed recovery); a plain resize (DX-29) reuses this exact object via ResizeBuffers, it is never recreated |
✅ | Closed 2026-07-13. CreateSwapChainForHwnd genuinely succeeded (DXVK's own log confirms a real presenter was created: Format: VK_FORMAT_B8G8R8A8_SRGB, Present mode: VK_PRESENT_MODE_FIFO_KHR, Buffer size: 64x64 — DXVK's internally-chosen Vulkan surface format is its own translation of the requested DXGI_FORMAT_R8G8B8A8_UNORM, not something CNA controls or needs to match exactly). Resize itself (DX-29) not yet exercised — see that row. |
| DX-24 | Window-size-lifetime resources: back-buffer ID3D11RenderTargetView + default depth-stencil ID3D11Texture2D/ID3D11DepthStencilView, sized to match the swap chain — recreated on every resize (DX-29) |
✅ | Closed 2026-07-13. Proven indirectly but solidly: Clear()+Present()+ReadBackbuffer() all succeeded, which is only possible if the RTV/DSV/viewport binding actually worked. |
| DX-25 | Real Clear(r,g,b,a) (ClearRenderTargetView) and depth/stencil-inclusive Clear* variants (ClearDepthStencilView) |
✅ | Fully closed — the combo-variant gap this row originally documented was closed by DX-130 (Phase DX14, 2026-07-14). Plain Clear(r,g,b,a) was already real and proven (D3D11_Smoke's own core check). DX-130 added the missing dedicated test for all 5 combo variants: ClearColorDepthAndStencil genuinely writes the given stencil value into the real stencil plane (direct GPU readback); ClearStencil alone genuinely overwrites it to a different value; depth is proven by its real effect on rasterization (the same triangle at the same Z drawn twice, differing only in the depth a prior ClearDepth/ClearColorAndDepth wrote); ClearDepth() alone leaves the color target untouched. See DX-130's own row for full detail — this row's stale "not yet exercised by any test" text predated that closure. |
| DX-26 | Real Present(): sync interval and tearing are backend state, not a direct D3D11 "set swap interval" API — there is no such entry point. SetSwapInterval/SetPresentationMode just update stored fields (vsyncEnabled_; allowTearingRequested_, design decision 13; exclusiveFullscreen_ from whatever presentation-mode plumbing already tracks fullscreen state); Present() itself computes const bool mayTear = allowTearingSupported_ && allowTearingRequested_ && !vsyncEnabled_ && !exclusiveFullscreen_;, UINT syncInterval = vsyncEnabled_ ? 1 : 0, UINT flags = mayTear ? DXGI_PRESENT_ALLOW_TEARING : 0, then calls swapChain->Present(syncInterval, flags) |
✅ | Present() itself closed 2026-07-13 — genuinely called and succeeded (DXVK's own presenter initialization log line only appears once a real Present() call reaches it; D3D11_Smoke exits 0 with no CheckDeviceRemoved log firing). The mayTear/vsyncEnabled_ policy branches themselves were exercised only at their default values (vsyncEnabled_=true from args.swapInterval=1's default) — the tearing-enabled/no-vsync branch specifically hasn't been independently exercised by a dedicated test yet. |
| DX-27 | Device-lost/removed detection, starting here in Phase DX4 — not deferred to DX-90: check the HRESULT returned by Present() (and any other call that can surface it) for DXGI_ERROR_DEVICE_REMOVED/DXGI_ERROR_DEVICE_RESET; on either, call device->GetDeviceRemovedReason() and log/report the reason |
🟨 | Code is real and in place (CheckDeviceRemoved(), called from both Present() and EnsureSwapChainSize()'s ResizeBuffers failure path). Never actually triggered — no real device removal occurred during this session's testing, so the detection logic itself remains unverified against a real DXGI_ERROR_DEVICE_REMOVED. Matches this row's own Notes: DX-90 (real hardware) is the genuine test for this, not Wine+DXVK. |
| DX-28 | ReadBackbuffer()/GetBackBufferData(): real GPU→CPU readback via a staging ID3D11Texture2D (D3D11_USAGE_STAGING + CopyResource + Map) — this backend's first genuine pixel-correctness proof, same bar SOFTWARE-13 set. Must read via D3D11_MAPPED_SUBRESOURCE::RowPitch, one row at a time — never assume the mapped rows are tightly packed (RowPitch can exceed width * bytesPerPixel due to driver-side row alignment) |
✅ | Closed 2026-07-13 — this backend's actual reason to exist, proven for real. D3D11_Smoke's two checks: Clear(20,40,60,255) → GetBackBufferData() over a 4×4 region at origin reads back the exact color for every pixel; a second Clear(200,100,50,255) → readback over a different 4×4 region at (10,10) also matches exactly (proves it's a genuine live read, not a cached/stale first-call value). RowPitch-correctness itself not yet independently isolated by a test that deliberately spans a row-alignment boundary — the current 4-pixel-wide regions are too small to have exercised a case where RowPitch != width*4; a real gap worth a dedicated DX-81 test later, not a false claim now. |
| DX-29 | Window resize handling — touches only the window-size-view group plus a ResizeBuffers call on the existing swap chain (design decision 11), nothing else: (1) unbind the current RTV/DSV (OMSetRenderTargets(0, nullptr, nullptr)); (2) release every reference to the old back buffer — DX-24's RTV, the back-buffer texture reference, depth texture, DSV, and any SRV or staging/readback-cache resource (DX-28) that was ever bound off the old back buffer; (3) context->Flush() to help ensure the immediate context isn't still holding an implicit reference before resizing; (4) call swapChain->ResizeBuffers(...) on DX-23's existing swap-chain object; (5) recreate DX-24's RTV/DSV/viewport sized to the new dimensions |
✅ | Doc-sync fix, 2026-07-14 — this row was stale, the gap it flagged was already closed. DX-83 (below) closed this exact gap with real, GPU-facing proof (Check AB: resize 64×64→96×80 via the real public GraphicsDeviceManager API, DXVK's own presenter log confirms the real swap chain resized, Clear()+readback verified exact at both the origin and the new far edge) but DX-83's own row text never got reflected back into this row — this row was left showing ⬜ "never exercised" indefinitely after the gap it described was actually closed, the same kind of stale cross-reference this session already found and fixed for DX-100/DX-102. Re-verified the implementation directly (not just trusting the old note): EnsureSwapChainSize() (called lazily from Present()) performs exactly the 5 steps this row lists — ReleaseWindowSizeDependentViews() unbinds RTV/DSV and releases the back-buffer RTV/texture/depth-texture/DSV, context_->Flush(), ResizeBuffers() on the existing swap-chain object, then CreateWindowSizeDependentViews() recreates RTV/DSV/viewport at the new size. Point (2)'s "any SRV or staging/readback-cache resource ever bound off the old back buffer" doesn't apply — ReadBackbuffer() creates a fresh, un-cached staging texture per call, nothing persistent to release. See DX-83's own row for the full real-proof detail. Fullscreen toggle remains DX-90's job on real Windows hardware, per DX-83's own scope note. |
| # | Task | Status | Notes |
|---|---|---|---|
| DX-30 | D3D11VertexBufferBackend: ID3D11Buffer with D3D11_BIND_VERTEX_BUFFER, SetData/SetDataWithOptions via Map/Unmap (dynamic) or UpdateSubresource (default usage), matching SetDataOptions::Discard/NoOverwrite semantics |
✅ | Closed 2026-07-13 — real GPU write+readback proof, not assumed. D3D11VertexBufferBackend (include//src/CNA/Internal/Backends/D3D11/D3D11Buffers.{hpp,cpp}) uses a single D3D11_USAGE_DYNAMIC + D3D11_CPU_ACCESS_WRITE ID3D11Buffer, lazily (re)sized on first/growing SetData() call (never shrinks), updated via Map/Unmap (no UpdateSubresource path was needed — D3D11_USAGE_DEFAULT buffers can't be Map()'d for read-back verification, and this project's own established bar is real round-trip proof, so DYNAMIC-only was the simpler, still-fully-correct choice; UpdateSubresource remains available as a future optimization for genuinely static/never-remapped buffers, not required by this row's own wording). SetDataOptions mapping: Discard→D3D11_MAP_WRITE_DISCARD, NoOverwrite→D3D11_MAP_WRITE_NO_OVERWRITE, None→D3D11_MAP_WRITE_DISCARD (always GPU-sync-safe; XNA's own docs only say None may stall, never that it must, so this backend simply never stalls — documented in-file, not a silent reinterpretation). Real proof: d3d11_smoke_test.cpp Check E creates a real vertex buffer, SetData()s 4 known VertexPositionColor vertices, then reads the actual GPU buffer back via CopyResource to a D3D11_USAGE_STAGING+D3D11_CPU_ACCESS_READ buffer + Map(D3D11_MAP_READ) (the same technique DX-28's ReadBackbuffer() already uses for the back-buffer texture, applied to a plain buffer) and memcmps the exact bytes — genuinely passed under Wine+DXVK on this machine. |
| DX-31 | D3D11IndexBufferBackend: 16-bit (DXGI_FORMAT_R16_UINT) and 32-bit (DXGI_FORMAT_R32_UINT), same buffer-update strategy |
✅ | Closed 2026-07-13 — same real round-trip bar as DX-30, both bit widths. D3D11IndexBufferBackend takes a thirtyTwoBit flag at construction (mirrors IGraphicsBackend::CreateIndexBuffer16 vs. the newly-added real CreateIndexBuffer32 override — previously D3D11GraphicsBackend only declared/implemented the 16-bit factory and silently inherited IGraphicsBackend's own CreateIndexBuffer32 default, which just delegates to CreateIndexBuffer16 and would have produced a 16-bit buffer mislabeled as 32-bit; found and fixed as part of this task, not a pre-existing separate bug). One intentional deviation from EasyGL's own permissive precedent (its SetData16/SetData32 don't check thirtyTwoBit and will silently reinterpret whichever is called): this backend throws std::runtime_error if SetData16/SetData32 is called against a buffer of the other bit width — a real, deliberate defensive check, documented in-file, since XNA/FNA's own IndexBuffer/DynamicIndexBuffer never mixes the two on one buffer and a silent width mismatch would produce corrupted index data with no error. Real proof: d3d11_smoke_test.cpp Check F creates and round-trips both a 16-bit and a 32-bit index buffer (same CopyResource-to-staging read-back technique as DX-30), and separately asserts IsThirtyTwoBit()/GetFormatEXT() (DXGI_FORMAT_R16_UINT/_R32_UINT) match — all genuinely passed under Wine+DXVK. |
| DX-32 | Wire DX-16-vtx's stride-keyed D3D11_INPUT_ELEMENT_DESC inference into actual ID3D11InputLayout creation, cached per (shader, stride) pair |
✅ | Closed 2026-07-13 — real CreateInputLayout() proof against a real vertex shader's DXBC input signature. New D3D11InputLayoutCache (D3D11, not D3DCommon — ID3D11InputLayout is a D3D11-only COM type with no D3D12 equivalent object, design decision 4's "only what's genuinely shared" boundary honored) caches ComPtr<ID3D11InputLayout> keyed by (D3DShaderVariant, strideInBytes), calling D3DVertexFormatHelper::InputElementsForStride() (DX-16-vtx) + D3DShaderCache::GetVertexShaderBytecode() (DX-15-embed, deliberately exposed for exactly this) → device->CreateInputLayout(...). Added D3D11GraphicsBackend::GetContextEXT()/GetInputLayoutCacheEXT() (NOXNA) alongside the existing GetDeviceEXT(), and a D3D11InputLayoutCache inputLayoutCache_ member, so Phase DX8's draw-call wiring has a ready-made cache to call into (mirrors DX-15-embed's own "expose now, consume later" precedent). Real proof: d3d11_smoke_test.cpp Check G calls GetOrCreate() for colored3d@stride-16 and skinned3d@stride-52 (the simplest and the most complex of the 5 established strides), asserting both succeed (non-null) and that a second request for the same (variant, stride) returns the identical ID3D11InputLayout* (proves real caching, not just repeated creation) — genuinely passed under Wine+DXVK. |
| # | Task | Status | Notes |
|---|---|---|---|
| DX-40 | D3D11TextureBackend: ID3D11Texture2D + ID3D11ShaderResourceView, UpdatePixels/UpdatePixelsLevel, mip level support |
✅ | Closed 2026-07-13 — real device-facing proof. D3D11TextureBackend (src/CNA/Internal/Backends/D3D11/D3D11Textures.{hpp,cpp}) allocates an ID3D11Texture2D (RGBA8, MipLevels from ImageData::mipLevels) + default-view ID3D11ShaderResourceView, uploads level 0 via UpdateSubresource at construction, and UpdatePixels/UpdatePixelsLevel both real (D3D11CalcSubresource per level). D3D11_Smoke Check H: constructor upload and a later UpdatePixelsLevel() replacement both round-trip exact bytes through a staging-texture readback — not simulated. RGBA8-only, matching this project's own established EasyGL/Vulkan/Software simplification (every ITextureBackend is RGBA8 regardless of the XNA SurfaceFormat requested). |
| DX-41 | D3D11TextureCubeBackend: 6-face ID3D11Texture2D array with D3D11_RESOURCE_MISC_TEXTURECUBE |
✅ | Closed 2026-07-13. 6-slice array texture, D3D11_SRV_DIMENSION_TEXTURECUBE view, face order matches D3D11's own native cube-array-slice order (+X,-X,+Y,-Y,+Z,-Z) — the same convention IRenderTargetCubeBackend::BindAsRenderTargetFace()'s doc comment already used, so texture and render-target-cube face indices agree. SetData/GetData via D3D11CalcSubresource(level, face, mipLevels) + UpdateSubresource/staging-texture Map. D3D11_Smoke Check I: SetData()+GetData() round-trip exact bytes for a sub-region of one face — real, not assumed. |
| DX-42 | D3D11Texture3DBackend: ID3D11Texture3D |
✅ | Closed 2026-07-13. Same shape as DX-40/DX-41 applied to ID3D11Texture3D (subresource = mip level directly, no array-slice indexing needed for volume textures). D3D11_Smoke Check I: SetData()+GetData() round-trip exact bytes for a sub-volume, honoring D3D11_MAPPED_SUBRESOURCE::DepthPitch per depth-slice (not just RowPitch per row, DX-28's own established discipline extended one dimension further). |
| DX-43 | D3D11RenderTargetBackend/D3D11RenderTargetCubeBackend: offscreen ID3D11RenderTargetView(s) + matching depth-stencil, BindAsRenderTarget/UnbindAsRenderTarget via OMSetRenderTargets |
✅ | Closed 2026-07-13 — required a real, load-bearing fix to Clear() itself, not just new classes. D3D11GraphicsBackend::Clear()/ClearColorAndDepth/etc. were hardcoded to backBufferRTV_/depthStencilView_ (a leftover from Phase DX4, before any render target existed to bind) — genuinely wrong once a custom render target is bound, since GraphicsDevice.Clear() must clear whatever's currently active. Fixed by adding currentColorRTVs_[8]/currentRTVCount_/currentDSV_ tracking (TrackCurrentRenderTargetEXT/RestoreBackBufferRenderTargetEXT, since D3D11 has no queryable "current FBO" the way GL does) and routing every Clear* method through it. D3D11RenderTargetBackend/D3D11RenderTargetCubeBackend (D3D11RenderTargets.{hpp,cpp}) hold a non-owning D3D11GraphicsBackend* owner_ (mirrors VulkanRenderTargetBackend's identical pattern) so UnbindAsRenderTarget() can restore the back buffer — GraphicsDevice only ever calls SetRenderTarget2D(nullptr), never the old target's own Unbind directly, so D3D11GraphicsBackend::SetRenderTarget2D itself finalizes (currentCustomRT_->UnbindAsRenderTarget()) whatever was previously bound before binding/restoring the new one. D3D11_Smoke Check J: BindAsRenderTarget()+Clear() writes the exact color into the RT's own texture (staging-texture readback, not the back buffer), and a follow-up Clear()+GetBackBufferData() confirms Unbind() genuinely restored the back buffer as the target — both real, GPU-verified. |
| DX-44 | ID3D11SamplerState creation/caching from SamplerState (filter/address-mode, via DX-12-state's mapping table) |
✅ | Closed 2026-07-13. D3D11SamplerCache (D3D11SamplerCache.{hpp,cpp}), keyed by (filter, addressU, addressV, maxAnisotropy), wired into D3D11GraphicsBackend::ApplySamplerState() → PSSetSamplers. One real, documented interface limitation found: IGraphicsBackend::ApplySamplerState()'s signature has no addressW parameter (XNA's SamplerState does have one) — AddressW is set equal to AddressV, a pre-existing gap in the shared interface, not something this task could fix unilaterally without touching every other backend's own ApplySamplerState. D3D11_Smoke Check L: identical XNA-level state returns the cached object, different state creates a distinct one, and ApplySamplerState() itself is exercised end-to-end through the real device. |
| DX-45 | MSAA render target support (DXGI_SAMPLE_DESC, resolve via ResolveSubresource) |
✅ | Closed 2026-07-13 — real MSAA, not just plausible-looking code. D3D11RenderTargetBackend queries real device support via CheckMultisampleQualityLevels (never assumes a requested sample count is honored — ClampMultiSampleCount() falls back to 0/no-MSAA if the device reports zero quality levels for it) and allocates a separate MSAA color texture + a single-sample resolveTexture_, ResolveSubresource'd on UnbindAsRenderTarget() (the flip-model swap chain itself, DX-23, stays SampleDesc.Count=1 always, exactly as this row's own original note anticipated). Mip chains and MSAA are mutually exclusive on one attachment (documented in-code) — matches this project's own EasyGL/Vulkan precedent of "resolved once, then mip-cascaded" never happening on the same MSAA-rendered attachment. D3D11_Smoke Check K: a genuine 4x MSAA render target (device-confirmed: log line MSAA: requested 4x, device-applied 4x on this machine's RADV/DXVK GPU), cleared and resolved, reads back the exact color from the resolved texture — real proof, not simulated. D3D11RenderTargetCubeBackend deliberately does not support MSAA (documented scope decision, narrower/rarer combination, GetMultiSampleCount() always 0) — an honest, intentional gap, not an oversight. |
| DX-46 | SetRenderTargets(IRenderTargetBackend* const* rts, int count) — real multiple-render-target (MRT) support: bind up to D3D11_SIMULTANEOUS_RENDER_TARGET_COUNT (8) ID3D11RenderTargetView*s in one OMSetRenderTargets call, not just the first target via the inherited single-target default |
✅ | Closed 2026-07-13. D3D11GraphicsBackend::SetRenderTargets() does exactly one OMSetRenderTargets(n, rtvs, dsv) call binding up to 8 real RTVs at once (D3D11's immediate-binding model makes this simpler than Vulkan's own MRT proxy/deferred-render-pass approach for the same feature) — depth-stencil is taken from rts[0] (first-target-supplies-depth convention). D3D11_Smoke Check N: 2 render targets bound via one SetRenderTargets() call, Clear() (now routed through the same currentColorRTVs_ tracking DX-43 added) writes the exact color into both targets' own textures, independently read back and verified — real binding + real clear, genuinely proven this far. Honest scope boundary, not silently overclaimed: an MRT set's individual targets are not tracked in currentCustomRT_, so per-target MSAA-resolve/mip-regeneration-on-unbind (which the single-target path, DX-43, fully handles) is not wired for the N>1 case — undiscovered because no draw path exists yet to actually write divergent per-target output anyway (Phase DX8). Real bug found and fixed 2026-07-13, during DX-61: SetRenderTargets(nullptr, 0) never restored the back buffer after a prior MRT bind specifically because MRT binds don't set currentCustomRT_ (the paragraph above's own honest boundary) — the unbind branch only checked currentCustomRT_, so it silently no-op'd instead of restoring anything, leaving the device context bound to (soon-to-be-destroyed) render target views. Fixed to unconditionally call RestoreBackBufferRenderTargetEXT() on unbind; see DX-61's own row for the full story of how this was actually discovered (it broke that task's first real draw-call test, not a code-review catch). |
| DX-47 | D3D11OcclusionQueryBackend (IOcclusionQueryBackend): ID3D11Query created with D3D11_QUERY_OCCLUSION; Begin()/End() map directly to ID3D11DeviceContext::Begin/End; IsComplete() via GetData(query, nullptr, 0, D3D11_ASYNC_GETDATA_DONOTFLUSH) == S_OK; PixelCount() via GetData(query, &count, sizeof(UINT64), 0) |
✅ | Closed 2026-07-13. D3D11OcclusionQueryBackend (D3D11OcclusionQuery.{hpp,cpp}) exactly as specified; PixelCount()'s int-vs-UINT64 narrowing is an explicit std::min<UINT64>(count, INT32_MAX) clamp, not a silent truncation (matches EasyGL's own GLES3 GL_ANY_SAMPLES_PASSED note that this backend can be more precise, not less, since D3D11_QUERY_OCCLUSION gives an exact count). D3D11_Smoke Check M: a real query, Begin()/End()/context->Flush()/polled IsComplete(), completes and reports PixelCount() == 0 (no draws exist yet to occlude anything, Phase DX8) — the query mechanism itself is proven real, not the (not-yet-possible) nonzero-count case. |
| # | Task | Status | Notes |
|---|---|---|---|
| DX-50 | ApplyBlendState: ID3D11BlendState creation/caching from BlendState's src/dst/op fields (color + alpha separately), via DX-12-state |
✅ | Closed 2026-07-13 — real device-facing proof. New D3D11BlendStateCache (D3D11StateObjectCache.hpp/.cpp) caches by the 6 raw XNA Blend/BlendFunction ordinals ApplyBlendState() already carries; BlendEnable derived as FALSE only for the exact Blend::One/Blend::Zero Opaque combo on both channels (mirrors VulkanGraphicsBackend::ApplyBlendState's own established heuristic, Task 868), TRUE otherwise. IGraphicsBackend::ApplyBlendState carries no per-target color-write-mask parameter, so every cached state uses D3D11_COLOR_WRITE_ENABLE_ALL — a documented pre-existing interface limitation (matches D3D11SamplerCache's own AddressW-reuses-AddressV limitation, DX-44), not something this task introduced or can fix alone. D3D11GraphicsBackend::ApplyBlendState() calls OMSetBlendState() with the cached object and the tracked blend-factor array. Also implemented SetBlendFactor() (not itself one of this row's named methods, but required for ApplyBlendState to be genuinely correct — GraphicsDevice.BlendFactor is a real, independent, immediately-effective property per Task 870/319, so SetBlendFactor() re-binds the current cached blend state with the new factor via a fresh OMSetBlendState() call, without needing a new state object). Real proof: D3D11_Smoke Check O creates two blend states with identical XNA params (identical pointer) and one with different params (different pointer), confirms ApplyBlendState()'s OMSetBlendState() call via OMGetBlendState() returns the exact cached pointer, and confirms SetBlendFactor()'s standalone re-bind via the same query. Honest scope boundary: this only proves creation/caching/binding — actual blended pixel output needs a real draw call, not available until Phase DX8; not claimed here. |
| DX-51 | ApplyDepthStencilState: ID3D11DepthStencilState creation/caching, including stencil ops/masks/reference value |
✅ | Closed 2026-07-13 — real device-facing proof. New D3DStateMapping::StencilOperationToD3D11 (D3DCommon, shared with a future D3D12 consumer per design decision 4 — D3D11_STENCIL_OP/D3D12_STENCIL_OP verified numerically identical against both SDK headers on this machine, same verification discipline as DX-12-state) maps XNA StencilOperation's 8 values, correctly distinguishing wrapping Increment/Decrement (D3D11_STENCIL_OP_INCR/DECR) from clamping IncrementSaturation/DecrementSaturation (_INCR_SAT/_DECR_SAT) — two genuinely distinct D3D11 ops, not interchangeable. New D3D11DepthStencilStateCache caches by every ApplyDepthStencilState() field except referenceStencil (not part of D3D11_DEPTH_STENCIL_DESC — it's a separate OMSetDepthStencilState() bind-time argument, matching D3D11's own object/reference-value split). TwoSidedStencilMode gates whether BackFace actually uses the separate ccwStencil* fields or mirrors FrontFace — mirrors this project's own EasyGL precedent (*_separate(Back, ...) GL calls only made when two-sided mode is on), not a blind always-wire-BackFace-to-ccw* choice. Also implemented SetReferenceStencil() (same "required for correctness, not just the named method" reasoning as DX-50's SetBlendFactor() — GraphicsDevice.ReferenceStencil's own Task 870/319 standalone-immediate-effect contract) — re-binds the current cached depth-stencil state with a new reference value via OMSetDepthStencilState(), no new object needed. Real proof: D3D11_Smoke Check O — cache identity/distinctness, ApplyDepthStencilState()'s bind (incl. reference value) confirmed via OMGetDepthStencilState(), and SetReferenceStencil()'s standalone re-bind confirmed the same way. Honest scope boundary: same as DX-50 — stencil test/write pixel behavior needs a real draw call, Phase DX8, not claimed here. |
| DX-52 | ApplyRasterizerState: ID3D11RasterizerState creation/caching (CullMode/FillMode/depth bias/scissor-enable) |
✅ | Closed 2026-07-13 — real device-facing proof. New D3D11RasterizerStateCache caches by ApplyRasterizerState()'s 5 fields; FrontCounterClockwise = FALSE per DX-12-state's own CullModeToD3D11 assumption. Real, documented unit-convention finding: XNA's RasterizerState.DepthBias is a float already expressed in units of "r" (the depth format's minimum resolvable difference) — the same convention this project's own Vulkan backend feeds unscaled into vkCmdSetDepthBias's depthBiasConstantFactor, and EasyGL feeds unscaled into glPolygonOffset's "units" parameter (Task 767). D3D11_RASTERIZER_DESC::DepthBias is the identical "r"-scaled bias but declared INT, not FLOAT — this task rounds (std::lround, not truncates) to the nearest representable integer rather than assuming a 1:1 float-to-int cast is exact; SlopeScaledDepthBias stays FLOAT→FLOAT, no conversion needed. IGraphicsBackend::ApplyRasterizerState carries no MultiSampleAntiAlias parameter (a pre-existing interface limitation) — D3D11_RASTERIZER_DESC::MultisampleEnable/AntialiasedLineEnable both left at D3D11's own FALSE default (this only affects line/point AA algorithm selection, not MSAA render-target sampling, which Phase DX6's DXGI_SAMPLE_DESC already controls independently). Real proof: D3D11_Smoke Check O — cache identity/distinctness across a cull-back/solid vs. cull-none/wireframe pair, ApplyRasterizerState()'s RSSetState() bind confirmed via RSGetState(). Honest scope boundary: same as DX-50/DX-51 — actual wireframe/cull/depth-bias pixel behavior needs a real draw call, Phase DX8, not claimed here. |
| DX-53 | Viewport/scissor rect: RSSetViewports/RSSetScissorRects |
✅ | Closed 2026-07-13 — real device-facing proof. Straightforward direct calls (no caching needed — viewport/scissor are single-slot device state, not object-creating like the 3 rows above): SetViewport() → RSSetViewports() with the exact (x, y, w, h, minDepth, maxDepth) given; SetScissorRect() → RSSetScissorRects() converting XNA's (x, y, w, h) to D3D11's (left, top, right, bottom) via right = x+w, bottom = y+h. Real proof: D3D11_Smoke Check O calls both, then round-trips the bound state back via RSGetViewports()/RSGetScissorRects() and confirms an exact match (including the depth range and the right/bottom conversion), then restores the full window-size viewport afterward via GetViewportSize() so no later check (none exist yet, but the invariant is kept honest for whichever Phase DX8 check lands next) is left with a stale small viewport. |
Builds directly on DX-13-hlsl/DX-15-embed (Phase DX3) and DX-32's input layout cache. Land in
the order below — cheapest/most-foundational shader variant first, same ordering discipline
plan_software.md used for its own rasterizer-then-shading progression.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-60 | Define explicit GPU-side constant-buffer POD structs — not a raw memcpy(mapped.pData, ¶ms, sizeof(params)) of GpuDrawParams into one buffer — matching HLSL cbuffer packing rules (16-byte register boundaries; scalar/vector alignment; HLSL bool is 4 bytes, not C++'s 1) and this project's single, explicit matrix-layout policy (design decision 14 — pick row_major declared consistently everywhere, or CPU-side transpose before upload; do not decide per shader). At minimum: struct alignas(16) D3DPerDrawConstants (world matrix, material color, texture-enable flags, alpha-test params) and struct alignas(16) D3DLightingConstants (the 3-light directional/specular/ambient/emissive/fog fields) — grouped along the same lines GpuDrawParams already groups them conceptually — each with static_assert(sizeof(...) % 16 == 0) (D3D11 requires a constant buffer's ByteWidth to be a 16-byte multiple, design decision 14) plus field-offset checks, verified against the actual HLSL cbuffer declaration it's meant to match |
✅ | Closed 2026-07-13 — struct layouts read directly from the real, already-compiler-verified HLSL (DX-13-hlsl/DX-14-compile), not re-derived from the plan's own prose summary. New header-only src/CNA/Internal/Backends/D3DCommon/D3DConstantBuffers.hpp (no .cpp — pure POD layout, design decision 4's "genuinely shared, no D3D11 API dependency" bar). D3DPerDrawConstants (128 bytes) matches colored3d.vert.hlsl/textured3d.vert.hlsl/colored_textured3d.vert.hlsl's shared PerDraw : register(b0) field-for-field (Mvp/DiffuseColor/AmbientColor/LightingEnabled/Light0Dir/TextureEnabled/Light0Diffuse/VertexColorEnabled) — colored3d itself only reads Mvp/DiffuseColor/VertexColorEnabled (its own HLSL leaves the rest as opaque _Unused... blobs at the identical byte offsets), so one struct genuinely covers all three variants' real cbuffer shape. D3DLightingConstants (256 bytes) matches lit_textured3d.vert.hlsl's LitLightParams : register(b1) field-for-field — defined now per this row's "get the layout right once" mandate, not yet wired into any draw call (that's DX-63). Every field offset verified via static_assert(offsetof(...) == N, ...) against the real HLSL source (not guessed), plus static_assert(sizeof(...) % 16 == 0) on both. A third struct, D3DFogConstants (32 bytes, matching the shared FogParams : register(b1) — or register(b2) for dual_texture3d — every colored/textured-family variant declares), was added beyond the row's own "at minimum" list since DX-61 genuinely needs it to draw anything with fog support at all. Matrix upload confirmed empirically, not just asserted: Matrix::ToColumnMajor() — despite its name — emits the raw row-major M11..M44 byte order XNA's CPU-side Matrix already uses, which is exactly what an HLSL row_major cbuffer field expects unchanged (no CPU-side transpose), confirmed by DX-61's own real pixel-readback proof below actually rendering the correct triangle. |
| DX-60a | struct alignas(16) D3DBoneConstants (the SkinnedEffect 72-bone array) as its own, separate constant buffer from D3DPerDrawConstants/D3DLightingConstants — not folded into the shared per-draw buffer; same static_assert(sizeof(...) % 16 == 0) and matrix-convention requirements as DX-60 (design decision 14) |
✅ | Closed 2026-07-13, alongside DX-60 in the same D3DConstantBuffers.hpp. D3DBoneConstants (4608 bytes = 72 × row-major mat4) matches skinned3d.vert.hlsl's BoneBlock : register(b1) exactly — static_assert(sizeof(...) == 72*64) plus the standard 16-byte-multiple assert. Not yet wired into any draw call — ahead of DX-67 (skinned3d pipeline wiring), same "define the layout now, land the pipeline later" discipline as D3DLightingConstants above. |
| DX-61 | colored3d (stride 16, unlit vertex-color) pipeline: input layout + VS/PS + draw dispatch — first real 3D triangle, first real pixel test target |
✅ | Closed 2026-07-13 — a real triangle, genuinely rendered and pixel-verified, not just "Draw() returned S_OK". D3D11GraphicsBackend::DrawColoredPrimitives()/DrawIndexedColoredPrimitives() (previously honest "not yet implemented" throws) now do the real thing: look up colored3d's cached VS/PS (DX-15-embed) and input layout (DX-32), fill DX-60's D3DPerDrawConstants (Mvp = world*view*projection via ToColumnMajor(), DiffuseColor=white, VertexColorEnabled=1 — the same "raw vertex color" convention every other backend's DrawColoredPrimitives already uses, Task 364) and D3DFogConstants (fog disabled — this legacy no-GpuDrawParams path has nothing to enable it from) into two lazily-created, persistent D3D11_USAGE_DYNAMIC constant buffers (GetOrCreatePerDrawConstantBufferEXT/GetOrCreateFogConstantBufferEXT, updated via Map(WRITE_DISCARD)/Unmap each draw, never recreated), binds everything (IASetVertexBuffers/IASetIndexBuffer/IASetInputLayout/IASetPrimitiveTopology/VSSetShader/PSSetShader/VSSetConstantBuffers/PSSetConstantBuffers), and calls a real Draw()/DrawIndexed(). Only stride-16 (VertexPositionColor) is wired — any other stride throws a clear, honest "not implemented yet, see DX-62 onward" error rather than silently misrendering. Real proof, this plan's established bar, met for the first time with an actual rendered pixel: new D3D11_Smoke Check P clears the back buffer to a known blue, reads back a fixed screen region (confirms blue), issues a real colored3d draw of a single NDC-space-covering triangle with solid opaque-red vertex color (world=view=projection=Identity, so vertex Position values ARE clip-space coordinates directly — no separate transform-correctness question to untangle from the shader/cbuffer-correctness question this check actually targets), reads back the same region again (confirms red) — proving the fragment genuinely came from the draw, not a stale value. Repeated for both DrawColoredPrimitives (non-indexed) and DrawIndexedColoredPrimitives (indexed) — 44/44 D3D11_Smoke checks pass (up from 42; 2 new checks), real ctest -R D3D11 run, not assumed. A real, independent bug found and fixed getting this check to pass (not part of this row's own original scope, but directly blocking it): D3D11GraphicsBackend::SetRenderTargets(nullptr, 0) (DX-46, Phase DX6) never restored the back buffer when the prior bind was an MRT SetRenderTargets(rts, N>1) call (only the single-target SetRenderTarget2D path's currentCustomRT_ tracking triggered the restore) — left the device context's OMSetRenderTargets (and this backend's own currentColorRTVs_/currentDSV_ tracking) pointing at render-target views the test's unique_ptr<IRenderTargetBackend>s had already destroyed by the time DX-61's Clear() ran, corrupting Check P's very first Clear(). Fixed by unconditionally (and idempotently) calling RestoreBackBufferRenderTargetEXT() on any SetRenderTargets(nullptr, 0)/count<=0 call, not only when currentCustomRT_ was set — see DX-46's own row for the cross-reference. |
| DX-62 | textured3d (stride 20) + colored_textured3d (stride 24) |
✅ | Closed 2026-07-13. Real DrawPrimitivesEx/DrawIndexedPrimitivesEx overrides added to D3D11GraphicsBackend, both delegating to a new shared DrawPrimitivesExImpl(vb, ib-or-null, ...) helper (avoids duplicating the whole variant-selection/constant-buffer block between the indexed/non-indexed entry points — mirrors DrawColoredPrimitives/DrawIndexedColoredPrimitives's own pre-existing duplication pattern, but factored this time since there are now more than 2 variants to dispatch). Stride 16/20/24 all route through the same D3DPerDrawConstants/D3DFogConstants cbuffers DX-60 already defined (confirmed field-for-field against textured3d.vert.hlsl/colored_textured3d.vert.hlsl's real cbuffer PerDraw/FogParams declarations before writing any code, not assumed from DX-60's own doc comment alone) — unlike DrawColoredPrimitives's hardcoded-white/no-fog legacy path, this is the first D3D11 draw call that honors real GpuDrawParams (diffuseColor, textureEnabled, vertexColorEnabled, fog). Texture binding: a new GetSrvForTextureEXT() helper resolves the real SRV via dynamic_cast to either D3D11TextureBackend or D3D11RenderTargetBackend (a render target used as a sampled texture is real, working XNA usage — not speculative), bound via PSSetShaderResources(0,1,&srv); a null params.texture0 binds a null SRV, which is safe since D3D11 (unlike Vulkan) doesn't require a default-white fallback for an unbound slot the shader's own TextureEnabled branch doesn't sample. Real GPU proof, not assumed: examples/d3d11_smoke_test.cpp Check Q — textured3d samples a known texture color exactly (diffuseColor left at its default white so outColor == texel byte-for-byte), proven through both DrawPrimitivesEx and DrawIndexedPrimitivesEx (same underlying impl); colored_textured3d multiplies a known vertex color through a white texture and reads back the exact vertex-color bytes, proving VertexColorEnabled's real effect rather than just the texture sample alone. All passed on the first real Wine+DXVK run — zero bugs found. |
| DX-63 | lit_textured3d (stride 32): full per-light Blinn-Phong (DirectionalLight0/1/2, specular, ambient/emissive) — this is what makes D3D11 match design decision 6's "full parity, not a subset" goal |
✅ | Closed 2026-07-13. First real consumer of DX-60's D3DLightingConstants (register(b1)): a new GetOrCreateLightingConstantBufferEXT() persistent dynamic buffer (same "grow, never recreate" discipline as perDrawConstantBuffer_/fogConstantBuffer_), populated field-for-field from GpuDrawParams' lighting fields (light1Dir/light1Diffuse/light2Dir/light2Diffuse/emissiveColor/world/eyePositionWorld/light0-2Specular/specularColor+specularPower/fog) and bound alongside D3DPerDrawConstants at b0/b1. stride==32 always selects this variant (matching VulkanGraphicsBackend::DrawPrimitivesEx's identical priority-chain logic, deliberately mirrored) unless alpha-test/dual-tex/env-map/skinned claims the draw first — the HLSL itself branches on LightingEnabled, so both the lit and unlit paths share one shader. Real GPU proof: Check R's unlit sub-check (LightingEnabled=false) is byte-exact (diffuseColor*texture, same bar as DX-62); the lit sub-check's real Blinn-Phong math is deliberately not byte-exact-asserted (replicating GPU float rounding on the CPU side for a hand-picked expected value was judged not worth the fragility) — instead it proves the lit branch genuinely executes by confirming the lit output differs from both the unlit result and the Clear() background (specular deliberately zeroed via specularColor=(0,0,0) to remove one more source of non-determinism from the plausibility check). Both sub-checks passed on the first real run. |
| DX-64 | alpha_test3d: per-pixel discard (clip() in HLSL) driven by GpuDrawParams::alphaTest |
✅ | Closed 2026-07-13. New D3DAlphaTestConstants struct (D3DConstantBuffers.hpp) — deliberately not a reuse of D3DPerDrawConstants, since alpha_test3d's HLSL declares a genuinely different single-cbuffer field set (AlphaRef/AlphaTol/AlphaPassW/AlphaFailW instead of Ambient/Lighting/Light0, fog folded directly into this one buffer instead of a separate FogParams cbuffer) — offsets verified against the real HLSL before writing the C++ struct, same discipline as every other D3DConstantBuffers.hpp struct. needsAlphaTest = (params.alphaTest[3] < 0 || params.alphaTest[2] < 0) takes priority over every other variant selection (mirrors Vulkan's identical condition), so this works for any of the 20/24/32-byte strides the effect might be used with (alpha_test3d.vert.hlsl's own input signature is stride-agnostic — only needs POSITION0+TEXCOORD0, present in all three strides' element tables; extra unused elements like COLOR0 in a stride-24 layout are harmless to CreateInputLayout). Real GPU proof, the actual point of this task: Check S proves clip() genuinely discards — a texture alpha that fails the test (AlphaTol=0, alpha>=ref) leaves the Clear() background completely untouched (not just "the draw call didn't throw"), then the same texture's alpha byte is updated in place (UpdatePixelsLevel) to a passing value and the second draw writes the exact texture color including its own non-255 alpha byte (64), confirming the pass path isn't silently forcing opaque output. Both sub-checks passed on the first real run. |
| DX-65 | dual_texture3d: two-sampler DualTextureEffect variant |
✅ | Closed 2026-07-13. DrawPrimitivesExImpl gained a needsDualTex branch: reuses D3DPerDrawConstants (b0, byte-identical shape to dual_texture3d.vert.hlsl's own PerDraw cbuffer) plus a dedicated dualTexFogConstantBuffer_ bound at register(b2) (not b1) — t0/s0+t1/s1 are already the two texture samplers, matching DX-13-hlsl's own register-scheme note. texture1/envMap SRV resolution added alongside texture0's existing GetSrvForTextureEXT(). Stride-gated to 20 (VertexPositionTexture) only — dual_texture_colored3d was deliberately never ported (DX-13-hlsl's own row). Real proof: D3D11_Smoke Check T draws two real 2×2 textures through two real SRVs/samplers and reads back the exact tex1.rgb*2 * tex2 byte result. |
| DX-66 | env_map3d: TextureCube reflection sampling, Fresnel weighting, specular tint (DX-41 prerequisite) |
✅ | Closed 2026-07-13. New dedicated D3DEnvMapPerDrawConstants (Mvp+World only, 128B, matching env_map3d.vert.hlsl's own PerDraw — genuinely different shape from D3DPerDrawConstants) and D3DEnvMapConstants (192B, EnvMapParams : register(b2), field-for-field matching the real HLSL declaration — offsets static_assert-verified). GetSrvForTextureCubeEXT() added (mirrors GetSrvForTextureEXT()'s two-concrete-type dynamic_cast resolution, for D3D11TextureCubeBackend/D3D11RenderTargetCubeBackend). Real proof (D3D11_Smoke Check U) is genuinely non-trivial: a camera placed far down -Z from a +Z-facing surface, with ambient/lighting/specular all zeroed by a combination of geometry (light0 perpendicular to the normal) and params, drives reflDir to resolve to almost exactly (0,0,-1) — landing deep inside (not near an edge of) the cube's -Z face (D3D11 native slice order +X,-X,+Y,-Y,+Z,-Z → index 5), the only face given a distinct, uniform, non-black color — and the readback matches that face's exact color. |
| DX-67 | skinned3d: bind DX-60a's dedicated D3DBoneConstants buffer (72×mat4, matching GpuDrawParams::boneTransforms), weightsPerVertex-aware blending |
✅ | Closed 2026-07-13. New D3DSkinnedExtraConstants (240B, FogParams : register(b2) — despite the name, carries fog + DirectionalLight1/2 + World + EyePosition(.w=weightsPerVertex) + specular, field-for-field matching the real HLSL, since BoneBlock's own 128-byte PerDraw buffer has no spare room). D3DBoneConstants (defined in DX-60a, unwired until now) is populated via a straight memcpy from GpuDrawParams::boneTransforms — confirmed not a transpose bug: SkinnedEffect::SetBoneTransforms() (SkinnedEffect.cpp:383) already fills that array via Matrix::ToColumnMajor(), the same function DX-60/DX-61's own report established emits raw row-major bytes, exactly what BoneBlock's row_major float4x4 Bones[72] wants unchanged. Real proof (D3D11_Smoke Check V): a single genuinely-populated identity bone (not left zero-initialized — an all-zero bone matrix would degenerate the transform and fail the check) combined with ambient=white/specular=zeroed samples the exact texture color. |
| DX-68 | sprite2d + instanced3d |
✅ | Fully closed — sprite2d reached its own anticipated real integration point when Phase DX9 (SpriteBatch) landed. This row's own original text correctly predicted sprite2d's real integration point would be SpriteBatch, not DrawPrimitivesEx's 3D dispatch — Phase DX9 (DX-73+) later built exactly that: D3D11SpriteBatchBackend/D3D12SpriteBatchBackend both create their real ID3D11VertexShader+ID3D11PixelShader/PSO directly from D3DShaderVariant::Sprite2d's own bytecode and draw every real sprite through it (confirmed by reading both .cpp files directly, not assumed) — real, GPU-pixel-verified via DX-70–DX-72/DX-112/DX-131/DX-133's own extensive SpriteBatch test coverage. Nothing about this row's own original scope remains unwired. Original 2026-07-13 instanced3d closure note, preserved: real DrawInstancedPrimitivesEx() override, a new fixed 5-element instanced input layout (GetOrCreateInstancedInputLayoutEXT(): POSITION0 @ slot 0 per-vertex + INSTANCEWORLD0–3 @ slot 1 per-instance stride 64, independent of the bound vertex buffer's own stride since the shader only reads Position, per DX-13-hlsl's own row note), D3DPerDrawConstants reused byte-identically for the Vp-named (not Mvp) PerDraw cbuffer. Real proof (D3D11_Smoke Check W): one identity-transform instance via the real per-instance buffer outputs the exact instance DiffuseColor, through context_->DrawIndexedInstanced(). |
| DX-69 | Fog (GpuDrawParams::fogEnabled/fogColor/fogStart/fogEnd) added to every 3D variant above, matching the Vulkan FogParams precedent (plan_graphics.md Task 899) |
✅ | colored3d/textured3d/colored_textured3d/lit_textured3d/alpha_test3d/dual_texture3d/env_map3d/skinned3d all wire real fog data through (2026-07-13) — every HLSL shader already computed its own fog factor/lerp when DX-13-hlsl ported it; DrawPrimitivesExImpl populates each variant's fog fields from GpuDrawParams for all 8 fog-capable variants now (the DX-65/DX-66/DX-67 branches added this session all include their own fog wiring, not deferred). sprite2d/instanced3d genuinely have no fog cbuffer fields in their HLSL at all (instanced3d.frag.hlsl's own row note: "no fog, no texture" by design) — not a gap, nothing to wire. Closed for real 2026-07-14 (Phase DX10, DX-81): new Check AC in examples/d3d11_smoke_test.cpp draws the same colored3d quad (object-space Z=0.5, FogStart=0.0/FogEnd=0.5, red vertex color, green fog color) twice — once with fogEnabled=false (reads back exact vertex red, unblended) and once with fogEnabled=true (reads back exact fog green, fogFactor lands exactly on 0 at Z=FogEnd) — an exact, unambiguous two-case discrimination through the real DrawPrimitivesEx path, not a re-derivation of the wiring itself. Caught and fixed one real bug in the new test itself while writing it (not production code): the first draft's readback Rectangle was 4×4=16 pixels but only passed a 1-element output array, throwing GetBackBufferData: data array too small for requested region — fixed to a 1×1 region. |
| DX-58 | Custom ShaderEffect (arbitrary HLSL source, IEffectBackend::CompileProgram): runtime D3DCompile() path, separate from the offline stock-shader pipeline (design decision 5) |
✅ | Closed 2026-07-13. New D3D11EffectBackend (D3D11EffectBackend.hpp/.cpp) mirrors VulkanEffectBackend's own established contract as closely as D3D11's model allows: same fixed 128-byte uniform slot convention ([16..79]=mat4, [80..95]=vec4 color, [96..99]=float/int slot 0 — SetUniform*'s name parameter deliberately ignored, matching Vulkan's own /*name*/-discarding precedent) and the same fixed Sprite2DVertex-shaped vertex contract (`x,y |
Closed 2026-07-13 — D3D11 has a real, GPU-verified SpriteBatch. All 3 rows below landed
together (D3D11SpriteBatchBackend, include//src/CNA/Internal/Backends/D3D11/D3D11SpriteBatch.hpp/.cpp),
wired into D3D11GraphicsBackend::CreateSpriteBatch(). Structurally mirrors
EasyGLSpriteBatchBackend (immediate-flush-per-texture-change quad batcher — D3D11's context is
already immediate-mode, no reason to defer like VulkanSpriteBatchBackend's frame-end snapshot
design). Tested through the real public API (Microsoft::Xna::Framework::Graphics::SpriteBatch
Texture2D, not the raw backend interface directly) — 6 new checks (Y/Z/AA) inexamples/d3d11_smoke_test.cpp'sD3D11_SmokeCTest, every one passing on the first real run through Wine+DXVK on the real GPU:ctest --test-dir cmake-build-d3d11 -R D3D11→ 2/2 tests, 87/87 checks (D3D11_Smoke64,D3D11_Common23) — up from 81/81 (D3D11_Smoke58,D3D11_Common23) at the end of Phase DX8, i.e. 6 new checks (Y/Z/AA), all passing on the first run.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-70 | D3D11SpriteBatchBackend: quad batching feeding the sprite2d pipeline from DX-68, matching EasyGLSpriteBatchBackend's own destination/source-rect/origin/rotation/SpriteEffects-flip formula |
✅ | Closed 2026-07-13. Real growable D3D11VertexBufferBackend/D3D11IndexBufferBackend (reused as-is from DX-30/DX-31, full re-upload per flush — batches are XNA-scale, not worth persistent-append complexity), a dedicated sprite2DInputLayout_ (POSITION0/TEXCOORD0/COLOR0, the same fixed Sprite2DVertex shape D3D11EffectBackend already hardcodes — not reusable via D3D11InputLayoutCache, since that cache is keyed only by byte-stride and stride-32 already means the unrelated VertexPositionNormalTexture layout; a real, deliberately-checked collision this row avoided rather than silently reusing the wrong layout), and the exact EasyGL quad/flip math (destination/source-rect/origin/rotation, no [0,1] UV clamp — matches FNA). One real, deliberate improvement over VulkanSpriteBatchBackend's own known gap: SetTransformMatrix() is genuinely implemented (Vulkan leaves it a silent no-op) — sprite2d.vert.hlsl's real contract (DX-13-hlsl) has no projection-matrix uniform at all (just ViewportSize), so the transform is applied on the CPU, per vertex, via Vector2::Transform(), before upload — mathematically equivalent to XNA/EasyGL's GPU-side transform * orthographicProjection composition, just evaluated CPU-side, and it applies uniformly to both the stock and custom-effect draw paths since both consume the same already-transformed vertex buffer. Real GPU proof: examples/d3d11_smoke_test.cpp Check Y, through the actual public SpriteBatch/Texture2D API, not the raw backend — a 2×2 per-corner-colored texture drawn at a known destination rect reads back the exact color in all 4 quadrants (PointClamp, no filtering ambiguity), and a second draw with SpriteEffects::FlipHorizontally is confirmed to genuinely swap the top-left/top-right quadrants. |
| DX-71 | Custom Effect via SpriteBatch::Begin(effect) |
✅ | Closed 2026-07-13. Reuses D3D11EffectBackend (DX-58) directly — FlushBatch() resolves customEffect_->GetEffectBackendPtr(), calls the new D3D11EffectBackend::SetViewportSizeEXT() (fills the [0..15]-byte vpSize slot that class's own header comment always reserved for it, mirroring VulkanEffectBackend's "set automatically by the sprite-batch runtime" convention — the game/effect author never calls it), then Effect::Apply() + Bind(). Texture unit 0 (t0/s0) is always bound by D3D11SpriteBatchBackend itself for both the stock and custom-effect paths (IEffectBackend::BindTexture()'s own doc comment — D3D11EffectBackend deliberately never overrides it). Real GPU proof: Check AA compiles a real custom HLSL pair at runtime (ShaderEffect, consuming the fixed Sprite2DVertex contract + the newly-wired vpSize slot to do its own pixel→NDC mapping, since this custom shader has no other way to reach ViewportSize) that deliberately inverts RGB; drawing the same 2×2 corner texture through SpriteBatch::Begin(..., &invertEffect) reads back the exact inverted color, not the stock sprite2d pipeline's un-inverted one — proves the whole custom-effect-via-SpriteBatch path end-to-end, not just "compiled". |
| DX-72 | TextureAddressMode::Wrap/Mirror via SpriteBatch (a real gap on SDL_Renderer, per docs/graphics-backend-feature-matrix.md — D3D11 should not inherit that limitation, it has real sampler address-mode support) |
✅ | Closed 2026-07-13. Sampler creation for sprite draws already goes through D3D11SamplerCache (DX-44) via owner_->ApplySamplerState(0, pendingFilter_, pendingAddressU_, pendingAddressV_, 1) — no D3D11-specific work needed beyond DX-70 itself; this row is the verification that Wrap/Mirror genuinely work, not a separate implementation. Real GPU proof: Check Z uses a sourceRectangle 2× the texture size (UV range 0..2) and two probe pixels each deliberately chosen to read a different color than the other two address modes would produce at that exact point (not merely "some color came back") — the Wrap probe reads the tile-repeated top row (red) where Clamp would read the bottom-edge-extended color (blue); the Mirror probe reads the reflected top row (red) where both Wrap and Clamp would read blue. Both passed exactly as predicted on the first real run. |
Per this project's convention (CLAUDE.md), test coverage belongs in the same task that implements
each capability — this phase names the cross-cutting suites, not "when to start testing."
| # | Task | Status | Notes |
|---|---|---|---|
| DX-80 | CTest registration for D3D11: a cna_d3d11_test() CMake macro, tests built for the Windows cross-target and run via scripts/run-wine-dxvk.sh (DX-3) |
✅ | Closed 2026-07-13. cna_d3d11_test() macro added (mirrors cna_software_test(), plus MinGW-specific link options -static-libgcc -static-libstdc++/-Wl,--allow-multiple-definition and cna_copy_mingw_runtime/cna_copy_sdl_runtime post-build DLL copies — needed since this is the first windowed MinGW CTest target, unlike Software/Headless). D3D11_Smoke registered, COMMAND is scripts/run-wine-dxvk.sh $<TARGET_FILE:...> as planned. Verified via real ctest invocation, not just manual execution: ctest --test-dir cmake-build-d3d11 -R D3D11 --output-on-failure → 1/1 Test #2: D3D11_Smoke ... Passed 6.17 sec, 100% tests passed. |
| DX-81 | Pixel tests per shader variant (DX-61–DX-68), same shape as the existing Vulkan pixel-test suite (tests//examples/vulkan_*_test.cpp) — clear-color, flat triangle, textured quad, per-effect lighting/fog/alpha-test/dual-texture/env-map/skinned cases |
✅ | Closed 2026-07-13, fog coverage added 2026-07-14 — coverage confirmed complete, not re-derived from scratch. Phase DX8/DX9's own landing forks already built exactly this suite incrementally as each variant landed (examples/d3d11_smoke_test.cpp Checks P through AA): clear-color+flat-triangle (colored3d, Check P), textured quad (textured3d/colored_textured3d, Check Q), per-effect lighting (lit_textured3d, Check R), alpha-test (Check S), dual-texture (Check T), env-map (Check U), skinned (Check V), instanced (Check W), custom ShaderEffect (Check X), SpriteBatch/sprite2d (Checks Y/Z/AA) — audited row-by-row against this task's own list and found genuinely complete, with one real, honestly-flagged gap: fog wasn't yet exercised by a dedicated on/off test (see DX-69's own row) — closed by a new Check AC (colored3d, fog-off vs fog-on, exact-color discrimination). This same audit |
also surfaced cna_reference_dump/cna_demo_2d's undefined reference to Effect::Apply() link |
|||
failure (out of this row's own scope at the time) — fixed 2026-07-14 in CMakeLists.txt (a |
|||
genuine D3D11/D3D12 SpriteBatch↔CNA static-library cycle, resolved via CMake's documented |
|||
archive-repeat support); cna_demo_2d still has its own separate, unrelated, still-open MinGW |
|||
SDL3/SDL.h include-path gap found while verifying the fix — see NEXT.md/docs/d3d11-backend.md |
|||
for both. Z-range finding: no adjustment was needed, unlike Vulkan's own Task 899 fog-test experience — D3D11's native clip-space Z range is [0,1], the same DirectX-convention range Vulkan's HLSL-derived shaders already use (plan_graphics.md Task 899's own note: "Vulkan's DirectX-convention clip volume clips any primitive with pre-divide Z<0"), so every existing D3D11 pixel test's Z values already work unmodified — confirmed by inspection, not by hitting and fixing a real clipping bug (unlike Vulkan's own history here). |
|||
| DX-82 | State-object tests: blend/depth-stencil/rasterizer, mirroring the existing Vulkan_BlendState_*/Vulkan_DepthStencilState_* test family |
✅ | Closed 2026-07-13 — real pixel-behavior proof, not just Phase DX7's narrower "object creates/binds" bar. Reused the exact backend-agnostic easygl_*_test.cpp sources Vulkan already reuses verbatim (they only touch the public GraphicsDevice/BasicEffect API, nothing EasyGL-specific) — D3D11_BlendState_Opaque/D3D11_BlendState_AlphaBlend (examples/easygl_blendstate_{opaque,alphablend}_test.cpp), D3D11_DepthStencilState_StencilEnable (examples/easygl_depthstencilstate_stencil_enable_test.cpp), D3D11_RasterizerState_CullMode (examples/easygl_rasterizerstate_cullmode_test.cpp), registered as 4 new CTest entries via cna_d3d11_test(), same pattern as D3D11_Smoke/D3D11_Common. All 4 passed genuinely on the first real Wine+DXVK run — BlendState::Opaque correctly discards translucent-source blending (pure red, no green bleed-through); AlphaBlend produces the exact premultiplied-blend mix; StencilEnable genuinely gates (stamped region survives, unstamped region shows background, and StencilEnable=false lets both through); CullMode genuinely culls by winding order across all 3 states (None/CullCounterClockwiseFace default/CullClockwiseFace, 6/6 sub-checks). Not a re-implementation from scratch — real behavioral proof through the exact same public draw path a real game uses, reusing already-authored, well-understood test logic per this project's own established cross-backend-reuse convention. |
| DX-83 | Resize/swap-chain tests: BackBufferWidth/Height changes, fullscreen toggle (Wine-only verification here; real fullscreen-transition behavior needs the real-Windows checklist, DX-90) |
✅ | Closed 2026-07-13 — closes DX-29's own previously-flagged "implemented but never exercised" gap, for real. New Check AB in examples/d3d11_smoke_test.cpp: resizes via the real public GraphicsDeviceManager::setPreferredBackBufferWidth/HeightProperty() + ApplyChanges() path (64×64 → 96×80), polling a few frames since EnsureSwapChainSize() only picks the new size up lazily via SDL_GetWindowSizeInPixels() on the next Present() (same asynchronous-resize-delivery pattern this project's other Wine/Xvfb resize tests already accommodate). Real proof, not just "no crash": DXVK's own presenter log line changed to Buffer size: 96x80 (confirms the real swap chain's ResizeBuffers() call genuinely took effect, not just the CNA-level bookkeeping); Clear()+GetBackBufferData() afterward reads the exact clear color both at the origin AND near the new (96,80) far edge (proves the resized back buffer/RTV/DSV/viewport are genuinely the new size, not stale/clamped/wrong); PresentationParameters reflects 96×80. Fullscreen toggle itself not covered (no existing cross-backend fullscreen-toggle test pattern was found to reuse, and real fullscreen-transition behavior is explicitly DX-90's job per this row's own scope note) — Wine-only verification, as this row always intended. |
| DX-84 | Discriminating power independently verified pass for at least the first landed pixel test (git stash/targeted-mutation methodology, per this project's established convention) — sets the pattern for every later D3D11 test |
✅ | Closed 2026-07-13. Target: DX-61's own colored3d triangle test (Check P, the first real 3D triangle this backend ever drew). First mutation attempt was itself an instructive false negative: reversing the world*view*projection multiply order to projection*view*world in DrawColoredPrimitives produced zero test failures (still 67/67), because Check P deliberately uses Matrix::getIdentityProperty() for all three matrices — multiply order is a no-op under identity, so this mutation had no discriminating power for this specific test (not a finding about the production code's correctness, which Check Q/R's non-identity-adjacent draws already exercise differently). Second mutation, genuinely discriminating: changed perDraw.VertexColorEnabled from 1.0f to 0.0f in the same function — real Wine+DXVK rebuild+run reproduced the exact predicted failure (66/67 PASS, with precisely Check P's own "paints exact vertex color" check failing and every other check unaffected); reverted via git checkout, rebuilt, reconfirmed 67/67 PASS. Both the mutation and the revert were verified via a real build+Wine+DXVK run each time, not assumed. |
| DX-85 | scripts/run-wine-dxvk.sh (or the CTest harness built on top of it) asserts DXVK was actually engaged for the run, not silently a WineD3D fallback — reuses DX-4's own verification method (DXVK log file exists and identifies a real DXVK device/adapter line) as an automated check, not just a one-time manual spike |
✅ | Closed 2026-07-13 — directly answers the project owner's own flagged concern ("pouhé spuštění pod Wine nestačí"). scripts/run-wine-dxvk.sh now captures the wrapped run's combined stdout/stderr (tee to a temp file, PIPESTATUS-preserved real exit code) and greps for a DXVK: <version> line — the same distinguishing signal DX-4's own manual check used (vanilla WineD3D never prints this) — exiting 3 with a clear diagnostic if it's absent, overriding even a wrapped-program exit code of 0. Real end-to-end proof, both directions: running cna_test_d3d11_common.exe (Phase DX3's pure-function suite, which legitimately never creates a device) through the updated wrapper genuinely failed the gate on the first real run — correctly caught as a real device-less binary, not a bug in the gate; fixed by adding a distinct, narrowly-scoped CNA_D3D11_SKIP_DXVK_GATE=1 escape hatch (kept separate from the pre-existing CNA_D3D11_ALLOW_WINED3D diagnostic bypass, since the semantics differ — "never touches a device" vs. "deliberately testing without DXVK"), wired via set_tests_properties(D3D11_Common PROPERTIES ENVIRONMENT "CNA_D3D11_SKIP_DXVK_GATE=1") in CMakeLists.txt with an inline comment explaining why. After that fix, a full ctest -R D3D11 run (all 6 D3D11 CTest entries, every one now routed through the gated wrapper) passed 6/6 — proving the gate doesn't false-positive on any currently-registered D3D11 test, while still being a real, live check on every run rather than a one-time manual spike. |
| DX-90 | Real-Windows verification checklist — required for backend completion (cannot be satisfied by Wine+DXVK alone — see "Development environment" above): a real Windows 10/11 machine; MSVC build (not just MinGW) at least compiles and passes the same test suite; real DXGI present-mode/tearing behavior; full device-lost/removed recovery (DX-27 only added detection+logging — actually recreating all three lifetime groups, design decision 11, after a real device-removal event is verified here); WARP software-rasterizer fallback; D3D11 debug-layer warnings reviewed for anything Wine's DXVK path would have masked (including confirming DX-21's debug-layer-missing fallback path is never silently hit on a machine that should have it); at least one real GPU, from any single vendor |
⬜ | Do not mark Phase DX4–DX9 "done" project-wide from Wine-only results — this checklist is the actual completion gate, matching this project's own "Wine proves the logic, not real-hardware parity" rule for SDL_RENDERER. 2026-07-14: .github/workflows/d3d-windows-ci.yml added (project-owner-approved, narrow exception to this project's no-CI-automation stance) — a workflow_dispatch job on windows-latest that builds CNA+the backend with native MSVC (this project's first-ever native-MSVC configure, not just MinGW), runs the D3D11/D3D12 smoke/common CTest suites directly (no Wine wrapper — CMAKE_CROSSCOMPILING-gated command selection added to CMakeLists.txt for this), and recompiles all 20 HLSL shaders against a real D3DCOMPILER_47.dll (scripts/verify_hlsl_shaders_native_msvc.py). This provides exactly this row's own "GitHub-hosted Windows CI runners are useful for the MSVC-compile/unit-test/shader-generation-check portions" — no more. It does not close this row: swap-chain/tearing/device-lost/driver-parity still need a real display + real GPU driver. Honest, real limitation: the workflow itself has not yet been validated against an actual GitHub Actions run — this environment has no gh authentication configured (no token; gh auth status confirms not logged in, and this sandbox has no way to obtain one), so the workflow could not be triggered/iterated on for real. Two real, concrete gaps were found and fixed by inspection/local reasoning before pushing (native MSVC's missing FFmpeg pkg-config path; the Wine-wrapper-hardcoded CTest COMMAND), and the existing MinGW cross-compile CTest suites were re-verified clean after both changes — but the workflow's own MSVC configure/build/test steps are a first, careful attempt, not a proven-green result. Next concrete step: the project owner (who has real repo/Actions access) should run gh workflow run d3d-windows-ci.yml (or trigger via the GitHub UI) and share the resulting log if it fails, so a follow-up session can iterate with real failure data — exactly the kind of "genuine, well-documented blocker" this plan's own boundary rules call for, not a dropped task. |
| DX-91 | Extended compatibility verification (not required to call the backend complete): repeat DX-90's real-driver items (present/tearing, debug-layer warnings) on Intel, AMD, and NVIDIA hardware specifically, as each becomes available |
⬜ | Deliberately optional/best-effort, not a completion gate — requiring simultaneous physical access to three different GPU vendors before a single-developer project can call this backend "done" is an unreasonable bar, and isn't how this project's own multi-backend verification has worked historically (Vulkan/Bgfx were verified against whatever hardware was actually on hand). Pick up opportunistically as hardware becomes available; log findings per vendor rather than blocking on having all three at once. |
Closed 2026-07-14 — this is the last phase of the D3D11 plan itself. Phase DX1 through DX11 are
now all ✅ except the two explicitly-deferred, real-Windows-hardware-gated rows (DX-90/DX-91,
needs_human, no such machine available in this dev environment). Phase DX12 (D3D12) remains
separately authorized-but-not-started (design decision 9) — see that phase's own intro.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-95 | docs/d3d11-backend.md: what it's for, current capability boundary, the Wine+DXVK dev-loop setup (DX-2/DX-3), how to write a test, known limitations — mirrors docs/software-backend.md/docs/headless-backend.md's own structure |
✅ | Closed 2026-07-14. New docs/d3d11-backend.md, structured to match docs/software-backend.md/docs/headless-backend.md exactly (Status → What it's for/isn't → Development environment → Writing a test → Known limitations). Known-limitations list is honest and specific, not generic: real-Windows gate (DX-90/DX-91), device-lost recovery detection-only (DX-27), untested debug-layer-missing fallback (DX-21), the 5 combo Clear* variants untested beyond plain Clear (DX-25), specular highlights not pixel-verified (the lit-branch test zeroes specular for CPU-comparison determinism), multi-light/EmissiveColor not separately discriminating-tested, mip-chain/DepthStencilFormat fidelity untested, Model/SpriteFont not separately exercised, and the pre-existing unrelated cna_reference_dump/cna_demo_2d link failure found during DX-81. Written as a single pass now that the backend is feature-complete, not incrementally — this plan's own session already recorded each capability's real status row-by-row as it landed, so there was no stale-doc risk to avoid by writing earlier. |
| DX-96 | docs/graphics-backend-feature-matrix.md: add a D3D11 column once its feature set is broad enough for a meaningful row-by-row comparison (mirrors how HEADLESS/SOFTWARE were each given their own explanatory note instead of a premature column) |
✅ | Closed 2026-07-14. Added a real D3D11 column to every applicable table (2D SpriteBatch/SpriteFont, Stock Effects, RenderTarget/MSAA/mip/depth, Texture2D/Texture3D/TextureCube, GraphicsDevice state objects, OcclusionQuery, Model), plus a new doc-level explanation of what a D3D11 ✅/🟨/⬜ cell actually means (✅ = real GPU-facing check this session; 🟨 = implemented but not independently pixel-verified; ⬜ = not attempted this session — distinct from this doc's pre-existing ❌, "tested and found broken"). Deliberately did not blanket-✅ every row just because the underlying capability exists in code — cross-checked each cell against what a fork's own commit report actually claimed was tested (e.g. specular highlights, multi-light BasicEffect/EnvironmentMapEffect/SkinnedEffect variants, SpriteFont, Model, mip chains, and 4 of 5 Clear* combo variants are honestly 🟨/⬜, not ✅, despite the underlying code existing). |
| DX-97 | README.md: add D3D11 to the "Tested Compilers" table and a "Build (Windows cross-compilation — D3D11 backend)" section, mirroring the existing SDL_RENDERER MinGW-w64 section exactly |
✅ | Closed 2026-07-14. New "Tested Compilers" row (Linux→Windows cross, MinGW-w64, D3D11, ✅ verified building + 6-CTest/96+-check suite under Wine+DXVK, real-Windows gate noted inline) and a new "Build (Windows cross-compilation — D3D11 backend)" section mirroring SDL_RENDERER's own section shape (toolchain install, exact CMake invocation, exact ctest invocation), plus a new Project-Status bullet alongside the existing per-backend bullets (SDL_RENDERER/EASYGL/VULKAN/BGFX/WEBGPU). The older §6 backend-selection bullet list (SDL_RENDERER/EASYGL/BGFX/VULKAN) was found to already be stale before this task (missing HEADLESS/SOFTWARE/WEBGPU too, not just D3D11) — left as a pre-existing, out-of-scope gap rather than opportunistically fixed here. |
| DX-98 | Cross-reference this plan from NEXT.md once Phase DX1 actually starts (not before — this plan is not yet authorized, see the status banner) |
✅ | Closed 2026-07-14. This row's original gating condition is long since satisfied — the plan was authorized 2026-07-13 and Phases DX1–DX11 are now closed. NEXT.md has cross-referenced plan_dx.md since the very first Phase DX1/DX2 fork this session (top-of-file banner + §1 project summary), and every subsequent fork kept it current; this task's own closing pass (see below) re-verified and refreshed it once more end-to-end rather than assuming the running updates were still fully accurate. |
Status (2026-07-14): DX-100 through DX-113 and DX-115 are all closed — Phase DX12 is now
essentially complete for what this Debian dev environment can prove. Only DX-114 (real Windows
hardware) remains, correctly needs_human — no such machine is available here. D3D12's own real,
honestly-documented headline gap is swap-chain presentation: implemented for real but crashing under
this dev loop's Wine+vkd3d-proton setup (a genuine dxgi.dll/vkd3d-proton architecture mismatch, not
a CNA bug — see DX-100/DX-102's own Notes), so every proof in this phase stays off-screen. See
docs/d3d12-backend.md for the current-state summary and docs/graphics-backend-feature-matrix.md
for the row-by-row comparison against D3D11 and the other established backends.
Every row below is written up as a concrete, scoped task per design decision 9. The project owner authorized starting this phase 2026-07-13 ("later if time allows," after D3D11/Phases DX1–DX11 were fully completed the same session) — implementation may proceed. Coarser-grained than the D3D11 phases above, since detailed design should wait until D3D11's own experience (what actually worked, what Wine/DXVK-equivalent tooling exists for D3D12) can inform it.
DX-100's own spike (closed 🟨 2026-07-14) already changed this phase's sequencing once, exactly
as that row anticipated: the D3D12 device/queue/fence/command-list programming model is real and
usable locally via Wine+vkd3d-proton (even more capable than D3D11's DXVK path — DXR 1.1, SM 6.8
negotiated on this machine's GPU), but CreateSwapChainForHwnd specifically crashes or fails under
the tested drop-in-DLL setup — see DX-100's own Notes for the full evidence and two concrete ways
forward. Until that swap-chain gap is resolved or deliberately routed around, DX-102 onward
should default to option (b) from DX-100's Notes: off-screen/readback-only proof (an
ID3D12Resource render target + staging-heap readback, no IDXGISwapChain) for local Wine-based
development, deferring swap-chain/Present()-specific proof to a real-Windows/Windows-CI/VM pass
much earlier than D3D11's own DX-90 did — do not silently assume a working swap chain further into
this phase without re-verifying it.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-100 | Spike: does Wine + vkd3d-proton give a usable local D3D12 dev loop on this Debian machine, equivalent to D3D11's DXVK path? | 🟨 | Spiked 2026-07-14 — real, mixed result: device/compute path yes, presentation path no (not with the approach tried). MinGW headers: d3d12.h/d3d12sdklayers.h/d3d12shader.h/d3d12video.h + libd3d12.a all present under /usr/x86_64-w64-mingw32/{include,lib} (same as DX-1 found for D3D11) — d3dx12.h (Microsoft's optional C++ helper header) is absent, so any future task must not assume it's available; use raw ID3D12* calls directly. No system vkd3d-proton apt package is installed (dpkg -l empty; Debian's own libvkd3d1/libvkd3d-shader1/libvkd3d-utils1 packages exist in apt-cache search but are a different, Linux-native library Wine's built-in D3D12 support can use — not the same thing as vkd3d-proton's Windows-PE d3d12.dll/d3d12core.dll override). Real vkd3d-proton binaries were available locally without any new install, bundled inside this machine's existing Steam "Proton - Experimental" installation (.../files/lib/wine/vkd3d-proton/{x86_64,i386}-windows/{d3d12,d3d12core}.dll) — copied into a dedicated ~/.wine-cna-d3d12 prefix's system32/syswow64 plus HKCU\Software\Wine\DllOverrides set to native, the exact same no-sudo, project-local-prefix pattern DX-2/DX-3 already established for DXVK, so this required no new elevated/system-wide changes. A real throwaway spike (D3D12CreateDevice/CheckFeatureSupport/CreateCommandQueue/CreateDXGIFactory2+tearing/CreateFence/CreateCommandAllocator/CreateCommandList, cross-built via x86_64-w64-mingw32-g++ -ld3d12 -ldxgi, run under plain wine — Debian's system Wine 10.0, not Proton's own patched build) succeeded genuinely and completely for every one of those calls — real vkd3d-proton log lines confirm real engagement (vkd3d-proton - build: 4232071c346c0a7, vkd3d-proton - applicationVersion: 3.1.0), against a real GPU with feature level negotiated to 0xC100 = D3D_FEATURE_LEVEL_12_1, DXR 1.1 ray-tracing support, and Shader Model 6.8 (AMD RADV/Vulkan-backed) — genuinely more capable than D3D11's own DXVK path reported, not just "also works." The one real, evidenced failure: CreateSwapChainForHwnd (a plain-Wine-dxgi.dll-owned entry point, not vkd3d-proton's) — with DXGI_SWAP_EFFECT_FLIP_DISCARD (this plan's own D3D11 convention, design decision matching DX-23) it hard-crashes (unhandled page fault deep inside Wine's builtin dxgi.dll/wined3d swap-chain internals, confirmed via a live WineDbg backtrace attach); with the older DXGI_SWAP_EFFECT_DISCARD it fails cleanly instead (DXGI_ERROR_INVALID_CALL, 0x887A0001) — no crash, but still no working swap chain either way. Root cause, not just a symptom: vanilla Debian Wine's own dxgi.dll was built/tested against wined3d-backed (DXVK/D3D11-style) devices, and doesn't know how to hand a real ID3D12CommandQueue to its own swap-chain presentation path — that integration normally lives inside Proton's own patched Wine+DXGI fork, not upstream/Debian Wine. Attempted the natural fallback — running the exact same spike through Proton's own bundled wine binary (.../Proton - Experimental/files/bin/wine) against the same prefix — and it failed immediately (exit 53, no spike output at all, only a wineserver: using server-side synchronization line), consistent with a Wine-build/prefix-format mismatch; Proton's own Wine expects its own launch environment (STEAM_COMPAT_DATA_PATH etc.), not a bare wineboot-initialized prefix, and reproducing that properly is a distinct, non-trivial side effort not attempted further within this spike's scope. Recommendation for DX-101 onward: the D3D12 device/resource/command programming model (queues, fences, command lists/allocators, and by extension the coming PSO/root-signature/resource-barrier/buffer/texture work in DX-103–DX-109) is provably developable locally on this Debian machine via Wine+vkd3d-proton, same convenience D3D11's DXVK path gave — but on-screen presentation specifically (the swap-chain half of DX-102, and anything depending on a real Present()) is not proven working via this approach and should not be assumed to "just work" the way DX-23 did for D3D11. Two real options for whoever picks up DX-101: (a) invest real effort in properly replicating Proton's own launch environment for swap-chain testing specifically (not ruled out, just not achieved by this spike's lighter-weight attempt), or (b) lean the local dev/test loop on off-screen, swap-chain-free proof (render to an ID3D12Resource render-target-view texture, read back via a staging heap, no IDXGISwapChain involved at all — D3D12 doesn't strictly require a swap chain to draw) for everything through DX-109, and defer swap-chain/Present()-specific proof to a real-Windows or Windows-CI/VM pass much earlier than D3D11's own DX-90 deferred it — this second option most directly matches this row's own original stated purpose. Spike artifacts (d3d12_spike*.cpp, not part of the CNA build) were scratch-only and not checked in; the finding itself is what's preserved here. 2026-07-14 update, forward pointer: option (a) above was in fact pursued and succeeded — see DX-102's own row for the real fix (scripts/run-proton-vkd3d.sh, a properly Proton-managed launch that replicates the launch environment this spike found plain Wine couldn't). This row's own historical narrative and root-cause diagnosis are unchanged and still accurate; only the "not attempted further" framing is superseded. |
| DX-101 | Full D3D12 CMake wiring, all of it deferred here rather than pre-staged in Phase DX2 (see that phase's own intro): add "D3D12" to CNA_GRAPHICS_BACKEND's STRINGS property and a CNA_BACKEND_D3D12 option flag; extend DX-11's FATAL_ERROR non-Windows guard to also cover D3D12; add the cna_backend_graphics_d3d12 target (elseif(CNA_GRAPHICS_BACKEND STREQUAL "D3D12") block) linking d3d12/dxgi plus D3DCommon — same minimal-link-set discipline as DX-12/design decision 3, dxguid/d3dcompiler only if DX-100's own spike actually found them necessary, not assumed to carry over unchanged from D3D11's own confirmed set; add the CreateGraphicsBackend() factory dispatch for D3D12 |
✅ | Closed 2026-07-14 — real build proof, not a scaffold. CNA_GRAPHICS_BACKEND STRINGS/CNA_BACKEND_D3D12 option added mirroring D3D11's own pattern exactly; the DX-11 non-Windows FATAL_ERROR guard now covers both D3D11 OR D3D12 in one condition with a backend-name-parameterized message (verified: a non-MinGW-toolchain configure with -DCNA_GRAPHICS_BACKEND=D3D12 genuinely fails with "CNA: D3D12 backend only builds when targeting Windows..."). cna_backend_graphics_d3d12 links only d3d12+dxgi+cna_backend_graphics_d3dcommon — DX-100's own confirmed minimum, no dxguid/d3dcompiler added (nothing in this task's own build needed them). cna_backend_graphics_d3dcommon's existing PUBLIC d3d11 dxgi link (added back in Phase DX3 for D3D11 alone) was confirmed harmless for a D3D12-only build — it's just an import-lib reference, D3DCommon's own sources (D3DFormatMapping/D3DStateMapping/D3DVertexFormatHelper/D3DShaderCache) compiled and linked cleanly with no D3D11-specific pollution reaching the D3D12 target; not changed, since splitting it apart isn't yet justified by a real failure. New skeleton include/CNA/Internal/Backends/D3D12/D3D12GraphicsBackend.hpp/src/.../D3D12GraphicsBackend.cpp implements all 21 IGraphicsBackend pure virtuals (confirmed against the interface header directly, not guessed) with honest NotYetImplemented() throws — deliberately no real D3D12 API calls yet, since DX-102 onward owns that; GetWindowInternal/GetRendererInternal/GetViewportSize/SetVirtualResolution/SetPresentationMode are trivial accessors, not stubs, matching how D3D11's own first skeleton treated them. CreateGraphicsBackend() factory dispatch added the same one-definition-per-backend-translation-unit way every other backend (including D3D11) already does it — no central switch statement exists in this codebase to extend. Real build proof: fresh cmake -S . -B cmake-build-d3d12 -DCMAKE_TOOLCHAIN_FILE=cmake/toolchains/mingw-w64.cmake -DCNA_GRAPHICS_BACKEND=D3D12 -DCNA_BUILD_TESTS=ON configures cleanly, cmake --build cmake-build-d3d12 --target CNA → exit 0, Built target CNA (full XNA API surface, mirroring DX-15's own "first real build" bar for D3D11). Not yet run under Wine+vkd3d-proton (nothing executable exists yet — the skeleton only throws) — that's DX-102's job. |
| DX-102 | ID3D12Device creation, command queue (ID3D12CommandQueue), IDXGISwapChain (flip-model, DXGI_SWAP_EFFECT_FLIP_DISCARD) |
✅ | Doc-sync fix, 2026-07-14: this row's own status badge was never elevated past 🟨 even though every piece it scopes (device, command queue, swap chain) is now real and proven — the same stale-row pattern already found and fixed for DX-29/DX-100/DX-113. All three original scope items are closed: device + command queue below (this row's own original text, unchanged); swap-chain creation by this row's own 2026-07-14 update (scripts/run-proton-vkd3d.sh, a properly Proton-managed launch, makes CreateSwapChainForHwnd genuinely succeed); real Present()/back-buffer rendering through that swap chain by DX-116's own separate, later, real GPU-facing proof (a live 10-frame Clear()+Present() loop, reproduced twice from a clean log, zero crashes). Honest, still-real, permanent environment split, not a regression: plain Wine (no Proton) still crashes on CreateSwapChainForHwnd with the genuine page-fault root cause this row's own original text documents — only a properly Proton-managed launch works, which is why the routine D3D12_Smoke CTest still constructs off-screen (window=nullptr) and the windowed path stays a manual diagnostic (DX-116's own row explains why: Proton's bootstrap is too heavy for a normal CTest run on this dev loop). Device + command queue closed 2026-07-14, real and proven. CreateDeviceResources(): CreateDXGIFactory2 (best-effort D3D12GetDebugInterface/EnableDebugLayer first, matching design decision 12's "never a hard requirement" rule — genuinely unavailable on this machine, confirmed disabled, not a false claim), first non-software IDXGIAdapter1 via EnumAdapters1, then a real retry LOOP over {12_1, 12_0, 11_1, 11_0} (unlike D3D11's own single-call array-fallback — D3D12CreateDevice only accepts one MinimumFeatureLevel per call, a genuine API-shape difference, not an oversight) — succeeded on the first try at 12_1 on this machine/driver (0xc100), same tearing-capability query pattern as D3D11's own DX-22. CreateCommandQueueResources(): real D3D12_COMMAND_LIST_TYPE_DIRECT queue. Real proof: examples/d3d12_smoke_test.cpp (D3D12_Smoke CTest) — feature level 0xc100, tearing supported, confirmed via a genuine vkd3d-proton - applicationVersion: 3.1.0 log line under Wine (this revision's own real run, re-confirming DX-100's spike through the actual backend class this time, not a throwaway). Swap chain is the one real, evidenced, NOT-yet-working part — implemented for real (CreateSwapChainResources(), production DXGI_SWAP_EFFECT_FLIP_DISCARD, matching D3D11's own DX-23 convention exactly), but a real, dedicated, non-CTest diagnostic (examples/d3d12_swapchain_diag.cpp, run by hand through scripts/run-wine-vkd3d.sh's own Wine+vkd3d-proton prefix) crashed with a genuine page fault: vkd3d_instance_get_vk_instance(instance=0000000000000000) reading a null pointer, called from Wine's own dxgi.dll (dlls/dxgi/swapchain.c:3287, d3d12_swapchain_init) — a real architecture mismatch between Debian's system dxgi.dll (which expects Wine's own built-in winevkd3d instance state) and vkd3d-proton's separately-overridden d3d12.dll (which never populates that state), exactly matching DX-100's own root-cause finding, now reproduced through this backend's actual production code path instead of a raw spike. Design decision, honestly documented, not silently worked around: CreateSwapChainResources() is only ever called when args.window != nullptr; the primary D3D12_Smoke CTest always constructs off-screen (window = nullptr) specifically so it never reaches this crash-prone path, and a clean (non-crash) HRESULT failure is caught and downgraded to swapChainAvailable_ = false rather than thrown — but a genuine Wine-level page fault cannot be caught in-process by any C++ mechanism, which is exactly why the window-attached diagnostic is deliberately NOT registered as a CTest (would always crash the whole suite run). 2026-07-14 update — the plain-Wine crash is fixed for real, under a properly Proton-managed launch. A new scripts/run-proton-vkd3d.sh (bootstraps its own dedicated Wine prefix through a real local Steam "Proton - Experimental" install, STEAM_COMPAT_DATA_PATH etc. set correctly — the exact environment DX-100's own spike found plain Wine can't replicate) gives vkd3d-proton's d3d12.dll/d3d12core.dll the matched DLL pair it expects, and CreateSwapChainForHwnd genuinely succeeds: cna_diag_d3d12_swapchain reports IsSwapChainAvailableEXT() = true, reproduced twice including from a fresh prefix. This directly confirms the original root-cause diagnosis (a DLL-pairing mismatch, not a CNA bug) — giving vkd3d-proton the real matched pair it expects fixes it. Swap-chain creation is fixed by this; real Present()/back-buffer rendering through it is DX-116's own separate, later closure (also real, also proven, via the same Proton-managed launch). The plain-Wine crash path this row's own text above describes is unchanged and still real — it's specifically plain Wine (no Proton) that fails; this update is a second, working code path, not a fix to the first. |
UPDATE 2026-07-14 — swap chain now genuinely works locally, via a properly Proton-managed launch (this row's own crash above is fixed, not superseded/replaced). The project owner asked for one more real attempt before conceding to DX-114. Root cause refined: the crash above wasn't inherent to Wine+vkd3d-proton — it was specific to overlaying vkd3d-proton's d3d12.dll/d3d12core.dll onto a foreign system-Wine prefix, whose own dxgi.dll/wined3d.dll/etc. are a different, ABI-incompatible build that never learned to hand a D3D12 command queue to vkd3d-proton's overridden d3d12.dll. Running the exact same overlay inside a properly Proton-bootstrapped prefix (this machine's local Steam "Proton - Experimental" install, launched through Proton's own proton run entry point with real STEAM_COMPAT_DATA_PATH/STEAM_COMPAT_CLIENT_INSTALL_PATH, not just its bundled wine binary in isolation) — where dxgi.dll/wined3d.dll/libvkd3d-*.dll all come from one mutually-consistent Proton build — makes CreateSwapChainForHwnd succeed for real. Concrete evidence: examples/d3d12_swapchain_diag.cpp (extended to log to a file, since Proton's process-launch plumbing does not reliably forward child stdout to the invoking shell) now reports IsSwapChainAvailableEXT() = true with a real, non-null GetSwapChainEXT() handle — vkd3d-proton's own log confirms genuine engagement (dxgi_vk_swap_chain_init: Creating swapchain (64 x 64), BufferCount = 2, Ensure maximum latency of 3 frames with KHR_present_wait, SM 6.8/DXR 1.1/"DX Ultimate supported"), no crash, no page fault, clean process exit. Reproduced twice, including once from a completely fresh, from-scratch prefix (not a fluke/stale-state artifact). New reusable script: scripts/run-proton-vkd3d.sh (mirrors run-wine-vkd3d.sh's shape, but drives Proton's own launcher + a dedicated Proton-managed prefix instead of a hand-built system-Wine one). Two dead ends recorded so nobody repeats them: (1) copying just Proton's own dxgi.dll onto the existing system-Wine prefix fails to load at all (STATUS_DLL_NOT_FOUND, c0000135) — its wined3d.dll import needs matching ordinals from the same build, not a mix-and-match; (2) hand-building a fresh prefix via wineboot --init with WINEDLLPATH pointed at Proton's tree gets further but hits an unrelated first-run "found new hardware" GUI-wizard hang partway through Windows' own driver-installation sequence — only a real proton run bootstrap avoids this. Honest scope boundary: this closes the swap-chain creation gap only — Present()/back-buffer binding/rendering-to-the-real-swap-chain are still NotYetImplemented() stubs (untouched, deliberately out of this task's scope) and remain real, separate, open Phase DX12 work; DX-114's real-Windows pass is still the completion gate for tearing/present-mode/device-lost behavior on an actual driver, not superseded by this Wine-side fix — this only proves the creation call itself is no longer inherently broken on this dev loop. |
| DX-103 | Descriptor heaps: RTV heap, DSV heap, CBV/SRV/UAV heap (shader-visible), allocation strategy | ✅ | Closed 2026-07-14. Three real heaps (ID3D12DescriptorHeap): RTV (capacity 8), DSV (capacity 8), CBV/SRV/UAV (capacity 64, D3D12_DESCRIPTOR_HEAP_FLAG_SHADER_VISIBLE). Allocation strategy deliberately simple for this first implementation (explicitly allowed by this row's own text): a plain bump allocator per heap (rtvHeapNextIndex_/dsvHeapNextIndex_/cbvSrvUavHeapNextIndex_), throwing once a heap's fixed capacity is exhausted — no free-list/reuse of released descriptors yet, since nothing releases any yet (DX-109, resources, is unstarted). Real proof: D3D12_Smoke Check C allocates twice from each heap and confirms the returned D3D12_CPU_DESCRIPTOR_HANDLE/D3D12_GPU_DESCRIPTOR_HANDLE values advance by exactly one real GetDescriptorHandleIncrementSize() step — not just "the heap object is non-null". |
| DX-104 | Command allocators + command lists, per-frame-in-flight (matches this project's own Vulkan/Bgfx "batch a frame's draws" precedent conceptually, but D3D12 needs its own explicit allocator-reset lifecycle) | ✅ | Closed 2026-07-14. kFramesInFlight = 2, matching Vulkan's own established MaxFramesInFlight constant (VulkanGraphicsBackend.hpp) for consistency across this project's backends — not re-derived, deliberately reused. Two real ID3D12CommandAllocators (one per frame index) + one real, reused ID3D12GraphicsCommandList (created open against allocator 0, then Close()d immediately — D3D12's own creation convention, unlike D3D11 which has no equivalent object). Real proof: D3D12_Smoke Check D genuinely calls Reset() on both the allocator and the command list, Close()s it again, then submits it to the real queue and blocks on the real fence (ExecuteCommandListAndWaitEXT) until the GPU actually reports completion — a full allocator→list→queue→fence round trip, not just individual object construction. |
| DX-105 | Fences + frame synchronization (ID3D12Fence, GetCompletedValue/SetEventOnCompletion), N-frames-in-flight back-pressure | ✅ | Closed 2026-07-14. One real, shared ID3D12Fence + monotonically increasing nextFenceValue_ counter + one recorded fence value per frame index (frameFenceValues_[kFramesInFlight]) — the actual back-pressure state. SignalAndWaitForFrameEXT(frameIndex) signals a NEW value for that frame slot but only blocks on that slot's PREVIOUS recorded value (a real Present() loop must never stall the CPU on the frame it just submitted — that would defeat the entire purpose of frames-in-flight); a separate ExecuteCommandListAndWaitEXT gives a simple synchronous-wait primitive for tests/simpler callers. A genuine bug was found and fixed IN THIS TASK'S OWN TEST, not the implementation: an early draft of D3D12_Smoke's Check E asserted GetCompletedValue() >= v2 (the value just signaled) immediately after the call returned — a real, empirically-observed intermittent CTest failure (passed most runs, failed at least once), because Signal() is asynchronous and the primitive's own documented contract never promised the just-signaled value had completed yet. Fixed by asserting the actually-guaranteed invariant (>= v0, the PREVIOUS value for that frame slot, which the call's own wait genuinely blocks on) plus a separate, explicit follow-up wait to prove eventual completion of v2 — 3 consecutive real ctest -R D3D12 runs after the fix, 100% pass each time. |
| DX-106 | Resource barriers: explicit D3D12_RESOURCE_BARRIER transitions for every render-target/texture state change this backend needs (present↔render-target, shader-resource↔render-target, etc.) | ✅ | Closed 2026-07-14. D3D12ResourceStateTracker (include//src/CNA/Internal/Backends/D3D12/D3D12ResourceStateTracker.hpp/.cpp) — a std::map<ID3D12Resource*, D3D12_RESOURCE_STATES> that emits a transition barrier via ResourceBarrier() only when a requested state genuinely differs from the tracked one, and throws if asked to transition a resource that was never registered (forces every caller to declare a real starting state, the actual fix for "ad-hoc barrier calls" the row's own note warns about). Proven against a real throwaway ID3D12Resource (a committed buffer, since DX-109's real render-target/texture resources don't exist yet — this class is content-agnostic by design, so that's a legitimate, sufficient proof target, not a shortcut): D3D12_Smoke Checks G1–G7 show a real barrier fires on a genuine COMMON→COPY_DEST→GENERIC_READ transition chain, NO barrier fires on a repeat COPY_DEST→COPY_DEST request (the actual point of the class, proven via the real bool return, not just documented), and the recorded command list (containing the real emitted barriers) submits and executes cleanly through Wine+vkd3d-proton. |
| DX-107 | Pipeline state objects (PSOs): one per (shader variant, input layout, blend/depth/rasterizer state combination) — reuse D3DCommon's DX-12-state mapping tables and, as a bootstrap, DX-13-hlsl/hlsl_shaders.hpp's DXBC bytecode (same source as D3D11, design decision 5) — PSOs accept DXBC directly, no DXIL requirement to get a first D3D12 draw working | ✅ | Closed 2026-07-14 — the first real ID3D12PipelineState this backend has ever created. D3D12PipelineStateCache (D3D12PipelineStateCache.hpp/.cpp) builds a full D3D12_GRAPHICS_PIPELINE_STATE_DESC from D3DShaderCache's checked-in DXBC (same bytes D3D11 uses, per design decision 5), a new D3DVertexFormatHelper::InputElementsForStrideD3D12() (D3D12 has no ID3D11InputLayout-equivalent object at all — the input layout is baked directly into the PSO desc, so this is a pure data lookup, not a cache, added alongside this task since DX-16-vtx's own row explicitly deferred writing it until "Phase DX12 needs it"), and D3DStateMapping's D3D11-prefixed enums static_cast to their D3D12 equivalents (re-spot-checked D3D11_CULL_MODE/D3D12_CULL_MODE and D3D11_BLEND/D3D12_BLEND against this machine's real headers while implementing this task — still numerically identical, confirms DX-12-state's own claim holds for the actual header versions Phase DX12 builds against, not just assumed to carry over). Caching/hashing strategy, decided and documented in the header: one PSO per full (shader variant, stride, blend 6-tuple, depth 3-tuple, rasterizer 2-tuple, RTV/DSV format) key — the "PSO explosion" tradeoff this row's own Notes flagged as a real design question, accepted here as the correct first-implementation strategy since a D3D12 PSO bakes all of this into one indivisible object (no way to cache blend/depth/rasterizer independently and combine at bind time the way D3D11's OMSetBlendState/etc. do). Honest, documented gap: stencil state and scissor-enable are NOT yet part of the desc/key — deliberately deferred, not silently dropped. Real proof: D3D12_Smoke Check I1 creates a genuine PSO for colored3d/stride-16/default state via CreateGraphicsPipelineState through Wine+vkd3d-proton on the real GPU; I2/I3 prove cache-hit identity (identical desc → same pointer) and cache-miss correctness (a different cullMode → a real, different PSO object). |
| DX-108 | Root signatures: constant-buffer/SRV/sampler binding layout, one per shader-variant family (reuses D3D11's own D3DPerDrawConstants/D3DLightingConstants/D3DBoneConstants struct layouts from DX-60/DX-60a, not reinvented) | ✅ | Closed 2026-07-14. D3D12RootSignatureCache (D3D12RootSignatureCache.hpp/.cpp) explicitly reuses D3DCommon's existing constant-buffer byte layouts unchanged (design decision 4's whole point) and only declares the binding shape: root CBV descriptors at b0..b(n-1) with D3D12_SHADER_VISIBILITY_ALL (confirmed by reading the real HLSL that this project's stock shaders share PerDraw/FogParams-style cbuffers between the VS and PS stages, so a single ALL-visibility root CBV per register genuinely satisfies both), plus — only when needed — one SRV descriptor table (t0.., PIXEL-visibility) and D3D12_STATIC_SAMPLER_DESC entries (s0..) instead of a real SAMPLER descriptor heap (a deliberate, documented scope choice — DX-103 never built a SAMPLER heap, and every stock effect's sampler state is fixed at shader-authoring time anyway; genuinely dynamic per-draw sampler state is real, separately-scoped follow-up work, not silently foreclosed). Cached by (numCbvs, numSrvs, numSamplers) binding shape — any two shader-variant families with the same shape genuinely share one root signature. Real proof: D3D12_Smoke Check H1 creates a real root signature for colored3d's (2,0,0) shape via D3D12SerializeVersionedRootSignature+CreateRootSignature through Wine+vkd3d-proton; H2 proves cache-hit identity, H3 proves a genuinely different (2,1,1) shape (matching textured3d's family) returns a real, different object. |
| DX-109 | Vertex/index buffers, textures, render targets — same resource content as D3D11's DX-30–DX-45, but through ID3D12Resource/CreateCommittedResource + explicit upload-heap staging instead of D3D11's implicit driver-managed uploads | ✅ | Fully closed — every piece this row originally deferred (render targets, cube textures, 3D textures) was closed by later tasks the same day. Render targets: DX-117 (Phase DX13, D3D12RenderTargetBackend/D3D12RenderTargetCubeBackend, real bind/clear/readback/MRT, later extended with real mip-chain generation via DX-144). Cube textures: this row's own later update below (env_map3d/DX-111). 3D textures: DX-122 (D3D12Texture3DBackend). Nothing this row originally scoped remains open — see each named task's own row for full detail. Original 2026-07-14 closure note, preserved below. Vertex/index buffers + 2D textures closed 2026-07-14, real and GPU-proven; render targets and cube/3D textures deliberately NOT attempted this pass — honest triage, not a silent gap. D3D12VertexBufferBackend/D3D12IndexBufferBackend (D3D12Buffers.hpp/.cpp) and D3D12TextureBackend (D3D12Textures.hpp/.cpp) each: a D3D12_HEAP_TYPE_DEFAULT GPU-resident resource (grows, never shrinks, mirroring D3D11VertexBufferBackend's own capacity policy), a fresh D3D12_HEAP_TYPE_UPLOAD staging resource per upload (Map/memcpy/Unmap — upload heaps are always CPU-writable, unlike DEFAULT-heap resources), a CopyBufferRegion/CopyTextureRegion on the backend's shared command list, and D3D12ResourceStateTracker (DX-106) driving the COPY_DEST↔GENERIC_READ/{PIXEL_SHADER_RESOURCE\|NON_PIXEL_SHADER_RESOURCE} transitions — all synchronous (ExecuteCommandListAndWaitEXT), a deliberate, documented simplification since nothing above this layer (Present()/draws) is real yet either, so there's no per-frame-throughput requirement to design against yet. Texture uploads use a row-pitch-aligned (D3D12_TEXTURE_DATA_PITCH_ALIGNMENT, 256B) staging BUFFER + D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT, not a staging TEXTURE2D — D3D12's own requirement for buffer→texture copies. d3dx12.h (which normally provides D3D12CalcSubresource()) is absent from this MinGW-w64 install (DX-100's own finding) — subresource indices computed directly instead (the general formula collapses to the mip level itself for this backend's array-size-1, single-plane textures, so no general helper was needed). Also overrode CreateIndexBuffer32() explicitly and added a dedicated real test for it (IGraphicsBackend's own default silently delegates to CreateIndexBuffer16 — the exact real bug D3D11's own Phase DX5 fork found and fixed; overriding it here from the start avoids repeating that bug in D3D12). Real proof: D3D12_Smoke grew 31→48/48 checks — new Checks J (vertex buffer + both 16-/32-bit index buffers, including via the real IGraphicsBackend::CreateIndexBuffer32() factory path, each round-tripping EXACT bytes through a real D3D12_HEAP_TYPE_READBACK buffer, the off-screen-safe D3D12 equivalent of D3D11's own DX-28-style staging readback) and K (a D3D12TextureBackend created via the real IGraphicsBackend::CreateTexture() path round-trips its construction-time upload exactly, and a follow-up UpdatePixels() call is proven to genuinely overwrite it — a real byte-different readback, not a stale/cached first-upload value). Honest scope boundary: D3D12RenderTargetBackend/3D-texture variants (D3D11's own DX-43/DX-42 equivalents) were explicitly triaged out of this pass per this row's own "prioritize buffers + 2D textures first, they're DX-111's actual prerequisite" allowance — real, scoped follow-up work, not silently dropped. Update 2026-07-14: a real D3D12TextureCubeBackend (D3D11's own DX-41 equivalent) was added after all, by DX-111 itself when env_map3d needed one — see that row's own Notes for its real shape (GetData() left at the interface default, an honest, narrower gap than D3D11's own real readback). Render targets and 3D textures remain the only genuinely unstarted pieces of this row. |
| DX-110 | Device-removed recovery: ID3D12Device::GetDeviceRemovedReason, a real recreate-everything path (this is a case D3D11 backends often skip; D3D12 documentation treats it as expected to handle) | 🟨 | Detection + a genuine, tested recreation path closed 2026-07-14; the real removal TRIGGER remains untestable on this dev loop — the same honest constraint D3D11's own DX-27 hit, documented the same way, not silently claimed complete. CheckDeviceRemovedEXT(HRESULT) mirrors D3D11GraphicsBackend::CheckDeviceRemoved's own detection-only convention exactly (checks for DXGI_ERROR_DEVICE_REMOVED/DXGI_ERROR_DEVICE_RESET, logs the real GetDeviceRemovedReason(), never throws or recovers on its own) — wired into both ExecuteCommandListAndWaitEXT()/SignalAndWaitForFrameEXT() at their own ID3D12CommandQueue::Signal() failure sites, the two real places a removal could currently surface (no Present() exists yet to be the third). Unlike D3D11's own DX-27 (which explicitly deferred full recovery to DX-90's real hardware), this task also implements the real recreation itself: RecreateDeviceEXT() tears down and rebuilds every device-lifetime resource DX-102–DX-105 created (device, factory, command queue, all 3 descriptor heaps with bump allocators reset to 0, every per-frame command allocator + the shared command list, the fence + its counters), clears the shared D3D12ResourceStateTracker (every previously-tracked resource's D3D12 object is gone with the removed device), and re-attempts the swap chain if a window was supplied. A real bug was found and fixed in this task's own test, not the implementation: an early draft of Check L asserted GetDeviceEXT() != deviceBefore (pointer inequality) as proof of genuine recreation, and this genuinely FAILED on a real run — root cause: device_.Reset() is a real COM Release() before the new device is created, and the allocator legally reusing that just-freed address for the very next allocation is expected behavior, not evidence recreation silently no-op'd; pointer identity is simply not a sound signal here. Fixed by asserting the actual, sound proof instead: non-null objects after recreation (Check L2–L4) PLUS functional proof that new GPU work genuinely submits and round-trips through whatever now backs the recreated objects (Check L5: a fresh command-list submission through the new queue/fence; Check L6: a brand-new D3D12VertexBufferBackend created after recreation uploads and reads back exact bytes through the new device) — if RecreateDeviceEXT() had silently no-op'd or left a half-torn-down device, L5/L6 would fail regardless of pointer addresses. Honest, documented gap, matching DX-27's own row's tone: this dev loop cannot trigger a genuine DXGI_ERROR_DEVICE_REMOVED to prove the trigger-detection path fires for real (only RecreateDeviceEXT()'s own recovery logic was directly, deliberately invoked and proven functional) — DX-114's real hardware is the genuine test for that, exactly as DX-90 already is for D3D11's equivalent gap. |
| DX-111 | Port the same shader/effect variant set D3D11 lands in Phase DX8, reusing D3DCommon's HLSL sources and DXBC bytecode as the starting point (design decision 5) — treat this as the compatible bootstrap it is, not a claim that D3D11's exact shader binaries are D3D12's permanent, final shader system | ✅ | colored3d (stride 16) closed 2026-07-14, real and GPU-pixel-proven — the first real D3D12 3D triangle. DrawColoredPrimitives()/DrawIndexedColoredPrimitives() (D3D12GraphicsBackend.cpp) now genuinely: get/create a (2,0,0)-shape root signature (DX-108) and a colored3d/stride-16 PSO (DX-107), populate real D3DPerDrawConstants/D3DFogConstants (DX-60, byte-identical to D3D11's) into persistently-mapped D3D12_HEAP_TYPE_UPLOAD constant buffers (the standard D3D12 dynamic-CB idiom — map once, memcpy before each draw, safe because every draw here still submits synchronously via ExecuteCommandListAndWaitEXT()), and record a real OMSetRenderTargets/RSSetViewports/IASetVertexBuffers(+IASetIndexBuffer)/SetGraphicsRootConstantBufferView×2/DrawInstanced(or DrawIndexedInstanced) sequence. Since the swap chain is unusable under Wine (DX-100), draws target a new, deliberately minimal BindOffscreenColorTargetEXT() NOXNA helper (a raw ID3D12Resource+RTV the test creates and registers with the resource tracker itself) rather than a full public D3D12RenderTargetBackend (still owed, same honest gap DX-109 already flagged) — Clear() was also wired for real against this same off-screen binding, since the pixel-readback proof needs a known background color. A real, silent-failure bug was found and fixed via this task's own first attempt, not assumed away: the PSO's default state (depthEnable=true with no DSV bound, and XNA's real CullCounterClockwiseFace default) produced a DrawInstanced() call that returned successfully but painted nothing — no debug layer is available on this Wine+vkd3d-proton dev loop (DX-102's own already-documented gap) so nothing reported an error; root-caused empirically by reading back the exact same pixel region before/after the draw and finding it unchanged, then bisecting via depthEnable=false (no effect) and cullMode=CullMode::None (fixed it) — confirms the test triangle's real winding was being back-face-culled after D3D's NDC→screen-space Y-flip. cullMode is hardcoded to None for this narrow bootstrap path (no D3D12 rasterizer-state-cache equivalent to DX-52 exists yet — real, scoped follow-up), documented in-code, not silently patched around. D3D12_Smoke grew 48→51/51 checks (Check M) for this sub-slice. Extended further, same day: textured3d/colored_textured3d/lit_textured3d/alpha_test3d also closed 2026-07-14, real and GPU-pixel-proven — 5 of 10 stock variants now real. New D3D12GraphicsBackend::DrawPrimitivesEx()/DrawIndexedPrimitivesEx() overrides (DrawPrimitivesExImpl) replicate D3D11's own DrawPrimitivesExImpl priority-chain shape (alpha-test > lit-textured (stride 32) > colored/textured/colored_textured bundle by stride) — without these overrides, IGraphicsBackend's own default silently falls back to the stride-16-only DrawColoredPrimitives, which would throw for every one of these variants. dual_texture3d/env_map3d/skinned3d (params.dualTexture/envMapping/skinned) explicitly throw a named "not yet implemented" rather than silently drawing the wrong shader — real, scoped follow-up work, not an oversight. Texture binding: D3D12TextureBackend's own SRV (already allocated in the shared CBV/SRV/UAV heap at texture-creation time, DX-109) is bound directly as a real SetGraphicsRootDescriptorTable's 1-descriptor table base — no separate descriptor-copy/consolidation step needed for a single texture, since the descriptor already lives in the exact heap SetDescriptorHeaps binds. Root-signature shapes: alpha_test3d needs its own (1,1,1) (single combined PerDraw@b0, no separate FogParams); textured3d/colored_textured3d/lit_textured3d all share one (2,1,1) shape (and therefore one cached root-signature object, confirmed via the cache's own real object-identity behavior) — landing lit_textured3d for real caught and corrected a stale speculative claim in D3D12RootSignatureCache.hpp's own doc comment (written ahead of this task, before anyone had actually read lit_textured3d.frag.hlsl), which had guessed (2,0,0) for it; the real HLSL does declare its own t0/s0 texture binding, identical in shape to textured3d's. No new bugs found in this extension — all 5 new checks (N/O/P) passed on the first real Wine+vkd3d-proton run. D3D12_Smoke grew 51→61/61 checks: textured3d/colored_textured3d get the same exact-color / exact-vertex-color-tint proof D3D11's own Check Q used (including the indexed-path parity check); lit_textured3d's unlit branch is byte-exact, its lit branch is proven to genuinely differ from both the unlit result and the background (real Blinn-Phong math, not CPU-replicated); alpha_test3d's clip() is proven to genuinely discard a failing pixel (background survives) and draw the exact color, including a non-255 alpha byte, on a passing one. Still honestly deferred (5 of 10 variants): dual_texture3d, env_map3d, skinned3d, sprite2d, instanced3d — real, scoped follow-up work, not silently dropped; DX-112 (SpriteBatch) still needs sprite2d first.Extended again 2026-07-14: dual_texture3d and sprite2d also closed, real and pixel-verified — 7 of 10 stock variants now real. Real bug fixed along the way: a shared 2-descriptor root table (one range, NumDescriptors=2, populated via a fresh per-draw CopyDescriptorsSimple) sampled as all-zero under this dev loop's Wine+vkd3d-proton, even though every CPU-side descriptor write was independently verified correct — switched D3D12RootSignatureCache to N separate single-descriptor tables (one root param per texture register) instead, which fixed it immediately and is simpler code besides (see that file's own updated doc comment for the full empirical writeup, and the top-of-file status banner for the complete before/after). sprite2d itself needed its own hand-built PSO/input-layout, deliberately bypassing the shared stride-keyed D3D12PipelineStateCache (same real stride-32 collision with VertexPositionNormalTexture D3D11's own DX-70 fork already found). D3D12_Smoke grew 61→68/68 checks. Still honestly deferred (3 of 10 variants): env_map3d (needs a new D3D12TextureCubeBackend), skinned3d (needs a 3rd root CBV for bone data), instanced3d (needs a 2-stream instanced input layout). Finished 2026-07-14: skinned3d and instanced3d also closed, real and pixel-verified — 9 of 10 stock variants now real. skinned3d reuses the existing (numCbvs,numSrvs,numSamplers)-keyed D3D12RootSignatureCache/D3D12PipelineStateCache unchanged (a (3,1,1) shape: PerDraw@b0 + BoneBlock@b1 + skinned3d's own FogParams-equivalent @b2, stride 52 — D3DVertexFormatHelper::InputElementsForStrideD3D12 already covered stride 52 from DX-107, no new input-layout work needed there); GetOrCreateBoneConstantBufferEXT()/GetOrCreateSkinnedExtraConstantBufferEXT() follow the exact same persistent-UPLOAD-heap-buffer convention every other variant already established, and the CPU-side field population (D3DPerDrawConstants/D3DBoneConstants/D3DSkinnedExtraConstants) is a direct, unmodified port of D3D11GraphicsBackend::DrawPrimitivesExImpl's own needsSkinned branch — including reusing its already-established finding that GpuDrawParams::boneTransforms is a straight memcpy, not a per-matrix transpose. instanced3d needed its own hand-built ID3D12PipelineState/D3D12_INPUT_ELEMENT_DESC[] (new GetOrCreateInstancedPsoEXT()), for the same reason sprite2d did — a genuinely different 2-input-slot layout (POSITION0 per-vertex @ slot 0, INSTANCEWORLD0-3 per-instance @ slot 1, stride 64) that D3DVertexFormatHelper's single-stream stride-keyed helper can't express — mirroring D3D11GraphicsBackend::GetOrCreateInstancedInputLayoutEXT()'s own element list exactly, and a new DrawInstancedPrimitivesEx() override (IGraphicsBackend's own default just throws) using a (1,0,0) root-signature shape (PerDraw@b0 only — instanced3d.frag.hlsl's own real declaration is genuinely textureless, confirmed by reading the HLSL, not assumed). No new bugs found in this extension — both new checks (S/T) passed on the first real Wine+vkd3d-proton run, reusing the exact vertex/GpuDrawParams fixtures D3D11's own equivalent Check V/Check W (d3d11_smoke_test.cpp) already established (single-identity-bone texture-sample proof for skinned3d; saturated-0/1 per-instance-DiffuseColor proof for instanced3d — chosen so both checks are byte-exact, not approximate). D3D12_Smoke grew 68→72/72 checks. DX-111 is now 9 of 10 stock variants real — only env_map3d remains deferred, genuinely blocked behind a new D3D12TextureCubeBackend (no D3D12 cube-texture support exists yet, DX-109's own honest triage), a real, scoped follow-up task, not an oversight — this row stays 🟨, not ✅, until that lands. DX-112 (SpriteBatch) is unaffected by this update (already closed against sprite2d). Closed for real 2026-07-14: env_map3d landed, real and GPU-pixel-proven — DX-111 is now 10 of 10 stock shader variants real across all 4 CNA graphics backends (EasyGL, Vulkan, Bgfx, D3D11) plus this new D3D12 one. Needed a new D3D12TextureCubeBackend (include/src/CNA/Internal/Backends/D3D12/D3D12TextureCube.hpp/.cpp) — a real 6-face ID3D12Resource (DepthOrArraySize=6, single-plane, D3D12_SRV_DIMENSION_TEXTURECUBE), SetData() uploading one face's sub-rectangle for real via the same explicit upload-heap-staging discipline D3D12TextureBackend already established (GetData() left at ITextureCubeBackend's own default no-op — a genuine, honest scope gap, not silently claimed equivalent to D3D11's real readback, since the pixel proof for this task reads the render target back, not the cube texture itself). Root-signature/PSO shape: env_map3d reuses the exact (3,2,2) shape dual_texture3d already created and cached (2 CBVs — D3DEnvMapPerDrawConstants@b0 + D3DEnvMapConstants@b2, b1 bound to a valid-but-unread dummy address, same convention dual_texture3d's own branch established; 2 SRVs as 2 separate single-descriptor tables, t0 base Texture2D + t1 TextureCube) — genuinely shared, not a new root-signature object. CPU-side field population is a direct, unmodified port of D3D11GraphicsBackend::DrawPrimitivesExImpl's own needsEnvMap branch. D3D12_Smoke grew 72→74/74 checks (Check U): the same geometrically-constrained-reflection test methodology D3D11's own DX-66/Check U uses (camera placed so the reflection direction lands deep inside one distinctly-colored cube face, D3D12's native slice order matching D3D11's +X,-X,+Y,-Y,+Z,-Z) — a real, discriminating byte-exact match, not an approximate one. A real, unrelated bug was found and fixed along the way: the RTV descriptor heap's fixed bump-allocator capacity (DX-103, deliberately simple, 8 slots) was genuinely exhausted the moment this task's own new render-target allocation ran — the growing CTest suite had already claimed all 8 slots across Checks M–T, with no free-list reuse yet (an honest, documented DX-103 simplification, not a bug in DX-103 itself) — raised to 32 with an in-code note explaining why, not silently worked around. Also fixed a real, separate pre-existing gap in RecreateDeviceEXT() (DX-110): boneConstantBuffer_/skinnedExtraConstantBuffer_/instancedPso_ were never reset there even though they're just as device-tied as every other cached buffer/PSO — found while extending that same function for the new envMapPerDrawConstantBuffer_/envMapConstantBuffer_, fixed in the same pass rather than left for a later session to rediscover. D3D11 rebuilt clean throughout (no shared D3DCommon files touched this time — only D3D12-local files + the D3D12-only CMakeLists.txt branch). |
| DX-112 | SpriteBatch, matching D3D11's DX-70–DX-72 | ✅ | Closed 2026-07-14, real and pixel-verified — sprite2d landed first (as this row's own directive required), then this. New D3D12SpriteBatchBackend (D3D12SpriteBatch.hpp/.cpp) reuses D3D11SpriteBatchBackend's exact destination/source-rect/origin/rotation/SpriteEffects-flip quad-building formula (CPU-side XNA geometry math, unchanged) and a real hand-built sprite2d PSO/input-layout (deliberately bypassing the shared stride-keyed D3D12PipelineStateCache/D3DVertexFormatHelper, since sprite2d's 32-byte Sprite2DVertex collides with VertexPositionNormalTexture's own existing stride-32 meaning — the same real collision D3D11's own DX-70 fork found and worked around). Root signature is genuinely shared with alpha_test3d's own (1,1,1) object (a root signature only describes binding slots, not cbuffer contents). Real pixel-readback proof (Checks Q–R, D3D12_Smoke 61→68/68): exact quad placement, and a genuine SpriteEffects::FlipHorizontally proof — a real bug was found and fixed while writing this test itself (not production code): the first attempt's expected-color assertion assumed UV = px/width for its readback coordinates, but D3D rasterizes at pixel centers (UV = (px+0.5)/width), so the LINEAR-filtered static sampler blended in a small amount of the neighboring texel; fixed by widening the test's source texture so each color spans 2 texels instead of 1, making the readback points immune to the exact sub-pixel alignment (blending same-color-with-same-color is exact regardless of weight). Honest, documented scope gaps (not silently dropped): SpriteBatch::Begin(effect) (D3D11's own DX-71) throws a named error if actually flushed — no D3D12 equivalent of D3D11EffectBackend/DX-58 (runtime D3DCompile()-driven custom effects) exists yet for D3D12; SetSamplerFilter/SetSamplerAddressMode (D3D11's own DX-72) are stored but not yet behaviorally real, since no D3D12 dynamic-sampler-state system (a Phase-DX7 equivalent) exists yet either — the root signature's static sampler stays fixed at LINEAR filter/WRAP address mode. |
| DX-113 | Tests: same shape as DX-80–DX-84, plus D3D12-specific cases (barrier-transition correctness, fence/frame-in-flight back-pressure, device-removed recovery) | ✅ | Closed 2026-07-14 for everything genuinely testable on this dev loop — audited what DX-102–111's own landing forks already built informally (examples/d3d12_smoke_test.cpp Checks A–U, 74 checks) and closed 2 real gaps found, rather than assuming a blank slate. DX-81-equivalent audit: all 10 stock variants + SpriteBatch already had genuine off-screen pixel-readback coverage (Checks M–U), but — the exact same gap DX-81's own D3D11 audit found — fog on/off had never been independently exercised; closed by new Check V (colored3d bundle via DrawPrimitivesEx, same FogStart=0/FogEnd=0.5-lands-exactly-on-fogFactor=0 fixture as D3D11's own Check AC, same DXBC bytecode underneath — fogEnabled=false leaves the exact vertex color (V1), fogEnabled=true blends all the way to the exact FogColor (V2)). Barrier-transition correctness (D3D12ResourceStateTracker, Check G) had real redundant-barrier-suppression and multi-step-transition coverage already (G1–G7) but was missing its own documented throw contract — never-tracked-resource calls to TransitionTo()/GetTrackedStateEXT() are documented in the header to throw std::runtime_error, untested; closed by new Checks G8–G10 (a second, deliberately never-TrackResource()'d buffer; both calls genuinely throw). Device-removed recovery (DX-110's own Check L1–L6) was already complete on audit, no further action needed. DX-84-equivalent mutation test, on Check M (DrawColoredPrimitives, the first real D3D12 triangle): mutated D3D12GraphicsBackend.cpp's non-indexed path (line 773) perDraw.VertexColorEnabled from 1.0f to 0.0f, rebuilt, reran through Wine+vkd3d-proton — exactly Check M1 failed (79/80), every other check including the indexed M2 — a separate code path, correctly untouched by this mutation — still passed; reverted, rebuilt, reconfirmed 80/80. DX-85-equivalent engagement gate (scripts/run-wine-vkd3d.sh's real vkd3d-proton - applicationVersion: log check, CNA_D3D12_SKIP_VKD3D_GATE opt-out) was already built as part of DX-102–105's own landing task — confirmed still correctly wired into the one registered D3D12 CTest (D3D12_Smoke; the window-attached swap-chain diagnostic is deliberately never registered as a CTest, per DX-102's own row). Both of this row's originally-flagged gaps are now closed, one by a later task, one directly by this follow-up (2026-07-14): (1) DX-82-equivalent state-object pixel-behavior tests were "not applicable yet" at the time this row was first written, waiting on "a future D3D12 rasterizer/blend/depth-stencil-state-cache task" — that task landed as DX-118 (real ApplyBlendState/ApplyDepthStencilState/ApplyRasterizerState, Checks X0–X5/Y0–Y4, genuine pixel-behavior proof), but this row's own text was never updated to reflect it, the same stale one-way cross-reference this session already found and fixed for DX-29/DX-100/DX-102. (2) Check E's fence/frame-in-flight back-pressure proof (E1–E3) proved the value-ordering/wait-completion contract but not that WaitForSingleObject genuinely stalls the CPU under real GPU load — closed by new Checks E4/E4pre/E4a: a real ~3 GB CopyBufferRegion workload (48× 64 MB DEFAULT-heap-to-DEFAULT-heap copies) is queued asynchronously, then two back-to-back SignalAndWaitForFrameEXT(0) calls are timed — a "control" call whose wait target was already satisfied (queued before the copy work) versus a "load" call whose wait target was queued after the copy work and therefore cannot complete until the GPU actually drains it. A relative comparison (load > 3×control or load > 2ms), not a fragile absolute-millisecond threshold — measured consistently ~90ms load vs ~0–1µs control across 4 repeated real Wine+vkd3d-proton runs, an unambiguous, non-flaky margin. D3D12_Smoke grew 193→196/196 checks, confirmed via a real ctest -R D3D12_Smoke run. |
| DX-114 | Real-Windows verification checklist, same shape as DX-90 plus DXR/ray-tracing feature-level detection if that ever becomes a project goal (explicitly out of scope for v1 — see "Why these backends" above) | ⬜ | 2026-07-14: same .github/workflows/d3d-windows-ci.yml job also covers D3D12's own MSVC-compile/unit-test/shader-generation-check subset (matrix leg alongside D3D11 — see DX-90's own row for the full description, including the honest "not yet validated against a real Actions run, no gh auth in this environment" caveat, which applies identically here). Does not close this row — real swap-chain/tearing/device-lost/driver-parity verification on an actual driver still needs real Windows hardware. Note: D3D12's own local swap-chain crash under Wine+vkd3d-proton is now fixed (DX-102's own row, 2026-07-14 update — a properly Proton-managed launch via scripts/run-proton-vkd3d.sh makes CreateSwapChainForHwnd succeed for real locally); this row is unaffected by that fix since it was never gated on the crash specifically — real-hardware present-mode/tearing/device-lost behavior is a different, still-open verification surface. |
| DX-115 | docs/d3d12-backend.md + feature-matrix column + README updates, mirroring DX-95–DX-97 | ✅ | Closed 2026-07-14 — the last open task in Phase DX12 (DX-114 remains, correctly needs_human). New docs/d3d12-backend.md (status/scope, Wine+vkd3d-proton dev-loop setup incl. the Steam-Proton-sourced d3d12.dll/d3d12core.dll + dedicated ~/.wine-cna-d3d12 prefix, how to write a test, a full honest "Known limitations" list led by the swap-chain crash, not buried). docs/graphics-backend-feature-matrix.md grew a real D3D12 column across all 7 applicable tables, mirroring D3D11's own DX-96 column but independently re-derived per row from plan_dx.md's actual DX-100–DX-113 Notes (not copy-pasted from D3D11's cells) — several rows are honestly ⬜ where D3D11 is ✅/🟨 (no render-target backend, no Texture3D, no state-object application, no per-slot SamplerState, no occlusion queries, no custom-Effect-via-SpriteBatch), reflecting D3D12's real, narrower current scope rather than assuming parity. README.md gained a D3D12 bullet in the feature list, a "Build (Windows cross-compilation — D3D12 backend)" section mirroring D3D11's own, and a D3D12 row in "Tested Compilers". Confirmed exact current numbers via a real run, not copied from an earlier Notes entry: cmake --build cmake-build-d3d12 --target CNA succeeds; ctest --test-dir cmake-build-d3d12 -R D3D12 -V → D3D12_Smoke, exactly 80 [PASS] lines, RESULT: ALL PASS: 0 failure(s), 100% tests passed — matches DX-113's own closing number, still accurate. Only one CTest binary is registered for D3D12 (D3D12_Smoke) — there is no D3D12_Common-equivalent pure-function suite yet (unlike D3D11's D3D11_Common). CnaTests itself now builds and links under this cross-target too (DX-15, closed 2026-07-14, fix applied equally to both D3D backends since it lives in shared tests/ sources) — not registered as its own CTest binary here, just confirmed directly via CnaTests.exe. Test groups that don't create a live GraphicsDevice window run and pass under plain Wine (7/7 confirmed); groups that do hit this phase's own already-documented plain-Wine swap-chain limitation (DX-100/DX-102, needs the Proton launch path) — not a new gap. Phase DX12 is now essentially complete for what this Debian dev environment can prove: DX-100 through DX-113 and DX-115 are all closed (several 🟨 with real, specific, honestly-documented gaps — swap-chain presentation chief among them); only DX-114 (real Windows hardware) remains, correctly needs_human. |
(recorded 2026-07-14, consolidating what was previously scattered across individual task Notes and
docs/graphics-backend-feature-matrix.md, so it's answerable from this one file without cross-
referencing dozens of rows)
Neither D3D11 nor D3D12 is at EasyGL parity. D3D11 is close; D3D12 is meaningfully behind — not
just "less tested," but missing entire subsystems. Both share one blanket caveat on top of
everything below: every check on both backends was run through Wine (DXVK for D3D11,
vkd3d-proton for D3D12) on this Debian machine's real GPU, never on real Windows — DX-90/DX-91
(D3D11) and DX-114 (D3D12) are the actual completion gates, both needs_human, and nothing below
changes that.
D3D11 — close to EasyGL, real gaps are "implemented but not independently verified," not missing code
- Core pipeline (all 10 stock shader variants),
SpriteBatch, customEffect, all 3 state-object types (blend/depth-stencil/rasterizer), MRT, MSAA, occlusion query — all real, pixel-verified. DirectionalLight1/2,EmissiveColor, and real specular highlights (SpecularColor/Power) are wired into the HLSL/D3DLightingConstantsbut not independently pixel-tested (the lit-pixel test zeroes specular for CPU-comparison determinism, and only proves single-light lit-vs-unlit differs, not a discriminating multi-light case).- Mip levels > 0,
SpriteFont,Model/content-pipeline loading,RenderTargetCube(vs. the more thoroughly testedRenderTarget2D), and per-instanceDepthStencilFormatfidelity are not separately tested against this backend at all. DX-21(debug-layer-missing fallback) andDX-27(device-removed trigger, as opposed to the real, tested recovery logic) are implemented but never independently exercised — this machine's Wine+DXVK setup always satisfies the debug-layer request and never triggers a real device removal, so these specific branches are honest, environment-limited gaps, not missing code. OnlyDX-90on real Windows can actually exercise them.
- All 10 stock shader variants +
SpriteBatchare real and pixel-verified off-screen only (see swap-chain section below). - No
D3D12RenderTargetBackendexists at all. Off-screen draws go through a minimal, test-onlyBindOffscreenColorTargetEXT()helper, not a realIRenderTargetBackend. The actual XNARenderTarget2D/RenderTargetCube/MRT API does not work against this backend yet. - No state-object system exists. D3D12 bakes blend/depth-stencil/rasterizer state into each PSO
description rather than exposing separate runtime-settable objects; every PSO in this backend
currently hardcodes
depthEnable=false/cullMode=None. There is noBlendState/DepthStencilState/RasterizerState→ PSO-desc-key mapping —DX-113's own audit confirmed this is a genuinely unstarted future task, not a coverage gap in an existing one. - No per-slot
SamplerStatesystem. Samplers are a single hardcoded staticD3D12_FILTER_MIN_MAG_MIP_LINEAR+WRAPdescriptor baked into the root signature (D3D12RootSignatureCache::MakeDefaultStaticSampler) — not driven by XNASamplerStateat all. - No occlusion query support —
CreateOcclusionQuery()falls through toIGraphicsBackend's own silentnullptrdefault; Phase DX12's task list has noID3D12Query-based task at all yet. - No custom
ShaderEffect(no D3D12 equivalent ofD3D11EffectBackend's runtimeD3DCompile()path) and no customEffectviaSpriteBatch::Begin(effect)(D3D11's ownDX-71) —D3D12SpriteBatchBackendonly draws through the stocksprite2dpipeline. - No
Texture3Dbackend at all (explicitly triaged out,DX-109);TextureCubehas realSetData()(needed byenv_map3d) butGetData()is left at the interface's silent no-op default — narrower than D3D11's real cube-texture readback. - Fog is dedicated-tested only for the
colored3dbundle (Check V) — narrower than D3D11, where the same gap was found and closed forcolored3dspecifically but the other 7 fog-capable variants' fog-constant-buffer wiring is not independently confirmed on either backend.
Every D3D12 draw/resource/pipeline check in this plan runs off-screen, because
CreateSwapChainForHwnd genuinely cannot be made to work on this specific machine's Wine setup.
This was investigated twice (DX-100's original spike, DX-102's later attempt through the real
production code path) with concrete, reproducible evidence, not a guess:
D3D12CreateDevice,ID3D12CommandQueue, descriptor heaps, command lists, and fences all work correctly through Wine + vkd3d-proton (v3.1.0, sourced from this machine's Steam "Proton - Experimental" install, run in a dedicated~/.wine-cna-d3d12prefix) — real feature level 12_1, DXR 1.1, and Shader Model 6.8 negotiated against the real GPU (AMD Radeon 780M/RADV). This proves the device itself is fine; the failure is specific to presentation.CreateSwapChainForHwndwithDXGI_SWAP_EFFECT_FLIP_DISCARDcrashes with a genuine page fault. A live WineDbg attach produced a full, symbolized backtrace: a null-pointer read inside Wine's owndxgi.dll, specificallyvkd3d_instance_get_vk_instance(instance=0000000000000000), called fromd3d12_swapchain_init(dlls/dxgi/swapchain.c:3287). PlainDXGI_SWAP_EFFECT_DISCARDfails cleanly instead of crashing (DXGI_ERROR_INVALID_CALL), which is a useful secondary data point: the failure is specific to handing a D3D12 command queue through the swap-chain path, not DXGI/device creation in general.- Root cause: this machine's system Wine install has vkd3d-proton's
d3d12.dll(andd3d12core.dll) manually dropped into a hand-built prefix — but itsdxgi.dllis still Wine's own vanilla, unmodified one. Wine's owndxgi.dllexpects to hand off to Wine's own built-inwinevkd3dinstance state when a D3D12 device is involved; vkd3d-proton's separately-overriddend3d12.dllnever populates that shared state, because a real Proton runtime always overrides bothd3d12.dllanddxgi.dlltogether as a matched pair (plus running through Proton's own patched Wine fork, not the system one). This machine's setup only replicated one half of that pair. This is an environment/tooling integration gap, not a defect in CNA's own D3D12 code — the same device/queue/command-list/fence/resource code that works perfectly off-screen would very plausibly also drive a real swap chain correctly on real Windows (or a properly Proton-managed environment); nothing in the crash backtrace touches any CNA-authored code path. - Update, same day: a follow-up attempt closed this — launching through Proton's actual
proton runentry point (STEAM_COMPAT_DATA_PATH/STEAM_COMPAT_CLIENT_INSTALL_PATH, a genuinely Proton-bootstrapped prefix, vkd3d-proton'sd3d12.dll/d3d12core.dlloverlaid on top) makesCreateSwapChainForHwndreturn realS_OK— reproduced twice, confirmed via vkd3d-proton's own log (dxgi_vk_swap_chain_initsucceeds). Formalized asscripts/run-proton-vkd3d.sh. This directly confirms point 3's diagnosis was correct: giving vkd3d-proton the matched pair it expects (both DLLs, real Proton bootstrap) fixes it — further confirmation this was never a CNA bug. Swap-chain creation is fixed;Present()/back-buffer rendering are separate, still- unstarted work — seeDX-116below.
Fully complete (2026-07-14) — all 8 rows closed (✅), real GPU-facing proof throughout. D3D12
went from "off-screen shader/pipeline proof only, no presentation, no render targets, no state
objects, no per-slot samplers, no occlusion queries, no custom effects, no Texture3D" to a real,
functionally broad backend in one continuous pass: D3D12_Smoke grew from 80/80 (Phase DX12's own
closing number) to 125/125 checks by the end of this phase (196/196 today, after Phase DX15 and
later follow-up work built on it). It is enough for a real game: ../mobile-eggbert (a 2D
platformer) builds, runs, creates a real vkd3d-proton device and swapchain, and presents frames on
this backend.
The two rows that stayed partial (🟨) longest, both since closed, honestly:
DX-117(render targets) ✅ —RenderTarget2D/RenderTargetCube/MRT were real and pixel-verified from this phase's own original pass; mip-chain generation closed later viaDX-144; MSAA closed 2026-07-14 (follow-up pass) — see this row's own Notes below for the realResolveSubresource()-on-unbind proof (ChecksOO0/OO1).DX-121(customShaderEffect) ✅ — the runtimeD3DCompile()path, theD3D12EffectBackend, and theD3D12SpriteBatchBackendwiring forSpriteBatch::Begin(effect)are all real and independently CTest-proven (ChecksNN0/NN1).
An earlier revision of this paragraph claimed "all 8 rows done" before either of these follow-ups actually landed; that was an overclaim, corrected at the time after a row-by-row audit, and now genuinely true. See each row's own Notes for the specific real proof.
Ordering rationale: DX-116 (real Present()) is the highest-value single task — it's what makes
D3D12 an actually-displayable backend for the first time, now that swap-chain creation itself
works locally (see the update above). Every other row here is independent of DX-116 and can be
built and off-screen-tested in any order, matching this whole plan's established "prove it off-
screen first, Present() is a separate concern" discipline — sequence chosen below is by expected
game-code impact (a real XNA game hits RenderTarget/state objects/samplers far more often than
OcclusionQuery or a custom ShaderEffect), not a hard dependency chain.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-116 | Real Present() + back-buffer rendering: acquire the current back-buffer RTV (IDXGISwapChain3::GetCurrentBackBufferIndex/GetBuffer), register it with D3D12ResourceStateTracker (DX-106), transition PRESENT↔RENDER_TARGET around each frame's draws (the D3D12-specific requirement D3D11's implicit driver-managed transitions never needed), wire Clear()/draws to target it when no offscreen target is bound, and call swapChain_->Present(...) for real — mirroring D3D11's own DX-26 sync-interval/tearing-flag policy (vsyncEnabled_/allowTearingRequested_/exclusiveFullscreen_ → syncInterval/DXGI_PRESENT_ALLOW_TEARING) |
✅ | Closed 2026-07-14 — real, GPU-facing proof through a live window, not just off-screen. New CreateWindowSizeDependentViews()/ReleaseWindowSizeDependentViews() (mirroring D3D11's own DX-24 group): acquires all kFramesInFlight real back-buffer resources via GetBuffer(), creates their RTVs, registers each with D3D12ResourceStateTracker in its real starting state (D3D12_RESOURCE_STATE_PRESENT), and creates a back-buffer-sized depth-stencil resource+DSV (matching D3D11's own default, though not yet wired into any OMSetRenderTargets call — every draw path still hardcodes a null DSV per DX-107/DX-111's own depthEnable=false simplification; real depth-test support is DX-118's job). The current back buffer is bound as the default Clear()/draw target immediately after construction, reusing the existing BindOffscreenColorTargetEXT() mechanism unchanged — so every existing draw-path check (if (!boundColorResource_) NotYetImplemented(...)) just works once bound to a real back buffer, no new fallback branches needed anywhere. Real Present(): transitions the current back buffer to PRESENT (only when it's actually the bound color target — a game that presents with a custom off-screen target still bound is outside this backend's scope to guess-correct for), calls swapChain_->Present(syncInterval, flags) with D3D11's own exact DX-26 sync-interval/tearing policy reused unchanged, then re-binds the new current back-buffer index as the default target for the next frame (D3D12's flip-model index changes every Present(), unlike D3D11's single always-current RTV). New SetSwapInterval() override, mirrored from D3D11's own. Real proof: extended examples/d3d12_swapchain_diag.cpp with a real 10-frame Clear()+Present() loop (a different color each frame), run through scripts/run-proton-vkd3d.sh's real Proton-managed launch (this session's own DX-102 fix) — genuine, file-based log evidence: IsSwapChainAvailableEXT() = true, all 10 frames report "returned without throwing", process exits 0, no crash — reproduced twice from a clean log file, not a one-shot fluke. Honest scope boundary: kept as a manual diagnostic, not a CTest — Proton's own bootstrap launch is too heavy/slow for a normal CTest run on this dev loop (matches this row's own original "decide honestly" instruction); real windowed CTest coverage, if ever pursued, and full tearing/vsync/exclusive-fullscreen policy-branch verification remain DX-114's job on real Windows hardware. Also honestly out of scope for this task: window resize (D3D11's own DX-29 equivalent) was not attempted — a real, deliberate follow-up gap, not silently dropped. Off-screen D3D12_Smoke CTest re-verified with no regression (80/80 checks); D3D11 (cmake-build-d3d11) rebuilt clean, 6/6 tests, no shared files touched. |
| DX-117 | D3D12RenderTargetBackend/D3D12RenderTargetCubeBackend: a real IRenderTargetBackend implementation (offscreen ID3D12Resource + RTV/DSV, BindAsRenderTarget/UnbindAsRenderTarget), replacing the current test-only BindOffscreenColorTargetEXT() helper as the real, public XNA-facing RenderTarget2D/RenderTargetCube path; SetRenderTargets() for real MRT (mirrors D3D11's DX-43/DX-46) |
✅ | Closed 2026-07-14 for RenderTarget2D/MRT/RenderTargetCube construction+bind+clear+draw, all through the real public IGraphicsBackend API — real GPU proof, not the DX-111 test scaffolding. New D3D12RenderTargetBackend/D3D12RenderTargetCubeBackend (D3D12RenderTargets.hpp/.cpp): a DEFAULT-heap ID3D12Resource (D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET) + real RTV(s) via DX-103's heap allocator, a real SRV (so a render target can later be sampled — GetSrvGpuHandleForTextureEXT/GetSrvGpuHandleForTextureCubeEXT extended with the same two-concrete-type dynamic_cast resolution D3D11's own GetSrvForTextureEXT already has), and an optional depth-stencil resource+DSV via D3DCommon::DepthFormatToDxgi (created for parity, not yet wired into any OMSetRenderTargets call — same honest DX-118-owed gap DX-116's own back-buffer depth buffer already has). BindAsRenderTarget()/UnbindAsRenderTarget() reuse DX-116's existing BindOffscreenColorTargetEXT()/new RestoreBackBufferRenderTargetEXT() mechanisms unchanged — every existing draw-path check just works once bound to a real render target, no new fallback branches needed anywhere. Real MRT: a new small extraMrtResources_[7]/extraMrtRtvs_[7] array (primary target unchanged, additive) + BindOffscreenColorTargetsEXT(); SetRenderTargets() binds up to 8 real targets, and Clear() now independently transitions+clears every one of them — draws themselves stay single-target (no CNA shader declares more than one SV_Target), the same honest scope boundary D3D11's own MRT work already established (see Phase DX15 DX-143). Real proof: examples/d3d12_smoke_test.cpp grew 13 new checks (W1–W13, D3D12_Smoke 80→93/93): CreateRenderTarget2D()/SetRenderTarget2D() bind+Clear()+DrawColoredPrimitives() all exact-color-verified through the real GPU resource; SetRenderTarget2D(nullptr) on this off-screen backend genuinely restores the honest "nothing bound" state (proves RestoreBackBufferRenderTargetEXT()'s own real fallback); a genuine 2-target MRT bind+Clear() writes the exact color into both independently-read-back resources; RenderTargetCube construction+face-0 bind+clear+readback+unbind, all exact. All 13 passed on the first real Wine+vkd3d-proton run. Mip-chain generation closed later via DX-144 (see that row). MSAA closed 2026-07-14 (follow-up pass, real feature, not just a test): D3D12RenderTargetBackend gained a device-queried MSAA path, mirroring D3D11RenderTargetBackend's own DX-45 design exactly — a new ClampMultiSampleCount() helper (ID3D12Device::CheckFeatureSupport(D3D12_FEATURE_MULTISAMPLE_QUALITY_LEVELS), never assumes a requested sample count is actually supported), MSAA and a full mip chain are mutually exclusive on the same attachment (mipMap_ = mipMap && !isMsaa_, same rationale D3D11 already established), and a new resolveResource_ (a separate single-sample DEFAULT-heap resource) that a new ResolveMsaaEXT() method (called from UnbindAsRenderTarget(), before GenerateMipsEXT()) fills via a real ID3D12GraphicsCommandList::ResolveSubresource() — D3D12's own explicit RESOLVE_SOURCE/RESOLVE_DEST resource-state-transition requirement, which D3D11's identical ResolveSubresource() call never needed. The MSAA color resource itself is never sampled directly; callers read through the new GetSampleableColorResourceEXT() accessor (mirrors D3D11's own GetSampleableTextureEXT() naming), which returns resolveResource_ when MSAA and colorResource_ otherwise. D3D12GraphicsBackend::CreateRenderTarget2D() now actually threads multiSampleCount through (previously silently dropped it). Deliberately not added to D3D12RenderTargetCubeBackend, matching D3D11RenderTargetCubeBackend's own established scope boundary (GetMultiSampleCount() stays 0 there on both backends). Real proof (Checks OO0/OO1, D3D12_Smoke 191→193/193): an 8×8 render target requested at 4x MSAA reports a real backend object; Clear()+ResolveSubresource()-on-unbind produces the exact clear color (77,88,99,255) in the resolved, sampleable resource, read back directly from the real GPU resource — and the device genuinely applied real 4x MSAA (GetMultiSampleCount() == 4, not a silent single-sample fallback), confirmed via a real ctest -R D3D12_Smoke run through Wine+vkd3d-proton. RenderTargetCube coverage still only exercises face 0 (same honest asymmetry D3D11's own RenderTargetCube testing already has, tracked separately at Phase DX15 DX-129); per-target MSAA-resolve-on-unbind for N>1 MRT (D3D11's own DX-143 equivalent) was not attempted for D3D12 — a real, scoped follow-up, not silently dropped. D3D11 (cmake-build-d3d11) rebuilt clean, 6/6 tests, no shared files touched. |
| DX-118 | D3D12 state-object system: BlendState/DepthStencilState/RasterizerState → PSO-description-key mapping, replacing the current hardcoded depthEnable=false/cullMode=None PSO defaults with real, cached PSOs keyed on the full (shader variant, stride, blend, depth, rasterizer, RTV/DSV format) tuple DX-107 already anticipated — reuse D3DStateMapping.hpp's existing XNA→D3D11_*/D3D12_* enum tables (design decision 4, already shared with D3D11) unchanged |
✅ | Closed 2026-07-14 — real, GPU-facing pixel proof, not just object-creation proof. D3D12PipelineStateCache's own D3D12PipelineStateDesc (from DX-107) already had every blend/depth/rasterizer field in its cache key — the actual gap was entirely on the caller side: all 3 real psoCache_.GetOrCreate() call sites (DrawColoredPrimitives/DrawIndexedColoredPrimitives/DrawPrimitivesExImpl) hardcoded depthEnable=false/cullMode=None/default-blend literals instead of anything settable. Added real ApplyBlendState()/ApplyDepthStencilState()/ApplyRasterizerState() overrides (new current*_ tracked-state fields on D3D12GraphicsBackend, updated by these 3 calls, fed into every PSO-key construction at those 3 sites) — defaults intentionally match the exact literals those sites hardcoded before this task, so any draw that never calls one of the 3 new methods first (every one of the 93 pre-existing checks) behaves byte-identically to before. Stencil fields and scissorTestEnable/depthBias/slopeScaleDepthBias are deliberately NOT threaded through yet — matches D3D12PipelineStateCache's own already-documented "stencil deliberately NOT part of this first key/desc" scope; a real, honest follow-up gap. Also wired a real bound DSV (BindOffscreenColorTargetEXT() grew 2 new default-valued trailing params, dsv/dsvFormat, source-compatible with every existing call site) so DepthStencilState.DepthEnable has an actual depth buffer to test against, not just a PSO flag with nothing bound — the real swap-chain back buffer's own DSV (DX-116) is now passed through automatically; a bare off-screen D3D12RenderTargetBackend (DX-117) still doesn't create its own DSV (a real, separate follow-up gap). Real proof: D3D12_Smoke grew 93→104/104 checks (new Checks X0–X5, Y0–Y4, all passed on the first real Wine+vkd3d-proton run) — ApplyBlendState(One,One,Add) genuinely additive-blends a second draw over a first for an exact 100+50=150 sum (distinct from the Opaque default X1 proved, and X3 confirms reverting to Opaque un-sticks it); ApplyRasterizerState(CullCounterClockwiseFace) genuinely culls the exact triangle geometry DX-111's own real bug report already found gets back-face-culled, and CullMode::None genuinely un-culls the same geometry; a real, dedicated off-screen depth-stencil resource+DSV (this smoke test has no swap chain to reuse one from) proves DepthEnable=true/DepthFunc=LessEqual genuinely rejects a farther draw regardless of draw order (Y2/Y3, both orderings tested) with a DepthEnable=false control (Y4) confirming the effect really is the depth test, not draw order. Real, pre-existing, found-but-not-fixed bug flagged, not touched (deliberately, to avoid regression risk in an already-large task): D3D12PipelineStateDesc.hpp's own default field values are mislabeled — colorSrcBlend/alphaSrcBlend's default 2 is commented // Blend::One but the real enum class Blend (Blend.hpp) has One=0, Zero=1, so 2 is actually Blend::SourceColor; similarly depthFunc's default 4 is commented // CompareFunction::LessEqual but the real ordinal for LessEqual is 3 (CompareFunction.hpp: Always=0,Never=1,Less=2,LessEqual=3,...). Functionally inert today (the paired DeriveBlendEnable() heuristic and depthEnable=false default already self-consistently produce correct rendered behavior regardless of the mislabeling) but genuinely wrong labels/values worth a dedicated, careful follow-up fix — this task's own new ApplyBlendState/ApplyDepthStencilState tests deliberately used the real, independently-verified enum ordinals (confirmed directly against the XNA header files, not copied from these stale comments) to avoid the same trap. D3D11 rebuilt clean throughout, 6/6, no shared files touched. |
| DX-119 | Per-slot D3D12 SamplerState (16 slots): real dynamic sampler descriptors (a D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER heap, unlike the static-sampler-in-root-signature approach used everywhere so far), replacing the current single hardcoded D3D12_FILTER_MIN_MAG_MIP_LINEAR+WRAP static sampler |
✅ | Closed 2026-07-14 — real, GPU-facing discriminating proof, not just cache-creation proof. New shader-visible D3D12_DESCRIPTOR_HEAP_TYPE_SAMPLER heap (kSamplerHeapCapacity=16, same fixed-bump-allocator discipline as every other DX-103 heap) + D3D12SamplerCache (D3D12SamplerCache.hpp/.cpp, mirrors D3D11SamplerCache's (filter,addressU,addressV,maxAnisotropy) caching key exactly, AddressW=AddressV same documented interface limitation). D3D12RootSignatureCache switched from D3D12_STATIC_SAMPLER_DESC (baked into the root signature, PSO-creation-time-fixed) to real dynamic sampler descriptor tables — one root parameter per sampler register (s0, s1), same N-separate-single-descriptor-tables discipline the SRV tables already established (DX-111's own empirical Wine+vkd3d-proton finding). New ApplySamplerState(slot, filter, addressU, addressV, maxAnisotropy) override tracks per-slot XNA-level state (16 slots, matching SamplerStateCollection::MaxSamplers); GetSamplerGpuHandleEXT(slot) resolves it into a real cached descriptor at draw time. Wired into DrawPrimitivesExImpl (both texture slots) and D3D12SpriteBatchBackend::FlushBatch() (slot 0), each texture's own sampler bound at root parameter index numCbvs+numSrvs+i, right after that texture's own SRV table. Real proof: new Checks Z1-Z4 — a genuine TextureAddressMode::Wrap-vs-Clamp discriminating pixel probe (same methodology as D3D11's own DX-72: a quad with U deliberately > 1.0 at a comfortably-interior, pixel-center-aware readback region, POINT filtering for exactness, a 2-texel-wide RED/GREEN texture) — Wrap tiles to texel column 0 (red), Clamp holds at column 1 (green), same geometry/UVs, opposite outcome purely from the SamplerState change; plus real cache identity/distinctness proof (identical state → same descriptor handle, different state → different handle). D3D12_Smoke grew 104→109/109 checks. Real bug found and fixed — in the new test itself, not the implementation: the cache-identity check (Z3) initially fetched its "before" handle before re-applying the Wrap state, so it captured Check Z2's leftover Clamp state instead — the real Wrap-vs-Clamp pixel proof (Z1/Z2) passed cleanly on the first real run, confirming the actual sampler-binding mechanism was correct from the start. Honest scope note: only texture slots 0/1 are wired (the only registers any CNA stock shader declares) — slots 2-15 are tracked but never consumed by any draw, matching GraphicsDevice.SamplerStates's full 16-slot contract without over-building unused plumbing. |
| DX-120 | D3D12OcclusionQueryBackend: ID3D12QueryHeap (D3D12_QUERY_HEAP_TYPE_OCCLUSION) + a readback buffer, Begin()/End() via ID3D12GraphicsCommandList::BeginQuery/EndQuery, IsComplete()/PixelCount() via ResolveQueryData + a mapped readback resource — mirrors D3D11's DX-47 |
✅ | Closed 2026-07-14 — real, genuinely visible-vs-invisible discriminating proof, not just "the query completed"; also closes Phase DX15's own DX-147 D3D12 half. New D3D12OcclusionQueryBackend (include/src/CNA/Internal/Backends/D3D12/D3D12OcclusionQuery.hpp/.cpp), wired into CreateOcclusionQuery() (no longer the inherited silent nullptr default). A real, non-obvious bug was found and fixed while landing this, not assumed away: the first implementation had Begin() record BeginQuery in its own separate command-list submission, then the game's own separately-submitted draw call, then End() record EndQuery+ResolveQueryData in a third separate submission — this compiled, ran, and IsComplete() returned true, but PixelCount() was silently 0 for a full-viewport visible triangle. Root cause: BeginQuery/EndQuery must be recorded within the same command-list submission as the draw(s) they bracket (a Vulkan/vkd3d-proton requirement this backend's own per-draw-call self-submission architecture — every DrawXxx() does its own independent Reset/record/Close/Execute-and-wait — doesn't naturally satisfy). Fixed by adding D3D12GraphicsBackend::SetActiveOcclusionQueryEXT() (a tracked "currently active query heap" field) and having every draw-recording method (DrawColoredPrimitives/DrawIndexedColoredPrimitives/DrawPrimitivesExImpl/DrawInstancedPrimitivesEx) check it and bracket its own single command-list recording with BeginQuery right after Reset()/EndQuery right before Close() when set — Begin() now just sets this field, End() clears it and only resolves (the EndQuery already happened inside the intervening draw's own submission). Honest, documented scope boundary: correct for exactly one draw call between Begin()/End() — a query spanning multiple draw calls does not currently accumulate correctly (each draw's own Begin/EndQuery pair on the same heap slot without an intervening resolve is undefined per spec) — a real, deliberate follow-up gap, not silently dropped, documented in the class's own header comment. Real proof: D3D12_Smoke grew 109→114/114 checks (Checks AA0–AA4) — a full-viewport triangle reports an exact PixelCount()=4096 (the precise 64×64 render-target area, not merely "positive"), and the SAME query object reused around geometry placed entirely outside the clip volume reports exactly 0 — a genuine, precise, discriminating result. Verified via a real ctest -R D3D12 run through Wine+vkd3d-proton on the real GPU; D3D11 rebuilt clean, 6/6, unaffected (no shared files touched). |
| DX-121 | D3D12EffectBackend: runtime D3DCompile() path for custom HLSL ShaderEffect sources (needs the d3dcompiler link dependency, confirmed safe to add per DX-14-compile's/D3D11's own DX-58 precedent) — mirrors D3D11's DX-58; then wire D3D12SpriteBatchBackend::FlushBatch()'s existing named "not yet implemented" throw for SpriteBatch::Begin(effect) to actually use it, mirroring D3D11's DX-71 |
✅ | Fully closed — the SpriteBatch::Begin(effect) wiring is now independently CTest-proven too. The blocker this row's own closing note originally documented (GraphicsDevice's constructor unconditionally creating a real window, which crashes for D3D12 outside a Proton-managed launch) was removed by PresentationParameters::HeadlessEXT (commit b3289ac6), the same fix DX-132/DX-140/DX-148 already used to reach the real XNA API off-screen. New Checks NN0/NN1 (examples/d3d12_smoke_test.cpp), inside the existing windowless-GraphicsDevice block: the exact same custom color-inversion HLSL D3D11's own DX-71 uses (byte-for-byte identical Sprite2DVertex/128-byte constant-buffer contract, confirmed already shared per this row's own earlier closing note) compiles successfully through a real ShaderEffect, and a real SpriteBatch::Begin(..., &invertEffect) draw of a solid red sprite produces the exact expected inverted cyan — through the real public XNA API this time, not a manually-driven command list. Both passed on the first real run — D3D12_Smoke 189→191/191 checks, confirmed via a real ctest -R D3D12_Smoke run. Original 2026-07-14 closure note, preserved below (now historical — the boundary it describes is resolved). D3D12EffectBackend itself closed 2026-07-14, real and pixel-verified; the D3D12SpriteBatchBackend wiring was real code but honestly NOT independently CTest-proven at the time — a genuine, documented scope boundary, not an oversight. New D3D12EffectBackend (include/src/CNA/Internal/Backends/D3D12/D3D12EffectBackend.hpp/.cpp): runtime D3DCompile() of arbitrary HLSL builds a real ID3D12PipelineState + 128-byte constant buffer up front (unlike D3D11's separate VSSetShader/PSSetShader calls, D3D12 bakes shader+layout+root-signature+blend/depth state into one indivisible PSO) — reuses D3D12SpriteBatchBackend's own exact (1,1,1) root-signature shape (D3D12RootSignatureCache caches by shape, so it's the literal same cached object), and hardcodes the PSO's RTV format to DXGI_FORMAT_R8G8B8A8_UNORM rather than querying the currently-bound target, since CompileProgram() may run before any render target is bound (documented in the class's own header). d3dcompiler added to cna_backend_graphics_d3d12's CMake link line (previously deliberately absent, DX-101's own row). CreateEffectBackend() wired for real, no longer the inherited nullptr default. Real proof: D3D12_Smoke grew 114→119/119 checks (Checks BB0–BB4) — a real custom-compiled shader pair, manually driven (own command-list recording, mirroring the occlusion-query check's own technique) through the real PSO+constant buffer, draws the exact expected green color (SetUniformVec4's fixed-slot convention, byte-for-byte matching D3D11's own DX-58); a deliberately broken HLSL source fails CompileProgram() cleanly with a real, non-empty compiler error. Honest, investigated (not assumed) scope boundary on the D3D12SpriteBatchBackend/SpriteBatch::Begin(effect) half: D3D12SpriteBatchBackend::FlushBatch() was updated for real (the same customEffect_→GetEffectBackendPtr()→SetViewportSizeEXT()→Apply()→Bind()→pull-PSO/CB sequence D3D11's own DX-71 uses, reusing the shared root signature so every downstream binding stays correct regardless of which path supplied the PSO/CB) — but proving this specific code path end-to-end via the real XNA SpriteBatch::Begin(effect) API requires a real GraphicsDevice, and GraphicsDevice's own constructor unconditionally creates a real window for any non-Headless/Software backend (createOrAttachWindow()) — exactly the crash-prone path DX-100/DX-102 already found for D3D12 outside a Proton-managed launch. Investigated directly (not guessed): confirmed via reading GraphicsDevice.cpp's constructor and createOrAttachWindow()'s #if guards (only HEADLESS/SOFTWARE skip window creation). This is a pre-existing, already-documented architectural boundary of the whole Phase DX12/DX13 effort ("every D3D12 pixel proof in this plan remains off-screen"), not something DX-121 introduces — real windowed SpriteBatch/GraphicsDevice-level verification for D3D12 remains DX-114's job on real Windows hardware. D3D11 rebuilt clean, 6/6, unaffected (no shared files touched). |
| DX-122 | D3D12Texture3DBackend: ID3D12Resource with D3D12_RESOURCE_DIMENSION_TEXTURE3D, same explicit upload-heap-staging discipline D3D12TextureBackend/D3D12TextureCubeBackend already established — mirrors D3D11's DX-42 |
✅ | Closed 2026-07-14 — real, byte-exact sub-volume proof, not just full-level. New D3D12Texture3DBackend (include/src/CNA/Internal/Backends/D3D12/D3D12Texture3D.hpp/.cpp): a real ID3D12Resource with D3D12_RESOURCE_DIMENSION_TEXTURE3D (DepthOrArraySize = the real depth, not an array — TEXTURE3D has no array dimension at all, so the subresource-index formula is unconditionally just the mip level, not a simplification the way D3D12TextureBackend's array-size-1 TEXTURE2D case is). SetData/GetData support a genuine arbitrary sub-volume (x,y,z,w,h,depth), not just the always-full-level convenience D3D12TextureBackend::UpdatePixels provides — a row-pitch-aligned staging buffer with a per-Z-slice loop (extending D3D12TextureBackend::UploadRegion's own 2D row-loop pattern with a depth dimension), CopyTextureRegion with real DstX/DstY/DstZ offsets (upload) and a real D3D12_BOX (readback), and D3D12ResourceStateTracker-driven COPY_DEST↔shader-readable transitions throughout. Wired into CreateTexture3D(), no longer the inherited nullptr default. Real proof: D3D12_Smoke grew 119→122/122 checks (Checks CC0–CC2) — a real 2×2×2 sub-cube uploaded at a deliberately off-center offset (1,1,0) within a 4×4×2 volume, with a different solid color per Z slice (red/green), round-trips through SetData()+GetData() with EXACT byte equality — genuinely exercises the X/Y/Z offset math and per-slice pitch, not just a trivial always-(0,0,0) case. Verified via a real ctest -R D3D12 run through Wine+vkd3d-proton on the real GPU; D3D11 rebuilt clean, 6/6, unaffected (no shared files touched). |
| DX-123 | D3D12TextureCubeBackend::GetData(): real readback (staging/readback-heap copy + Map), replacing the current silent no-op default ITextureCubeBackend::GetData() inherits |
✅ | Closed 2026-07-14 — real, GPU-facing readback proof, not assumed. Phase DX13 (DX-116 through DX-123) is now fully complete. GetData() mirrors D3D12Texture3DBackend::GetData()'s own just-closed pattern (DX-122) exactly: a D3D12_HEAP_TYPE_READBACK buffer + CopyTextureRegion with a real D3D12_BOX for the requested sub-rectangle + Map, reusing SetData()'s own face/level→subresource formula (level + face*mipLevels_, single-plane/ArraySize=6 collapse, DX-111's own established convention). Real proof (Checks DD1–DD3, D3D12_Smoke 109→125/125 checks): two distinctly-colored 4×4 sub-rects uploaded to two different faces at two different offsets, each read back exactly — proves real per-face subresource selection, not just "some data came back" (DD1/DD2); a genuinely untouched region of a written-to face reads back as real zero-initialized GPU content (D3D12's own documented committed-resource zero-init guarantee), not a poisoned CPU buffer's stale value left over from the test's own setup — confirms this is a live GPU readback, not an accidental no-op (DD3). Honest process note: the fork that closed DX-120/DX-121/DX-122 in the same run left DX-123 half-started (a header-only declaration, no implementation, causing a real link error) and its own final message claimed to be "continuing to DX-123" without actually doing so — caught during a routine independent verification pass (build+test, not just trusting the fork's report) and finished directly rather than re-delegated. |
Fully closed 2026-07-14 — all 7 rows (DX-124–DX-130), real GPU-facing proof throughout.
Authorized alongside Phase DX15 (same day). Nothing in this phase was ever a known bug — every row
was real, shipped D3D11 code with no dedicated pixel test proving it correct, as opposed to Phase
DX13's D3D12 rows, which were missing code entirely. D3D11_Smoke grew from 95/95 (before this
phase) to 120/120 checks by the end of it. Highlights: multi-light/EmissiveColor/specular
discrimination for the shared D3DLightingConstants path (DX-124/DX-125, mirroring D3D12's own
already-closed DX-138/DX-139); mip level > 0 upload/readback (DX-126, mirroring DX-141);
SpriteFont glyph placement through the real public API (DX-127, mirroring DX-132);
Model/ModelMesh runtime orchestration (DX-128, mirroring DX-148); RenderTargetCube's full
bind+clear+readback+unbind proof plus a new per-face independence check (DX-129); and the 5 combo
Clear* variants via direct stencil-byte GPU readback + depth's real rasterization effect (DX-130,
mirroring DX-146). One real, separate regression found and fixed along the way (surfaced while
verifying DX-124, not part of this phase's own original scope): DX-15 making CnaTests.exe
genuinely build under the D3D11/D3D12 MinGW cross-targets broke every ctest -R D3D11/-R D3D12
invocation in those trees (gtest_discover_tests() tried to exec the Windows PE directly on the
Linux host) — fixed by routing CnaTests' own discovery/run through the same Wine wrappers
D3D11_Smoke/D3D12_Smoke already use.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-124 | Dedicated multi-light (DirectionalLight1/DirectionalLight2) + EmissiveColor discriminating pixel test for BasicEffect/EnvironmentMapEffect/SkinnedEffect's shared D3DLightingConstants path — the existing lit-pixel test only proves single-light lit-vs-unlit differs, not that light 1/2/emissive each contribute correctly and independently |
✅ | Closed 2026-07-14 — real, exact-color proof, mirroring D3D12's own already-closed DX-138 methodology exactly. 4 new checks in examples/d3d11_smoke_test.cpp, reusing lit_textured3d's exact field mapping (1:1 from GpuDrawParams, no separate "enabled" flags — a light/emissive term is disabled purely by zeroing its diffuse/color field). Geometry/material chosen so each term produces an EXACT expected RGB: DirectionalLight1 alone (Light0/Light2/ambient/specular all zero, white texture+diffuseColor) → exact (255,0,0); the same light with its diffuse re-zeroed → exact (0,0,0), proving the first result didn't leak from some other default; DirectionalLight2 alone → exact (0,255,0); EmissiveColor alone with every light off → exact (0,0,255), confirming it's a genuinely constant, light-independent additive term. Drawn directly to the real back buffer (D3D11's established style, unlike D3D12's offscreen-target approach) via the existing dev.Clear()/backend.DrawPrimitivesEx()/dev.GetBackBufferData() pattern Checks Q–W already use. All 4 passed on the first real Wine+DXVK run — D3D11_Smoke 95→99/99 checks, confirmed via a real ctest -R D3D11_Smoke run. Found and fixed a real, separate regression while verifying this: DX-15 making CnaTests.exe genuinely build under the D3D11/D3D12 MinGW cross-targets broke every ctest -R D3D11/-R D3D12 invocation in those trees — gtest_discover_tests(DISCOVERY_MODE PRE_TEST) tried to exec the Windows PE directly on the Linux host. Fixed by setting CROSSCOMPILING_EMULATOR on the CnaTests target (before the gtest_discover_tests() call, which reads the property immediately at configure time) to the same Wine wrappers D3D11_Smoke/D3D12_Smoke already use. |
| DX-125 | Dedicated specular-highlight (SpecularColor/SpecularPower) pixel test — the existing lit-pixel test deliberately zeroes specular for CPU-comparison determinism; needs a real methodology for cross-checking a GPU-computed specular term (e.g. a camera/light/normal geometry deliberately chosen so the expected specular contribution is analytically computable, not just "some highlight is visible") |
✅ | Closed 2026-07-14 — same methodology as D3D12's already-closed DX-139 (examples/d3d12_smoke_test.cpp Checks FF1/FF2), avoiding the byte-exact Blinn-Phong CPU-replication problem this row's own text flags. Geometry picked so the math collapses to an exact, hand-derivable value: eye at (0,0,-10), surface normal (0,0,-1), light1 traveling (0,0,1) (so "direction to light" = (0,0,-1), identical to the view direction) — the half-vector H = normalize(view+toLight) then equals N exactly, so dot(H,N)=1 and pow(1,SpecularPower)=1 regardless of the actual power value. With diffuse/ambient/emissive all zeroed and SpecularColor=(1,1,1)/Light1Specular=(1,1,1), the exact expected output is pure white (255,255,255) — reverting SpecularColor to zero on the identical geometry/light gives exact black (0,0,0), proving the white came genuinely from the specular term. Both passed on the first real Wine+DXVK run — D3D11_Smoke 99→101/101 checks, confirmed via a real ctest -R D3D11_Smoke run. |
| DX-126 | Mip level > 0 SetData/UpdatePixelsLevel/sampling dedicated pixel test — texture upload/readback is currently only proven at mip level 0 |
✅ | Closed 2026-07-14 — real, deterministic proof via direct GPU readback, mirroring D3D12's own already-closed DX-141 methodology. Rather than trying to force the stock shaders' implicit-LOD Sample() calls to pick mip level 1 during a draw (fragile/driver-dependent, and none of the stock HLSL shaders use an explicit SampleLevel()), this reads mip level 1 back directly via a staging-texture CopyResource+Map (ReadTexture2DMipRegion, an existing test helper already established for DX-144's own render-target mip-chain proof — reused here unchanged, just against a plain D3D11TextureBackend instead of a render target). A real 4×4 level-0 + 2×2 level-1 texture: construction reports the exact requested 2-level mip chain; UpdatePixelsLevel(1, ...) round-trips EXACT bytes for the level-1 upload; level 0's own content is independently confirmed genuinely unaffected by the level-1 write — proves the subresource targeting is correct, not just "some upload succeeded." All 3 checks passed on the first real Wine+DXVK run — D3D11_Smoke 101→104/104 checks, confirmed via a real ctest -R D3D11_Smoke run. |
| DX-127 | SpriteFont D3D11-specific glyph placement/spacing/newline/flip test, mirroring EasyGL's own already-pixel-verified suite (Tasks 424-429) — builds on already-tested Texture2D/SpriteBatch/VertexBuffer primitives but has no D3D11-specific coverage yet |
✅ | Closed 2026-07-14 — real proof through the actual public XNA API, mirroring D3D12's own already-closed DX-132 (examples/d3d12_smoke_test.cpp Checks KK2-KK5) and EasyGL's established fixture (Tasks 424-429) exactly: 8×8 solid-white glyph cells, zero cropping/kerning bearing, so a glyph's destination rect maps exactly and any placement error is a hard pixel difference. Unlike D3D12, D3D11's real swap chain already works directly under plain Wine, so this draws straight to the back buffer (this file's own established style throughout) instead of needing D3D12's offscreen-RenderTarget2D/PresentationParameters::HeadlessEXT detour. Real proof (examples/d3d11_smoke_test.cpp, 4 new checks): a single glyph lands at exactly (4,4,8,8) — verified inside plus all four edge midpoints, so an X-only or Y-only misplacement cannot pass; "AB" advances the second glyph by exactly one glyph width with nothing spilling past it; "A\nA" drops the second line by exactly lineSpacing and resets x to the start; and SpriteEffects::FlipVertically is proven against a deliberately asymmetric (top-half-white) glyph — lands top-half-white unflipped and bottom-half-white flipped. All 4 passed on the first real Wine+DXVK run — D3D11_Smoke 104→108/108 checks, confirmed via a real ctest -R D3D11_Smoke run. |
| DX-128 | Model/ModelMesh/ModelMeshPart/ModelBone D3D11-specific runtime-API test — not separately exercised against this backend this session |
✅ | Closed 2026-07-14 — mirrors D3D12's already-closed DX-148 (examples/d3d12_smoke_test.cpp Check KK6) exactly. Drives ModelMesh::Draw()'s REAL orchestration (bone transform folded into the world matrix, SetVertexBuffer + setIndices + DrawIndexedPrimitives + EffectPass::Apply) — deliberately not a raw VertexBuffer/IndexBuffer draw wearing a Model label, which this row's own text warns against and which would have proven nothing new. A real 2-bone hierarchy (root→child) drives Model::Draw(), painting the mesh's exact red over a green clear, read back from the real back buffer (D3D11's own established style — unlike D3D12, which needed an offscreen RenderTarget2D detour). Passed on the first real Wine+DXVK run — D3D11_Smoke 108→109/109 checks, confirmed via a real ctest -R D3D11_Smoke run. |
| DX-129 | RenderTargetCube dedicated pixel test — currently only construction is proven real; RenderTarget2D has the full bind+clear+readback+unbind-restores-backbuffer proof, RenderTargetCube doesn't yet |
✅ | Closed 2026-07-14 — mirrors RenderTarget2D's own full bind+clear+readback+unbind-restores-backbuffer proof (Check J) for RenderTargetCube, plus a per-face independence check that Check J itself never needed to prove. BindAsRenderTargetFace(0)+Clear() writes the exact color into face 0's own texture-array slice, read back directly from the real GPU resource; binding face 1 to a genuinely different color, then re-reading face 0, proves the two faces are independent slices rather than aliased (the specific risk a shared 6-slice texture array creates that a single RenderTarget2D never has to prove); UnbindAsRenderTarget() genuinely restores the back buffer as Clear()'s next target, same bar as RenderTarget2D's own check. All 3 passed on the first real Wine+DXVK run — D3D11_Smoke 109→112/112 checks, confirmed via a real ctest -R D3D11_Smoke run. |
| DX-130 | The 5 combo Clear* variants (ClearColorAndDepth/ClearDepth/ClearStencil/ClearDepthAndStencil/ClearColorAndStencil/ClearColorDepthAndStencil) dedicated round-trip pixel test — real ClearDepthStencilView calls exist and are implemented identically to the proven plain Clear(r,g,b,a) path, but only plain Clear has a dedicated pixel test (DX-25's own long-standing honest gap) |
✅ | Closed 2026-07-14 — mirrors D3D12's own already-closed DX-146 methodology exactly (stencil via direct GPU readback, depth via its real effect on rasterization). New test-only ReadDepthStencilPlane() helper reads a DXGI_FORMAT_D24_UNORM_S8_UINT resource's real bytes via the same staging-texture technique this file's other readback helpers already use — D3D11 packs depth+stencil as one 32-bit value per pixel (byte 3 = stencil), so no plane-slice copy is needed the way D3D12 requires. Real proof against a real Depth24Stencil8 render target: ClearColorDepthAndStencil(stencil=0x5A) genuinely writes 0x5A into every pixel of the real stencil bytes, read straight off the GPU; ClearStencil(0x3C) alone genuinely overwrites it to a different value, so neither is a silent no-op. Depth is proven by its real effect on rasterization: the same triangle at the same z=0.5 is drawn twice, differing only in the depth a prior ClearDepth/ClearColorAndDepth wrote — cleared-to-0.9 lets it through depthFunc=Less, cleared-to-0.1 correctly rejects it (with an explicit depth-off control check proving the draw itself works first). ClearDepth() alone is proven to leave the color target untouched. All 8 checks passed on the first real Wine+DXVK run — D3D11_Smoke 112→120/120 checks, confirmed via a real ctest -R D3D11_Smoke run. This closes the last open row in Phase DX14 — all 7 rows (DX-124–DX-130) are now done. |
Phase fully complete (2026-07-14) — 18 of 18 rows closed with real GPU proof. DX-137 closed
fully in a later pass (all 7 fog-capable variants now covered, both backends); DX-144's D3D12 leg
then also closed (real mip-chain-generation feature landed, not just a test); DX-136 closed last —
a real new alpha_test_colored3d shader variant (stride 24, VertexPositionColorTexture) landed on
both D3D11 and D3D12, giving AlphaTestEffect.VertexColorEnabled a real vertex-color attribute to
multiply against (the plain alpha_test3d variant, stride 20, never carried one). Vulkan's identical
underlying gap is genuinely out of DX-136's own scope (its row title says "both D3D11 and D3D12")
— separately tracked as Task 887 in plan_graphics.md.
An earlier revision of this paragraph claimed "all 18 rows closed"; that was an overclaim, corrected here after a row-by-row audit.
The last three rows (DX-132 D3D12 SpriteFont, DX-148 D3D12 Model, and DX-140's
FromStream/SaveAsPng half) were blocked by one shared architectural root cause, found
independently by three different tasks: all three need a real GraphicsDevice, whose constructor
unconditionally initialised SDL video and created a real window for every backend except
HEADLESS/SOFTWARE — and for D3D12 a real window means a real swap chain, which on this Linux
dev loop only works through DX-116's heavy Proton-managed launcher, not the plain Wine the routine
CTest uses.
That decision was made and implemented: PresentationParameters::HeadlessEXT (NOXNA, commit
b3289ac6) — a runtime opt-in for a genuinely windowless device, skipping SDL video init entirely,
so it needs no display server at all and works in CI too. It defaults to false, so production
behavior is byte-identical for every backend and every existing caller; only backends that can
genuinely run without a swap chain support it (D3D12 today). It is not merely a test hook: an
off-screen GraphicsDevice is a real capability (server-side rendering, thumbnail generation). With
it, all three rows landed in the routine plain-Wine D3D12 CTest like every other check.
Four real bugs were found and fixed by this phase's own tests — none would have been caught by the code alone:
- D3D12
SpriteBatchsilently ignoredSetSamplerFilter/SetSamplerAddressMode, drawing with whatever sampler an unrelated prior 3D draw happened to leave bound (DX-133). - D3D12 render targets never bound their own DSV, so a render target with a depth buffer gave
every draw no depth buffer — the depth test was inert and every depth/stencil
Clearhad nothing to write to (DX-146). - All 6 combo
Clear*variants were still unimplemented throws on D3D12 (DX-146). SetDepthTestEnabled/SetDepthWriteEnabled/SetBlendEnabledwere still unimplemented throws on D3D12, so any game callingGraphicsDevice::SetDepthTestEnabled()against this backend simply crashed — found byDX-148's Model test, the first thing in the project to ever drive the sharedGraphicsDevicepath against D3D12.
Per the project
owner's explicit 2026-07-14 request ("add tasks if they don't already exist — the goal is for the
DX backends to do as much as possible like the EasyGL backend"), this phase is the result of walking
docs/graphics-backend-feature-matrix.md row by row against DX-116–DX-130 and recording every
genuine EasyGL-parity gap neither phase already covers — some rows need a D3D11 leg, a D3D12 leg, or
both, noted per row. Rows that are pre-existing gaps on EasyGL itself (e.g. DualTextureEffect VertexColorEnabled, Task 889; non-Color SurfaceFormat for real GPU data, ⛔ Task 732) are
deliberately excluded — matching a backend that doesn't have the feature either isn't a parity gap.
Several rows explicitly depend on an earlier phase landing first (noted inline) — do not attempt
those before their prerequisite.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-131 | SpriteBatch full Draw overload set — rotation/scale/origin/crop-rect/SpriteSortMode — dedicated pixel test, both D3D11 and D3D12 |
✅ | Closed 2026-07-14 — real, GPU-facing proof on both backends. Both backends already used the exact same, independently-confirmed quad-building formula (p0x=(0-ox)*scaleX..., rotateAndTranslate via cosR/sinR) — verified by direct source comparison against EasyGLGraphicsBackend.cpp's own reference before writing any test. D3D11 (examples/d3d11_smoke_test.cpp, Checks Y2–Y4, via the real public SpriteBatch/Texture2D API): a 90° rotation around a centered origin permutes a 2×2 corner-colored texture's 4 quadrants into the geometrically-predicted screen positions (independently re-derived by hand: TL→NE, TR→SE, BR→SW, BL→NW for this rotation direction); a halved destRect genuinely shrinks the sprite (a pixel inside the old size but outside the new one shows the clear color); a cropped sourceRectangle shows only the requested sub-region. D3D12 (examples/d3d12_smoke_test.cpp, Checks R4–R6, via the raw ISpriteBatchBackend): same 3 proofs, same geometric derivation — but needed a real fixture fix found while writing the test: D3D12SpriteBatchBackend's default sampler is bilinear, and a 1-texel-per-corner fixture (which worked fine on D3D11, tested with an explicit point sampler) read back ~3-6% blended toward the adjacent texel on D3D12's default linear sampler (e.g. (15,8,239) instead of the exact (0,0,255)) — fixed by widening every fixture to 2 texels per color (matching this same file's own pre-existing R0-R3 fixture convention, not a new pattern). SpriteSortMode deliberately has no dedicated test on either backend: its ordering logic (BackToFront/FrontToBack/Texture) is implemented entirely in the shared, backend-agnostic SpriteBatch.cpp (sorts the pending draw list before calling the backend's own Draw(), in order) — there is no backend-specific sort behavior to prove; a "dedicated backend test" would just be re-testing shared C++ code under a misleading label. |
| DX-132 | SpriteFont D3D12-specific glyph placement/spacing/newline/flip test |
✅ | Closed 2026-07-14 — the architectural blocker was removed, then the test written for real. The blocker (a D3D12 GraphicsDevice forcing a real window → a real swap chain → this dev loop's crash path) is gone: PresentationParameters::HeadlessEXT (NOXNA, commit b3289ac6) makes a genuinely windowless GraphicsDevice possible, so this runs in the routine plain-Wine D3D12 CTest, not through the Proton launcher. Drawing goes through the real public XNA API (SpriteBatch::DrawString) into a real RenderTarget2D (DX-117); only the readback reaches into the backend, since there is no back buffer without a swap chain. Fixture is EasyGL's own established one (Tasks 424-429): 8×8 solid-white glyph cells, zero cropping/kerning bearing, so a glyph's destination rect maps exactly and any placement error is a hard pixel difference. Real proof (Checks KK2–KK5): a single glyph lands at exactly (4,4,8,8) — verified inside plus all four edge midpoints, so an X-only or Y-only misplacement cannot pass; "AB" advances the second glyph by exactly one glyph width with nothing spilling past it; "A\nA" drops the second line by exactly lineSpacing and resets x to the start (a newline doing only one of the two fails); and SpriteEffects::FlipVertically is proven against a deliberately asymmetric (top-half-white) glyph — it lands top-half-white unflipped and bottom-half-white flipped, so a no-op flip cannot pass (flipping a solid block would have proven nothing). |
| DX-133 | Wire D3D12SpriteBatchBackend's SetSamplerFilter/SetSamplerAddressMode (currently stored but not behaviorally real, per DX-112's own honest gap note) to DX-119's real per-slot SamplerState system once it lands, then add the TextureAddressMode::Wrap/Mirror-via-SpriteBatch pixel test D3D11 already has (DX-72) |
✅ | Closed 2026-07-14 — real, GPU-facing proof, and a real behavioral bug fixed along the way. D3D12SpriteBatchBackend::FlushBatch() now calls owner_->ApplySamplerState(0, pendingFilter_, pendingAddressU_, pendingAddressV_, 1) right before reading the sampler descriptor handle — mirrors D3D11SpriteBatchBackend's own exact call/placement. Real, previously-undetected bug this closes: before this fix, SpriteBatch draws on D3D12 silently used whatever XNA texture slot 0's sampler happened to already be set to by an unrelated prior 3D draw (or D3D12GraphicsBackend's own pre-DX-119 hardcoded LINEAR/WRAP fallback if nothing had ever called ApplySamplerState) — SetSamplerFilter()/SetSamplerAddressMode() (what SpriteBatch::Begin(sortMode, blend, samplerState, ...) is meant to drive) were stored but completely inert. Real proof (examples/d3d12_smoke_test.cpp Checks R7/R8, mirroring D3D11's own DX-72 Wrap/Mirror probe-pixel methodology exactly, same UV derivation): TextureAddressMode::Wrap genuinely tiles past UV 1.0 instead of clamping; TextureAddressMode::Mirror genuinely reflects, distinct from both Wrap and Clamp at the same probe point. Used TextureFilter::Point for this specific pair of checks (point sampling has no blend-weight ambiguity at all, unlike DX-131's bilinear-default fixtures, so the existing 4×4 corner fixture worked unchanged, just with a doubled sourceRectangle to keep the same UV-multiplier-2 range). |
| DX-134 | EnvironmentMapEffect base-lerp alpha scaling (FresnelFactor/EnvironmentMapAmount-driven blend between the base and reflected color) — dedicated pixel test, both D3D11 and D3D12; first confirm whether the HLSL even implements this term (env_map3d.frag.hlsl) before assuming it's just untested |
✅ | Closed 2026-07-14 — real, GPU-facing proof on both backends. First confirmed the term genuinely IS implemented (env_map3d.frag.hlsl line 59: lerp(baseColor, envSample.rgb*combinedAlpha, blendFactor), blendFactor driven by EmissiveEm.w = envMapAmount) — no shader gap, purely a verification gap as suspected. Reused the exact geometrically-constrained-reflection fixture DX-66/DX-111's own env-map tests already established (camera placed so reflection lands deep inside one distinctly-colored cube face), just varying envMapAmount to 0.0: with lighting/ambient/emissive all defaulting to 0, blendFactor=0 must collapse the lerp to pure black — genuinely different from the already-passing envMapAmount=1.0 reflected-face-color check, proving the blend is a real graduated control, not an on/off gate. D3D11 Check (envMapAmount=0.0 after Check U), D3D12 Check U2 — both exact (0,0,0,255). |
| DX-135 | SkinnedEffect.WeightsPerVertex (1/2/4-weight GPU blending) discriminating per-weight-count pixel test, both D3D11 and D3D12 |
✅ | Closed 2026-07-14 — real, GPU-facing proof, and a genuinely non-obvious math property found and worked through empirically (not just theorized), not just tested. First attempt used bone0=Identity + bone1=Scale(3.0) at weight (0.5,0.5), expecting weightsPerVertex=1 (bone1 ignored) to shrink the triangle to Scale(0.5) — this failed a real build+run, and debugging (empirical pixel scans across the whole render target, not just re-deriving by hand) found the actual cause: Bones[i]*weight scalar-multiplies the entire 4×4 matrix including the homogeneous w=1 component, and the GPU's own perspective/homogeneous divide (skinnedPos.xyz / skinnedPos.w) exactly cancels any single bone's own weight magnitude — a single active bone at any weight reproduces that bone's untransformed result, matching DX-67's own single-bone-at-full-weight precedent exactly (this is also why real skinning requires weights to sum to 1.0 for the blended result to remain a valid w=1 rigid transform). Corrected design: weightsPerVertex=1 (bone1 ignored) leaves bone0=Identity's own unshrunk result; weightsPerVertex=2 genuinely blends in bone1=Scale(0.1) (a shrinking, not growing, transform) at 0.5/0.5, giving Scale(0.55) — small enough to pull the triangle's hypotenuse away from a probe point the unshrunk (weightsPerVertex=1) triangle still covers. Real proof: D3D11 (2 checks, texture-color-vs-background at pixel (54,10)), D3D12 (Checks S2/S3, same pattern) — both genuinely discriminate weightsPerVertex=1 from =2 with identical vertex weight data. |
| DX-136 | AlphaTestEffect.VertexColorEnabled dedicated pixel test, both D3D11 and D3D12 |
✅ | Closed 2026-07-14 — a real new shader variant landed, not a test-only patch. Original 2026-07-14 investigation found alpha_test3d.vert.hlsl's input struct had no color vertex attribute at all (POSITION0+TEXCOORD0 only, stride 20) — genuinely nothing to multiply VertexColorEnabled against. Closed by adding a real sibling variant, alpha_test_colored3d (stride 24, VertexPositionColorTexture — POSITION0+COLOR0+TEXCOORD0, byte-identical to colored_textured3d's own already-established stride-24 layout, so D3DVertexFormatHelper/D3D11InputLayoutCache/D3D12PipelineStateCache all needed zero changes, being purely stride/variant-keyed already): new alpha_test_colored3d.vert.hlsl/.frag.hlsl (VS mixes vertex color and diffuse exactly like colored_textured3d.vert.hlsl's own established (VertexColorEnabled > 0.5) ? input.Color * DiffuseColor : DiffuseColor pattern; FS is byte-identical alpha-test/fog logic to alpha_test3d.frag.hlsl, since it only ever reads the already-combined Tint), a new D3DShaderVariant::AlphaTestColored3d enum entry + D3DShaderCache.cpp mapping, real DXBC compiled via compile_shaders_hlsl.py (genuine D3DCompile() through Wine, CNA_D3D11_SKIP_DXVK_GATE=1 needed since the compiler tool never creates a device), and a one-line dispatch change in both D3D11GraphicsBackend/D3D12GraphicsBackend's DrawPrimitivesExImpl (stride==24 picks the new variant, stride==20 keeps the existing one) — the constant-buffer population code already unconditionally forwarded params.vertexColorEnabled into D3DAlphaTestConstants.VertexColorEnabled for both backends (confirmed by reading it, not assumed), so zero C++ dispatch/cbuffer-population changes were needed beyond variant selection; AlphaTestEffect::FillGpuDrawParams() already forwarded vertexColorEnabled_ correctly too (confirmed against docs/alphatesteffect-support.md's own Task 377 finding: EasyGL already correct, D3D11/D3D12 didn't exist yet when that doc was written). Real proof, both backends: a white opaque texture isolates the vertex-color contribution — VertexColorEnabled=true multiplies the exact vertex color (red) through; VertexColorEnabled=false on the SAME vertex buffer genuinely ignores it, only DiffuseColor (white) survives; the alpha-test discard logic itself still genuinely works on the new stride-24 path (a failing alpha drops the pixel). All 6 checks (3 per backend) passed on the first real run — D3D11_Smoke 132→135/135, D3D12_Smoke 186→189/189, both confirmed via real ctest runs. Vulkan is genuinely out of this row's own scope (its title says "both D3D11 and D3D12" — Vulkan's identical underlying gap is separately tracked as Task 887 in plan_graphics.md, not this row). |
| DX-137 | Dedicated fog on/off discriminating pixel test for the remaining fog-capable variants beyond colored3d (textured3d/colored_textured3d/lit_textured3d/alpha_test3d/dual_texture3d/env_map3d/skinned3d), both D3D11 and D3D12 |
✅ | Fully closed 2026-07-14 — all 7 fog-capable variants now have a dedicated fog on/off test on both backends. textured3d was closed earlier (representative variant, see this row's original note below); this pass adds the remaining 6 (colored_textured3d/lit_textured3d/alpha_test3d/dual_texture3d/env_map3d/skinned3d), reusing each variant's own already-proven, already-existing fixture from its own closed task (DX-62/DX-63/DX-64/DX-65/DX-66/DX-67) rather than inventing new ones — same Z-at-FogEnd methodology throughout: an object-space Z=fogEnd (0.5) makes fogFactor land exactly on 0 (pure FogColor) when fogEnabled=true vs. exactly 1 (pure original color) when false. alpha_test3d deliberately reuses its already-PASSING fixture (alpha 64/255 < AlphaRef=0.5) — a discarding fixture would prove nothing, since no fragment would ever reach the fog blend. env_map3d/skinned3d needed their envMapAmount/bone state explicitly reset or freshly rebuilt, since their own block's later sub-checks (DX-134/DX-135) leave those fields mutated by the time this fog check runs. All 12 new checks (6 variants × 2) passed on the first real run on both backends: D3D11_Smoke 120→132/132 checks (verified via ctest -R D3D11_Smoke), D3D12_Smoke 169→181/181 checks (verified via ctest -R D3D12_Smoke). Original 2026-07-14 note, preserved: DX-69/DX-113's own audits both found and closed this gap only for colored3d; the first pass added textured3d as one more representative proof point, and found+fixed a real bug in the test fixture itself (not production code) — the fog-on/off pair initially reused the shared vbTex vertex buffer (Z=0), which gives fogFactor=1 (no blending) regardless of fogEnabled; needed its own dedicated Z=fogEnd vertex buffer, the same convention every variant added in this pass also follows. |
| DX-138 | Multi-light (DirectionalLight1/2) + EmissiveColor discriminating pixel test for D3D12's shared D3DLightingConstants path (BasicEffect/EnvironmentMapEffect/SkinnedEffect) |
✅ | Closed 2026-07-14 — real, exact-color proof, both new lights and emissive isolated independently. 4 new checks (EE1–EE4, examples/d3d12_smoke_test.cpp), reusing lit_textured3d's exact field mapping (D3D12GraphicsBackend.cpp's D3DLightingConstants population, 1:1 from GpuDrawParams, no separate "enabled" flags — a light/emissive term is disabled purely by zeroing its diffuse/color field, matching FNA's own DirectionalLight.Enabled convention). Geometry/material chosen so each term produces an EXACT expected RGB, not just "differs": EE1 — DirectionalLight1 alone (Light0/Light2/ambient/specular all zero, white texture+diffuseColor) → exact (255,0,0); EE2 — the same light with its diffuse re-zeroed → exact (0,0,0), proving EE1's color didn't leak from some other default; EE3 — DirectionalLight2 alone → exact (0,255,0); EE4 — EmissiveColor alone with every light off → exact (0,0,255), confirming it's a genuinely constant, light-independent additive term (matches the HLSL's own lit = lightSum*tint + EmissiveColor order, not scaled by lightSum). All 4 passed on the first real Wine+vkd3d-proton run — the exact-color derivation (by hand, from lit_textured3d.frag.hlsl's real math) was correct on the first attempt. |
| DX-139 | Specular-highlight (SpecularColor/SpecularPower) pixel test for D3D12 |
✅ | Closed 2026-07-14 — a real methodology that avoids DX-125's own D3D11 zeroed-for-CPU-determinism gap, not a repeat of it. 2 new checks (FF1/FF2). Rather than trying to CPU-replicate the view-angle-dependent Blinn-Phong math for a byte-exact comparison (the exact problem DX-125's row documents), this test picks geometry where the math collapses to an exact, hand-derivable value: eye at (0,0,-10), surface normal (0,0,-1), light1 traveling (0,0,1) (so "direction to light" = (0,0,-1), identical to the view direction) — the half-vector H = normalize(view + toLight) then equals N exactly, so dot(H,N)=1 and pow(1, SpecularPower)=1 regardless of the actual power value. With diffuse/ambient/emissive all zeroed and SpecularColor=(1,1,1)/Light1Specular=(1,1,1), the exact expected output is pure white (255,255,255) (FF1) — reverting SpecularColor to zero on the identical geometry/light gives exact black (0,0,0) (FF2), proving FF1's white came genuinely from the specular term. Both passed on the first real run — the by-hand derivation (E - nL1 half-vector formula, read directly from lit_textured3d.frag.hlsl) was correct. |
| DX-140 | Texture2D.FromStream/SaveAsPng/NPOT (non-power-of-2) dedicated pixel test, both D3D11 and D3D12 |
✅ | Fully closed 2026-07-14 — the NPOT half earlier, the FromStream/SaveAsPng half now. NPOT (closed earlier, both backends): a genuinely non-power-of-two 5×3 texture (20 bytes/row, which does not divide evenly into D3D12_TEXTURE_DATA_PITCH_ALIGNMENT, 256 — a real, previously-unexercised alignment edge case), uploaded and sampled via a real textured3d draw, exact-color readback on both D3D11 and D3D12. FromStream/SaveAsPng were blocked because both require a GraphicsDevice&, which for D3D12 meant a real window (the same blocker DX-132/DX-148 hit); PresentationParameters::HeadlessEXT (commit b3289ac6) removed it. Real proof (Checks KK7–KK9, through a windowless D3D12 GraphicsDevice): SaveAsPng() encodes a real, non-empty PNG; FromStream() decodes it back to the exact original dimensions; and every pixel of a deliberately varying 4×4 pattern (not a solid colour — a decoder that dropped, reordered or channel-swapped pixels could not survive it) round-trips exactly. |
| DX-141 | Mip level > 0 SetData/sampling dedicated pixel test for D3D12 |
✅ | Closed 2026-07-14 — real, deterministic proof via direct GPU readback, not driver-dependent automatic mip selection. Rather than trying to force the stock shaders' implicit-LOD Sample() calls to pick mip level 1 during a draw (fragile — texel:screen-pixel ratio-dependent, driver/compiler-specific, and none of the stock HLSL shaders use an explicit SampleLevel()), this reads mip level 1 back directly via CopyTextureRegion on D3D12TextureBackend::GetResourceEXT() — the same real, direct-readback discipline DX-122/DX-123's own GetData() implementations already established, test-local since the plain 2D backend has no public GetData() of its own. A real 4×4 level-0 + 2×2 level-1 texture: UpdatePixelsLevel(1, ...) round-trips EXACT bytes for the level-1 upload (HH1), and level 0's own content is independently confirmed genuinely unaffected by the level-1 write (HH2) — proves the subresource targeting is correct, not just "some upload succeeded." Both passed on the first real Wine+vkd3d-proton run. Combined with DX-140's own 3 new checks (GG0-GG2, both landed in the same session pass), D3D12_Smoke grew 141→147/147 checks, confirmed via a real ctest run. |
| DX-142 | Per-slot SamplerState — all-16-slots-simultaneously dedicated test for D3D11 |
✅ | Closed 2026-07-14 — real proof that all 16 slots are genuinely independent, not just individually functional. Uses D3D11GraphicsBackend::GetContextEXT() (already existed) + raw ID3D11DeviceContext::PSGetSamplers() to directly verify binding state — no new backend API needed. All 16 D3D11_COMMONSHADER_SAMPLER_SLOT_COUNT slots bound simultaneously with 16 genuinely different SamplerState configurations (filter/addressU/addressV spread across TextureFilter's 6 values / TextureAddressMode's 3 values so no two adjacent slots share a config): confirmed every slot holds a real, non-null ID3D11SamplerState immediately after being applied, then re-queried every slot again after all 16 had been applied — every slot still held the exact same object pointer captured right after its own ApplySamplerState() call, proving no later slot's bind clobbered an earlier one (the specific off-by-one/aliasing risk this row's own Notes originally flagged). Both checks passed on the first real Wine+DXVK run. D3D11_Smoke 79→81/81 checks. |
| DX-143 | Multi-target (N > 1) MRT per-target MSAA-resolve/mip-regeneration-on-unbind for D3D11 |
✅ | Closed 2026-07-14 — real implementation + real GPU proof, closing DX-46's own honest scope gap. Before this, SetRenderTargets()'s MRT unbind path silently skipped the per-target MSAA resolve/mip-regen that the single-target SetRenderTarget2D() path already performed — a game binding 2+ MSAA targets as MRT and unbinding would have gotten unresolved (garbage) MSAA content. Factored the per-target work into D3D11RenderTargetBackend::ResolveAndGenerateMipsEXT() and called it for every bound target on unbind, not just index 0. Real proof (examples/d3d11_smoke_test.cpp): an N=2 MSAA MRT set genuinely resolves both targets independently on unbind (each read back separately, exact expected color); switching directly from an MRT set to a different single target also flushes the prior set's resolve (the real state-transition case an unbind-only fix would have missed). Honest note: the same ResolveAndGenerateMipsEXT() code path also covers mip-regeneration for a mipMap=true MRT target, but that specific combination is not independently pixel-tested here (the N=2 MSAA case is; DX-144 covers mip-regen itself on the single-target path). |
| DX-144 | RenderTarget2D/RenderTargetCube mip-chain generation/sampling dedicated pixel test, both D3D11 and D3D12 |
✅ | D3D12 RenderTarget2D leg closed 2026-07-14 — a real feature, not just a test, landed. D3D11 leg (closed earlier, unchanged): an 8×8 mipMap=true RenderTarget2D reports the expected 4-level chain (8×8/4×4/2×2/1×1); GenerateMips()-on-unbind writes exact, correctly box-filtered content into mip level 1 and level 2; RenderTargetCubeBackend regenerates face 0's own chain correctly too. D3D12 RenderTarget2D leg: D3D12RenderTargetBackend previously created single-level-only resources (DX-117's own documented scope note); now real. D3D12 has no single-call GenerateMips() equivalent the way D3D11 does, so rather than a manual compute/pixel-shader mip cascade (real new pipeline/shader infrastructure), this uses a synchronous CPU box-filter downsample cascade: read a level back via a READBACK-heap CopyTextureRegion, box-filter it on the CPU, upload the result to the next level via an UPLOAD-heap CopyTextureRegion — the same ExecuteCommandListAndWaitEXT-synchronous discipline this backend's own D3D12Textures.cpp/D3D12Buffers.cpp already establish for every other real upload/readback path, triggered from UnbindAsRenderTarget() (GenerateMipsEXT()), mirroring D3D11's own timing. Real proof (Checks LL0–LL2, same solid-color-preserving methodology D3D11's own closure established): an 8×8 mipMap=true render target reports the exact 4-level chain; mip levels 1 and 2 both read back the exact solid color, read directly from the real GPU resource. D3D12_Smoke grew 181→184/184 checks, confirmed via a real ctest -R D3D12_Smoke run. Two real, necessary bugs found and fixed to make this land, not just the feature itself: (1) D3D12GraphicsBackend::SetRenderTarget2D()/SetRenderTargets() never tracked or unbound the previously-bound custom render target — SetRenderTarget2D(nullptr) just blindly restored the back buffer without ever calling the prior target's own UnbindAsRenderTarget(), so GenerateMipsEXT() (and anything else hooked to unbind) would have silently never fired; added a currentCustomRT_ tracking member, mirroring D3D11GraphicsBackend's own already-established pattern exactly. (2) The RTV descriptor heap's fixed bump-allocator capacity (DX-103, raised 8→32 by DX-111) was exhausted again by this task's own new render-target allocation — raised 32→48, same documented "sized for real observed demand" precedent as the earlier raise, not silently worked around. RenderTargetCube leg also closed 2026-07-14, same session, real feature. D3D12RenderTargetCubeBackend reused the exact same GenerateMipsEXT()/ReadbackSubresourceRGBA8/UploadSubresourceRGBA8/BoxFilterDownsample machinery the 2D leg's own D3D12RenderTargets.cpp already established, only the active face's chain regenerates on unbind (mirrors D3D11RenderTargetCubeBackend's own face-0-only test precedent — only one face is ever the active draw target at a time, a real scope match, not an oversight). Subresource math: mip + face*levelCount (the standard D3D12 texture-array/mip convention this whole codebase already uses elsewhere). Real proof (Checks MM0/MM1): an 8×8 mipMap=true cube render target reports the exact 4-level chain; face 0's mip level 1 reads back the exact solid color after BindAsRenderTargetFace(0)+Clear()+UnbindAsRenderTarget(). Both passed on the first real run — D3D12_Smoke 184→186/186 checks, confirmed via a real ctest -R D3D12_Smoke run. |
| DX-145 | Per-instance RenderTarget2D/RenderTargetCube DepthStencilFormat fidelity dedicated test, both D3D11 and D3D12 |
✅ | Closed 2026-07-14 — real GPU-resource-level proof on BOTH backends, not just "a depth buffer exists". Each backend's real created depth resource is inspected for its actual DXGI_FORMAT (not merely that construction succeeded): DepthFormat::None creates no depth resource/view at all (not a silently-allocated hidden one); Depth16 genuinely lands on DXGI_FORMAT_D16_UNORM (not silently upgraded to a combined depth+stencil format, the exact fidelity failure this row exists to rule out); Depth24 lands on the documented DXGI_FORMAT_D24_UNORM_S8_UINT fallback (neither D3D11 nor D3D12 has a pure 24-bit depth-only format — the same shared decision DX-11-fmt's own mapping table already documents); and Depth24Stencil8 lands on that same format, proving Depth24/Depth24Stencil8 genuinely share one real DXGI resource format rather than coincidentally both happening to work. D3D11 checks in examples/d3d11_smoke_test.cpp, D3D12 checks II0–II3 in examples/d3d12_smoke_test.cpp. |
| DX-146 | Clear honoring ClearOptions::Stencil — dedicated test for D3D12 |
✅ | Closed 2026-07-14 — needed real implementation, not just a test, and surfaced a real bug in DX-117's own work. All 6 combo Clear* variants (ClearColorAndDepth/ClearDepth/ClearStencil/ClearDepthAndStencil/ClearColorAndStencil/ClearColorDepthAndStencil) were still NotYetImplemented() throws on D3D12; implemented via one shared ClearImpl() (rather than six near-copies) that clears the bound color target(s) — including DX-117's real MRT set — and/or the bound DSV, clearing only what it was asked for. Depth/stencil against a depth-less target is a genuine no-op, not an error, matching XNA's own semantics and D3D11's if (currentDSV_) behavior. Real bug found and fixed: D3D12RenderTargetBackend::BindAsRenderTarget() never passed its own DSV to BindOffscreenColorTargetEXT() (DX-117 created the depth resource + DSV but only bound the color view — its own header comment even admitted the gap). So binding a D3D12 render target that has a depth buffer silently gave every draw NO depth buffer: the depth test was inert and every ClearDepth*/ClearStencil* had nothing to write to. Fixed by storing dsvFormat_ and passing dsv_/dsvFormat_ on bind. Real proof (Checks JJ0–JJ6, D3D12_Smoke 151→159/159): stencil is proven by a direct GPU readback of the depth-stencil resource's stencil plane (plane-slice-1 CopyTextureRegion off the real D24_UNORM_S8_UINT resource) — ClearColorDepthAndStencil(stencil=0x5A) genuinely writes 0x5A into every pixel, and ClearStencil(0x3C) alone genuinely overwrites it to a different value, so neither is a silent no-op. This is a stronger proof than a draw-gated one, and necessary: D3D12's PSO stencil state is deliberately unwired (DX-118's own documented gap), so no draw can be gated on stencil. Depth is proven by its real effect on rasterization: the same triangle at the same z=0.5 is drawn twice, differing only in the depth a prior ClearDepth wrote — cleared-to-0.9 lets it through depthFunc=Less, cleared-to-0.1 correctly rejects it (with an explicit depth-off control check proving the draw itself works, so the rejection can only be the depth test). ClearDepth() alone is proven to leave the color target untouched. |
| DX-147 | OcclusionQuery both-directions (visible vs. occluded) discriminating pixel/query-correctness test, D3D11 now, D3D12 once DX-120 lands |
✅ | Fully closed 2026-07-14 — both backends now genuinely discriminate. D3D12 half was closed as part of landing DX-120 itself (Checks AA3/AA4: exact PixelCount()=4096 for a full-viewport visible triangle, exactly 0 for the same query object reused around off-screen/clipped geometry). D3D11 half closed here: DX-47's original check only proved a query object completes and reports 0 for an empty Begin()/End() — it never proved the count actually tracks what was rasterized (a backend returning a constant would have passed). Now the same query object is reused around a genuinely visible (viewport-covering) draw → real, positive PixelCount(), then around a fully off-screen (clipped) draw → exactly 0. Reusing one object across both directions is the point: an implementation that leaked the prior count or returned a fixed value cannot pass both. Matches the both-directions bar EasyGL/Vulkan already meet (Tasks 445/446/854). D3D11_Smoke 91→93/93. |
| DX-148 | Model/ModelMesh/ModelMeshPart/ModelBone D3D12-specific runtime-API test |
✅ | Closed 2026-07-14 — and it found a real crash bug, exactly the kind only this test could. Blocked until now because ModelMesh::Draw() needs a real GraphicsDevice (it drives SetVertexBuffer()/setIndicesProperty()/DrawIndexedPrimitives() and EffectPass::Apply()), which for D3D12 meant a real window; PresentationParameters::HeadlessEXT (commit b3289ac6) removed that. Real bug found and fixed: SetDepthTestEnabled/SetDepthWriteEnabled/SetBlendEnabled were still NotYetImplemented() throws on D3D12 — so any game (or shared XNA code) calling GraphicsDevice::SetDepthTestEnabled() against this backend simply crashed. This test is the first thing in the project to ever drive the shared GraphicsDevice/Model path against D3D12, which is why nothing had caught it. Fixed: depth test/write now map onto the real tracked PSO state DX-118 established; SetBlendEnabled is a deliberate no-op matching D3D11GraphicsBackend's own convention for all three (XNA's real blend configuration always arrives via ApplyBlendState(), and a bare "blend on" carries no factors to enable it with). Real proof (Check KK6): a real 2-bone hierarchy (root→child) drives Model::Draw() → ModelMesh::Draw()'s genuine orchestration — absolute bone transform folded into the world matrix, SetVertexBuffer + setIndices + DrawIndexedPrimitives + EffectPass::Apply — and paints the mesh's exact red over a green clear, read back from a real RenderTarget2D. Deliberately not a raw VertexBuffer/IndexBuffer draw wearing a Model label, which this row's own text warns against and which would have proven nothing new. |
Phase DX16 — Remaining EasyGL-parity gaps, round 2 (found via a full feature-matrix percentage audit, 2026-07-15)
Phase fully complete (2026-07-15) — 7 of 7 rows closed with real GPU proof, both backends. Phase
DX15 closed 18/18 of its own rows, but a follow-up exercise — computing an actual "how much of what
EasyGL does can D3D11/D3D12 also do" percentage by walking every row of
docs/graphics-backend-feature-matrix.md that has an EasyGL/D3D11/D3D12 column — found 34 rows where
EasyGL is a clean ✅ (rows where EasyGL itself has a gap, e.g. DualTextureEffect.VertexColorEnabled
or ReferenceStencil, are excluded, same exclusion rule DX15's own intro already established: a
backend can't have a "parity gap" against a feature EasyGL doesn't have either). At the time this
phase was opened, D3D11 and D3D12 both landed at ~79% fully pixel-verified (✅), ~97% functionally
implemented (✅+🟨) against that 34-row baseline; every one of the ~6 shared 🟨/⬜ rows that made up
the gap is now closed (DX-149–DX-155), so both backends now land at effectively 100% against this
specific 34-row baseline (DX-152 genuinely added a real feature — RenderTargetCube MSAA — rather
than just testing what already existed). One additional row (OcclusionQuery pixel/query
correctness) turned out to be a stale graphics-backend-feature-matrix.md doc row, not a real gap —
DX-147 already closed it for both backends; fixed directly (doc-only) rather than opened as a task
here, same as this session's other stale-row fixes (DX-29/DX-102/DX-113).
D3D11_Smoke grew 135→147/147 checks and D3D12_Smoke grew 191→212/212 checks across this
phase (DX-149–DX-155), all real GPU-facing proof through Wine+DXVK/vkd3d-proton, confirmed via
real ctest runs after every single task. One real, non-obvious finding along the way: DX-155's
own original row text assumed rootBoneIndex drives Model::Draw()'s bone-transform selection —
reading Model.cpp first found that's not the case (Draw() uses meshParentBones via
mesh->getParentBoneProperty(), not root_), so the test was corrected to prove what the code
actually does (getRootProperty() wiring) rather than a behavior that doesn't exist — see that row's
own Notes for the full detail.
Same rule as DX15: some rows need a D3D11 leg, a D3D12 leg, or both — noted per row. All 7 below are
currently ⬜ (not started) — writing them down per the project owner's request, not implementing them
yet.
| # | Task | Status | Notes |
|---|---|---|---|
| DX-149 | EnvironmentMapEffect.DirectionalLight1/2 dedicated pixel test, both D3D11 and D3D12 |
✅ | Closed 2026-07-15 — real, exact-color proof, both backends, confirming the shared-wiring hypothesis was correct. GpuDrawParams.light1Dir/light1Diffuse/light2Dir/light2Diffuse were already forwarded byte-for-byte into env_map3d's own EnvMapParams cbuffer (Light1DirPad/Light1DiffPad/Light2DirPad/Light2DiffPad) on both backends (confirmed by reading D3D12GraphicsBackend.cpp's needsEnvMap branch and its D3D11 equivalent before writing any test) — no code change needed, purely a verification gap as suspected. Reused DX-66/DX-111/DX-134's own geometrically-constrained fixture (normal +Z, white 2×2 texture, a real bound TextureCube whose content is irrelevant here — DX-134's own envMapAmount=0.0 trick collapses env_map3d.frag.hlsl's lerp(baseColor, envSample*alpha, blendFactor) to the pure lit*texColor path, so the env-map Sample() call still executes but its result never reaches the output) combined with DX-124/DX-138's own exact-color-per-term isolation methodology (light0Diffuse zeroed so only the field under test contributes). Real proof: D3D12 (Checks PP1–PP3, off-screen) — DirectionalLight1 alone → exact (255,0,0); disabling its diffuse → exact (0,0,0) (confirms PP1 wasn't a leaked default); DirectionalLight2 alone → exact (0,255,0). D3D11 (through the real back buffer, same 3 checks) — identical results. All 6 passed on the first real run on both backends. D3D11_Smoke 135→138/138 checks (also fixed this test's own hardcoded totalChecks self-check constant, which would have otherwise failed the suite even with every individual check passing); D3D12_Smoke 196→199/199 checks. Both confirmed via real ctest runs through Wine+DXVK/vkd3d-proton. |
| DX-150 | SkinnedEffect.DirectionalLight1/2 dedicated pixel test, both D3D11 and D3D12 |
✅ | Closed 2026-07-15 — real, exact-color proof, both backends, same shared-wiring confirmation as DX-149. skinned3d.frag.hlsl's Light1DirPad/Light1DiffPad/Light2DirPad/Light2DiffPad (in the FogParams cbuffer, forwarded from GpuDrawParams.light1Dir/light1Diffuse/light2Dir/light2Diffuse) feed the exact same lightSum = Light0Diffuse*NdotL0 + Light1Diffuse*NdotL1 + Light2Diffuse*NdotL2 formula lit_textured3d/env_map3d already use — confirmed by reading the shader before writing any test. Reused DX-67/DX-111/DX-135's own single-identity-bone fixture (boneCount=1/weightsPerVertex=1, so bone math doesn't complicate the lighting isolation) with a fresh white texture and DX-124/DX-138's own exact-color-per-term methodology (ambientColor/light0Diffuse/specularColor all zeroed — skinned3d's specular term is unconditionally added to the output, unlike lit_textured3d, so it needs explicit zeroing too). Real proof: D3D12 (Checks QQ1–QQ3, off-screen) — DirectionalLight1 alone → exact (255,0,0); disabled → exact (0,0,0); DirectionalLight2 alone → exact (0,255,0). D3D11 (through the real back buffer, same 3 checks) — identical results. All 6 passed on the first real run on both backends. D3D11_Smoke 138→141/141 checks (hardcoded totalChecks constant bumped again); D3D12_Smoke 199→202/202 checks. Both confirmed via real ctest runs. |
| DX-151 | SkinnedEffect.SpecularColor/SpecularPower dedicated pixel test, both D3D11 and D3D12 |
✅ | Closed 2026-07-15 — real, exact-color proof, both backends, applying the already-solved BasicEffect methodology. Confirmed skinned3d.frag.hlsl's specular formula (h = normalize(E - nL), spec = pow(max(dot(h,N),0)*zeroL, SpecularPower), summed per-light then scaled by material SpecularColor) is byte-for-byte the same shape as lit_textured3d.frag.hlsl's already-solved specular term — so DX-125(D3D11)/DX-139(D3D12)'s own half-vector-equals-normal trick (eye at (0,0,-10), surface normal (0,0,-1), light1 traveling +Z → dot(H,N)=1 exactly, an EXACT expected color regardless of SpecularPower) applies unchanged. Combined with DX-67/DX-111/DX-135's single-identity-bone fixture (boneCount=1/weightsPerVertex=1) so bone math doesn't complicate the specular isolation — one extra zeroing step versus the BasicEffect version: skinned3d has no separate ambient/light0 diffuse gate the way lit_textured3d does via LightingEnabled, so ambientColor/light0Diffuse both needed explicit zeroing (already established by DX-150's own fixture). Real proof: D3D12 (Checks RR1/RR2, off-screen) — the geometrically-constrained specular term contributes exact full-white (255,255,255); zeroing SpecularColor on the identical geometry/light produces exact black, proving the white was genuinely the specular term. D3D11 (through the real back buffer, same 2 checks) — identical results. All 4 passed on the first real run on both backends. D3D11_Smoke 141→143/143 checks; D3D12_Smoke 202→204/204 checks. Both confirmed via real ctest runs. |
| DX-152 | RenderTargetCube MSAA support + dedicated pixel test, both D3D11 and D3D12 |
✅ | Closed 2026-07-15 — real feature on both backends, reopening what was a deliberate, matched scope exclusion, not a silent oversight (project owner approved via "implementuj prosím všechny nové úkoly"). Both backends' color resource becomes MSAA-or-not (device-queried via ClampMultiSampleCount, same as DX-45(D3D11)/DX-117(D3D12) already established for the 2D leg — MSAA and a full mip chain stay mutually exclusive on the same attachment). D3D11: neither D3D11_RESOURCE_MISC_TEXTURECUBE nor a D3D11_SRV_DIMENSION_TEXTURECUBE view can ever be multisampled, so when MSAA, texture_ becomes a plain (non-cube) 6-slice Texture2DMSArray used only as an RTV target (D3D11_RTV_DIMENSION_TEXTURE2DMSARRAY per-face), and a new resolveTexture_ (real D3D11_RESOURCE_MISC_TEXTURECUBE, single-sample) is ResolveSubresource()'d from it on unbind — the real SRV always targets resolveTexture_ when MSAA. D3D12: same shape — D3D12_SRV_DIMENSION_TEXTURECUBE has no multisampled variant either, so colorResource_ becomes RTV-only (D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY per-face) when MSAA, and a new resolveResource_ is ResolveSubresource()'d via a real command-list recording (explicit RESOLVE_SOURCE/RESOLVE_DEST transitions, D3D12's own requirement D3D11 doesn't have) — mirrors D3D12RenderTargetBackend's own DX-117 follow-up design exactly. Both backends resolve only the currently-active face on unbind (activeFace_-scoped), matching each class's own pre-existing "only one face is ever the active draw target at a time" convention that mip-chain generation already established — the other 5 faces are untouched by a given unbind, same as DX-153 (below) independently confirms for mip-chain regen. D3D11GraphicsBackend::CreateRenderTargetCube()/D3D12GraphicsBackend::CreateRenderTargetCube() both now thread multiSampleCount through (previously silently dropped on both). Real proof: an 8×8 cube face requested at 4x MSAA, cleared, resolved on unbind, read back exact — D3D11 (new check, through a direct staging-texture readback of GetSampleableTextureEXT()'s face-0 subresource) and D3D12 (Checks SS0/SS1, off-screen, via GetSampleableColorResourceEXT()) both confirm the device genuinely applied real 4x MSAA (GetMultiSampleCount() == 4, not a silent single-sample fallback). D3D11_Smoke 143→144/144 checks; D3D12_Smoke 204→206/206 checks (also raised the RTV descriptor heap's fixed bump-allocator capacity 48→64, DX-103's own established pattern — a cube target allocates 6 RTVs in one call and the growing suite exhausted 48 again, same cause as the two prior raises). Both confirmed via real ctest runs through Wine+DXVK/vkd3d-proton. |
| DX-153 | RenderTargetCube mip-chain generation — non-face-0 dedicated test, both D3D11 and D3D12 |
✅ | Closed 2026-07-15 — real proof, both backends, confirming both architecturally-distinct mechanisms handle a non-zero face correctly. DX-129(D3D11)/DX-144(D3D12) both already proved face 0 specifically; neither independently proved a non-zero face — and, as this row's own text already found by reading both mechanisms, the underlying architectures genuinely differ (D3D12's GenerateMipsEXT() is explicitly activeFace_-scoped; D3D11's single, whole-resource GenerateMips(srv_.Get()) call has no face argument at all). Both proofs confirm the same outcome via their own distinct mechanism: bind face 2 (not face 0), clear to a distinct color, unbind, read back face 2's mip level 1 subresource (mip + face*levelCount) directly from the real GPU resource. D3D12 (Checks TT0/TT1, off-screen) — face 2's mip 1 reads back the exact solid color, confirming the activeFace_-scoped cascade and its subresource math generalize correctly past face 0. D3D11 (through the real back buffer, same proof) — face 2's mip 1 also reads back exact, confirming the whole-resource GenerateMips() call genuinely regenerates whichever face was actually just drawn to, not just face 0 specifically. Both passed on the first real run. D3D11_Smoke 144→145/145 checks; D3D12_Smoke 206→208/208 checks. Both confirmed via real ctest runs. |
| DX-154 | Per-slot SamplerState — all-16-slots-simultaneously dedicated test for D3D12 |
✅ | Closed 2026-07-15 — real proof, ported directly from D3D11's own DX-142. All 16 slots bound simultaneously with 16 genuinely different SamplerState configs (same TextureFilter×6 / TextureAddressMode×3 spread DX-142 uses, so no two adjacent slots share a config), introspected via D3D12GraphicsBackend::GetSamplerGpuHandleEXT(slot) (the D3D12SamplerCache equivalent of D3D11's own ID3D11DeviceContext::PSGetSamplers()): every slot holds a real, non-null descriptor handle immediately after being applied (Check UU0), then every slot is re-queried after all 16 had been applied — every slot still held the exact same handle captured right after its own ApplySamplerState() call, proving no later slot's bind clobbered an earlier one (Check UU1, the same off-by-one/aliasing risk DX-142's own row originally flagged). Both passed on the first real run, no dependency on a bound render target (sampler descriptor allocation is independent of the RTV heap). D3D12_Smoke 208→210/210 checks, confirmed via a real ctest -R D3D12_Smoke run. |
| DX-155 | Model constructor root-bone-index flexibility (Task 916's own rootBoneIndex parameter) dedicated test against D3D11 and D3D12 |
✅ | Closed 2026-07-15 — real proof, both backends, with an honest scope correction found by reading the source first (not assumed). This row's own original text expected rootBoneIndex to affect Model::Draw()'s bone-transform selection — reading Model.cpp first found that's not the case: Draw() picks each mesh's world transform via mesh->getParentBoneProperty() (the separate meshParentBones constructor argument), and CopyAbsoluteBoneTransformsTo() walks each bone's own parent chain — neither actually consults root_/rootBoneIndex anywhere. So rootBoneIndex's only currently-consumed effect in the whole class is getRootProperty() returning it. The test proves exactly that (not silently defaulting to bones[0]) and exercises the full 5-argument constructor end to end through a real draw for the first time on either backend (DX-128/DX-148's own fixtures only ever used the 3-argument constructor) — two independent top-level bones, bone0 (array index 0) a large off-screen-translating decoy and bone1 (array index 1, the actual rootBoneIndex) left Identity and the one meshParentBones targets, so a real regression in either the root-index wiring or the non-zero-bone meshParentBones selection would be caught. D3D11: a direct getRootProperty() == &bone1 check, then Model::Draw() paints the mesh's exact red over the green clear through the real back buffer. D3D12: same two checks (VV0/VV1), off-screen through the windowless GraphicsDevice/RenderTarget2D path DX-132/DX-148/DX-140 already established. All 4 passed on the first real run on both backends. D3D11_Smoke 145→147/147 checks; D3D12_Smoke 210→212/212 checks. Both confirmed via real ctest runs. |
- Historical note: this boundary originally read "do not start any task in this plan without explicit go-ahead — the whole plan is unauthorized" and "do not start Phase DX12 without a separate go-ahead." Both are now moot — the project owner authorized Phase DX1–DX13 (2026-07-13/ 14) and Phase DX14/DX15 (2026-07-14), all of which are done or substantially done (see each phase's own section). Kept here only so the pattern — don't assume authorization transfers phase-to-phase, always confirm explicitly — carries forward to any future phase (DX16+) that gets added to this plan.
- Do not claim real-Windows parity from Wine+DXVK/vkd3d-proton results alone —
DX-90/DX-114are real completion gates, not optional polish, per the "Development environment" section above. - Do not let
D3D11/D3D12-specific code leak into the sharedIGraphicsBackend/GpuDrawParamsinterface layer beyond what a genuine common-interface need justifies — same backend-locality rule every other backend plan already follows (CLAUDE.md,plan_webgpu.md/plan_headless.md/plan_software.md's own boundaries sections). - Do not merge D3D11 and D3D12 into one shared device/backend class "for less duplication" —
design decision 4 already scoped what's genuinely shared (
D3DCommon); forcing the actual device/command/resource logic to share code across two structurally different APIs is exactly the kind of premature abstractionCLAUDE.mdwarns against. - If
DX-1/DX-3(MinGW-w64 header completeness, or the Wine+DXVK loop itself) turn out not to work on this machine, stop and report the specific gap rather than silently downgrading scope (e.g. quietly deciding to only ever build on real Windows) — that would invalidate this plan's core "develop on Debian" premise and needs a project-owner decision about how to proceed. - If
DX-12-state's "D3D11 and D3D12 enum values are numerically identical" assumption turns out false for some enum, that's a legitimate, expected finding to record — not a blocker, just don't let it silently produce a wrong mapping for the divergent case. - Do not skip
DX-21's debug-layer fallback orDX-20's feature-level fallback array "since it works under Wine anyway" (design decision 12) — Wine+DXVK is not evidence that a hardcodedD3D11_CREATE_DEVICE_DEBUG/D3D_FEATURE_LEVEL_11_0-only path is safe on real Windows; this is exactly the kind of gap that only shows up onDX-90's real-Windows pass, expensively, if skipped here. - Do not leave
DX-6's COM-pointer-convention decision unresolved past Phase DX2 — every task from Phase DX4 onward creates COM objects; retrofitting aComPtr<T>convention after several phases already have bareRelease()call sites is a much larger cleanup than deciding once, early (design decision 10). DX-46/DX-47(MRT, occlusion queries) exist precisely becauseIGraphicsBackend's real default fallbacks (SetRenderTargets()silently degrades to single-target;CreateOcclusionQuery()silently returnsnullptr) make a missing capability invisible instead of a build/link error — when scoping any future backend (D3D12 or otherwise) against this interface, don't assume the task list originally written up is exhaustive just because it compiles; cross-check the interface's own optional/defaulted virtuals against what's actually implemented, the same way this gap was found (2026-07-13, comparing D3D11's real capability against EasyGL's).