GraphicsDevice internals
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. Test files named here exist at this snapshot; none was run for this page. Device-loss behaviour is stated from the source of each family, not from a run.
GraphicsDevice is CNA's public, XNA-shaped graphics state machine. It validates calls, owns the selected IGraphicsRenderer, tracks public resources and translates platform-independent state into the renderer contract; the renderer owns the API-specific GPU or CPU objects. Because every family sits behind this one class, a change here can alter every renderer even when no renderer file is edited. This page maps what the device owns, how it is built and torn down, and which rules a change must keep.
Objects and dependency direction
Game (owns Graphics::GraphicsDevice GraphicsDevice_ as a value member)
└─ GraphicsDeviceManager borrows the Game's device; applies preferences via Reset();
│ holds the per-frame renderer context lease
└─ GraphicsDevice
├─ platform_ IPlatform* — the process-current platform (the Game installs its own)
├─ platformWindow_ unique_ptr<IPlatformWindow>: created (owns) or adopted (borrows)
├─ ownsWindow_ policy flag: may a fallback destroy and rebuild this window?
├─ surfacePresenter_ unique_ptr<IPlatformSurfacePresenter> (Software on a TTY only)
├─ renderer_ unique_ptr<IGraphicsRenderer> — the native implementation
│ └─ ITextureRenderer / IVertexBufferRenderer / IRenderTargetRenderer … per resource
├─ activeDescriptor_ borrowed pointer into the static registry table
├─ rendererCapabilityProfile_ lazily built, invalidated on renderer rebuild
├─ PresentationParameters, Viewport, Blend/DepthStencil/Rasterizer state, scissor
├─ current effect, vertex/index bindings, Texture/SamplerState collections
├─ currentRenderTargets_, renderTargetBound_, boundRenderTargetDestroyed_
├─ resources_ vector<GraphicsResource*> — tracking for disposal, not allocation
└─ resourceDeviceLifetime_ shared_ptr<void>; resources hold weak_ptr copies
The manager is a runtime service: GraphicsDeviceManager.cpp points at Game::getGraphicsDeviceProperty() with ownsGraphicsDevice_ = false and never deletes the Game's device (its CreateDevice() refuses to run without a Game-owned device). The device reads its platform from CNA::Platform::GetCurrentPlatform() in the constructor; a Game installs its own platform as the current one, and a bare device triggers the lazy default. The platform outlives both the window and the device. The window wrapper may own the native window (IPlatform::CreateWindow) or merely adopt a caller-supplied DeviceWindowHandle (AdoptWindowHandle); the wrapper enforces native-handle ownership, and ownsWindow_ survives only as the fallback policy “may CNA replace this window?”. surfacePresenter_ is declared before renderer_, so reverse member destruction keeps presentation alive through the renderer's destructor. See Ownership and shutdown for the enclosing Game tree.
The renderer contract lives in CNA/Internal/Renderers/Common, mainly IGraphicsRenderer.hpp. Public device code does not bind Vulkan or Direct3D objects; it passes neutral values such as GpuDrawParams, GpuVertexStreamBinding, RenderTargetBindingDescriptor, RendererSurfaceInfo and GraphicsRendererCreateArgs, and each family implements the operation. Capability hooks are part of the contract because not every family supports every operation; a public call must refuse unsupported behaviour honestly rather than silently downgrade it.
Construction: the native boundary moves
The constructor in GraphicsDevice.cpp takes an adapter, a GraphicsProfile and PresentationParameters (the default constructor uses the default adapter, Reach and default parameters). In order:
- Member initialisation: platform pointer, collections bound to
this, defaultBlendState::Opaque,DepthStencilState::Default,RasterizerState::CullCounterClockwiseas C++ values, virtual resolution from the back-buffer size, and the lifetime token (declared before the collections so they never observe it unconstructed). - Capture the caller's current GL binding (if the platform has a GL context service), so it can be restored on both success and failure.
- Publish the virtual size to
TouchPanel. resolveRenderer(): device-type flags, attempt order, and per candidate the video-subsystem reference, window creation or adoption,applyPresentationParametersToWindow(), andcreateRenderer(); the selection latches when a renderer exists. The loop is traced in Renderer selection internals.createRenderer()invalidates the capability profile, fillsGraphicsRendererCreateArgs— the surface snapshot (window id, native handle, drawable size, display scale), only the narrow services the descriptor asks for (surface presenter, GL context, Vulkan surface), virtual size, context-recovery flag, MSAA count, swap interval, back-buffer and depth formats, full-screen flag, profile, and adeviceEventCallback— and callsdescriptor.create(args). It then sets the virtual resolution, normalises the applied presentation formats, writes back the renderer's applied MSAA count, and logs the active renderer name once (with “selected at runtime from N compiled in” in a multi-renderer build).UpdateViewportFromWindow(), then push the three default states to the real renderer. The depth-stencil push is skipped whenSupportsDepthStencil()is false (2D-only families), so they can refuse every later assignment consistently. C++ field defaults alone would leave each native API at its own defaults.- Restore the caller's GL binding.
Any exception in steps 4–6 is caught: destroyNativeResources() (renderer, presenter, input handles, window), release the video reference through setVideoSubsystemAcquired(false), restore the caller's GL binding, rethrow. The video reference is a single boolean-owned count changed only by setVideoSubsystemAcquired(), which is what keeps every path — success, failure, fallback A→B→C, Dispose() — balanced by construction.
Public state is observable state
GraphicsDevice.hpp stores the current effect, vertex and index bindings, texture and sampler collections, blend, depth-stencil and rasterizer state, blend factor, multisample mask, reference stencil, viewport, scissor and render-target bindings. Draw paths consume these fields; they are not pass-through setters. DrawIndexedPrimitives requires an applied effect and bound buffers, extracts the effect's matrices, fills GpuDrawParams, folds vertex-stream offsets exactly once, validates profile, ranges, draw state and stream capability, pushes rasterizer and sampler state, and only then calls DrawIndexedPrimitivesEx. Whether the device also enforces buffer ranges is the renderer's answer (RequiresManagedBufferedDrawRangeValidationEXT()), not the device's assumption. The function-level trace is on Indexed draw trace.
SetRenderTargets is likewise more than a setter: it returns early for an unchanged set, enforces the four-binding ceiling and the profile's limit, rejects null, disposed and foreign-device targets, duplicate resources and mismatched dimensions, applied sample counts or pixel sizes, and calls the renderer before publishing the new bindings, then resets viewport and scissor to the first target and performs the DiscardContents clear. A failed renderer transition must not leave the device claiming a successful bind. Textures and render targets traces it step by step.
Present, viewport and the backbuffer
Present()throwsInvalidOperationException(“Cannot present while render targets are bound”) whilerenderTargetBound_is true — including after a bound target was destroyed, until the nextSetRenderTargets. It acquires a context lease (RestorePreviousBinding), callsrenderer_->Present(), thenUpdateViewportFromWindow().Present(sourceRectangle, destinationRectangle, overrideWindowHandle)with all three empty isPresent(). Otherwise rectangles must have positive extent and are clipped to the back buffer (source) or the window's pixel size (destination); a rectangle clipped to nothing is anArgumentException. The renderer'sPresentRegionEXT()decides; only the Headless and Software families implement it at this snapshot. Headless accepts all three arguments (it records them in its trace); Software accepts a source rectangle only (for its surface presenter) and refuses a destination rectangle or an override window. Every other family, and any refused request, gets aNotSupportedExceptionrather than a whole-frame present.UpdateViewportFromWindow()refreshes the renderer's surface snapshot (OnSurfaceChanged; a platform that refuses the size query is logged and the old surface kept), takes the logical size fromGetViewportSize()and the physical rectangle fromGetDefaultViewportRect(), and resetsViewportand scissor only when either changed since the last time this method ran, so a game-set split-screen viewport survives an ordinaryPresent(). The physical rectangle differs from the logical size only under Letterbox or Overscan; families with a real virtual-resolution transform overrideGetDefaultViewportRect()(EasyGL, OpenGL4, Vulkan, WebGPU, SDL_gpu, DirectX 11/12, FNA3D, Metal).Reset(parameters, adapter)raisesDeviceResetting, unbinds any render targets (XNA does the same), applies the new presentation parameters to the window and the virtual resolution to the renderer — restoring every public field and rethrowing if that resize fails — then callsUpdatePresentationFormatEXT,ApplyMultiSampleCount(writing back the clamped count),SetSwapInterval, updates the viewport and raisesDeviceReset. The renderer object is kept; only the CNAEXT hookRecreateRendererForMultiSampleCount()(used by tests and the C API) rebuilds it, with the pinned descriptor.
Capability answers
GraphicsDevice::SupportsCapability() answers the 19 GraphicsCapability members, but not all from the renderer's own switch. CompiledEffects, HalfFloatTextureLinearFiltering, ComputeShaders and IndirectDraw come from separate renderer virtuals whose default is false; FloatRenderTargets and HalfFloatRenderTargets are derived from whether Vector4 and HdrBlendable are accepted as render-target formats; MultipleRenderTargets is the renderer's answer ANDed with the profile's limit being greater than one. The reason is structural: several renderer switches end in default: return true, and an enumerator added later must not be claimed by a switch that has never heard of it. The device-level answer is the public one; a caller holding the renderer interface directly can see different values.
GetRendererCapabilityProfileEXT() builds a RendererCapabilityProfile (32 features, 22 limits, 27 per-format usage entries, an English report) on first use and caches it in rendererCapabilityProfile_; createRenderer(), destroyNativeResources(), a Reset that applies an MSAA count and RecreateRendererForMultiSampleCount() invalidate it. The public view of both APIs is Capability reporting and RendererCapabilityProfile.
Resource registry versus C++ ownership
A GraphicsResource constructed with a device appends its raw this to resources_, raises ResourceCreated, and stores a weak_ptr to resourceDeviceLifetime_. The registry lets device disposal call Dispose() on live resources while their renderer objects can still be released; it does not make the device the C++ allocator of textures, buffers or states. A copied value-style state shares one resource identity instead of producing a second create/destroy event, and a moved resource transfers its registry entry (and, for textures, bound texture and render-target references). Read the copy and move code in GraphicsResource.cpp before adding resource members or assuming a simple unique-ownership tree; Textures and render targets covers it in detail.
Dispose(bool disposing) runs in this order:
- Return if already disposed; otherwise set
isDisposed_first, so re-entrant disposal is a no-op and aDisposinghandler cannot issue work against objects being torn down. - Raise
Disposing, only for an explicit disposal (disposing == true). - Move
resources_into a local vector and clear it, then call each resource's publicDispose(); their re-entrantRemoveResourceReference()calls are harmless no-ops. destroyNativeResources(): invalidate the capability profile, reset the renderer, then the presenter, clear theTextInputEXTandMousewindow handles if they point at this window, then reset the window wrapper.- Release the video-subsystem reference (after the window, because the subsystem backs it).
The destructor calls Dispose() and then resets resourceDeviceLifetime_, which expires every resource's weak token. Inserting teardown after the renderer reset can turn an ordinary resource destructor into a use-after-free.
Device loss is not one uniform cross-renderer protocol. createRenderer() hands every family a deviceEventCallback that maps Lost, Resetting and Reset to GraphicsDeviceStatus and the public DeviceLost, DeviceResetting and DeviceReset events (plus ContentLost notification on Reset). At this snapshot six families call it: DirectX 9, DirectX 11, DirectX 12 and Direct2D report loss and reset; WebGPU reports them only from its debug simulation hooks (DebugSimulateContextLoss, DebugRestoreContext), while a real driver-reported loss is only logged (OnDeviceLost); Vulkan reports Lost once and then throws from the failing call, because a lost VkDevice is not recreated under live wrappers. The other families never call it, and the member comment in GraphicsDevice.hpp that says only Direct3D 9 does is out of date. Do not promise device-loss behaviour from the public events' existence alone.
Context and thread rules
GraphicsDeviceManager::BeginDraw() returns false when CanBeginDrawEXT() says the renderer cannot draw (a lost browser WebGL context), otherwise stores AcquireRendererThreadContextLeaseForFrame(); EndDraw() presents and releases the lease, also on exception, and a Disposing handler releases it if the game disposes the device from Draw(). The lease is an IRendererThreadContextLease; only the EasyGL and OpenGL4 families return one (to move a GL context between the game thread and background content loading); every other family returns null. The lease is not evidence that GraphicsDevice is generally thread-safe: public state, the resource registry and bindings are mutated without any lock in this class. Treat game-thread use as the supported default unless a specific renderer contract and call site prove otherwise. GPU completion is family-specific: Software finishes a draw inside the call, Vulkan records it and executes at submit (see the Vulkan path). The cross-subsystem picture is in Thread and callback map.
How to change this layer
- A public validation change: find the exact public method and every overload that funnels into it (the many
DrawUserPrimitivesoverloads,SetRenderTargetintoSetRenderTargets); read the neutral tests for the same call; compare at least two families, one that stages through the CPU and one that forwards to a native API, becauseRequiresManagedBufferedDrawRangeValidationEXT()splits them. - A new native capability: decide whether it belongs in the neutral
IGraphicsRenderercontract, an EXT hook with a false default, or one family only. If the neutral contract changes, enumerate all 21 families and check that each default means “unsupported”. If it is a newGraphicsCapabilitymember, derive it at device level, from a renderer virtual whose default is false or from a format query, as the six guarded members (CompiledEffects, both float render-target entries, half-float linear filtering, compute and indirect draw) are;MultipleRenderTargetsis the seventh special case, derived for a different reason (the profile limit). - Public headers or enum values: audit the C ABI and the external bindings separately (C API internals).
- Ownership or teardown: test explicit
Dispose(), theGamedestructor, a failed constructor, move construction and assignment, disposal duringDraw(), and a fallback that crosses window kinds.
Evidence and gaps
| Test source | Covers |
|---|---|
GraphicsDeviceValidationTests.cpp | Public argument and state rejection, including the four-target SetRenderTargets cap. |
RenderTargetSemanticsTests.cpp, RenderTargetBindingTests.cpp | Binding semantics and the RenderTargetBinding value type. |
GraphicsDeviceDisposalHookTests.cpp | Public Dispose reaches the protected hook, repeated disposal is inert, the device is already disposed when Disposing fires, Disposing only on explicit disposal, owned resources still disposed. |
GraphicsDeviceCapabilityTests.cpp, RendererCapabilityProfileTests.cpp | Device-level capability answers and the profile API. |
GraphicsDeviceRendererTests.cpp | Renderer identity reporting, including that the startup diagnostic never writes to stdout. |
| Selection, fallback and registry tests | Construction policy; see Renderer selection internals. |
These tests compile into CnaGraphicsTests and run against whichever renderer the configuration compiled in; many are guarded with CNA_RENDERER_IS(...) or CNA_SKIP_IF_RENDERER_IS_NONE_OF(...). In the last commits before this snapshot, OpenGL4 was added to those guards in 30 neutral test files, so an OPENGL4 build now takes the same branches as the other capable GL renderers. The tests are necessary but not sufficient for API-specific submission, native resource retirement or swapchain behaviour; those claims need the family's own tests on a host that has the API. Nothing listed here was executed for this page.
Curated source tour
GraphicsDeviceManager.cpp: how a Game's manager borrows the device, applies preferences throughReset, subscribes to device events and holds the frame lease.GraphicsDevice.hpp: the private members at the end of the class — ownership, lifetime token, binding state and the flags thatPresentandSetRenderTargetsshare.GraphicsDevice.cpp: constructor,resolveRenderer,createOrAttachWindow,createRenderer,DrawIndexedPrimitives,SetRenderTargets,Present,Reset,Dispose, in that order.IGraphicsRenderer.hpp:GraphicsRendererCreateArgs,RendererSurfaceInfo, the lease interface and the capability defaults.GraphicsResource.cpp: why the raw registry is safe only with the weak lifetime token and the transfer logic.modules/graphics/tests: map the neutral contract assertions before visiting one family's tests.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- 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.
- 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 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.
- 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.
- GameTime and the timestep: exact clock semantics — What CNA's game clock reports: GameTime protection, fixed-step catch-up with worked numbers, IsRunningSlowly hysteresis versus XNA 4.0, ResetElapsedTime, vsync versus timestep, and the browser clock.
- GameWindow, GraphicsDeviceManager and the supporting types — CNA's single GameWindow facade and its failure policy, GraphicsDeviceManager construction, CreateDevice, ApplyChanges and presentation preferences, and LaunchParameters, TitleContainer, TitleLocation and FrameworkDispatcher.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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 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.
- 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.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-012: Two tests still encode the wrong occlusion-query expectation for SDL_GPU after the renderer was made truthful — SDL_GPU now reports OcclusionQuery false and refuses queries, but the shared capability test still expects true and SdlGpu_OcclusionQuery_Limitation expects a refusal message the public constructor never produces.
- CNA-BUG-095: A GraphicsDevice constructor that fails after renderer resolution leaves the selection latched to the renderer it destroyed — The GraphicsDevice constructor latches the selection inside resolveRenderer(), then updates the viewport and pushes default states; if one of those throws, the catch block destroys the renderer without unlatching, so Set
- CNA-BUG-101: GraphicsDevice::Reset rolls back only the window and virtual-resolution stage; a later renderer failure leaves the new PresentationParameters stored and DeviceReset unraised — An exception from UpdatePresentationFormatEXT, ApplyMultiSampleCount or SetSwapInterval escapes after the new parameters are stored and render targets were unbound, and after DeviceResetting was raised, so the device rep
- CNA-BUG-118: GraphicsDevice::GetMaxTextureDimension() is documented as the renderer's real maximum, but most renderers return an unqueried 16384 that the capability profile marks as known — Only VULKAN, DIRECT2D and GDI report a real or bounded limit; the other families inherit a constant 16384 (no code in the repository queries GL_MAX_TEXTURE_SIZE), which the renderer capability profile nevertheless report
- 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
- CNA-VGAP-054: Destroying a caller-created GraphicsDevice while resources on it are still live is allowed by cna_graphics_device_destroy but untested — graphics_device.h says resources on a caller-created device are released with it and the destroy route checks no child count, but every C test destroys the resources first, so what later calls on those resource handles d
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- Internals
- Renderer selection internals · Indexed draw trace · Textures and render targets · Ownership and shutdown
- Maintainer workflow
- Fix a renderer bug · Debug shutdown and lifetime · Ownership and lifetime master map · Thread and callback map
- Tests and validation
- Test architecture
- Reference
- Public header index · Test target index