Skip to content

Live web demo ​

guido normally runs as a native runtime that your code drives over a socket, a child process, or the in-process C API. But the same engine also compiles to WebAssembly and runs directly in a browser tab — rendering real Godot Control nodes to a <canvas> with the WebGL2 (GL Compatibility) renderer, driven by the exact same JSON-RPC bridge.

The demo below is the counter example, built entirely over the wire. No project or PCK is shipped: when you click Run demo the page downloads the engine, boots it with --no-project, and constructs every node (VBoxContainer, Label, two Buttons) by sending bridge messages.

[ − ] 0 [ + ]

A real guido-rendered UI, compiled to WebAssembly, driven live over the JSON bridge.

Downloads a multi-megabyte engine only when you click.

Idle

Click the + / − buttons inside the canvas — each pressed signal arrives back over the bridge as a signal notification, JavaScript updates the count, and pushes a set on the label's text property. This is the same loop a Bun or Python host runs, only the transport endpoints are JavaScript functions.

How it works ​

On the web the wasm module is an executable (library builds are not supported on the web platform), so there is no guido_start host loop. Instead:

  • Emscripten drives the engine's own render loop (emscripten_set_main_loop → Main::iteration()), so the engine paces and renders itself and BridgeServer::poll_frame() runs every frame.

  • The build is started with --no-project --bridge-inproc, which brings up the bridge over the in-process transport (the same command queue the native in-process flavor uses).

  • Two C functions are exported to JavaScript so a driver can pump the protocol:

    JS callC symbolPurpose
    Module.ccall("guido_send", …)guido_send(const char*)queue one JSON-RPC line for the next frame
    Module.ccall("guido_recv", …)guido_recv()pop the next engine→host line (or null)

The JavaScript driver is tiny: send handshake, read rootId, instantiate each node, addChild / set to shape it, connect the button pressed signals, then drain guido_recv() once per animation frame and dispatch signal notifications. See the wire protocol for the message shapes.

Requirements ​

  • A browser with WebAssembly and WebGL2 (all current desktop and mobile browsers). If WebGL2 is unavailable the canvas stays blank and the status line reports the failure.
  • No cross-origin isolation is required: this is a single-threaded build (threads=no), so it needs no SharedArrayBuffer and no COOP/COEP response headers — it works from any static host.

Building the wasm yourself ​

The artifacts under docs/public/guido-web/ are produced by an Emscripten build of this repository. To reproduce them:

bash
# 1. Install the Emscripten SDK (no sudo needed), version 4.0.0+:
git clone https://github.com/emscripten-core/emsdk ~/emsdk
cd ~/emsdk && ./emsdk install latest && ./emsdk activate latest
source ~/emsdk/emsdk_env.sh

# 2. Build the single-threaded web template with the bridge module:
cd /path/to/guido
scons platform=web target=template_release \
  threads=no allow_memory_growth=no initial_memory=512 \
  lto=none optimize=size debug_symbols=no \
  disable_3d=yes deprecated=no \
  disable_physics_2d=yes disable_physics_3d=yes \
  disable_navigation_2d=yes disable_navigation_3d=yes \
  disable_xr=yes accesskit=no sdl=no \
  minizip=no brotli=no graphite=no \
  modules_enabled_by_default=no \
  module_freetype_enabled=yes module_text_server_adv_enabled=yes \
  module_svg_enabled=yes module_jpg_enabled=yes module_webp_enabled=yes \
  module_godot_bridge_enabled=yes \
  -j"$(nproc)"

This emits bin/godot.js (the engine loader, which defines window.Engine), bin/godot.wasm, and the audio worklet helpers (staged unzipped in bin/.web_zip/). Copy them into docs/public/guido-web/ as godot.js / godot.wasm (the demo loads /guido-web/godot.js). The bridge exports are wired in platform/web/detect.py (EXPORTED_FUNCTIONS += _guido_send, _guido_recv) and implemented in modules/godot_bridge/bridge_inproc.cpp under WEB_ENABLED.

allow_memory_growth=no (with a large initial_memory) is important: a growable WASM heap is backed by a resizable ArrayBuffer, and current Chrome refuses HEAP views over a resizable buffer in WebGL calls (texImage2D: "must not be resizable"). A fixed heap avoids that; the trade-off is that the tab reserves initial_memory MiB up front.

Bundle size

The engine is several megabytes of wasm. The demo component lazy-loads it only on click, so the docs page itself stays light.