Stub renderer internals
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. 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.GetViewportSizereturns the configured virtual dimensions, or 1024×768 when they are not positive;SetVirtualResolutionchanges them. - Buffers. A vertex buffer remembers a count and discards uploaded bytes;
SetVertexDeclarationis an explicit empty override, the decisionIVertexBufferRendererforces 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 throwsstd::runtime_error(SetData16on a 32-bit buffer or the reverse), the only validation in the renderer. - Textures. A texture remembers width and height and no pixels:
UpdatePixelsandUpdatePixelsLevelkeep their empty defaults, so an upload is discarded, andGetDatakeeps the interface default offalse, meaning “this renderer read nothing back”. The header comment onStubTextureRendererdescribes both as “accepted and discarded”, which is stale for reads: the interface's default answer is a refusal, and the shared layer raisesNotSupportedExceptiononly where it has no CPU shadow of its own to answer from (a plainTexture2Danswers from its own CPU-side pixels). The user-level consequence is that a texture upload test proves nothing about STUB. - SpriteBatch.
Begin,Endand everyDrawoverload are empty, so batching state is not modelled at all (unlike HEADLESS, an unmatchedBeginis not even noticed). - Capabilities.
SupportsCapabilityreturnsfalsefor every queried capability, soThreeD,CustomEffectsand the rest all report false, and STUB is the honest end of the capability spectrum.Texture3Dconstruction 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:
CreateRenderTarget2DandCreateRenderTargetCubekeep the sharednullptrdefaults, and itsSetRenderTargetsis 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 aRenderTarget2Dwith a null renderer on purpose, andGraphicsDevice::SetRenderTargetsthen throwsNotSupportedException(“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 throwsstd::runtime_error(“ReadBackbuffer: not implemented in this renderer”), whichGraphicsDevice::GetBackBufferDatareaches after its own argument validation. HEADLESS refuses the same call withSystem::NotSupportedExceptioninstead, so a test that catches the specific type behaves differently on the two. - Instanced draws.
DrawInstancedPrimitivesExis not overridden either. Its interface default returns a warning-producing no-op only when the app selectedUnsupported3DGraphicsCallBehavior::WarnAndStubon a renderer that reportsThreeDfalse, and otherwise throwsstd::runtime_error; the default behaviour isThrow. This was read from the header, and whetherGraphicsDevicerejects 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
- Read the descriptor and the family target above to see why
GraphicsDeviceskips window and video creation. - Read
StubRenderer.hppagainstIGraphicsRenderer.hpp: decide which defaults STUB intentionally inherits and which methods it explicitly overrides. Two of its overrides (SetVertexDeclarationandSetRenderTargets) exist only because the interface requires every renderer to state a decision. - Read
StubRenderer.cppfor the factory and the allocation path, then the smoke fixture for the exact tested public route. - 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.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-185: Stub_Smoke links SDL3::SDL3 unconditionally, so STUB + tests cannot be configured with CNA_ENABLE_SDL=OFF — The STUB renderer's smoke-test macro links SDL3::SDL3 in both branches and its source includes the SDL3 header, while CNA_ENABLE_SDL=OFF is a supported configuration; with STUB selected and tests on, the SDL-free configu
- CNA-BUG-202: In a multi-renderer build, non-default renderers' example CTests are registered but run under the default renderer — The SDL_GPU, SOFTWARE, STUB and VULKAN example blocks are entered for a non-default member of CNA_GRAPHICS_RENDERERS, yet their registrations select no renderer, so each executable runs under the build default.
- CNA-BUG-227: StubTextureRenderer's comment says GetData() is accepted and discarded, but the inherited default refuses the read — The class comment on StubTextureRenderer says SetData()/GetData() are accepted no-op defaults, while the ITextureRenderer::GetData default it inherits returns false, which the shared Texture2D layer turns into NotSupport
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- Architecture
- Graphics architecture
- Internals
- HEADLESS renderer (records and validates) · HEADLESS platform (a different axis) · Renderer selection internals · GraphicsDevice internals · Software renderer (real CPU pixels)
- Maintainer workflow
- I need to fix a renderer bug · Add a regression test
- Tests and validation
- Test architecture · What to test after changing X
- Reference
- Test target index