Run in a browser
Every backend compiles to wasm32-unknown-unknown. Which one to use depends on what the game looks
like in the browser: a text UI inside a terminal emulator widget, or a canvas.
Text UI: terminal-wasm
retroglyph-terminal-wasm implements Backend
directly, like Headless: there’s no event loop in this crate at all. A browser terminal emulator
(xterm.js, or any other; the crate has no dependency on one) is driven from JS, which calls in once
per animation frame to pull freshly rendered ANSI bytes and push back whatever input it collected.
On wasm32 the crate exposes free functions (wasm_terminal_new, wasm_terminal_resize,
wasm_terminal_push_key, wasm_terminal_take_output, plus mouse/paste/focus variants) that drive a
TerminalWasm by opaque handle. Here’s the crate’s own reference driver for xterm.js in full:
import init, {
wasm_terminal_new,
wasm_terminal_resize,
wasm_terminal_push_key,
wasm_terminal_take_output,
} from './pkg.js';
// `code` values above 0x110000 select a named key; see this crate's `key_codes` module for the
// full list (arrows, Home/End, F1-F24, etc).
const NAMED_KEY_BASE = 0x00110000;
const KEY_ENTER = NAMED_KEY_BASE + 1;
const KEY_BACKSPACE = NAMED_KEY_BASE;
// `mods` is a bitmask: SHIFT = 1, CONTROL = 2, ALT = 4, SUPER = 8.
function decodeXtermData(data) {
if (data === '\r') return { code: KEY_ENTER, mods: 0 };
if (data === '\x7f') return { code: KEY_BACKSPACE, mods: 0 };
// A single printable character forwards as its Unicode codepoint; xterm.js already resolves
// Shift into the codepoint itself (e.g. 'A' vs 'a'), so no SHIFT bit is needed here.
if (data.length === 1) return { code: data.codePointAt(0), mods: 0 };
return null;
}
async function main() {
await init();
const term = new Terminal({ cols: 80, rows: 24 });
term.open(document.getElementById('screen'));
const handle = wasm_terminal_new(term.cols, term.rows);
term.onData((data) => {
const key = decodeXtermData(data);
if (key) wasm_terminal_push_key(handle, key.code, key.mods);
});
window.addEventListener('resize', () => {
// Call whatever fit-to-container logic resizes `term` first (e.g. xterm.js's FitAddon), then
// tell the backend to match.
wasm_terminal_resize(handle, term.cols, term.rows);
});
function frame() {
const ansi = wasm_terminal_take_output(handle);
if (ansi) term.write(ansi);
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
main();
That’s a wiring template, not a full game: it plumbs input/output through the FFI but calls no
per-frame drawing logic of its own; that’s still your Rust code, holding a Terminal<TerminalWasm>
the same way it would hold a Terminal<Headless> in a test.
A game built on retroglyph-core’s App trait usually wants this crate’s app_entry! macro
instead of driving TerminalWasm by hand: it generates a single-instance-per-page FFI surface that
owns the Terminal<TerminalWasm> and drives App::update for you, including a backgrounded-tab
delta clamp. See docs.rs for both.
Canvas: software, gl, or wgpu
All three windowed backends port to wasm32 unchanged via winit’s web backend: the same
run_windowed/run_app call that opens a native window targets a <canvas> element in the browser
instead, with gl speaking WebGL2 and wgpu speaking WebGPU on that target. See
Choose a backend for which of the three to reach for. One behavioral
difference to know about porting from native: on wasm32 the browser owns frame pacing (winit
services each requested redraw on the next requestAnimationFrame), so WindowConfig::fit’s
target_fps cap is a native-only optimization; an app that relies on it to throttle below the
display refresh rate will run uncapped once it’s running in a browser tab.
Building and packaging
Add the wasm32-unknown-unknown target and wasm-bindgen, then build your binary/example the same
way you would natively, aimed at that target:
rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli
cargo build --target wasm32-unknown-unknown --release --features software # or gl, wgpu, terminal-wasm
wasm-bindgen --target web --out-dir pkg target/wasm32-unknown-unknown/release/your_game.wasm
wasm-bindgen’s output is an ES module (pkg/your_game.js) plus the .wasm binary; serve them
over real HTTP (fetch()-ing a .wasm module is blocked from a file:// origin) alongside an HTML
page that calls the generated init() before anything else. tools/build-wasm-example.sh in this
workspace is a complete, working reference for exactly this build-and-package step, used to produce
the live examples gallery: every example in this repo runs
there in all four wasm-capable variants (headless text, terminal-wasm, software canvas, gl
WebGL2) with no local toolchain required to try one.
See also
- Choose a backend for the terminal-vs-canvas decision in full.
- Handle resize: a browser window/canvas resizes the same way a native one
does, through
Event::Resize.