Evidence tiers of the native modern GPU renderers
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 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.
Four of CNA's 25 renderer identities drive a modern explicit GPU API: VULKAN, SDL_GPU, WEBGPU and METAL. 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 among them, porting to one of them, or reading a test result from one of them.
Four identities, four 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.
| Identity | Declared maturity and category | Drives | Stock shaders | Custom shader intake | RenderTarget2D / MRT | Occlusion query | Back-buffer colour policy |
|---|---|---|---|---|---|---|---|
VULKAN | Production; native | Vulkan directly (instance API 1.1) | CNA GLSL compiled offline to embedded SPIR-V | SPIR-V words only; GLSL text refused | yes / up to 4 | real, per-frame reset; first contiguous run only; exact where occlusionQueryPrecise exists | prefers B8G8R8A8_UNORM |
SDL_GPU | Supported; translation layer | SDL3's GPU API; SDL picks Vulkan, Direct3D 12 or Metal | GLSL compiled offline to SPIR-V in SDL_gpu's set convention; translated by SDL_shadercross for Direct3D 12 and Metal | SPIR-V (remapped at run time); Vulkan-rule GLSL only where libshaderc was found | yes / up to 4 | refused: creation throws NotSupportedException | SDR swapchain composition; readback normalises BGRA |
WEBGPU | Experimental; translation layer | wgpu-native natively, navigator.gpu in a browser | WGSL | WGSL | yes / 2 to 4 | real; first contiguous run per flush only; 32 query slots | prefers non-sRGB; reinterprets an sRGB-only surface |
METAL | Supported; native | Metal directly (macOS only) | MSL embedded as a string, compiled with newLibraryWithSource: at run time | none: creation throws | yes / refused | refused: creation throws | BGRA8Unorm layer |
Maturity and category are CNA's own code-owned declarations, not measurements. Every one of the four keeps XNA's SurfaceFormat::Color free of an automatic sRGB encode; how Vulkan and WebGPU arrive there is explained in Vulkan: UNORM on purpose and 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:
- A capability answer.
GraphicsDevice::SupportsCapabilitysays what the renderer claims. For most of the original fourteen members the shared base default is true, 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).VULKAN,SDL_GPUandMETALanswer from switches without adefault:arm, so a member they do not name is false (METALnames thirteen of the nineteen), whileWEBGPUoverrides the members it probes and lets the rest fall through to the shared default. - A device-level answer versus a renderer-level answer. On
SDL_GPUthe renderer's own switch answers compute, indirect draw and the float render-target members false, whileGraphicsDevicederives them from dedicated queries and format probes and answers true. The device-level answer is the public one; a caller holding the renderer interface directly sees different values. - 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, and native and browser WebGPU are distinct evidence environments even though they share one identity and one implementation.
What each identity's evidence actually covers
| Identity | Renderer-specific tests | Where CNA's records place execution | Continuous integration | Not established |
|---|---|---|---|---|
VULKAN | The broadest renderer-specific pixel suite of the four, under an output gate that fails on Khronos validation messages | Linux; source comments cite the lavapipe software driver and an AMD RADV device | Built and run in the platform and glTF-conformance matrices (glTF on lavapipe); no workflow gates the full pixel suite | Real-XNA comparison; the 32-fixture parity corpus does not include it; MoltenVK and Android |
SDL_GPU | Pixel, lifecycle and modern-resource tests in its own directory, fatal on validation output, plus the parity corpus | SDL_gpu's Vulkan driver on Linux; a cross-compiled Direct3D 12 probe run under Wine with vkd3d-proton | No workflow selects it | Native Direct3D 12 presentation; any Metal run; a green classic suite (CNA's last recorded run lists 26 failing classic SdlGpu_* tests, CNA-BUG-099) |
WEBGPU | A WebGPU-labelled test set, a native smoke test, pure-CMake checksum and present-mode mapping tests, plus the parity corpus | Native on Linux x86_64; browser pages driven in headless Chrome by CNA's scripts | No workflow selects it | The other pinned native packages (Windows, macOS, Linux aarch64); cross-browser behaviour |
METAL | Metal_PortableHelpers, Metal_Smoke (clear and present) and Metal_Capabilities | macOS, in CI | metal-macos-ci.yml builds on a macOS runner and runs ctest -R '^Metal' with MTL_SHADER_VALIDATION and MTL_DEBUG_LAYER | Representative 3D draw pixels; its readback is refused, so pixel tests cannot run |
No test registered for these four 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 four is part of the evidence described here. The shared cross-renderer parity corpus, 32 fixtures registered for EasyGL, WEBGPU, SDL_GPU and OPENGL4, 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 and CNA's own notes.
The METAL row is a sharper statement than calling the renderer either complete or untested: its device path executes on a real Mac in CI, while much of its feature surface is deliberately outside the claimed evidence boundary (Tutorial 109: why the surface is this narrow).
Explicit refusal as an integration pattern
Because the shared capability default is permissive, a renderer can overclaim by doing nothing. METAL shows the safer pattern: enumerate the unsupported surface, throw at the boundary, and widen it only when a platform-specific observation proves the new path. Its refusals name their reason, for example "Metal backbuffer readback is disabled because the historical macOS test run returned clear-color-only data after real draws" and "Metal OcclusionQuery is disabled until command-boundary completion and slot recycling have adapted macOS proof" in MetalRenderer.mm. 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_GPUthrows fromCreateOcclusionQuery()because SDL_gpu has no query commands, instead of returning a null query whoseBeginandEndwould silently do nothing.VULKANthrows a named exception when a descriptor pool cannot be grown, instead of drawing with a substituted default texture (descriptor pools).WEBGPUrefuses 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: Vulkan once drew white sprites when its descriptor pool was full, and 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 Vulkan on one machine and Metal 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'sSDL_GPU_DRIVERenvironment variable (an SDL setting, not a CNA option). A Vulkan result says nothing about Direct3D 12 or Metal, and a Direct3D 12 result under Wine is Wine evidence, not native Windows presentation.VULKAN: the physical device matters as well as the API. Several behaviours are device-conditional (wireframe, anisotropy, precise occlusion counts, first-instance indirect draws, BC formats), and the applied present mode can differ from the request;GetAppliedPresentModeEXT()andOcclusionQuery::isPixelCountPreciseEXT()report what the device gave.WEBGPU: record native or browser, and for native whichwgpu-nativepackage; the browser's shader compiler is Tint and the native one is Naga, which can disagree about the same WGSL.METAL: the only macOS evidence is the CI lane above; CNA's own Metal page treats a fresh macOS run of the adapted Objective-C++ source as an external validation boundary.
Defect shapes these renderers taught
The defects recorded for these four 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. Vulkan's blend and stencil state were the canonical cases, and SDL_GPU's depth bias was captured but omitted from its pipeline key. 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.
- An oracle derived from the wrong contract. Vulkan's depth-bias test used OpenGL's depth convention and blamed the driver; WebGPU's cull-mode mapping was inverted to satisfy a probe quad whose winding had been mis-derived; an early Vulkan stencil repair swapped front and back faces on a hypothesis about one software driver. Each 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 the Vulkan 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.
- An arm that volume cannot reach. Descriptor-pool exhaustion never happened on the tested drivers even with thousands of live resources, so Vulkan's regression injects the failure. A test that only generates volume can pass while executing none of the code it claims to cover.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Rendering backends: modern GPU APIs · Verification: renderers · Tutorial 101: Querying renderer capabilities
- Architecture
- Graphics architecture
- Maintainer workflow
- Fix a renderer bug
- Tests and validation
- Test architecture: GPU tests
- Reference
- Test target index