Tutorial 81: Emscripten: Building for WebAssembly
What you’ll learn
- Installing the Emscripten SDK and pointing CMake at its toolchain file.
- Running the
WEBGL2renderer on WebGL 2, and the four other Emscripten-only renderers. - The three web caveats:
Gamemust be heap-allocated, saves do not persist, and video is absent. - Why the browser needs
emscripten_set_main_loop()instead of a blocking loop. - Customising the shell HTML and loading files asynchronously with Asyncify.
Before you start — Tutorial 80: Cross-Platform Build Guide (the WebAssembly build is one branch of that matrix) and Tutorial 05: The Game Loop (the loop shape that must change). Web builds default to the WEBGL2 renderer on WebGL 2.
Read this before you write a line of web-specific code. Three things about CNA on the web are not obvious, not optional, and not in the Emscripten manual.
1. Your Game subclass must be heap-allocated. A stack-allocated Game is silently corrupted under Emscripten and then fails frames later as an indirect-call fault — a crash with no obvious connection to its cause. Allocate it with new or std::make_unique and never let the object live in main’s frame. This is the single most expensive mistake to debug on this platform.
2. There is no save persistence at all. Under Emscripten SDL_GetPrefPath resolves to volatile MEMFS. CNA mounts no IDBFS and never calls FS.syncfs, so every save is silently discarded on page reload. Nothing throws and nothing warns — the write appears to succeed. If your game needs persistence in the browser you must implement it yourself, for example through localStorage or IndexedDB via EM_JS.
3. There is no video. CNA’s video translation units are excluded from web builds, so Video and VideoPlayer are missing symbols. The headers still exist, which means calling code compiles and then fails to link. The same is true on Windows and Android.
Installing Emscripten SDK
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh
# Verify:
emcc --version
CMake toolchain file
# Configure CNA for WebAssembly
cmake -S . -B build-wasm \
-DCMAKE_TOOLCHAIN_FILE=$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \
-DCNA_GRAPHICS_RENDERER=WEBGL2 \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm
# Output: build-wasm/MyGame.html, MyGame.js, MyGame.wasm
# CNA ships a "web" CMake preset that configures exactly this:
cmake --preset web
Do not pass --target CNA: CNA is an INTERFACE library with no sources and is not a buildable target.
SDL3 Emscripten target
SDL3 has native Emscripten support. CNA's vendored SDL3 submodule includes the Emscripten port. Pass
-s USE_SDL=3 in link flags or let the CMake integration handle it automatically when
CMAKE_TOOLCHAIN_FILE is set to the Emscripten toolchain.
The WEBGL2 renderer on WebGL 2
The WEBGL2 renderer maps OpenGL ES 3.0 calls directly to WebGL 2, which is supported in all modern browsers
(Chrome, Firefox, Safari, Edge). Set -s FULL_ES3=1 to enable the full ES 3.0 feature set.
Unsupported features in WebGL 2: compute shaders (not available in WebGL 2), and
glBlitFramebuffer with multisampling.
The other Emscripten-only renderers
Five of CNA's 50 renderer identities are gated to Emscripten and refuse to configure for a native target.
WEBGL2 is the default and WEBGL1 its lower profile — both are the shared EasyGL
implementation, and WEBGL1 genuinely loses MRT, occlusion queries, Texture3D,
instancing and multi-stream vertex input rather than merely relabelling a context.
The other three do not use WebGL at all and are 2D-only: CANVAS (HTML Canvas 2D),
HTML_DOM (CSS-composited DOM elements) and SVG_DOM (SVG nodes). HTML_DOM
has a dedicated automated browser workflow, alongside the separate Emscripten multi-renderer workflow. HTML_DOM and
SVG_DOM throw on a custom ShaderEffect — the honest behaviour, since they have
no programmable pipeline to run it on.
emscripten_set_main_loop()
Browsers require cooperative multitasking — you cannot block the main thread in an infinite loop.
CNA detects the Emscripten environment and automatically replaces the Game::Run() busy loop
with emscripten_set_main_loop. If you need to call this yourself, the pattern is:
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#include <emscripten/html5.h>
// The Game object must outlive main()'s frame, and must NOT live on the stack.
static MyGame* g_game = nullptr;
void EmscriptenMainLoop() {
g_game->RunOneFrame(); // CNA provides this method
}
int main() {
// HEAP, always. A stack-allocated Game is silently corrupted under
// Emscripten and faults frames later as an indirect-call error.
g_game = new MyGame(); // deliberately never deleted; see below
g_game->Initialize();
g_game->LoadContent();
emscripten_set_main_loop(EmscriptenMainLoop, 0, 1);
// Control never returns here in the browser
return 0;
}
#else
int main() {
// On native targets a stack-allocated Game is perfectly fine.
MyGame game;
game.Run();
return 0;
}
#endif
Leaking the Game deliberately is the right call: emscripten_set_main_loop with a
simulate_infinite_loop argument of 1 never returns, so any destructor you were
counting on would not run anyway, and the browser reclaims the whole heap when the page unloads.
Shell HTML template
Emscripten generates a default shell HTML page. Customize it by providing your own shell file:
# Link with custom shell
em++ -o MyGame.html MyGame.cpp \
--shell-file my_shell.html \
-s USE_SDL=3 -s FULL_ES3=1
The shell HTML must contain {{{ SCRIPT }}} where Emscripten injects the loader JavaScript.
Use a <canvas id="canvas"> element as the render target. SDL3 looks for an element
with the id canvas by default.
Async file loading (Asyncify)
Loading files asynchronously in the browser requires either Asyncify or preloading assets into the Emscripten virtual filesystem. Preloading is simpler and works well for games with a bounded asset set:
# Preload assets directory into WASM virtual filesystem
set_target_properties(MyGame PROPERTIES
LINK_FLAGS "-s USE_SDL=3 -s FULL_ES3=1 --preload-file assets@/assets"
)
For larger assets, use Asyncify to suspend C++ execution while the browser fetches a file:
set_target_properties(MyGame PROPERTIES
LINK_FLAGS "-s ASYNCIFY=1 -s USE_SDL=3 -s FULL_ES3=1"
)
Deployed examples
Two CNA demos are live on the web and were built with exactly this toolchain:
- CNA House 3D Demo — a real-time 3D house built with CNA's 3D rendering pipeline, deployed to GitHub Pages. Demonstrates Model loading, BasicEffect, and camera controls running at 60 fps in Chrome and Firefox.
- CNA Demo — the primary CNA feature showcase including 2D sprites, audio, and input, also deployed via GitHub Pages.
See the Demos page to run them directly in your browser.