Tutorial 132: The WebGPU Renderer: Native and Browser
What you’ll learn: how to build and verify the WEBGPU renderer natively (pinned wgpu-native, CNA_WEBGPU_ROOT) and in the browser (emdawnwebgpu), how to choose it at run time, and what it reports for WGSL shaders, compiled effects, float targets and compute.
Before you start — Tutorial 72: Choosing a Renderer for the group comparison, Tutorial 81: Building for the Web if you want the browser route, and Tutorial 131: The SDL_GPU Renderer, whose probe program this tutorial reuses. This tutorial is about one renderer, WEBGPU, in CNA snapshot 009d40f5.
Experimental, and measured on one native platform. CNA declares WEBGPU Experimental (a declaration, not a measurement). CNA's own renderer notes describe the native route as run on Linux x86_64; the pinned Windows, macOS and Linux aarch64 wgpu-native packages are hash-pinned but their build, link and run verification is recorded there as still open. The browser route has been driven in headless Chrome by CNA's own scripts, but no GitHub workflow selects WEBGPU, natively or under Emscripten. Everything below is checked against CNA branch next at commit 009d40f5dd085c4e674d3479675fac84b12b3e0a; the product version string is still 0.1.0-alpha.1, and a plain git clone gives you the older develop branch.
One identity, two routes
WEBGPU is a single renderer identity with two backends behind it. The same WebGPURenderer code and the same WGSL shaders serve both; only a handful of seams differ.
| Native route | Browser route (new since alpha.1) | |
|---|---|---|
| Implementation | Pinned wgpu-native v29.0.1.1, a prebuilt shared library CMake downloads and verifies | Emscripten's emdawnwebgpu port (--use-port=emdawnwebgpu); the browser owns the WebGPU implementation (navigator.gpu) |
| Hosts | Linux, macOS, Windows; x86_64 and aarch64 (asset table below) | Emscripten builds only; no wgpu-native library to download or ship |
| Surface | Built from the platform's native window handle: Win32 HWND, a Metal layer on Apple, an Android window, or Wayland / X11 on Linux | The <canvas> selected by #canvas — SDL3's default Emscripten canvas |
| Waiting for the GPU | Polls wgpuInstanceProcessEvents() with a 10-second timeout for adapter and device requests | Callbacks fire from the browser event loop; the renderer's synchronous waits yield with emscripten_sleep() under Asyncify |
| Presenting | Explicit wgpuSurfacePresent() | No explicit present: the frame is shown when control returns to requestAnimationFrame |
Rgba64 (RGBA16 UNORM) | Needs wgpu-native's own TextureFormat16bitNorm feature | Not available |
| Compiled-effect shader form | SPIR-V by default | WGSL only |
| Recorded evidence | Native smoke and test suite on Linux x86_64 per CNA's notes | 2D and 3D demos and every stock effect page in headless Chrome, by script |
In CNA's own category scheme WEBGPU is a translation layer: the native GPU API (Vulkan, Metal or Direct3D 12) is picked by wgpu-native or by the browser, not by CNA. It draws in 2D and 3D and is not one of the seven 2D-only renderers.
Native route: build and run
Prerequisites
cd /path/to/workspace
git clone -b next https://github.com/libcna/cna.git
git clone -b next https://github.com/libcna/sharp-runtime.git # must be its `next` branch too
cd cna
git submodule update --init # non-recursive is correct
You need the usual SDL-based build dependencies (see Building), an internet connection for the first configure (or an extracted wgpu-native package, below), and at run time a real GPU adapter. There is no easy-gl or other sibling requirement beyond sharp-runtime. On Linux the surface comes from the window's Wayland or X11 handle; any other native window kind stops with CNA WebGPU: unsupported Linux native window.
Configure and build
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=WEBGPU \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_C_COMPILER_LAUNCHER=ccache
cmake --build build --target cna_demo_renderer_selection cna_demo_2d cna_house3d_demo --parallel
The configure step prints CNA WebGPU: downloading https://github.com/gfx-rs/wgpu-native/releases/download/v29.0.1.1/… the first time, then CNA WebGPU: using wgpu-native v29.0.1.1 from …. The download is checked against a SHA-256 pinned in cmake/WebGPUChecksum.cmake before it is extracted; a mismatch removes the archive and stops the configure (fail closed), and an asset with no pinned hash is refused rather than trusted.
| Cache variable | Default | Meaning |
|---|---|---|
CNA_WEBGPU_VERSION | v29.0.1.1 | The pinned wgpu-native release. Changing it means supplying matching hashes. |
CNA_WEBGPU_ROOT | empty | Root of an extracted official wgpu-native release. When set, nothing is downloaded and the tree is not re-hashed. |
CNA_WEBGPU_AUTO_DOWNLOAD | ON | Download the pinned release when CNA_WEBGPU_ROOT is empty. With OFF and no root, configure stops. |
CNA_WEBGPU_COMPILED_EFFECTS | OFF | Build support for compiled XNA effect bytecode; see Shaders. |
The automatic download knows these packages (all from the gfx-rs/wgpu-native GitHub release, each with a pinned SHA-256):
| Host | Architectures with a pinned asset |
|---|---|
| Linux | x86_64, aarch64 |
| macOS | x86_64, aarch64 |
| Windows, MSVC | x86_64, aarch64 |
| Windows, MinGW | x86_64 only (the GNU package) |
Any other host or processor needs CNA_WEBGPU_ROOT. For an offline or reproducible build extract the official package yourself, which must provide include/webgpu/webgpu.h (or include/webgpu.h) and a libwgpu_native library under lib/:
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=WEBGPU \
-DCNA_WEBGPU_ROOT=/absolute/path/to/extracted/wgpu-native \
-DCNA_WEBGPU_AUTO_DOWNLOAD=OFF
Verify that WEBGPU really started
./build/cna_demo_renderer_selection # lists WEBGPU, then "Active renderer: WEBGPU"
./build/cna_demo_2d --smoke 120 # 2D sprite demo, exits cleanly after 120 frames
./build/cna_demo_2d --webgpu-2d-validation --smoke 120 # deterministic SpriteBatch coverage scene
./build/cna_house3d_demo # the 3D BasicEffect demo (built for WEBGPU)
CNA copies the wgpu-native library next to cna_demo_2d and gives the executable an $ORIGIN runtime path on Linux, so it runs straight from the build directory. The --webgpu-2d-validation scene uploads a texture and exercises source rectangles, tint and alpha, rotation, flips, linear and point filtering, and the three address modes, and resizes the window mid-run; it is a WebGPU-specific scene in the shared 2D demo.
For tests, a build with CNA_BUILD_TESTS=ON registers WebGPU_Native2D_Smoke, which runs cna_demo_2d --smoke 120 and reports a clear skip rather than a failure when neither WAYLAND_DISPLAY nor DISPLAY exists or the adapter cannot be obtained:
ctest --test-dir build -R '^WebGPU_Native2D_Smoke$' --output-on-failure
tools/platform/run_gpu_tests_private.sh build -L WebGPU --output-on-failure -j1 # the full WebGPU set
The wider set needs an X display with a real GPU adapter behind it; CNA's own notes say a plain software Xvfb has no WebGPU adapter and the readback tests fail for that reason alone. WebGPU_ChecksumVerification and WebGPU_PresentModeMapping are pure CMake or unit tests and need neither display nor GPU.
Ask the device what it can do
Reuse the probe program from Tutorial 131 unchanged — it compiles against every renderer and prints all 19 GraphicsCapability answers plus CNA's generated capability report. The only build difference for WEBGPU is that your own executable needs the wgpu-native library beside it; CNA provides a helper for that:
set(CNA_GRAPHICS_RENDERER "WEBGPU" CACHE STRING "CNA graphics renderer")
set(CNA_BUILD_TESTS OFF)
set(CNA_BUILD_EXAMPLES OFF)
add_subdirectory(../cna ${CMAKE_BINARY_DIR}/cna)
add_executable(webgpu_probe main.cpp)
target_link_libraries(webgpu_probe PRIVATE CNA)
cna_link_sharp_runtime(webgpu_probe PRIVATE)
cna_copy_renderer_runtime(webgpu_probe) # copies libwgpu_native next to your executable
Ship that library with your game: it is a separate shared library, not linked into the executable.
Browser route: build, serve, run
The source-tree prerequisites are the same as for the native route (sharp-runtime on next, git submodule update --init), plus an Emscripten SDK that provides the emdawnwebgpu port. CNA's build uses --use-port=emdawnwebgpu; CNA's notes say the older -sUSE_WEBGPU=1 built-in was removed in Emscripten 4.0.10 and is not used. (CNA's Emscripten CI pins emsdk 6.0.3 for its own bundles, which do not include WEBGPU.) The page must run in a browser that exposes navigator.gpu.
emcmake cmake -S . -B build-web -DCNA_GRAPHICS_RENDERER=WEBGPU -DCNA_BUILD_EXAMPLES=ON
cmake --build build-web --target cna_demo_2d --parallel
The configure prints CNA WebGPU: using Emscripten emdawnwebgpu port (browser navigator.gpu). Nothing is downloaded. Build the 3D BasicEffect demo with --target cna_house3d_demo the same way; both are gated to include WEBGPU.
WebAssembly does not load from file://. Serve the build directory over HTTP and open the generated page:
python3 -m http.server 8080 --directory build-web
# then open http://127.0.0.1:8080/cna_demo_2d.html in a WebGPU-capable browser
CNA's own automation of this is scripts/run-webgpu-browser-test.sh: it serves the build directory, drives headless Google Chrome with the demo's --smoke N argument, and treats a clean wasm exit with no WebGPU error as the pass. It defaults to the flags --enable-unsafe-webgpu --enable-features=Vulkan,WebGPU --use-angle=vulkan, which were chosen for CNA's Linux development machine (override with CNA_WEBGPU_CHROME_FLAGS):
scripts/run-webgpu-browser-test.sh build-web 120 # cna_demo_2d, 120 frames
CNA_WEBGPU_DEMO=cna_house3d_demo scripts/run-webgpu-browser-test.sh build-web 120 # the 3D demo
CNA's notes record these driven pages green in headless Chrome on that machine: the 2D demo, the 3D demo, the PBR, environment-map, skinned, dual-texture and alpha-test effect pages, and a coverage page for wireframe, semantic vertex layouts, multi-stream input, a custom WGSL ShaderEffect and device loss. That is a development-machine result, not a CI gate.
What is different in the browser
- The canvas is
#canvas. The WebGPU surface targets that CSS selector, so your page must use SDL3's default canvas id. - Blocking waits need Asyncify. Adapter, device and readback waits call
emscripten_sleep(). CNA-owned Emscripten executables opt into-sASYNCIFY=1automatically; an external final executable that uses the blockingGame::Run()linksCNA::EmscriptenAsyncify(the compatibility targetCNA::EmscriptenAbicontains both it andCNA::EmscriptenExceptionAbi). A localGameobject is fine: CNA's own notes say the alpha.1-era rule to heap-allocate the game in a browser build no longer applies. - Resize follows the canvas. The browser sizes the surface from the canvas backing store, and the renderer re-syncs its depth and MSAA attachments to the texture it actually acquires.
- Reading pixels back yields to the browser. A same-frame readback marks the acquired texture stale and re-acquires before the next draw; this is handled inside the renderer, but it is why heavy per-frame readback is slower here.
- The browser's shader compiler is Tint, not wgpu-native's Naga. A shader that one accepts can be rejected by the other, so test your WGSL in both routes.
Choosing WEBGPU at run time in a browser bundle
The default renderer under Emscripten is WEBGL2. If you build a multi-renderer bundle, the page can pick before the program starts — the same precedence as a native environment variable, below an explicit SetPreferred() call in your program:
emcmake cmake -S . -B build-web -DCNA_GRAPHICS_RENDERER=WEBGL2 \
-DCNA_GRAPHICS_RENDERERS="WEBGL2;WEBGPU" -DCNA_BUILD_EXAMPLES=ON
<canvas id="canvas" width="800" height="480"></canvas>
<script>
var Module = {
canvas: document.getElementById('canvas'),
cnaPreferredRenderer: 'WEBGPU', // a public renderer name; case-insensitive
print: function (t) { console.log(t); },
printErr: function (t) { console.warn(t); }
};
</script>
<script src="cna_demo_2d.js"></script>
The property is read once. A value that is not a CNA renderer name throws InvalidOperationException (does not name any CNA graphics renderer), and so does a real name that is not compiled into the bundle, with the available renderers listed — unless a fallback chain is enabled, in which case the rejection is recorded instead. An exported Module._cna_set_preferred_renderer is an alternative that behaves like SetPreferred() and returns 0 instead of throwing; it takes a C string, so it needs string marshalling on the JavaScript side. Inside the program, this is how to prefer WEBGPU and opt in to a second chance (the braced form SetFallbackChain({…}) does not compile as C++23, because a std::span cannot be built from an initializer list; pass a named array):
#include <array>
#include <iostream>
#include "CNA/GraphicsRendererFallbackRecord.hpp"
#include "CNA/GraphicsRendererSelection.hpp"
#include "CNA/GraphicsRendererType.hpp"
#include "MyGame.hpp" // your Game subclass
int main()
{
using CNA::GraphicsRendererSelection;
using CNA::GraphicsRendererType;
// Only ask for what this bundle actually contains.
if (GraphicsRendererSelection::IsAvailable(GraphicsRendererType::WebGPU))
{
GraphicsRendererSelection::SetPreferred(GraphicsRendererType::WebGPU);
// Opt in to a second chance; without it a failed WEBGPU start is an error.
static constexpr std::array chain{GraphicsRendererType::WebGL2};
GraphicsRendererSelection::SetFallbackChain(chain);
}
MyGame game; // your Game subclass; a local object is fine in a browser build too
game.Run();
std::cout << "active: "
<< CNA::getGraphicsRendererName(GraphicsRendererSelection::GetActive()) << '\n';
for (const auto& record : GraphicsRendererSelection::GetFallbackHistory())
{
std::cout << "skipped " << CNA::getGraphicsRendererName(record.type) << " ("
<< CNA::getGraphicsRendererFallbackReasonName(record.reason) << "): "
<< record.message << '\n';
}
return 0;
}
Untested combination. CMake allows WEBGPU beside the browser renderers (it belongs to no platform partition), but the only Emscripten multi-renderer bundle CNA's CI builds is WEBGL2;CANVAS;HTML_DOM;SVG_DOM, and that job builds and checks symbols rather than running the renderers. Check the configure line CNA: renderer set -- … (default: WEBGL2) and confirm with GraphicsRendererSelection::GetAvailable() and GetActive() rather than assuming. Fallback is off unless you enable it, and selection latches on the first successful device creation.
Shaders: WGSL, and compiled effects
Custom ShaderEffect shaders are WGSL. Unlike VULKAN and SDL_GPU (SPIR-V) and the GL family (GLSL), this renderer really compiles the source a ShaderEffect carries, and that source must be WGSL: the renderer's shader dialect is Wgsl, accepted at the vertex, fragment and compute stages, and nothing else. A custom effect whose fragment stage writes @location(0..N-1) fans out to every attachment of a multiple-render-target set. See Tutorial 52 for the ShaderEffect API.
Compiled XNA effects (.fxb, Direct3D 9 Effect Framework bytecode) are opt-in:
cmake -S . -B build -DCNA_GRAPHICS_RENDERER=WEBGPU -DCNA_WEBGPU_COMPILED_EFFECTS=ON
# offline / shared checkout of the pinned FNA3D (supplies MojoShader):
# -DFETCHCONTENT_SOURCE_DIR_FNA3D=/path/to/FNA3D
The option pulls the pinned FNA3D checkout for MojoShader, which this renderer does not otherwise need, and is buildable on every target. Natively the route emits SPIR-V by default. In a browser the shader module must be WGSL, so CNA translates the SPIR-V into WGSL (SpirvToWgsl); asking a browser build for anything other than WGSL throws NotSupportedException (browser WebGPU ingests WGSL and nothing else). With the option on, CompiledEffects reports yes; in a default configure it reports no. CNA's notes record a 13-check compiled-effect page passing in headless Chrome, which is what settled that the browser's Tint accepts the generated WGSL.
What it reports
Answers from GraphicsDevice::SupportsCapability() for a default configuration under the HiDef profile. "Probed" means the renderer asks the live device, so the answer can differ between machines and between native and browser.
| Capability | WEBGPU | Notes |
|---|---|---|
ThreeD, DepthStencilBuffer, StencilBuffer | yes | Every stock effect, including skinned and PBR, is implemented. |
MultiSampleAntiAliasing | probed | The renderer creates a scratch multisampled texture inside a validation error scope and reads whether it was rejected; the result is cached. |
MultipleRenderTargets | yes under HiDef | 2 to 4 RenderTarget2D targets that share width, height and sample count. Stock and SpriteBatch draws write attachment 0 only; a custom WGSL effect fans out. Under the default Reach profile the public answer is no. |
AnisotropicFiltering, Texture3D, Instancing, AdditiveBlending | yes | |
WireFrame | yes | Emulated by expanding each triangle's edges into a line list; the same route works natively and in the browser, where WebGPU has no polygon mode. |
OcclusionQuery | yes | Real begin/end queries counting the samples that pass depth and stencil (the test distinguishes zero from a near-full count; no exact count is asserted). Only a query's first contiguous run of draws per flush is recorded, and at most 32 query objects can be alive at once. |
MultiStreamVertexInput | yes | One native vertex-buffer layout per stream. One device slot is reserved, so the reported maximum is the smaller of the device limit minus one and CNA's own stream table. |
CustomEffects | yes | WGSL source. |
CompiledEffects | only with CNA_WEBGPU_COMPILED_EFFECTS=ON | See above. |
FloatRenderTargets, HalfFloatRenderTargets | probed | Derived from creating a 1x1 render-attachment texture of the exact format inside an error scope. See the float caveats below. |
HalfFloatTextureLinearFiltering | no | The renderer does not claim it. |
ComputeShaders | yes | Compute is core WebGPU; true once a device exists. |
IndirectDraw | only with IndirectFirstInstance | Answered from the device actually created, because the modern API's argument structs carry firstInstance, and a non-zero one needs that optional feature. |
Two optional adapter features matter for HDR. Float32Filterable is needed to sample a 32-bit float render target with a filtering sampler, and Float32Blendable to blend into one. When the adapter lacks them the renderer refuses that draw by name (NotSupportedException) instead of quietly overwriting: TextureFilter::Point samples such a target without the feature, and an opaque blend state avoids the blending restriction. Compare with GetRendererCapabilityReportEXT(), and see Tutorial 133 for the detailed profile.
Texture and render-target formats
- Block-compressed (
Dxt1/Dxt3/Dxt5, BC7 and their sRGB forms): stored and uploaded as native BC blocks only when the adapter offers the BC compression feature, which the renderer requests when available. On a device without it, the framework's own rule applies and content is decoded toColor. Cube maps follow the same rule. NormalizedByte2/NormalizedByte4: core WebGPU texture formats, stored natively — for textures only; neither is renderable, so aRenderTarget2Dasking for one is refused.- Render targets:
Coloris the swap-chain format and always supported; the float and half-float formats are probed against the device as above.Rgba64has no render-target mapping, andRgba64textures exist only in native builds with wgpu-native's 16-bit-norm feature. - Everything else follows the framework's
Color-only rule and throws.
Modern features
The "modern graphics" campaign for this renderer merged into this snapshot. It adds, for the CNAEXT engine layer and the modern renderer API:
- WGSL shader-package variants for the stock engine passes (generated from the same Vulkan GLSL as their SPIR-V), with a descriptor binding contract that mirrors Vulkan's.
- Compute shaders, storage buffers,
Texture2DArray, storage textures and GPU timers (timers only where the adapter supports timestamp queries), with every reported limit read from the device. - Indirect draws for stock families and custom effects (with the capability caveat above), shadow reception in the four stock lit families, and image-based lighting in the PBR families — both claimed by the renderer.
- Debug labels and per-instance vertex streams for a custom effect.
The renderer also takes part in the 32-fixture cross-renderer parity corpus, as WebGPU_Parity_<fixture> tests (with EasyGL, SDL_GPU and OPENGL4). Each fixture carries its own expected result; the oracle is the fixture's assertions, not real XNA output.
When it does not start
| Symptom | Cause and fix |
|---|---|
Configure: CNA_WEBGPU_ROOT is empty and CNA_WEBGPU_AUTO_DOWNLOAD=OFF | Extract an official wgpu-native v29.0.1.1 release and set CNA_WEBGPU_ROOT, or turn auto-download back on. |
Configure: unsupported processor or automatic wgpu-native download is unsupported on … | No pinned asset for this host. Set CNA_WEBGPU_ROOT to a package you vouch for. |
Configure: failed to download wgpu-native | Network problem. Download the asset manually and set CNA_WEBGPU_ROOT. |
Configure: SHA-256 mismatch | The archive was corrupt or tampered with; it was removed. Re-run to download again, or use CNA_WEBGPU_ROOT. |
Configure: webgpu.h was not found / libwgpu_native was not found | The root you gave is not an extracted official package (or an unexpected layout). |
Run time: CNA WebGPU: adapter request failed or timed out waiting for adapter request | No usable GPU adapter for the surface, for example a software-only X server, or a browser without WebGPU. CNA never substitutes another renderer; enable a fallback chain if you want that. |
Run time: unsupported Linux native window | The platform's native handle is neither Wayland nor X11. |
Emscripten rejects --use-port=emdawnwebgpu | The SDK does not provide the emdawnwebgpu port; use a newer SDK. |
NotSupportedException mentioning Float32Filterable or Float32Blendable | The adapter lacks the optional feature. Use point filtering, or avoid blending, on 32-bit float targets. |
NotSupportedException: browser WebGPU ingests WGSL and nothing else | Something asked a browser build to use a non-WGSL compiled-effect route. Leave the shader language at its default. |
A ShaderEffect fails to compile | The source must be WGSL, and Tint (browser) and Naga (native) can disagree. Check the compile error the effect reports. |
What backs these answers
- Declared maturity: Experimental. That is CNA's own label.
- Native: a native smoke test, a WebGPU-labelled CTest set, a pure-CMake checksum test, and the shared parity fixtures. CNA's notes say the native route was verified on Linux x86_64; the other pinned packages are still to be verified.
- Browser: scripted headless-Chrome runs of the demos and effect pages, as above.
- No XNA pixel oracle and no CI. Only
DIRECTX9is gated on the whole real-XNA reference corpus (at tolerance 0). EasyGL has a CTest on two line scenes that fails on any pixel difference, SOFTWARE registers the same two but its script fails only on a render failure, and FNA3D gates only that every scene renders (see the XNA oracle). WEBGPU is in none of them, and no GitHub workflow namesWEBGPU.
Where to go next
- Tutorial 133: Read the Renderer Capability Profile
- Tutorial 131: The SDL_GPU Renderer: Build, Verify and Query and Tutorial 130: Run CNA on Desktop OpenGL 4 (OPENGL4)
- Tutorial 105: Browser-native renderers and Tutorial 81: Building for the Web
- Tutorial 126: Build and Select Several Renderers
- Runtime Renderer Selection and the Renderers reference
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- WebGPU renderer semantics: surfaces, targets, mips and pipeline state — Exact behaviour of CNA's WEBGPU renderer: non-sRGB surface policy, target-relative SpriteBatch coordinates, cube render targets, blit-free mip generation, dynamic and baked state, ordered clears and BC textures.