Tutorial 105: Browser Renderers: WebGL 2 and WebGPU
What you’ll learn
- What
WEBGL2andWEBGPUeach draw with in the browser, and how to choose between them. - The WebGL-specific divergences worth knowing.
- How to pick the browser renderer at run time, how much of this has run in a real browser, and the web caveats that apply no matter which one you pick.
Before you start — Tutorial 81: Building for the Web covers the Emscripten toolchain these renderers require.
Under Emscripten CNA offers two renderers. WEBGL2 (the Emscripten default) is a profile of the shared EasyGL implementation that also drives OPENGLES3 and OPENGL33 on the desktop; WEBGPU, the same identity CNA builds natively over wgpu-native, reaches the browser through Emscripten’s emdawnwebgpu port and is declared experimental (see Tutorial 132). OPENGLES3 and OPENGL33 are refused under Emscripten.
WEBGL2 is Emscripten-only and refuses to configure for a native target with a FATAL_ERROR, because a WebGL context only exists inside a browser. In both cases SDL3 still owns the page’s <canvas> element, keeps it sized and delivers input through it.
What each one draws with
| Renderer | Browser API | What you get |
|---|---|---|
WEBGL2 | A WebGL 2.0 context (EasyGL asks for a GLES 3.0-shaped context and the browser maps it to WebGL 2) | Full 2D and 3D, custom ShaderEffect in GLSL ES 3.00, render targets, MRT, instancing, occlusion queries (a 0-or-1 result), Texture3D. Feature-equivalent to OPENGLES3, minus what WebGL itself lacks (below). |
WEBGPU | WebGPU through emdawnwebgpu, presenting into the page’s #canvas | 2D and 3D with WGSL custom shaders, from the same renderer source as the native build. Declared experimental; it needs a browser with WebGPU enabled. |
WEBGL2: the GL family in the browser
WEBGL2 is the EasyGL renderer compiled with a different profile define, so most of Tutorial 102 applies unchanged: the same stock effects, the same capability answers, and the same rule that your own ShaderEffect source body is passed to the driver as written. WEBGL2 wants GLSL ES 3.00 (#version 300 es). A few things are specific to WebGL:
- No compute and no indirect draw in any WebGL version; the capability answers are
falsethere whatever the GPU underneath could do. - Wireframe needs
WEBGL_polygon_mode. Without that extensionGraphicsCapability::WireFramestaysfalse; EasyGL emulates wireframe for two bounded stock routes (non-indexedVertexPositionColortriangle lists, and indexed position-and-normal geometry), and every other wireframe draw is refused. - No sampler LOD bias. WebGL, like OpenGL ES, has no
GL_TEXTURE_LOD_BIAS, soSamplerState.MipMapLevelOfDetailBiassamples the natural mip level there; CNA’s fixtures measure exactly that answer on the ES profiles. - Two recorded divergences from Direct3D. WebGL never applies
MultiSampleMask(an all-ones mask is fine), and the channels a one- or two-channel render target does not store read back as 0 rather than D3D’s 1, because WebGL has no texture swizzle. - The back buffer is the canvas itself. On the desktop EasyGL draws into an off-screen framebuffer and blits it at
Present; on the web it draws straight into the canvas, whose size CNA manages. - Compiled XNA effects are opt-in with
-DCNA_EASYGL_COMPILED_EFFECTS=ON, as on the desktop. In this snapshot compiled effects that use volume samplers or sample float textures also build onWEBGL2(GLSL ES 3.00 precision fixes to the pinned MojoShader).
WEBGPU in the browser
WEBGPU is one renderer identity with two backends, not two renderers. The browser build reuses the native renderer and every WGSL shader; under Emscripten it links the emdawnwebgpu port (no WebGPU library to download or ship), creates its surface from the CSS selector #canvas, waits for WebGPU’s promises by yielding through Asyncify (CNA sizes the Asyncify stack for WEBGPU automatically), and lets requestAnimationFrame present each frame. CNA’s records describe cna_demo_2d, the 3D house demo and pages for every stock effect running in headless Chrome, and compiled XNA effects passing there with -DCNA_WEBGPU_COMPILED_EFFECTS=ON; those runs use the repository’s scripts/run-webgpu-browser-test.sh harness and are not part of CI. Treat the browser route as experimental, and check that your target browsers expose WebGPU at all. Tutorial 132 covers the renderer itself.
How much of this is actually exercised in a browser
| What | Browser verification |
|---|---|
WEBGL2 and WEBGPU in one bundle | emscripten-multi-renderer-ci.yml configures and links one WebAssembly bundle with both (-DCNA_GRAPHICS_RENDERERS="WEBGL2;WEBGPU") and asserts that every renderer and the JavaScript selection surface are in it; it does not run them, and the job checks sharp-runtime out on the branch that matches CNA’s. Separately, CNA’s records describe a manual run in headless Chrome (October 2026) in which its renderer benchmark completed under each of the two, chosen through Module.cnaPreferredRenderer; that run is not automated. |
WEBGL2 | CNA records a C API build obtaining a real WebGL 2 context in Chrome and completing 5-, 60- and 600-frame runs with exact update and draw counts and clean teardown. No CI job runs it in a browser. |
WEBGPU | The headless-Chrome harness runs described above; no CI job runs it in a browser. |
Web caveats that apply to both
Heap-allocate your Game? Not any more. Alpha.1 built the web main loop on emscripten_set_main_loop with an infinite-loop simulation, which unwound the caller and destroyed a stack-local Game under the running callback. In this snapshot Game::Run() blocks on the caller's own stack through Asyncify (EM_ASYNC_JS awaiting requestAnimationFrame), so an ordinary local MyGame game; game.Run(); is supported, exactly as on native platforms. CNA-owned application executables opt into Asyncify automatically; an external final executable that uses a blocking Game::Run() must link the CNA::EmscriptenAsyncify CMake interface target.
Saves persist through IndexedDB. CNA’s storage module mounts IndexedDB-backed IDBFS at /cna-storage (StorageDevice) and /save (IsolatedStorage) and restores both before main(), so saves survive a page reload. A threaded build uses WasmFS by default, which cannot host IDBFS, and StorageDevice reports storage as unavailable there unless you configure -DCNA_EMSCRIPTEN_USE_WASMFS=OFF. See Tutorial 19.
Video links but does not play. The Video and VideoPlayer types exist and link on the web (FFmpeg is never built for Emscripten), and any attempt to decode throws System::NotSupportedException at run time. Alpha.1's “fails to link” behaviour is gone.
Picking the renderer from the page
When one WebAssembly bundle links both browser renderers (the multi-renderer build below), the page chooses between them without recompiling. The property Module.cnaPreferredRenderer is consulted at the same precedence as the CNA_GRAPHICS_RENDERER environment variable (an actual environment variable wins over it); an explicit GraphicsRendererSelection::SetPreferred() call wins over both:
<script>
// Must be defined before the Emscripten glue script loads.
var Module = { cnaPreferredRenderer: "WEBGPU" };
</script>
The exported Module._cna_set_preferred_renderer(name) is an alternative that behaves like SetPreferred(). Names are the 14 canonical spellings, matched case-insensitively at run time; a name that does not name a renderer, or names one that is not linked into the bundle, throws instead of falling back. Selection latches when the first GraphicsDevice is successfully created (see Runtime Renderer Selection). A page can therefore try WEBGPU first and offer WEBGL2 to players whose browser has no WebGPU.
Building
emcmake cmake -S . -B build-web -DCNA_GRAPHICS_RENDERER=WEBGL2 # the default
emcmake cmake -S . -B build-webgpu -DCNA_GRAPHICS_RENDERER=WEBGPU # experimental
cmake --build build-web --parallel
# One bundle with both browser renderers (WEBGL2 stays the default)
emcmake cmake -S . -B build-web-multi \
-DCNA_GRAPHICS_RENDERER=WEBGL2 \
-DCNA_GRAPHICS_RENDERERS="WEBGL2;WEBGPU"
WEBGL2 needs the ../easy-gl and ../meta-gl sibling checkouts, as on the desktop; WEBGPU needs neither.
Choosing between them
| You need | Pick |
|---|---|
| 3D, custom shaders, or simply the safe default | WEBGL2 — see Tutorial 102 |
To experiment with WebGPU in the browser, with WEBGL2 as the fallback in the same bundle | WEBGPU — see Tutorial 132 |
Where to go next
- Tutorial 81: Building for the Web
- Tutorial 102: The OpenGL family
- Tutorial 124: Web gotchas
- Tutorial 126: Build and select several renderers
- Tutorial 132: WebGPU
- Tutorial 101: Querying renderer capabilities
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- The web target: Emscripten build contract, browser loop, storage and renderer evidence — CNA's Emscripten build contract (exception ABI, Asyncify, threads), the Asyncify browser loop, content and IndexedDB-backed save storage in the virtual file system, web networking, and the evidence per browser renderer.