Skip to main content

maolan_engine/plugins/
vst3_proc.rs

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