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 c1c316b9; 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; a failure surfaces fromGame's constructor asNoSuitableGraphicsDeviceException(passed through, or wrapping the cause), as XNA'sGraphicsDeviceManager.CreateDevicereports it. 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 (three families need neither:HEADLESS,SOFTWAREandSTUB), the high-DPI policy (only Metal setswantsHighDpi), the GL framebuffer bits that must be fixed before the window exists (FNA3Drequests 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
(inside Game::Tick, which already holds the game-thread context lease from PollEvents to the end of the draw)
Game::BeginDraw
└─ GraphicsDeviceManager::BeginDraw
├─ IGraphicsRenderer::TryBeginDrawEXT() 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; EasyGL, the GL family, overrides it so that a complete operation runs with the renderer's GL context owned by the calling thread, serialized against other threads. Game holds such a lease for Initialize, BeginRun, every tick (Update and Draw) and EndRun, and texture, buffer and render-target construction, SetData, GetData and Dispose take one on whatever thread they run, so a loading thread works between frames; a game thread that waits inside a frame for such a thread deadlocks (the frame trace). 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; three GL-profile identities share one family, so one source change can move three 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, four families use that callback: DirectX 9, DirectX 11, 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 9, DirectX 11, WebGPU, EasyGL) keeps the game'sUpdaterunning while it refusesDraw. The manager asks it throughTryBeginDrawEXT(), which DirectX 9 overrides to attempt its pending device recovery before answering.- 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 OpenGL family
CNA exposes 14 public renderer identities over 12 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. One relationship matters when reading GL code. OPENGLES3, OPENGL33 and WEBGL2 are three 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; every other identity is its own family. EasyGL is CNA's only OpenGL family: OPENGL33 keeps a 3.3 contract, and there is no separate desktop GL 4 renderer. The GL-specific helpers live with the family (GlStockShaderSources.hpp for the stock-shader corpus and GlPresentationSurfaceState.hpp for the presentation transform, both under modules/renderers/easygl), while PlatformGlRendererState.hpp, the context owner, stays in the graphics module's common renderer headers. Of the other families, only SDL_RENDERER is deliberately 2D-only, and STUB reports no capability at all.
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.
- 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 and DIRECTX9 compile custom HLSL, resolve uniform names by reflection, feed SpriteBatch and 3D draws and bind textures, and where DIRECTX9 stops.
- 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.
- Direct3D evidence: MinGW cross-builds, Wine translators and native Windows — How DIRECTX9 and DIRECTX11 are built and tested: MinGW cross-builds, DXVK gates and separate Wine prefixes, the shared parity inventory, the manual Windows job and what each evidence tier proves.
- Direct3D presentation, swap interval, clears, viewport and scissor — How DIRECTX9 and DIRECTX11 treat presentation modes, PresentInterval, back-buffer and depth formats, full screen, flip-model rebinding, viewport, scissor, clears and depth bias.
- DIRECTX11 internals: lifetimes, recovery, readback and target finalisation — DIRECTX11 device creation, the three lifetime groups and device recovery, compute and indirect draws, the HeadlessEXT refusal, back-buffer readback, MRT and cube finalisation and render-target usage.
- 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: three GL profiles, one implementation, and how far evidence carries — What OPENGLES3, OPENGL33 and WEBGL2 share in EasyGL, where they differ, how far evidence carries between them, and how to choose among them.
- 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: A copied Texture2D or TextureCube raises GraphicsDevice.ResourceDestroyed without a matching ResourceCreated, and a copy taken after its device died still reaches the device on disposal — Texture copies are now registered with their device, but a copy never raises ResourceCreated while its disposal raises ResourceDestroyed, and a copy taken after the device was destroyed still touches the dead device's te
- 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-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 three renderer families raise DeviceLost from their real error pa
- 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