Indexed draw trace
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. The Vulkan and Software paths were re-read at this snapshot; the listed tests exist but none was run for this page.
The public call is the same on every renderer; the GPU boundary is not. This trace follows GraphicsDevice::DrawIndexedPrimitives through its shared validation into two contrasting families: Vulkan, which snapshots the draw and replays it at Present, and Software, which rasterises into a CPU framebuffer before the call returns. A maintainer who knows where the shared layer ends can tell a bug in the neutral contract from a bug in one translation layer.
Call and submission paths
Game::Draw (an effect pass applied, vertex and index buffers bound)
→ GraphicsDevice::DrawIndexedPrimitives(type, baseVertex, minVertexIndex, numVertices,
startIndex, primitiveCount)
→ public validation, effect snapshot into GpuDrawParams, offset folding, stream table
→ applySamplerStatesToRenderer() → validateDrawState(), rasterizer + sampler state
→ IGraphicsRenderer::DrawIndexedPrimitivesEx(vb, ib, world, view, projection,
type, primitiveCount, params)
├─ VulkanRenderer: copy bytes + state into a Pending3DDraw → pending3D_
│ … GraphicsDevice::Present → VulkanRenderer::Present → SubmitFrame(false)
│ → fence wait → retire → vkAcquireNextImageKHR → RecordCommandBuffer
│ → vkQueueSubmit → vkQueuePresentKHR
└─ SoftwareRenderer: DrawIndexedPrimitivesInternal → decode indices → transform,
clip, rasterise into CurrentFramebuffer()
… GraphicsDevice::Present → SoftwareRenderer::Present
→ no-op off-screen, or an RGBA8 frame to the platform surface presenter
→ CNA_RECORD_GRAPHICS_DRAW (diagnostic counter, only after the renderer returns)
This split is a lifetime rule. A family that defers work must not keep unprotected pointers into caller-owned mutable vertex, index or effect state, because the game can change or destroy them before submission. A family that rasterises immediately still needs stable state for the duration of the call, but its submission boundary is the call itself.
Step 1: the neutral public method
GraphicsDevice::DrawIndexedPrimitives is the only shared validation on this route. In order:
ThrowIfDisposed(); return silently if there is no renderer;renderer_->Ensure3DSupported("GraphicsDevice::DrawIndexedPrimitives"), which a 2D-only family uses to throw or, withWarnAndStub, to warn.numVerticesandprimitiveCountmust be positive;ValidateProfilePrimitiveCount()applies the profile's primitive limit;CheckedPrimitiveElementCount()derives the consumed index count from the topology.- No applied effect, no index buffer or no vertex buffer is an
InvalidOperationException; a bound but disposed buffer is anObjectDisposedException. ExtractMatrices()reads world, view and projection from the applied effect;Effect::FillGpuDrawParams()captures the effect's parameters (textures, lighting, fog, alpha test, skinning, PBR, a compiled-effect runtime or custom-shader request) intoGpuDrawParams.- Offsets:
startIndexis copied unchanged;params.baseVertexisbaseVertexplusFoldedVertexStreamOffset(), added with a saturating helper so the fold itself cannot overflow. FillVertexStreamBindings()describes every bound stream (see below).- Range checks, only when there is no stream table (the legacy empty-declaration case) or the renderer answers true to
RequiresManagedBufferedDrawRangeValidationEXT(): non-negativestartIndexandminVertexIndex, the consumed indices inside the index buffer, and the declared vertex window inside each shader-consumed stream. ValidateVertexStreamCapability(): more than one stream of the same input rate needsMultiStreamVertexInputand at mostGetMaxVertexStreams()streams; the binding list is never silently truncated.applySamplerStatesToRenderer(): runsvalidateDrawState()— XNA's draw-time profile rules: no filtered sampling of point-filter-only float formats (except inside the CNAEXT engine layer's float-filtering scope, when the renderer can filter them), clamp addressing for non-power-of-two textures underReach, no blending into non-blendable render-target formats — then pushes rasterizer state and every sampler slot's filter, address, anisotropy and mip state.renderer_->DrawIndexedPrimitivesEx(...);CNA_RECORD_GRAPHICS_DRAW(true, primitiveCount)runs only if that returns.
Element offsets, not byte offsets
startIndex is an index-element offset. baseVertex and each VertexBufferBinding.VertexOffset are vertex-element offsets. FoldedVertexStreamOffset() takes the smallest VertexOffset among the bound per-vertex streams and folds it into baseVertex once; each per-vertex stream keeps the remainder as its own vertexOffset, and a per-instance stream keeps its whole offset because baseVertex never advances it. The renderer converts elements to bytes with each stream's own declaration stride. Multiplying by a stride in the neutral layer, or shifting startIndex by a vertex offset, applies the wrong unit — most visibly with multi-stream geometry.
Each GpuVertexStreamBinding carries the slot, the stream's renderer buffer, its stride, its instance frequency, its capacity (VertexBuffer::VertexCount, not the length of the last SetData, because XNA lets a game rewrite a prefix of a larger buffer), the element offset, and per-element effective usage indices: a repeated (usage, usageIndex) pair across streams is moved to the next free index of that usage, as FNA3D's drivers do. vertexShaderInputUsed marks streams the stock program actually reads; ranges are validated only for those. A single per-vertex buffer whose declaration is empty (the CNAEXT VertexBuffer(device, count) convenience form) is deliberately left as vertexStreamCount == 0, so the renderer uses its own upload stride.
RequiresManagedBufferedDrawRangeValidationEXT() | Families | Consequence |
|---|---|---|
| false (override) | EasyGL, OpenGL4, Software | Ranges are forwarded as XNA forwards them to the native API; the family promises it cannot read outside host memory (GL never reads host memory for them; Software defaults missing records). |
true (the IGraphicsRenderer default) | every other family, including Vulkan | The device keeps its compatibility range guard, because these families stage draw input through CPU copies. |
This method does not issue a Vulkan command, acquire a swapchain image or know a Direct3D input layout. Those are deliberate abstraction boundaries.
Step 2A: Vulkan snapshots, then replays
VulkanRenderer::DrawIndexedPrimitivesEx casts to its own vertex and index buffer renderers and computes the index width (2 or 4 bytes), the consumed index count and a pointer to the selected span of the CPU-mapped index data.
- Custom shader route. If the effect requested a custom
ShaderEffectprogram, the draw goes toQueueCustomEffect3DDrawEXT, which builds its own immutable stream snapshots, and the method returns. - Multi-stream packing. With several per-vertex streams,
VulkanIndexedStreamWindowfinds the vertex window the selected indices reach andPackVulkanStreamsEXTinterleaves exactly that window into one record, with a combined declaration and a rebased native base vertex. - Program family. Stock program families (alpha test, dual texture, environment map, skinned, PBR and skinned PBR, lit textured/untextured/coloured, the BasicEffect shapes) are chosen from the effect parameters and the vertex declaration, not from the stride alone. A multi-stream declaration that does not supply every input of the chosen program is refused here (
NotSupportedExceptionfor the packed record) rather than letting pipeline creation fail later atPresent; an incomplete single-stream declaration falls back to the stride-inferred layout behindRequireFaithfulDeclarationEXT, which refuses only what that layout would read from the wrong bytes. Stride lists for PBR and skinned buffers and the bone-index format check apply only when no complete declaration exists. - Sources and matrices.
NoteSampledSourcesEXTrecords the render targets this draw samples, so a mid-frame readback flush can replay their producing passes first. The world-view-projection matrix isworld * view * projection * XnaPixelCenterCorrectionEXT(primitive), the Direct3D 9 pixel-centre convention post-multiplied in row-vector order. - Compiled effects. With
CNA_VULKAN_COMPILED_EFFECTSthe compiled program replaces the stock selection (PrepareCompiledEffectDrawEXT); without it a compiled-effect draw throwsNotSupportedExceptioninstead of silently using a stock shader.
The method then fills a Pending3DDraw (declared in VulkanRenderer.hpp): the vertex bytes (the whole bound buffer, vertexCount × stride, or the packed window), the base vertex, only the selected index bytes, topology, draw count, index type, depth, stencil, blend, cull, wireframe and depth-bias state, the destination render target, the stride, the family flags and push constants, and the vertex layout taken at draw time. PushPending3DDraw() adds what must also be frozen at enqueue time — the active occlusion query, scissor, viewport, blend factor, the bind-cycle segment the draw belongs to and its position in the frame's command order — and appends the record to pending3D_. The snapshot exists because buffers, bindings and effect passes may change before the frame is recorded: a new field that affects replay must be captured here; reading a mutable renderer field only in RecordCommandBuffer gives earlier draws the last draw's state.
Present and SubmitFrame
VulkanRenderer::Present() calls SubmitFrame(false):
- Wait on the current frame slot's fence (a lost device is detected here, at acquire, at submit and at present by
CheckDeviceLostEXT, which raises the device-lost event once and throws). ProcessRetiredResources(false): free retired handles and MRT proxies whose generation is more thanMaxFramesInFlight(2) frames old.vkAcquireNextImageKHR; onVK_ERROR_OUT_OF_DATE_KHRrecreate the swapchain and return without resetting the fence, so a failed acquire cannot leave the slot's fence unsignalled forever.- Reset the fence and command buffer,
RecordCommandBuffer()replays the pending passes,vkQueueSubmitwith the image-available and render-finished semaphores, advance the retirement generation. vkQueuePresentKHR; out-of-date or suboptimal recreates the swapchain; the frame slot advances moduloMaxFramesInFlight.
A draw returning to game code is therefore not evidence that the GPU executed it; the queue and fence are the execution boundary. Backbuffer readback takes a separate route: ReadBackbuffer() calls SubmitFrame(true), which records a copy into a staging buffer, waits for the fence and holds the image; the pixels are read and only then FinishDeferredPresent() presents.
Step 2B: Software draws now
SoftwareRenderer::DrawIndexedPrimitivesEx calls DrawIndexedPrimitivesInternal(..., false) (the instanced route calls it once per instance with adjusted per-instance offsets).
- Reject
primitiveCount <= 0and any topology other than TriangleList, TriangleStrip, LineList, LineStrip andPointListEXT. - A compiled-effect draw requires a build with
CNA_SOFTWARE_COMPILED_EFFECTS(otherwiseNotSupportedException); compiled point lists are not implemented. - Declared streams are read by semantic. Only a buffer with no declaration falls back to the recognised stride set (16, 20, 24, 32, 48, 52, 56, 60, 68, 76, 80 bytes); any other stride without a declaration throws.
- Strict addressing validation (
ValidateIndexedAddressing) runs only for the legacy no-stream-table or empty-declaration shapes, whose fallback would otherwise form raw host pointers. For declared streams the ranges pass through as in XNA, and an index or vertex record outside its buffer is skipped rather than read. - Snapshot the draw state by value: depth, stencil, blend state and blend factor, the active viewport transform and the clip rectangle (framebuffer ∩ viewport ∩ scissor when enabled).
- For each primitive, decode the index at
startIndex + local(16- or 32-bit), addbaseVertexonce, and resolve each consumed stream atstream.vertexOffset + vertexIndexwith that stream's own stride (a per-instance stream at its own offset). - Transform by
world * view * projection, clip to the frustum, fan the clipped polygon, and rasterise intoCurrentFramebuffer()— the bound render target or the backbuffer — or, for a compiled effect, into every bound MRT framebuffer.
There is no Vulkan-style Pending3DDraw: when the method returns, the pixels are written. SoftwareRenderer::Present() (in SoftwareRenderer2DState.cpp) is a no-op without a surface presenter; with one (Terminal on a TTY) it resolves MSAA and hands the backbuffer — never a still-bound render target — as an RGBA8 frame to the presenter. In the GDI build the same sources are compiled with CNA_SOFTWARE_2D_ONLY, where this method throws NotSupportedException.
A bug common to both families usually belongs in the public offset or state construction; a bug in only one is more likely declaration interpretation, program mapping, or raster and submission specifics.
Other families
Every family receives the same call; this page traces only two. Where the other implementations live, for a maintainer who needs to follow the same draw elsewhere:
| Family | DrawIndexedPrimitivesEx | Where it is traced |
|---|---|---|
| EasyGL | EasyGLRenderer.cpp | EasyGL renderer internals |
| OpenGL4 | OpenGL4StockDraw.cpp | OpenGL4 renderer internals |
| SDL_gpu | SdlGpuRenderer.cpp | SDL_gpu renderer internals |
| Headless | HeadlessRenderer.cpp: checks the effect inputs it was given and rasterises nothing | Headless renderer internals |
| Stub | no override; the base implementation forwards to DrawIndexedColoredPrimitives, which Stub implements as a no-op | Stub renderer internals |
| WebGPU, DirectX 9/11/12, FNA3D, Metal, PortableGL | WebGPURenderer.cpp, D3D9EffectDraw.cpp, DirectX11Renderer.cpp, DirectX12Renderer.cpp, Fna3dDraw.cpp, MetalRenderer.mm, PortableGLRenderer.cpp | Not traced in the Development area yet. |
| 2D-only families | HTML DOM and SVG DOM throw in Ensure3DSupported; Direct2D routes it through HandleUnsupported3DCall (throw by default, a warning under WarnAndStub); GDI's DrawIndexedPrimitivesEx throws through ThrowUnsupportedFeature; the others reach the base implementation or their own unsupported-3D handling | Renderers: 2D-only renderers |
Where failures appear
| Symptom | Inspect first |
|---|---|
| Immediate public exception | Effect and buffer binding, disposed buffers, profile primitive limit, the range checks the renderer asked for, validateDrawState() profile rules, stream capability. |
| Wrong geometry on every family | GpuDrawParams construction, element-versus-byte arithmetic, FoldedVertexStreamOffset(), usage-index remapping, matrix extraction. |
| Vulkan throws at draw time | The declaration guard: the chosen stock program needs an input the declaration does not supply. |
Vulkan fails only at Present | Deferred record completeness, RecordCommandBuffer, pipeline or descriptor creation, the submit result, validation-layer output. |
| Vulkan shows a later draw's state on earlier draws | A field read at record time instead of captured in Pending3DDraw or PushPending3DDraw. |
| Software differs from Vulkan | Software's declaration or stride fallback, index decoder, viewport and scissor clip, raster conventions, versus Vulkan's program family and input layout. |
| GPU appears to hang | Fence wait, acquire, queue submit and present; distinguish a stalled GPU from an application loop that never reaches Present. |
Historical case (fixed). An earlier Software renderer read indices from element zero in both indexed raster loops and fetched vbBase + index * stride with no base addend, so startIndex and a positive baseVertex were silently discarded and every draw rendered the buffer prefix, while the GPU families were already correct. The current loop adds both exactly once, and software_indexed_addressing_test.cpp (Software_IndexedAddressing) pins the contract: element offsets, a single baseVertex addend, topology-derived counts, and minVertexIndex/numVertices treated as hints only. It is the pattern to expect when one family disagrees: the neutral layer was right and one translation ignored a field.
Safe change path and evidence
Write or select a small indexed-draw fixture that fixes geometry, effect, declaration, offsets and expected pixels. Run the neutral tests first, then renderer conformance for the changed family and for an independent one. For Vulkan include a multi-frame case — a one-frame screenshot will not expose stale deferred state or resource retirement — and keep validation on: debug builds enable VK_LAYER_KHRONOS_validation when it is installed, and in any configuration that compiles VULKAN, TestHelpers.cmake makes every renderer test fail on a [Vulkan Validation] line (the exemption list is empty). For Software, test both declaration-driven and empty-declaration buffers.
| Level | Tests present at this snapshot |
|---|---|
Neutral (CnaGraphicsTests, run against the compiled-in renderer) | GraphicsDeviceValidationTests.cpp, DrawRouteValidationTests.cpp, IndexedDrawDeferredTests.cpp (start index, base vertex, 32-bit indices, range hints), OrdinaryDrawBindingOffsetTests.cpp and OrdinaryDrawMultiStreamTests.cpp (the element-offset and multi-stream contract), GraphicsProfileDrawLimitTests.cpp |
Vulkan (registered in vulkan/examples) | Vulkan_IndexedDrawRange, Vulkan_DrawRangeValidation, Vulkan_DeferredResourceLifetime, Vulkan_DeferredSourceLifetime, Vulkan_Deferred_Scissor, Vulkan_Deferred_Viewport, Vulkan_Swapchain_Sync, Vulkan_SwapchainOutOfDate, Vulkan_SwapchainChurn, Vulkan_DeviceLostContract, Vulkan_RenderTarget_ProducerConsumer_SyncVal; unit tests under vulkan/tests cover compiled effects |
Software (registered in software/examples) | Software_IndexedAddressing, Software_DrawRangeValidation, Software_DrawNoIndexBuffer, Software_VertexDeclaration, Software_PresentLifecycle; unit tests under software/tests cover presentation and compiled-effect conformance |
These are present in the source; none was executed for this page, and which host and CI job runs each is recorded on Test architecture and Verification: CI.
Read in execution order
GraphicsDevice.cpp:DrawIndexedPrimitives,FoldedVertexStreamOffset,FillVertexStreamBindings,ValidateVertexStreamRanges,ValidateVertexStreamCapability,applySamplerStatesToRendererandvalidateDrawState— the only shared validation and the exact delegate call.IGraphicsRenderer.hppin the renderer-contract headers (CNA/Internal/Renderers):GpuVertexStreamBinding,GpuDrawParams,DrawIndexedPrimitivesExandRequiresManagedBufferedDrawRangeValidationEXT— what can cross the boundary.VulkanRenderer.cpp:DrawIndexedPrimitivesEx,PushPending3DDraw,SubmitFrame,RecordCommandBuffer,ProcessRetiredResourcesandPresent, in that causal order.SoftwareRenderer.cpp:DrawIndexedPrimitivesInternaland its immediate framebuffer writes; thenPresentinSoftwareRenderer2DState.cpp.vulkan_deferred_resource_lifetime_test.cppandsoftware_indexed_addressing_test.cpp: pick the fixture that establishes the semantics you change, not merely a test with “draw” in its name.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Capability answers, interface defaults and draw evidence — What a GraphicsCapability answer asks and guarantees in CNA: the 19 contract questions, renderer versus device polarity, known report mismatches, the unsupported-3D policy, four interface-default failure shapes and portable draw claims.
- Custom HLSL ShaderEffect on the Direct3D renderers — How DIRECTX11, DIRECTX12 and DIRECTX9 compile custom HLSL, resolve uniform names by reflection, feed SpriteBatch and 3D draws, bind textures and build Direct3D 12 pipeline states.
- Effect object model: techniques, passes, parameters and the draw packet — What a CNA Effect contains in its stock, compiled and ShaderEffect forms: collections, pass application, Clone and Dispose, stock parameter tables versus XNA, EffectParameter storage and GpuDrawParams.
- Four shader routes: stock semantics, D3D9 stock sources, compiled effects and ShaderEffect — How CNA answers XNA's .fx: renderer-owned stock effects, DIRECTX9's recompiled Microsoft sources, compiled Effect Framework bytecode on qualified renderers, and the renderer-specific ShaderEffect contract.
- GraphicsDevice: the shared device contract — Exact device-level behaviour of CNA's GraphicsDevice: construction and windows, bound state objects, viewport and scissor, Clear and Present, bindings, draw calls, readback and extensions.
- SDL_GPU shader intake, pipeline keys and draw order — Why CNA's SDL_GPU renderer uses precompiled SPIR-V in SDL_gpu's set convention, which GLSL ShaderEffect accepts, how pipelines are keyed, what state is dynamic, and how draw order and vsync are kept.
- The renderer contract: IGraphicsRenderer defaults, factories and failure shapes — Which IGraphicsRenderer bodies a renderer family must write, what each inherited default does to a public call, how null factories fail, and the evidence ladder behind a feature.
- Vertex declarations, bindings and stream composition — From C++ vertex values to the renderer boundary: stream layouts, VertexDeclaration rules and profile limits, index widths, dynamic updates, VertexBufferBinding, semantic composition, the minimum-offset fold and draw validation order.
- Vulkan draw-time state, ordered clears, occlusion queries and descriptor pools — How CNA's deferred VULKAN renderer carries blend, stencil, viewport and scissor state, orders clears, counts occlusion queries and grows descriptor pools, with the defects behind each rule.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-006: SpriteEffect caches a MatrixTransform parameter that never exists, so OnApply() never sets the sprite projection — SpriteEffect looks up a MatrixTransform parameter its base Effect never creates, so Parameters["MatrixTransform"] is null and OnApply() returns before computing the orthographic projection and half-pixel offset.
- CNA-GAP-013: METAL refuses a large part of the graphics API until it has macOS evidence: MRT, custom effects, occlusion queries, instancing, multi-stream input, back-buffer readback and several sampler/blend states — MetalRenderer throws NotSupportedException for MRT, custom effects, occlusion queries, instancing, multi-stream input, back-buffer readback and some sampler/blend states, and applies every MSAA request as none.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- 3D rendering: binding and draw calls · Renderers: modern GPU APIs · Renderers: CPU renderers
- Architecture
- Graphics architecture
- Internals
- GraphicsDevice internals · Vulkan renderer internals · Software renderer internals · One frame source trace
- Maintainer workflow
- Fix a renderer bug · Add a regression test
- Tests and validation
- Test architecture · Verification: renderers
- Reference
- Test target index