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_loopMain::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.