-
Notifications
You must be signed in to change notification settings - Fork 2
Description
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-unknowntarget 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):
WindowBuildermust support canvas binding on WASM targetsstart_runtimemust 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-rscompiles successfully -
cargo build --target wasm32-unknown-unknown -p lambda-rs-platformcompiles successfully - Native builds continue to work without regression
Platform Abstraction:
-
pollster::block_onreplaced with conditional compilation (native: sync, WASM: async) -
EventLoop::run()replaced withEventLoop::spawn()on WASM targets - GPU adapter and device requests use async initialization on WASM
-
WindowBuildersupports canvas binding viawith_canvas_id()on WASM
Examples:
-
triangleexample renders correctly in a WebGPU-enabled browser -
textured_quadexample renders correctly with texture loading -
instanced_quadsexample 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.mdupdated 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-rsAPI should remain synchronous-looking; async complexity is encapsulated inlambda-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_hookfor browser debugging