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: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

    Fields

    No fields configured for Feature.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions