Chapter 18 Vertex Streams and Capabilities
This chapter is the handoff from renderer-independent graphics objects to the per-renderer Part. It follows a draw from public vertex values through declarations and bindings, then explains what a capability answer does—and does not—guarantee. The distinction is essential in CNA because the shared renderer interface contains useful defaults, explicit refusals, silent no-ops, and one effect-discarding draw fallback.
18.1 Public vertex objects are not upload layouts
The built-in public types preserve XNA-shaped fields and IVertexType behavior, but their C++ object representation is not necessarily the GPU byte representation. In particular, Color and the vertex interfaces are polymorphic. Chapter 8 documents the plain internal stream structs and their asserted 16/20/24/32/48/52/68-byte layouts.
Typed VertexBuffer::SetData and GraphicsDevice::DrawUserPrimitives routes therefore pack supported public values into those stream forms. Raw-pointer routes are a different contract: the caller supplies already-packed bytes and, where required, an explicit VertexDeclaration. Passing an array of public objects through void* does not turn their vptr-bearing C++ layout into the XNA stride.
18.2 VertexDeclaration is the byte contract
A VertexDeclaration’s layout is immutable after construction. Each element stores an offset, usage, and usage index. Its VertexElementFormat describes the byte shape; the declaration’s stride is either explicit or computed as the largest . The public default constructor is a deliberate exception: it creates an empty, zero-stride declaration used by the legacy VertexBuffer(device, count) convenience path. The constructors that explicitly receive an element list reject an empty list with ArgumentNullException; explicit non-positive strides throw the out-of-range exception.
The declaration reaches every renderer through a mandatory call. The vertex-buffer renderer interface method is SetVertexDeclaration. An old description that says CNA selects layouts from stride alone is obsolete. Stride is still visible because several renderer implementations optimize known built-in layouts, but a faithful path must also validate and bind the declared elements.
VertexElement itself is mutable and its hash currently returns zero, following the corresponding FNA limitation. Do not use the hash as a declaration identity; compare the complete element sequence and stride when building a cache key.
18.3 Buffers, index widths, and dynamic updates
VertexBuffer stores a declaration, vertex count, usage, and renderer-owned resource. Its typed transfer families cover CNA’s seven built-in stream shapes; the CNAEXT raw route accepts a pointer, element count, and stride. Copy is deleted and move is explicit/noexcept, which avoids duplicating a native buffer handle.
IndexBuffer exposes 16-bit and 32-bit transfer forms selected by IndexElementSize. The semantic choice is not the enum’s ordinal. Content and oracle tools must compare the 16/32 meaning rather than assuming an underlying 0/1 value, a mismatch that has produced real cross-implementation disagreements.
DynamicVertexBuffer and DynamicIndexBuffer add a content-lost surface and SetData(..., SetDataOptions). At the pin, the dynamic update destination always begins at offset zero; the options influence native update strategy, not an arbitrary destination offset. In normal CNA operation, their content-lost property remains false.
18.4 Bindings carry offset and input rate
VertexBufferBinding combines a buffer, VertexOffset, and InstanceFrequency. Frequency zero is per-vertex input; a positive frequency is per-instance input. GraphicsDevice accepts up to sixteen binding slots through SetVertexBuffers. It permits null buffers as unused slots. The public ceiling is not proof that the selected renderer can consume sixteen active streams.
The classic shapes need no multi-stream capability:
-
•
one per-vertex binding;
-
•
one per-vertex binding plus one per-instance binding.
More than one per-vertex stream, more than one per-instance stream, or a binding count beyond the renderer-reported native ceiling invokes the MultiStreamVertexInput gate. A false answer causes NotSupportedException before native submission instead of letting the renderer silently read only stream zero.
Streams compose by , not merely by slot order. The shared layer claims each semantic pair while walking bindings. A later stream whose declaration is entirely duplicated contributes nothing, matching the FNA3D driver rule that such a stream is not in use. A declaration that repeats only some earlier semantics is rejected: CNA’s combined-stride description cannot express a record whose fields are partly suppressed. Split declarations should therefore use distinct semantic/index pairs deliberately.
18.4.1 The minimum-offset fold
For multiple per-vertex bindings, CNA finds the smallest VertexOffset and folds it into the draw’s vertexStart or baseVertex. Each binding carries only its non-negative remainder. Renderers already multiply the shared element-unit base by each stream’s own stride, so this scheme applies the common portion exactly once without requiring equal strides.
With one stream, the transformation is byte-identical to the older behavior. With several streams, folding each full offset independently into one global base would be wrong; dropping the remainder would align only the earliest stream. This normalization belongs to the shared layer so every renderer sees the same binding geometry.
18.5 Draw validation has a deliberate order
Every buffer-backed 3D draw first asks the renderer to enforce its 3D policy, then requires the necessary vertex/index buffers and a currently applied Effect. Missing effect state is a plain std::runtime_error, not an XNA-shaped exception.
Range validation occurs before renderer capability validation. An index span leaving the index buffer or a declared vertex range leaving the vertex buffer is invalid on every renderer and must produce the same public argument failure even on a 2D-only product. Only after those universal invariants are established does CNA reject an unsupported stream shape or instancing path.
The user-draw overloads follow the same conceptual stages: establish a declaration, pack typed objects when necessary, validate counts and ranges, construct transient upload data, fill GpuDrawParams from the applied effect, and enter the renderer contract.
18.6 Thirteen capability questions
GraphicsCapability contains thirteen entries at the pinned revision:
| Capability | Exact question |
|---|---|
| ThreeD | Is the vertex/index/effect 3D pipeline supported? |
| DepthStencilBuffer | Is a complete real depth/stencil attachment available? |
| MultiSampleAntiAliasing | Can a sample count above one be applied? |
| MultipleRenderTargets | Can more than one target be bound simultaneously? |
| AnisotropicFiltering | Does the current device/driver honor anisotropic filtering? |
| WireFrame | Is RasterizerState.FillMode=WireFrame real? |
| OcclusionQuery | Is there a real query with Begin/End/completion/pixel result? |
| CustomEffects | Can a non-stock effect enter the documented custom-effect path? |
| Texture3D | Does real volume texture storage and transfer exist (not necessarily shader sampling)? |
| MultiStreamVertexInput | Can repeated input rates be represented across several bindings? |
| Instancing | Can DrawInstancedPrimitives submit distinct instance records? |
| StencilBuffer | Is a real stencil plane available independently of a depth/3D claim? |
| AdditiveBlending | Does additive compositing avoid silent source-over degradation? |
These are CNA contract questions, not a dump of native API feature bits. For example, Skia can report bounded Texture3D CPU storage while its general 3D pipeline remains false. GDI can offer a 2D stencil mask without claiming a depth attachment. HTML DOM additive support depends on whether the browser honors CSS plus-lighter.
18.7 The default polarity is permissive
IGraphicsRenderer::SupportsCapability returns true by default for every entry except MultiStreamVertexInput; StencilBuffer delegates to the older stencil query. This opt-out design protects mature renderers from boilerplate, but a new or narrow renderer that forgets an override overclaims.
The implementation is the authority. At the pin, HEADLESS and SOFTWARE report MRT through the default even though their bind paths do not provide simultaneous target rendering. WEBGPU reports MRT and occlusion queries while rejecting a second target and returning no real query object; SDL_GPU similarly inherits a positive query answer without a query implementation. Custom effects add two direct false positives: WEBGPU inherits true while retaining the null effect factory, and BGFX reports true on a real renderer even though its returned effect object’s CompileProgram() always returns false. FNA3D is the useful opposite example: it executes six closed stock bytecode programs but truthfully reports custom effects false (Chapter 17). Appendix B records such contradictions rather than copying the Boolean as support.
A capability check is therefore necessary but, for known contradictory entries, not sufficient. Pair it with a resource/draw path and an observable oracle until the upstream report becomes truthful.
18.8 Unsupported 3D policy is separate from capability
Each GraphicsDevice carries Unsupported3DGraphicsCallBehavior, with two values:
- Throw
-
The default. A deliberately unsupported 3D call follows the renderer’s established refusal path.
- WarnAndStub
-
The renderer logs once per method name and returns a safe no-op or null-object resource where the contract defines one.
Changing the policy clears the warn-once history. It does not change capability answers and does not suppress invalid arguments, disposed resources, driver failures, or unfinished work on a renderer that otherwise claims 3D. There is no environment variable for it; applications opt in through the device API.
Six deliberately 2D-focused families override the early Ensure3DSupported hook: Blend2D, Direct2D, HTML DOM, OpenVG, Skia, and SVG DOM. Other 2D/refusal behavior can live in individual factory or draw methods, which is why an absent override is not itself proof of 3D.
18.9 Four interface-default failure shapes
The shared renderer contract is over two thousand lines and not every default has the same semantics:
-
1.
Effect-discarding fallback.
The ordinary effect-aware draw defaults discard the GpuDrawParams block, then call coloured primitive submission. Texture, lighting, fog, skinning, and custom effect state can disappear without an exception.
-
2.
Hard refusal. Instancing and backbuffer readback defaults throw unless a narrowly defined unsupported-3D policy path intercepts the operation.
-
3.
Silent no-op. Several state/debug hooks have empty defaults; a public cache may update even when no native state changes unless the shared setter guards that path.
-
4.
Null factory. Optional resource/effect factories may return null, leaving the wrapper to refuse, degrade, or expose an inert object.
The renderer chapters in Part IV classify the actual destination of each family instead of inferring it from override presence. Thirty-one families own both ordinary extended draw methods; eleven inherit both. Of the inherited group, DIRECTX10 alone reaches a real coloured draw after effect state is discarded, while refusal/no-op/trace families terminate differently. Four additional override families retain a conditional coloured tail.
18.10 A portable draw claim
“The triangle rendered” is incomplete. A durable vertex-path claim names:
-
•
the public identity and implementation family;
-
•
the vertex declaration, packed stride, index width, and binding rates;
-
•
the capability answers and any known report/path contradiction;
-
•
the applied effect and whether the renderer consumed its GpuDrawParams;
-
•
the native driver/host that engaged;
-
•
the pixel, query, or structured oracle that distinguished the expected route from a coloured fallback or no-op.
With those nouns present, a failure can be localized to packing, binding normalization, capability reporting, effect dispatch, or native rendering instead of being summarized as “this renderer does not work.”