Tutorial 81: Emscripten: Building for WebAssembly
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_BACKEND=EASYGL \
-DCMAKE_BUILD_TYPE=Release
cmake --build build-wasm -j$(nproc)
# Output: build-wasm/MyGame.html, MyGame.js, MyGame.wasm
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.
EASYGL backend on WebGL 2
The EASYGL backend 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.
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>
// CNA calls this internally, but you can also use it directly:
static MyGame* g_game = nullptr;
void EmscriptenMainLoop() {
g_game->RunOneFrame(); // CNA provides this method
}
int main() {
static MyGame game;
g_game = &game;
game.Initialize();
game.LoadContent();
emscripten_set_main_loop(EmscriptenMainLoop, 0, 1);
// Control never returns here in the browser
return 0;
}
#else
int main() {
MyGame game;
game.Run();
return 0;
}
#endif
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.