Skip to content

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

@vmarcella

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:209
block_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:237
self.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.
fn main() {
  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")]
impl WindowBuilder {
  /// Bind to an existing HTML canvas element by ID for WASM targets.
  pub fn with_canvas_id(mut self, 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) fn request_adapter(
  &self,
  options: &wgpu::RequestAdapterOptions<'_, '_>,
) -> Result<wgpu::Adapter, wgpu::RequestAdapterError> {
  pollster::block_on(self.instance.request_adapter(options))
}

#[cfg(target_arch = "wasm32")]
pub(crate) async fn request_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"))]
pub fn build(self, adapter: &wgpu::Adapter) -> Result<Gpu, GpuBuildError> {
  let (device, queue) = pollster::block_on(adapter.request_device(&descriptor))?;
  // ...
}

#[cfg(target_arch = "wasm32")]
pub async fn build_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"))]
pub fn run_forever<Callback>(self, mut callback: Callback)
where
  Callback: '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")]
pub fn run_forever<Callback>(self, callback: Callback)
where
  Callback: 'static + FnMut(Event<E>, &EventLoopWindowTarget<E>),
{
  // spawn() does not block and integrates with browser's requestAnimationFrame
  self.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)]
pub fn wasm_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"))]
fn main() {
  // existing main code
}

#[cfg(target_arch = "wasm32")]
fn main() {
  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:

Concern Native WASM
GPU adapter request pollster::block_on() wasm_bindgen_futures::spawn_local() + async
GPU device request pollster::block_on() async .await
Event loop EventLoop::run() (blocking) EventLoop::spawn() (non-blocking)
Window surface Native window handle HTML canvas element
Logging lambda-rs-logging console_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