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