Concept · Platform
WebAssembly & the Browser Build
When you press "Launch" on this site, a full 3D engine — streaming terrain, procedural forests, a day/night cycle — starts running inside your browser tab, with no plugin and no server doing the work. The technology that makes that possible is WebAssembly. This page explains what it is, how Rust code becomes it, and how FloraForge's browser build actually boots.
A compile target, not a language
WebAssembly — usually shortened to wasm — is a
compact binary instruction format that browsers can execute at close to
native speed. The crucial thing to understand is that nobody writes
WebAssembly the way people write JavaScript. It is a compile
target: you write a program in Rust, C++, Go, or another compiled
language, and the compiler emits a .wasm file instead of a
Windows or macOS executable. The browser then validates that file and
compiles it down to real machine code for whatever CPU it's running on —
which is why wasm performance lands so close to a native binary's.
That design solves a problem JavaScript never could. JavaScript is a dynamically-typed language that the browser must parse, interpret, and optimise on the fly; an engine doing millions of floating-point operations per frame fights that model constantly. A wasm module arrives already compiled and statically typed, so the heavy numeric work — noise functions, mesh building, physics — runs at essentially the speed the CPU allows.
The sandbox: linear memory
A wasm module is also radically contained. All of its data lives in one linear memory — a single contiguous, growable array of bytes that the module reads and writes with plain offsets. It cannot reach outside that array: no pointers into the browser's memory, no system calls, no file system, no network. Anything the module wants from the outside world must come through functions the host page explicitly hands it.
Those hand-offs are where JS glue comes in. For Rust, the
standard tool is wasm-bindgen: it scans the Rust code for
exported functions, then generates a small JavaScript module that loads the
.wasm file, wires up the imports it needs (DOM access, fetch,
the WebGPU API), and exposes the Rust functions as ordinary JavaScript
functions — marshalling strings and objects across the boundary in both
directions. The page calls the glue; the glue calls the wasm.
How FloraForge boots in a tab
FloraForge's release pipeline compiles the entire engine — the same crates
that make the desktop binary — with
cargo build --release --target wasm32-unknown-unknown, then
runs wasm-bindgen over the result to produce
web/pkg/world_gen.js and the wasm binary beside it. The landing
page's launch script is then disarmingly small:
const wasmEntryUrl = new URL("./pkg/world_gen.js", window.location.href);
async function ensureWasmModule() {
if (wasmModule) return wasmModule;
const module = await import(wasmEntryUrl.href);
await module.default(); // download + instantiate the .wasm
wasmModule = module;
return module;
}
async function launchGame() {
const module = await ensureWasmModule();
await module.start_web_app(); // hand control to the Rust engine
}
Two lines do all the work. module.default() is the
wasm-bindgen initialiser: it fetches the wasm bytes, has the browser
compile them, and wires up the imports. start_web_app() is not
a JavaScript function at all — it's a Rust function, exported across the
boundary by the #[wasm_bindgen] attribute. Here is its real
body, from the engine's crate root:
#[wasm_bindgen]
pub fn start_web_app() -> Result<(), JsValue> {
let canvas = web_sys::window()
.and_then(|w| w.document())
.and_then(|d| d.get_element_by_id("world-gen-canvas"))
.ok_or_else(|| JsValue::from_str("canvas element not found"))?;
let event_loop = winit::event_loop::EventLoop::new()?;
let window = WindowBuilder::new()
.with_canvas(Some(canvas.unchecked_into()))
.build(&event_loop)?;
app::run_event_loop_web(window, event_loop);
Ok(())
}
The hand-off is the <canvas> element: the page finds it
in the DOM, the Rust side wraps it as its "window", and from that point the
engine's normal
game loop
runs inside the tab, drawing through
WebGPU
exactly as it would on the desktop.
Loading a world without freezing the tab
One desktop luxury does not survive the trip: threads. FloraForge's browser build is single-threaded, and a wasm module shares the page's main thread — if the engine computes for three seconds straight, the tab is frozen for three seconds. Building a world from scratch is minutes of work, so the web build sidesteps it twice over.
First, it mostly doesn't generate the world at all. The release pipeline
runs the procedural generator
once in CI and deploys the result, world_base.bin (~31 MB),
right next to the wasm bundle. The app starts downloading it the moment the
page loads, before you even press Launch, and the request carries the
world's generation key as a query string so a stale cached snapshot is never
matched against a newer engine. Second, what work remains — decoding that
snapshot and rebuilding tens of thousands of plants from it — is
cooperative: it's split into small bounded steps, and the
engine yields back to the browser between every step so the loading screen
keeps animating. The driving type is a tiny state machine:
pub enum BuildStep {
/// Still working; `0.0..=1.0` is the fraction of the build completed.
Working(f32),
/// The runtime is ready.
Done(Box<WorldRuntime>),
}
An async task calls WebWorldBuilder::step() in a loop: each
Working result updates the progress bar and yields; the
Done result is the finished world. Once it arrives, the normal
chunk streaming
takes over — itself adapted for the single thread: where the desktop build
generates chunks on a worker pool, the web build's loader generates at most
two chunks per frame on the main thread, spreading the cost the same
cooperative way.
cfg(target_arch = "wasm32") switches
swap the edges: the windowing target becomes a canvas, file storage becomes
browser storage, threads become cooperative steps, and the graphics backend
becomes the browser's own WebGPU. Everything in between — terrain, biomes,
rivers, plant growth, the renderer — is one codebase compiled twice.