canary-rs/src/lib.rs

375 lines
12 KiB
Rust

// Copyright (c) 2022 Marceline Cramer
// SDPX-License-Identifier: LGPL-3.0-or-later
pub use canary_script::*;
use parking_lot::{Mutex, RwLock};
use slab::Slab;
use std::collections::HashMap;
use std::sync::Arc;
pub mod text;
/// Proportion constant between pixels (at 96dpi) to millimeters (Canary's unit measurement).
pub const PX_PER_MM: f32 = 25.4 / 96.0;
/// Low-level script API callbacks.
///
/// If you're a casual user of Canary the struct you're looking for is
/// [ScriptAbiImpl]. This trait exists to help with making mocks for testing.
pub trait ScriptAbi {
fn start_draw(&self);
fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]);
fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color);
fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand]));
fn font_load(&self, family: &str) -> u32;
fn text_layout_new(&self, font_id: u32, text: &str) -> u32;
fn text_layout_delete(&self, id: u32);
fn text_layout_get_bounds(&self, id: u32, rect: &mut Rect);
fn message_new(&self, data: Vec<u8>) -> u32;
fn message_free(&self, id: u32);
fn message_get_len(&self, id: u32) -> u32;
fn message_get_data(&self, id: u32, dst: &mut [u8]);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PanelId(pub(crate) usize);
pub trait ScriptInstance {
fn bind_panel(&mut self, msg: Vec<u8>) -> PanelId;
fn update(&mut self, panel: PanelId, dt: f32);
fn draw(&mut self, panel: PanelId, f: impl FnOnce(&[DrawCommand]));
fn on_cursor_event(&mut self, panel: PanelId, kind: CursorEventKind, at: Vec2);
fn on_message(&mut self, panel: PanelId, msg: Vec<u8>);
}
#[non_exhaustive]
pub enum DrawCommand {
Mesh {
vertices: Vec<MeshVertex>,
indices: Vec<MeshIndex>,
},
}
pub struct WasmtimeRuntime {
engine: wasmtime::Engine,
}
impl WasmtimeRuntime {
pub fn new() -> anyhow::Result<Self> {
let mut config = wasmtime::Config::new();
config.wasm_simd(true);
config.wasm_bulk_memory(true);
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
let engine = wasmtime::Engine::new(&config)?;
Ok(Self { engine })
}
pub fn load_module<T: ScriptAbi>(
&self,
abi: T,
module: &[u8],
) -> anyhow::Result<WasmtimeScript<T>> {
let module = wasmtime::Module::new(&self.engine, module)?;
let mut store = wasmtime::Store::new(&self.engine, abi);
let panel_datas = Default::default();
let mut linker = wasmtime::Linker::new(&self.engine);
WasmtimeScript::link(&mut linker)?;
let instance = linker.instantiate(&mut store, &module)?;
let bind_panel = instance.get_typed_func(&mut store, "bind_panel")?;
let update = instance.get_typed_func(&mut store, "update")?;
let draw = instance.get_typed_func(&mut store, "draw")?;
let on_cursor_event = instance.get_typed_func(&mut store, "on_cursor_event")?;
let on_message = instance.get_typed_func(&mut store, "on_message")?;
Ok(WasmtimeScript {
store,
panel_datas,
bind_panel,
update,
draw,
on_cursor_event,
on_message,
})
}
}
pub struct WasmtimeScript<T> {
store: wasmtime::Store<T>,
panel_datas: Slab<u32>,
bind_panel: wasmtime::TypedFunc<(u32, u32), u32>,
update: wasmtime::TypedFunc<(u32, f32), ()>,
draw: wasmtime::TypedFunc<u32, ()>,
on_cursor_event: wasmtime::TypedFunc<(u32, u32, f32, f32), ()>,
on_message: wasmtime::TypedFunc<(u32, u32), ()>,
}
impl<T: ScriptAbi> WasmtimeScript<T> {
pub fn link(linker: &mut wasmtime::Linker<T>) -> anyhow::Result<()> {
let module = "env";
linker.func_wrap(
module,
"draw_indexed",
|mut caller: wasmtime::Caller<'_, T>,
vertices_ptr: u32,
vertices_num: u32,
indices_ptr: u32,
indices_num: u32| {
let vertices = Self::get_memory_slice(&mut caller, vertices_ptr, vertices_num);
let indices = Self::get_memory_slice(&mut caller, indices_ptr, indices_num);
caller.data().draw_indexed(vertices, indices);
},
)?;
linker.func_wrap(
module,
"draw_text_layout",
|caller: wasmtime::Caller<'_, T>,
id: u32,
xoff: f32,
yoff: f32,
scale: f32,
color: u32| {
let offset = Vec2 { x: xoff, y: yoff };
let color = Color(color);
caller.data().draw_text_layout(id, offset, scale, color);
},
)?;
linker.func_wrap(
module,
"font_load",
|mut caller: wasmtime::Caller<'_, T>, family_ptr: u32, family_len: u32| {
let family = Self::get_memory_slice_str(&mut caller, family_ptr, family_len);
caller.data().font_load(family)
},
)?;
linker.func_wrap(
module,
"text_layout_new",
|mut caller: wasmtime::Caller<'_, T>, font_id: u32, text_ptr: u32, text_len: u32| {
let text = Self::get_memory_slice_str(&mut caller, text_ptr, text_len);
caller.data().text_layout_new(font_id, text)
},
)?;
linker.func_wrap(
module,
"text_layout_delete",
|caller: wasmtime::Caller<'_, T>, id: u32| caller.data().text_layout_delete(id),
)?;
linker.func_wrap(
module,
"text_layout_get_bounds",
|mut caller: wasmtime::Caller<'_, T>, id: u32, rect_ptr: u32| {
let rect: &mut Rect = Self::get_memory_ref(&mut caller, rect_ptr);
caller.data().text_layout_get_bounds(id, rect);
},
)?;
linker.func_wrap(
module,
"message_get_len",
|caller: wasmtime::Caller<'_, T>, id: u32| -> u32 { caller.data().message_get_len(id) },
)?;
linker.func_wrap(
module,
"message_get_data",
|mut caller: wasmtime::Caller<'_, T>, id: u32, ptr: u32| {
let ptr = ptr as usize;
let len = caller.data().message_get_len(id) as usize;
let dst = Self::get_memory_slice_bytes(&mut caller, ptr, len);
caller.data().message_get_data(id, dst)
},
)?;
Ok(())
}
fn get_panel_data(&self, panel: PanelId) -> u32 {
*self.panel_datas.get(panel.0).unwrap()
}
fn get_memory_ref<D: bytemuck::Pod>(
caller: &mut wasmtime::Caller<'_, T>,
ptr: u32,
) -> &'static mut D {
let len = std::mem::size_of::<D>();
let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len);
bytemuck::from_bytes_mut(bytes)
}
fn get_memory_slice<D: bytemuck::Pod>(
caller: &mut wasmtime::Caller<'_, T>,
ptr: u32,
num: u32,
) -> &'static mut [D] {
let len = num as usize * std::mem::size_of::<D>();
let bytes = Self::get_memory_slice_bytes(caller, ptr as usize, len);
bytemuck::cast_slice_mut(bytes)
}
fn get_memory_slice_str(
caller: &mut wasmtime::Caller<'_, T>,
ptr: u32,
len: u32,
) -> &'static mut str {
let memory = Self::get_memory_slice_bytes(caller, ptr as usize, len as usize);
std::str::from_utf8_mut(memory).unwrap()
}
fn get_memory_slice_bytes(
caller: &mut wasmtime::Caller<'_, T>,
ptr: usize,
len: usize,
) -> &'static mut [u8] {
let memory = caller.get_export("memory").unwrap().into_memory().unwrap();
if ptr + len > memory.data_size(&caller) {
panic!("Attempted wasm memory read is out-of-bounds!");
}
unsafe {
let ptr = memory.data_ptr(caller).add(ptr);
std::slice::from_raw_parts_mut(ptr, len)
}
}
}
impl<T: ScriptAbi> ScriptInstance for WasmtimeScript<T> {
fn bind_panel(&mut self, msg: Vec<u8>) -> PanelId {
let id = self.panel_datas.insert(0);
let msg = self.store.data().message_new(msg);
let args = (id as u32, msg);
let data = self.bind_panel.call(&mut self.store, args).unwrap();
*self.panel_datas.get_mut(id).unwrap() = data;
self.store.data().message_free(msg);
PanelId(id)
}
fn update(&mut self, panel: PanelId, dt: f32) {
let data = self.get_panel_data(panel);
self.update.call(&mut self.store, (data, dt)).unwrap();
}
fn draw(&mut self, panel: PanelId, f: impl FnOnce(&[DrawCommand])) {
let data = self.get_panel_data(panel);
self.store.data().start_draw();
self.draw.call(&mut self.store, data).unwrap();
self.store.data().with_draw_commands(f);
}
fn on_cursor_event(&mut self, panel: PanelId, kind: CursorEventKind, at: Vec2) {
let data = self.get_panel_data(panel);
self.on_cursor_event
.call(&mut self.store, (data, kind as u32, at.x, at.y))
.unwrap();
}
fn on_message(&mut self, panel: PanelId, msg: Vec<u8>) {
let data = self.get_panel_data(panel);
let msg = self.store.data().message_new(msg);
self.on_message.call(&mut self.store, (data, msg)).unwrap();
self.store.data().message_free(msg);
}
}
/// The standard [ScriptAbi] implementation to use.
#[derive(Default)]
pub struct ScriptAbiImpl {
draw_cmds: Mutex<Vec<DrawCommand>>,
font_store: text::FontStore,
font_families: Mutex<HashMap<String, u32>>,
loaded_fonts: RwLock<Vec<Arc<text::Font>>>,
text_layouts: RwLock<Slab<text::TextLayout>>,
message_store: RwLock<Slab<Vec<u8>>>,
}
impl ScriptAbi for ScriptAbiImpl {
fn start_draw(&self) {
let mut lock = self.draw_cmds.lock();
lock.clear();
}
fn draw_indexed(&self, vertices: &[MeshVertex], indices: &[MeshIndex]) {
self.draw_cmds.lock().push(DrawCommand::Mesh {
vertices: vertices.to_vec(),
indices: indices.to_vec(),
})
}
fn draw_text_layout(&self, id: u32, offset: Vec2, scale: f32, color: Color) {
// TODO multiple fonts per layout
let layouts = self.text_layouts.read();
let layout = layouts.get(id as usize).unwrap();
let glyphs = layout.glyphs.as_slice();
let loaded = self.loaded_fonts.read();
let font = loaded.get(layout.font_id as usize).unwrap();
let cmds = font.draw(glyphs, offset, scale, color);
self.draw_cmds.lock().extend(cmds.into_iter());
}
fn with_draw_commands(&self, f: impl FnOnce(&[DrawCommand])) {
f(self.draw_cmds.lock().as_slice());
}
fn font_load(&self, family: &str) -> u32 {
let mut family_cache = self.font_families.lock();
if let Some(cached) = family_cache.get(family) {
return *cached;
}
let font = self.font_store.load_font(family);
let mut loaded = self.loaded_fonts.write();
let id = loaded.len() as u32;
family_cache.insert(family.to_string(), id);
loaded.push(font);
id
}
fn text_layout_new(&self, font_id: u32, text: &str) -> u32 {
let loaded = self.loaded_fonts.read();
let font = loaded.get(font_id as usize).unwrap();
let layout = font.shape(text);
self.text_layouts.write().insert(layout) as u32
}
fn text_layout_delete(&self, id: u32) {
self.text_layouts.write().remove(id as usize);
}
fn text_layout_get_bounds(&self, id: u32, dst: &mut Rect) {
let src = self.text_layouts.read().get(id as usize).unwrap().bounds;
let _ = std::mem::replace(dst, src);
}
fn message_new(&self, data: Vec<u8>) -> u32 {
let mut store = self.message_store.write();
let id = store.insert(data) as u32;
id
}
fn message_free(&self, id: u32) {
let mut store = self.message_store.write();
store.remove(id as usize);
}
fn message_get_len(&self, id: u32) -> u32 {
self.message_store.read().get(id as usize).unwrap().len() as u32
}
fn message_get_data(&self, id: u32, dst: &mut [u8]) {
let store = self.message_store.read();
let src = store.get(id as usize).unwrap();
dst.copy_from_slice(src);
}
}