WASM Modules

Advanced

High-performance WebAssembly modules written in Rust for accelerating performance-critical operations with SIMD optimizations.

Web Engine leverages WebAssembly (WASM) to accelerate performance-critical operations that benefit from native code execution. Our WASM modules are written in Rust and compiled using wasm-bindgen, providing near-native performance for transforms, physics, culling, and network compression.

Why WASM?#

Near-Native Performance

Execute compute-intensive operations at speeds approaching native C/C++

SIMD Acceleration

Leverage SIMD instructions for parallel data processing

Type Safety

Rust's memory safety guarantees prevent common bugs and crashes

Zero-Copy FFI

Efficient data sharing between JavaScript and WASM via TypedArrays

The WASM package (@web-engine-dev/wasm) contains optimized implementations for transform computation, physics simulation, frustum culling, octree spatial partitioning, and network batch compression.

WASM Architecture#

Web Engine's WASM modules follow a carefully designed architecture that maximizes performance while maintaining clean JavaScript integration:

  • Rust Source — All WASM code is written in Rust with explicit safety annotations
  • wasm-bindgen — Automatic JavaScript bindings generation with TypeScript definitions
  • TypedArray Interface — Zero-copy data sharing via shared memory buffers
  • SIMD Optimization — Vectorized operations using WebAssembly SIMD instructions
  • Batch Processing — Process thousands of entities in single WASM calls
  • Pre-built Binaries — Shipped with npm package, no Rust toolchain required

Build Optimizations

WASM modules are compiled with aggressive optimizations: -O3 (maximum optimization), --enable-simd (SIMD instructions), -ffm (fast float math), and LTO (Link Time Optimization) for minimal binary size and maximum runtime performance.

Core Modules#

Transform Module#

SIMD-accelerated transform hierarchy computation using the glam math library. Processes entity transforms in batch with O(n) complexity:

TransformSystem.ts
typescript
import init, {
update_world_transforms,
update_quaternions_from_euler,
compute_transform_matrices,
} from '@web-engine-dev/wasm';
// Initialize WASM module once at startup
await init();
// Update world transforms for entire entity hierarchy
// Processes parent-child relationships recursively in O(n) time
update_world_transforms(
localPositions, // Float32Array: [x, y, z] per entity
localRotations, // Float32Array: [x, y, z] Euler angles per entity
localScales, // Float32Array: [x, y, z] per entity
localQuaternions, // Float32Array: [x, y, z, w] per entity
parents, // Uint32Array: parent entity ID per entity
entityIds, // Uint32Array: entity IDs being processed
outWorldPositions, // Float32Array: output world positions
outWorldRotations, // Float32Array: output world rotations
outWorldScales, // Float32Array: output world scales
outWorldQuaternions // Float32Array: output world quaternions
);
// Convert Euler angles to quaternions (batch operation)
update_quaternions_from_euler(
rotations, // Float32Array: [x, y, z] Euler angles (radians)
quaternions, // Float32Array: output [x, y, z, w] quaternions
entityIds // Uint32Array: entity IDs
);
// Compute 4x4 matrices for instanced rendering
compute_transform_matrices(
positions, // Float32Array: world positions
rotations, // Float32Array: Euler rotations
scales, // Float32Array: scale vectors
quaternions, // Float32Array: quaternions (preferred)
entityIds, // Uint32Array: entity IDs
matrices // Float32Array: output 4x4 matrices (16 floats each)
);

Physics Module#

Physics simulation bridge using Rapier3D, Rust's high-performance physics engine. Provides rigid body dynamics, collision detection, and character controllers:

PhysicsSystem.ts
typescript
import { PhysicsWorld } from '@web-engine-dev/wasm';
// Create physics world
const physics = new PhysicsWorld();
// Bulk add bodies (single FFI call vs n calls)
physics.add_bodies_batch(
entityIds, // Uint32Array: entity IDs
positions, // Float32Array: initial positions [x, y, z]
isDynamicArray // Uint8Array: 0 = static, 1 = dynamic
);
// Update body properties in batch
physics.update_bodies_batch(
entityIds, // Uint32Array: entity IDs to update
masses, // Float32Array: mass per entity
frictions, // Float32Array: friction coefficients
restitutions // Float32Array: restitution (bounciness)
);
// Step simulation
physics.step(deltaTime);
// Sync physics state back to ECS (SIMD-accelerated)
const syncedCount = physics.sync_states_simd_bulk(
positions, // Float32Array: output positions
rotations, // Float32Array: output rotations
entityIds, // Uint32Array: entity IDs
velocities, // Float32Array: output velocities
angularVelocities, // Float32Array: output angular velocities
true, // use_velocities
true // use_angular_velocities
);
// Raycast with shapecast support
const hit = physics.raycast(
originX, originY, originZ,
directionX, directionY, directionZ,
maxDistance
);
if (hit) {
console.log(`Hit entity ${hit.eid} at distance ${hit.toi}`);
}

Spatial Hash Optimization

The physics module uses a custom spatial hash broad-phase designed for 100k+ collider scenes with sub-millisecond updates. It performs incremental updates and maintains cell-level sleeping to avoid waking inactive clusters.

Culling Module#

Accelerated frustum culling and octree spatial partitioning for visibility determination:

CullingSystem.ts
typescript
import { OctreeBvh, SpatialGrid, cull_entities } from '@web-engine-dev/wasm';
// Create octree for hierarchical culling
const octree = new OctreeBvh(
100000, // max entities
8 // max depth
);
// Rebuild octree from entity data
octree.rebuild(
positions, // Float32Array: [x, y, z] per entity
radii, // Float32Array: bounding sphere radius per entity
entityIds, // Uint32Array: entity IDs
count // number: entity count
);
// Frustum cull with front-to-back traversal
const visibleCount = octree.frustum_cull(
viewProjMatrix, // Float32Array: 4x4 view-projection matrix
cameraPos, // Float32Array: [x, y, z] camera position
outVisibility, // Uint8Array: visibility mask (indexed by EID)
outVisibleIds // Uint32Array: ordered list of visible entity IDs
);
console.log(`Visible: ${visibleCount} / ${count}`);
// Get octree statistics
const stats = octree.stats;
console.log(`Nodes: ${stats.node_count}, Leaves: ${stats.leaf_count}`);
console.log(`Depth: ${stats.max_depth}, Avg occupancy: ${stats.average_leaf_occupancy}`);
// Alternative: Simple frustum culling without octree
cull_entities(
positions, // Float32Array: entity positions
radii, // Float32Array: bounding radii
count, // number: entity count
viewProjMatrix, // Float32Array: view-projection matrix
outVisibility // Uint8Array: output visibility mask
);

Batch Compression Module#

SIMD-accelerated network packet compression for multiplayer state synchronization. Uses delta compression and quantization to minimize bandwidth:

NetworkCompression.ts
typescript
import { BatchCompressionWASM, CompressionConfig } from '@web-engine-dev/wasm';
// Create compression instance
const compressor = new BatchCompressionWASM(10000); // max entities
// Configure compression thresholds
const config: CompressionConfig = {
enable_simd: true,
position_threshold: 0.01, // 1cm position change
rotation_threshold: 0.001, // ~0.06 degree rotation change
velocity_threshold: 0.1, // 10cm/s velocity change
quantization_scale: 1000.0 // quantize to millimeters
};
// Compress entity state batch
const result = compressor.compress_batch(
currentPositions, // Float32Array: current frame positions
previousPositions, // Float32Array: previous frame positions
currentRotations, // Float32Array: current frame quaternions
previousRotations, // Float32Array: previous frame quaternions
currentVelocities, // Float32Array: current frame velocities
previousVelocities, // Float32Array: previous frame velocities
config
);
console.log(`Compression: ${result.compression_ratio.toFixed(2)}x`);
console.log(`Size: ${result.compressed_size} / ${result.uncompressed_size} bytes`);
console.log(`Time: ${result.processing_time_ms.toFixed(2)}ms`);
console.log(`SIMD ops: ${result.simd_operations}`);
// Low-level delta computation (SIMD-accelerated)
const changedCount = compressor.compute_position_deltas_simd(
currentPositions,
previousPositions,
deltaOutput,
threshold
);

Building from Source#

The WASM package ships with pre-built binaries, but you can rebuild from source if needed:

Prerequisites#

terminal
bash
# Install Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install wasm-pack
cargo install wasm-pack
# Add wasm32 target
rustup target add wasm32-unknown-unknown

Build Commands#

terminal
bash
# Navigate to WASM package
cd packages/wasm
# Build optimized WASM binary (production)
pnpm run build
# Build with SIMD support (requires browser support)
pnpm run build:simd
# Development build (faster compilation, larger binary)
wasm-pack build --target web --dev
# Run Rust tests
cargo test
# Run WASM tests in headless browser
wasm-pack test --headless --firefox

Build Configuration#

The WASM build is configured in Cargo.toml with aggressive optimizations:

Cargo.toml
toml
[profile.release]
opt-level = 3 # Maximum optimization
lto = true # Link Time Optimization
codegen-units = 1 # Single codegen unit for better optimization
panic = "abort" # Smaller binary size
strip = true # Strip debug symbols
[package.metadata.wasm-pack.profile.release]
wasm-opt = [
"-O3", # Maximum optimization
"--enable-simd", # SIMD instructions
"-ffm", # Fast float math
"--precompute-propagate" # Aggressive constant propagation
]

Memory Management#

WASM modules use shared memory for zero-copy data transfer between JavaScript and Rust:

  • TypedArrays — JavaScript TypedArrays map directly to Rust slices
  • Zero-Copy — No serialization overhead for bulk data transfer
  • Linear Memory — WASM uses a single linear memory space, no GC pressure
  • Manual Deallocation — Rust handles memory, JavaScript owns array buffers
  • Bounds Checking — All array accesses are bounds-checked in Rust
MemoryManagement.ts
typescript
// Allocate data in JavaScript (you own this memory)
const positions = new Float32Array(entityCount * 3);
const matrices = new Float32Array(entityCount * 16);
// Pass to WASM (zero-copy, shares memory)
compute_transform_matrices(
positions, // Rust sees this as &[f32]
rotations,
scales,
quaternions,
entityIds,
matrices // Rust sees this as &mut [f32]
);
// Data is modified in-place, no copies made
console.log(matrices[0]); // Updated by WASM
// WASM objects need manual disposal
const octree = new OctreeBvh(10000, 8);
// ... use octree ...
octree.free(); // Release WASM memory

Memory Ownership

When passing TypedArrays to WASM, JavaScript retains ownership of the memory. WASM functions receive temporary borrows (references) and cannot outlive the function call. For long-lived WASM objects like PhysicsWorld, call .free() when done to release WASM-allocated memory.

Performance Characteristics#

OperationComplexityPerformanceNotes
Transform hierarchy updateO(n)~0.1ms / 10k entitiesSIMD-accelerated, single-pass DFS
Quaternion conversionO(n)~0.05ms / 10k entitiesVectorized batch operation
Physics stepO(n log n)~2ms / 10k bodiesSpatial hash broad-phase
Frustum cullingO(n)~0.3ms / 10k entitiesSimple sphere tests
Octree frustum cullO(log n)~0.1ms / 10k entitiesHierarchical early-out
Batch compressionO(n)~0.5ms / 1k entitiesSIMD delta + quantization

Benchmarks performed on: Apple M1, Chrome 120, SIMD enabled. Real-world performance varies by entity complexity and data distribution.

SIMD Support#

WebAssembly SIMD provides 128-bit vector operations for parallel data processing. Web Engine's WASM modules are compiled with SIMD support:

Browser Support#

  • Chrome 91+, Edge 91+ — Full SIMD support
  • Firefox 89+ — Full SIMD support
  • Safari 16.4+ — Full SIMD support
  • Node.js 16+ — SIMD support via V8
SIMDDetection.ts
typescript
// Check SIMD support at runtime
const simdSupported = WebAssembly.validate(
new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 123, 3, 2, 1, 0, 10, 10, 1, 8, 0, 65, 0, 253, 15, 253, 98, 11])
);
if (simdSupported) {
console.log('WASM SIMD available - using optimized code paths');
} else {
console.log('WASM SIMD unavailable - using scalar fallback');
}
// Most WASM functions automatically use SIMD when available
// Some modules provide explicit control:
const config = {
enable_simd: simdSupported,
// ... other config
};

TypeScript Integration#

The WASM package includes generated TypeScript definitions from wasm-bindgen:

types.d.ts
typescript
// Auto-generated TypeScript definitions
export function update_world_transforms(
local_positions: Float32Array,
local_rotations: Float32Array,
local_scales: Float32Array,
local_quaternions: Float32Array,
parents: Uint32Array,
entity_ids: Uint32Array,
out_world_positions: Float32Array,
out_world_rotations: Float32Array,
out_world_scales: Float32Array,
out_world_quaternions: Float32Array
): void;
export class PhysicsWorld {
constructor();
free(): void;
step(dt: number): void;
add_body(eid: number, x: number, y: number, z: number, is_dynamic: boolean): void;
// ... all methods fully typed
}
export class OctreeBvh {
constructor(max_entities: number, max_depth: number);
free(): void;
rebuild(positions: Float32Array, radii: Float32Array, entity_ids: Uint32Array, count: number): void;
frustum_cull(view_proj_matrix: Float32Array, camera_pos: Float32Array, out_visibility: Uint8Array, out_visible_ids: Uint32Array): number;
readonly stats: OctreeStats;
}

Debugging WASM#

WASM modules include panic hooks for better error messages in browser console:

ErrorHandling.ts
typescript
import init, { wasm_version } from '@web-engine-dev/wasm';
try {
await init();
console.log(`WASM version: ${wasm_version()}`);
} catch (error) {
console.error('WASM initialization failed:', error);
}
// WASM panics are caught and displayed in console with full Rust backtrace
// Example panic output:
// panicked at 'index out of bounds: the len is 1000 but the index is 1001', src/transform.rs:42:5
// Enable source maps for better debugging (development builds only)
// Production builds are stripped and optimized

Development vs Production

Use wasm-pack build --dev during development for faster compile times and better error messages. Production builds with pnpm run build are 2-3x smaller but strip debug info.

Best Practices#

Initialization#

WasmInit.ts
typescript
// Initialize WASM once at app startup
import init from '@web-engine-dev/wasm';
let wasmInitialized = false;
export async function initializeWasm() {
if (wasmInitialized) return;
try {
await init();
wasmInitialized = true;
console.log('WASM initialized successfully');
} catch (error) {
console.error('Failed to initialize WASM:', error);
throw error;
}
}
// Call before using any WASM functions
await initializeWasm();

Batch Processing#

BatchProcessing.ts
typescript
// ❌ Bad: Multiple FFI calls (slow)
for (let i = 0; i < entities.length; i++) {
physics.add_body(entities[i], x[i], y[i], z[i], true);
}
// ✅ Good: Single batch call (fast)
physics.add_bodies_batch(entityIds, positions, isDynamicArray);
// Minimize FFI boundary crossings
// Each JS -> WASM call has overhead (~10-50ns)
// Process data in large batches to amortize this cost

Memory Reuse#

MemoryReuse.ts
typescript
// ❌ Bad: Allocate new arrays every frame
function update() {
const positions = new Float32Array(count * 3);
const matrices = new Float32Array(count * 16);
compute_transform_matrices(positions, rotations, scales, quaternions, entityIds, matrices);
}
// ✅ Good: Reuse pre-allocated buffers
const positions = new Float32Array(MAX_ENTITIES * 3);
const matrices = new Float32Array(MAX_ENTITIES * 16);
function update() {
compute_transform_matrices(positions, rotations, scales, quaternions, entityIds, matrices);
}
// Zero allocations in the hot path
Advanced | Web Engine Docs | Web Engine Docs