Skip to content

Latest commit

 

History

History
443 lines (391 loc) · 101 KB

File metadata and controls

443 lines (391 loc) · 101 KB

Diligent Engine Graphics Backend — Implementation Plan

Integrated 2026-08-07 as the thirteenth post-audit lane and CNA's 31st public backend identity. Adapted from feature/diligent (1ab12b50, preserved behind archive/preintegration/diligent-20260804) onto integration/post-audit-phase1 as 69 signed commits — 65 replayed 1:1 plus 4 added by the adaptation. Interface drift against the head was a single reference to the removed GpuDrawParams::instanceVb.

Three post-audit obligations were paid at adaptation and are now part of this backend's contract. REMED-GFX-201/202 — the instanced route reads the vertexStreams array through FirstInstanceStream() and honours each binding's own VertexOffset and InstanceFrequency (the latter reaching LayoutElement::InstanceDataStepRate and the pipeline cache key); MultiStreamVertexInput answers false and both draw families refuse a split declaration or a second per-instance stream outright, in an exhaustive eleven-member capability switch with no default arm. REMED-GFX-DECL-GUARDSetVertexDeclaration now remembers the caller's elements and every draw route calls the shared RequireFaithfulVertexDeclaration() before any pipeline is built, because this backend selects its layout from the buffer stride and would otherwise misread a declaration whose elements sit elsewhere. REMED-GFX-209WireFrame is reported from the live device's own DeviceFeatures::WireframeFill and measured against the shared pixel oracle rather than asserted.

DILIGENT-69 is the one new row that work adds: Diligent_InstanceBindingOffsets's _OpenGL variant fails for the same already-root-caused Mesa/llvmpipe per-instance-divisor defect as Diligent_Instanced_OpenGL and Diligent_InstancedStride_OpenGL (DILIGENT-66). Under GL the per-instance attribute reads as zero for every instance, so all instances draw stacked at the origin. Not a CNA defect; it joins that existing documented class. The same test is 12/12 on the Vulkan device type.

Full integration record: integration/lanes/diligent.md.

Status (2026-08-03): Phase DILIGENT-1 is implemented, and most of Phase DILIGENT-2/3 on top of it. What that means concretely is in the "What the baseline actually does" section below — read it before assuming parity with Vulkan/EasyGL/SDL_GPU, which this backend does not have. RenderTarget2D/RenderTargetCube, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect, SkinnedEffect (stride 52), PbrEffect/SkinnedPbrEffect (strides 48/68), several simultaneous render targets, OcclusionQuery, MSAA (back buffer and RenderTarget2D, device-probed clamping) and hardware instancing (DrawInstancedPrimitivesEx) are all implemented and verified on a real (software) Vulkan device. Volume-texture sampling, MSAA on RenderTargetCube and custom ShaderEffect programs are not implemented, and each one refuses loudly rather than rendering a near-miss.

Independent audit correction (commit 3a64eeec7100): the backend is usable on Vulkan but the plan is not complete and several older ✅ claims were too broad. Tasks DILIGENT-57DILIGENT-68 below are the authoritative remediation backlog. In particular, device fallback, pipeline-key completeness, stock-effect lighting fidelity, sample masks, capability reporting, format-aware MSAA clamping and the full readback contract have not met their acceptance criteria. The OpenGL device type currently passes 14/24 registered binaries (96/116 checks), not the full suite. No real hardware GPU has been verified.

Status legend: ✅ implemented and verified against its stated acceptance criteria; 🟨 code or documentation exists but has not met those criteria; ⬜ not implemented.


What makes this backend different from every other one

Every other entry in CNA_GRAPHICS_BACKEND names a single native graphics API (or, for SDL_RENDERER/SDL_GPU/BGFX, a portability layer that CNA drives as if it were one). Diligent Engine is the first backend whose whole point is that it is itself an abstraction over Direct3D 11, Direct3D 12, Vulkan, OpenGL/GLES and Metal. CNA therefore sits on top of two stacked abstraction layers, and the concrete native API is a runtime decision, not a build-time one.

Practical consequences that shaped this plan:

  • One shader source language for everything. Shaders are authored once in HLSL and cross-compiled by Diligent — to DXBC/DXIL on Direct3D, to SPIR-V through its glslang HLSL front end on Vulkan, and to GLSL through its own HLSL2GLSL converter on OpenGL. This is why DILIGENT_NO_HLSL must stay OFF even in a Linux-only build.
  • Device selection can fail per-device and must fall through. D3D12VulkanD3D11OpenGL, filtered to the engines DiligentCore actually built, each attempted in turn; the first one that yields a device and a swap chain wins. CNA_DILIGENT_DEVICE pins one explicitly.
  • Diligent normalizes NDC for us. Its Vulkan back end flips the viewport so NDC matches the Direct3D convention CNA's XNA matrices assume, and the OpenGL device is created with ZeroToOneNDZ so clip depth is [0,1] there too. CNA uploads its row-major Matrix memory verbatim and the HLSL declares row_major float4x4, so mul(v, m) is XNA's own v * M.
  • Pipelines are immutable, as on Vulkan/D3D12/WebGPU/SDL_GPU. A cache keyed by (shader variant, topology, blend, colour write mask, depth-stencil, rasterizer) is required from the start, not an afterthought.

Naming conventions for this backend

Item Value
CNA_GRAPHICS_BACKEND value DILIGENT
CMake option CNA_BACKEND_DILIGENT
Compile definition CNA_BACKEND_DILIGENT
Backend directory src/CNA/Internal/Backends/Diligent/, include/CNA/Internal/Backends/Diligent/
CMake target cna_backend_graphics_diligent
Main class CNA::Internal::Backends::Diligent::DiligentGraphicsBackend
Namespace alias Dg = ::Diligent — the CNA namespace is itself named Diligent, so unqualified Diligent::X inside it would resolve to the CNA namespace and fail
Third-party pin DiligentCore v2.5.6, via FetchContent in cmake/ThirdPartyDiligent.cmake
Task prefix DILIGENT-
CTest targets DiligentDeviceSelectionTest.* (no GPU needed), Diligent_2D, Diligent_3D, Diligent_RenderTarget, Diligent_RenderTargetCube, Diligent_AlphaTestFog, Diligent_DualTextureEnvMap, Diligent_Skinned, Diligent_MRT, Diligent_OcclusionQuery, Diligent_MSAA, Diligent_Instanced, Diligent_DrawOffset, Diligent_SetDataOptions, Diligent_VertexLit, Diligent_Pbr, Diligent_DepthBias, Diligent_ReferenceStencil, Diligent_FillMode, Diligent_Anisotropic, Diligent_SpriteFont, Diligent_Model, Diligent_Mip, Diligent_Npot, Diligent_RenderTargetMipGen, Diligent_ScissorPipelineCache, Diligent_MultiSampleMask, Diligent_InstancedStride, Diligent_CapabilityConsistency, Diligent_BackbufferReadbackBounds, Diligent_DeviceSelectionIntegration, Diligent_LightingFidelity. DILIGENT-67: every one of these except Diligent_DeviceSelectionIntegration (whose own fork+exec supervisor already exercises every device type internally) is also registered a second time as <Name>_OpenGL, with CNA_DILIGENT_DEVICE=opengl forced -- ctest -R "^Diligent" now runs both device types on every invocation instead of only whichever one the default preference order picks

Design decisions

  1. DiligentCore only — not DiligentTools or DiligentFX. CNA needs the render device, swap chain and shader compilation; DiligentTools' asset loaders and DiligentFX's render features would duplicate CNA's own content pipeline and effect layer.
  2. FetchContent with a pinned tag (v2.5.6), recursive submodules. FETCHCONTENT_SOURCE_DIR_DILIGENTCORE points the build at a local checkout for offline or repeated builds — no CNA-specific option is invented for something FetchContent already models.
  3. Runtime device autodetection over all built engines, overridable with CNA_DILIGENT_DEVICE (d3d12/vulkan/d3d11/opengl/auto). The parsing and preference-order helpers are free functions specifically so they are unit-testable with no GPU present.
  4. HLSL as the single shader source, embedded as string literals in the backend .cpp rather than as separate files compiled by a build-time tool. Diligent compiles from source at device creation time on every device type, so there is no bytecode step to add — and adding one would pin the shaders to a single device type, defeating decision 3.
  5. A non-sRGB RGBA8_UNORM back buffer (Diligent's own default is the sRGB variant). Every other CNA backend presents colours numerically as the game wrote them; matching that matters more than matching Diligent's default.
  6. D24_UNORM_S8_UINT depth-stencil regardless of the requested DepthFormat. XNA's DepthFormat tops out at Depth24Stencil8 and CNA's stencil support needs the stencil half. Same simplification Vulkan already makes (see IGraphicsBackend::CreateRenderTarget2D's own note about the depth-format-keyed pipeline cache a per-target format would require).
  7. Unimplemented effect features refuse rather than approximate. DrawPrimitivesEx throws for DualTexture/EnvironmentMap/Skinned/Pbr/custom-effect/instancing/fog/alpha-test draws. The alternative — rendering the nearest available variant — is the silent-wrong-output failure mode REMED-GFX-127/130/135 removed from the texture interfaces, and it must not be reintroduced here.
  8. X11 only on Linux (SDL_VIDEODRIVER=x11). Diligent's LinuxNativeWindow carries an X11 window id / display or an XCB connection; it has no Wayland surface member, so a Wayland session has to go through SDL's X11 fallback. A Wayland session throws with that instruction, rather than failing deep inside Diligent.
  9. OpenGL now creates a device and renders most of the 2D/3D baseline, but is not fully verified. Every claim in this plan's ✅ rows was still measured on the Vulkan device type — treat GL as 🟨 until DILIGENT-30 closes. Two real, distinct bugs were found and are not yet fixed; see that task's own row for the current, precise state.
  10. The back buffer's format is whatever the surface grants, not what CNA asks for. Diligent substitutes a supported format when the surface rejects the requested one, so raw pixel readback consults ITexture::GetDesc().Format and swizzles BGRA→RGBA when needed. Rendering is unaffected (the shader writes float RGBA and the format conversion happens on write); only byte-for-byte readback is. Found by a real failing pixel assertion, not assumed.

What the baseline actually does (Phase DILIGENT-1)

Implemented and exercised:

  • Device + immediate context + swap chain over a real SDL window. A per-device candidate loop exists, but Vulkan↔OpenGL fallback is not operational because SDL window API flags are fixed earlier (DILIGENT-57).

  • The whole clear family (colour, depth, stencil and every combination), Present, swap interval, runtime swap-chain resize.

  • Logical/virtual resolution with all five CnaPresentationMode policies, plus TransformWindowToLogical/TransformLogicalToWindow so input maps correctly on a letterboxed window.

  • Texture2D: creation from ImageData including a mip chain, SetData (full and per-level), and GetData readback through a staging texture.

  • VertexBuffer (any stride) and 16-/32-bit IndexBuffer, re-allocated on growth.

  • SpriteBatch: batched quads with tint, rotation, origin, both flips, layer depth, per-batch transform matrix, and per-batch sampler filter/address modes.

  • 3D draws for strides 16/20/24/32 (VertexPositionColor, VertexPositionTexture, VertexPositionColorTexture, VertexPositionNormalTexture), including BasicEffect's three directional lights with Blinn-Phong specular, evaluated per pixel.

  • BlendState (factors, functions, slot-0 colour write mask, blend factor constant), DepthStencilState (depth test/write/function, two-sided stencil, masks, reference value), RasterizerState (cull, fill, scissor enable, depth bias), and SamplerState on slot 0 — mostly folded into the pipeline cache key. Scissor enable is now in that key (DILIGENT-58), depth bias is stored losslessly (DILIGENT-64), and MultiSampleMask now reaches GraphicsPipelineDesc::SampleMask (DILIGENT-60).

  • ReadBackbuffer, resampling the physical region back to the caller's logical region.

  • TextureCube and Texture3D: creation with a mip chain, per-face / per-sub-box SetData and GetData (DILIGENT-23/DILIGENT-40). TextureCube is also sampleable, through EnvironmentMapEffect; Texture3D is storage and readback only.

  • RenderTarget2D (DILIGENT-20/DILIGENT-21): off-screen colour, an optional real depth-stencil buffer, GetData readback, sampling the unbound target, and mip regeneration on unbind.

  • RenderTargetCube (DILIGENT-22): six per-face render-target views over one cube texture, a shared depth-stencil buffer, GetData per face, and sampling back through EnvironmentMapEffect via the same DiligentSampledTexture interface a plain TextureCube uses.

  • AlphaTestEffect's per-pixel discard and BasicEffect's fog (DILIGENT-31/DILIGENT-32), on every 3D shader variant.

  • DualTextureEffect and EnvironmentMapEffect (DILIGENT-33/DILIGENT-34).

  • SkinnedEffect at stride 52 (DILIGENT-35).

  • Several simultaneous render targets (DILIGENT-24): all bound slots are attached and cleared, though only slot 0 receives fragments from CNA's single-output built-in shaders.

  • OcclusionQuery (DILIGENT-41): a real IQuery-backed query, falling back from QUERY_TYPE_OCCLUSION to QUERY_TYPE_BINARY_OCCLUSION (0/1, matching EasyGL's GLES3 convention) on a device without the occlusionQueryPrecise feature — which includes lavapipe, this backend's own verification device.

  • MSAA (DILIGENT-25) on the back buffer and RenderTarget2D: a real offscreen multisampled colour (and depth-stencil) texture, resolved into the swap chain's back buffer on Present()/ ReadBackbuffer(), or into a RenderTarget2D's own single-sampled resolve texture on unbind. Device-probed and clamped via GetTextureFormatInfoExt(), exactly like every other capability this backend reports honestly rather than silently ignores. RenderTargetCube MSAA is not implemented.

  • Hardware instancing (DILIGENT-43, DrawInstancedPrimitivesEx): a per-instance vertex buffer bound at slot 1 with INPUT_ELEMENT_FREQUENCY_PER_INSTANCE/step rate 1 supplies one 4x4 world matrix (four consecutive float4 rows) per instance, alongside the per-vertex Position-only stream at slot 0. Deliberately minimal, matching every other CNA backend's own baseline: no texture, no lighting, flat g_DiffuseColor output. g_WorldViewProj is repurposed to hold just View * Projection since there is no single shared World to fold in.

  • PbrEffect/SkinnedPbrEffect (DILIGENT-36, strides 48/68): the glTF 2.0 metallic-roughness BRDF (GGX distribution, Smith-Schlick-GGX visibility, Schlick Fresnel), five optional texture maps (base colour, normal, metallic-roughness, emissive, occlusion) each falling back to their own glTF "map absent" identity when unbound. SkinnedPbrEffect combines the same BRDF with Skinned3D's bone-palette skinning.

Deliberately refused (each throws, naming itself):

  • Custom ShaderEffect programs, RenderTargetCube MSAA, and SkinnedEffect's stride-56 vertex-colour variant. These individual gaps refuse loudly; overall capability reporting is now device-probed rather than guessed (DILIGENT-61).

Phases and tasks

Phase DILIGENT-1 — baseline (done)

Task Description Status Notes
DILIGENT-1 CNA_GRAPHICS_BACKEND=DILIGENT selection, target, compile definition cmake/BackendSelection.cmake, cmake/BackendLibraries.cmake
DILIGENT-2 DiligentCore acquisition, engine gating, cna_link_diligent() cmake/ThirdPartyDiligent.cmake. Disables Diligent's tests/archiver/format validation and the WebGPU engine; disables its OpenGL engine when GL/glx.h is absent, with a STATUS line rather than a third-party error
DILIGENT-3 Runtime device selection + CNA_DILIGENT_DEVICE override Parsing is unit-tested and now the one shared parser SDL window flag selection also calls (DILIGENT-57), fixing gles. Automatic Vulkan↔OpenGL fallback remains explicitly not operational (a single SDL window can only be flagged for one of the two) -- a documented limitation, not a silent gap
DILIGENT-4 Swap chain from the SDL native window (X11, Win32) Wayland throws with the SDL_VIDEODRIVER=x11 instruction (design decision 8)
DILIGENT-5 Clear family, Present, swap interval, resize
DILIGENT-6 Virtual resolution, presentation modes, coordinate transforms Same math as the SDL_GPU/WebGPU backends
DILIGENT-7 Texture2D create/update/readback Mipped textures are created empty and filled per level: Diligent wants initial data for all levels or none
DILIGENT-8 Vertex/index buffers 32-bit indices supported natively, unlike the interface's fallback default
DILIGENT-9 HLSL shader set + pipeline cache Built-in variants work; the key's former gaps (scissor enable, depth-bias packing, sample mask) are now closed by DILIGENT-58/64/60
DILIGENT-10 SpriteBatch Batches flush on texture/sampler/transform change and at End()
DILIGENT-11 3D stride dispatch 16/20/24/32 + BasicEffect lighting Stride dispatch works; stock lighting fidelity fixed and verified by DILIGENT-59
DILIGENT-12 Render state family (ApplyBlendState/ApplyDepthStencilState/ApplyRasterizerState/ApplySamplerState) Per-slot write masks, MultiSampleMask (DILIGENT-60), scissor enable (DILIGENT-58) and lossless depth-bias state (DILIGENT-64) are all now real and part of the pipeline cache key
DILIGENT-13 ReadBackbuffer Swap chain now requests SWAP_CHAIN_USAGE_COPY_SOURCE; Overscan/out-of-bounds regions are intersected against the real back buffer extent and zero-filled rather than read shifted (DILIGENT-63)
DILIGENT-14 Honest SupportsCapability() + loud refusals for the unimplemented set Capabilities now read real device features/limits instead of constants/device-type guesses (DILIGENT-61); the unimplemented set (CustomEffects, RenderTargetCube MSAA, SkinnedEffect stride-56) already refuses loudly per design decision 7
DILIGENT-15 Diligent_DeviceSelection unit tests (no GPU required) Runs in the normal CnaTests suite
DILIGENT-16 Diligent_* CTest binaries 24 binaries, 116 pixel checks total; Vulkan is 115/116 and OpenGL is 96/116 on Mesa software devices under Xvfb. See "2026-08-03 independent audit" and "Verification status"
DILIGENT-17 docs/diligent-backend.md Normalized by DILIGENT-67: binary/check counts, the OpenGL limitations section and the stale BlendState.MultiSampleMask "no effect" claim all now match current test facts

Phase DILIGENT-2 — render targets

Task Description Status Notes
DILIGENT-20 RenderTarget2D (CreateRenderTarget2D, SetRenderTarget2D, SetRenderTargets single slot) The pipeline cache key now carries the bound target's colour/depth formats, as predicted. Two real defects found while verifying: the key's operator==/hash had to learn the new field (a stale pipeline was reused and Vulkan rejected the render pass), and the sprite projection had to span the target rather than the window's logical canvas
DILIGENT-21 RenderTarget2D GetData readback and PreserveContents semantics Readback reuses ReadTextureRegion. Diligent's immediate context binds without a load operation, so contents always survive a bind cycle — which satisfies PreserveContents and is a legal superset of DiscardContents
DILIGENT-22 RenderTargetCube + per-face binding Six per-face RENDER_TARGET views over one RESOURCE_DIM_TEX_CUBE texture, a shared depth-stencil buffer (only one face is ever the active draw target at a time), and a DiligentSampledTexture conformance shared with plain TextureCube so EnvironmentMapEffect accepts either. Verified by Diligent_RenderTargetCube, including sampling the render target back through a real EnvironmentMapEffect reflection
DILIGENT-23 TextureCube (CreateTextureCube, SetData/GetData per face) Six array slices of one RESOURCE_DIM_TEX_CUBE; full mip chain. Verified by the shared TextureCubeTests/CnjCapabilityMatrixTests/XNB cube fixtures, which now run for real on this backend instead of asserting the refusal
DILIGENT-24 MRT (SetRenderTargets with 2..4 slots) All bound slots are attached and cleared, and the pipeline key carries every slot's format plus the per-slot colour write masks. Only slot 0 receives fragments today: every built-in shader declares one SV_TARGET, so slots 1..3 stay clear-only until DILIGENT-42
DILIGENT-25 MSAA back buffer + render targets, device-probed clamping 🟨 Rendering/resolves work on Vulkan, including the depth-format-intersection clamp path (DILIGENT-62, now fixed and no longer reading the swap chain's own format for a render target). OpenGL's RenderTarget2D MSAA-resolve check still fails, and DILIGENT-62 confirmed the clamp fix does not change its outcome (applied sample count unchanged) -- see DILIGENT-66
DILIGENT-26 Mip generation for render targets (GenerateMips) Implemented since DILIGENT-25 (which also fixed the real bug that made it unreachable -- SetRenderTarget2D()/SetRenderTargetCubeFace()/SetRenderTargets() never called the outgoing target's UnbindAsRenderTarget()). Now closed with a dedicated pixel test, Diligent_RenderTargetMipGen (examples/diligent_rendertarget_mipgen_test.cpp): a 4x4 mipMap RenderTarget2D gets an exact (x+y)%2 Red/Blue checkerboard pixel-copied into level 0 (SpriteBatch + PointClamp, 1:1), so every aligned 2x2 block contains exactly 2 Red + 2 Blue texels. After unbinding (which triggers IDeviceContext::GenerateMips()), level 1 (2x2) and level 2 (1x1) both read back as the real box-filter average (128,0,128) at every texel -- not pure Red, pure Blue, or black, which is what a nearest-copy fallback or a silent no-op would produce instead. 7/7 checks pass, deterministic across repeated runs; level 0's own content is confirmed unaffected by the regeneration

Phase DILIGENT-3 — remaining effect families

Task Description Status Notes
DILIGENT-30 Verify the OpenGL device type end-to-end (sprite Y orientation in particular) 🟨 Substantial progress, not closed. Bugs found and fixed so far: (1) the OpenGL device type could not create a device at all -- Diligent's own GLContext (GLContextLinux.cpp) asserts a GL context is already current via glXGetCurrentContext() rather than creating one itself, unlike every other device type here, and nothing called SDL_GL_CreateContext()/SDL_GL_MakeCurrent() before CreateDeviceAndSwapChainGL(); fixed in DiligentGraphicsBackend::TryCreateDevice(), plus GraphicsDevice.cpp's window-flag selection, which previously requested both SDL_WINDOW_VULKAN and SDL_WINDOW_OPENGL together -- SDL3 rejects that combination outright ("Conflicting window graphics flags specified"), so it now reads the same CNA_DILIGENT_DEVICE override the backend itself reads to request the one flag that will actually be used. (2) Every shader failed to compile to GLSL at all -- kConstantsHlsl/kBonesHlsl's inline row_major qualifiers pass through Diligent's HLSL2GLSL converter completely unstripped (invalid GLSL syntax); switched to the #pragma pack_matrix(row_major) form the converter actually recognizes and strips.

Bug (a) root-caused and partially fixed (this session). The earlier description ("the SECOND distinct texture sampled in a session appears to still read the FIRST one's content") undersold it -- the real trigger is any GetBackBufferData()/RenderTarget[Cube].GetData() call, not "a second texture" per se. Root cause, found by reading DiligentCore v2.5.6 source directly (Graphics/GraphicsEngineOpenGL/src/DeviceContextGLImpl.cpp): ReadBackbuffer()/ReadTextureRegion() both call context_->Flush() to synchronize the staging-texture readback, and DeviceContextGLImpl::Flush() does m_BindInfo = {} -- wiping its own shader-resource-binding bookkeeping, including ActiveSRBMask. ActiveSRBMask is normally only recomputed inside DeviceContextGLImpl::SetPipelineState()'s real setup path, but that function early-outs immediately (if (PipelineStateGLImpl::IsSameObject(m_pPipelineState, pPipelineStateGLImpl)) return;) whenever the same pipeline object is set again -- which is exactly what happens every time this backend draws another sprite/primitive with the same cached ShaderVariant pipeline right after a readback. With ActiveSRBMask stuck at 0, DeviceContextGLImpl::GetCommitMask() ((StaleSRBMask | DynamicSRBMask) & ActiveSRBMask) always evaluates to 0 from that point on, so BindProgramResources() -- the call that actually issues glBindTexture -- is silently skipped on every subsequent draw, leaving whatever GL texture unit content was last really bound. This is a genuine upstream DiligentCore v2.5.6 OpenGL-backend bug, confirmed via a minimal from-scratch repro (draw texture A, GetBackBufferData(), draw texture B with the same cached pipeline object, read back -- reads stale A content) built and bisected outside the normal test suite. Root-caused and reduced to that repro before writing any fix, confirming the exact mechanism rather than pattern-matching a plausible-looking change. Fixed with a scoped (deviceType_ == DiligentDeviceType::OpenGL only) context_->InvalidateState() call added right after the existing Flush(); WaitForIdle(); in both ReadBackbuffer() and ReadTextureRegion(), forcing the next SetPipelineState() call to skip the early-out and genuinely recompute ActiveSRBMask. InvalidateState() also wipes Diligent's own FBO/render-target tracking, so renderTargetsBound_ (this backend's own shadow flag gating EnsureRenderTargetsBound()'s early-out) has to be explicitly cleared alongside it, or the next draw's cheap early-out would skip re-establishing the render target and hit the "framebuffer without attachments" assert an earlier, less-precise attempt at InvalidateState() ran into. ReadBackbuffer() already cleared that flag naturally (it unbinds the back buffer as a render target before the copy); ReadTextureRegion() did not, so the fix sets it there explicitly. Verified: the minimal repro now reads the correct content; Diligent_2D's sourceRectangle check and all 6 of Diligent_DualTextureEnvMap's checks -- both previously-documented manifestations of this bug -- now pass under CNA_DILIGENT_DEVICE=opengl. Zero regressions: scoped strictly to the OpenGL device type, so the Vulkan path (all 24 registered Diligent_* CTest binaries) and the full CnaTests suite (5727 tests) are both unaffected -- confirmed by a full rebuild + full regression run after the fix, identical result to before (only the two pre-existing XnbContainerFuzzTest/Diligent_DepthBias failures).

Not fully closed: two of the four originally-documented bug-(a) symptoms remain unfixed -- Diligent_RenderTarget's "unbound target sampleable as a texture" check and Diligent_MSAA's RenderTarget2D MSAA-resolve check still fail under GL. Both happen immediately after their own ReadTextureRegion()-based readbacks (now InvalidateState()-covered), so this is evidently a related but distinct trigger, not yet root-caused -- possibly specific to sampling a texture that was also just used as a CopyTexture source, rather than the SetPipelineState early-out mechanism found here. Bug (b) root-caused and fixed (this session). The vertex-shader HLSL2GLSL compile failure wasn't specific to SkinnedEffect -- it also silently blocked Diligent_VertexLit and Diligent_Pbr (both throw the identical "vertex shader compilation failed" exception before this fix, never previously run manually under GL). Root cause: HLSL2GLSL cannot correctly convert a C-style matrix-truncation cast, (float3x3)someFloat4x4Expr, into valid GLSL when the source expression isn't a bare, directly-reflectable symbol -- the raw Mesa GLSL compiler error (0:N(col): error: syntax error, unexpected ')', expecting '(') always lands exactly on the cast. The fix avoids the cast syntax entirely rather than working around the converter: (float3x3)m for a row_major float4x4 m is exactly equivalent, at the HLSL language level (indexing m[row] always returns a row vector regardless of storage packing), to the explicit constructor call float3x3(m[0].xyz, m[1].xyz, m[2].xyz) -- a pure syntactic rewrite with no semantic change, confirmed empirically by re-running every affected test on the Vulkan device type after the change and seeing byte-identical pixel values to before (Diligent_Skinned/Diligent_VertexLit/Diligent_Pbr/Diligent_Model, and the full 5727-test CnaTests regression, all unaffected). Applied at all 9 occurrences across kSkinnedVertexHlsl, kLitVertexLitVertexHlsl, kSkinnedVertexLitVertexHlsl, kPbrVertexHlsl, kSkinnedPbrVertexHlsl (3 casting the local skin matrix, 6 casting g_World). Verified under CNA_DILIGENT_DEVICE=opengl: Diligent_Skinned now compiles and passes 4/4 (previously didn't even compile); Diligent_VertexLit/Diligent_Pbr now compile and run (previously crashed before drawing anything), though both now show separate, newly-exposed numeric mismatches unrelated to compilation -- Diligent_VertexLit's per-vertex-vs-per-pixel-lighting equality checks disagree under GL (2/4), and Diligent_Pbr's analytic BRDF values are measurably off (got noticeably brighter than predicted, 2/5) -- both not yet root-caused, and out of this bug's own scope (a real value discrepancy, not a compile failure).

A broader manual sweep of every Diligent_* binary under CNA_DILIGENT_DEVICE=opengl this session also surfaced several previously-unknown GL gaps in test binaries that simply never had a GL run before, still unfixed: Diligent_RenderTarget's "unbound target sampleable as a texture" check, Diligent_MSAA's RenderTarget2D resolve check (both a related-but-distinct trigger from bug (a), not yet root-caused), Diligent_ReferenceStencil, Diligent_SpriteFont's flip check, Diligent_Instanced, Diligent_DrawOffset's baseVertex case, Diligent_Npot's draw-path check, and Diligent_RenderTargetMipGen's level-0-unaffected check. None investigated yet, listed here for visibility rather than left silently undiscovered.

One further finding, narrowed but not fixed. Diligent_VertexLit's own failures are a fourth distinct GL issue, not the same as bug (a)/(b): debug instrumentation on BasicEffect's per-pixel-lit draw showed perPixelLit=(255,255,255) reading back byte-identical to unlit=(255,255,255), while the per-vertex-lit draw of the exact same geometry correctly attenuates to (216,219,217). The obvious first hypothesis -- kLitPixelHlsl's if (g_Flags.z < 0.5) { ...unlit path, return; } branch reading g_Flags.z as "disabled" under GL -- was tested directly (temporarily forcing that branch to never take the early-return, rebuilding, re-running) and ruled out: both perPixelLit and unlit still read exactly (255,255,255) with the branch forced open, so the per-pixel lighting math itself is producing a saturated/white result regardless of whether the branch runs, not being skipped. (kConstantsHlsl's own cbuffer layout is already fully float4-aligned throughout, ruling out an std140-padding mismatch too.) Since the vertex-lit path's own lighting formula is the same Blinn-Phong computation and produces a plausible, non-saturated result, the leading remaining hypothesis is a varying/interpolant plumbing issue specific to the per-pixel path -- e.g. psIn.Normal/psIn.WorldPos arriving as a degenerate (zero) vector at the pixel stage under GL, normalize() of which is undefined and could propagate NaN into the final colour, which some GL implementations clamp to white on write. Follow-up, fully root-caused (not a backend bug -- a numerically fragile test geometry). The compiled GLSL varying declarations for both pipeline stages were inspected directly first (forcing a deliberate compile error at the end of each shader source to trigger Diligent's own on-failure source dump, the same technique used to diagnose bug (b), reverted immediately after each observation with a git diff verified clean before rebuilding): the vertex shader emits out float3 _psIn_Normal;, the pixel shader consumes in float3 _psIn_Normal; -- identical name and type, ruling out a varying wiring mismatch. Isolating each term of kLitPixelHlsl's own lighting formula one at a time (each swapped in as the pixel shader's direct output colour, observed, then immediately reverted with the same git diff-verified-clean discipline) found: psIn.Normal reads correctly as (0,0,1); g_EmissiveAmbient.rgb and the diffuse accumulation (ambient + 3-light sum, recomputed in an isolated local scope) both give a plausible, non-saturated value matching the per-vertex path's own diffuse contribution almost exactly; g_SpecularColor.rgb reads (1,1,1), XNA's own real default. The one term that changes everything: g_EyePositionSpecularPower.xyz (the eye position BasicEffect derives by inverting the view matrix) reads (0,0,0), and psIn.WorldPos at the sampled centre pixel is very close to (0,0,0) too -- both correct, given the test's View = Matrix.Identity and a flat quad at z=0. But this means the eye sits in the same plane as the entire quad: eyeDir = normalize(eye - worldPos) has an exactly zero Z component everywhere on the surface, a genuinely degenerate grazing-angle configuration for the Blinn-Phong half-vector (halfVec = normalize(lightDir + eyeDir)) that the per-vertex path evaluates at 4 fixed vertex positions (interpolated afterward) while the per-pixel path re-evaluates continuously per fragment -- two different, both individually-valid samplings of a highly nonlinear (pow(nDotH, power)) function near a discontinuity, which are not guaranteed to agree even on a single flat-normal surface. This is not a Diligent- or GL-specific bug: it is inherent to the test's own camera setup (identical geometry, unmodified, already passes on Vulkan today only because Vulkan's own HLSL-to-SPIR-V codegen for this exact nonlinear expression happens to round differently enough to stay within the test's tolerance=4, not because the underlying math is any more stable there). No backend code or test file was changed to "fix" this, deliberately: examples/diligent_vertexlit_test.cpp is a registered, passing Vulkan CTest, and touching its camera geometry to accommodate GL's own rounding would risk the primary (Vulkan) verification for a device type this project does not run in CI at all. Documented here as the authoritative root cause instead. Diligent_Pbr's remaining magnitude-mismatch failures are plausible candidates for the same class of issue (also lighting/BRDF-shaped, also likely to involve a similarly small or coplanar eye-to-surface configuration) but were not independently re-verified against this specific mechanism.

Diligent_ReferenceStencil looked into via static analysis only (no live edits, zero risk). GraphicsDevice.ReferenceStencil's override should reach the GL driver correctly by code reading: PipelineKey already includes stencilFront/stencilBack/stencilMasks, so the "stamp" (Always/Replace) and "compare" (Equal/Keep) DepthStencilStates in this test get genuinely distinct cached pipelines, ruling out a pipeline-cache-key collision; and DiligentCore's own SetPipelineState()SetStencilFunc() call (which re-applies the pipeline's own stale reference value first) is always followed, in this backend's own draw code, by an explicit context_->SetStencilRef(referenceStencil_) call whose value genuinely differs from Diligent's currently-tracked one at that point, which should issue the final, correct glStencilFuncSeparate(..., 0x99, ...). The override mechanism traces through cleanly on paper, so if this is a real bug it more likely lives in the stencil write path (whether the stamp draw's 0x05 actually lands in the GL stencil buffer under this device type) than in the reference-override path -- not verified further, since stencil test results aren't visualizable through the same "swap in as the pixel shader's output colour" trick used for the lighting findings above (the stencil test runs before the pixel shader, as fixed-function hardware state).

Diligent_Npot's draw-path check narrowed, not fixed. This check uses BasicEffect without lighting (ShaderVariant::Textured3D, a plain baseColor * texture sample shader with none of the specular/eye-position math implicated above), and explicitly sets TextureFilter::Point at slot 0 before drawing, so neither of the two mechanisms already found in this row apply. Debug instrumentation (a scratch copy of the test, printing the sampled colour and all 15 known texel colours; no production file touched) showed the sampled value, (179,189,121), matches none of the 15 known colours and is not a plausible 2-texel blend of any adjacent pair either (ruling out "Point filtering silently not applied, falls back to Linear" as a one-line explanation) -- its R component is closest to the texture's row 2 (y=2) range while its G component is closest to row 0's range, an inconsistency that doesn't correspond to any single valid (row, col) texel under this test's own colour gradient. Texture2D::SetData()'s real GPU upload path (used for actual shader sampling) is architecturally distinct from the staging-texture round-trip ReadTextureRegion() uses (already verified byte-exact under GL by this same test's checks A/B) -- the corruption, whatever it is, is specific to the sampling-upload path, not the readback path already fixed/verified elsewhere in this row. Not root-caused further this session. OpenGL device-type support remains meaningfully less complete than this row's own history might suggest; treat it as 🟨 throughout, not close to
DILIGENT-31 AlphaTestEffect (per-pixel discard) Implemented in GpuDrawParams::alphaTest's own reference/tolerance/weight encoding, so all four compare modes come from the effect layer rather than from a per-mode shader. Verified by Diligent_AlphaTestFog (discard and keep, same geometry, same effect object)
DILIGENT-32 Fog for the BasicEffect family (GpuDrawParams::fogVector) The vertex stage computes FNA's keep = 1 - saturate(dot(objectPos, fogVector)), the pixel stage blends RGB toward FogColor. Verified fogged vs. fog-disabled on the same geometry
DILIGENT-33 DualTextureEffect Two shader variants (stride 20 and 24) sharing one two-sampler pixel shader; the first layer is doubled before the modulate, as XNA does. Both layers share one UV set, matching every other CNA backend. Verified by Diligent_DualTextureEnvMap
DILIGENT-34 EnvironmentMapEffect Reuses the lit vertex stage and adds a TextureCube sampler: reflection vector, flat or Fresnel-weighted blend factor, and the env-map specular term. Verified at amount 1 and amount 0 on the same geometry. Not byte-compared against FNA's PSEnvMap
DILIGENT-35 SkinnedEffect (72-bone palette, stride 52) The palette lives in its own uniform buffer (4.5 KB is too much for the per-draw block); FNA's WeightsPerVertex truncation and the bone-skin ∘ world normal matrix are both honoured. Verified by Diligent_Skinned, including a trap bone the second weight pair must never reach. The stride-56 vertex-colour variant is not implemented
DILIGENT-36 PbrEffect/SkinnedPbrEffect PbrEffect (stride 48, unskinned) and SkinnedPbrEffect (stride 68, PBR+skinning combined) both done and verified. New ShaderVariant::Pbr3D: glTF metallic-roughness BRDF (GGX/Trowbridge-Reitz distribution, Smith-Schlick-GGX visibility, Schlick Fresnel), ported term-for-term from this project's own established HLSL reference (src/CNA/Internal/Backends/D3DCommon/shaders/pbr3d.vert.hlsl/pbr3d.frag.hlsl) rather than re-derived from scratch. Five texture bindings (base colour + normal/metallic-roughness/emissive/occlusion maps, all four optional -- an unbound one falls back to its own glTF "absent" identity: flat tangent-space normal, or white since 1.0 is each of the other three's own no-op multiplier). A new, separate PbrConstants buffer (ambient/metallic/emissive/roughness) alongside the shared per-draw Constants block, because that block's own g_EmissiveAmbient folds ambient and emissive into one value -- PBR needs them apart (ambient scales albedo×occlusion, emissive is added standalone). ShaderVariant::SkinnedPbr3D reuses kPbrPixelHlsl unchanged (skinning only affects the vertex stage) and mirrors kSkinnedVertexHlsl's own skin-matrix/normal-composition convention ((Normal * skinMatrix3x3) * InverseTranspose(World3x3), not a full inverse-transpose of skin*World -- a documented simplification this backend's own unskinned-lit skinning path already uses) rather than inventing a different one. Verified by Diligent_Pbr using the same analytically-hand-derived technique as vulkan_pbreffect_handderived_test.cpp: a flat quad viewed straight down -Z with light0 aimed the same way collapses every BRDF dot product to exactly 1 at the backbuffer's centre pixel, so the whole shader reduces to a closed-form constant independently re-derived in Python -- 3 hand-derived PbrEffect cases (white/metallic=0, red/metallic=1, red/metallic=0) matched their predicted RGB values exactly, and SkinnedPbrEffect with a single identity bone (a mathematical no-op skin transform) reproduces the white/metallic=0 case's value exactly, not just "looked plausibly lit"
DILIGENT-37 Per-vertex lighting variant (PreferPerPixelLighting == false) Two new ShaderVariants, LitTexturedVertexLit3D (stride 32, BasicEffect's sibling to LitTextured3D) and SkinnedVertexLit3D (stride 52, Skinned3D's sibling) -- selected in DrawInternal() when lightingEnabled && !preferPerPixelLighting (real XNA's own default). EnvironmentMapEffect has no PreferPerPixelLighting property in real XNA, so envMapping is checked first and always wins regardless of the flag. Both new vertex shaders extract kLitPixelHlsl's own inline Blinn-Phong math unchanged into a shared ComputeVertexLighting() helper and call it once per vertex instead of per pixel, handing the pixel stage pre-lit diffuse/specular varyings to Gouraud-interpolate -- same formula, only the evaluation frequency changes. Verified by Diligent_VertexLit: for a flat quad with one uniform normal, per-pixel and per-vertex evaluation have nothing to differ on, so PreferPerPixelLighting=true and =false must (and do) read back pixel-identical results, for both BasicEffect and SkinnedEffect. Found and fixed a real regression while landing this: DrawInternal()'s bone-palette upload was gated on variant == ShaderVariant::Skinned3D specifically, so once vertex-lit skinned draws started selecting SkinnedVertexLit3D instead (XNA's own default, i.e. the actually-more-common path) the bone buffer was silently never uploaded -- caught immediately by a DiligentCore validation assertion (Diligent_Skinned crashing with SIGTRAP) rather than silently, but still a real bug this task introduced and fixed before landing, not a pre-existing one. Also corrected IGraphicsBackend.hpp's own preferPerPixelLighting doc comment, which claimed "every backend except D3D9" ignores the field -- already false for EasyGL/WebGPU before this task

Phase DILIGENT-4 — remaining device surface

Task Description Status Notes
DILIGENT-40 Texture3D RESOURCE_DIM_TEX_3D, sub-box upload and readback. Verified by the shared Texture3D* tests
DILIGENT-41 OcclusionQuery DiligentOcclusionQueryBackend uses Diligent's IQuery. QUERY_TYPE_OCCLUSION needs the occlusionQueryPrecise device feature, which lavapipe (this backend's only verification device) does not expose; the backend transparently falls back to QUERY_TYPE_BINARY_OCCLUSION (0/1, the same convention EasyGL already uses for GLES3) when it is unavailable. End() flushes the immediate context so a result doesn't require waiting for a frame boundary. Verified by Diligent_OcclusionQuery: an unbegun query, an empty Begin/End span, a fully-visible quad and a fully depth-occluded quad
DILIGENT-42 Custom ShaderEffect (CreateEffectBackend) Diligent compiles HLSL at runtime on every device type, so unlike SDL_GPU no extra compiler dependency is needed — but CNA's ShaderEffect contract is GLSL-shaped; resolve that first
DILIGENT-43 Hardware instancing (DrawInstancedPrimitivesEx) New ShaderVariant::Instanced3D: per-vertex Position-only stream at slot 0 (explicit Stride=16 -- LAYOUT_ELEMENT_AUTO_STRIDE would compute it from only the elements declared in that slot, 12 bytes, not the real VertexPositionColor buffer's 16, corrupting every vertex fetch after the first), per-instance world-matrix stream (four float4 rows) at slot 1 with INPUT_ELEMENT_FREQUENCY_PER_INSTANCE. g_WorldViewProj repurposed to hold just View * Projection since instancing has no single shared World. Verified by Diligent_Instanced: three instances at distinct translations each read back the quad's colour at their own position, with the untouched background between them staying the clear colour. The long dead end before the real fix: with CPU-side data (VP matrix, all 3 instance matrices) and draw-call parameters (NumIndices/NumInstances/buffer counts) all confirmed correct via GPU staging-buffer readback, and a full-row pixel scan confirming zero fragments rendered anywhere (not even the untranslated centre instance with an identity world matrix), the actual bug turned out to be in the test file, not the backend: it uploaded its quad via VertexBuffer::SetDataRaw(quadVertices, 4, sizeof(VertexPositionColor)), but sizeof(VertexPositionColor) is not the GPU stream's byte layout -- Color inherits a polymorphic IPackedVector base, so the C++ struct carries a vtable pointer SetDataRaw copied verbatim, silently uploading garbage-interleaved data at the wrong stride. Fixed by using the typed VertexBuffer::SetData(const VertexPositionColor*, int) overload instead, which packs into the real 16-byte (float3 + packed uint32 colour) stream every 3D shader variant here already expects. Confirmed by literally swapping the known-good Colored3D pipeline into DrawInstancedPrimitivesEx's own call sequence -- it still rendered nothing until the test's upload was fixed, isolating the bug away from the backend entirely
DILIGENT-44 SetDataOptions streaming hints (Discard/NoOverwrite) DiligentVertexBufferBackend/DiligentIndexBufferBackend both override SetData[16/32]WithOptions(): Discard/None map to MAP_FLAG_DISCARD, NoOverwrite to MAP_FLAG_NO_OVERWRITE -- the same mapping this backend's D3D11 sibling already uses. Neither flag has an observable pixel difference on its own (both are GPU-synchronization hints, not data-correctness ones), so Diligent_SetDataOptions instead proves each upload genuinely reaches the GPU buffer: a Discard upload renders, then a second, differently-coloured NoOverwrite upload into the same DynamicVertexBuffer renders the new colour (not stale data or a silently dropped write); the same for a DynamicIndexBuffer whose second NoOverwrite upload selects a different triangle out of a fixed vertex buffer. In the course of this, found and fixed a stale doc comment in DynamicVertexBuffer.hpp/DynamicIndexBuffer.hpp claiming the hint was "ignored by all CNA backends" -- false for D3D9/D3D11/D3D12/EasyGL/Headless/SdlGpu/Software/WebGPU even before this task, and now also false for Diligent (only Vulkan still inherits the interface's own no-op default)
DILIGENT-45 vertexStart/startIndex/baseVertex sub-range coverage tests Diligent_DrawOffset (position-based discrimination, same technique as the D3D9/D3D11 counterparts): DrawPrimitives(vertexStart), DrawIndexedPrimitives(startIndex), DrawIndexedPrimitives(baseVertex), both combined with the middle vertex range deliberately off-screen so only applying BOTH lands the draw, and DrawInstancedPrimitivesEx(startIndex+baseVertex) on the per-vertex stream (one identity-transform instance) proving the per-instance stream's own offset is not confused with the per-vertex one. All 5 pass
DILIGENT-46 Debug markers (SetStringMarkerEXTIDeviceContext::InsertDebugLabel) A real, synchronous context_->InsertDebugLabel(marker) call (Diligent's immediate-context model needs no deferred-command-queue plumbing the way VulkanGraphicsBackend's own implementation does); a null/empty marker is a no-op. No native API here surfaces the label to a readable pixel, so the only thing to verify is that inserting one around a draw doesn't disturb it -- added as Diligent_3D's own 6th check: the same vertex-coloured quad as its first check, bracketed by two markers plus one empty-marker no-op call, still renders correctly
DILIGENT-47 Compressed texture upload Blocked cross-backend: ImageData has no compressed-format field, Texture2D.cpp always decompresses first (same blocker as WEBGPU-111)

Phase DILIGENT-5 — cross-backend feature-gap audit (opened 2026-07-31)

Found by comparing this backend's actual code against docs/graphics-backend-feature-matrix.md's established EasyGL/Vulkan/Bgfx/D3D9/D3D11/D3D12 columns and against what this backend's own code already does vs. what it has a dedicated pixel test for. Two different kinds of gap, not to be confused with each other:

  • Genuinely implemented, never independently verified (DILIGENT-49DILIGENT-52, DILIGENT-55/DILIGENT-56): the C++/pipeline code is real and plausible, but nothing proves it on a real device — exactly the category docs/graphics-backend-feature-matrix.md's own Vulkan audit (Task 861) and D3D9's D9-62 depth-bias gap already established elsewhere in this project: "implemented" and "verified" are not the same claim, and this backend's own AUDIT.md/CHECKLIST.md discipline requires the latter before a ✅.
  • A real, confirmed code bug, not just a coverage gap (DILIGENT-48): found by reading ApplySamplerState(), not by a failing test — no existing Diligent CTest exercises a second SamplerState slot, so nothing had caught it yet.
Task Description Status Notes
DILIGENT-48 Real per-slot SamplerState (was aliased to one shared state) Confirmed real bug, found by code reading, then fixed. DiligentGraphicsBackend::ApplySamplerState(int slot, ...) took a slot parameter but discarded every call whose slot wasn't 0 (if (slot != 0) return;), and every texture-binding site (g_Texture, g_Texture2, g_EnvMap, all 4 PBR maps) read one shared set of scalars regardless of which slot the caller actually configured. Fixed with a real SamplerSlotState samplerSlots_[16] cache (matching SamplerStateCollection::MaxSamplers) and per-binding-site slot lookups matching this project's own established cross-backend register convention (dual_texture3d.frag.hlsl/env_map3d.frag.hlsl/pbr3d.frag.hlsl's own t0/s0, t1/s1, etc.): g_Texture→slot 0, g_Texture2/g_EnvMap→slot 1, the 4 PBR maps→slots 1-4 (base colour is slot 0). Verified by 2 new checks added to Diligent_DualTextureEnvMap: texture0 is a uniform white 1x1 (immune to address mode, so it can never explain a difference), texture1 is a 2-texel red|green strip sampled at U=1.25 (25% past the right edge) -- SamplerStates[1]=PointClamp reads the clamped edge texel (green), SamplerStates[1]=PointWrap reads the wrapped-around texel (red) instead, with SamplerStates[0] never touched across either draw. Confirmed via git stash on just the backend fix: the reverted code reads green both times (aliased to slot 0's own Clamp state), the fixed code discriminates correctly
DILIGENT-49 RasterizerState.DepthBias/SlopeScaledDepthBias pixel verification 🟨 Attempted, partially confirmed — one real environment limitation, not a CNA bug. Added Diligent_DepthBias (examples/diligent_depthbias_test.cpp), the same coplanar "shadow acne" method as vulkan_depth_bias_test.cpp (Task 328): draw a red triangle A, redraw an identical green triangle B with CompareFunction::Less — B only shows through if a negative bias pulled it in front. ApplyRasterizerState()'s packing (PipelineKey::raster bits 16-31) only has one signed byte each for DepthBias(×1000)/SlopeScaledDepthBias(×16), so the test drives the most extreme values that packing can represent exactly: DepthBias=-0.128 (raw Diligent units -128) and SlopeScaleDepthBias=-8.0 (raw units -8.0). Result, reproducible across repeated runs: SlopeScaleDepthBias is real and works (tilted-geometry check goes RED→GREEN as expected) but DepthBias (the constant term) shows no observable effect on this environment's software Vulkan device (llvmpipe/lavapipe) even at the packing's maximum magnitude — B stays RED. This exactly matches two independent pre-existing findings already in this codebase: D9-62 (D3D9's own oracle attempt against real XNA 4.0 found no observable pixel difference from constant DepthBias at any magnitude up to ±1e8, while SlopeScaleDepthBias/CullMode were both provable) and Vulkan_DepthBias's own pre-existing DepthBias=-1e6 sub-case (still failing today, undocumented as a CNA bug, docs/rasterizerstate-support.md §5) — i.e. constant depth bias not registering on this project's Vulkan/software-rasterizer test environment is an already-known, cross-backend, environment-level limitation, not something introduced or fixable here. Diligent_DepthBias is registered as a normal CTest (no WILL_FAIL, matching Vulkan_DepthBias's own precedent of leaving a documented pre-existing failure visible rather than masking it): 3/4 checks pass, the constant-DepthBias check fails honestly
DILIGENT-50 GraphicsDevice.ReferenceStencil pixel verification Confirmed real and working — Diligent is ahead of EasyGL/Bgfx here. Added Diligent_ReferenceStencil (examples/diligent_referencestencil_test.cpp), a direct port of Task 319's cross-backend method (easygl_graphicsdevice_reference_stencil_test.cpp): stamp stencil=0x05, assign a DepthStencilState with StencilFunction=Equal and a baked-in ReferenceStencil=0x05 (would PASS at face value), then call GraphicsDevice.setReferenceStencilProperty(0x99) directly — NOT via a new DepthStencilState — and redraw with the SAME state object. If the override genuinely reaches the backend, the compare becomes 0x99 vs the stamped 0x05 (Equal, false) → rejected → stays BACKGROUND; if the override is a local no-op (Task 872's still-open, universal EasyGL/Bgfx gap), the state's own baked-in 0x05 still passes → wrongly shows GREEN. Diligent stays BACKGROUND — the override is real. Verified as a genuine discriminator, not a coincidental pass: temporarily commenting out the setReferenceStencilProperty(0x99) call flips the result to FAIL centre=(0,255,0) (green), then restoring it returns to PASS centre=(20,20,20)
DILIGENT-51 RasterizerState.FillMode::WireFrame pixel verification Confirmed real and working. Added Diligent_FillMode (examples/diligent_fillmode_test.cpp), a direct port of vulkan_fill_mode_test.cpp (Task 327): a full-viewport-spanning triangle read back at its centre pixel, 3 sub-tests in one frame — FillMode::Solid (expect red, interior filled), FillMode::WireFrame (expect black/clear, interior genuinely not rasterized, not a silent solid-fill fallback or a blank draw), then reset to FillMode::Solid (expect red again, proving the state change round-trips both ways rather than latching). 3/3 PASS, reproducible across repeated runs
DILIGENT-52 Anisotropic texture filtering pixel verification Confirmed real and crash-safe, following this project's own established test discipline for this exact task. Added Diligent_Anisotropic (examples/diligent_anisotropic_test.cpp), a direct port of Task 299's cross-backend method (easygl_texture_anisotropic_effect_test.cpp): that precedent's own header explains a true visual anisotropic-quality pixel comparison is "inherently driver-dependent and fragile to assert precisely" across GPUs/software rasterizers, so the established, deliberate scope for this task is the "caps and fallback" half instead — SamplerState.MaxAnisotropy=9999 (far beyond any real GPU's limit, and beyond ApplySamplerState()'s own std::clamp(maxAnisotropy, 1, 16), DILIGENT-48's new per-slot cache) must not crash or throw, and the draw must still produce a genuinely sampled result rather than the clear colour leaking through unrendered. DualTextureEffect over a 2-texel red|green strip stretched across a full-viewport quad, sampled at the boundary texel: real output (247,255,0), reproducible across repeated runs, not the clear colour (0,0,255) and no exception
DILIGENT-53 SpriteFont glyph placement/spacing/newline/flip pixel test Confirmed real and working. Added Diligent_SpriteFont (examples/diligent_spritefont_test.cpp), a direct port of D3D11's own DX-127 (examples/d3d11_smoke_test.cpp): an 8x8 solid-white atlas per glyph, zero cropping offset, zero left/right kerning bearing, so a glyph's destination rect maps exactly and any placement error is a hard pixel difference. Check A — a single glyph at (4,4) occupies exactly [4,12)×[4,12), checked inside plus all 4 edge midpoints (rules out an X-only or Y-only misplacement). Check B — "AB" advances the second glyph by exactly one glyph width. Check C — "A\nA" drops the second line by exactly lineSpacing AND resets x to the start. Check D — SpriteEffects::FlipVertically genuinely flips an asymmetric (top-half-white) glyph to bottom-half-white, ruling out a no-op flip. 4/4 PASS, reproducible across repeated runs — shared, backend-agnostic SpriteFont/SpriteBatch code confirmed working through this backend too
DILIGENT-54 Model multi-mesh/bone-hierarchy orchestration test Confirmed real and working — no stub-behind-a-code-path bug found. Added Diligent_Model (examples/diligent_model_test.cpp), a direct port of D3D12's own DX-148 Check KK6 (examples/d3d12_smoke_test.cpp): a real 2-bone hierarchy (root → child, ModelBone::AddChild) driving Model::Draw()'s full orchestration end to end (bone transform → SetVertexBuffer/setIndicesProperty/DrawIndexedPrimitives/EffectPass.Apply), not a raw VertexBuffer draw wearing a Model label. D3D12's own version of this exact test previously caught a real crash from unimplemented SetDepthTestEnabled/SetDepthWriteEnabled/SetBlendEnabled stubs nothing else in that backend's suite exercised — Diligent has no equivalent gap: the mesh's red renders exactly over the green clear, PASS, reproducible across repeated runs
DILIGENT-55 Texture2D mip-level SetData/GetData (level > 0) dedicated round-trip test Confirmed real, genuine GPU round-trip — not a CPU-shadow-only readback. Added Diligent_Mip (examples/diligent_mip_test.cpp), a port of the cross-backend easygl_texture2d_mip_test.cpp (Task 171) fixture, but strictly stronger here: DiligentSampledTexture::GetData() is documented as reading a mip level back through a real staging-texture GPU readback (unlike the EasyGL precedent's own explicit "pure CPU shadow buffer" note), so this genuinely exercises the class of bug D3D11's DX-126 was written to catch elsewhere (Vulkan/Bgfx silently no-op-ing a non-zero mip level's SetData/GetData entirely, Task 867). A 4×4 mipMap=true texture (levels 4×4/2×2/1×1) gets a distinct solid colour per level; every level round-trips byte-exact, and a final level-0 re-read after levels 1/2's own uploads confirms UpdatePixelsLevel() targeted the correct subresource each time, not level 0. 22/22 PASS, reproducible across repeated runs
DILIGENT-56 NPOT (non-power-of-two) Texture2D real GPU round-trip test Confirmed real and correct — no row-pitch/stride bug found. Added Diligent_Npot (examples/diligent_npot_test.cpp), going further than D3D11's own DX-140 (which only checked "does NPOT sampling look plausible" against a solid-colour texture) in the same direction DILIGENT-55 already established: a genuinely non-power-of-two 5×3 texture (5×4=20 bytes/row, not alignment-friendly) filled with 15 DISTINCT pseudo-random colours, so a D3D12_TEXTURE_DATA_PITCH_ALIGNMENT-shaped row-pitch bug in the staging-texture upload/readback path would shift pixels sideways between rows — something a solid fill could never reveal. Check A — full-texture SetData/GetData round-trips all 15 pixels byte-exact. Check B — a sub-rectangle GetData() read (columns [1,4), not aligned to the full 5-pixel row) round-trips exactly, independently exercising the row-pitch-vs-requested-width skip path. Check C — a real BasicEffect draw samples one of the texture's known colours at the viewport centre (not garbage/clear-colour), proving the normal draw path doesn't corrupt NPOT content either. 3/3 PASS, reproducible across repeated runs. This closes Phase DILIGENT-5's full task list

2026-08-03 independent audit (commit 3a64eeec7100)

This is the latest authoritative execution record. It was produced from a clean source archive at the exact commit above, with the repository's exact SDL/SDL_image/SDL_mixer/googletest submodule revisions and pinned DiligentCore v2.5.6. All graphics runs used Xvfb display :199 and Mesa software devices; no physical display or hardware GPU was used.

  • Build: all 24 registered Diligent_* binaries compiled successfully.
  • Vulkan (CNA_DILIGENT_DEVICE=vulkan, llvmpipe): 23/24 binaries fully pass, 115/116 checks pass. Diligent_DepthBias remains 3/4. Vulkan validation also reports that back-buffer readback transitions/copies a swap-chain image lacking VK_IMAGE_USAGE_TRANSFER_SRC_BIT; this is a real CNA swap-chain usage defect, not the documented constant-depth-bias limitation.
  • OpenGL (CNA_DILIGENT_DEVICE=opengl, OpenGL 4.5 llvmpipe), updated after DILIGENT-66's two systemic fixes (sRGB gamma, ReadBackbuffer() Y-flip): 25/31 registered Diligent_* binaries fully pass. The failing binaries are Diligent_Instanced, Diligent_InstancedStride, Diligent_DepthBias (1 of 7 checks, the same pre-existing DILIGENT-49 constant-bias limitation Vulkan also has), Diligent_ReferenceStencil, Diligent_RenderTargetMipGen (2 of 7 checks -- render-to-FBO content is genuinely Y-flipped, a related but distinct and still-open defect from the two fixed this session) and Diligent_MultiSampleMask (confirmed unimplemented in DiligentCore v2.5.6's own GL backend, not fixable from CNA's side). See DILIGENT-66 for the full root-cause writeup of each.
  • Device selection: fixed by DILIGENT-57 -- CNA_DILIGENT_DEVICE aliases (including gles) now resolve identically for window-flag selection and device creation.
  • Coverage audit: closed by tests added this session -- Diligent_ScissorPipelineCache, Diligent_MultiSampleMask and Diligent_LightingFidelity now cover scissor enable, sample mask, stock-effect emissive colour and non-uniform world normal transforms.

Phase DILIGENT-6 — independent-audit remediation (opened 2026-08-03)

These tasks supersede the over-broad older ✅ claims cited in their notes. A task may become ✅ only after its stated pixel/unit acceptance checks pass on every applicable compiled device type; a Vulkan-only pass does not close an OpenGL failure.

Task Description Status Acceptance criteria / implementation notes
DILIGENT-57 Make runtime device selection and SDL window API selection one transaction Confirmed real bug, fixed and pixel-verified. DiligentDeviceType/GetDeviceTypeName()/ParseDeviceTypeOverride()/GetDeviceTypePreferenceOrder() (no DiligentCore type dependency) split into a new DiligentDeviceSelection.hpp; GraphicsDevice.cpp's own window-flag selection now includes just that header and calls the SAME parser the backend itself uses, instead of a second, narrower "opengl"/"gl"-only check that silently defaulted every other alias (including gles) to a Vulkan-flagged window. Confirmed live: CNA_DILIGENT_DEVICE=gles now passes diligent_clear_readback_test 6/6 (previously failed with "the specified window isn't an OpenGL window"). New Diligent_DeviceSelectionIntegration CTest: vulkan/vk/opengl/gl/gles/auto each round-trip Clear()+GetBackBufferData() in their own forked process, plus an unrecognised-override leg confirming a clean, deterministic rejection -- 6/6 + bogus all PASS. auto crossing Vulkan↔OpenGL is not implemented (a single SDL window can only be flagged for one API), chosen deliberately over the task's own permitted alternative: this is now an explicitly documented limitation (commented at the window-flag site and here) rather than a silently broken promise -- true recreation would need DiligentGraphicsBackend to own, not just borrow, its window, out of this task's scope. Full Diligent CTest regression after a full relink: only the pre-existing Diligent_DepthBias failure. Closes the remaining correctness part of DILIGENT-3.
DILIGENT-58 Make PipelineKey cover every immutable PSO field, starting with scissor enable Confirmed real bug, fixed and pixel-verified. PipelineKey gained a scissorEnable field (set in MakePipelineKey() from the live scissorEnabled_ member, mirroring how sampleCount/targetFormats/extraTargetFormats are already computed fresh per key rather than persisted); GetOrCreatePipeline() now reads key.scissorEnable instead of the live member when building RasterizerDesc.ScissorEnable. Auditing every other line in GetOrCreatePipeline() for the same live-member-bypassing-the-key pattern (grepping the function body for any _ member read outside key.*/cached.*/local pipeline-desc temporaries) found scissor enable was the only offender — sample mask and depth bias are separate, already-tracked gaps (DILIGENT-60/64), not additional instances of this same bug. New Diligent_ScissorPipelineCache CTest (examples/diligent_scissor_pipelinecache_test.cpp): holds every other pipeline axis (variant/topology/blend/depth-stencil/target format/sample count) fixed within each of three draw paths (SpriteBatch, an indexed 3D draw, a hardware-instanced draw) so a stale cache hit is the only way a wrong pixel could appear, then runs both an off→on→off and an on→off→on ScissorTestEnable sequence per path, sampling a point inside vs. outside a fixed right-half scissor rectangle after each step. 36/36 PASS on the real Vulkan device (lavapipe); full ctest -R "^Diligent" regression afterward stayed at the same single pre-existing failure (Diligent_DepthBias, DILIGENT-49's documented llvmpipe limitation) with no new breakage. Together with DILIGENT-60/64, closes DILIGENT-9/12.
DILIGENT-59 Restore XNA/FNA stock-effect lighting fidelity Fixed and verified. Split the combined g_EmissiveAmbient cbuffer field into separate g_Ambient/g_Emissive, so emissive is added after (ambient + lights) * DiffuseColor instead of being folded into the ambient term and multiplied by diffuse a second time (previously: a black DiffuseColor silently zeroed emissive too). Scaled specular by baseColor.a (final output alpha, matching FNA's AddSpecular: color.rgb += specular * color.a) in both the per-pixel (kLitPixelHlsl) and per-vertex (kLitVertexLitPixelHlsl) paths, and in kEnvironmentMapPixelHlsl. Added SafeNormalizeLightDir() (both a vertex-side copy in kVertexLightingHlsl and a pixel-side copy in kPixelHelpersHlsl, since these are separate HLSL compilations) guarding against normalize() of a disabled light's (0,0,0) direction, which is left unnormalized/zero by the effect layer and would otherwise propagate NaN through the light sum. Fixed the normal transform in kLitVertexHlsl/kLitVertexLitVertexHlsl to use World's inverse-transpose (InverseTranspose3x3, moved out of kBonesHlsl into the always-prepended kVertexLightingHlsl so skinned and non-skinned variants share one definition) instead of transforming directly by World, which was wrong under non-uniform scale. Verified by the new Diligent_LightingFidelity (examples/diligent_lighting_fidelity_test.cpp), 6/6 checks passing with values matching hand-derivations to the pixel: (A) emissive reaches the pixel unmultiplied by a black DiffuseColor; (B) specular scaled by alpha -- compared at DiffuseColor.a=0.5 vs a=1 (not a=0: BasicEffect's own CPU layer already premultiplies DiffuseColor.rgb by alpha per SetMaterialColor, so a=0 would zero the diffuse term too and couldn't isolate the specular fix on its own) -- correct behaviour reads an unsaturated mid-grey (140) at a=0.5 where the old unconditional-specular-addition bug would saturate to white regardless of alpha; (C) two differently-coloured lights sum to magenta rather than one overwriting the other; (D) a diagonal normal under World=Scale(4,1,1) stays strongly lit via the inverse-transpose (nDotL=0.97) rather than reading weakly lit as a direct-World transform would (nDotL~=0.24). Full ctest -R "^Diligent" regression after landing: 45/46, the sole failure being the pre-existing DILIGENT-49 constant-DepthBias llvmpipe limitation, unrelated to this fix. Closes DILIGENT-11. DILIGENT-34/35/37 were already and independently verified before this task; not re-verified against this specific fix beyond the regression suite passing.
DILIGENT-60 Implement BlendState.MultiSampleMask Confirmed real gap, fixed and pixel-verified. PipelineKey gained a sampleMask field (default 0xFFFFFFFF, matching BlendWriteState's own default), set directly from writeState.multiSampleMask in ApplyBlendState() and read into GraphicsPipelineDesc::SampleMask in GetOrCreatePipeline(); included in operator==/hash. New Diligent_MultiSampleMask CTest: masks 0/1/all-ones on both a single-sample RenderTarget2D and a genuine 4x MSAA one, the MSAA cases using a ~1/4-blend signature (only 1 of 4 samples written) to prove real per-sample coverage masking rather than a binary discard, plus an A(all-ones)->B(0)->A(all-ones) cache-transition sequence. 9/9 PASS; full Diligent regression unaffected beyond the same pre-existing Diligent_DepthBias failure. Closes the missing part of DILIGENT-12.
DILIGENT-61 Report capabilities from the actual device Confirmed real bugs (two, both found while verifying), fixed and pixel/live-device-verified. SupportsCapability() is now a thin wrapper around a new pure EvaluateCapability() free function that takes already-queried Dg::DeviceFeatures/MaxAnisotropy/an MSAA-support bool -- no device access, testable with no GPU present (DILIGENT-3's own established reason for free functions). Bug 1: EngineCreateInfo::Features defaults every field to DISABLED, which the API guarantees never auto-enables regardless of hardware support, and this backend never touched createInfo.Features at any of its 4 device-creation sites -- GetDeviceInfo().Features would have reported every optional feature unsupported. Fixed with a new RequestOptionalCapabilityFeatures() requesting WireframeFill/OcclusionQueries/BinaryOcclusionQueries as OPTIONAL (never fails creation) at all 4 sites. Bug 2, caught live by the new Diligent_CapabilityConsistency test: the MultiSampleAntiAliasing probe used ClampSampleCount(2, RGBA8_UNORM), which reports "unsupported" on a device supporting 4x/8x but not 2x specifically -- exactly this backend's own lavapipe verification device (SupportsCapability(MSAA) read false while a real RenderTarget2D requesting MultiSampleCount=4 genuinely got 4) -- fixed by probing with 64, the largest candidate ClampSampleCount() itself ever considers. AnisotropicFiltering now reads GetAdapterInfo().Sampler.MaxAnisotropy > 1 instead of guessing from device type; ThreeD/DepthStencilBuffer/Texture3D/MultipleRenderTargets stay true with a comment explaining they are structural to this backend (a real depth-stencil buffer and up to DILIGENT_MAX_RENDER_TARGETS=8 are unconditional here), not device-variable facts needing a probe. 5 new DiligentDeviceSelectionTest.EvaluateCapability* GTest cases (no GPU) plus a new Diligent_CapabilityConsistency CTest (4 checks, real device, cross-checking SupportsCapability() against what OcclusionQuery/WireFrame/MultiSampleAntiAliasing/AnisotropicFiltering actually do) -- 4/4 PASS after both fixes. Full regression: only the pre-existing Diligent_DepthBias failure; Diligent_FillMode/Diligent_OcclusionQuery/Diligent_Anisotropic/Diligent_MSAA unaffected. Closes DILIGENT-14.
DILIGENT-62 Clamp MSAA against the resources actually being created Confirmed real bug, fixed and pixel-verified -- but does not close DILIGENT-25. ClampSampleCount() now takes an explicit colour format and optional depth format instead of always reading swapChain_->GetDesc(): the back buffer's two call sites pass the swap chain's own granted formats, DiligentRenderTargetBackend's constructor passes its own RGBA8_UNORM and, only when depth was requested, its own D24_UNORM_S8_UINT -- never the swap chain's, which design decision 10 already established can be a substituted format (e.g. BGRA8_UNORM). New Diligent_MSAA check F (DepthFormat::Depth24Stencil8 at MultiSampleCount=8) exercises the depth-intersection branch the existing depth-none check D couldn't; 6/6 PASS on Vulkan, full regression otherwise unchanged. Re-running Diligent_MSAA under CNA_DILIGENT_DEVICE=opengl after this fix still fails (applied sample count unchanged at 4, resolved edge still unblended) -- the clamp-source bug was real but is not what breaks this check under GL; that failure's actual cause is unaffected by this task and matches DILIGENT-30's own already-documented, not-yet-root-caused GL sampling note. Remains open under DILIGENT-66.
DILIGENT-63 Make back-buffer readback valid and bounded for every presentation mode Confirmed two real bugs, fixed and pixel-verified. Swap chain now requests SWAP_CHAIN_USAGE_RENDER_TARGET | SWAP_CHAIN_USAGE_COPY_SOURCE (was render-target-only, a real Vulkan validation violation on every readback). ReadBackbuffer()'s source box previously clamped MinX/MinY to 0 but computed MaxX/MaxY as clampedMin + physicalW/H -- shifting the copied region rather than clipping it whenever the physical origin was negative (Overscan's own centred-crop math makes this arise from an ordinary full-canvas read, not a contrived one) -- and never clamped against the real back buffer extent at all. Rewritten to intersect the full requested region against the real extent, size the staging texture from that intersection, and zero-fill any destination pixel whose position falls outside it. New Diligent_BackbufferReadbackBounds CTest, constructed directly against IGraphicsBackend (bypassing GraphicsDeviceManager -- the physical/virtual mismatch this needs can't be produced through the public API since SDL_SetWindowSize is a no-op under this project's headless Xvfb): full-canvas + subregion checks across all 5 CnaPresentationModes, plus Overscan's own naturally-out-of-bounds top/bottom edges (viewport y=-8, height=64 at this test's 64x48/40x40 configuration) reading back zero instead of crashing or shifting. 16/16 PASS; full Diligent regression after a full relink stayed at the same single pre-existing failure. Closes DILIGENT-13.
DILIGENT-64 Store depth-bias state without lossy signed-byte modulo packing Confirmed real sign-wrap bug, fixed and verified. PipelineKey gained its own lossless depthBias (Int32, via the new free ComputeDiligentDepthBiasRawUnits() helper -- clamped to the Int32 range rather than truncated into a byte) and slopeScaledDepthBias (exact Float32, matching Dg::RasterizerStateDesc's own field type) members, replacing the old PipelineKey::raster byte packing that silently flipped sign past +-0.127/-0.128 (e.g. DepthBias=+0.129 decoded as -127). New DiligentDeviceSelectionTest.DepthBiasRawUnits* GTest cases prove the boundary/sign/clamp behaviour with no GPU. Diligent_DepthBias gained a 3-check A(-8.0)->B(0)->A(-8.0) SlopeScaleDepthBias sequence at a fixed draw position (the "A→B→A cache" acceptance criterion), all passing -- the pipeline cache still creates and reuses the right pipeline now that these fields moved out of the shared byte-packed key member. Full Diligent CTest regression stayed at the same single pre-existing failure. Does not close DILIGENT-49: that task's own constant-DepthBias llvmpipe limitation is unrelated to the storage format and remains, now proven so more precisely (a lossless value still shows no observable effect on this software device).
DILIGENT-65 Make instancing respect vertex declarations and real stream strides Confirmed real silent-misfetch bug, fixed and pixel-verified on Vulkan; the pre-existing GL instancing failure is unrelated and untouched. PipelineKey gained instancedVertexStride/instancedInstanceStride (zero for every non-instanced variant), threaded through MakePipelineKey(); GetOrCreatePipeline() now builds the Instanced3D LayoutElement::Strides from these instead of a hardcoded 16/LAYOUT_ELEMENT_AUTO_STRIDE-derived 64. DrawInstancedPrimitivesEx() reads the real strides off the bound buffers and throws for a stride too small to hold what the shader reads, rather than attempting an impossible fetch. New Diligent_InstancedStride CTest: a 12-byte position-only vertex stream, a padded 80-byte instance stream, both combined, startIndex+baseVertex composed with the 12-byte stream, and two undersized-stride rejections -- 6/6 PASS on Vulkan; full regression otherwise unaffected (the pre-existing standard-stride Diligent_Instanced/Diligent_DrawOffset checks stay green). Re-running Diligent_InstancedStride under CNA_DILIGENT_DEVICE=opengl gives 3/6 (the two rejection checks and the offset check pass; the three rendering checks fail) -- but this is not a regression from this task: re-running the unmodified Diligent_Instanced binary (standard 16/64 strides, untouched by this change) under the same GL device independently fails 2/4 in the identical pattern (the first and third instance don't render, the middle one does), confirming hardware instancing under GL was already broken before this task for reasons unrelated to stride handling. Closes the general contract behind DILIGENT-43/45 on Vulkan; the separate GL instancing defect remains open under DILIGENT-66.
DILIGENT-66 Close OpenGL parity instead of documenting expected failures 🟨 Two systemic bugs root-caused and fixed this session, closing 8 of the 14 binaries failing under GL at the start of this task (10 originally named plus 4 more that simply never had a GL run before: Diligent_Skinned, Diligent_MultiSampleMask, Diligent_InstancedStride, Diligent_LightingFidelity). Not closed -- 6 of 31 registered Diligent_* binaries still fail under CNA_DILIGENT_DEVICE=opengl, three of them confirmed genuine upstream/driver limitations rather than CNA bugs.

Fix 1 -- OpenGL silently gamma-encoded every pixel (sRGB). DiligentCore's RenderDeviceGLImpl::Initialize() unconditionally calls glEnable(GL_FRAMEBUFFER_SRGB) whenever the GL version supports the feature (>=4.0), regardless of the swap chain's own requested colour format -- TryCreateDevice() always requests TEX_FORMAT_RGBA8_UNORM (non-sRGB), but the default window framebuffer GLX hands back for a plain SDL_WINDOW_OPENGL window on this Mesa/llvmpipe environment is itself sRGB-capable, so every write silently round-tripped through a gamma curve. Root-caused by hand-deriving the sRGB decode of two independently-failing binaries' mismatched values and finding an almost-exact match (Diligent_Pbr got=(161,161,161) vs expected=(91,91,91): 91/255 -> ^(1/2.2) -> *255 ~= 156-161 across all three cases; Diligent_LightingFidelity got=(231,124,170) vs expected=(204,51,102), same curve on all three channels). Fixed by resolving glDisable via SDL_GL_GetProcAddress (no new GL loader dependency) and disabling GL_FRAMEBUFFER_SRGB right after OpenGL device/swap-chain creation. Fixed as a side effect, with no test-specific changes: Diligent_Pbr (5/5, was 2/5), Diligent_LightingFidelity (6/6, was 4/6), Diligent_Skinned (4/4, was 2/4 -- a borderline IsRedish() G<90 threshold check that the gamma curve was pushing just over the line), Diligent_Npot (3/3, was 2/3).

Fix 2 -- ReadBackbuffer()'s Y axis was flipped relative to every other texture read. DiligentCore's GL backend copies the swap chain's own default framebuffer (Texture2D_GL::CopyTexSubimage -> glCopyTexSubImage2D) with the source Y taken straight from the requested Box.MinY/MaxY, but the DEFAULT framebuffer's Y axis is GL's own native bottom-up convention (row 0 = the bottom of the window) -- unlike every OTHER texture read in this backend, which are ordinary allocated textures and therefore self-consistently top-down (upload via glTexSubImage2D and this backend's own staging-texture readback use the same convention), which is why ReadTextureRegion() (Texture2D::GetData()) was never affected and gave no hint of this. Root-caused via elimination, not a guess: every combination of texture creation method (CreateFromPixels vs constructor+SetData()), draw overload (implicit/explicit/sub-region source rect), and even a byte-for-byte reuse of Diligent_2D's own already-passing SpriteBatch pattern kept failing until the destination rectangle no longer spanned the full 64px backbuffer height -- a full-height quad is invariant under a Y-flip about the canvas centre, which is exactly why every previously-passing GL sprite/text test happened to mask this; reading at the deliberately-flipped Y position for a small, non-full-height quad confirmed the geometry was there, just mirrored. Fixed in ReadBackbuffer() (CNA's own code, not vendored DiligentCore) with two matched halves, both scoped to deviceType_ == OpenGL: the requested source Box.MinY/MaxY is flipped about the back buffer's real height before being handed to CopyTexture(), and the CPU-side resample loop's stagingRow is un-reversed afterward (the flipped box makes glCopyTexSubImage2D copy GL-bottom-up rows in ascending order, landing the logically-bottom row of the box at staging row 0). Fixes Diligent_MSAA (RT MSAA-resolve check), Diligent_DrawOffset (instanced startIndex+baseVertex check), Diligent_VertexLit (both PreferPerPixelLighting true/false equality checks -- not the same "degenerate coplanar eye/quad" issue documented under DILIGENT-30; that finding was about Vulkan's own rounding tolerance and remains correct on its own terms, this was purely a readback artifact stacked on top under GL specifically), and Diligent_SpriteFont (4/4, was 0/4 -- every check failed identically because every check's destination rect was smaller than the full 64px canvas). Also fixes 6 of Diligent_DepthBias's 7 checks (all the SlopeScaleDepthBias tilted-triangle checks, previously reading pure black) -- the remaining 1/7 failure is the already-documented, cross-backend, environment-level constant-DepthBias limitation (DILIGENT-49), now at full parity with Vulkan's own gap rather than a GL-specific regression.

Confirmed genuine upstream/driver limitations, not CNA bugs (verified this session, not assumed): Diligent_MultiSampleMask -- DiligentCore's GLContextState::SetBlendState() (GLContextState.cpp) explicitly checks if (SampleMask != 0xFFFFFFFF) LOG_ERROR_MESSAGE("Sample mask is not currently implemented in GL backend"); and does nothing -- BlendState.MultiSampleMask is unimplemented in DiligentCore v2.5.6's own OpenGL backend, full stop, not something CNA's code can reach around without bypassing Diligent's abstraction entirely. Diligent_Instanced/Diligent_InstancedStride -- DiligentCore's VAOCache.cpp correctly calls glVertexAttribDivisor() and sets up per-instance attribute pointers with no visible defect; the reproducible "first and third instance don't render, the middle one does" pattern (identical with both standard 16/64-byte strides and DILIGENT-65's non-standard-stride cases) most plausibly indicates a Mesa/llvmpipe software-rasterizer bug in per-instance divisor attribute fetching, a known-fragile GL code path on software rasterizers; not independently confirmed against upstream Mesa, but DiligentCore's own instancing setup code was read in full and shows no defect.

Newly root-caused this session, NOT yet fixed -- Diligent_RenderTargetMipGen's level-0 checks. Unlike the backbuffer case, RenderTarget2D::GetData() uses ReadTextureRegion() (already fixed/self-consistent, confirmed via Diligent_Npot's passing round-trip), so Fix 2 above does not apply here -- and instrumenting the test's own checkerboard comparison against a vertically-mirrored expected pattern matched exactly (exactFlipped=1), proving the checkerboard's CONTENT, not the readback, is written upside-down inside the render target. Root cause: XNA's screen-space convention (CreateOrthographicOffCenter(0, width, height, 0, ...), "top=0") implicitly assumes a rasterizer where NDC Y=+1 maps to row 0 -- true for Vulkan/D3D11/D3D12, but OpenGL's rasterizer always maps NDC Y=+1 to the highest row index of the current viewport, an unconfigurable, fixed property of the API itself, not something DeviceContextGLImpl::SetViewports()'s own BottomLeftY = RTHeight - (TopLeftY + Height) display-compensation logic touches (that formula is a no-op for a full-coverage viewport, e.g. RTHeight - (0 + RTHeight) = 0, so it cannot be the source of a full-coverage-viewport content flip and does not need chasing further). For the swap chain, this same rasterizer-level mismatch also exists, but the DEFAULT framebuffer's own bottom-up row storage happens to interact with it such that the net displayed image is correct (unverified visually in this headless sandbox, but consistent with the general 3D GL tests already passing and with Fix 2 alone being sufficient to fix every backbuffer-readback symptom found) -- an ordinary FBO has no such display-side compensation, so its content comes out genuinely mirrored. A real fix needs to flip Y in the projection/vertex path specifically when the active render target is a plain FBO (not the default framebuffer) under GL, and must be verified across both the sprite path (DrawSpriteQuads()) and the general 3D path (DrawInternal()), and across every non-cube and cube render-target type -- deliberately not attempted in this session given the blast radius and the risk of a partial fix creating new, harder-to-spot flipped-content regressions elsewhere; left as the clear next step under this same task.

Still open, not re-investigated this session (static-analysis-only findings from the prior session stand as-is): Diligent_ReferenceStencil -- traces through cleanly on paper per the prior static-analysis note; a centre-pixel check on presumably-symmetric geometry rules out any of this session's Y-flip findings as an explanation, so this remains a genuinely separate, unexplained defect.

Regression discipline: every fix here was verified with a full untargeted rebuild + full ctest -R "^Diligent" regression on both device types before committing. Vulkan: 45/46 unchanged throughout (the sole failure is the pre-existing DILIGENT-49 constant-DepthBias limitation). OpenGL: 36/46 -> 40/46 registered-test-count pass rate across this session (31 Diligent_* GraphicsSmoke binaries total, up from the plan's previously-stated 24 as more tests were added by other tasks this session; 25/31 binaries fully pass as of the last run). Acceptance (24/24 binaries -- now 31/31, 116/116 checks) is not met; treat as 🟨 throughout, not . Does not yet complete DILIGENT-30.
DILIGENT-67 Add a dual-device CI/CTest matrix and normalize documentation Dual-device matrix: added cna_register_diligent_test(name command) (cmake/Tests/DiligentTests.cmake), replacing the 30 near-identical cna_register_backend_test() call sites -- registers every Diligent CTest twice, once as-is and once as <Name>_OpenGL with CNA_DILIGENT_DEVICE=opengl forced. Diligent_DeviceSelectionIntegration deliberately excluded (its own fork+exec supervisor already exercises every device type internally; a twin would just rerun the identical matrix for no new coverage). Verified: ctest -R "^Diligent" now runs 76 tests (up from 46) -- 30 Vulkan/OpenGL pairs, DeviceSelectionIntegration once, 15 GPU-independent DiligentDeviceSelectionTest cases; 91% pass, the 7 failures being exactly the 6 already-documented open DILIGENT-66 OpenGL findings plus the one pre-existing Vulkan DepthBias limitation -- no new failures surfaced by doubling coverage. The "missing scissor/sample-mask/emissive/non-uniform-normal" tests this row asked for were already added by DILIGENT-58/60/59 earlier this session (Diligent_ScissorPipelineCache, Diligent_MultiSampleMask, Diligent_LightingFidelity); the stale "single-sampled everywhere"/"five variants"/PBR-instancing-out-of-scope claims this row named no longer existed in docs/diligent-backend.md by the time this task ran (already corrected in an earlier pass) -- verified by grep, not assumed. Documentation normalization: rewrote docs/diligent-backend.md's binary/check counts (24→31 binaries, 116→197 checks, plus DeviceSelectionIntegration's own 7 scenarios), added descriptions for the 6 previously-undocumented binaries, replaced the entire stale "OpenGL not fully verified, most gaps not investigated yet" limitations paragraph with DILIGENT-66's actual findings (two systemic fixes + which binaries each closed + the three confirmed upstream/driver limitations + the newly-root-caused-but-open RenderTargetMipGen FBO Y-flip + ReferenceStencil still unexplained), and corrected the stale "BlendState.MultiSampleMask also has no effect" backend-wide claim (stale since DILIGENT-60; it works on Vulkan/D3D11/D3D12, only OpenGL is affected, and only because of DiligentCore's own upstream gap). Closes DILIGENT-17.
DILIGENT-68 Split the backend and reuse shared shader references 🟨 Shader unit extracted and verified; the remaining device/resources/pipeline/spritebatch split not yet attempted. Moved all 26 built-in kXxxHlsl string constants (914 of the file's 4757 lines, ~19%) unchanged into include/CNA/Internal/Backends/Diligent/DiligentShaderSources.hpp, included at file scope before DiligentGraphicsBackend.cpp's own namespace opens so its anonymous-namespace declarations join the same per-TU anonymous namespace the rest of the file already uses -- pure mechanical relocation, no shader source touched. Verified with a full rebuild + full dual-device ctest -R "^Diligent" regression (76 tests): identical result to before the extraction, same 7 pre-existing failures, no new ones. DiligentGraphicsBackend.cpp: 4757 → 3845 lines.

Not attempted this session: splitting the remaining file into device/swap-chain, resources/readback, pipeline/state and SpriteBatch units. Mapped the boundaries first rather than guessing: the file interleaves a block of small anonymous-namespace conversion helpers (ToBlendFactor/ToComparisonFunction/MipLevelExtent/PackBytes/etc., ~lines 34-230), externally-linkable device-selection functions from DiligentDeviceSelection.hpp (ParseDeviceTypeOverride/GetDeviceTypePreferenceOrder/ComputeDiligentDepthBiasRawUnits, non-anonymous, ~lines 230-343), eight small self-contained resource-backend classes (DiligentTextureBackend/TextureCubeBackend/Texture3DBackend/RenderTargetBackend/RenderTargetCubeBackend/VertexBufferBackend/IndexBufferBackend/SpriteBatchBackend/OcclusionQueryBackend, ~lines 344-1409), then DiligentGraphicsBackend itself -- by far the largest piece at roughly 80 of its own methods spanning ~lines 1410-3556 (constructor/device creation/swap chain/MSAA/clear/present/resource factories/state application/ReadBackbuffer/ReadTextureRegion/draw dispatch, all one class). The resource-backend classes and SpriteBatchBackend could likely move into their own files at similarly low risk to the shader extraction (self-contained classes, minimal cross-dependencies), but splitting DiligentGraphicsBackend's own ~80 methods across multiple translation units -- while legal C++, since private-member access works identically across a class's own TUs -- needs the anonymous-namespace helper block correctly shared (a small internal header, since anonymous-namespace declarations are safely per-TU-duplicable) and a full dual-device regression after each cut to hold the "no behavior change" bar this task sets. Deliberately not rushed given the risk of a subtle, hard-to-catch regression in the single most complex backend file in the project; left as future work with the boundary already scoped above. Generating Diligent's HLSL variants from the D3DCommon shader implementations (this row's other listed option) was not pursued -- Diligent cross-compiles one shared HLSL source per variant to SPIR-V/GLSL/DXBC at runtime, architecturally different from D3DCommon's per-API .hlsl files, so this would be a new codegen tool, not a reuse.

Verification status — read before claiming anything works

This plan distinguishes three levels, in the same discipline plan_dx.md/plan_webgpu.md already use:

  1. Buildscmake --build ... --target cna_backend_graphics_diligent succeeds against the pinned DiligentCore. ✅
  2. Runs without a GPUDiligent_DeviceSelection exercises the device-preference and override parsing with no device created at all. ✅
  3. Real device pixels — a real Diligent device renders and a test asserts on read-back pixels. ✅ reached, on a software device: 23 of the 24 Diligent_* CTest binaries are fully green (112 of their own checks — Diligent_2D 6, Diligent_3D 6, Diligent_RenderTarget 5, Diligent_RenderTargetCube 4, Diligent_AlphaTestFog 4, Diligent_DualTextureEnvMap 6, Diligent_Skinned 4, Diligent_MRT 4, Diligent_OcclusionQuery 4, Diligent_MSAA 5, Diligent_Instanced 4, Diligent_DrawOffset 5, Diligent_SetDataOptions 4, Diligent_VertexLit 4, Diligent_Pbr 5, Diligent_ReferenceStencil 1, Diligent_FillMode 3, Diligent_Anisotropic 1, Diligent_SpriteFont 4, Diligent_Model 1, Diligent_Mip 22, Diligent_Npot 3, Diligent_RenderTargetMipGen 7 — plus Diligent_DepthBias's own 4 checks, 3 of which pass, makes 116 checks total, 115 passing) run against a genuine Vulkan device provided by Mesa's lavapipe ICD under Xvfb. The 24th, Diligent_DepthBias (DILIGENT-49), is 3/4: constant DepthBias shows no observable effect on this software device, matching two independent pre-existing findings elsewhere in this codebase (D9-62's oracle attempt against real XNA 4.0, Vulkan_DepthBias's own pre-existing DepthBias=-1e6 sub-case) — a documented, cross-backend environment limitation, not a CNA-side bug, and left as a real, visible CTest failure rather than masked. These are real draws through the real Vulkan engine — real pipelines, real HLSL→SPIR-V compilation, real depth testing — read back through GraphicsDevice.GetBackBufferData/RenderTarget[Cube].GetData, not stubs. Several real defects were found this way and fixed, spanning both the backend and its own tests — see each DILIGENT-* task's own row for detail. Two are the most instructive:
    • Diligent_RenderTargetCube's EnvironmentMapEffect check initially read back solid black and cost a long investigation (resource-state tracing, view-descriptor dumps, raw-sample shader hacks) before the actual cause turned out to be in the test, not the backend — SpriteBatch leaves its own RasterizerState bound after End() (XNA semantics), silently culling the very geometry the next check tried to draw. The fix was a one-line RasterizerState::CullNone reset, already a documented pattern from Diligent_RenderTarget's identical gotcha; the lesson generalized here is to check a test's own state hygiene before suspecting the backend, especially once low-level backend signals (content, resource state, view descriptors) have all confirmed correct.
    • Diligent_MSAA's RenderTarget2D check initially showed a correctly multisampled and resolved texture with zero anti-aliasing in the sampled result. Root cause was a genuine, separate, pre-existing bug, not anything about MSAA itself: DiligentGraphicsBackend::SetRenderTarget2D()/ SetRenderTargetCubeFace()/SetRenderTargets() never called the outgoing target's UnbindAsRenderTarget() at all — GraphicsDevice::SetRenderTarget() calls the backend's SetRenderTarget2D() directly and was never routed through the interface's own UnbindAsRenderTarget(), unlike every other CNA backend (EasyGL, D3D11/12, SdlGpu, Vulkan), which all call it themselves from their own bind-switching method. This meant DILIGENT-26's mip regeneration had also never actually fired since it was implemented, silently — exactly the gap that row's own "no pixel test asserts the generated levels" caveat was covering for. Fixed by having SetRenderTarget2D()/SetRenderTargetCubeFace()/ SetRenderTargets() call the outgoing target's UnbindAsRenderTarget() themselves before swapping state, and by removing the recursive SetRenderTarget2D(nullptr) call UnbindAsRenderTarget() used to make at its own end (which is what had silently made it unsafe to call from the backend's own bind-switching methods in the first place).
  4. Real hardware GPU pixels — ⬜ not reached. lavapipe is a CPU rasterizer; it exercises the API and the shaders but not a vendor driver. Anything driver-dependent (real MSAA sample counts, anisotropy, present modes, GL's swap-chain origin) stays unproven. Do not add a DILIGENT column to docs/graphics-backend-feature-matrix.md until this level is reached — that document's ✅ means hardware-verified.

Cross-backend test suite

CnaTests under CNA_GRAPHICS_BACKEND=DILIGENT: 5692 passed, 7 skipped, 1 failed. The total held steady through DILIGENT-41 and DILIGENT-25: each closed one more stale guard in GraphicsDeviceCapabilityTests.cpp (SupportsOcclusionQuery then implicitly MultiSampleAntiAliasing, both previously false/EXPECT_FALSE on this backend) rather than adding a new one. The single failure is XnbContainerFuzzTest. MutatedRealModelFixtureNeverCrashesAndOnlyFailsCleanly, which fails identically on the HEADLESS backend (verified in the same session) — a pre-existing gap in that test's accepted-exception list (System::ArgumentException from VertexBuffer::SetData's declaration/stride validation is not listed), unrelated to this backend.


Post-integration rows (2026-08-07)

Task Description Status Notes
DILIGENT-69 Diligent_InstanceBindingOffsets fails under the OpenGL device type 🟨 Not a CNA defect; joins DILIGENT-66's existing GL instancing class. The test added by the integration's REMED-GFX-202 work proves the instance binding's own VertexOffset, its InstanceFrequency and the geometry binding's VertexOffset on the instanced route. Each leg is built so "consumed" and "ignored" produce different, in-bounds pixels. 12/12 on the Vulkan device type. Its _OpenGL variant reads 7/12 for the identical, already-root-caused reason as Diligent_Instanced_OpenGL and Diligent_InstancedStride_OpenGL: under Mesa/llvmpipe the per-instance attribute reads as zero for every instance, so all instances draw stacked at the origin and none of the probe columns is lit. Closes when DILIGENT-66's GL divisor limitation does.

Cross-backend test suite, re-measured at integration

ctest -j1 over the whole tree under CNA_GRAPHICS_BACKEND=DILIGENT, on the integrated content: 5816 registered · 5800 passed · 8 failed · 7 truthful skips. All eight failures are this backend's own dedicated suites — the seven this plan already records plus DILIGENT-69 above — and there is not one non-Diligent failure in the run. The XnbContainerFuzzTest failure recorded in the section above no longer reproduces at this head.