GraphicsDevice: the shared device contract

CNA snapshot 009d40f5  ·  Deep Dives › The graphics machine  ·  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. Checked by reading GraphicsDevice.cpp, its header, IGraphicsRenderer.hpp and the neutral graphics tests at 009d40f5; no test was executed. Per-family native behaviour is owned by the family pages.

GraphicsDevice is the one XNA-shaped object every renderer sits behind: it validates each public call, stores the public pipeline state, tracks resources and hands neutral values to the selected renderer family. This page states the exact device-level contract at this snapshot — construction and windows, what the state properties store and when they reach the renderer, viewport and scissor rules, the five Clear overloads, vertex and index bindings, the draw-call surface, back-buffer readback and the CNA extensions — for game programmers who need precise behaviour and for maintainers who change the shared layer.

From a public call to a renderer

Every graphics call a game makes passes through the same layers. The public device checks arguments and profile rules, records the state it now describes, and forwards neutral values to the IGraphicsRenderer contract; one of the 21 implementation families then talks to a native API, rasterises on the CPU, drives browser DOM elements, validates and traces (HEADLESS) or does nothing at all (STUB). Resources follow a parallel path: the public object is created and tracked by the device, while its renderer-side counterpart is produced by a family factory and owned by the public object.

 Game / GraphicsDeviceManager
          |
          v
 GraphicsDevice  (public XNA-shaped contract)
   validates, stores public state,          creates / tracks
   normalises bindings  ----------------------------------->  GraphicsResource
          |                                                    (Texture2D, VertexBuffer,
          |  neutral values: GpuDrawParams,                     RenderTarget2D, ...)
          |  RenderTargetBindingDescriptor, ...                        ^
          v                                                            | owned by
 IGraphicsRenderer  (virtual contract + capability hooks)              |
          |                                                            |
          v                                         factory            |
 selected family (1 of 21) -------------------------------->  renderer-owned object
          |                                                    (ITextureRenderer, ...)
          v
 native API | CPU rasteriser | browser DOM | validate + trace | no-op
Figure. The renderer-independent call and resource path. A call flows top to bottom from the game through GraphicsDevice, which validates and records state, into the IGraphicsRenderer contract and the one selected family, which reaches a native API, a CPU rasteriser, the browser DOM, a trace or nothing. On the right, the device creates and tracks each public GraphicsResource, and the family's factory produces the renderer-side object that the public resource owns. A public call that reached IGraphicsRenderer is weaker evidence than a family that visibly acted on it, which is weaker again than a correct pixel.

The layering has a practical consequence: public state can be valid and readable while a particular family ignores or approximates the native operation behind it. A property that round-trips proves only the shared layer; a family's behaviour is established by a test that engages that family and checks its output. The object ownership map of the device (window, platform, renderer, presenter, registry) is on GraphicsDevice internals.

Construction and the device window

Two constructors and the Game-owned device

The XNA-shaped constructor takes a GraphicsAdapter&, a GraphicsProfile and const PresentationParameters&. The CNAEXT default constructor delegates with GraphicsAdapter::getDefaultAdapterProperty(), GraphicsProfile::Reach and a default-constructed PresentationParameters, whose back buffer is 800 × 480, SurfaceFormat::Color, no depth buffer and RenderTargetUsage::DiscardContents (PresentationParameters.cpp). Copy and move are deleted. In a game the device is not built by the manager: Game holds Graphics::GraphicsDevice GraphicsDevice_ as a value member (Game.hpp), constructed before any derived game code runs, and GraphicsDeviceManager later reconfigures that same object through Reset — ordinary game code never constructs a second device. How the manager applies its preferences is on GraphicsDeviceManager: CreateDevice.

Because that member is default-constructed, a game's device starts under Reach; the manager copies its own GraphicsProfile preference into the existing device with SetGraphicsProfileEXT immediately before the settle-in reset. The profile is enforced on every renderer (multiple render targets, occlusion queries, 32-bit indices, float targets, large textures and back-buffer readback all need HiDef); the complete list is on Choosing a GraphicsProfile and the device-side details on the profile the device enforces.

What the constructor does, in order

The constructor in GraphicsDevice.cpp runs a fixed sequence, and the order is observable:

  1. Take the process-current platform (CNA::Platform::GetCurrentPlatform()) and remember the caller's current GL context binding, if the platform has a GL context service, so it can be restored on success and on failure.
  2. Publish the virtual back-buffer size to TouchPanel (touch coordinates are scaled against it).
  3. resolveRenderer(): for each candidate renderer, take a video-subsystem reference if the family needs one, create or adopt the window, apply the presentation parameters to it and create the renderer with GraphicsRendererCreateArgs; the renderer selection latches once a renderer exists. The loop and its fallback rules are on Renderer selection internals.
  4. UpdateViewportFromWindow(): take the renderer's logical size and physical default rectangle and install the full-surface viewport and scissor.
  5. Push the three default states to the real renderer: BlendState::Opaque, DepthStencilState::Default (skipped when SupportsDepthStencil() is false, so a 2D-only family can refuse every later depth assignment consistently) and RasterizerState::CullCounterClockwise. Without this push each native API would start from its own defaults.
  6. Restore the caller's GL binding.

Which window the device gets depends on the family descriptor and the parameters, not on the toolkit. A family whose descriptor sets no needsWindow (SOFTWARE, PORTABLEGL, HEADLESS, STUB) gets none, except that SOFTWARE on a platform whose only display route is a surface presenter (the terminal platform on a TTY) receives the terminal as its window. PresentationParameters.HeadlessEXT requests a windowless device from a family that normally wants one; only DIRECTX12 and SDL_GPU can honour it, and a family that cannot (a Direct3D 11 swap chain, an EasyGL context bound to a window) throws from its own constructor (PresentationParameters.hpp). A non-zero DeviceWindowHandle is adopted through the platform's AdoptWindowHandle: CNA borrows it, publishes it to Mouse and TextInputEXT exactly as it would an owned window, and never destroys or rebuilds it. Otherwise the platform creates a window from the back-buffer size — 1024 × 768 only when a dimension is not positive — titled with the default window title and not resizable, so the native window agrees with XNA's AllowUserResizing == false from the start.

Construction is transactional

Steps 3–5 run inside a try block. Any exception destroys what was built (destroyNativeResources(): renderer, presenter, the input handles that point at this window, the window wrapper), releases the video reference through setVideoSubsystemAcquired(false), restores the caller's GL binding and rethrows the original exception. The video reference is a single Boolean that only setVideoSubsystemAcquired changes, so success, failure, a fallback that tries several candidates and Dispose() all stay balanced by construction; a device that never raised the video subsystem never releases it. (Earlier revisions could leak the window or the video reference when a later construction step threw, and released the video subsystem unconditionally at teardown; the current code has neither path.)

One thing the rollback does not undo is the renderer-selection latch: resolveRenderer() latches the selection as soon as a renderer exists and the constructor's catch block never unlatches it. If UpdateViewportFromWindow() or one of the three default-state pushes throws afterwards, the renderer is destroyed but GraphicsRendererSelection::IsLatched() stays true, GetActive() names the destroyed renderer and SetPreferred/SetFallbackChain throw, so the process cannot retry with another renderer (known issue CNA-BUG-095).

Public state: what is stored, what is forwarded

The property surface is easiest to use when grouped by ownership and by when it reaches the renderer:

GroupMembersContract at this snapshot
Identity and lifecycleadapter, GraphicsProfile, GraphicsDeviceStatus, IsDisposed, PresentationParameters, six eventsAdapter and profile are selected state. The presentation parameters report what the renderer says it applied (formats normalised, MSAA clamped), not the raw request. Status changes only in the families that report device loss (see device status).
DisplayDisplayMode, Viewport, ScissorRectangleViewport and scissor are validated, forwarded, then stored; a getter returns the stored logical value, not a native query.
Shader slotsTextures, SamplerStates, VertexTextures, VertexSamplerStates16 pixel-stage slots; the vertex-stage collections expose 4 slots under HiDef and none under Reach. Sampler states are pushed to the renderer before every draw; textures reach a draw only through the routes described below.
Pipeline stateBlendState, DepthStencilState, RasterizerState, BlendFactor, MultiSampleMask, ReferenceStencilAssigning a state object binds it: from then on it is immutable, and the device shares its payload rather than copying a snapshot. Every setter commits the public value only after the renderer accepted it.
Bindingsindex buffer, vertex-buffer list, render-target listOne coherent vertex-binding state behind the singular and plural setters; render targets through one normalised transaction.

State objects are bound, not copied

setBlendStateProperty, setDepthStencilStateProperty and setRasterizerStateProperty follow one shape: assigning the object that is already active is a no-op (the device compares the shared payload, as XNA compares the managed reference); otherwise the value is validated against the profile (Reach refuses separate alpha blending and SourceAlphaSaturation as a destination factor; every profile refuses Min/Max blend functions with factors other than One/One), bound for use — after which its setters throw InvalidOperationException — translated into normalised ordinals for the renderer, and only then stored. The standalone BlendFactor, MultiSampleMask and ReferenceStencil properties re-apply the current blend or depth-stencil state with one value replaced and mark it dirty, so reassigning the same object afterwards is not skipped. The consequence for porting: “mutate the state object, then reassign it” does not work in CNA any more than in XNA; build a new object (the preset objects are pre-bound). The state types themselves are covered on Graphics state; the device-side tests are in GraphicsDeviceDefaultStateTests.cpp.

Texture slots are not a general binding mechanism

TextureCollection (setter spelled textures(slot, texture), since C++ cannot overload operator[] assignment the way C# does) enforces real invariants: the slot range, a disposed texture (ObjectDisposedException), a texture currently bound as a render target (InvalidOperationException) and, for the vertex collections, the profile's slot count — Reach has no vertex-texture samplers at all. But what a draw samples depends on the effect. A stock effect (BasicEffect, AlphaTestEffect, DualTextureEffect, …) carries its own texture into GpuDrawParams through Effect::FillGpuDrawParams; a ShaderEffect uses its own texture setters; only a compiled XNA effect, where the renderer supports one, reads the device's texture and sampler collections, as XNA's own effect framework does — its passes also publish the textures and samplers they assign back into those collections (Effect.cpp). Assigning Textures[0] and drawing with BasicEffect therefore samples the effect's texture, not slot 0. Sampler state is different: applySamplerStatesToRenderer() pushes the filter, address, anisotropy and mip state of every slot before each draw.

Viewport and scissor rectangle

Both setters validate first and throw ArgumentException without changing state when the rectangle does not fit the active render surface — the bound render target's size, or the live logical back-buffer size — or, for the viewport, when MinDepth/MaxDepth are outside [0, 1] or out of order. A valid value is then mapped from the public, logical rectangle to the drawable rectangle (MapLogicalRectToPresentation; the identity unless a presentation mode letterboxes or overscans the back buffer, and never while a render target is bound), handed to the renderer, and stored only after the renderer returned, so a rejected native update never leaves the getter describing something that was not installed. The getter returns the logical value the game assigned. Scissoring itself is enabled by RasterizerState::ScissorTestEnable, a separate state; a test of scissor behaviour therefore has to draw across the rectangle's edge, because a property round trip only proves the stored value.

Three lifecycle rules decide when CNA replaces a game's rectangles:

  • Construction and real size changes. UpdateViewportFromWindow() installs a full-surface viewport and scissor, but only when the logical size or the physical default rectangle differs from the last one it produced itself. It compares against its own record, not against the current viewport, precisely so that a game-set split-screen viewport is not mistaken for a resize.
  • Present() preserves custom state. It calls UpdateViewportFromWindow() afterwards, which returns early when nothing changed.
  • Render-target transitions reset both to the first bound target's size, or to the back buffer's size when unbinding — but an unchanged binding set returns before any work, so rebinding the same target keeps the game's rectangles (see SetRenderTargets).

A family can use a physical default viewport that differs from the logical public dimensions (letterbox or overscan bars); the families that implement a real virtual-resolution transform override GetDefaultViewportRect(). The user-level description is on Viewport and ScissorRectangle; the validation tests are ViewportRejectsInvalidValuesWithoutChangingState, ScissorRejectsInvalidValuesWithoutChangingState and ViewportAndScissorUseActiveRenderTargetBounds in GraphicsDeviceValidationTests.cpp.

Clear and ClearOptions

The five overloads deliberately have different scopes:

CallWhat it clears
Clear(Color)XNA's default clear options: the colour target plus whichever depth and stencil planes the active target (or back buffer) really has, depth to 1.0f — not the viewport's MaxDepth — and stencil to 0.
Clear(ClearOptions, Color, float depth, int stencil)Exactly the requested aspects; the central overload the others funnel into.
Clear(ClearOptions, Vector4, float, int)As above; the colour is quantised through Color first, as Microsoft XNA does (FNA forwards raw floats).
Clear(float r, float g, float b, float a) (CNAEXT)A direct colour clear through the renderer's colour hook; no aspect routing and no depth or stencil.
Clear(Color, float depth) (CNAEXT)ClearOptions::Target | ClearOptions::DepthBuffer with the given depth.

Asking for a plane that does not exist throws

ClearOptions combines Target, DepthBuffer and Stencil (unknown bits are ignored). The central overload asks independently whether the active target has a real depth plane and a real stencil plane — the first bound 2D target or cube face through its renderer object's HasRealDepthBuffer/HasRealStencilBuffer, the back buffer through the renderer's back-buffer equivalents — because Depth16, Depth24 and Depth24Stencil8 differ and some families keep a standalone stencil plane. A request for depth or stencil that the target lacks throws InvalidOperationException (“the device does not have an active depth or stencil buffer”) before anything is cleared, as Microsoft XNA does; it is not silently narrowed to the remaining aspects. The requested depth is normalised the way Direct3D's clear path treats it (NaN becomes 0, other values are clamped to [0, 1]). The request is then dispatched to exactly one of seven renderer methods (ClearColorDepthAndStencil, ClearColorAndDepth, ClearColorAndStencil, ClearDepthAndStencil, Clear, ClearDepth, ClearStencil).

⚠

Code written against an older description — “a depth clear on a target without depth is a no-op” — now throws. Clear(Color, 1.0f) on a RenderTarget2D(device, w, h) (whose depth format is DepthFormat::None) is the common case; use Clear(Color), which asks only for the planes that exist. A Game's back buffer does have depth: the manager's default depth preference is Depth24.

The contract is pinned by BackBufferDepthStencilContractTests.cpp (ExplicitMissingDepthOrStencilThrowsAtomically, DepthOnlySurfaceAllowsDepthButRejectsStencilAtomically, SingleArgumentClearUsesOneInsteadOfViewportMaxDepth, ExplicitDepthClearSaturatesLikeMicrosoftXna, UnknownClearOptionBitsAreIgnored), which run on builds of SOFTWARE, OPENGL33, OPENGL4, OPENGLES3, DIRECTX11 and DIRECTX12 and skip elsewhere. How each family orders colour, depth and stencil natively is renderer-specific; the Vulkan route is on Vulkan draw-time state and ordered clears.

Present

Present() throws InvalidOperationException (“Cannot present while render targets are bound”) while a render target is bound — including after a bound target was destroyed, until the game calls SetRenderTargets again — then presents through the renderer under a context lease and calls UpdateViewportFromWindow(), which refreshes viewport and scissor only if the destination geometry changed. XNA's Present(sourceRectangle, destinationRectangle, overrideWindowHandle) also exists: with all three empty it is Present(); otherwise rectangles must have a positive extent and are clipped to the back buffer (source) or the window's pixel size (destination), and a family that cannot honour the request throws NotSupportedException rather than presenting the whole frame. At this snapshot only HEADLESS (all three arguments, recorded in its trace) and SOFTWARE (a source rectangle, for its surface presenter) implement the region hook; the tests are in GraphicsDevicePresentRegionTests.cpp. Inside a Game, do not call Present yourself: Game::EndDraw presents through the manager. Presentation parameters, formats and the swap interval are applied by Reset (see Reset order); the per-family present internals are on GraphicsDevice internals: Present.

Vertex and index bindings

The singular and plural vertex APIs describe one binding list. SetVertexBuffer(buffer, vertexOffset) rejects a negative offset, a disposed buffer and a buffer created by another device, then replaces the whole list with one binding in slot 0; SetVertexBuffer(nullptr) clears the list. SetVertexBuffers(vector) accepts at most 16 bindings (more throws NotSupportedException) and, following Microsoft XNA rather than FNA, refuses a null entry with ArgumentException; it applies the bindings in order and, if one fails validation (null, disposed, foreign device), keeps exactly the prefix already processed before rethrowing. An empty list clears the binding. GetVertexBuffer() returns slot 0 of the list and GetVertexBuffers() returns a copy of it, so the two always agree. GetRenderTargets() likewise returns a new copy of the render-target list on every call; there is no allocation-free variant. For the index buffer, the Indices property, SetIndexBuffer/GetIndexBuffer and the method-call spelling Indices(...) are three names for one pointer — surface redundancy, not independent state. The tests are SetVertexBuffers_DefaultNullBindingThrowsAndClearsState, SetVertexBuffers_SeventeenBindingsThrowNotSupported, SetVertexBuffers_FailureKeepsOnlyProcessedPrefix, SetVertexBuffers_EmptyClearsSingularBinding and ForeignBuffersAreRejectedTransactionally.

Draw calls

The buffer-backed calls are DrawPrimitives, DrawIndexedPrimitives and DrawInstancedPrimitives, plus the CNAEXT DrawInstancedPrimitivesBaseInstanceEXT, DrawPrimitivesIndirectEXT and DrawIndexedPrimitivesIndirectEXT. Before any native submission the shared layer checks, in the precedence recovered from Microsoft XNA, the numeric arguments, the profile's primitive ceiling (65,535 primitives per call under Reach, 1,048,575 under HiDef) and the topology-derived element count (computed in 64 bits, so an overflowing request is refused rather than wrapped); then an applied effect (missing: InvalidOperationException) and bound, undisposed buffers; then buffer ranges where the family asks for them, multi-stream capability and the draw-time profile rules; and finally pushes rasteriser and sampler state. The function-level trace with its offset arithmetic is on Indexed draw trace; the user-level tour is 3D rendering: binding and draw calls and Tutorial 38: Vertex Buffers.

User primitives: overload sets instead of generics

XNA's generic DrawUserPrimitives<T> and DrawUserIndexedPrimitives<T> become overload sets in GraphicsDevice.hpp:

  • typed pointer overloads for the four built-in vertex types VertexPositionColor, VertexPositionTexture, VertexPositionColorTexture and VertexPositionNormalTexture, with and without an explicit VertexDeclaration, and for indexed draws with 16-bit (std::uint16_t) or 32-bit (std::uint32_t) indices;
  • const void* overloads for caller-packed bytes, which need a VertexDeclaration — with one exception: the declaration-less DrawUserPrimitives(type, const void*, offset, count) reads its array as VertexPositionColor objects;
  • std::vector<TVertex> templates (indices as std::vector<std::int16_t> or std::vector<std::int32_t>), which check the vector's size against the offset and primitive count before drawing. Without a declaration TVertex must derive from IVertexType (a static_assert); a non-polymorphic type with a declaration is drawn from its raw bytes; a polymorphic type other than the four built-ins throws NotSupportedException, because C++ has no reflection to derive a GPU stream from a declaration and the object carries a vtable pointer.

That last point is the rule behind all of them: CNA's public vertex structures are polymorphic C++ objects, so their bytes are never a GPU stream. Typed objects are packed into the asserted stream layout (CNA::Internal::Graphics::Pack) before upload; callers must not reinterpret a VertexPositionColor[] as bytes for a const void* overload. The packed stream goes into a temporary renderer vertex buffer created for that call and carrying the vertex type's canonical declaration, so a compiled effect can match its semantics. The typed overloads pack into device-owned scratch vectors whose capacity persists, so a call of the same or a smaller size reuses the allocation — an implementation warm-up cost, not a promise of allocation-free user draws. Under Reach, 32-bit user indices throw NotSupportedException. DrawUserPrimitivesArgumentGuardTest and PrimitiveVertsTest in DrawUserPrimitivesTests.cpp pin the argument guards; UserDrawsUseTheProfileCeilingAndReachRejectsWideIndices the profile rules.

Instanced stream transport

Every draw route describes its streams with one helper, FillVertexStreamBindings(), which copies up to 16 slot-aligned descriptions into GpuDrawParams: the renderer buffer, the declaration, its stride, the element offset, the instance frequency, the buffer's capacity in vertices and whether the vertex shader actually reads the stream. An instanced draw is simply a binding list in which some streams have a non-zero instance frequency. Validation applies the caller's baseVertex only to per-vertex streams and requires each per-instance stream that the shader reads to hold enough records from its own offset: with first instance i, n instances and frequency f, the stream needs 1 + ⌊(i + n − 1) / f⌋ records, i.e. 1 + ⌊(n − 1) / f⌋ for an ordinary draw. Unlike the ordinary routes, the instanced route folds no offset into baseVertex: each stream carries its whole VertexOffset, because the instanced renderers have a native per-binding offset. More than one stream of the same input rate needs MultiStreamVertexInput; families still differ in accepted layouts and native divisor support, so a two-stream call that compiles is not a portable arbitrary-effect claim. The range checks themselves are made only for families whose RequiresManagedBufferedDrawRangeValidationEXT() is true (the interface default) or for a bound buffer with an empty declaration; EasyGL, OPENGL4 and SOFTWARE return false and receive the ranges natively, so a too-short per-instance stream is not rejected by the device there. The tests are in InstancedDrawRangeTests.cpp and its neighbours; the user guide is Tutorial 60: Instancing.

World, view and projection come from the effect

Every shared draw starts world, view and projection at identity and then asks the applied effect for IEffectMatrices through a dynamic_cast (ExtractMatrices). The stock effects, ShaderEffect, PbrEffect and SkinnedPbrEffect implement it. A custom effect that deliberately omits the interface gets identity transforms rather than an exception — a legitimate way to draw vertices that are already in clip space. The CNAEXT static GraphicsDevice::PrimitiveVerts(type, count) converts a primitive count into the element count a draw consumes (list ×3 or ×2, strip +2 or +1, PointListEXT ×1), computed in 64 bits, throwing for an unknown topology or an overflow; it mirrors an internal FNA helper that CNA makes public.

Back-buffer readback

GetBackBufferData has three Color overloads (whole back buffer; with a start index; with an optional rectangle) and three templates for any trivially copyable element type. All funnel into one core that enforces, in order:

  • the HiDef profile — under Reach it throws NotSupportedException, as XNA does, so a default Game must request HiDef before it can take a screenshot this way;
  • a non-null destination, a non-negative startIndex, a positive elementCount and no int overflow of their sum;
  • no active render target (InvalidOperationException): the method reads the back buffer, never the bound target;
  • a rectangle with positive size inside the back buffer, whose size comes from PresentationParameters — never from the renderer's live viewport, which a presentation mode can widen;
  • an element size that divides the back-buffer format's pixel size, and an elementCount that covers exactly the requested bytes (not “at least”).

The renderer writes plain RGBA bytes; the Color overloads construct each Color object from them, because Color carries a vtable pointer and its first byte is not the red channel. The generic templates copy bytes and require a four-byte back-buffer format. C++ cannot see the destination array's length, so the caller must supply startIndex + elementCount elements. The rules are pinned by BackBufferReadbackIsHiDefOnly in GraphicsProfileResourceCeilingTests.cpp and the readback cases of the neutral suite. What a family returns still differs: a GPU family copies its native back buffer, a CPU family its framebuffer, VULKAN must first submit the pending frame, and HEADLESS and STUB, which own no pixels, throw after the shared validation instead of inventing a frame (HEADLESS used to fill the destination with the last clear colour) (see Vulkan readback). Pixel evidence therefore has to name the family and the coordinate space; the portable way to verify an off-screen result is RenderTarget2D::GetData after unbinding, as Tutorial 125: pixel testing shows.

CNA extensions on the device

The extension surface falls into four groups; everything here is marked CNAEXT in the header, which CNA_STRICT_XNA_API turns into a deprecation warning. A strict build is a useful filter, but it proves only that tagged declarations are extensions, not that every untagged one is XNA.

PurposeMembersBoundary
Resource instrumentationOnResourceCreated, OnResourceDestroyed, AddResourceReference, RemoveResourceReference, GetTrackedResourceCount, NotifyContentLostResourcesEXTPublic for framework plumbing and tests; the events fire at base-resource construction and disposal (see resource registration).
Renderer identity and capabilityGetGraphicsRendererType/Name, SupportsCapability, GetRendererCapabilityProfileEXT and its accessors, SupportsSurfaceFormatAsRenderTargetEXT, GetMaxTextureDimension, Get/SetUnsupported3DGraphicsCallBehavior, shader-dialect and compute limitsThe identity is this device's actual renderer, chosen at run time in a multi-renderer build. Capability answers are derived at device level for six of the 19 members and ANDed with the profile for multiple render targets; see capability answers and Tutorial 101. The texture limit is enforced by the texture and content paths.
State and effect plumbingSetDepthTestEnabled, SetBlendEnabled, SetDepthWriteEnabled, SetCurrentEffect, SetGraphicsProfileEXT, SetPresentationParametersInternal routes. The three toggles go straight to the renderer and bypass the public state objects (the getters do not change); SetGraphicsProfileEXT only stores the profile for the manager's first reset and re-validates nothing already created; SetPresentationParameters stores normalised parameters and forwards only the swap interval. None is a substitute for XNA state objects or a runtime profile switch.
Debug and recoverySetContextRecoveryEnabled, SetStringMarkerEXT, GetRenderer, RecreateRendererForMultiSampleCountFamily-specific or test-oriented; code that calls GetRenderer() is not portable across families.

Three of the debug members need care. SetContextRecoveryEnabled(false) records the flag and forwards it; with recovery off, textures drop their CPU shadow copies when they can, and re-enabling recovery later cannot rebuild shadows that were already freed (see the CPU shadow). SetStringMarkerEXT inserts a debugger-visible marker on VULKAN, OPENGL4, SDL_GPU, WEBGPU, METAL and FNA3D and is a no-op on most other families; on DIRECTX9 both this call and SetContextRecoveryEnabled throw a “not yet implemented” std::runtime_error (DirectX9Renderer.cpp). The header comment still says the marker is Vulkan-only. RecreateRendererForMultiSampleCount destroys the renderer and builds a new one with the pinned descriptor; it preserves no resources, no pushed state and no events, and its header rationale (“mid-game reset is not implemented”) is out of date now that Reset applies MSAA in place. Keep it for the tests and C API callers that create no resources first.

Worked examples

Two triangles through a vertex buffer

The smallest complete use of the buffer-backed path: a Game whose back buffer has the manager's default Depth24 depth plane clears colour and depth, applies a BasicEffect with vertex colours, and draws two VertexPositionColor triangles from two buffers. The nearer triangle occludes the farther one only because DepthStencilState::Default is active, and the colours interpolate across the face. Cull mode is set to CullNone so the example does not depend on winding.

#include "Microsoft/Xna/Framework/Game.hpp"
#include "Microsoft/Xna/Framework/GraphicsDeviceManager.hpp"
#include "Microsoft/Xna/Framework/Graphics/BasicEffect.hpp"
#include "Microsoft/Xna/Framework/Graphics/GraphicsDevice.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexBuffer.hpp"
#include "Microsoft/Xna/Framework/Graphics/VertexPositionColor.hpp"

using namespace Microsoft::Xna::Framework;
using namespace Microsoft::Xna::Framework::Graphics;

class TwoTriangles : public Game
{
    GraphicsDeviceManager graphics_{this};

protected:
    void Draw(const GameTime& gameTime) override
    {
        auto& dev = getGraphicsDeviceProperty();
        dev.setRasterizerStateProperty(RasterizerState::CullNone);
        dev.Clear(Color(30, 30, 60, 255));      // colour + the depth plane the back buffer has

        BasicEffect fx(dev);
        fx.setVertexColorEnabledProperty(true);
        fx.getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();

        const VertexPositionColor farTri[3] = {
            {Vector3(-1.0f, -0.2f, 0.8f), Color(40, 60, 160, 255)},
            {Vector3( 0.0f,  0.9f, 0.8f), Color(90, 120, 220, 255)},
            {Vector3( 1.0f, -0.2f, 0.8f), Color(40, 60, 160, 255)}};
        const VertexPositionColor nearTri[3] = {
            {Vector3( 0.0f,  0.6f, 0.3f), Color::Red},
            {Vector3(-0.7f, -0.6f, 0.3f), Color(0, 220, 90, 255)},
            {Vector3( 0.7f, -0.6f, 0.3f), Color(240, 200, 40, 255)}};

        VertexBuffer background(dev, VertexPositionColor::getVertexDeclarationStatic(), 3,
                                BufferUsage::WriteOnly);
        background.SetData(farTri, 3);
        dev.SetVertexBuffer(&background);
        dev.DrawPrimitives(PrimitiveType::TriangleList, 0, 1);

        VertexBuffer foreground(dev, VertexPositionColor::getVertexDeclarationStatic(), 3,
                                BufferUsage::WriteOnly);
        foreground.SetData(nearTri, 3);
        dev.SetVertexBuffer(&foreground);
        dev.DrawPrimitives(PrimitiveType::TriangleList, 0, 1);
        dev.SetVertexBuffer(nullptr);           // unbind before the buffers are destroyed

        Game::Draw(gameTime);
    }
};

Buffers are created per frame only to keep the example short; a real game creates them in LoadContent. With identity matrices the vertex positions are already clip-space coordinates, which is also why BasicEffect's default world, view and projection suffice here. On a 2D-only renderer the draw throws (or warns and does nothing under WarnAndStub).

Render to a texture, read it back, draw it as a sprite

The producer–consumer sequence every renderer with render targets implements: bind a target, clear and draw into it, unbind, read the result with RenderTarget2D::GetData — the portable readback, since GetBackBufferData refuses while a target is bound — and then sample the target as an ordinary texture through SpriteBatch. The target has no depth plane, so it is cleared with Clear(Color).

RenderTarget2D offscreen(dev, 128, 128);            // Color, DepthFormat::None, DiscardContents
dev.SetRenderTarget(&offscreen);                    // viewport and scissor become 128 x 128
dev.Clear(Color(60, 20, 80, 255));                  // Clear(Color, 1.0f) would throw: no depth plane
fx.getCurrentTechniqueProperty()->getPassesProperty()[0]->Apply();
DrawTriangle(dev, offscreenTriangle);               // application helper: SetVertexBuffer + DrawPrimitives

dev.SetRenderTarget(nullptr);                       // back to the back buffer; Present() is legal again
Color centre;
const Rectangle centreRegion(64, 64, 1, 1);
offscreen.GetData(0, &centreRegion, &centre, 0, 1);  // reads the target, not the back buffer

SpriteBatch batch(dev);
batch.Begin();
batch.Draw(offscreen, Rectangle(146, 20, 90, 90), Color::White);
batch.End();

This is an excerpt, not a complete program: dev, fx, DrawTriangle and offscreenTriangle come from the surrounding game. The same producer–readback–sampler sequence is what the registered Software_RenderTargetReadback test (software_rendertarget_readback_test.cpp) checks with sentinel-filled destinations for the CPU renderer, including sampling through SpriteBatch and through a textured primitive. That test exists because an earlier SOFTWARE build failed the consumer half in two independent ways, both named in the test's header: the target's renderer overrode no GetData, so RenderTarget2D::GetData returned a fabricated, fully written transparent-black frame (a “did GetData write anything?” check passes on it), and the sampler cast only to the texture renderer, so a target sampled as a null texture and drew a white inset — a reminder that a producer-side check and a consumer-side pixel check answer different questions. Two binding rules show up in such code. A DiscardContents target is cleared to opaque black, plus whichever depth and stencil planes it really has, on every real bind; XNA itself promises only that discarded content is undefined, so portable code should not rely on reading black. And for multiple render targets the public call accepting up to four RenderTargetBindings does not show that one draw writes all of them: stock effects write attachment 0 only. Treat SupportsCapability(GraphicsCapability::MultipleRenderTargets) as the first question, then require a successful bind and a shader plus pixel check that distinguishes every output. Render-target formats, usage and the per-renderer MRT list are covered on Render targets: MRT and Tutorial 23.

Evidence and limits

Everything above was checked by reading GraphicsDevice.cpp, its header, IGraphicsRenderer.hpp, Effect.cpp, PresentationParameters.cpp and the test files named in each section at 009d40f5; nothing was built or executed for this page. The neutral tests compile into CnaGraphicsTests and run against whichever renderer the configuration compiled in, and many are gated to a subset of renderers, so a registered test is evidence for the renderers it runs on, not for all 25. The examples were checked with g++ -std=c++23 -fsyntax-only -DCNA_RENDERER_VULKAN (the headers need one renderer define) against the TARGET public headers and a sibling sharp-runtime checkout, which TARGET does not pin; the render-to-texture excerpt was checked inside a wrapper function that declares the surrounding names. Neither was built or run. Per-family behaviour — what a readback returns, how a clear is ordered natively, which families report device loss — is owned by each family's own tests and pages.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Maintainer workflow
Change public XNA behaviour
Tests and validation
Test architecture