WASM Modules
AdvancedHigh-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
-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:
import init, { update_world_transforms, update_quaternions_from_euler, compute_transform_matrices,} from '@web-engine-dev/wasm'; // Initialize WASM module once at startupawait init(); // Update world transforms for entire entity hierarchy// Processes parent-child relationships recursively in O(n) timeupdate_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 renderingcompute_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:
import { PhysicsWorld } from '@web-engine-dev/wasm'; // Create physics worldconst 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 batchphysics.update_bodies_batch( entityIds, // Uint32Array: entity IDs to update masses, // Float32Array: mass per entity frictions, // Float32Array: friction coefficients restitutions // Float32Array: restitution (bounciness)); // Step simulationphysics.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 supportconst 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
Culling Module#
Accelerated frustum culling and octree spatial partitioning for visibility determination:
import { OctreeBvh, SpatialGrid, cull_entities } from '@web-engine-dev/wasm'; // Create octree for hierarchical cullingconst octree = new OctreeBvh( 100000, // max entities 8 // max depth); // Rebuild octree from entity dataoctree.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 traversalconst 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 statisticsconst 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 octreecull_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:
import { BatchCompressionWASM, CompressionConfig } from '@web-engine-dev/wasm'; // Create compression instanceconst compressor = new BatchCompressionWASM(10000); // max entities // Configure compression thresholdsconst 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 batchconst 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#
# Install Rust toolchaincurl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh # Install wasm-packcargo install wasm-pack # Add wasm32 targetrustup target add wasm32-unknown-unknownBuild Commands#
# Navigate to WASM packagecd 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 testscargo test # Run WASM tests in headless browserwasm-pack test --headless --firefoxBuild Configuration#
The WASM build is configured in Cargo.toml with aggressive optimizations:
[profile.release]opt-level = 3 # Maximum optimizationlto = true # Link Time Optimizationcodegen-units = 1 # Single codegen unit for better optimizationpanic = "abort" # Smaller binary sizestrip = 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
// 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 madeconsole.log(matrices[0]); // Updated by WASM // WASM objects need manual disposalconst octree = new OctreeBvh(10000, 8);// ... use octree ...octree.free(); // Release WASM memoryMemory Ownership
PhysicsWorld, call .free() when done to release WASM-allocated memory.Performance Characteristics#
| Operation | Complexity | Performance | Notes |
|---|---|---|---|
| Transform hierarchy update | O(n) | ~0.1ms / 10k entities | SIMD-accelerated, single-pass DFS |
| Quaternion conversion | O(n) | ~0.05ms / 10k entities | Vectorized batch operation |
| Physics step | O(n log n) | ~2ms / 10k bodies | Spatial hash broad-phase |
| Frustum culling | O(n) | ~0.3ms / 10k entities | Simple sphere tests |
| Octree frustum cull | O(log n) | ~0.1ms / 10k entities | Hierarchical early-out |
| Batch compression | O(n) | ~0.5ms / 1k entities | SIMD 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
// Check SIMD support at runtimeconst 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:
// Auto-generated TypeScript definitionsexport 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:
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 optimizedDevelopment vs Production
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#
// Initialize WASM once at app startupimport 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 functionsawait initializeWasm();Batch Processing#
// ❌ 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 costMemory Reuse#
// ❌ Bad: Allocate new arrays every framefunction 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 buffersconst 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