#![cfg(target_arch = "wasm32")] use std::cell::RefCell; use compositor::{Compositor, FrameDescriptor, RenderFrameOptions}; use gpu::wgpu; use js_sys::Object; use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; use crate::gpu::{ import_canvas_texture, read_offscreen_canvas_property, read_serde_property, read_u32_property, with_gpu_runtime, }; use crate::perf; struct CompositorRuntime { canvas: web_sys::HtmlCanvasElement, compositor: Compositor, surface: wgpu::Surface<'static>, surface_size: (u32, u32), } thread_local! { static COMPOSITOR_RUNTIME: RefCell> = const { RefCell::new(None) }; } #[wasm_bindgen(js_name = initCompositor)] pub fn init_compositor(width: u32, height: u32) -> Result<(), JsValue> { with_gpu_runtime(|gpu_runtime| { // On WebGL, wgpu is bound to a specific canvas; reuse it so the UI // can mount the output directly instead of copying pixels through // an intermediate 2D canvas every frame. On WebGPU, surface rendering // works against any canvas so we create a fresh one. let canvas = if let Some(gl_canvas) = gpu_runtime.context.gl_canvas() { gl_canvas.clone() } else { let document = web_sys::window() .and_then(|window| window.document()) .ok_or_else(|| JsValue::from_str("Document is not available"))?; document .create_element("canvas")? .dyn_into::() .map_err(|_| JsValue::from_str("Failed to create compositor canvas"))? }; canvas.set_width(width); canvas.set_height(height); let compositor = Compositor::new(&gpu_runtime.context); let surface = gpu_runtime .context .instance() .create_surface(wgpu::SurfaceTarget::Canvas(canvas.clone())) .map_err(|error| JsValue::from_str(&error.to_string()))?; gpu_runtime .context .configure_surface(&surface, width, height) .map_err(|error| JsValue::from_str(&error.to_string()))?; COMPOSITOR_RUNTIME.with(|runtime| { runtime.replace(Some(CompositorRuntime { canvas, compositor, surface, surface_size: (width, height), })); }); Ok(()) }) } #[wasm_bindgen(js_name = resizeCompositor)] pub fn resize_compositor(width: u32, height: u32) -> Result<(), JsValue> { with_gpu_runtime(|gpu_runtime| { COMPOSITOR_RUNTIME.with(|runtime| { let mut borrow = runtime.borrow_mut(); let Some(runtime) = borrow.as_mut() else { return Err(JsValue::from_str( "Compositor is not initialized. Call initCompositor() first.", )); }; runtime.canvas.set_width(width); runtime.canvas.set_height(height); if runtime.surface_size != (width, height) { gpu_runtime .context .configure_surface(&runtime.surface, width, height) .map_err(|error| JsValue::from_str(&error.to_string()))?; runtime.surface_size = (width, height); } Ok(()) }) }) } #[wasm_bindgen(js_name = getCompositorCanvas)] pub fn get_compositor_canvas() -> Result { COMPOSITOR_RUNTIME.with(|runtime| { let borrow = runtime.borrow(); let Some(runtime) = borrow.as_ref() else { return Err(JsValue::from_str( "Compositor is not initialized. Call initCompositor() first.", )); }; Ok(runtime.canvas.clone()) }) } #[wasm_bindgen(js_name = uploadTexture)] pub fn upload_texture(options: JsValue) -> Result<(), JsValue> { let UploadTextureOptions { id, source, width, height, } = parse_upload_texture_options(options)?; with_gpu_runtime(|gpu_runtime| { COMPOSITOR_RUNTIME.with(|runtime| { let mut borrow = runtime.borrow_mut(); let Some(runtime) = borrow.as_mut() else { return Err(JsValue::from_str( "Compositor is not initialized. Call initCompositor() first.", )); }; let texture = import_canvas_texture( &gpu_runtime.context, &source, width, height, "compositor-upload-texture", ); runtime.compositor.upsert_texture(id, texture); Ok(()) }) }) } #[wasm_bindgen(js_name = releaseTexture)] pub fn release_texture(id: String) -> Result<(), JsValue> { COMPOSITOR_RUNTIME.with(|runtime| { let mut borrow = runtime.borrow_mut(); let Some(runtime) = borrow.as_mut() else { return Err(JsValue::from_str( "Compositor is not initialized. Call initCompositor() first.", )); }; runtime.compositor.release_texture(&id); Ok(()) }) } #[wasm_bindgen(js_name = renderFrame)] pub fn render_frame(options: JsValue) -> Result<(), JsValue> { perf::reset(); let t_deserialize = perf::now_ms(); let frame: FrameDescriptor = serde_wasm_bindgen::from_value(options) .map_err(|error| JsValue::from_str(&format!("Invalid frame descriptor: {error}")))?; perf::record("wasm.deserialize", perf::now_ms() - t_deserialize); with_gpu_runtime(|gpu_runtime| { COMPOSITOR_RUNTIME.with(|runtime| { let mut borrow = runtime.borrow_mut(); let Some(runtime) = borrow.as_mut() else { return Err(JsValue::from_str( "Compositor is not initialized. Call initCompositor() first.", )); }; if runtime.surface_size != (frame.width, frame.height) { runtime.canvas.set_width(frame.width); runtime.canvas.set_height(frame.height); let t_surface = perf::now_ms(); gpu_runtime .context .configure_surface(&runtime.surface, frame.width, frame.height) .map_err(|error| JsValue::from_str(&error.to_string()))?; perf::record("wasm.surfaceConfigure", perf::now_ms() - t_surface); runtime.surface_size = (frame.width, frame.height); } if gpu_runtime.context.supports_surface_rendering() { let t_render = perf::now_ms(); let result = runtime .compositor .render_frame( &gpu_runtime.context, RenderFrameOptions { frame: &frame, surface: &runtime.surface, }, ) .map_err(|error| JsValue::from_str(&error.to_string())); perf::record("wasm.renderFrameToSurface", perf::now_ms() - t_render); result } else { // WebGL still needs a separate composition pass, but the output // surface is now persistent just like the WebGPU path. let t_composite = perf::now_ms(); let texture = runtime .compositor .render_frame_to_texture(&gpu_runtime.context, &frame) .map_err(|error| JsValue::from_str(&error.to_string()))?; perf::record("wasm.compositeToTexture", perf::now_ms() - t_composite); let t_present = perf::now_ms(); gpu_runtime .context .present_texture_to_surface(&texture, &runtime.surface) .map_err(|error| JsValue::from_str(&error.to_string()))?; perf::record("wasm.presentToSurface", perf::now_ms() - t_present); Ok(()) } }) }) } #[derive(Debug)] struct UploadTextureOptions { id: String, source: wgpu::web_sys::OffscreenCanvas, width: u32, height: u32, } fn parse_upload_texture_options(value: JsValue) -> Result { let object: Object = value .dyn_into() .map_err(|_| JsValue::from_str("uploadTexture expects an options object"))?; Ok(UploadTextureOptions { id: read_serde_property(&object, "id")?, source: read_offscreen_canvas_property(&object, "source")?, width: read_u32_property(&object, "width")?, height: read_u32_property(&object, "height")?, }) }