#![cfg(target_arch = "wasm32")] use std::cell::RefCell; use effects::EffectPipeline; use gpu::{GpuContext, wgpu}; use js_sys::{Object, Reflect}; use masks::MaskFeatherPipeline; use serde::Deserialize; use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen}; pub(crate) struct GpuRuntime { pub(crate) context: GpuContext, pub(crate) effects: EffectPipeline, pub(crate) masks: MaskFeatherPipeline, } thread_local! { static GPU_RUNTIME: RefCell> = const { RefCell::new(None) }; } fn set_panic_hook() { static SET_HOOK: std::sync::Once = std::sync::Once::new(); SET_HOOK.call_once(|| { std::panic::set_hook(Box::new(|info| { // Store the full panic message in window.__wasmPanic so the JS catch block // can surface it instead of the opaque "Unreachable" WASM trap message. if let Some(window) = web_sys::window() { let _ = Reflect::set( &window, &JsValue::from_str("__wasmPanic"), &JsValue::from_str(&info.to_string()), ); } console_error_panic_hook::hook(info); })); }); } #[wasm_bindgen(js_name = initializeGpu)] pub async fn initialize_gpu() -> Result<(), JsValue> { set_panic_hook(); if GPU_RUNTIME.with(|runtime| runtime.borrow().is_some()) { return Ok(()); } let context = GpuContext::new() .await .map_err(|error| JsValue::from_str(&error.to_string()))?; let effects = EffectPipeline::new(&context); let masks = MaskFeatherPipeline::new(&context); GPU_RUNTIME.with(|runtime| { runtime.replace(Some(GpuRuntime { context, effects, masks, })); }); Ok(()) } pub(crate) fn with_gpu_runtime( action: impl FnOnce(&GpuRuntime) -> Result, ) -> Result { GPU_RUNTIME.with(|runtime| { let borrow = runtime.borrow(); let Some(gpu_runtime) = borrow.as_ref() else { return Err(JsValue::from_str( "GPU context not initialized. Call initializeGpu() first.", )); }; action(gpu_runtime) }) } pub(crate) fn import_canvas_texture( context: &GpuContext, canvas: &wgpu::web_sys::OffscreenCanvas, width: u32, height: u32, label: &'static str, ) -> wgpu::Texture { context.import_offscreen_canvas_texture(canvas, width, height, label) } pub(crate) fn render_texture_to_canvas( context: &GpuContext, texture: &wgpu::Texture, width: u32, height: u32, ) -> Result { let canvas = wgpu::web_sys::OffscreenCanvas::new(width, height)?; context .render_texture_to_offscreen_canvas(texture, &canvas, width, height) .map_err(|error| JsValue::from_str(&error.to_string()))?; Ok(canvas) } pub(crate) fn read_property(object: &Object, name: &str) -> Result { Reflect::get(object, &JsValue::from_str(name)) .map_err(|_| JsValue::from_str(&format!("Missing property '{name}'"))) } pub(crate) fn read_offscreen_canvas_property( object: &Object, name: &str, ) -> Result { read_property(object, name)? .dyn_into::() .map_err(|_| JsValue::from_str(&format!("Property '{name}' must be an OffscreenCanvas"))) } pub(crate) fn read_u32_property(object: &Object, name: &str) -> Result { let value = read_property(object, name)?; let Some(number) = value.as_f64() else { return Err(JsValue::from_str(&format!( "Property '{name}' must be a number" ))); }; Ok(number as u32) } pub(crate) fn read_f32_property(object: &Object, name: &str) -> Result { let value = read_property(object, name)?; let Some(number) = value.as_f64() else { return Err(JsValue::from_str(&format!( "Property '{name}' must be a number" ))); }; Ok(number as f32) } pub(crate) fn read_serde_property(object: &Object, name: &str) -> Result where T: for<'de> Deserialize<'de>, { let value = read_property(object, name)?; serde_wasm_bindgen::from_value(value) .map_err(|error| JsValue::from_str(&format!("Invalid property '{name}': {error}"))) }