WASM Script Runtime

How Aether executes WebAssembly scripts with per-script resource caps, priority scheduling, hot-reload, and server-side AOT compilation.

Aether runs all user-authored and engine scripts inside a sandboxed WebAssembly runtime. Scripts are compiled to .wasm modules, verified for integrity, and executed under strict resource limits so that no single script can starve the simulation loop. This guide covers the runtime architecture, resource policies, scheduling model, and server-side AOT pipeline.

Overview

The scripting system lives in the aether-scripting crate and is split into two layers:

  • Client runtime -- JIT-compiled WASM execution with fuel metering, SHA-256 integrity checks, and host API bindings. Used when scripts run locally on a player's device.
  • Server runtime -- AOT-compiled native artifacts optimized for deterministic, low-latency execution on Linux servers. Used by authoritative world instances that process hundreds of scripts per tick.

Both layers share the same host API surface but differ in compilation strategy and resource enforcement scope.

Key Concepts

Fuel Metering

Every script execution is assigned a fuel budget. Each WASM instruction consumes one unit of fuel. When fuel runs out, the runtime suspends the script and reports a MeteringOutcome::BudgetExceeded result. This prevents infinite loops and guarantees bounded execution time per tick.

Resource Caps

Resource limits are enforced at two levels:

LevelControls
Per-scriptCPU time, memory allocation, action rate (spawns, RPCs, storage writes)
World-levelTotal CPU budget per tick, aggregate memory, maximum scripted entity count

The world-level budget is typically 8 ms per tick. The scheduler divides this budget across all active scripts based on their priority.

Script Priority and Aging

Each script carries a priority value. The scheduler processes higher-priority scripts first. To prevent starvation, deferred (low-priority) scripts accumulate an aging bonus that gradually raises their effective priority over time.

Architecture

The runtime follows a pipeline from source bytes to sandboxed execution:

WASM Source Bytes
    -> Compilation (JIT on client, AOT on server)
    -> Integrity Verification (SHA-256 manifest)
    -> Module Cache
    -> Resource-Limited Execution
    -> Script Sandbox

On the server side, the ServerRuntime struct orchestrates the full pipeline:

use aether_scripting::server_runtime::{
    ServerRuntime, ServerRuntimeConfig, AotTarget,
    ServerResourcePolicy, SyscallPolicy,
};

// Create a server runtime with default configuration
let config = ServerRuntimeConfig::from_env();
let mut runtime = ServerRuntime::new(config)?;

// Compile a WASM module to a native artifact
let artifact = runtime.compile_aot(&wasm_bytes, AotTarget::LinuxX64)?;

// Register the artifact under a script ID
runtime.register_artifact(script_id, artifact)?;

// Load and execute with resource limits
let module = runtime.load_module(script_id, version)?;
let policy = ServerResourcePolicy {
    fuel_budget: 1_000_000,
    memory_cap_mb: 64,
    syscall_policy: SyscallPolicy::deny_all(),
};
let result = runtime.execute(&module, &policy)?;

Host API Surface

Scripts interact with the engine through six API traits. Each trait defines a set of callable functions that the runtime bridges into the host engine:

TraitCapabilities
EntityApiQuery, spawn, destroy, and modify entities
PhysicsApiApply forces, raycasts, overlap tests
UIApiShow/hide UI elements, update text
AudioApiPlay sounds, adjust spatial audio parameters
NetworkApiSend RPCs, broadcast events
StorageApiRead and write persistent key-value data

All API calls are rate-limited. For example, entity spawns and storage writes are capped per tick to prevent abuse:

use aether_scripting::{EntityApi, RateLimitedAction};

// The runtime enforces rate limits transparently.
// Exceeding the cap returns an ActionRateLimited error.
let entity = entity_api.spawn_entity(prefab_id)?;

Server-Side AOT Pipeline

Client-side JIT compilation introduces cold-start latency and non-deterministic performance. The server pipeline eliminates this with ahead-of-time compilation.

Compilation Targets

The AotTarget enum defines supported platforms:

pub enum AotTarget {
    LinuxX64,
    LinuxAArch64,
}

The AOT compiler uses Wasmtime's Engine::precompile_module to produce native artifacts. Each artifact includes a SHA-256 hash of both the source WASM and the compiled output for integrity verification.

Artifact Registry

The ArtifactRegistry stores compiled artifacts indexed by (script_id, version). Before loading a module, the registry verifies that the artifact hash matches the expected manifest:

// Register and verify an artifact
runtime.register_artifact(script_id, artifact)?;

// Loading verifies the manifest automatically
let module = runtime.load_module(script_id, version)?;

If the hash does not match, the registry rejects the artifact with a ManifestVerificationError.

Hot-Reload

Scripts can be updated without restarting the world. The hot-reload manager performs an atomic version swap:

  1. Upload a new artifact version.
  2. Verify its integrity against the manifest.
  3. Prepare a new ModuleSlot with the pending version.
  4. Atomically swap: mark the new version as active, the old as draining.
  5. After all in-flight executions on the old version complete, unload it.
let new_artifact = runtime.compile_aot(&updated_wasm, AotTarget::LinuxX64)?;
let outcome = runtime.hot_reload(script_id, new_artifact)?;

match outcome {
    ReloadOutcome::Success { old_version, new_version } => {
        log::info!("Reloaded script {script_id}: v{old_version} -> v{new_version}");
    }
    ReloadOutcome::Rollback { reason } => {
        log::warn!("Reload rolled back: {reason}");
    }
}

World-Level Orchestration

The TickScheduler in aether-world-runtime drives script execution each tick. The orchestration flow is:

  1. The scheduler collects all runnable scripts and sorts them by effective priority.
  2. It allocates the per-tick CPU budget across scripts proportionally.
  3. Each script executes within its fuel allotment.
  4. If the world-level budget is exhausted, remaining scripts are deferred to the next tick.
  5. An overload tracker monitors a 10-second sliding window. If average utilization exceeds the threshold, the scheduler force-suspends the lowest-priority scripts.

Overload Protection

When the world is under heavy scripting load, the runtime applies back-pressure:

  • Sliding window -- Tracks CPU usage over the last 10 seconds.
  • Suspension -- Scripts that consistently exceed their budget are suspended.
  • Graceful degradation -- Suspended scripts receive a callback opportunity to save state before being paused.

Configuration

All runtime settings are controlled through environment variables:

VariableDefaultDescription
AETHER_SERVER_AOT_CACHE_DIR/tmp/aether/aot_cacheDirectory for AOT artifact storage
AETHER_SERVER_MAX_MODULES256Maximum loaded modules per world
AETHER_SERVER_DEFAULT_FUEL1000000Default fuel budget per script execution
AETHER_SERVER_DEFAULT_MEMORY_MB64Default memory cap per script (MB)
AETHER_SCRIPT_MAX_OPS10000Max instructions per VM execution
AETHER_SCRIPT_MAX_VARS1024Max variables per script

Key Types

TypeDescription
ServerRuntimeTop-level orchestrator composing AOT, registry, limits, and hot-reload
AotArtifactCompiled native bytes with SHA-256 hash and target metadata
ArtifactManifestSHA-256 of source WASM, native artifact, version, and target
ServerResourcePolicyPer-script CPU time limit, memory cap, fuel budget, syscall restrictions
HotReloadManagerManages module slots and performs atomic version swaps
MeteringOutcomeWhether execution completed within budget or was terminated
SyscallPolicyAllowed and denied syscall categories (filesystem, network, clock)