Worker Execution Model
Overview
SiliconGhetto uses a worker-first execution model where the WASM game module runs in a Web Worker, not on the browser’s main thread. This keeps the page responsive and dedicates a full CPU core to the game loop.
Architecture
┌─────────────────────────────────┐ ┌─────────────────────────────────┐
│ Main Thread │ │ Worker Thread │
│ │ │ │
│ HTML Shell │ │ WASM Module │
│ ├─ <canvas id="sg-canvas"> │────→│ ├─ OffscreenCanvas (received) │
│ │ (transferred to worker) │ │ ├─ wgpu Surface │
│ │ │ │ ├─ sg_app game loop │
│ ├─ Input Event Listeners │ │ ├─ sg_scene ECS world │
│ │ keydown, keyup │────→│ ├─ sg_render pipeline │
│ │ mousemove, mousedown │ │ └─ sg_input state │
│ │ touchstart, touchmove │ │ │
│ │ │ │ requestAnimationFrame │
│ ├─ Overlay UI │ │ ├─ process input messages │
│ │ <div id="perf-overlay"> │←────│ ├─ fixed_update (physics) │
│ │ HUD elements │ │ ├─ prepare_render │
│ └─ Accessibility layer │ │ ├─ GPU submit │
│ │ │ └─ postMessage (stats) │
└─────────────────────────────────┘ └─────────────────────────────────┘
Startup Sequence
- HTML page loads — minimal shell with a
<canvas>element - Transfer canvas —
canvas.transferControlToOffscreen()creates an OffscreenCanvas - Create worker —
new Worker("worker.js", { type: "module" }) - Send canvas —
worker.postMessage({ canvas }, [canvas])(transferable) - Worker initializes — loads WASM, creates wgpu surface from OffscreenCanvas
- Game loop starts —
requestAnimationFramein the worker drives the frame loop
Input Forwarding
Input events are captured on the main thread and forwarded to the worker:
// Main thread
document.addEventListener("keydown", (e) => {
worker.postMessage({ type: "keydown", code: e.code });
});
// Worker thread
self.onmessage = (e) => {
if (e.data.type === "keydown") {
engine.handle_key_down(e.data.code);
}
};
Input events are small and infrequent relative to frame rate, so postMessage overhead is negligible.
Fallback: Main Thread Mode
When OffscreenCanvas is unavailable (older browsers), the engine falls back to main-thread rendering:
- WASM module initializes directly with the canvas element
- Game loop uses
requestAnimationFrameon the main thread - Input events are handled directly (no postMessage)
- Performance may degrade under heavy DOM activity
The sg_worker crate handles capability detection and transparent fallback.
Communication Patterns
Main → Worker
| Message | Purpose | Frequency |
|---|---|---|
canvas | Transfer OffscreenCanvas | Once (startup) |
keydown/keyup | Keyboard input | Per key event |
mousemove | Mouse position | Per pointer move |
resize | Canvas size change | On window resize |
focus/blur | Visibility state | On tab switch |
Worker → Main
| Message | Purpose | Frequency |
|---|---|---|
perf | FPS and frame time stats | Every 60 frames |
ready | Initialization complete | Once |
error | Fatal error report | On failure |
SharedArrayBuffer (Future)
With cross-origin isolation enabled (COOP/COEP headers), SharedArrayBuffer becomes available. This enables:
- WASM threads: Multiple threads sharing the same linear memory
- Lock-free input: Input state written by main thread, read by worker without postMessage
- Parallel systems: ECS systems running on multiple threads
This is a v0.3+ feature. The current architecture is designed to support it without structural changes.
Browser Compatibility
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| Web Workers | Yes | Yes | Yes | Yes |
| OffscreenCanvas | 69+ | 105+ | 16.4+ | 79+ |
| transferControlToOffscreen | 69+ | 105+ | 16.4+ | 79+ |
| SharedArrayBuffer | 91+ | 79+ | 15.2+ | 91+ |
See compatibility.md for full browser support details.