Capability answers, interface defaults and draw evidence

CNA snapshot 009d40f5  ·  Deep Dives › The graphics machine  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Capability answers were derived from each family's source; only some families have capability truth tests or recorded snapshots, and none was run for this page.

This page explains what a CNA capability answer is, and what it is not: the exact question each GraphicsCapability member asks, why the renderer-level default is permissive while the device-level answer is partly derived, the report/path mismatches that remain at this snapshot, how the unsupported-3D policy differs from a capability, the four ways a renderer-interface default can fail, and what a claim that "the draw works on renderer X" has to name to be worth anything. It is for developers who branch on capabilities and for maintainers adding or auditing a renderer. The task-level treatment is Tutorial 101 and the per-identity matrix is on Renderers: capability matrix.

  SupportsCapability(c) --> reported Boolean            (may be an inherited permissive
                            |                             default, or derived at device level)
                            v
                          factory / method exists and returns a usable object
                            |                            (not null, not an inert stub)
                            v
                          the bind, draw or query completes with the stated semantics
                            |
                            v
                          the named renderer really engaged
                            |                            (not a coloured fallback or no-op)
                            v
                          an oracle or pixel comparison distinguishes the expected result

  A failure at any rung narrows the claim to the rung below it.
Figure. Capability reporting versus capability evidence, as a ladder read from top to bottom. A query returns a Boolean that may be inherited from a permissive default; independent checks must then show that a usable factory or method exists, that the requested operation completes with its documented semantics, that the intended renderer path actually ran, and finally that an oracle or pixel comparison tells the expected output apart from a fallback. A true query result is an API response, not a substitute for any of the lower rungs.

Nineteen contract questions

CNA::GraphicsCapability (GraphicsCapability.hpp) has 19 members; values are append-only (the C ABI carries them as 0–18), which is why the five newest sit at the end. Each asks a CNA contract question, not a native API feature bit:

GroupMember and the exact question
PipelineThreeD: is the vertex/index/effect 3D pipeline (3D draws, depth/stencil clears and state) supported? DepthStencilBuffer: is a complete real depth/stencil attachment available? StencilBuffer: is a real stencil plane usable independently of depth and 3D?
RasterisationMultiSampleAntiAliasing: can a sample count above one be applied? WireFrame: is FillMode::WireFrame real? AnisotropicFiltering: does the current device honour anisotropic filtering? AdditiveBlending: does BlendState::Additive take a documented additive path rather than silently degrading to source-over?
Targets and queriesMultipleRenderTargets: can more than one target be bound at once (also limited by the profile)? OcclusionQuery: is there a real query with begin, end, completion and pixel count? FloatRenderTargets/HalfFloatRenderTargets: does a 32-bit or 16-bit float target really keep unclamped values? HalfFloatTextureLinearFiltering: can a half-float texture be sampled with linear filtering?
GeometryMultiStreamVertexInput: can more than one binding of the same input rate be consumed? Instancing: does DrawInstancedPrimitives submit distinct instance records? IndirectDraw: can draw arguments be read from a GPU buffer?
ProgrammabilityCustomEffects: can a non-stock effect be passed to SpriteBatch::Begin() (the header's own definition; source execution is a separate detailed feature)? CompiledEffects: is XNA/FNA Effect Framework bytecode reflected and executed? ComputeShaders: compute shaders with storage buffers?
StorageTexture3D: do volume textures really store and return data? It never promises shader sampling.

Because the questions are about CNA's contract, a narrow renderer can answer honestly without inheriting GPU semantics: SOFTWARE reports bounded CPU Texture3D storage without a shader-sampling path; GDI offers a 2D stencil mask (StencilBuffer true) without a depth attachment or 3D pipeline; HTML_DOM's AdditiveBlending depends on whether the running browser honours CSS plus-lighter. Older CNA documentation counted thirteen or fourteen members; the five appended at this snapshot are the float/half-float target pair, half-float filtering, compute and indirect draw.

Two levels, two polarities

The renderer default is permissive

IGraphicsRenderer::SupportsCapability (IGraphicsRenderer.hpp) returns true for every member except five: StencilBuffer delegates to the renderer's stencil query, MultiStreamVertexInput is false, CompiledEffects asks the false-by-default SupportsCompiledEffects(), and the two float render-target entries are false. The opt-out design spares mature renderers boilerplate, but a new or narrow renderer that forgets an override overclaims. At this snapshot DIRECTX9 is the only family with no override at all; VULKAN, DIRECTX11, DIRECTX12, OPENGL4, SDL_GPU, DIRECT2D and METAL use switches without a default arm (a new member is then a deliberate decision), while WEBGPU, HEADLESS, SOFTWARE, EasyGL and FNA3D end in default: return true.

The device answer is partly derived and fails closed

GraphicsDevice::SupportsCapability (GraphicsDevice.cpp) never asks the renderer's switch for six members: CompiledEffects, FloatRenderTargets and HalfFloatRenderTargets (can a Vector4 or HdrBlendable render target really be created), HalfFloatTextureLinearFiltering, ComputeShaders and IndirectDraw each come from a dedicated renderer query whose default is false. The code comments give the reason each time: a switch ending in default: return true would otherwise claim an enumerator the renderer has never heard of. MultipleRenderTargets is the renderer's answer ANDed with the profile's target limit, so a Reach device reports false on every renderer. Ask the device, not the renderer interface: SDL_GPU's own switch answers several derived members false while the device derives them true.

Answers depend on configuration and host

A Boolean describes one build on one device: SDL_GPU reports OcclusionQuery false (its vendored SDL GPU API has no query) and CustomEffects true only in builds that found libshaderc; CompiledEffects changes with each family's CMake option; FNA3D executes compiled Effect Framework programs while truthfully reporting CustomEffects false; EasyGL, Vulkan, WebGPU, DIRECTX11/12 and FNA3D answer several members from run-time device probes; HEADLESS reports ThreeD true and produces no pixels. The finer RendererCapabilityProfile separates what the Booleans merge — 32 features with a four-state answer (Unknown, Unsupported, Supported, Restricted), 22 numeric limits and per-format usage masks in which a missing "known" bit means "not classified", not "unsupported" (RendererCapabilityProfile.hpp; Tutorial 133).

Known report/path mismatches at this snapshot

A capability check is necessary but, for these entries, not sufficient; pair the query with the operation and an observable result (see Known Issues for their status):

  • DIRECTX9 inherits CustomEffects true and declares no source-execution flag, so the detailed profile says source text does not determine pixels — yet its SpriteBatch compiles and runs caller HLSL; no DIRECTX9 3D draw consumes a custom program.
  • VULKAN and SDL_GPU report all six ShaderDialect* profile features as unsupported because their declared dialect, SPIR-V, has no feature entry, although both execute caller SPIR-V; SupportsShaderLanguageEXT(ShaderLanguageEXT::SpirV, stage) is the truthful query.
  • The engine layer's capability-name table used in its refusal messages covers 18 of the 19 members; a refusal on IndirectDraw would print an unrecognised name.
  • The comment on ComputeShaders says Direct3D 11 and 12 "can express" compute; those renderers answer false and implement none.

The unsupported-3D policy is not a capability

Each device carries a CNA::Unsupported3DGraphicsCallBehavior. Throw, the default, keeps each renderer's established refusal. WarnAndStub makes a renderer log once per method name and return a safe no-op or null-object resource where the contract defines one. The policy is set per device through GraphicsDevice::SetUnsupported3DGraphicsCallBehavior (there is no environment variable), takes effect immediately, and changing it to a different value clears the warn-once history so the next stubbed call is visible again. It does not change any capability answer, and it does not suppress invalid arguments, disposed resources, driver failures or unfinished work on a renderer that otherwise claims 3D.

Refusal is enforced in different places. The shared draw routes call the renderer's Ensure3DSupported() hook first; its default is a no-op, and only DIRECT2D, HTML_DOM and SVG_DOM override it. The other 2D-only families refuse in their capability answers, factories or draw methods. An absent override is therefore not evidence of 3D support. Only the families that route through the shared HandleUnsupported3DCall honour WarnAndStub — SDL_RENDERER, CANVAS, FREEDIRECT and DIRECT2D; GDI, HTML_DOM and SVG_DOM always throw, and STUB never reaches the path because its draws are no-ops.

The null-object resources are the NoOp* classes in NoOp3DResources.hpp: vertex and index buffers, an occlusion query, a volume texture, a cube texture and a cube render target. They keep only the counts and sizes the public wrappers need, allocate no GPU storage and never draw. The four families that honour WarnAndStub return them from their 3D factories after HandleUnsupported3DCall has logged the warning (a few factories return null instead when the policy is Throw, and the public constructor then refuses). The active policy is read back with GraphicsDevice::GetUnsupported3DGraphicsCallBehavior(). Read at the pinned commit; not executed.

Four interface-default failure shapes

The renderer contract header is over four thousand lines, and its defaults do not fail the same way. Knowing which shape a family inherits tells you what a missing override looks like from the outside:

  1. Effect-discarding fallback. The default DrawPrimitivesEx/DrawIndexedPrimitivesEx ignore the GpuDrawParams packet and call the coloured primitive route, so texture, lighting, fog, skinning and custom-effect state can vanish without an exception. At this snapshot only SDL_RENDERER, DIRECT2D, CANVAS, HTML_DOM, SVG_DOM, FREEDIRECT and STUB inherit these defaults, and their coloured routes refuse (or, on STUB, do nothing); every 3D-capable family overrides both. An earlier, larger renderer set contained a family that drew a plausible untextured image through this fallback — the reason the shape is worth remembering when adding a renderer.
  2. Hard refusal. The default instanced draw throws std::runtime_error (or, on a 2D-only renderer under WarnAndStub, warns and returns), and the default back-buffer readback throws "not implemented in this renderer".
  3. Silent no-op. About fifty virtual methods in the header have empty bodies — the IEffectRenderer uniform setters among them — so a public call can update a cache or return normally while no native state changes, unless the shared setter guards the path.
  4. Null factory. Optional factories such as CreateOcclusionQuery, CreateTexture3D, CreateTextureCube, CreateRenderTarget2D and CreateEffectRenderer return null by default; the public wrapper then refuses (an OcclusionQuery constructor throws NotSupportedException), degrades, or exposes an inert object (a ShaderEffect whose IsEffectValid() is false).

Real families variously own the extended draw methods, delegate to a coloured route, reject, trace (HEADLESS) or provide a bounded CPU implementation (SOFTWARE, PORTABLEGL). Classify each family by where its calls actually end, not by counting overrides; the per-family traces are on Indexed draw trace: other families.

A portable draw claim

"The triangle rendered" is incomplete. A claim about a vertex path that someone else can reproduce names:

  • the public renderer identity and its implementation family (GraphicsDevice::GetGraphicsRendererName() reports the one that really started in a multi-renderer build);
  • the vertex declaration, packed stride, index width and binding rates;
  • the capability answers — ideally the GetRendererCapabilityReportEXT() text — and any known report/path mismatch that applies;
  • the applied effect and whether the renderer consumed its GpuDrawParams rather than a fallback;
  • the native driver and host that engaged (a Wine translation layer, a software rasteriser and a hardware GPU are different evidence);
  • the pixel, query or structured oracle that distinguished the expected route from a coloured fallback or a no-op.

With those named, a failure can be localised to packing, binding normalisation, capability reporting, effect dispatch or native rendering instead of being summarised as "this renderer does not work". CNA's own evidence vocabulary for such claims is on Compatibility levels and the evidence vector.

Evidence and limits

Checked by reading the capability enum, the renderer-interface defaults, the device-level derivation and every family's capability switch and DrawPrimitivesEx overrides at snapshot 009d40f5; nothing was built or executed. The override inventory was made by searching each renderer directory, so it describes declarations, not measured behaviour. Capability truth tests exist for some families (for example the renderer capability truth example registered for DIRECTX11 and DIRECTX12, and the Vulkan capability snapshot recorded on llvmpipe); most answers are code-derived.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Maintainer workflow
Fix a renderer bug