devalang_wasm/engine/plugin/
runner.rs

1use std::cell::RefCell;
2use std::collections::HashMap;
3#[cfg(feature = "cli")]
4use wasmtime::{Engine, Instance, Linker, Module, Store};
5
6#[cfg(feature = "cli")]
7pub struct WasmPluginRunner {
8    engine: Engine,
9    // Cache instances by WASM hash to reuse state
10    cache: RefCell<HashMap<u64, (Store<()>, Instance)>>,
11    // Cache last parameters per synth_id to restore on each note
12    last_params: RefCell<HashMap<String, HashMap<String, f32>>>,
13}
14
15#[cfg(feature = "cli")]
16impl Default for WasmPluginRunner {
17    fn default() -> Self {
18        Self::new()
19    }
20}
21
22#[cfg(feature = "cli")]
23impl WasmPluginRunner {
24    pub fn new() -> Self {
25        let engine = Engine::default();
26        Self {
27            engine,
28            cache: RefCell::new(HashMap::new()),
29            last_params: RefCell::new(HashMap::new()),
30        }
31    }
32
33    /// Renders a note using a WASM plugin with optional parameter overrides
34    ///
35    /// Tries multiple function name patterns in order:
36    /// 1. Named export (if synth_name provided): e.g. "synth", "saw"
37    /// 2. Generic "render_note"
38    pub fn render_note_in_place(
39        &self,
40        wasm_bytes: &[u8],
41        buffer: &mut [f32],
42        instance_key: Option<&str>, // ← NEW: For cache hash ("acidScreamer" / "acidSquare")
43        synth_name: Option<&str>,   // ← Function to call ("synth")
44        freq: f32,
45        amp: f32,
46        duration_ms: i32,
47        sample_rate: i32,
48        channels: i32,
49        options: Option<&HashMap<String, f32>>,
50    ) -> Result<(), String> {
51        // Hash du WASM + instance_key pour avoir une instance par synth!
52        use std::collections::hash_map::DefaultHasher;
53        use std::hash::{Hash, Hasher};
54        let mut hasher = DefaultHasher::new();
55        wasm_bytes.hash(&mut hasher);
56        instance_key.hash(&mut hasher); // Use instance_key (synth_id) for caching!
57        let hash = hasher.finish();
58
59        // Mutably borrow the cache
60        let mut cache = self.cache.borrow_mut();
61
62        // Create instance if it doesn't exist
63        if !cache.contains_key(&hash) {
64            // Creating new instance for synth_id
65            let module = Module::new(&self.engine, wasm_bytes)
66                .map_err(|e| format!("Failed to compile wasm: {e}"))?;
67
68            let mut store = Store::new(&self.engine, ());
69            let mut linker = Linker::new(&self.engine);
70
71            // Add wasm-bindgen placeholder imports (for plugins compiled with wasm-bindgen)
72            linker
73                .func_wrap(
74                    "__wbindgen_placeholder__",
75                    "__wbindgen_describe",
76                    |_: i32| {},
77                )
78                .map_err(|e| format!("Failed to define wbindgen import: {e}"))?;
79            linker
80                .func_wrap(
81                    "__wbindgen_placeholder__",
82                    "__wbindgen_object_clone_ref",
83                    |_: i32| -> i32 { 0 },
84                )
85                .map_err(|e| format!("Failed to define wbindgen import: {e}"))?;
86            linker
87                .func_wrap(
88                    "__wbindgen_placeholder__",
89                    "__wbindgen_object_drop_ref",
90                    |_: i32| {},
91                )
92                .map_err(|e| format!("Failed to define wbindgen import: {e}"))?;
93            linker
94                .func_wrap(
95                    "__wbindgen_placeholder__",
96                    "__wbindgen_string_new",
97                    |_: i32, _: i32| -> i32 { 0 },
98                )
99                .map_err(|e| format!("Failed to define wbindgen import: {e}"))?;
100            linker
101                .func_wrap(
102                    "__wbindgen_placeholder__",
103                    "__wbindgen_throw",
104                    |_: i32, _: i32| {},
105                )
106                .map_err(|e| format!("Failed to define wbindgen import: {e}"))?;
107
108            let instance = linker
109                .instantiate(&mut store, &module)
110                .map_err(|e| format!("Failed to instantiate wasm: {e}"))?;
111
112            cache.insert(hash, (store, instance));
113        } else {
114            // Reusing cached instance for synth_id
115        }
116
117        // Retrieve cached instance
118        let entry = cache.get_mut(&hash).unwrap();
119
120        let memory = entry
121            .1
122            .get_memory(&mut entry.0, "memory")
123            .ok_or_else(|| "WASM memory export not found".to_string())?;
124
125        // Find the right function to call
126        let func_name = if let Some(name) = synth_name {
127            // Try the named export first
128            if entry.1.get_func(&mut entry.0, name).is_some() {
129                name
130            } else if entry.1.get_func(&mut entry.0, "render_note").is_some() {
131                "render_note"
132            } else {
133                return Err(format!("Plugin export '{}' not found", name));
134            }
135        } else {
136            "render_note"
137        };
138
139        let func = entry
140            .1
141            .get_typed_func::<(i32, i32, f32, f32, i32, i32, i32), ()>(&mut entry.0, func_name)
142            .map_err(|e| {
143                format!(
144                    "Function '{}' not found or wrong signature: {}",
145                    func_name, e
146                )
147            })?;
148
149        // Call reset() FIRST if available (to reset phase/envelope between notes while preserving parameters)
150        if let Ok(reset_func) = entry.1.get_typed_func::<f32, ()>(&mut entry.0, "setReset") {
151            let _ = reset_func.call(&mut entry.0, 1.0);
152        }
153
154        // Map of parameter names to setter functions
155        let setter_map: HashMap<&str, &str> = [
156            ("waveform", "setWaveform"),
157            ("cutoff", "setCutoff"),
158            ("resonance", "setResonance"),
159            ("env_mod", "setEnvMod"),
160            ("decay", "setDecay"),
161            ("accent", "setAccent"),
162            ("drive", "setDrive"),
163            ("tone", "setTone"),
164            ("slide", "setSlide"),
165            ("glide", "setGlide"),
166        ]
167        .iter()
168        .cloned()
169        .collect();
170
171        // Get mutable access to cached params and prepare params to apply
172        let synth_id_str = instance_key.unwrap_or("default").to_string();
173
174        let params_to_apply = {
175            let mut last_params = self.last_params.borrow_mut();
176            let current_params = last_params
177                .entry(synth_id_str.clone())
178                .or_insert_with(HashMap::new);
179
180            // If new options provided, update cache and use them
181            if let Some(opts) = options {
182                for (key, value) in opts.iter() {
183                    current_params.insert(key.clone(), *value);
184                }
185                opts.clone()
186            } else {
187                current_params.clone()
188            }
189        };
190
191        // Now apply the params with setters
192        for (key, value) in params_to_apply.iter() {
193            if let Some(setter_name) = setter_map.get(key.as_str()) {
194                if let Ok(setter) = entry.1.get_typed_func::<f32, ()>(&mut entry.0, setter_name) {
195                    let _ = setter.call(&mut entry.0, *value);
196                }
197            }
198        }
199
200        // Allocate memory in WASM for the buffer
201        let byte_len = std::mem::size_of_val(buffer);
202        let ptr = Self::alloc_temp(&mut entry.0, &entry.1, &memory, byte_len)? as i32;
203
204        // Copy buffer into WASM memory
205        let mem_slice = memory
206            .data_mut(&mut entry.0)
207            .get_mut(ptr as usize..(ptr as usize) + byte_len)
208            .ok_or_else(|| "Failed to get memory slice".to_string())?;
209        let src_bytes =
210            unsafe { std::slice::from_raw_parts(buffer.as_ptr() as *const u8, byte_len) };
211        mem_slice.copy_from_slice(src_bytes);
212
213        // Call the plugin function
214        func.call(
215            &mut entry.0,
216            (
217                ptr,
218                buffer.len() as i32,
219                freq,
220                amp,
221                duration_ms,
222                sample_rate,
223                channels,
224            ),
225        )
226        .map_err(|e| format!("Error calling '{}': {}", func_name, e))?;
227
228        // Copy result back from WASM memory
229        let mem_slice_after = memory
230            .data(&entry.0)
231            .get(ptr as usize..(ptr as usize) + byte_len)
232            .ok_or_else(|| "Failed to get memory slice after".to_string())?;
233        let dst_bytes =
234            unsafe { std::slice::from_raw_parts_mut(buffer.as_mut_ptr() as *mut u8, byte_len) };
235        dst_bytes.copy_from_slice(mem_slice_after);
236
237        // Debug: Check if buffer has any non-zero values
238        let _non_zero = buffer.iter().any(|&x| x.abs() > 0.0001);
239        let _max_val = buffer.iter().map(|&x| x.abs()).fold(0.0f32, f32::max);
240        // Plugin result diagnostics
241
242        Ok(())
243    }
244
245    /// Helper to allocate temporary memory in WASM module
246    fn alloc_temp(
247        store: &mut Store<()>,
248        _instance: &Instance,
249        memory: &wasmtime::Memory,
250        size: usize,
251    ) -> Result<i32, String> {
252        // Simple bump allocator: just use end of current memory
253        let pages = memory.size(&*store);
254        let current_size = pages * 65536; // WASM page size
255        let ptr = current_size as i32;
256
257        // Grow memory if needed
258        let needed_pages = ((size + 65535) / 65536) as u64;
259        if needed_pages > 0 {
260            memory
261                .grow(store, needed_pages)
262                .map_err(|e| format!("Failed to grow memory: {}", e))?;
263        }
264
265        Ok(ptr)
266    }
267}