Runtime Lifecycle
TL;DR: A WasmVMAnchor on a root GameObject holds the compiled WasmModuleAsset. At load, WasmVM.Setup creates a Store, instantiates the module, wires up event exports, then for each behaviour calls _createInstance and _deserializeInstance. Afterwards the host calls typed scripting_call_* exports on Unity/physics/game events.
Stages
Section titled “Stages”- Build time — Build Pipeline compiles author C# into
Assets/WasmModule.wasm, in-place replacesWasmBehaviourwithWasmRuntimeBehaviour, and writesWasmVMAnchormetadata (behaviourInfos,moduleAsset,definedGameEvents). - Asset load — When the world/avatar/prop loads, Unity instantiates the prefab.
WasmVMAnchor.Setup(and/orWasmRuntimeInitializeOnLoadMethod) kicks off VM construction. - VM setup —
WasmVM.Setupcreates the Store, instantiates, calls init exports, then calls per-behaviour_createInstance+_deserializeInstance. - Event dispatch — The Unity main loop (or engine events) calls into the VM, which invokes the matching guest export.
- Shutdown —
OnDestroydisposes the Store;WasmManager.OnDestroytears down the Engine/Linker at quit.
Setup (per VM)
Section titled “Setup (per VM)”From CVR-GameFiles/WasmScripting/WasmVM.cs (Setup, called on the main thread by MTJobManager.RunOnMainThread("InitializeVM", ...)):
_store = new Store(WasmManager.Engine);WasmManager.Linker.FillNonLinkedWithEmptyStubs(_store, _module);_instance = WasmManager.Linker.Instantiate(_store, _module);
WasmManager.StartEpochTimer(_store);_store.SetEpochDeadline(100000uL); // one-time startup deadline: ~1 s
_instance.GetAction("_initialize")?.Invoke(); // WASI init (if any)_instance.GetAction("scripting_initialize")?.Invoke(); // CVR runtime init
_createInstance = _instance.GetAction<long, int>("scripting_create_instance");_deserializeInstance = _instance.GetAction<long, int>("scripting_deserialize_instance");
InitializeEvents();WasmNetworkManager.RegisterVM(this);_store.SetData(new StoreData(rootTransform, _instance, this, NetworkId));
foreach (var b in behaviours) CreateInstance(b);foreach (var b in behaviours) DeserializeInstance(b);
WasmManager.StopEpochTimer();StoreData (CVR-GameFiles/WasmScripting/StoreData.cs) resolves the guest-side hooks scripting_raise_exception(code, msgPtr, msgLen) and scripting_alloc(size) -> ptr at this point and stashes the memory export. These are what the host uses to marshal strings and arrays and to surface exceptions.
The module is cached on disk by FNV-1a hash (WasmVM.LoadCachedWasmModule); a matching hash skips recompilation via Module.DeserializeFile (or Module.Deserialize under Wine/Proton, because the deserialize-file path is memory-mapped and that interacts badly with Wine’s loader).
Per-behaviour instantiation
Section titled “Per-behaviour instantiation”For each WasmRuntimeBehaviour on the root:
// 1) Create:long behaviourHandle = storeData.AccessManager.ToWrapped( behaviour, overrideScope: true, CVRScriptScopeContext.Self).Handle;_createInstance(behaviourHandle, info.behaviourId);
// 2) Deserialize:int stateArg = behaviour.serializationData.MarshalSerializedObject(storeData);_deserializeInstance(behaviourHandle, stateArg);info.behaviourId is an 8-bit index derived from the type name hash by WasmBuildUtility.GetHash. The guest uses it to dispatch to the right subclass.
Event export table
Section titled “Event export table”WasmVM.InitializeEvents looks up these guest exports and caches delegates. All names come from CVR-GameFiles/WasmScripting/WasmVM.cs (search GetAction/GetFunction):
Unity lifecycle
Section titled “Unity lifecycle”Single dispatcher: all Awake / Start / OnEnable / OnDisable / OnDestroy / render events go through scripting_call_event(handle, scriptEventId), where scriptEventId is the integer value from WasmScripting.Enums.ScriptEvent. The runtime calls _callEvent in WasmVM.CallScriptEvent. Awake and Start are therefore not separate exports.
The per-phase Update family uses its own exports, because they fan out to every active behaviour in a single host call:
| Guest export | Parameters | Dispatched from |
|---|---|---|
scripting_call_update(behavioursPtr, count) | pointer to long[count] handles | WasmVM.Update |
scripting_call_post_update(...) | same | LateEventsManager.OnPostUpdate |
scripting_call_late_update(...) | same | WasmVM.LateUpdate |
scripting_call_post_late_update(...) | same | LateEventsManager.OnPostLateUpdate |
scripting_call_fixed_update(...) | same | WasmVM.FixedUpdate |
scripting_call_post_fixed_update(...) | same | LateEventsManager.OnPostFixedUpdate |
Legacy scripting_call_awake(behavioursPtr, count) and scripting_call_start(behavioursPtr, count) are still looked up but the current runtime dispatches per-behaviour Awake / Start via the single scripting_call_event path.
Physics
Section titled “Physics”| Guest export | Meaning |
|---|---|
scripting_call_on_collision_enter(handle, collisionHandle) | OnCollisionEnter |
scripting_call_on_collision_stay(handle, collisionHandle) | OnCollisionStay |
scripting_call_on_collision_exit(handle, collisionHandle) | OnCollisionExit |
scripting_call_on_trigger_enter(handle, colliderHandle, colliderTypeId) | OnTriggerEnter |
scripting_call_on_trigger_stay(handle, colliderHandle, colliderTypeId) | OnTriggerStay |
scripting_call_on_trigger_exit(handle, colliderHandle, colliderTypeId) | OnTriggerExit |
colliderTypeId comes from WasmScripting.TypeMap.GetId(collider.GetType()) — lets the guest dispatcher pick the concrete subclass (BoxCollider, CapsuleCollider, etc.) without a follow-up reflection call.
Script events (from rewired UnityEvents or direct guest calls)
Section titled “Script events (from rewired UnityEvents or direct guest calls)”| Guest export | Meaning |
|---|---|
scripting_call_trigger_script_event(handle, namePtr, nameLen) | void method |
scripting_call_trigger_script_event_string(handle, namePtr, nameLen, argPtr, argLen) | string method |
scripting_call_trigger_script_event_int(handle, namePtr, nameLen, intArg) | int method |
scripting_call_trigger_script_event_float(handle, namePtr, nameLen, floatArg) | float method |
scripting_call_trigger_script_event_bool(handle, namePtr, nameLen, boolArg) | bool method (as i32 0/1) |
scripting_call_trigger_script_event_object(handle, namePtr, nameLen, objHandle, objTypeId) | object method |
The object variant also passes a TypeMap.GetId result so the guest can cast the wrapped handle back to the right concrete type. Method-name strings are written to guest memory as UTF-16 and sized in char count, not byte count.
Game events (fired once per WasmVM, fanned out inside guest)
Section titled “Game events (fired once per WasmVM, fanned out inside guest)”| Guest export | Signature | Meaning |
|---|---|---|
CVR_WasmBehaviour_OnPlayerEvent(behavioursPtr, count, playerHandle, scriptEventId) | int, int, long, int | OnPlayerJoined/Left/Respawned + single-behaviour dispatch for OnPlayerTrigger* / OnPlayerCollision* variants |
CVR_WasmBehaviour_OnPropEvent(behavioursPtr, count, propHandle, scriptEventId) | int, int, long, int | OnPropSpawned/Despawned |
CVR_WasmBehaviour_OnPropTriggerEvent(handle, propHandle, colliderHandle, scriptEventId) | long, long, long, int | per-behaviour prop trigger hits |
CVR_WasmBehaviour_OnPropCollisionEvent(handle, propHandle, collisionHandle, scriptEventId) | long, long, long, int | per-behaviour prop collision hits |
CVR_WasmBehaviour_OnPortalEvent(behavioursPtr, count, portalHandle, scriptEventId) | int, int, long, int | OnPortalCreated/Destroyed |
CVR_WasmBehaviour_OnInstanceOwnerChanged(behavioursPtr, count, playerHandle) | int, int, long | OnInstanceOwnerChange. playerHandle == 0 if the new owner hasn’t resolved yet. |
CVR_WasmBehaviour_OnInputReady(behavioursPtr, count) | int, int | OnInputReady |
CVR_Networking_OnReceiveMessage(senderHandle, msgPtr, msgLen) | long, int, int | incoming network payload, routed to the Networking.OnReceiveMessage delegate |
CVR_WorldPermissions_OnWorldPermissionsChanged(marshalStructPtr) | int | world VMs only; guest fans out to every behaviour that defined the method |
CVR_Input_GetInputPointer() -> i32 | () -> int | one-time lookup at VM init for the guest-owned CVRInputStruct slot the host writes each tick |
Per-call sequence
Section titled “Per-call sequence”For any event dispatch, the host:
- Calls
WasmManager.StartEpochTimer(_store)— sets the deadline and wakes the timer. - Invokes the cached delegate (a
scripting_call_*export). - If the call throws
TrapException(epoch overrun, WASM trap, or guest exception) the host catches it, setsIsCrashed = true, and stops dispatching further events on this VM. - Calls
WasmManager.StopEpochTimer().
Exception flow
Section titled “Exception flow”Host bindings wrap every delegate body in try / catch:
try { // ... access check + real work ...} catch (Exception exception) { WasmBindingUtils.RaiseException(data, exception);}RaiseException allocates the exception message in guest memory and calls the guest export scripting_raise_exception(code, msgPtr, msgLen) (resolved once at StoreData construction). WasmBindingUtils.ExceptionToId maps 74 .NET + Unity exception types (Argument, InvalidOperation, NullReference, FileNotFound, UnityException, WasmAccessDeniedException, …) to integer codes so the guest can rebuild idiomatic exceptions rather than carry a string-only error.
Shutdown
Section titled “Shutdown”WasmRuntimeBehaviour.OnDestroy (engine side) — calls VM.CallScriptEvent(this, ScriptEvent.OnDestroy) iff the behaviour defined OnDestroy.
WasmVM.OnDestroy — calls UnregisterEvents (unhooks every WasmGameEvents / LateEventsManager subscription + the optional WorldPermissionsManager.OnPermissionsChanged handler on world VMs), disposes the Store (frees guest memory), disposes the Module, and calls WasmNetworkManager.UnregisterVM.
WasmManager.OnDestroy — disposes Linker, Engine, Config on scene/application teardown. Also stops the epoch timer thread.
Source
Section titled “Source”CVR-GameFiles/WasmScripting/WasmManager.cs— sharedEngine/Linker, epoch timer.CVR-GameFiles/WasmScripting/WasmVM.cs— per-root store, instance, event dispatch.CVR-GameFiles/WasmScripting/WasmRuntimeBehaviour.cs— engine-sideMonoBehaviourthat bridges Unity callbacks to the VM.CVR-GameFiles/WasmScripting/StoreData.cs— guest-memory accessor +scripting_raise_exception/scripting_alloclookups.CVR-GameFiles/ABI_RC.Systems.WasmScripting/WasmScriptingBridge.cs— content-load plumbing that drops theWasmVMcomponent on an avatar/prop/world root when it arrives in the scene.
Related
Section titled “Related”- Events — full list of dispatchable events from the author’s perspective.
- Architecture — the static wiring diagram.
- Permissions — what
CheckAccesscan throw during any binding call.