Evidence tiers of the native modern GPU renderers

CNA snapshot 3b43bd86  ·  Deep Dives › Renderers  ·  source links pinned to 3b43bd86

✓

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 3b43bd86; unless a sentence says otherwise, nothing here was built or executed. The tables record registrations, workflow files and CNA's own notes; no renderer test was executed for this page, and CNA's declared maturity levels are declarations, not measurements.

Two of CNA's 10 renderer identities drive a modern explicit GPU API: SDL_GPU, CNA's modern native path, under which SDL selects its Vulkan, Direct3D 12 or Metal GPU driver, and WEBGPU. They share a vocabulary (immutable pipelines, visible resource transitions, shader delivery as a build concern) but not an implementation, and a successful device creation says little about which CNA features a given run actually proves. This page compares what each identity implements at this snapshot with the evidence behind it, explains why a capability bit is not evidence, describes the explicit-refusal pattern, and collects the defect shapes these renderers exposed. It is for anyone choosing between them, porting to one of them, or reading a test result from one of them.

Two identities, two implementations

The table states what the code does at this snapshot, not only what the capability bits say. "Custom shader intake" is what a ShaderEffect accepts; the stock effects are always the renderer's own precompiled or embedded shaders.

IdentityDeclared maturity and categoryDrivesStock shadersCustom shader intakeRenderTarget2D / MRTOcclusion queryBack-buffer colour policy
SDL_GPUSupported; translation layerSDL3's GPU API; SDL picks its Vulkan, Metal or Direct3D 12 driverGLSL compiled offline to SPIR-V in SDL_gpu's set convention; translated by SDL_shadercross for SDL’s Direct3D 12 and Metal driversSPIR-V (descriptors remapped at run time) on SDL’s Vulkan GPU driver; Vulkan-rule GLSL only in a build that found a target-native libshaderc. A Windows, Apple or Emscripten build has no usable route, because SDL’s Direct3D 12 and Metal GPU drivers cannot consume the raw SPIR-V, and CustomEffects answers false thereyes / up to 4refused: creation throws NotSupportedExceptionSDR swapchain composition; readback normalises BGRA
WEBGPUSupported; translation layerwgpu-native natively, navigator.gpu in a browserWGSLWGSLyes / 2 to 4real; a query's later runs in one flush take fresh slots and are summed; 32 query slotsprefers non-sRGB; reinterprets an sRGB-only surface

Maturity and category are CNA's own code-owned declarations, not measurements. Both keep XNA's SurfaceFormat::Color free of an automatic sRGB encode: SDL_GPU asks SDL for SDR swapchain composition, and how WEBGPU arrives there is explained in WebGPU surface format. The user-facing summary, including platform gates and dependencies, is in Rendering backends: modern GPU APIs.

A capability bit is a promise, not evidence

Three separate things are easy to conflate:

  1. A capability answer. GraphicsDevice::SupportsCapability says what the renderer claims. For most of the 17 members the shared base default is true (the exceptions are MultiStreamVertexInput, the two float render-target members and CompiledEffects, which a renderer must opt into), so a true answer can mean either an authored yes or a default nobody revisited, while a false answer is always authored (Tutorial 101 explains the asymmetry). SDL_GPU answers from a switch that names every member and returns false after it, so nothing falls through to the default, while WEBGPU overrides the members it probes (MultiStreamVertexInput, MultiSampleAntiAliasing, HalfFloatTextureLinearFiltering) and lets the rest fall through to the shared default.
  2. A device-level answer versus a renderer-level answer. On SDL_GPU the renderer's own switch answers the two float render-target members false, while GraphicsDevice derives them from render-target format probes (Vector4 and HdrBlendable) and can answer true; CompiledEffects, HalfFloatTextureLinearFiltering, MultipleRenderTargets and Texture3D are likewise derived or narrowed by the device for every renderer (GraphicsDevice.cpp). The device-level answer is the public one; a caller holding the renderer interface directly sees different values.
  3. Evidence that the feature renders correctly. A test that creates a device and does not throw proves that the device path executes. It does not prove pixels, and neither does a capability bit. The pages for each renderer name which pixel assertions back which feature.

A capability bit therefore never promotes a no-throw smoke test into feature evidence. Native and browser WebGPU are distinct evidence environments even though they share one identity and one implementation, and so are SDL's Vulkan, Direct3D 12 and Metal GPU drivers under one SDL_GPU identity.

What each identity's evidence actually covers

IdentityRenderer-specific testsWhere CNA's records place executionContinuous integrationNot established
SDL_GPUPixel, lifecycle and render-target programs in its own directory, fatal on SDL_gpu validation output, unit tests of its SPIR-V binding, sampler cache and texture lifetime, plus the parity corpusSDL_gpu's Vulkan driver on Linux: CNA records a full Debug run of an SDL_GPU tree on RADV on 17 October 2026 with 11,284 tests, none failed and 256 skipped; a cross-compiled run of SDL_gpu’s Direct3D 12 driver under Wine with vkd3d-protonThe SDL_GPU legs of the platform and input matrices and of the glTF-conformance matrix (Vulkan on lavapipe); a gating macOS smoke job on SDL’s Metal driver and non-gating iOS legs in the Apple workflow, and a Direct3D 12 smoke leg in the manual Windows workflow, none of which had a recorded run at this snapshotNative Windows presentation through SDL’s Direct3D 12 driver; any run on Apple hardware at this snapshot; an iOS build; Android; occlusion queries (SDL_gpu has none); ShaderEffect on Windows, Apple and the browser, where it is compiled out
WEBGPUA WebGPU-labelled test set, a native smoke test, pure-CMake checksum and present-mode mapping tests, plus the parity corpusNative on Linux x86_64 (CNA records a full Debug run of a WEBGPU tree on 17 October 2026 with 10,964 tests, none failed and 256 skipped); browser pages driven in headless Chrome by CNA's scriptsThe WEBGPU legs of the platform and glTF-conformance matrices (wgpu-native's Vulkan driver on lavapipe); built, not run, in the Emscripten bundle workflow (WEBGL2;WEBGPU)The other pinned native packages (Windows, macOS, Linux aarch64); cross-browser behaviour

No test registered for these two renderers compares a scene with CNA's 39-scene real-XNA reference corpus. The only complete exact comparison registered as a test is DIRECTX9's; EasyGL gates two scenes at tolerance 0, SOFTWARE registers the same two but fails only on a render failure or a missing reference (CNA-BUG-206), and FNA3D gates only that the corpus renders (see Verification: renderers). CNA's multi-renderer corpus script can render the corpus through any renderer of a multi-renderer build, but no such result for these two is part of the evidence described here. The shared cross-renderer parity corpus, 32 fixtures registered for EasyGL, WEBGPU and SDL_GPU (cross-renderer-parity-fixtures.md), compares each renderer against the fixture's own computed expectations, not against XNA. None of these tests was executed for this page; the table records registrations, workflow files and runs recorded by CNA (handoff.md, sections 2 and 5.2).

SDL_GPU is the default renderer on Windows, macOS and iOS, and its evidence is narrowest exactly there: CNA’s own handoff lists it as not qualified on those systems (a native Windows run on SDL's Direct3D 12 GPU driver, a run on Apple hardware and any iOS build are all still to be done). A default is a selection rule, not a qualification; WEBGPU is likewise declared Supported on the strength of Linux x86_64 and headless Chrome alone. The macOS and iOS picture is in Tutorial 109 and Android and Apple platforms.

Explicit refusal as an integration pattern

Because the shared capability default is permissive, a renderer can overclaim by doing nothing. The safer pattern is to enumerate the unsupported surface, throw at the boundary with a message that names the reason, and widen it only when a platform-specific observation proves the new path. SDL_GPU does this for occlusion queries: CreateOcclusionQuery() throws "CNA SDL_GPU: OcclusionQuery is unavailable because vendored SDL_gpu 3.5.0 exposes no occlusion-query or query-pool commands; GPU fences report only command-buffer completion and cannot count samples that pass depth/stencil." (SdlGpuRenderer.cpp), and a ShaderEffect built without a target-native GLSL compiler fails with "SDL_GPU ShaderEffect is unavailable: this build has no target-native GLSL-to-driver shader compiler" while CustomEffects answers false. Callers get an early diagnostic instead of a later blank frame.

The same pattern appears across this group wherever a feature cannot be delivered:

  • SDL_GPU throws from CreateOcclusionQuery() because SDL_gpu has no query commands, instead of returning a null query whose Begin and End would silently do nothing.
  • SDL_GPU refuses a MultiSampleMask that keeps only some of a multisampled target's samples with System::NotSupportedException, because SDL_gpu reserves its sample-mask field; it applies the two masks it can reproduce exactly (every sample, or none, which drops the draw), and RendererFeature::MultiSampleMask answers Restricted, rather than ignoring the mask as it did until 24 October 2026.
  • WEBGPU refuses cube faces in a multiple-render-target set with System::NotSupportedException while MultipleRenderTargets stays true, a documented capability boundary rather than a silently dropped slot (SDL_GPU binds them since 24 October 2026).
  • WEBGPU refuses a draw that samples or blends a 32-bit float target on an adapter without the needed optional feature, instead of quietly overwriting.

The opposite, silent substitution, is the most expensive defect shape in this group, because nothing fails: WebGPU once sampled a 1 × 1 white cube when a RenderTargetCube was bound as an environment map.

Record the identity and the native driver

For portability work, a result needs two coordinates: which CNA identity ran, and which native driver or device path served it. An abstraction that selects SDL's Vulkan GPU driver on one machine and its Metal GPU driver on another does not turn one successful run into proof of both.

  • SDL_GPU: GetDriverNameEXT() returns SDL's driver name, and the renderer logs the backend SDL chose at construction. CNA's own tests force a driver with SDL's SDL_GPU_DRIVER environment variable (an SDL setting, not a CNA option). A Vulkan result says nothing about SDL’s Direct3D 12 or Metal drivers, and a Direct3D 12 driver result under Wine is Wine evidence, not native Windows presentation.
  • WEBGPU: record native or browser, and for native which wgpu-native package; the browser's shader compiler is Tint and the native one is Naga, which can disagree about the same WGSL.
  • On Apple hardware: record the machine and whether the console was unlocked. GitHub’s hosted macOS runner exposes an “Apple Paravirtual device”, so a hosted SDL_GPU result on SDL's Metal GPU driver is evidence for that device only, and the iOS Simulator is not an iPhone.

Defect shapes these renderers taught

The defects recorded for these two renderers are mostly fixed at this snapshot. The shapes recur, and recognising them is the useful part:

  • State accepted, then discarded. An interface takes a complete state packet and the implementation consumes part of it. SDL_GPU's depth bias was captured but omitted from its pipeline key, and a WebGPU 3D draw once lost the BlendFactor it was queued with. Tests that vary each field independently catch it.
  • Silent substitution. A default resource replaces the one asked for (white textures, a white cube) with no error. The fix is a named refusal, and the test must use values that the substitute cannot produce, such as colours that encode an index.
  • Replay in implementation order instead of public order. Both SDL_GPU and WebGPU once replayed every 3D family before every sprite, justified as a typical draw order. A typical order is not a contract. The discriminating test issues the same two opaque draws in both orders and expects different winners. Uploads showed the same shape: until 24 October 2026 a SetData reached the GPU ahead of draws queued before it, so an Immediate SpriteBatch drew every sprite with the texture's final contents; both renderers now order such an upload after the queued draws that sample the texture (Immediate mode below the seam).
  • An oracle derived from the wrong contract. WebGPU's cull-mode mapping was inverted to satisfy a probe quad whose winding had been mis-derived. It looked like a measurement. Derive expectations from the reference contract (XNA and FNA) and confirm a driver-specific explanation on a second driver.
  • A review that looks instead of measuring. A manual screenshot review missed WebGPU's translucent sprites rendering opaque, and a pixel-value assertion caught it. Conversely, a captured screenshot caught SDL_GPU's upside-down sprites, which a "did it throw" test could not. Neither a no-throw check nor a visual review is a pixel assertion.
  • Validation as evidence with a scope. Turning on SDL_gpu's validation made two tolerated contract violations visible, each of which could hang SDL's Vulkan GPU driver, and then forced the design back to the public resource contract. A clean validation run proves the API usage it exercised, not the pixels.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Maintainer workflow
Fix a renderer bug
Tests and validation
Test architecture: GPU tests