Software 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. The full shader-opcode and format matrix and the GDI 2D consumer were not audited line by line; no test was executed.
The SOFTWARE identity is a real CPU renderer, not a headless no-op: modules/renderers/software/ owns vertex and index bytes, textures, render-target framebuffers and rasterised colour, depth and stencil in host memory. That makes it a valuable differential implementation when a graphics contract is in doubt, but an exact output oracle only where the tested feature path implements the same semantics. The GDI identity compiles a deliberately restricted subset of this family, not a second copy of its 3D engine, which is the first thing to remember before editing a "local" 2D file here.
Identity, build target and unusual GDI reuse
software/CMakeLists.txt builds the full cna_renderer_software archive when the family is entered as SOFTWARE: it globs every src/*.cpp except the 2D-only wrapper and compiles it once. SoftwareRendererDescriptor.cpp publishes one GraphicsRendererType::Software descriptor: RendererWindowKind::None, no required window or video subsystem, AlwaysAvailable, and needsSurfacePresenter, a request for the IPlatformSurfacePresenter seam. The factory at the end of SoftwareRenderer.cpp constructs SoftwareRenderer from the virtual size and the depth format (depth storage unless None, stencil only for Depth24Stencil8), applies the multisample count and attaches the presenter GraphicsDevice supplied, which is null on every configuration where nothing can display a CPU frame. A windowing platform does not gain a visible Software window: only a platform that hands over a presenter, in practice the Terminal platform, displays its output. An off-screen Software frame is still real and readable; see CPU and no-GPU renderers.
When GDI is selected instead, the same file exports CNA_GDI_SOFTWARE_SOURCES, a curated list of eight translation units (framebuffer and texture allocation, SoftwareFramebuffer.cpp, SoftwareTexture2D.cpp, SoftwareRenderTarget2D.cpp, SoftwareRenderer2DState.cpp, SoftwareSpriteBatch.cpp and SoftwareRenderer2D.cpp), plus the interface target cna_renderer_software_headers for the GDI archive. SoftwareRenderer2D.cpp defines CNA_SOFTWARE_2D_ONLY and includes SoftwareRenderer.cpp, omitting Software's 3D and cube implementation and 3D draw entries (they throw NotSupportedException or std::runtime_error in that compilation). This is the one documented physical-source/target-membership exception: changing a supposedly local 2D unit can change GDI, and adding a new Software translation unit to GDI without reviewing the 2D-only boundary is unsafe. For the same reason GDI and SOFTWARE cannot be linked into one binary (combination rules). Optional CNA_SOFTWARE_COMPILED_EFFECTS adds a Python build step that embeds FNA3D's SpriteEffect.fxb through embed_effects.py and links the MojoShader effect helper; it is not the baseline build and does not imply arbitrary shader parity.
Framebuffer, draw and present path
GraphicsDevice neutral validation and state -> SoftwareRenderer::DrawIndexedPrimitivesEx -> DrawIndexedPrimitivesInternal: count/topology checks, compiled-effect gate -> declared vertex streams or legacy fixed-stride record; index decode + startIndex/baseVertex -> world * view * projection -> clip to the view volume -> framebuffer/viewport/scissor -> CPU point / line / triangle rasterizer -> active SoftwareFramebuffer(s) -> depth / stencil / blend / format / sample writes -> SoftwareRenderer::Present: resolve backbuffer_ -> SurfaceFrame -> IPlatformSurfacePresenter::Present if attached; otherwise nothing
SoftwareVertexBufferRenderer and SoftwareIndexBufferRenderer own byte vectors. A declared vertex buffer resolves attributes by semantic, index and stream-local offset; the older no-declaration path accepts only a fixed list of strides (16, 20, 24, 32, 48, 52, 56, 60, 68, 76, 80 bytes) and throws for anything else. DrawIndexedPrimitivesInternal rejects a non-positive primitive count and topologies other than triangle list and strip, line list and strip, and PointListEXT; routes a compiled-effect draw (or throws NotSupportedException when the option was not built); and uses params.startIndex as an index-element offset and params.baseVertex once per index (REMED-GFX-110: both used to be dropped). minVertexIndex and numVertices remain hints and are not consulted. Raw fallback addressing is validated strictly because it would otherwise form host pointers; declared public streams pass native ranges through and default missing records as XNA does. The draw snapshots depth, stencil and blend state, transforms, clips, intersects framebuffer, viewport and scissor, and rasterises points, lines or clipped triangulated polygons. All of this happens inside the draw call: there is no command queue, fence or device-loss model to reason about.
SoftwareFramebuffer, declared in SoftwareRenderer.hpp and implemented in SoftwareFramebuffer.cpp, owns resolved RGBA8 colour, an optional wide floating-point colour plane for float and half targets, depth and stencil, and optional four-sample planes (multiSampleCount 4 is the only multisample mode). Its declared-format encode, decode and quantisation matter for render targets: storing RGBA8 for a non-Color format would make readback and sampling lie. ResolveColor collapses the sample planes into the presentation and readback cache. There are allocation plans, overflow and bounds checks, and a native presenter lifetime to respect; "CPU" does not mean "unbounded safe allocation".
Present, in SoftwareRenderer2DState.cpp, deliberately presents backbuffer_, not CurrentFramebuffer(): a render target left bound at frame end must not be displayed. With no presenter it returns immediately; otherwise it resolves MSAA, checks dimensions and storage, and hands a borrowed pointer to tightly packed, top-row-first RGBA8 bytes (strideBytes 0) to the presenter, which owns display and scaling from there. PresentRegionEXT accepts a source rectangle, clips it again against the framebuffer actually allocated (a resize may have happened in between) and presents it by row stride without copying, but returns false for a destination rectangle or an override window handle, which the presenter contract cannot express. SetVirtualResolution resizes the back buffer only while no render target, cube target or MRT set is bound, and GetViewportSize reports the current framebuffer, so a wrong viewport size while a target is bound is not necessarily a window-resize bug. ReadBackbuffer resolves for the same reason Present does.
Texture and render-target ownership
Creation methods return unique_ptr renderer resources. Software textures own CPU pixel and mip storage; 2D and cube render targets own a SoftwareFramebuffer plus optional mip levels. SoftwareRenderTargetRenderer validates an allocation layout before constructing storage and catches allocation failure during mip creation, and CreateRenderTarget2DEXT refuses a format ClassifyRenderTargetFormatEXT does not support. UpdatePixels honours a positive row stride, rejects a pitch shorter than one declared-format row, loads exact format values and invalidates or regenerates mips where appropriate. A target exposes its colour through SoftwareColorSurface, the read-only sampling interface a texture also implements, so a completed target is sampled as a texture rather than accidentally shading white. The neutral GraphicsResource still owns and disposes the renderer resource; Software's unique_ptr is the backend-native half, not the public lifetime registry (textures and render targets).
SetRenderTarget2D throws for a resource from another renderer, unbinds the old target and binds the new one. SetRenderTargets routes a single binding to the single-target or cube-face path, accepts at most four bindings, checks every binding's type before mutating active state, binds them in order, and unbinds the partial set if a bind throws. UnbindCurrentTargets resolves and finalises through each target implementation before returning to the back buffer, so a target-sampling or mip regression may originate at unbind or resolve rather than in the later sampler. The first MRT attachment owns depth and stencil, stock effects write only colour 0, and clear, resolve and mip finalisation visit every attachment. The 2D-only GDI compilation rejects cube targets. When changing this code review disposal while bound and incomplete construction, not only steady-state pixels.
What shader/effect support actually means
The ordinary draw path contains built-in CPU shading for XNA's stock effects and for texture and sampler state. SupportsCapability(CustomEffects) answers false: SoftwareEffectRenderer accepts source text for resource compatibility, but the rasteriser never executes it (see renderer parity). Optional compiled effects use SoftwareCompiledEffect.cpp with MojoShader translation plus CPU vertex and pixel interpreters (SoftwareShaderInterpreter.cpp, SoftwarePixelShaderInterpreter.cpp), a separate execution path with bounded reflected items, shader bytes and register arrays. CreateCompiledEffect returns null in a build without the option. It cannot be inferred from effect construction alone that every source shader or opcode is supported; the indexed draw path, for example, refuses compiled-effect PointListEXT with NotSupportedException. When fixing one shader case, identify whether the failing path is stock shading, the translated compiled effect, the vertex interpreter, the pixel interpreter or sampler/format handling, and test a real output pixel, not only effect construction.
Several capability answers promise storage or CPU behaviour rather than GPU features: Texture3D (exact CPU volume storage), AnisotropicFiltering (a CPU footprint filter), OcclusionQuery (counts samples surviving the shared fragment path), MultiStreamVertexInput, MultipleRenderTargets (four), FloatRenderTargets/HalfFloatRenderTargets and Instancing. The switch ends in default: return true; the public answers for the members GraphicsDevice derives from separate virtuals come from those virtuals, as the capability matrix records. There is no shadow or image-based-lighting reception here.
Validation and known limits
examples/CMakeLists.txt returns early, with a status message, when the configuration has no SDL3::SDL3 target, because its test harness links SDL3 directly even though the renderer needs no display or SDL. Otherwise it registers inside if(CNA_BUILD_TESTS AND CNA_GRAPHICS_RENDERER STREQUAL "SOFTWARE"). modules/renderers/CMakeLists.txt re-points CNA_GRAPHICS_RENDERER to each family's identity while entering it, so the block is also entered when SOFTWARE is a non-default member of CNA_GRAPHICS_RENDERERS (the multi-renderer preset is HEADLESS;SOFTWARE;STUB with HEADLESS as default); the registrations set no renderer selection, so in such a build confirm which renderer each executable creates before citing a Software result. Check the configured inventory and skips (ctest -N) before claiming the suite ran.
Registered families include Software_Smoke, Software_Rasterizer, Software_Effects, depth, culling and clipping (Software_DepthState, Software_Culling, Software_Clipping, Software_XnaPixelCenter, Software_TopLeftFill), stencil (Software_DepthStencilState_*), declaration and addressing (Software_VertexDeclaration, Software_IndexedAddressing), readback and MSAA (Software_RenderTargetReadback, Software_MsaaStorage, Software_MsaaFragmentContract), presentation (Software_PresentationResetContract, Software_BackbufferResize), lifetime (Software_BoundResourceDispose, Software_ResourceLeak, Software_DeviceDisposeOrder), shared GraphicsDevice contracts, SpriteBatch and SpriteFont parity, model loading, and Software_XnaLineCoverage, the family's gate on two scenes of the XNA oracle corpus (how renderers are verified); that script fails the test on a render failure or a missing reference but only prints a pixel difference. Many of these sources are shared with other renderers (EasyGL, SDL_GPU and HEADLESS register the same fixtures), which makes differential review possible. Software_CompiledEffectRuntime exists only with CNA_SOFTWARE_COMPILED_EFFECTS.
Module GoogleTests live under tests/: SoftwarePresentationTests.cpp (SoftwarePresentation.*: no-op without a presenter, first-frame dimensions, RGBA8 channel order, tightly packed rows, consecutive and unchanged frames, resize, MSAA resolve before presentation, nothing presented after destruction), compiled only when CNA_RENDERER_SOFTWARE is defined, and the compiled-effect conformance tests, which additionally need CNA_SOFTWARE_COMPILED_EFFECTS.
For a pixel mismatch: run the smallest named Software test, capture the exact input, state and back-buffer bytes, and compare the same neutral contract on EasyGL or another backend. Decide first whether the expected pixel is an XNA or renderer-neutral contract or an assumption encoded in one test. A Software pass proves its CPU implementation, not hardware parity; a GPU pass alone can hide a common neutral-state error. For presenter or resize work run SoftwarePresentation and, where possible, a Terminal live smoke; for target or format work pair exact-format readback with target-as-texture sampling; for compiled effects build both option states. What to test after changing X explains how to report configured and skipped evidence. The CNA-recorded oracle figures for this renderer are summarised in Tutorial 107; nothing was run for this page.
Source reading route for a maintainer
software/CMakeLists.txt,SoftwareRendererDescriptor.cpp, then the factory at the end ofSoftwareRenderer.cpp: physical target, identity, no-window/presenter contract and the GDI split, before tracing pixels.SoftwareRenderer.hppandSoftwareRenderer2DState.cpp: current framebuffer, state and target binding, resize, readback andPresentownership.DrawIndexedPrimitivesInternalinSoftwareRenderer.cppandSoftwareFramebuffer.cpp: follow one index through declaration streams, clipping, per-sample state and exact format storage; thenSoftwareRenderTarget2D.cppfor bind, unbind and mips.SoftwareCompiledEffect.cppand the two interpreter files, only if the build enables compiled effects; their opcode handling is not the baseline shading route.examples/CMakeLists.txtandSoftwarePresentationTests.cpp: pick an executable pixel test and know which configuration includes it.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- DIRECT2D and GDI: two Windows 2D delivery stacks — How CNA's two Windows-only 2D renderers draw, refuse, present and recover: Direct2D over a private Direct3D 11 device versus GDI over a private CPU 2D core, and what their tests prove.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- 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-206: Software_XnaLineCoverage passes when a scene differs from the XNA reference: run-oracle-corpus-diff-software.sh fails only on render errors — The CTest described as the exact Software regression for the two aliased-line scenes runs a script that prints DIFF for a pixel mismatch and still exits 0; only a missing reference or a failed render makes it fail.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- Architecture
- Graphics architecture
- Internals
- Indexed draw trace · Textures and render targets · HEADLESS renderer · Terminal platform (the presenter)
- Maintainer workflow
- Fix a renderer bug · Architectural invariants
- Tests and validation
- Test architecture · What to test after changing X
- Reference
- Test target index · CMake option index