//! # effectcraft-plugin (L3) //! //! Loads **WebAssembly effect plug-ins** (WebAssembly ABI v1 and v2, see `docs/plugins.md`; the //! native plug-in API 2 additions such as layer parameters and host services are not part of it) //! into the effect registry ([`effectcraft_effects::plugin`]). A plug-in is a `.wasm` (or //! `.wat`) module with no imports that exports: //! //! | export | signature | | //! |---|---|---| //! | `memory` | memory | linear memory shared with the host | //! | `ec_api_version` | `() -> i32` | 1 for ABI 1, 2 for ABI 2 (neighbouring frames) | //! | `ec_manifest_ptr`, `ec_manifest_len` | `() -> i32` | a UTF-8 JSON [`PluginManifest`] in memory | //! | `ec_alloc` | `(bytes: i32) -> i32` | a buffer of at least `bytes` bytes, 8-byte aligned; it may reuse the previous one | //! | `ec_render` (ABI 1) | `(pixels: i32, width: i32, height: i32, params: i32, nparams: i32, time: f64, scale: f64) -> i32` | process the pixels in place; 0 = ok | //! | `ec_render2` (ABI 2) | `ec_render`'s arguments, then `prev: i32, prev_time: f64, next: i32, next_time: f64, flags: i32` | the same, with the neighbouring frames; 0 = ok | //! //! For each render the host asks `ec_alloc` for one buffer holding the parameters (`nparams` //! little-endian `f64`s) followed by the pixels (`width × height × 4` little-endian `f32`s, //! premultiplied RGBA, row-major), calls the render export, and reads the pixels back. An ABI 2 //! module gets two more pixel buffers after the current frame: the layer's previous and next //! frames, resampled onto the current grid, and their layer times. The buffers are always //! allocated (zeroed when a frame does not exist); `flags` bit 0/1 says which are real. //! //! Modules run in the [wasmi](https://github.com/wasmi-labs/wasmi) interpreter (pure Rust, //! MIT/Apache-2.0) with deterministic floats, no imports (no files, network or clock) and a //! fuel limit per frame, so a plug-in can't escape its sandbox, hang the app, or render //! differently on another machine. The runtime is the `wasm` feature (on for the desktop app and //! the CLI); without it [`load_wasm`] says plug-ins are unavailable. #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)] use effectcraft_effects::EffectSpec; pub use effectcraft_effects::plugin::{ EffectPlugin, MIN_WASM_API_VERSION, PLUGIN_API_VERSION, PluginFrame, PluginImage, PluginManifest, PluginParams, WASM_API_VERSION, register_plugin, }; #[cfg(feature = "wasm")] mod wasm; /// Whether this build can load WebAssembly plug-ins. pub fn wasm_available() -> bool { cfg!(feature = "wasm") } /// Load and register a WebAssembly plug-in (`bytes` = a `.wasm` binary or `.wat` text; /// `source` = where it came from, for listings). Returns the registered effect. pub fn load_wasm(bytes: &[u8], source: &str) -> Result<&'static EffectSpec, String> { #[cfg(feature = "wasm")] { let p = wasm::WasmPlugin::new(bytes, source)?; register_plugin(std::sync::Arc::new(p)) } #[cfg(not(feature = "wasm"))] { let _ = (bytes, source); Err("WebAssembly plug-ins are not available in this build".into()) } } /// [`load_wasm`] for the engine's plug-in loader hook: `{id, name, category, version, api}`. pub fn loader(bytes: &[u8], source: &str) -> Result { let spec = load_wasm(bytes, source)?; let m = effectcraft_effects::plugin::plugin(spec.id).map(|p| p.manifest().clone()); Ok(serde_json::json!({ "id": spec.id, "name": spec.name, "category": spec.category, "version": m.as_ref().map(|m| m.version.clone()).unwrap_or_default(), "api": m.as_ref().map(|m| m.api).unwrap_or(PLUGIN_API_VERSION), "params": spec.params.len(), })) } /// Render a module's effect without registering it (the Plug-in Library's preview): default /// parameters, `pixels` (premultiplied RGBA, `width × height`) at each of `times` (seconds). /// Returns the manifest as JSON and one frame per time. pub fn preview(bytes: &[u8], width: u32, height: u32, pixels: &[[f32; 4]], times: &[f64]) -> Result<(serde_json::Value, Vec>), String> { #[cfg(feature = "wasm")] { use effectcraft_effects::plugin::{EffectPlugin, PluginFrame, PluginParamKind, PluginParams}; let p = wasm::WasmPlugin::new(bytes, "preview")?; let m = p.manifest(); let mut flat = Vec::with_capacity(m.flat_len()); for param in &m.params { match ¶m.kind { PluginParamKind::Slider { default, .. } | PluginParamKind::Angle { default } => flat.push(*default), PluginParamKind::Checkbox { default } => flat.push(if *default { 1.0 } else { 0.0 }), PluginParamKind::Popup { default, .. } => flat.push(*default as f64), // Points default to fractions of the layer: the frame is the layer. PluginParamKind::Point { default } => flat.extend([default[0] * f64::from(width), default[1] * f64::from(height)]), PluginParamKind::Color { default } => flat.extend(default.iter().copied()), // API 2 kinds: a WebAssembly module can't declare them (`WasmPlugin::new` refuses them). PluginParamKind::Point3 { default } => flat.extend(default.iter().copied()), PluginParamKind::Layer => flat.push(-1.0), PluginParamKind::Text { .. } | PluginParamKind::Hidden { .. } => {} } } let params = PluginParams { manifest: m, flat: &flat, texts: &[] }; let mut frames = Vec::with_capacity(times.len()); for &t in times { let mut px = pixels.to_vec(); let mut frame = PluginFrame { width, height, pixels: &mut px, scale: 1.0, origin: [0.0; 2], layer_size: [f64::from(width), f64::from(height)] }; p.render(&mut frame, ¶ms, t)?; frames.push(px); } let manifest = serde_json::to_value(m).map_err(|e| e.to_string())?; Ok((manifest, frames)) } #[cfg(not(feature = "wasm"))] { let _ = (bytes, width, height, pixels, times); Err("WebAssembly plug-ins are not available in this build".into()) } } // Two attributes, so clippy sees `cfg(test)` and treats the module as test code. #[cfg(test)] #[cfg(feature = "wasm")] mod tests;