Skip to content
The CNA BibleCNA 0.1.0-alpha.1 Edition

Chapter 12 GraphicsDevice and the Rendering Pipeline

GraphicsDevice is CNA’s public rendering hub. It owns the selected renderer and window when applicable, stores public pipeline state, tracks resources for ordered teardown, normalizes render-target and vertex bindings, and forwards draws. That sequence matters: public state can be valid even when a family ignores the corresponding native operation. Figure 12.1 separates those layers.

A game or GraphicsDeviceManager calls the public GraphicsDevice API. GraphicsDevice validates and stores public state, forwards operations through IGraphicsRenderer, and receives renderer-owned resource implementations from factories. The selected implementation family then calls a native API, CPU rasterizer, DOM, or trace route.
Figure 12.1: The renderer-independent call and resource path. A public call reaching IGraphicsRenderer is still weaker evidence than family engagement or a correct native result.

This chapter owns the shared device contract. Resource classes are detailed in Chapters 1418; renderer defaults and divergences are in Chapter 19.

12.1 Construction and native lifetime

The XNA-shaped constructor accepts a GraphicsAdapter, GraphicsProfile, and PresentationParameters. The CNAEXT default constructor delegates with the default adapter, Reach, and default presentation parameters. It is windowless only when the compiled family is inherently windowless (including HEADLESS, SOFTWARE, STUB, and PORTABLEGL). D3D12’s runtime HeadlessEXT path instead requires explicit presentation parameters. A default constructor on a normal windowed family creates an SDL window, usually 1024×768 when no size was requested.

Game owns a device value member before a derived game’s GraphicsDeviceManager exists. During initialization the manager reconfigures that same device through Reset; ordinary game code does not construct a second device.

12.1.1 Window, renderer, and presentation setup

The private construction path is observable through its ordering:

  1. 1.

    Initialize SDL video unless the compiled or requested path is windowless.

  2. 2.

    Create a window, attach a caller-owned DeviceWindowHandle, or retain null. Both CNA-owned and attached windows are published to mouse and text-input services.

  3. 3.

    Apply SDL window size/fullscreen state.

  4. 4.

    Build GraphicsRendererCreateArgs and call the selected family factory.

  5. 5.

    Normalize reported backbuffer/depth formats, query the renderer’s logical and physical viewport, and apply initial blend, depth/stencil, and rasterizer state.

An attached window is borrowed: CNA clears matching input handles at teardown but destroys the window only when ownsWindow_ is true. Chapter 19, §19.1.3, covers the remaining native-handle rules.

Limitation.  Construction is not fully transactional. After SDL video initialization, a later window, renderer, or initial-state exception can bypass GraphicsDevice’s destructor and leak the raw owned window, published input handle, or video-subsystem lease. Teardown also calls SDL_QuitSubSystem(SDL_INIT_VIDEO) even for a device that skipped initialization, which can release another client’s reference. The located tests establish windowless operation, not mixed-client reference accounting or injected construction failure.

12.2 Public state

The property surface is easier to use when grouped by ownership and forwarding:

Group Properties Contract
Identity/lifecycle adapter, profile, device status, disposed state, presentation parameters Adapter/profile are selected state; only D3D9 currently drives the lost/not-reset status callback.
Display display mode, viewport, scissor rectangle Viewport/scissor are stored and forwarded; a getter is not a native-state query.
Shader slots pixel/vertex textures and sampler-state collections Collections validate bookkeeping; draws bind textures through effects, while sampler state is forwarded before draws.
Pipeline state blend, depth/stencil, rasterizer, blend factor, multisample mask, reference stencil State objects are copied into the device, not frozen references. Reassign after mutation.
Bindings index buffer, vertex-buffer list, render-target list Singular and plural vertex routes now share one coherent binding state.

Assigning Textures[0] alone does not make a draw sample that texture. A stock effect populates GpuDrawParams; ShaderEffect uses its own texture setters. The device’s texture collections enforce slot, disposal, and active-target invariants, but they are not a general shader binding mechanism.

12.2.1 Viewport and scissor

Both setters first ask the renderer to apply the value and then retain public state according to the individual setter’s success path. Scissor enable is a separate RasterizerState::ScissorTestEnable value. A useful test therefore draws across the boundary; property round trips alone are insufficient.

Three lifecycle rules define when CNA replaces custom rectangles:

  • Construction initializes the full logical viewport. Current resize handling also sets the scissor to full bounds after an actual logical or physical-viewport change.

  • Present() preserves custom state while the renderer’s logical size and physical default rectangle remain unchanged.

  • Every 2D, cube, MRT, or backbuffer target switch resets both to the first destination’s full bounds, even for a redundant binding.

Renderers may use a physical default viewport distinct from the logical public dimensions for letterbox or overscan. Chapter 19, §19.4.3, records draw-time coverage and exceptions.

12.3 Clear, present, and reset

The four clear overloads have intentionally different scopes:

Call Shared request
Clear(Color) target, depth, and stencil; depth uses current Viewport.MaxDepth, stencil uses zero
Clear(options,color,depth,stencil) exact requested aspects after attachment masking
Clear(r,g,b,a) CNAEXT direct color-hook call; it does not pass through full aspect routing
Clear(Color,depth) CNAEXT target plus depth convenience form

FNA’s Vector4-color overload is absent. CNA also exposes only parameterless Present(); FNA’s source/destination-rectangle and override-window form has no CNA counterpart. Present refuses while a user target is bound, calls the renderer, then refreshes viewport/scissor only if the reported destination geometry changed.

12.3.1 ClearOptions and attachment masking

ClearOptions is the bitwise combination of Target, DepthBuffer, and Stencil. Shared code validates a requested depth value, independently asks whether the active 2D/cube target or backbuffer has depth and stencil, masks only absent aspects, and selects one of seven non-empty renderer methods. A depth/stencil-only clear on an attachment with neither becomes a no-op, matching current FNA.

This independent aspect query is important for Depth16/Depth24 versus Depth24Stencil8 and for families with a standalone stencil plane. Family-specific ordering and remaining defects—notably EasyGL’s color-hook/depth coupling and D3D9’s additional backbuffer-format gate—are in §19.4.2.

12.3.2 Reset order and rollback boundary

All four reset overloads end in Reset(const PresentationParameters&, GraphicsAdapter*). The order is public behavior:

  1. 1.

    Raise DeviceResetting; handlers still see the old adapter and parameters.

  2. 2.

    Clone the request, normalize the renderer’s applied color/depth formats, store the adapter and logical size, and update touch dimensions.

  3. 3.

    Apply window state and virtual resolution inside a rollback guard. A failure restores public presentation state, adapter, logical size, touch dimensions, and attempts to restore the former window state before rethrowing the original diagnostic.

  4. 4.

    Apply/clamp MSAA and write the applied count back; forward swap interval and the presentation-format update hook.

  5. 5.

    Refresh viewport/scissor and raise DeviceReset.

The rollback guard ends before MSAA, interval, and format hooks. A later exception can therefore leave earlier work applied and suppress DeviceReset. Successful completion establishes the shared route, not that a compositor accepted fullscreen or an interval request. Applied presentation values are stronger than the original request but still depend on each family’s truthful reporting; see §19.2.5 and §19.2.4.

FNA stores the new state before its resetting event and performs one FNA3D backbuffer reset, so event observations and native transaction shape differ.

12.3.3 GraphicsDeviceManager event and ownership model

A game-attached GraphicsDeviceManager registers the manager and device-service interfaces but does not own the game device. Initial CreateDevice() raises PreparingDeviceSettings, resets the existing device, subscribes to later device reset events, and then raises DeviceCreated. The initial settle-in reset is intentionally not forwarded as a manager reset pair. Subsequent ApplyChanges() reaches device reset; the manager forwards the device’s events once.

Edits made in PreparingDeviceSettings apply to that candidate only and are not copied back into preferred fields. The callback must retain a non-null adapter and must not re-enter ApplyChanges. Listener exceptions propagate and can interrupt the staged operation.

The manager subscribes to GameWindow::ClientSizeChanged with a lambda capturing raw this, discards the removal token, and removes only service registrations on disposal. Destroying or replacing a manager while its game window remains live can therefore leave a dangling resize callback. Normal member lifetime avoids a later resize during base destruction, but that ordering is not a general ownership guarantee.

Manager disposal raises DeviceDisposing even though it does not delete a game-owned device; Game uses that notification for UnloadContent. The disposed flag is set after the Disposed event, so a re-entrant or throwing listener remains another source-proven lifecycle hole.

12.4 Render targets

SetRenderTarget(RenderTarget2D*), the cube-face overload, and SetRenderTargets(vector) all normalize through the plural path. An empty list restores the backbuffer. GetRenderTargets() returns a new vector; CNA still lacks FNA’s no-allocation extension.

12.4.1 Binding contract

Each public binding becomes a descriptor naming a 2D target or cube face, dimensions, and the renderer-applied sample count. The shared route enforces:

  • at most four bindings, with D3D9’s Reach profile narrowed to one;

  • non-null, non-disposed targets with a usable private renderer;

  • valid cube faces and zero for unsupported 2D array slices;

  • equal dimensions and applied sample counts;

  • no duplicate subresource, while distinct faces of one cube remain legal.

Native binding must succeed before public bindings and viewport/scissor are committed. CNA has no redundant-binding early return: rebinding the same target reaches the renderer, resets rectangles, and clears a DiscardContents destination again. Preserve and Platform usage retain content; discard requests a black clear of color and each reported depth/stencil aspect. The API does not guarantee that discarded content later reads as black.

12.4.2 A render-to-texture example

The complete example is retained at tools/cna-screenshot-infra/rendertarget_roundtrip_demo.cpp. This excerpt shows the public sequence; it is adapted from that executable, with setup and triangle data omitted:

1 RenderTarget2D offscreen(dev, 128, 128);
2 dev.SetRenderTarget(&offscreen);
3 dev.Clear(Color(60, 20, 80, 255), 1.0f);
4 fx.Apply();
5 DrawTriangle(dev, offscreenTriangle);
6
7 const Rectangle centerRegion(64, 64, 1, 1);
8 Color centerPixel;
9 dev.GetBackBufferData(&centerRegion, &centerPixel, 0, 1); // SOFTWARE-specific
10
11 dev.SetRenderTarget(static_cast<RenderTarget2D*>(nullptr));
12 assert(dev.GetRenderTargets().empty());
13
14 SpriteBatch batch(dev);
15 batch.Begin();
16 batch.Draw(offscreen, Rectangle(146, 20, 90, 90), Color::White);
17 batch.End();

The readback probe is intentionally marked renderer-specific: some families read the active target, some always read the backbuffer, Headless synthesizes state, and others throw. Portable target verification uses RenderTarget2D::GetData where that family supports it.

Historical note.  The captured image in Figure 12.2 predates the pin. It exposed a Software defect: the sampler cast only to SoftwareTextureRenderer, so the sibling render-target renderer became a null texture and produced a white inset. At 1bb2145d, SoftwareColorSurface is the shared storage capability, and the registered Software_RenderTargetReadback test verifies exact target readback plus SpriteBatch and textured-primitive sampling. The historical image is retained because it demonstrates why a numeric producer check and a consumer pixel check answer different questions; it is not a current expected result.

Historical Software-renderer screenshot. A large blue field contains a white inset where the off-screen render target should have appeared, demonstrating that target population succeeded but later SpriteBatch sampling failed.
Figure 12.2: Historical pre-fix Software output: the target was populated, but its white inset showed that SpriteBatch could not sample it. The defect is fixed and covered at the edition pin.

12.4.3 Multiple render targets

The public call accepts up to four RenderTargetBinding entries. That shape does not establish that one draw writes all attachments. WebGPU rejects MRT; Software’s current route does not provide a portable multi-output stock-effect contract; diagnostic renderers may record only logical state. D3D12 can bind several RTVs while stock draws still target slot zero. Use MultipleRenderTargets as an initial query, then require a successful bind and a shader and pixel oracle that distinguish every output. Chapter 19, §19.4.1, owns the current qualifications.

12.5 Vertex and index bindings

The singular and plural vertex APIs now describe one coherent state. A singular bind validates the nonnegative element offset, replaces the vector with slot zero, and null clears both. The plural form accepts up to sixteen entries, retains legal null slots, and sets the singular pointer to slot zero (or null for an empty list). GetVertexBuffer() and GetVertexBuffers() therefore agree. This corrects the stale split-state account in an earlier edition.

The Indices property, SetIndexBuffer/GetIndexBuffer, and method-call Indices() spellings all use the same index pointer. This is surface redundancy, not independent state.

12.6 Draw calls

Buffer-backed calls are DrawPrimitives, DrawIndexedPrimitives, and DrawInstancedPrimitives. Shared validation checks bound resources, applied effect, topology-derived element ranges, and vertex/index bounds before native submission. Sampler state is applied before each route.

FNA’s generic user-primitive methods become CNA overload sets for raw packed bytes and the four built-in vertex types: VertexPositionColor, VertexPositionColorTexture, VertexPositionTexture, and VertexPositionNormalTexture. Indexed variants accept 16- and 32-bit indices. Raw nonstandard layouts require an explicit VertexDeclaration. Typed public objects are packed into the asserted stream ABI before upload; callers must not reinterpret their polymorphic C++ object representation as GPU bytes.

Reusable scratch vectors stage user draws. Their capacity persists, so the first large call may allocate while subsequent calls of the same or smaller size do not. That is an implementation warm-up cost, not a per-draw allocation promise.

12.6.1 Instanced binding transport

Current CNA no longer forwards only one instance buffer. The same FillVertexStreamBindings() helper serves ordinary and instanced draws and copies up to sixteen slot-aligned descriptions into GpuDrawParams: renderer handle, declaration, stride, element offset, instance frequency, and count. Instanced validation applies the caller’s base vertex only to per-vertex streams and computes each instance stream’s required records as 1+(n1)/f.

Families still differ in accepted layouts and native divisor support. The complete current boundary is §19.3.2; a compile-successful two-stream call is not a portable arbitrary-effect claim.

12.6.2 Effect matrices

Every shared draw starts World/View/Projection at identity, then uses dynamic_cast<const IEffectMatrices*> on the applied effect. Stock effects, ShaderEffect, and CNA’s current PBR effect implement that interface. A custom effect that deliberately omits it receives identity transforms instead of an exception; screen-space clip coordinates are a legitimate use of that fallback.

The CNAEXT PrimitiveVerts() helper converts a primitive count into the required vertex count and rejects unknown topology. It mirrors an internal FNA helper but is public in CNA.

12.7 Backbuffer readback

Three GetBackBufferData overloads write Color: whole backbuffer, caller offset, or an optional rectangle. Unlike FNA’s generic method, CNA exposes no other destination element type. Current shared code derives whole-image dimensions from PresentationParameters, validates a positive in-bounds rectangle, checks a minimum elementCount, reads RGBA bytes, and constructs each Color so its vptr-bearing C++ layout is never treated as a byte array.

Two caller-safety gaps remain. startIndex is not checked for negativity, and the method checks elementCount >= pixelCount rather than capacity after the start index. The signed width-times-height calculation is also not explicitly overflow-checked. Validate the complete destination span before calling.

The method name does not define one image across families. Active-target, physical-backbuffer, synthetic, and refusal classes are listed in §19.2.6. Pixel evidence must state the family and coordinate space.

12.8 Resources and disposal

GraphicsDevice keeps a non-owning vector of resource addresses. A base-resource constructor registers this and raises ResourceCreated before the derived native handle exists. Derived disposal releases its renderer handle; the base then raises ResourceDestroyed, removes the address, and marks itself disposed. Destructor-driven Dispose(false) suppresses the resource’s own Disposing event but still raises the device’s destroyed event.

Device disposal moves and clears the registry, disposes each resource, raises device Disposing, destroys renderer/window state, quits SDL video, and finally marks the device disposed. Two limits follow from that order:

  • a still-bound RenderTarget2D can throw during resource disposal after the registry has already been cleared, preventing renderer teardown and making retry incomplete;

  • a device Disposing listener can re-enter because IsDisposed remains false until after the event and native teardown.

Unbind targets before explicit device disposal and do not dispose the device recursively from an event. Renderer-specific in-flight/deferred destruction rules remain in the family chapters.

12.8.1 GraphicsResource and relocation

GraphicsResource supplies device, disposed state, name, tag, event, and ToString. Its copy/move operations do not repair the device’s address registry. Texture2D is copyable and shares its renderer handle; several other resources are movable. A destination can therefore remain live but untracked after relocation. Keep live resources address-stable and within device lifetime; Chapter 19, §19.1.1, gives the ownership consequences.

12.9 Adapters and format queries

GraphicsAdapter exposes current/supported display modes, description, display name, monitor handle, default/widescreen flags, and identity fields. On Linux, vendor/device IDs are best-effort values from sysfs; elsewhere they can be zero. Revision and subsystem ID are always zero at the pin. DisplayMode holds width, height, format, and computed aspect ratio. The three device exception types are thin runtime_error subclasses.

12.9.1 Adapter enumeration

getAdaptersProperty() lazily builds one adapter per SDL display and caches the owning unique_ptr vector. AdaptersChanged() destroys and rebuilds it; do not retain adapter references across that call. DefaultAdapter is the current first entry, not a separate GPU-selection service.

12.9.2 Profile and format-query scope

D3D9 is the only family whose IsProfileSupported, render-target format query, and backbuffer format query consult its native device caps. It combines profile whitelists with native format support and clamps samples per format. Other builds return true for either profile; render-target queries use a shared format list and report zero samples, while backbuffer queries select Color and zero samples. These methods therefore are not portable hardware probes.

12.10 CNA extensions and diagnostics

The extension surface falls into four groups:

Purpose Representative methods Boundary
Resource instrumentation create/destroy hooks, add/remove reference, tracked count Public for framework plumbing and tests; events occur at base construction/disposal boundaries.
Renderer identity/capability renderer type/name, SupportsCapability, maximum texture dimension, unsupported-3D policy Identity is compile-time; capability is optimistic unless the family overrides; the texture limit is enforced by content/texture paths.
State and effect plumbing depth/blend/write toggles, current effect, graphics-profile switch, stored presentation parameters Primarily internal routes; not substitutes for XNA state objects or a general runtime profile change.
Debug/recovery context-recovery policy, string marker, renderer reference, legacy MSAA recreation Renderer-specific or test-oriented; direct renderer access is not portable.

SetContextRecoveryEnabled(false) can discard future texture CPU shadows even when the family hook is empty; re-enabling cannot rebuild them. SetStringMarkerEXT is a no-op on most families and can throw on D3D9. RecreateRendererForMultiSampleCount destroys the renderer without preserving resources, state, events, or rollback; its header rationale is stale now that normal reset applies MSAA. Retain it only for its legacy pre-resource tests.

Some convenience aliases and overloads still lack the expected CNAEXT annotation. A strict-surface build is valuable but is not a proof that every untagged declaration belongs to XNA.

12.11 Compiled example: direct two-triangle rendering

The complete executable is tools/cna-screenshot-infra/software_screenshot_demo.cpp. It constructs a game, selects a 256×256 backbuffer, and draws two VertexPositionColor triangles through BasicEffect. The essential draw body is:

1 auto& dev = getGraphicsDeviceProperty();
2 dev.setRasterizerStateProperty(RasterizerState::CullNone);
3 dev.Clear(Color(30, 30, 60, 255), 1.0f);
4
5 BasicEffect fx(dev);
6 fx.VertexColorEnabled = true;
7 fx.Apply();
8
9 VertexBuffer background(dev, 3);
10 background.SetData(backgroundVertices, 3);
11 dev.SetVertexBuffer(&background);
12 dev.DrawPrimitives(PrimitiveType::TriangleList, 0, 1);
13
14 VertexBuffer foreground(dev, 3);
15 foreground.SetData(foregroundVertices, 3);
16 dev.SetVertexBuffer(&foreground);
17 dev.DrawPrimitives(PrimitiveType::TriangleList, 0, 1);
18 dev.SetVertexBuffer(nullptr);

Evidence.  This is adapted from the retained complete source, not standalone copy-and-paste code: headers, vertex arrays, game lifecycle, manager setup, screenshot helper, and main are omitted. The named source is the compile target used for verification. Figure 12.3 is its current Software-renderer artifact; it establishes CPU rasterization, depth occlusion, and interpolated vertex color on that configuration.

Software-renderer screenshot of two overlapping triangles. The nearer triangle occludes the farther triangle and its interior smoothly interpolates red, green, and blue vertex colors.
Figure 12.3: Software-renderer output from the direct GraphicsDevice example. The nearer triangle occludes the farther one and interpolates its three vertex colors.

12.12 Summary

GraphicsDevice supplies a coherent public state machine, not one uniform native implementation. The strongest portable assumptions are the shared validation and binding rules. Presentation fidelity, advanced draw paths, target behavior, and readback still require the selected family’s evidence. Applied values and discriminating output are more informative than stored requests or successful property round trips.

Type at least three characters. Results are ranked by how often and where the words occur.