Stub renderer 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. Read from source, the shared IGraphicsRenderer defaults, the smoke test and CMake registrations at the TARGET snapshot; no build or test was executed. The SDL3 build trap and the multi-renderer registration point are by inspection of the CMake files; such configurations were not configured.

STUB is the smallest physical IGraphicsRenderer implementation in the tree. It has no window, video subsystem, GPU, pixel storage or diagnostic registry. Its value to a maintainer is twofold: it shows that the neutral Game and GraphicsDevice route can run with a minimal renderer, and it shows which interface methods already have a usable shared default. A successful draw in STUB proves that a call path is reachable, not that a draw is correct. This page covers how it is built and constructed, what it really does, what its one smoke test does and does not establish, and how to change it; the fuller sibling that records and validates is the HEADLESS renderer.

Build and construction contract

The family CMakeLists.txt creates the target with cna_add_renderer(); its own comment says it “renders nothing, touches no SDL window/video subsystem/GPU library”, and the STUB arm of cmake/RendererSelection.cmake probes no dependency at all. StubRendererDescriptor.cpp registers the STUB identity with RendererWindowKind::None, needsWindow=false, needsVideoSubsystem=false, an always-available predicate and its own factory; the presenter, GL-context and Vulkan-surface requirements stay at their false defaults. GraphicsDevice::createOrAttachWindow therefore drops the platform window and returns before it consults the platform, so even a TERMINAL platform with surface presentation never gives STUB a presenter (the _cna_terminal_renderers list in cmake/RendererSelection.cmake admits STUB, and it then simply shows nothing). CreateGraphicsRenderer in StubRenderer.cpp constructs a StubRenderer from the virtual width and height only.

This is the graphics axis. It is distinct from the HEADLESS platform (Headless platform internals, a value of CNA_PLATFORM) and from the more diagnostic HEADLESS renderer; the two names that look alike are compared in the HEADLESS page. Combining STUB with any platform is a build-axis choice, not something either implies. STUB is the default renderer of three configure presets in CMakePresets.json: dev (tests, examples, C API, networking, video and Draco all off), unit (tests on, for fast non-pixel GoogleTests) and release-modules; unit-pch inherits unit. The user-level view is in Tutorial 107 and the CPU renderers section of the backends guide.

GraphicsDevice + public resource wrappers
  -> STUB descriptor / StubRenderer(virtualWidth, virtualHeight)
       -> StubVertexBufferRenderer: count only
       -> StubIndexBufferRenderer: count + 16/32-bit kind
       -> StubTextureRenderer: dimensions only
       -> StubSpriteBatchRenderer: Begin/Draw/End no-op
  -> no native surface, pixels, draw submission or presenter
  -> inherited IGraphicsRenderer defaults for everything not listed

What the implementation actually does

StubRenderer.hpp holds the whole implementation. The rules below are what the header and the inherited defaults in IGraphicsRenderer.hpp add up to.

  • Frame calls. Clear, Present, SetPresentationMode, every clear variant, the depth, blend and depth-write toggles, and both colored primitive draws are empty. GetViewportSize returns the configured virtual dimensions, or 1024×768 when they are not positive; SetVirtualResolution changes them.
  • Buffers. A vertex buffer remembers a count and discards uploaded bytes; SetVertexDeclaration is an explicit empty override, the decision IVertexBufferRenderer forces every renderer to make, because STUB stores no vertices and binds no native layout, so there is nothing a declaration could describe unfaithfully. An index buffer remembers count and 16/32-bit identity, and using the wrong upload width throws std::runtime_error (SetData16 on a 32-bit buffer or the reverse), the only validation in the renderer.
  • Textures. A texture remembers width and height and no pixels: UpdatePixels and UpdatePixelsLevel keep their empty defaults, so an upload is discarded, and GetData keeps the interface default of false, meaning “this renderer read nothing back”. The header comment on StubTextureRenderer describes both as “accepted and discarded”, which is stale for reads: the interface's default answer is a refusal, and the shared layer raises NotSupportedException only where it has no CPU shadow of its own to answer from (a plain Texture2D answers from its own CPU-side pixels). The user-level consequence is that a texture upload test proves nothing about STUB.
  • SpriteBatch. Begin, End and every Draw overload are empty, so batching state is not modelled at all (unlike HEADLESS, an unmatched Begin is not even noticed).
  • Capabilities. SupportsCapability returns false for every queried capability, so ThreeD, CustomEffects and the rest all report false, and STUB is the honest end of the capability spectrum. Texture3D construction therefore fails through the public capability gate. A 3D draw is nevertheless a silent no-op rather than a refusal, because the colored draws are overridden empty and the extended draws inherit a default that forwards to them.
  • Render targets. STUB creates none: CreateRenderTarget2D and CreateRenderTargetCube keep the shared nullptr defaults, and its SetRenderTargets is an explicit empty override for the same reason as the vertex declaration (no attachment to bind, no surface a misread descriptor could corrupt). The public layer constructs a RenderTarget2D with a null renderer on purpose, and GraphicsDevice::SetRenderTargets then throws NotSupportedException (“this renderer does not support RenderTarget2D”, and the cube equivalent) before it reaches the backend, so the empty override is only reached for the unbind case. A target test that requires real attachments must use another renderer.
  • Readback. STUB does not override ReadBackbuffer, and the interface default throws std::runtime_error (“ReadBackbuffer: not implemented in this renderer”), which GraphicsDevice::GetBackBufferData reaches after its own argument validation. HEADLESS refuses the same call with System::NotSupportedException instead, so a test that catches the specific type behaves differently on the two.
  • Instanced draws. DrawInstancedPrimitivesEx is not overridden either. Its interface default returns a warning-producing no-op only when the app selected Unsupported3DGraphicsCallBehavior::WarnAndStub on a renderer that reports ThreeD false, and otherwise throws std::runtime_error; the default behaviour is Throw. This was read from the header, and whether GraphicsDevice rejects an instanced call earlier on a capability check was not traced.

The renderer owns no shared native device, and its resource implementations are ordinary unique_ptr-returned objects owned by the neutral wrappers, so there is no GPU cleanup, fence or presentation teardown to analyse here. Conversely STUB cannot detect a native use-after-free or a resource leak: it has no AliveResources equivalent, and it keeps no counters or trace. Its absent synchronization is not a thread-safety guarantee for GraphicsDevice.

Smoke scope and a build trap

The only family-local registration is Stub_Smoke (30 second timeout, label Stub), in the examples CMakeLists.txt, conditioned on CNA_BUILD_TESTS and CNA_GRAPHICS_RENDERER=STUB. stub_smoke_test.cpp runs a real Game on a 64×64 back buffer for three frames and passes when seven checks hold: SDL_INIT_VIDEO was never initialised, the window handle is null, a plain DrawPrimitives (vertex buffer plus BasicEffect) does not throw, an indexed draw does not throw, a SpriteBatch Begin/Draw/End does not throw, and the vertex and index buffers report the counts they were given (two checks). It does not inspect pixels, count draws or validate anything.

⚠

The fixture needs SDL3 although the renderer does not. The test source includes <SDL3/SDL.h> unconditionally (Check A calls SDL_WasInit, and the window-handle check casts to SDL_Window*), and the cna_stub_test macro in the same CMake file links SDL3::SDL3 with no if(TARGET ...) guard. The HEADLESS macro, by contrast, defines CNA_HEADLESS_TEST_HAS_SDL and links SDL only where a target exists. A configuration without an SDL3 target therefore cannot build this executable, and the failure would appear at configure or generate time rather than as a skipped test (by inspection; such a build was not configured). A renderer build with no SDL does not by itself imply that Stub_Smoke is available: inspect the configured targets before claiming test coverage.

Two further points about when the smoke test exists. The unit build preset names only CnaTests as its build target, so the separate smoke executable is registered by that configuration but is built only when a wider target such as all is requested (read from CMakePresets.json; not exercised). And the guard has no comparison with _cna_default_renderer_identity, the check the HEADLESS block gained after its targets could not find the renderer header in a multi-renderer build. In a build such as the multi-renderer preset (default HEADLESS, members HEADLESS;SOFTWARE;STUB) the family loop in modules/renderers/CMakeLists.txt re-points CNA_GRAPHICS_RENDERER while entering each family, so the STUB block is entered although STUB is not the default. The macro's own include-directory comment records that a multi-renderer build with STUB as a non-default member was hit. The fixture would then run under the default renderer and only cast it to StubRenderer (it never calls through the cast), so it would pass without exercising STUB. The multi-renderer workflow (multi-renderer-ci.yml) builds CnaTests, the renderer-selection demo and the descriptor gate, not this executable, so nothing automatic covers that case; this is a reading of the CMake files, not an observed run.

The unit preset uses STUB as its default renderer, which makes it useful for fast non-pixel GoogleTests. That does not upgrade it to a renderer conformance oracle. Texture-reader and other renderer-gated tests skip rather than pass under it (the gate macro is CNA_SKIP_IF_RENDERER_IS_NONE_OF, see the content runtime page for an example). When a shared graphics change passes STUB, add HEADLESS in Validation mode for state and lifetime behaviour, Software for pixel behaviour, and an affected native renderer for driver and surface behaviour as appropriate. One test result should be reported with its actual evidence level.

How to read and modify it

  1. Read the descriptor and the family target above to see why GraphicsDevice skips window and video creation.
  2. Read StubRenderer.hpp against IGraphicsRenderer.hpp: decide which defaults STUB intentionally inherits and which methods it explicitly overrides. Two of its overrides (SetVertexDeclaration and SetRenderTargets) exist only because the interface requires every renderer to state a decision.
  3. Read StubRenderer.cpp for the factory and the allocation path, then the smoke fixture for the exact tested public route.
  4. If adding a method to IGraphicsRenderer, make an explicit STUB decision (no-op, refusal or minimal state) rather than inheriting a behaviour that falsely claims a feature works. Test the minimal build and all substantive backends independently.

STUB's role is deliberately narrow. The fuller human testing decision tree is on What to test after changing X, and the renderer-selection machinery that makes a renderer the default is on Renderer selection internals.

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