Skip to main content

maolan_engine/plugins/
vst3_proc.rs

1//! Out-of-process VST3 processor using `maolan-engine-plugin-host` IPC.
2
3use crate::audio::io::AudioIO;
4use crate::midi::io::MidiEvent;
5use crate::mutex::UnsafeMutex;
6use crate::vst3::port::ParameterInfo;
7use crate::vst3::state::Vst3PluginState;
8use maolan_plugin_protocol::events::EventPair;
9use maolan_plugin_protocol::protocol::*;
10use maolan_plugin_protocol::ringbuf::RingBuffer;
11use maolan_plugin_protocol::shm::ShmMapping;
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::process::{Child, Command, Stdio};
15use std::sync::atomic::{AtomicBool, Ordering};
16use std::sync::{Arc, atomic::AtomicU32};
17use std::time::{Duration, Instant};
18
19/// Shared state for an out-of-process VST3 plugin instance.
20pub struct Vst3Processor {
21    path: String,
22    name: String,
23    audio_inputs: Vec<Arc<AudioIO>>,
24    audio_outputs: Vec<Arc<AudioIO>>,
25    main_audio_inputs: usize,
26    main_audio_outputs: usize,
27    param_infos: Vec<ParameterInfo>,
28    param_values: UnsafeMutex<HashMap<u32, f64>>,
29    bypassed: Arc<AtomicBool>,
30    // IPC state
31    child: UnsafeMutex<Option<Child>>,
32    mapping: Option<ShmMapping>,
33    events: Option<EventPair>,
34    shm_name: String,
35    // Crash recovery
36    crash_count: AtomicU32,
37    last_process_time: UnsafeMutex<Instant>,
38}
39
40pub type SharedVst3Processor = Arc<UnsafeMutex<Vst3Processor>>;
41
42impl Vst3Processor {
43    pub fn new(
44        sample_rate: f64,
45        buffer_size: usize,
46        plugin_path: &str,
47        input_count: usize,
48        output_count: usize,
49        host_binary: PathBuf,
50    ) -> Result<Self, String> {
51        let audio_inputs = (0..input_count.max(1))
52            .map(|_| Arc::new(AudioIO::new(buffer_size)))
53            .collect::<Vec<_>>();
54        let audio_outputs = (0..output_count.max(1))
55            .map(|_| Arc::new(AudioIO::new(buffer_size)))
56            .collect::<Vec<_>>();
57
58        let instance_id = format!("vst3-{}", std::process::id());
59        let (mut child, mapping, events, shm_name) = spawn_host(
60            &host_binary,
61            plugin_path,
62            &instance_id,
63            sample_rate,
64            buffer_size,
65            input_count.max(1),
66            output_count.max(1),
67        )?;
68
69        let header = unsafe { header_ref(mapping.as_ptr()) };
70        if !wait_for_ready(header, Duration::from_secs(10)) {
71            let _ = child.kill();
72            return Err("VST3 host did not signal ready".to_string());
73        }
74
75        let param_infos = Vec::new();
76        let name = Path::new(plugin_path)
77            .file_stem()
78            .and_then(|s| s.to_str())
79            .unwrap_or("VST3")
80            .to_string();
81
82        Ok(Self {
83            path: plugin_path.to_string(),
84            name,
85            audio_inputs,
86            audio_outputs,
87            main_audio_inputs: input_count.max(1),
88            main_audio_outputs: output_count.max(1),
89            param_infos,
90            param_values: UnsafeMutex::new(HashMap::new()),
91            bypassed: Arc::new(AtomicBool::new(false)),
92            child: UnsafeMutex::new(Some(child)),
93            mapping: Some(mapping),
94            events: Some(events),
95            shm_name,
96            crash_count: AtomicU32::new(0),
97            last_process_time: UnsafeMutex::new(Instant::now()),
98        })
99    }
100
101    pub fn setup_audio_ports(&self) {
102        for port in &self.audio_inputs {
103            port.setup();
104        }
105        for port in &self.audio_outputs {
106            port.setup();
107        }
108    }
109
110    pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
111        &self.audio_inputs
112    }
113
114    pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
115        &self.audio_outputs
116    }
117
118    pub fn main_audio_input_count(&self) -> usize {
119        self.main_audio_inputs
120    }
121
122    pub fn main_audio_output_count(&self) -> usize {
123        self.main_audio_outputs
124    }
125
126    pub fn midi_input_count(&self) -> usize {
127        0
128    }
129
130    pub fn midi_output_count(&self) -> usize {
131        0
132    }
133
134    pub fn set_bypassed(&self, bypassed: bool) {
135        self.bypassed.store(bypassed, Ordering::Relaxed);
136    }
137
138    pub fn is_bypassed(&self) -> bool {
139        self.bypassed.load(Ordering::Relaxed)
140    }
141
142    pub fn parameter_infos(&self) -> Vec<ParameterInfo> {
143        self.param_infos.clone()
144    }
145
146    pub fn parameter_values(&self) -> HashMap<u32, f64> {
147        self.param_values.lock().clone()
148    }
149
150    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
151        self.set_parameter_at(param_id, value, 0)
152    }
153
154    pub fn set_parameter_at(&self, param_id: u32, value: f64, _frame: u32) -> Result<(), String> {
155        self.param_values.lock().insert(param_id, value);
156        if let Some(ref mapping) = self.mapping {
157            let ring = unsafe {
158                let buf = param_ring_ptr(mapping.as_ptr());
159                let (w, r) = param_indices(mapping.as_ptr());
160                RingBuffer::new(buf, w, r, RING_CAPACITY)
161            };
162            let ev = ParameterEvent {
163                param_index: param_id,
164                value: value as f32,
165                sample_offset: 0,
166                event_kind: maolan_plugin_protocol::PARAM_EVENT_VALUE,
167            };
168            if !ring.push(ev) {
169                tracing::warn!("VST3 param ring full, dropping parameter event");
170            }
171        }
172        Ok(())
173    }
174
175    pub fn begin_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
176        Ok(())
177    }
178
179    pub fn end_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
180        Ok(())
181    }
182
183    pub fn is_parameter_edit_active(&self, _param_id: u32) -> bool {
184        false
185    }
186
187    pub fn snapshot_state(&self) -> Result<Vst3PluginState, String> {
188        let (mapping, events) = match (&self.mapping, &self.events) {
189            (Some(m), Some(e)) => (m, e),
190            _ => return Err("VST3 processor not initialized".to_string()),
191        };
192        let ptr = mapping.as_ptr();
193        let header = unsafe { header_mut(ptr) };
194
195        // Signal host to save state.
196        header.request_type.store(1, Ordering::Release);
197        header.request_status.store(0, Ordering::Release);
198        if let Err(e) = events.signal_host() {
199            header.request_type.store(0, Ordering::Release);
200            return Err(format!("Failed to signal host for state save: {}", e));
201        }
202
203        // Wait for host to complete (up to 5 seconds).
204        if let Err(e) = events.wait_host(Duration::from_secs(5)) {
205            header.request_type.store(0, Ordering::Release);
206            return Err(format!("Host did not respond to state save: {}", e));
207        }
208
209        let status = header.request_status.load(Ordering::Acquire);
210        let size = header.scratch_size.load(Ordering::Acquire) as usize;
211        if status != 1 {
212            header.request_type.store(0, Ordering::Release);
213            return Err("State save failed in host".to_string());
214        }
215
216        let scratch = unsafe { scratch_ptr(ptr) };
217        let state = deserialize_vst3_state(scratch, size)?;
218        header.request_type.store(0, Ordering::Release);
219        Ok(state)
220    }
221
222    pub fn restore_state(&self, state: &Vst3PluginState) -> Result<(), String> {
223        let (mapping, events) = match (&self.mapping, &self.events) {
224            (Some(m), Some(e)) => (m, e),
225            _ => return Err("VST3 processor not initialized".to_string()),
226        };
227        let ptr = mapping.as_ptr();
228        let header = unsafe { header_mut(ptr) };
229
230        // Serialize state into scratch area.
231        let scratch = unsafe { scratch_ptr(ptr) };
232        let size = serialize_vst3_state(scratch, state)?;
233        header.scratch_size.store(size as u32, Ordering::Release);
234
235        // Signal host to restore state.
236        header.request_type.store(2, Ordering::Release);
237        header.request_status.store(0, Ordering::Release);
238        if let Err(e) = events.signal_host() {
239            header.request_type.store(0, Ordering::Release);
240            return Err(format!("Failed to signal host for state restore: {}", e));
241        }
242
243        // Wait for host to complete (up to 5 seconds).
244        if let Err(e) = events.wait_host(Duration::from_secs(5)) {
245            header.request_type.store(0, Ordering::Release);
246            return Err(format!("Host did not respond to state restore: {}", e));
247        }
248
249        let status = header.request_status.load(Ordering::Acquire);
250        header.request_type.store(0, Ordering::Release);
251        if status != 1 {
252            return Err("State restore failed in host".to_string());
253        }
254        Ok(())
255    }
256
257    pub fn process_with_audio_io(&self, frames: usize) {
258        let _ = self.process_with_midi(frames, &[]);
259    }
260
261    pub fn process_with_midi(&self, frames: usize, _midi_in: &[MidiEvent]) -> Vec<MidiEvent> {
262        if self.bypassed.load(Ordering::Relaxed) {
263            self.bypass_copy_inputs_to_outputs();
264            return Vec::new();
265        }
266
267        // Check if host process has crashed.
268        {
269            let child = self.child.lock();
270            if let Some(ref mut c) = child.as_mut() {
271                match c.try_wait() {
272                    Ok(Some(status)) if !status.success() => {
273                        tracing::error!(
274                            "VST3 plugin host crashed for '{}' ({})",
275                            self.name,
276                            self.path
277                        );
278                        self.crash_count.fetch_add(1, Ordering::Relaxed);
279                        self.bypass_copy_inputs_to_outputs();
280                        return Vec::new();
281                    }
282                    Ok(None) => {
283                        eprintln!("[VST3 debug] host still alive");
284                    }
285                    Ok(Some(status)) => {
286                        eprintln!("[VST3 debug] host exited with success: {:?}", status);
287                    }
288                    Err(e) => {
289                        eprintln!("[VST3 debug] try_wait error: {}", e);
290                    }
291                }
292            }
293        }
294
295        let started = Instant::now();
296
297        let (mapping, events) = match (&self.mapping, &self.events) {
298            (Some(m), Some(e)) => (m, e),
299            _ => {
300                self.bypass_copy_inputs_to_outputs();
301                return Vec::new();
302            }
303        };
304
305        let ptr = mapping.as_ptr();
306        let num_in = self.audio_inputs.len();
307        let num_out = self.audio_outputs.len();
308        unsafe {
309            let h = header_mut(ptr);
310            h.block_size.store(frames as u32, Ordering::Release);
311            h.num_input_channels.store(num_in as u32, Ordering::Release);
312            h.num_output_channels
313                .store(num_out as u32, Ordering::Release);
314            // Write default transport state (can be overridden by track later).
315            let t = transport_mut(ptr);
316            t.playhead_sample = 0;
317            t.tempo = 120.0;
318            t.numerator = 4;
319            t.denominator = 4;
320            t.flags = 1; // playing
321        }
322
323        // Copy input AudioIO buffers to shared memory (bus 0).
324        for (ch, input) in self.audio_inputs.iter().enumerate() {
325            let src = input.buffer.lock();
326            let dst = unsafe { audio_channel_ptr(ptr, ch, 0) };
327            let len = frames.min(src.len());
328            unsafe {
329                std::ptr::copy_nonoverlapping(src.as_ptr(), dst, len);
330            }
331        }
332
333        // Signal host to process.
334        if let Err(e) = events.signal_host() {
335            tracing::error!("Failed to signal VST3 host: {e}");
336            self.bypass_copy_inputs_to_outputs();
337            return Vec::new();
338        }
339        eprintln!("[VST3 debug] signal_host succeeded");
340
341        // Wait for host to complete (with timeout).
342        let timeout = Duration::from_millis(100);
343        match events.wait_host(timeout) {
344            Ok(()) => {
345                eprintln!("[VST3 debug] wait_host succeeded");
346            }
347            Err(e) => {
348                eprintln!(
349                    "[VST3 debug] host did not respond for '{}' ({}): {}",
350                    self.name, self.path, e
351                );
352                self.bypass_copy_inputs_to_outputs();
353                return Vec::new();
354            }
355        }
356
357        // Copy output shared memory (bus 1) back to AudioIO buffers.
358        for (ch, output) in self.audio_outputs.iter().enumerate() {
359            let dst = output.buffer.lock();
360            let src = unsafe { audio_channel_ptr(ptr, ch, 1) };
361            let len = frames.min(dst.len());
362            unsafe {
363                std::ptr::copy_nonoverlapping(src, dst.as_mut_ptr(), len);
364            }
365            *output.finished.lock() = true;
366        }
367
368        let elapsed = started.elapsed();
369        if elapsed > Duration::from_millis(20) {
370            tracing::warn!(
371                "Slow VST3 process '{}' ({}) took {:.3} ms for {} frames",
372                self.name,
373                self.path,
374                elapsed.as_secs_f64() * 1000.0,
375                frames
376            );
377        }
378
379        *self.last_process_time.lock() = Instant::now();
380        Vec::new()
381    }
382
383    fn bypass_copy_inputs_to_outputs(&self) {
384        for (input, output) in self.audio_inputs.iter().zip(self.audio_outputs.iter()) {
385            let src = input.buffer.lock();
386            let dst = output.buffer.lock();
387            dst.fill(0.0);
388            for (d, s) in dst.iter_mut().zip(src.iter()) {
389                *d = *s;
390            }
391            *output.finished.lock() = true;
392        }
393        for output in self.audio_outputs.iter().skip(self.audio_inputs.len()) {
394            output.buffer.lock().fill(0.0);
395            *output.finished.lock() = true;
396        }
397    }
398
399    pub fn path(&self) -> &str {
400        &self.path
401    }
402
403    pub fn name(&self) -> &str {
404        &self.name
405    }
406
407    pub fn begin_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
408        Ok(())
409    }
410
411    pub fn end_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
412        Ok(())
413    }
414
415    pub fn run_host_callbacks_main_thread(&self) {}
416
417    pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
418        Ok(false)
419    }
420
421    pub fn ui_begin_session(&self) {}
422    pub fn ui_end_session(&self) {}
423    pub fn ui_should_close(&self) -> bool {
424        false
425    }
426    pub fn ui_take_due_timers(&self) -> Vec<u32> {
427        Vec::new()
428    }
429    pub fn ui_take_param_updates(&self) -> Vec<(u32, f64)> {
430        Vec::new()
431    }
432    pub fn ui_take_state_update(&self) -> Option<Vst3PluginState> {
433        None
434    }
435
436    pub fn gui_info(&self) -> Result<crate::vst3::interfaces::Vst3GuiInfo, String> {
437        Err("GUI not yet supported for VST3 plugins".to_string())
438    }
439
440    pub fn gui_create(&self, _platform_type: &str) -> Result<(), String> {
441        Err("GUI not yet supported for VST3 plugins".to_string())
442    }
443
444    pub fn gui_get_size(&self) -> Result<(i32, i32), String> {
445        Err("GUI not yet supported for VST3 plugins".to_string())
446    }
447
448    pub fn gui_set_parent(&self, _window: usize, _platform_type: &str) -> Result<(), String> {
449        Err("GUI not yet supported for VST3 plugins".to_string())
450    }
451
452    pub fn gui_on_size(&self, _width: i32, _height: i32) -> Result<(), String> {
453        Err("GUI not yet supported for VST3 plugins".to_string())
454    }
455
456    pub fn gui_show(&self) -> Result<(), String> {
457        if let Some(ref mapping) = self.mapping
458            && let Some(ref events) = self.events
459        {
460            let header = unsafe { header_mut(mapping.as_ptr()) };
461            header.request_type.store(3, Ordering::Release);
462            let _ = events.signal_host();
463            return Ok(());
464        }
465        Err("No active host to show GUI".to_string())
466    }
467
468    pub fn gui_hide(&self) {
469        if let Some(ref mapping) = self.mapping
470            && let Some(ref events) = self.events
471        {
472            let header = unsafe { header_mut(mapping.as_ptr()) };
473            header.request_type.store(4, Ordering::Release);
474            let _ = events.signal_host();
475        }
476    }
477
478    pub fn gui_destroy(&self) {}
479
480    pub fn gui_on_main_thread(&self) {}
481
482    pub fn gui_on_timer(&self, _timer_id: u32) {}
483
484    pub fn gui_check_resize(&self) -> Option<(i32, i32)> {
485        None
486    }
487
488    pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
489        let mut result = Vec::new();
490        if let Some(ref mapping) = self.mapping {
491            let ring = unsafe {
492                let buf = echo_ring_ptr(mapping.as_ptr());
493                let (w, r) = echo_indices(mapping.as_ptr());
494                RingBuffer::new(buf, w, r, RING_CAPACITY)
495            };
496            while let Some(ev) = ring.pop() {
497                result.push(ev);
498            }
499        }
500        result
501    }
502}
503
504impl Drop for Vst3Processor {
505    fn drop(&mut self) {
506        if let Some(ref mapping) = self.mapping
507            && let Some(ref events) = self.events
508        {
509            let header = unsafe { header_mut(mapping.as_ptr()) };
510            header.shutdown_request.store(1, Ordering::Release);
511            let _ = events.signal_host();
512        }
513        let mut child_opt = self.child.lock().take();
514        if let Some(mut child) = child_opt.take() {
515            let start = Instant::now();
516            while start.elapsed() < Duration::from_secs(2) {
517                if child.try_wait().map(|s| s.is_some()).unwrap_or(true) {
518                    break;
519                }
520                std::thread::sleep(Duration::from_millis(10));
521            }
522            if child.try_wait().map(|s| s.is_none()).unwrap_or(false) {
523                let _ = child.kill();
524            }
525        }
526        let _ = ShmMapping::unlink(&self.shm_name);
527    }
528}
529
530impl UnsafeMutex<Vst3Processor> {
531    pub fn setup_audio_ports(&self) {
532        self.lock().setup_audio_ports();
533    }
534
535    pub fn process_with_midi(&self, frames: usize, midi_events: &[MidiEvent]) -> Vec<MidiEvent> {
536        self.lock().process_with_midi(frames, midi_events)
537    }
538
539    pub fn set_bypassed(&self, bypassed: bool) {
540        self.lock().set_bypassed(bypassed);
541    }
542
543    pub fn is_bypassed(&self) -> bool {
544        self.lock().is_bypassed()
545    }
546
547    pub fn parameter_infos(&self) -> Vec<ParameterInfo> {
548        self.lock().parameter_infos()
549    }
550
551    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
552        self.lock().set_parameter(param_id, value)
553    }
554
555    pub fn set_parameter_at(&self, param_id: u32, value: f64, frame: u32) -> Result<(), String> {
556        self.lock().set_parameter_at(param_id, value, frame)
557    }
558
559    pub fn begin_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
560        self.lock().begin_parameter_edit_at(param_id, frame)
561    }
562
563    pub fn end_parameter_edit_at(&self, param_id: u32, frame: u32) -> Result<(), String> {
564        self.lock().end_parameter_edit_at(param_id, frame)
565    }
566
567    pub fn snapshot_state(&self) -> Result<Vst3PluginState, String> {
568        self.lock().snapshot_state()
569    }
570
571    pub fn restore_state(&self, state: &Vst3PluginState) -> Result<(), String> {
572        self.lock().restore_state(state)
573    }
574
575    pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
576        self.lock().audio_inputs()
577    }
578
579    pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
580        self.lock().audio_outputs()
581    }
582
583    pub fn main_audio_input_count(&self) -> usize {
584        self.lock().main_audio_input_count()
585    }
586
587    pub fn main_audio_output_count(&self) -> usize {
588        self.lock().main_audio_output_count()
589    }
590
591    pub fn midi_input_count(&self) -> usize {
592        self.lock().midi_input_count()
593    }
594
595    pub fn midi_output_count(&self) -> usize {
596        self.lock().midi_output_count()
597    }
598
599    pub fn path(&self) -> String {
600        self.lock().path().to_string()
601    }
602
603    pub fn name(&self) -> String {
604        self.lock().name().to_string()
605    }
606
607    pub fn run_host_callbacks_main_thread(&self) {
608        self.lock().run_host_callbacks_main_thread();
609    }
610
611    pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
612        self.lock().reconfigure_ports_if_needed()
613    }
614
615    pub fn ui_begin_session(&self) {
616        self.lock().ui_begin_session();
617    }
618
619    pub fn ui_end_session(&self) {
620        self.lock().ui_end_session();
621    }
622
623    pub fn ui_should_close(&self) -> bool {
624        self.lock().ui_should_close()
625    }
626
627    pub fn ui_take_due_timers(&self) -> Vec<u32> {
628        self.lock().ui_take_due_timers()
629    }
630
631    pub fn ui_take_param_updates(&self) -> Vec<(u32, f64)> {
632        self.lock().ui_take_param_updates()
633    }
634
635    pub fn ui_take_state_update(&self) -> Option<Vst3PluginState> {
636        self.lock().ui_take_state_update()
637    }
638
639    pub fn gui_info(&self) -> Result<crate::vst3::interfaces::Vst3GuiInfo, String> {
640        self.lock().gui_info()
641    }
642
643    pub fn gui_create(&self, platform_type: &str) -> Result<(), String> {
644        self.lock().gui_create(platform_type)
645    }
646
647    pub fn gui_get_size(&self) -> Result<(i32, i32), String> {
648        self.lock().gui_get_size()
649    }
650
651    pub fn gui_set_parent(&self, window: usize, platform_type: &str) -> Result<(), String> {
652        self.lock().gui_set_parent(window, platform_type)
653    }
654
655    pub fn gui_on_size(&self, width: i32, height: i32) -> Result<(), String> {
656        self.lock().gui_on_size(width, height)
657    }
658
659    pub fn gui_show(&self) -> Result<(), String> {
660        self.lock().gui_show()
661    }
662
663    pub fn gui_hide(&self) {
664        self.lock().gui_hide();
665    }
666
667    pub fn gui_destroy(&self) {
668        self.lock().gui_destroy();
669    }
670
671    pub fn gui_on_main_thread(&self) {
672        self.lock().gui_on_main_thread();
673    }
674
675    pub fn gui_on_timer(&self, timer_id: u32) {
676        self.lock().gui_on_timer(timer_id);
677    }
678
679    pub fn gui_check_resize(&self) -> Option<(i32, i32)> {
680        self.lock().gui_check_resize()
681    }
682}
683
684/// Locate the `maolan-engine-plugin-host` binary at runtime.
685pub fn find_host_binary() -> Option<PathBuf> {
686    let exe_dir = std::env::current_exe()
687        .ok()
688        .and_then(|p| p.parent().map(PathBuf::from));
689
690    // 1. Same directory as current executable.
691    if let Some(ref dir) = exe_dir {
692        let candidate = dir.join("maolan-engine-plugin-host");
693        if candidate.exists() {
694            return Some(candidate);
695        }
696    }
697
698    // 2. Engine target directory (development).
699    if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
700        let engine_root = Path::new(&manifest);
701        for profile in ["debug", "release"] {
702            let candidate = engine_root
703                .join("target")
704                .join(profile)
705                .join("maolan-engine-plugin-host");
706            if candidate.exists() {
707                return Some(candidate);
708            }
709        }
710    }
711
712    // 3. Development workspace paths (DAW target dir).
713    if let Ok(manifest) = std::env::var("CARGO_MANIFEST_DIR") {
714        let engine_root = Path::new(&manifest);
715        for profile in ["debug", "release"] {
716            let candidate = engine_root
717                .parent()
718                .unwrap_or(Path::new(""))
719                .join("daw")
720                .join("target")
721                .join(profile)
722                .join("maolan-engine-plugin-host");
723            if candidate.exists() {
724                return Some(candidate);
725            }
726        }
727    }
728
729    // 4. PATH.
730    if let Ok(path_var) = std::env::var("PATH") {
731        for dir in path_var.split(':') {
732            let candidate = Path::new(dir).join("maolan-engine-plugin-host");
733            if candidate.exists() {
734                return Some(candidate);
735            }
736        }
737    }
738
739    None
740}
741
742fn spawn_host(
743    host_binary: &PathBuf,
744    plugin_path: &str,
745    instance_id: &str,
746    sample_rate: f64,
747    buffer_size: usize,
748    num_inputs: usize,
749    num_outputs: usize,
750) -> Result<(Child, ShmMapping, EventPair, String), String> {
751    let pid = std::process::id();
752    let shm_name = format!("/maolan-{pid}-{instance_id}");
753
754    let mapping = ShmMapping::create(&shm_name, SHM_SIZE)?;
755    unsafe {
756        init_shm_layout(mapping.as_ptr(), mapping.size());
757    }
758
759    let mut events = EventPair::new().map_err(|e| format!("failed to create pipes: {e}"))?;
760
761    let mut cmd = Command::new(host_binary);
762    cmd.arg("vst3")
763        .arg(plugin_path)
764        .arg(&shm_name)
765        .arg(instance_id)
766        .arg(events.host_read_fd().to_string())
767        .arg(events.host_write_fd().to_string())
768        .arg(sample_rate.to_string())
769        .arg(buffer_size.to_string())
770        .arg(num_inputs.to_string())
771        .arg(num_outputs.to_string())
772        .stdin(Stdio::null())
773        .stdout(Stdio::null())
774        .stderr(Stdio::inherit());
775
776    let child = cmd
777        .spawn()
778        .map_err(|e| format!("failed to spawn VST3 host: {e}"))?;
779
780    events.close_daw_unused();
781
782    Ok((child, mapping, events, shm_name))
783}
784
785fn wait_for_ready(header: &ShmHeader, timeout: Duration) -> bool {
786    let start = Instant::now();
787    while start.elapsed() < timeout {
788        if header.ready.load(Ordering::Acquire) != 0 {
789            return true;
790        }
791        std::thread::sleep(Duration::from_millis(10));
792    }
793    false
794}
795
796/// Serialize VST3 state into scratch area. Returns bytes written or error.
797fn serialize_vst3_state(scratch: *mut u8, state: &Vst3PluginState) -> Result<usize, String> {
798    let max_len = maolan_plugin_protocol::protocol::SCRATCH_SIZE;
799    let mut offset = 0usize;
800
801    let plugin_id_bytes = state.plugin_id.as_bytes();
802    if offset + 4 > max_len {
803        return Err("scratch overflow".to_string());
804    }
805    unsafe {
806        std::ptr::write_unaligned(
807            scratch.add(offset) as *mut u32,
808            plugin_id_bytes.len() as u32,
809        );
810    }
811    offset += 4;
812    if offset + plugin_id_bytes.len() > max_len {
813        return Err("scratch overflow".to_string());
814    }
815    unsafe {
816        std::ptr::copy_nonoverlapping(
817            plugin_id_bytes.as_ptr(),
818            scratch.add(offset),
819            plugin_id_bytes.len(),
820        );
821    }
822    offset += plugin_id_bytes.len();
823
824    if offset + 4 > max_len {
825        return Err("scratch overflow".to_string());
826    }
827    unsafe {
828        std::ptr::write_unaligned(
829            scratch.add(offset) as *mut u32,
830            state.component_state.len() as u32,
831        );
832    }
833    offset += 4;
834    if offset + state.component_state.len() > max_len {
835        return Err("scratch overflow".to_string());
836    }
837    unsafe {
838        std::ptr::copy_nonoverlapping(
839            state.component_state.as_ptr(),
840            scratch.add(offset),
841            state.component_state.len(),
842        );
843    }
844    offset += state.component_state.len();
845
846    if offset + 4 > max_len {
847        return Err("scratch overflow".to_string());
848    }
849    unsafe {
850        std::ptr::write_unaligned(
851            scratch.add(offset) as *mut u32,
852            state.controller_state.len() as u32,
853        );
854    }
855    offset += 4;
856    if offset + state.controller_state.len() > max_len {
857        return Err("scratch overflow".to_string());
858    }
859    unsafe {
860        std::ptr::copy_nonoverlapping(
861            state.controller_state.as_ptr(),
862            scratch.add(offset),
863            state.controller_state.len(),
864        );
865    }
866    offset += state.controller_state.len();
867
868    Ok(offset)
869}
870
871/// Deserialize VST3 state from scratch area.
872fn deserialize_vst3_state(scratch: *const u8, size: usize) -> Result<Vst3PluginState, String> {
873    if size < 12 {
874        return Err("scratch too small for VST3 state".to_string());
875    }
876    let mut offset = 0usize;
877
878    let plugin_id_len =
879        unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
880    offset += 4;
881    if offset + plugin_id_len > size {
882        return Err("scratch underflow".to_string());
883    }
884    let mut plugin_id_bytes = vec![0u8; plugin_id_len];
885    unsafe {
886        std::ptr::copy_nonoverlapping(
887            scratch.add(offset),
888            plugin_id_bytes.as_mut_ptr(),
889            plugin_id_len,
890        );
891    }
892    offset += plugin_id_len;
893    let plugin_id = String::from_utf8(plugin_id_bytes).map_err(|e| e.to_string())?;
894
895    let component_state_len =
896        unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
897    offset += 4;
898    if offset + component_state_len > size {
899        return Err("scratch underflow".to_string());
900    }
901    let mut component_state = vec![0u8; component_state_len];
902    unsafe {
903        std::ptr::copy_nonoverlapping(
904            scratch.add(offset),
905            component_state.as_mut_ptr(),
906            component_state_len,
907        );
908    }
909    offset += component_state_len;
910
911    let controller_state_len =
912        unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
913    offset += 4;
914    if offset + controller_state_len > size {
915        return Err("scratch underflow".to_string());
916    }
917    let mut controller_state = vec![0u8; controller_state_len];
918    unsafe {
919        std::ptr::copy_nonoverlapping(
920            scratch.add(offset),
921            controller_state.as_mut_ptr(),
922            controller_state_len,
923        );
924    }
925
926    Ok(Vst3PluginState {
927        plugin_id,
928        component_state,
929        controller_state,
930    })
931}
932
933#[cfg(test)]
934mod tests {
935    use super::*;
936
937    fn find_host_binary() -> PathBuf {
938        super::find_host_binary()
939            .expect("maolan-engine-plugin-host binary should be built for tests")
940    }
941
942    #[test]
943    fn find_host_binary_locates_binary() {
944        let host_bin = find_host_binary();
945        assert!(
946            host_bin.exists(),
947            "plugin-host binary should exist at {}",
948            host_bin.display()
949        );
950    }
951
952    #[test]
953    fn vst3_state_serialization_roundtrip() {
954        let state = Vst3PluginState {
955            plugin_id: "test.plugin.vst3".to_string(),
956            component_state: vec![1, 2, 3, 4, 5],
957            controller_state: vec![10, 20, 30],
958        };
959        let mut scratch = vec![0u8; SCRATCH_SIZE];
960        let size =
961            serialize_vst3_state(scratch.as_mut_ptr(), &state).expect("serialize should succeed");
962        assert!(size > 0);
963        assert!(size < SCRATCH_SIZE);
964
965        let decoded =
966            deserialize_vst3_state(scratch.as_ptr(), size).expect("deserialize should succeed");
967        assert_eq!(decoded.plugin_id, state.plugin_id);
968        assert_eq!(decoded.component_state, state.component_state);
969        assert_eq!(decoded.controller_state, state.controller_state);
970    }
971
972    #[test]
973    fn vst3_processor_crash_bypass() {
974        let host_bin = find_host_binary();
975
976        let processor = Vst3Processor::new(48000.0, 256, "__crash__", 1, 1, host_bin)
977            .expect("should create VST3 processor for crash test");
978
979        processor.setup_audio_ports();
980
981        // Fill input buffer.
982        {
983            let buf = processor.audio_inputs()[0].buffer.lock();
984            buf.fill(1.0);
985            *processor.audio_inputs()[0].finished.lock() = true;
986        }
987
988        // First process should trigger the crash; subsequent calls should bypass.
989        processor.process_with_audio_io(256);
990
991        // After crash, output should be a copy of input (bypass).
992        let out_buf = processor.audio_outputs()[0].buffer.lock();
993        assert!(
994            out_buf.iter().all(|&s| s == 1.0),
995            "after crash, output should be bypass copy of input"
996        );
997    }
998}