GraphicsDevice internals

CNA snapshot 009d40f5  ·  Development › Graphics internals  ·  source links pinned to 009d40f5

✓

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:

  1. Member initialisation: platform pointer, collections bound to this, default BlendState::Opaque, DepthStencilState::Default, RasterizerState::CullCounterClockwise as C++ values, virtual resolution from the back-buffer size, and the lifetime token (declared before the collections so they never observe it unconstructed).
  2. 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.
  3. Publish the virtual size to TouchPanel.
  4. resolveRenderer(): device-type flags, attempt order, and per candidate the video-subsystem reference, window creation or adoption, applyPresentationParametersToWindow(), and createRenderer(); the selection latches when a renderer exists. The loop is traced in Renderer selection internals.
  5. createRenderer() invalidates the capability profile, fills GraphicsRendererCreateArgs — 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 a deviceEventCallback — and calls descriptor.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).
  6. UpdateViewportFromWindow(), then push the three default states to the real renderer. The depth-stencil push is skipped when SupportsDepthStencil() 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.
  7. 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() throws InvalidOperationException (“Cannot present while render targets are bound”) while renderTargetBound_ is true — including after a bound target was destroyed, until the next SetRenderTargets. It acquires a context lease (RestorePreviousBinding), calls renderer_->Present(), then UpdateViewportFromWindow().
  • Present(sourceRectangle, destinationRectangle, overrideWindowHandle) with all three empty is Present(). 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 an ArgumentException. The renderer's PresentRegionEXT() 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 a NotSupportedException rather 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 from GetViewportSize() and the physical rectangle from GetDefaultViewportRect(), and resets Viewport and scissor only when either changed since the last time this method ran, so a game-set split-screen viewport survives an ordinary Present(). The physical rectangle differs from the logical size only under Letterbox or Overscan; families with a real virtual-resolution transform override GetDefaultViewportRect() (EasyGL, OpenGL4, Vulkan, WebGPU, SDL_gpu, DirectX 11/12, FNA3D, Metal).
  • Reset(parameters, adapter) raises DeviceResetting, 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 calls UpdatePresentationFormatEXT, ApplyMultiSampleCount (writing back the clamped count), SetSwapInterval, updates the viewport and raises DeviceReset. The renderer object is kept; only the CNAEXT hook RecreateRendererForMultiSampleCount() (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:

  1. Return if already disposed; otherwise set isDisposed_ first, so re-entrant disposal is a no-op and a Disposing handler cannot issue work against objects being torn down.
  2. Raise Disposing, only for an explicit disposal (disposing == true).
  3. Move resources_ into a local vector and clear it, then call each resource's public Dispose(); their re-entrant RemoveResourceReference() calls are harmless no-ops.
  4. destroyNativeResources(): invalidate the capability profile, reset the renderer, then the presenter, clear the TextInputEXT and Mouse window handles if they point at this window, then reset the window wrapper.
  5. 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 DrawUserPrimitives overloads, SetRenderTarget into SetRenderTargets); 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, because RequiresManagedBufferedDrawRangeValidationEXT() splits them.
  • A new native capability: decide whether it belongs in the neutral IGraphicsRenderer contract, 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 new GraphicsCapability member, 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; MultipleRenderTargets is 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(), the Game destructor, a failed constructor, move construction and assignment, disposal during Draw(), and a fallback that crosses window kinds.

Evidence and gaps

Test sourceCovers
GraphicsDeviceValidationTests.cppPublic argument and state rejection, including the four-target SetRenderTargets cap.
RenderTargetSemanticsTests.cpp, RenderTargetBindingTests.cppBinding semantics and the RenderTargetBinding value type.
GraphicsDeviceDisposalHookTests.cppPublic 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.cppDevice-level capability answers and the profile API.
GraphicsDeviceRendererTests.cppRenderer identity reporting, including that the startup diagnostic never writes to stdout.
Selection, fallback and registry testsConstruction 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

  1. GraphicsDeviceManager.cpp: how a Game's manager borrows the device, applies preferences through Reset, subscribes to device events and holds the frame lease.
  2. GraphicsDevice.hpp: the private members at the end of the class — ownership, lifetime token, binding state and the flags that Present and SetRenderTargets share.
  3. GraphicsDevice.cpp: constructor, resolveRenderer, createOrAttachWindow, createRenderer, DrawIndexedPrimitives, SetRenderTargets, Present, Reset, Dispose, in that order.
  4. IGraphicsRenderer.hpp: GraphicsRendererCreateArgs, RendererSurfaceInfo, the lease interface and the capability defaults.
  5. GraphicsResource.cpp: why the raw registry is safe only with the weak lifetime token and the transfer logic.
  6. modules/graphics/tests: map the neutral contract assertions before visiting one family's tests.

The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.

Tests and validation
Test architecture