Graphics architecture
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. Creation, frame and disposal order were read from GraphicsDevice.cpp, GraphicsDeviceManager.cpp and the descriptor header; the device-loss family list comes from searching the renderer sources for the device-event callback, not from running any renderer. Named tests exist but were not executed for this page.
GraphicsDevice (GraphicsDevice.hpp, GraphicsDevice.cpp) is the public state and lifetime center of the graphics stack. A registry-selected renderer translates its resources and commands into one graphics API, and a pre-construction descriptor supplies the window and surface requirements that no renderer object can answer before it exists. This page states how those pieces are created, how a frame reaches a renderer, who owns which resource, and how much CNA can honestly promise about device loss. The selection machinery has its own tour in renderer selection internals.
Device creation
Gameconstructs its value memberGraphicsDevice_inside the base constructor, with the default adapter,GraphicsProfile::Reachand defaultPresentationParameters. TheGraphicsDeviceconstructor callsresolveRenderer().resolveRenderer()asks the selection layer for an attempt order (an explicitSetPreferred, else theCNA_GRAPHICS_RENDERERenvironment variable, else the compiled default; the selection latches once a device has created a renderer) and walks it. For each candidate it looks up aGraphicsRendererDescriptorin theGraphicsRendererRegistry, the table thatRendererRegistry.cmakegenerates intocna_graphics_core. With no fallback chain opted in there is exactly one attempt and its exception propagates unchanged.- The descriptor declares the early needs: the window kind (
None,Plain,OpenGL,VulkanorMetal), whether a window and the video subsystem are needed at all (four families need neither:HEADLESS,SOFTWARE,STUB,PORTABLEGL), the high-DPI policy (only Metal setswantsHighDpi), the GL framebuffer bits that must be fixed before the window exists (OPENGL4andFNA3Drequest 24-bit depth, 8-bit stencil and double buffering, because a GLX visual is chosen at window creation), and which single narrow service the family is handed: a GL context, a Vulkan surface or a surface presenter. createOrAttachWindow()lets the platform create a compatible window, or adopts a caller-supplied one throughPresentationParameters.DeviceWindowHandle(borrowed, never destroyed or recreated). A CPU family that presents through the platform gets a window and a presenter only where the platform reports surface presentation and no native window handle; see the platform contract.createRenderer()buildsGraphicsRendererCreateArgs(the surface snapshot, only the services the descriptor asked for, the presentation format, the device-event callback) and callsdescriptor.create(args). The selection latches as soon as the renderer object exists; the constructor then applies the initial blend, rasterizer and, when the renderer supports depth and stencil, depth-stencil state objects.- Later,
GraphicsDeviceManagerapplies the game's preferences to that existing device: the requested profile, the presentation mode, then an in-placeReset(window, virtual resolution, presentation formats, MSAA and swap interval are applied to the live renderer). The device pins the resolved descriptor, so rebuilding a renderer on a live device (RecreateRendererForMultiSampleCount) reuses it instead of re-running resolution; an MSAA change cannot switch graphics APIs mid-game.
The descriptor exists because virtual calls on a renderer cannot answer requirements before that renderer and its compatible window have been constructed. Its adapter-level hooks (profile support, render-target and back-buffer format support, depth-format selection and MSAA clamping) exist for the same reason: GraphicsAdapter queries run before any device does. The identity list is checked at configure time and the descriptors are compiled into the registry, so adding a renderer is build-graph work as well as a factory class.
Frame and submission flow
Game::BeginDraw
└─ GraphicsDeviceManager::BeginDraw
├─ IGraphicsRenderer::CanBeginDrawEXT() false → skip Draw and EndDraw for this tick
└─ GraphicsDevice::AcquireRendererThreadContextLeaseForFrame()
└─ game Draw: state, resource and command translation
└─ Game::EndDraw → GraphicsDeviceManager::EndDraw → GraphicsDevice::Present
├─ reject if a render target remains bound
├─ short renderer-thread-context lease around IGraphicsRenderer::Present
└─ UpdateViewportFromWindow; the frame lease is released after Present (also on an exception)
Backend synchronization is implementation-specific. Do not infer Vulkan, WebGPU or Direct3D fences from the public API; inspect the selected renderer's frame and present implementation, starting from the indexed draw trace. A renderer that needs no context lease returns none; the GL families (EasyGL and OpenGL4) override it so that a complete operation runs with the renderer's GL context owned by the calling thread, serialized against other threads. A game with no registered device manager still presents through Game::EndDraw calling GraphicsDevice::Present() directly.
Resource lifetime
Buffers, textures, render targets, effects and related device resources register with GraphicsDevice. A registry entry is a raw pointer that tracks disposal; it is not an allocation claim, and each resource also holds a weak lifetime token so that one destroyed after its device does not dereference it. On disposal the device marks itself disposed first (so re-entrant disposal is a no-op), raises Disposing, moves the registry into a local list and disposes every registered resource while the renderer still exists, and only afterwards resets the renderer, the surface presenter and the window and releases its video-subsystem reference. Resource implementations must tolerate that documented disposal path and must not require already-destroyed platform services. GraphicsDeviceLifecycleTest.DisposalIsReentrantAndReleasesABoundRenderTarget and GraphicsDeviceLifecycleTest.RendererFacingOperationsRejectUseAfterDeviceDisposal pin the device side; the textures and render targets trace follows one resource end to end.
State and render targets
The public device models XNA-style graphics state. Backends translate blend, depth-stencil, rasterizer, sampler, vertex and index bindings and render targets to their native representation, so a change to a state object is cross-renderer work unless it is proven to sit entirely in public validation or caching. A device that has a render target bound refuses to present: GraphicsDevice::Present throws an InvalidOperationException, and the framework does not unbind on the application's behalf (the reasoning is written up in present_lifecycle_contract_test.cpp, which several renderer families register; not executed here). Presentation is valid only for the back-buffer path: SetRenderTargets(nullptr, 0) returns to it, and targets must be resolved or unbound first. The user guide describes the same rule from the game's side.
Renderer change checklist
- Locate the neutral contract and every registry identity that claims it; five GL-profile identities share one family, so one source change can move five identities.
- Trace creation, destruction and device-dispose behavior for the resource, including the disposal order above.
- Check shader and input-layout translation and coordinate and format conversions.
- Verify render-target transitions, swapchain resize and present behavior.
- Run focused renderer tests on a private display, with validation layers or debug output where the family wires them;
run_gpu_tests_private.shexists so that window and GPU tests do not touch the desktop. - Compare at least one independent backend: passing only the changed backend can preserve a wrong shared assumption. CNA's cross-renderer parity fixtures exist for exactly this (see verification: renderers); their oracle is the fixtures' own assertions, not real XNA.
Device loss/reset
The repository contains backend-specific failure, recovery and resize handling, but nothing shows that every registered renderer provides one uniform, complete device-loss recovery guarantee. Treat loss and reset as backend-specific until the selected implementation and its tests demonstrate otherwise. What the source establishes is the plumbing:
- A renderer that detects a real loss reports
RendererDeviceEvent::Lost,ResettingorResetthroughGraphicsRendererCreateArgs::deviceEventCallback;GraphicsDeviceturns those into itsDeviceLost,DeviceResettingandDeviceResetevents and status, and raises content-lost notifications for resources whose contents were lost.GraphicsDeviceManagerTest.RendererDetectedDeviceLostIsForwardedToManagerListenerscovers the forwarding. - By reading the renderer sources, six families use that callback: Direct2D, DirectX 9, DirectX 11, DirectX 12, Vulkan and WebGPU, the last only from its debug simulation hooks (a real WebGPU device loss is only logged, not reported). Vulkan is the limiting case: it reports
Lostonce and then throws from the failing call, and by its own error text it does not attempt a reset. EasyGL has browser context-loss handling of its own. The remaining families do not use that callback, and the defaultDebugSimulateContextLossandDebugRestoreContexthooks are no-ops. CanBeginDrawEXT()is how a family that can lose its context or device (DirectX 11, DirectX 12, WebGPU, EasyGL) keeps the game'sUpdaterunning while it refusesDraw.- An application-initiated
GraphicsDevice::Resetis a separate path: it raises the same two events directly, unbinds bound render targets first, and does not imply that anything was lost.
Identities, families and the two OpenGL families
CNA exposes 25 public renderer identities over 21 implementation families. The identity-to-family map is generated from RendererRegistry.cmake on the selection axes index; the family-first view with build gates and internals pages is Graphics backends. Two relationships matter when reading GL code. OPENGLES2, OPENGLES3, OPENGL33, WEBGL1 and WEBGL2 are five profiles of the single EasyGL family (the sibling easy-gl and meta-gl libraries), and the profile is a run-time value, so several of them can share one binary. OPENGL4 is not a sixth EasyGL profile: it is a separate family with its own directory, target (cna_renderer_opengl4), namespace and descriptor, its own gl4_-prefixed loader for desktop GL 4.1 core, and no dependency on easy-gl. The two families share GL semantics through headers in the graphics module (GlStockShaderSources.hpp for the stock-shader corpus, GlPresentationSurfaceState.hpp for the presentation transform, PlatformGlRendererState.hpp for the context owner), so an XNA-semantic change to one usually has to be checked in the other. PORTABLEGL cannot be linked beside either, because it defines the global gl* symbols; no configure rule forbids OPENGL4 beside an EasyGL identity, but none of CNA's multi-renderer CI sets contains that pair.
Where to go deeper
- GraphicsDevice internals, then indexed draw trace and textures and render targets.
- Renderer selection internals for the registry, descriptor and latch, and Graphics backends for the family map.
- One family tour: EasyGL, Vulkan, SDL_gpu, Software, Headless or Stub.
- The user-level view: Renderers and Runtime renderer selection.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- CANVAS, HTML_DOM and SVG_DOM: browser object models, refusals and evidence — How CNA's three non-WebGL browser renderers represent sprites, targets and blending, where each refuses work, why two cannot read the back buffer, and which browser tests actually run.
- 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.
- Compiled XNA effects: admission, reflection, passes and renderer runtimes — What happens to Direct3D 9 Effect Framework bytecode in CNA: admission order and preflight bounds, the reflected object graph, parameter upload, pass-state publication, cloning, the XNB EffectReader and per-renderer translation.
- Coordinate and composition conventions: handedness, row vectors, depth and clip space — CNA's right-handed basis, row-vector matrices applied in reading order, quaternion products that run the other way, the [0,1] depth range derived three times, clip-space W and the ToColumnMajor bridge.
- Core framework and graphics API map — An orientation map of CNA's core framework and graphics API: owning headers, public shape, the boundary that trips ports, and where each behaviour is explained, for math, framework, content, resources, effects and device state.
- 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.
- Device reset, disposal, adapters and format queries — Order and failure boundaries of GraphicsDevice::Reset, the manager's hooks, device-loss status, resource registration and disposal, plus CNA's adapter, format and profile queries.
- DIRECT2D and GDI: two Windows 2D delivery stacks — How CNA's two Windows-only 2D renderers draw, refuse, present and recover: Direct2D over a private Direct3D 11 device versus GDI over a private CPU 2D core, and what their tests prove.
- Direct3D evidence: MinGW cross-builds, Wine translators and native Windows — How DIRECTX9, DIRECTX11 and DIRECTX12 are built and tested: MinGW cross-builds, DXVK and vkd3d-proton gates, the shared parity inventory, forced-headless runs and the manual Windows job.
- Direct3D presentation, swap interval, clears, viewport and scissor — How DIRECTX9, DIRECTX11 and DIRECTX12 treat presentation modes, PresentInterval, back-buffer and depth formats, full screen, flip-model rebinding, viewport, scissor, clears and depth bias.
- DIRECTX11 and DIRECTX12 internals: lifetimes, frames in flight, descriptors and target finalisation — Device creation, lifetime groups and recovery, Direct3D 12 frame slots and growable descriptor heaps, HeadlessEXT, readback, MRT and cube finalisation and render-target usage on the DXGI renderers.
- DIRECTX9: stock-effect bytecode, device lifecycle and oracle findings — How the XNA-fidelity renderer compiles Microsoft's stock effects, enforces GraphicsProfile from D3DCAPS9, recovers lost devices, handles targets and why its sprite projection is what it is.
- EasyGL state, clears, targets, queries and buffers: current semantics — What the EasyGL GL-profile renderer does at this snapshot for wireframe, occlusion counts, colour masks, clears, two-sided stencil, fog, base vertex, render targets, context-loss policy and viewports.
- EasyGL: five GL profiles, one implementation, and the other GL-named renderers — What OPENGLES2, OPENGLES3, OPENGL33, WEBGL1 and WEBGL2 share in EasyGL, where they differ, how far evidence carries between them, and how OPENGL4 and PORTABLEGL differ.
- 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.
- Evidence tiers of the native modern GPU renderers — What VULKAN, SDL_GPU, WEBGPU and METAL implement at this snapshot, what evidence backs each, why a capability bit is not evidence, and the defect shapes these renderers exposed.
- 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.
- Graphics resource lifetime: tracking, copies, moves and disposal — Which graphics resources a CNA GraphicsDevice tracks, how copies and moves of each resource type behave, what disposal unbinds, and which resources report lost content.
- 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.
- Math value types in C++: object layout, equality, hashing and API shape — Which CNA math types carry a vtable, why Color is 24 bytes on 64-bit hosts, the internal vertex stream structs, output-reference aliasing, exact equality, hash and ToString differences, and the split argument exceptions.
- Planes, rays and bounding volumes: exact containment and intersection semantics — Half-space conventions, plane transforms, ray tolerances, box corner order, sphere and frustum containment rules in CNA, compared function by function with XNA 4.0, with workarounds for every mismatch.
- Presentation modes, swap interval, native handles and back-buffer readback across renderers — What each renderer family does with the presentation mode, swap interval, formats and full-screen request, how window handles are borrowed, and exactly what GetBackBufferData returns.
- Rectangle, Point and Color: integer geometry and packed colour semantics — Rectangle's half-open edges, touching and empty rules, Point's missing Vector2 bridge and rounding, Color's AABBGGRR word, the 141/140/139 named-colour counts, construction rounding and premultiplied alpha.
- Render targets: usage, cube faces, resolve and readback — RenderTarget2D and RenderTargetCube semantics at the pinned snapshot: construction, binding rules, RenderTargetUsage, cube faces, resolve and mip generation, readback, disposal and lost content.
- Render-target binding, clears, viewport and scissor across renderers — The shared SetRenderTargets transaction, the no-op rebind, what RenderTargetUsage selects, which clears throw, and how each renderer family applies clears, viewport and scissor.
- 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.
- SDL_GPU uploads, render-target lifetime and swapchain recovery — What SDL_gpu validation exposed in CNA's SDL_GPU renderer, when uploads may cycle, how render targets outlive their wrappers, what MRT writes, and how a failed swapchain acquisition keeps the frame.
- SDL_RENDERER: the 2D contract, its refusals and its evidence — Where SDL_RENDERER's 2D boundary sits: execution-time 3D refusal, its single capability, emulated and unhonoured XNA features, address modes, clears, readback coordinates and how its tests run.
- SpriteBatch sorting, flushing and renderer batching — How CNA's SpriteBatch flushes and sorts (XNA's unstable quicksort, reproduced), what each renderer kind does with the sprite stream, the viewport-local projection and the Direct3D 9 half-pixel offset.
- SpriteBatch: state, lifecycle and error semantics — Exact shared SpriteBatch contract in CNA: Begin defaults, when states reach the device and what End leaves behind, Immediate exclusivity, exceptions, Draw overloads, sub-pixel destinations and texture lifetime.
- SpriteFont and DrawString text layout — Exact text layout in CNA: SpriteFont tables and default character, per-glyph advance and the XNA first-glyph rule, MeasureString, UTF-8 decoding, whole-string flips, rotation and sub-pixel glyphs.
- State objects: identity, binding and what reaches the renderer — BlendState, DepthStencilState, RasterizerState and SamplerState in CNA: shared identity, XNA's freeze-on-bind rule, what each renderer hook receives, profile checks and per-family support.
- Surface formats: profile gates, renderer verdicts and format usage — How CNA decides whether a texture, cube, volume or render target may use a SurfaceFormat: per-resource profile tables, the renderer verdict, draw-time rules and usage masks.
- Texture data transfer: SetData, GetData, mip levels and streams — Exact SetData and GetData semantics for Texture2D, TextureCube and Texture3D: transfer windows, the CPU shadow, mip levels, compressed blocks, readback and FromStream.
- 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.
- The XNA stock effects: exact semantics, worked uses and verification history — BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect and SkinnedEffect in CNA: defaults, formulas, ordering traps, per-vertex versus per-pixel lighting, worked uses and the defect patterns behind the current code.
- Vector, Matrix and MathHelper numerics: interpolation, clamping and degenerate inputs — What CNA's vector, matrix and MathHelper functions return for out-of-range amounts, inverted clamps, NaN, zero vectors, singular matrices and bad camera input, compared with XNA 4.0, plus the precision and test evidence.
- Vertex and index buffers: CPU shadows, SetDataOptions and layouts — How CNA vertex and index buffers store, upload and read data, what SetDataOptions does on each renderer family, and how vertex layouts reach a renderer by stride and by declaration.
- 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.
- Vertex packing: the glTF stride ABI, index widths and topology — The eleven canonical vertex strides, how a glTF primitive's layout is chosen, typed versus raw upload, index narrowing, the seven topologies, tangents, mirroring and the hard limits the bytes impose.
- 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.
- Vulkan presentation, frame pacing and back-buffer readback — How CNA's VULKAN renderer picks its swapchain format and present mode, synchronises two frames in flight, and reads the back buffer without racing the presentation engine.
- WebGPU renderer semantics: surfaces, targets, mips and pipeline state — Exact behaviour of CNA's WEBGPU renderer: non-sRGB surface policy, target-relative SpriteBatch coordinates, cube render targets, blit-free mip generation, dynamic and baked state, ordered clears and BC textures.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-029: Copying a GraphicsResource bypasses GraphicsDevice resource tracking (copies of Texture2D and TextureCube escape device disposal) — A copy-constructed Texture2D or TextureCube is never registered with its GraphicsDevice, so device disposal neither disposes it nor releases the renderer texture it shares, and a cross-device copy assignment leaves a sta
- CNA-BUG-054: HEADLESS reports occlusion-query support and a precise pixel count while its query always answers 1 — HEADLESS inherits OcclusionQuery = true and the default isPixelCountPreciseEXT() = true, but HeadlessOcclusionQueryRenderer completes at once and returns PixelCount() == 1 whatever was drawn.
- CNA-BUG-097: RenderPipeline::releaseDeviceResourcesEXT() keeps the Bloom, SSAO and volumetric-fog target pools, and the memory estimate never counts them — The DeviceReset handler documented to drop every target the pipeline owns resets only the scene target and the chain's pool; the private pools of BloomPass, SsaoPass and VolumetricFogPass survive, and getGpuMemoryEstimat
- CNA-BUG-224: GraphicsDevice's DeviceLost documentation and device-status comments still say only Direct3D 9 reports device loss — The DeviceLost Doxygen says the event is never raised on desktop, and three maintainer comments say only Direct3D 9 calls the device-event callback, while five renderer families raise DeviceLost from their real error pat
- CNA-GAP-064: WEBGPU does not propagate a real device loss: OnDeviceLost only logs, so DeviceLost, the device status and the draw gate react only to the debug hooks — WebGPURenderer sets its lost flag, closes CanBeginDrawEXT() and raises DeviceLost only inside DebugSimulateContextLoss; the callback registered for a real loss only writes to stderr. CNA's own plan (WEBGPU-182) records t
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- How renderers work · Runtime renderer selection · Render targets: restoring the back buffer · Graphics state
- Architecture
- Renderer registry, descriptors and selection · Architecture overview · Runtime lifecycle · Platform architecture
- Internals
- GraphicsDevice internals · Renderer selection internals · Indexed draw trace · Textures and render targets · Graphics backends (family map) · EasyGL renderer internals · Vulkan renderer internals
- Maintainer workflow
- I need to fix a renderer bug · Blast radius and readiness
- Tests and validation
- Test architecture · Verification: renderers
- Reference
- Selection axes: renderer identities · Module index