Skip to content

[Feature] WebAssembly (WASM) Build Support for Rendering Demos #170

Description

@vmarcella

Overview

Add WebAssembly (WASM) build support to lambda-rs, enabling the engine's rendering examples and demos to compile and run in web browsers. This feature would allow users to experience lambda-rs demos directly in a browser without local installation, improving accessibility and enabling web-based applications built with the engine. WASM support would leverage wgpu's WebGPU backend to provide GPU-accelerated rendering in compatible browsers.

Current State

The engine currently targets native desktop platforms only. All rendering examples (triangle.rs, textured_quad.rs, instanced_quads.rs, etc.) require compilation to native binaries and cannot run in web environments.

The underlying dependencies (wgpu, winit) already support WASM targets, but lambda-rs-platform uses blocking patterns that are incompatible with WASM:

Blocking GPU Initialization (pollster::block_on):

// crates/lambda-rs-platform/src/wgpu/instance.rs:209block_on(self.instance.request_adapter(options))// crates/lambda-rs-platform/src/wgpu/gpu.rs:157
let (device, queue) = block_on(adapter.request_device(&descriptor))?;

Blocking Event Loop:

// crates/lambda-rs-platform/src/winit/mod.rs:237self.event_loop.run(move |event, target| { ...})

On WASM targets, pollster::block_on() panics because browsers do not permit blocking the main thread. Similarly, EventLoop::run() cannot block in the browser environment.

Scope

Goals:

  • Enable wasm32-unknown-unknown target compilation for lambda-rs and lambda-rs-platform
  • Ensure rendering examples compile and run in browsers with WebGPU support
  • Provide build tooling and documentation for WASM builds
  • Abstract platform-specific initialization (canvas binding, event loop differences) in lambda-rs-platform
  • Create a web hosting setup (e.g., wasm-pack, trunk, or similar) for running demos

Non-Goals:

  • Audio support in WASM (can be addressed in a separate feature)
  • Full feature parity with native builds in the initial implementation
  • Support for browsers without WebGPU (WebGL fallback)
  • Production-optimized WASM bundle sizes (optimization can follow)

Proposed API

The public API should remain largely unchanged. The existing ApplicationRuntimeBuilder pattern and Component trait would continue to work, with platform-specific initialization handled internally via conditional compilation in lambda-rs-platform.

Existing Application Pattern (unchanged):

// Current pattern used in examples like triangle.rs, textured_quad.rs, etc.fnmain(){let runtime = ApplicationRuntimeBuilder::new("2D Triangle Demo").with_renderer_configured_as(move |render_context_builder| {return render_context_builder.with_render_timeout(1_000_000_000);}).with_window_configured_as(move |window_builder| {return window_builder
.with_dimensions(1200,600).with_name("2D Triangle Window");}).with_component(move |runtime,demo:DemoComponent| {return(runtime, demo);}).build();start_runtime(runtime);}

Build Configuration (Cargo.toml additions):

[target.'cfg(target_arch="wasm32")'.dependencies]
wasm-bindgen = "0.2"wasm-bindgen-futures = "0.4"web-sys = { version = "0.3", features = ["Document", "Window", "Element", "HtmlCanvasElement"] }
console_error_panic_hook = "0.1"console_log = "1.0"
[lib]
crate-type = ["cdylib", "rlib"]

Platform Abstraction Changes (lambda-rs-platform):

  • WindowBuilder must support canvas binding on WASM targets
  • start_runtime must handle async event loop requirements on web
  • Surface creation in GPU initialization must use canvas elements instead of native windows
// crates/lambda-rs-platform/src/windowing/mod.rs#[cfg(target_arch = "wasm32")]implWindowBuilder{/// Bind to an existing HTML canvas element by ID for WASM targets.pubfnwith_canvas_id(mutself,canvas_id:&str) -> Self;}

Async/Sync Abstraction (lambda-rs-platform internal):

GPU initialization must be conditionally compiled to use blocking calls on native and async on WASM:

// crates/lambda-rs-platform/src/wgpu/instance.rs#[cfg(not(target_arch = "wasm32"))]pub(crate)fnrequest_adapter(&self,options:&wgpu::RequestAdapterOptions<'_,'_>,) -> Result<wgpu::Adapter, wgpu::RequestAdapterError>{
pollster::block_on(self.instance.request_adapter(options))}#[cfg(target_arch = "wasm32")]pub(crate)asyncfnrequest_adapter_async(&self,options:&wgpu::RequestAdapterOptions<'_,'_>,) -> Result<wgpu::Adapter, wgpu::RequestAdapterError>{self.instance.request_adapter(options).await}
// crates/lambda-rs-platform/src/wgpu/gpu.rs#[cfg(not(target_arch = "wasm32"))]pubfnbuild(self,adapter:&wgpu::Adapter) -> Result<Gpu,GpuBuildError>{let(device, queue) = pollster::block_on(adapter.request_device(&descriptor))?;// ...}#[cfg(target_arch = "wasm32")]pubasyncfnbuild_async(self,adapter:&wgpu::Adapter) -> Result<Gpu,GpuBuildError>{let(device, queue) = adapter.request_device(&descriptor).await?;// ...}

Event Loop Handling (lambda-rs-platform internal):

The event loop must use EventLoop::spawn() on WASM instead of EventLoop::run():

// crates/lambda-rs-platform/src/winit/mod.rs#[cfg(not(target_arch = "wasm32"))]pubfnrun_forever<Callback>(self,mutcallback:Callback)whereCallback:'static + FnMut(Event<E>,&EventLoopWindowTarget<E>),{self.event_loop.run(move |event, target| {
target.set_control_flow(ControlFlow::Poll);callback(event, target);}).expect("Event loop terminated unexpectedly");}#[cfg(target_arch = "wasm32")]pubfnrun_forever<Callback>(self,callback:Callback)whereCallback:'static + FnMut(Event<E>,&EventLoopWindowTarget<E>),{// spawn() does not block and integrates with browser's requestAnimationFrameself.event_loop.spawn(callback);}

WASM Entry Point (example adaptation):

// examples/triangle.rs - conditional compilation for WASM entry#[cfg(target_arch = "wasm32")]use wasm_bindgen::prelude::*;#[cfg(target_arch = "wasm32")]#[wasm_bindgen(start)]pubfnwasm_main(){
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init_with_level(log::Level::Info).expect("Logger init failed");main();}#[cfg(not(target_arch = "wasm32"))]fnmain(){// existing main code}#[cfg(target_arch = "wasm32")]fnmain(){let runtime = ApplicationRuntimeBuilder::new("2D Triangle Demo").with_window_configured_as(move |window_builder| {return window_builder.with_canvas_id("lambda-canvas");}).with_component(move |runtime,demo:DemoComponent| {return(runtime, demo);}).build();start_runtime(runtime);}

Acceptance Criteria

Compilation:

  • cargo build --target wasm32-unknown-unknown -p lambda-rs compiles successfully
  • cargo build --target wasm32-unknown-unknown -p lambda-rs-platform compiles successfully
  • Native builds continue to work without regression

Platform Abstraction:

  • pollster::block_on replaced with conditional compilation (native: sync, WASM: async)
  • EventLoop::run() replaced with EventLoop::spawn() on WASM targets
  • GPU adapter and device requests use async initialization on WASM
  • WindowBuilder supports canvas binding via with_canvas_id() on WASM

Examples:

  • triangle example renders correctly in a WebGPU-enabled browser
  • textured_quad example renders correctly with texture loading
  • instanced_quads example demonstrates instanced rendering in browser

Tooling & Documentation:

  • Build script or task added for WASM compilation (e.g., scripts/build_wasm.sh)
  • HTML template provided for hosting WASM demos
  • CI workflow added or updated to verify WASM compilation
  • Documentation added to docs/ explaining WASM build process and architecture
  • docs/features.md updated with WASM-related feature flags
  • README updated with browser compatibility notes

Affected Crates

lambda-rs, lambda-rs-platform

Notes

  • Browser Requirements: WebGPU is required; Chrome 113+, Firefox 121+, and Safari 18+ have support
  • Related Dependencies: wgpu 26.x supports WASM via WebGPU; winit 0.29.x supports web targets
  • Testing: Manual browser testing required; consider adding Playwright or similar for automated web testing in the future

Architectural Considerations:

ConcernNativeWASM
GPU adapter requestpollster::block_on()wasm_bindgen_futures::spawn_local() + async
GPU device requestpollster::block_on()async .await
Event loopEventLoop::run() (blocking)EventLoop::spawn() (non-blocking)
Window surfaceNative window handleHTML canvas element
Logginglambda-rs-loggingconsole_log crate
  • The public lambda-rs API should remain synchronous-looking; async complexity is encapsulated in lambda-rs-platform
  • Use #[cfg(target_arch = "wasm32")] and #[cfg(not(target_arch = "wasm32"))] for platform-specific code paths
  • On WASM, runtime initialization must be wrapped in wasm_bindgen_futures::spawn_local() to execute async GPU setup
  • Consider creating internal helper functions/macros to reduce duplication between sync and async code paths

Future Work:

  • WebGL2 fallback for broader browser support (separate feature)
  • WASM audio support via Web Audio API (separate feature)
  • Bundle size optimization with wasm-opt
  • Hosting demos on GitHub Pages or similar
  • Potential async public API if use cases demand it (would be a breaking change)

Platform Considerations:

  • Event loop handling differs on web (cannot block the main thread)
  • File/asset loading requires fetch API or embedding
  • Window resizing and fullscreen require web-specific handling
  • Panic hooks should use console_error_panic_hook for browser debugging

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestlambda-rsIssues pertaining to the core frameworklambda-rs-loggingIssues pertaining to the in-house loggerlambda-rs-platformIssues pertaining to the dependency & platform wrappersrenderAll things render relatedwasmAll things WASM related

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions