Runtime lifecycle
This page ties Play’s runtime “processes” to concrete code locations. It is intended to reduce hand-wavy architecture discussions by pointing at the functions/modules that implement each step.
Entry points
Main thread:
index.html: static HTML shell + import map + mount elements.app/main.mjs: main entrypoint. UI wiring, renderer, backend, and plugin Host exposure.
Worker:
worker/physics.worker.mjs: module Worker entrypoint. forge/WASM load, MuJoCo stepping, and snapshot emission.
Backend module:
backend/backend_core.mjs: exportscreateBackend(...)which spawns the Worker and provides a higher-level API to the UI/plugins.
Boot sequence
HTML loads
app/main.mjsas ESM.URL/global config is consumed:
URL helpers live in
core/viewer_runtime.mjs. This includesconsumeViewerParams(...),buildWorkerUrl(...), and strict/verbose toggles.
Backend is created:
app/main.mjscallscreateBackend(...)frombackend/backend_core.mjs.The backend spawns
worker/physics.worker.mjsas a module Worker and begins the command/event handshake.
Forge dist is resolved and loaded in the Worker:
The Worker resolves a forge
dist/<ver>/base fromforgeBase=...or local fallbacks.It dynamically imports
mujoco.jsand loadsmujoco.wasm.
Ready:
The Worker emits
ready.The main thread finishes UI wiring and starts the render loop.
Command/event transport
The main thread and the Worker communicate via postMessage:
Commands: main → worker, shaped like
{ cmd: string, ...payload }.Events: worker → main, shaped like
{ kind: string, ...payload }.
Protocol source of truth:
tools/worker_protocol.json: JSON IDL.Generated runtime helpers:
worker/protocol.gen.mjs: lists, field specs, transfer fields
worker/dispatch.gen.mjs: encode, decode, dispatch
Snapshot pipeline
High-frequency state is delivered via the snapshot event.
Worker side:
Runs stepping internally via MuJoCo/WASM.
Emits
snapshotevents that often contain TypedArray views and uses transfer lists to avoid copies.
Main thread side:
Receives snapshots via the backend subscription API. - Merges snapshot fields into the viewer store:
mergeBackendSnapshot(...)is implemented/exported byui/state.mjs.app/main.mjsalso keeps alatestSnapshotfor quick access and exposes it aswindow.__lastSnapshotfor debugging.
UI ticks and “lanes”
Play intentionally decouples:
Worker stepping
snapshot delivery, adaptive and worker-driven
UI updates, throttled lanes for DOM work
The main entrypoint installs a small clock/subscription system that provides:
onSnapshot: snapshot-aligned work, state derivationonFrame: render-frame barrier, overlay commits and per-frame animationonUiTick/onUiMainTick: normal DOM updates, default ~30HzonUiControlsTick: slower lane for expensive control syncingonUiSlowTick: slow lane for heavy cards/tables
These hooks are exposed to plugins via the Host API. See Plugin contract.
Rendering pipeline
Rendering runs on the main thread and is driven by snapshots + viewer state.
Key modules:
renderer/pipeline.mjsexportscreateRendererManager(...): scene and render loop.renderer/controllers.mjsexports:createCameraController(...): camera interaction and synccreatePickingController(...): picking and selection
environment/environment.mjsexportscreateEnvironmentManager(...)for sky / environment handling.
The renderer:
Consumes snapshots: poses, scene SoA fields, render assets.
Updates Three.js scene objects and materials.
Flushes overlay3d commits at the render-frame barrier to keep overlays in sync with the MuJoCo scene.
Model loading
End-user:
Use
model=...to select a builtin alias or a path undermodel/. Uselocal_model/for local-only files.
Developer:
Use
host.backend.loadXmlText(...)for raw XML strings.Use
host.backend.loadXmlBundle(...)to provide XML + referenced assets as explicit files.
Related utilities:
core/xml_refs.mjscontains helpers for parsing MJCF file references and building virtual bundles.bridge/contains forge/WASM heap helpers and theMjSimLitewrapper for interacting with the module heap.
Plugin lifecycle
Plugins are dynamically imported ESM modules and are expected to export
registerPlayPlugin(host) or a default export.
Loading sources:
URL query parameter:
plugins=...Global:
globalThis.PLAY_PLUGINS = [...]. This must be set before the main module runs.
See Plugin contract for mounts, UI injection, overlay3d, and Worker boundary constraints.
Strict mode and diagnostics
Strict/verbose/perf helpers live in core/viewer_runtime.mjs:
strictCatch(...)/strictEnsure(...)/strictOverride(...)perfMark(...)/perfSample(...)/perfSummary()logging helpers:
logStatus,logWarn,logError
For developer-facing debug globals, see Runtime configuration.