Graphics backends

CNA snapshot c1c316b9  ·  Development › Graphics internals  ·  source links pinned to c1c316b9

✓

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 c1c316b9; unless a sentence says otherwise, nothing here was built or executed. Descriptor values, gates and maturity were read from the source.

CNA declares 14 public renderer identities at this snapshot, implemented by 12 families under modules/renderers (plus two shared helper targets, common/d3d and common/mojoshader). An identity in the registry is evidence of selectable wiring, not of feature completeness. This page is the family map for maintainers: where each family lives, what its descriptor asks of the platform, what gates its build, and which families have a source tour in the Development area.

Start with an implementation, not an identity name

A bug report names an identity; the code to read is a family. Three identities share EasyGL, so the mapping is not one-to-one. The identity-to-accessor table with C ABI values is generated on Selection axes: renderer identities; the tables below are the family-first view. “Window” is the descriptor's RendererWindowKind and the platform service it requests; maturity is CNA's own declaration in GraphicsBackendMaturity.hpp, not a measurement.

Families with an internals page

These six families have source tours that identify what the code at this snapshot executes and where its tests stop. A tour is not a parity certificate.

FamilyIdentities (declared maturity)Window and servicesBuild gate and dependencyWhat the page establishes
EasyGL (easygl/)OPENGLES3, OPENGL33 (Production); WEBGL2 (Supported)OpenGL; GL context service; config chosen at context creationES/desktop profiles native only, the WebGL profile Emscripten only; sibling ../easy-gl (+ ../meta-gl)One implementation with a runtime GlProfile per identity, context, state, shader and FBO paths, context loss, tested limits.
Vulkan (vulkan/)VULKAN (Production)Vulkan; Vulkan surface service; adapter MSAA clampVulkan SDK/loader (find_package(Vulkan REQUIRED))Device, surface and swapchain, deferred draw, frame synchronisation, resource retirement and teardown.
SDL_gpu (sdl-gpu/)SDL_GPU (Supported)Plain windowSDL3 platform required; SDL_shadercross + SPIRV-Cross (default ON on Windows and Apple)SDL device and shader routes, queued draw and pass replay, acquire, submit and present, lifetime.
Software (software/)SOFTWARE (Experimental)None; surface presenter when the platform offers only presentation (Terminal on a TTY)Any host; compiled effects opt-in (CNA_SOFTWARE_COMPILED_EFFECTS)The CPU framebuffer and rasteriser, texture and target storage, the optional presenter and how a presenting platform shows the frame.
Headless (headless/)HEADLESS (Supported)NoneAny host; CNA_HEADLESS_MODE = Fast, Validation (default) or TraceNon-rasterising validation, resource bookkeeping, trace, truthful readback refusal and the limits of its tests.
Stub (stub/)STUB (Supported)NoneAny host; the renderer of the dev, unit and release-modules configure presetsThe minimal no-op implementation, count-only resource objects (vertex and index counts) and what smoke tests on it can and cannot show.

Families without an internals page yet

For these six families the Development area has no source tour. What is known about them from the source at this snapshot is on the user-level pages linked in the last column; start there, then read the family directory directly.

FamilyIdentity (declared maturity)WindowBuild gate and dependencyStart at
webgpu/WEBGPU (Experimental)PlainPinned wgpu-native (Linux, macOS, Windows) or, under Emscripten, the emdawnwebgpu portModern GPU APIs
metal/METAL (Supported)Metal; high-DPI backingApple only (macOS and iOS); Objective-C++ and Apple frameworks; SDL3 supplies the window and CAMetalLayerModern GPU APIs
directx9/DIRECTX9 (Production)Plain; adapter queries for profile, formats and MSAAWindows target only (native or mingw-w64)Windows renderers
directx11/DIRECTX11 (Production)Plain; adapter queries for formats, depth selection and MSAAWindows target only; common/d3d helpersWindows renderers
sdl-renderer/SDL_RENDERER (Production), 2D-onlyPlainSDL3 platform required; default renderer on every host except Linux and Emscripten2D-only renderers
fna3d/FNA3D (Experimental)OpenGL; pre-window 24/8/double-bufferedSDL3 platform required; fetched FNA3D and MojoShaderTranslation and adapter layers

The window kinds and services above come from each family's *RendererDescriptor.cpp; platform gates from cmake/RendererSelection.cmake and cmake/RendererCombinations.cmake; the three SDL3-only families (sdl-renderer, sdl-gpu, fna3d) set the REQUIRES_PLATFORM SDL3 target property in their own CMakeLists.txt. Capability answers per identity are in the capability matrix and are not repeated here.

Contract notes for FNA3D and the 2D family

These families have no source tour yet. The notes below were checked by reading the TARGET source at c1c316b9 (not executed) and name what a maintainer is most likely to break.

FNA3D: native handles outlive the draw, targets finalise on unbind

FNA3D is one CNA identity even though the library picks its own driver at run time; the driver is an implementation detail, pinned in tests with FNA3D_FORCE_DRIVER (Tutorial 108). Every wrapper in Fna3dResources.cpp (textures, render targets and their renderbuffers, vertex and index buffers) and every effect wrapper releases its native handle through FNA3D's own deferred-disposal queue — FNA3D_AddDisposeTexture, FNA3D_AddDisposeRenderbuffer, FNA3D_AddDisposeVertexBuffer, FNA3D_AddDisposeEffect — and only while the owning device is still live. A handle that a queued batch still references is therefore freed by FNA3D after its command stream is done with it, not at C++ destruction time. Fna3dRenderer::UnbindTargetsEXT first binds no targets, then calls FNA3D_ResolveTarget for every previously bound target that is multisampled or mipped: that call is where FNA3D resolves MSAA and builds the mip chain, so a later draw samples finished contents. A PbrEffect or SkinnedPbrEffect draw is refused by RequirePbrShadingSupportEXT before any buffer is bound (Fna3dDraw.cpp), because the stock-effect selection has no PBR case and used to fall through to BasicEffect or SkinnedEffect.

SDL_RENDERER

  • SDL_RENDERER uploads each texture as an SDL_PIXELFORMAT_RGBA32 SDL texture and draws sprites with SDL_RenderTexture, SDL_RenderTextureRotated and SDL_RenderTextureAffine; readback is SDL_RenderReadPixels (SdlRenderer.cpp).

Its contract is negative: ThreeD is false (it answers SupportsCapability true for AdditiveBlending only), 3D factories and draws fail at a predictable seam (throw by default, warn-and-stub only where honoured) and a draw is never accepted and silently discarded. Test that contract through the public resource and the public draw route, as the shared unsupported_3d_call_behavior_test.cpp fixture does (registered as a test for SDL_RENDERER); a unit test of an internal format or state mapper proves nothing about whether GraphicsDevice refuses the call safely.

How to determine actual state

  1. Find the identity in cmake/RendererIdentities.cmake and its declared maturity and category in GraphicsBackendMaturity.hpp and GraphicsBackendCategory.hpp.
  2. Find its arm in cna_configure_renderer_identity() (cmake/RendererSelection.cmake): host gates, dependencies, compile options; then any rule in cmake/RendererCombinations.cmake.
  3. Find its row in cmake/RendererRegistry.cmake and read the family's descriptor: window kind, services, pre-window request, adapter hooks.
  4. Trace the resource methods, present and resize (OnSurfaceChanged, GetDefaultViewportRect), looking for refusal branches: HandleUnsupported3DCall and Ensure3DSupported, NotYetImplemented() from NotYetImplemented.hpp (used by the DirectX 9 family), null render-target factories, and capability switches with a default: return true arm.
  5. Find its focused and conformance tests in modules/renderers/<family>/examples/CMakeLists.txt and tests/, and the CI workflow that runs them on a host that really has the API (Verification: CI).

Only after all five should documentation call a feature supported. File presence alone is insufficient, and a registered test is not a passing one.

Common implementation obligations

Every family implements the same IGraphicsRenderer contract. The obligations that recur in every source tour, with the hook that carries each:

  • Translate public formats, states, primitive topology and shader binding consistently: ClassifySurfaceFormatEXT and ClassifyRenderTargetFormatEXT (no classifier means Color only), the normalised state ordinals from XnaStateConversion.hpp, GpuDrawParams.
  • Create resources on the required device or context thread and destroy them before that device or context. EasyGL, the only family that returns a lease from AcquireThreadContextLeaseEXT, serialises cross-thread work through it; a family whose resource objects can outlive it (shared ITextureRenderer copies) must release and disconnect them in its own destructor, as Vulkan does.
  • Separate logical window size from drawable pixel size: RendererSurfaceInfo carries drawableSize and displayScale; GetViewportSize() is logical, GetDefaultViewportRect() physical.
  • Handle zero-sized or minimised surfaces without presenting invalid extents (Vulkan, for example, recreates its swapchain in OnSurfaceChanged only for a positive drawable size).
  • Keep render-target and back-buffer transitions and coordinate conventions consistent: SetRenderTargets(nullptr, 0) returns to the back buffer; EasyGL stores render targets bottom-up and corrects for it when sampling and reading back.
  • Report unsupported capabilities honestly rather than silently succeeding: answer SupportsCapability without a permissive default arm, return false from EXT hooks, and refuse 3D work through Ensure3DSupported or HandleUnsupported3DCall (which honours Unsupported3DGraphicsCallBehavior).

Contract comments that lag the code

The renderer headers carry many design notes, and most of them are accurate. A few, read at c1c316b9, now describe an earlier state. A maintainer should trust the code over these comments:

  • SetDepthTestEnabled. The @note in IGraphicsRenderer.hpp says only EasyGL honours the toggle and that other renderers throw on first 3D use. The method is pure virtual, like SetBlendEnabled and SetDepthWriteEnabled, so every family defines it. Vulkan, DirectX 11 and Software store or apply the value, and DirectX11Renderer.cpp records the silent cross-renderer divergence that existed before its override.
  • The DirectX 9 default list. DirectX9Renderer.hpp says CreateOcclusionQuery and CreateTexture3D are "left un-overridden here on purpose", yet the same header overrides both. The principle stated in that comment still holds, and every family follows it: "inheriting 'throws' is fine, only inheriting silence is the trap".
  • Where FNA3D's stock effects come from. The CNA_RENDERER_FNA3D comment in RendererSelection.cmake says the stock-effect blobs are fetched from a pinned FNA checkout. ThirdPartyFNA3D.cmake declares a single fetch, of FNA3D itself, and states that modules/renderers/fna3d/effects holds the binaries. Those binaries are committed, with their provenance in that directory's README.
  • The split depth/stencil queries. The documentation of HasRealBackBufferDepthBuffer motivates the split with a graphics API that CNA does not ship, and the neighbouring SupportsStencilBuffer comment says that no renderer in the tree answers SupportsDepthBuffer and SupportsStencilBuffer differently today. That is out of date: SDL_GPU answers depth from the depth format it created and stencil only when that format has a stencil plane (SdlGpuRenderer.hpp), so a depth-only back buffer reports depth without stencil. The comment's reason for keeping the split still stands: the single-argument Clear(Color) must keep an available depth clear and leave out a stencil plane that does not exist.
  • A Metal test that nothing builds. metal_spritebatch_customeffect_test.cpp is referenced by no CMake file. It asserts a custom-effect path that MetalRenderer::CreateEffectRenderer refuses at this snapshot ("Metal custom effects are disabled until the adapted renderer has passing macOS shader and pixel evidence"). It is therefore neither evidence for that path nor a regression guard for it.

Validation by backend

Use the API's own validation where the family wires it, but keep it supplementary to CNA's conformance tests:

FamilyHook in the source
VulkanVK_LAYER_KHRONOS_validation is enabled in non-NDEBUG builds when installed; synchronisation validation is opt-in per test. In any configuration that compiles VULKAN, TestHelpers.cmake fails a renderer test on any [Vulkan Validation] output.
EasyGLglGetError checks at specific points in the renderer; no debug-message callback is installed by CNA's EasyGL source.
DirectX 11D3D11_CREATE_DEVICE_DEBUG in non-NDEBUG builds, forced on or off with CNA_D3D11_DEBUG_LAYER=1/0; device creation retries without it when the SDK debug component is missing.
WebGPUError scopes and uncaptured-error callbacks in WebGPURenderer.cpp.

Run window and GPU tests through tools/platform/run_gpu_tests_private.sh: it starts a private headless Weston plus a rootful Xwayland with DRI3 on the real GPU, points DISPLAY and WAYLAND_DISPLAY at them, and runs ctest (or one command with --exec), so nothing appears on the desktop; it exits 77 when no compositor is available and refuses a build tree that forces its own test display. Compare a second family to separate a shared-layer bug from one translation layer.

What these pages do not establish

This page routes; it is not a renderer-parity certificate. The Development area has source tours for 6 of the 12 families; the other 6 are listed above with the user-level sections that describe them. There is no feature-by-feature evidence matrix for all 14 identities here: capability answers are code-derived, test registrations are not results, and several families (the Direct3D families, Metal) are chiefly exercised on hosts or under Wine and CI lanes that were not examined for this page. Before a risky change, inspect the chosen family's build wiring, tests and a host that can actually run it.

Read in this order

  1. RendererIdentities.cmake and RendererRegistry.cmake: the 14 identities and the 12 families behind them.
  2. modules/renderers: one family directory, its CMakeLists.txt, src/*RendererDescriptor.cpp and examples/CMakeLists.txt.
  3. IGraphicsRenderer.hpp: the contract and its defaults, before reading any override.
  4. GlStockShaderSources.hpp and GlPresentationSurfaceState.hpp: EasyGL's single GLSL ES 3.00 stock-shader corpus (adapted per profile) and its presentation transform, before reading a GL draw or present path.
  5. run_gpu_tests_private.sh: how GPU tests are run without touching the desktop.

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

Internals
EasyGL · Vulkan · SDL_gpu · Software · Headless · Stub