Skip to main content

maolan_engine/plugins/
vst3_proc.rs

1use crate::audio::io::AudioIO;
2use crate::midi::io::{MIDIIO, MidiEvent};
3use crate::plugins::ipc;
4use crate::plugins::types::ParameterInfo;
5use crate::plugins::types::Vst3PluginState;
6use arc_swap::ArcSwapOption;
7use maolan_plugin_protocol::events::EventPair;
8use maolan_plugin_protocol::protocol::*;
9use maolan_plugin_protocol::ringbuf::RingBuffer;
10use maolan_plugin_protocol::shm::ShmMapping;
11use std::cell::UnsafeCell;
12use std::collections::HashMap;
13use std::path::{Path, PathBuf};
14use std::process::{Child, ChildStderr};
15use std::sync::Arc;
16use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering};
17use std::time::Duration;
18
19const SHM_LATENCY_SAMPLES_OFFSET: usize = 84;
20
21unsafe fn latency_samples_atomic(ptr: *mut u8) -> &'static AtomicU32 {
22    unsafe { &*(ptr.add(SHM_LATENCY_SAMPLES_OFFSET) as *const AtomicU32) }
23}
24
25pub struct Vst3Processor {
26    path: String,
27    plugin_id: String,
28    name: String,
29    audio_inputs: Vec<Arc<AudioIO>>,
30    audio_outputs: Vec<Arc<AudioIO>>,
31    main_audio_inputs: usize,
32    main_audio_outputs: usize,
33    midi_input_ports: Vec<Arc<MIDIIO>>,
34    midi_output_ports: Vec<Arc<MIDIIO>>,
35    param_infos: Vec<ParameterInfo>,
36    /// Current value of every known parameter, keyed by parameter id and
37    /// stored as `f64` bits. Pre-populated from `param_infos` at construction
38    /// and only ever touched through atomic loads/stores.
39    param_values: HashMap<u32, AtomicU64>,
40    bypassed: Arc<AtomicBool>,
41
42    /// Host child process handle. Interior-mutable so `process_with_audio_buffers`
43    /// (which takes `&self`) can poll it with `try_wait`.
44    ///
45    /// Invariant: at any moment there is at most one accessor — either the
46    /// audio thread running this plugin's own plan task node (exactly one per
47    /// cycle), or control code running after the last `Arc` reference to this
48    /// processor is gone (`Drop`).
49    child: UnsafeCell<Option<Child>>,
50    /// Host stderr pipe; control-side only (`take_stderr`). RCU-published so
51    /// no blocking primitive is involved.
52    stderr: ArcSwapOption<ChildStderr>,
53    mapping: Option<ShmMapping>,
54    events: Option<EventPair>,
55    shm_name: String,
56
57    crash_count: AtomicU32,
58    last_latency_samples: AtomicUsize,
59    latency_changed: AtomicBool,
60}
61
62// Safety: see the invariants on `child` and `stderr` above. Every other field
63// is either immutable after construction or synchronized on its own
64// (atomics / RCU).
65unsafe impl Sync for Vst3Processor {}
66
67pub type SharedVst3Processor = Arc<Vst3Processor>;
68
69impl Vst3Processor {
70    pub fn new(
71        sample_rate: f64,
72        buffer_size: usize,
73        plugin_path: &str,
74        plugin_id: &str,
75        input_count: usize,
76        output_count: usize,
77        host_binary: PathBuf,
78    ) -> Result<Self, String> {
79        let audio_inputs = (0..input_count.max(1))
80            .map(|_| Arc::new(AudioIO::new(buffer_size)))
81            .collect::<Vec<_>>();
82        let audio_outputs = (0..output_count.max(1))
83            .map(|_| Arc::new(AudioIO::new(buffer_size)))
84            .collect::<Vec<_>>();
85
86        let instance_id = ipc::unique_instance_id("vst3");
87        let num_inputs = input_count.max(1);
88        let num_outputs = output_count.max(1);
89        let (mut child, mapping, events, shm_name, stderr) = ipc::spawn_host(ipc::HostSpawnArgs {
90            host_binary: &host_binary,
91            format: "vst3",
92            plugin_spec: plugin_path,
93            instance_id: &instance_id,
94            extra_args: &[
95                &sample_rate.to_string(),
96                &buffer_size.to_string(),
97                &num_inputs.to_string(),
98                &num_outputs.to_string(),
99            ],
100        })?;
101
102        let header = unsafe { header_ref(mapping.as_ptr()) };
103        if !ipc::wait_for_ready(header, Duration::from_secs(10)) {
104            let _ = child.kill();
105            return Err("VST3 host did not signal ready".to_string());
106        }
107
108        let name = unsafe {
109            maolan_plugin_protocol::protocol::read_plugin_name_from_scratch(mapping.as_ptr())
110                .unwrap_or_else(|| {
111                    Path::new(plugin_path)
112                        .file_stem()
113                        .and_then(|s| s.to_str())
114                        .unwrap_or("VST3")
115                        .to_string()
116                })
117        };
118
119        let param_infos: Vec<ParameterInfo> = Vec::new();
120        let param_values = param_infos
121            .iter()
122            .map(|info| (info.id, AtomicU64::new(info.default_value.to_bits())))
123            .collect();
124
125        let header = unsafe { header_ref(mapping.as_ptr()) };
126        let midi_in_count = header.midi_in_port_count.load(Ordering::Acquire) as usize;
127        let midi_out_count = header.midi_out_port_count.load(Ordering::Acquire) as usize;
128        let midi_input_ports: Vec<_> = (0..midi_in_count)
129            .map(|_| Arc::new(MIDIIO::new()))
130            .collect();
131        let midi_output_ports: Vec<_> = (0..midi_out_count)
132            .map(|_| Arc::new(MIDIIO::new()))
133            .collect();
134
135        Ok(Self {
136            path: plugin_path.to_string(),
137            plugin_id: plugin_id.to_string(),
138            name,
139            audio_inputs,
140            audio_outputs,
141            main_audio_inputs: input_count.max(1),
142            main_audio_outputs: output_count.max(1),
143            midi_input_ports,
144            midi_output_ports,
145            param_infos,
146            param_values,
147            bypassed: Arc::new(AtomicBool::new(false)),
148            child: UnsafeCell::new(Some(child)),
149            stderr: ArcSwapOption::from_pointee(stderr),
150            mapping: Some(mapping),
151            events: Some(events),
152            shm_name,
153            crash_count: AtomicU32::new(0),
154            last_latency_samples: AtomicUsize::new(0),
155            latency_changed: AtomicBool::new(false),
156        })
157    }
158
159    /// Access the host child process handle.
160    ///
161    /// # Safety
162    /// The caller must be the sole accessor of `child` at this time: either
163    /// the audio thread running this plugin's own plan task node (exactly one
164    /// per cycle), or control code running after the last `Arc` reference to
165    /// this processor is gone.
166    unsafe fn with_child<R>(&self, f: impl FnOnce(&mut Option<Child>) -> R) -> R {
167        f(unsafe { &mut *self.child.get() })
168    }
169
170    pub fn setup_audio_ports(&self) {
171        for port in &self.audio_inputs {
172            port.setup();
173        }
174        for port in &self.audio_outputs {
175            port.setup();
176        }
177    }
178
179    pub fn setup_midi_ports(&self) {
180        for port in &self.midi_input_ports {
181            // Safety: plan single-writer invariant — this task is the sole
182            // writer of its own ports this cycle; sources it reads were
183            // produced by earlier plan nodes (LOCKLESS.md Phase 3).
184            unsafe { port.setup() };
185        }
186        for port in &self.midi_output_ports {
187            // Safety: as above — sole writer of this port this cycle.
188            unsafe { port.setup() };
189        }
190    }
191
192    pub fn audio_inputs(&self) -> &[Arc<AudioIO>] {
193        &self.audio_inputs
194    }
195
196    pub fn audio_outputs(&self) -> &[Arc<AudioIO>] {
197        &self.audio_outputs
198    }
199
200    pub fn main_audio_input_count(&self) -> usize {
201        self.main_audio_inputs
202    }
203
204    pub fn main_audio_output_count(&self) -> usize {
205        self.main_audio_outputs
206    }
207
208    pub fn midi_input_count(&self) -> usize {
209        self.midi_input_ports.len()
210    }
211
212    pub fn midi_output_count(&self) -> usize {
213        self.midi_output_ports.len()
214    }
215
216    pub fn midi_input_ports(&self) -> &[Arc<MIDIIO>] {
217        &self.midi_input_ports
218    }
219
220    pub fn midi_output_ports(&self) -> &[Arc<MIDIIO>] {
221        &self.midi_output_ports
222    }
223
224    pub fn set_bypassed(&self, bypassed: bool) {
225        let previous = self.bypassed.swap(bypassed, Ordering::Relaxed);
226        if previous != bypassed {
227            self.latency_changed.store(true, Ordering::Release);
228        }
229    }
230
231    pub fn is_bypassed(&self) -> bool {
232        self.bypassed.load(Ordering::Relaxed)
233    }
234
235    pub fn latency_samples(&self) -> usize {
236        if self.bypassed.load(Ordering::Relaxed) {
237            let previous = self.last_latency_samples.swap(0, Ordering::AcqRel);
238            if previous != 0 {
239                self.latency_changed.store(true, Ordering::Release);
240            }
241            return 0;
242        }
243        let latency = self
244            .mapping
245            .as_ref()
246            .map(|mapping| unsafe {
247                latency_samples_atomic(mapping.as_ptr()).load(Ordering::Acquire) as usize
248            })
249            .unwrap_or(0);
250        let previous = self.last_latency_samples.swap(latency, Ordering::AcqRel);
251        if previous != latency {
252            self.latency_changed.store(true, Ordering::Release);
253        }
254        latency
255    }
256
257    pub fn take_latency_changed(&self) -> bool {
258        self.latency_changed.swap(false, Ordering::AcqRel)
259    }
260
261    pub fn parameter_infos(&self) -> Vec<ParameterInfo> {
262        self.param_infos.clone()
263    }
264
265    pub fn parameter_values(&self) -> HashMap<u32, f64> {
266        self.param_values
267            .iter()
268            .map(|(&id, value)| (id, f64::from_bits(value.load(Ordering::Relaxed))))
269            .collect()
270    }
271
272    pub fn set_parameter(&self, param_id: u32, value: f64) -> Result<(), String> {
273        self.set_parameter_at(param_id, value, 0)
274    }
275
276    pub fn set_parameter_at(&self, param_id: u32, value: f64, _frame: u32) -> Result<(), String> {
277        if let Some(slot) = self.param_values.get(&param_id) {
278            slot.store(value.to_bits(), Ordering::Relaxed);
279        } else {
280            tracing::warn!("VST3 set_parameter_at: unknown parameter id {param_id}");
281        }
282
283        if let Some(ref mapping) = self.mapping {
284            let ring = unsafe {
285                let buf = param_ring_ptr(mapping.as_ptr());
286                let (w, r) = param_indices(mapping.as_ptr());
287                RingBuffer::new(buf, w, r, RING_CAPACITY)
288            };
289            let ev = ParameterEvent {
290                param_index: param_id,
291                value: value as f32,
292                sample_offset: 0,
293                event_kind: maolan_plugin_protocol::PARAM_EVENT_VALUE,
294            };
295            if !ring.push(ev) {}
296        }
297        Ok(())
298    }
299
300    pub fn begin_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
301        Ok(())
302    }
303
304    pub fn end_parameter_edit(&self, _param_id: u32) -> Result<(), String> {
305        Ok(())
306    }
307
308    pub fn is_parameter_edit_active(&self, _param_id: u32) -> bool {
309        false
310    }
311
312    pub fn snapshot_state(&self) -> Result<Vst3PluginState, String> {
313        let (mapping, events) = match (&self.mapping, &self.events) {
314            (Some(m), Some(e)) => (m, e),
315            _ => return Err("VST3 processor not initialized".to_string()),
316        };
317        let ptr = mapping.as_ptr();
318        let header = unsafe { header_mut(ptr) };
319
320        header.request_type.store(1, Ordering::Release);
321        header.request_status.store(0, Ordering::Release);
322        if let Err(e) = events.signal_host() {
323            header.request_type.store(0, Ordering::Release);
324            return Err(format!("Failed to signal host for state save: {}", e));
325        }
326
327        if let Err(e) = events.wait_host(Duration::from_secs(5)) {
328            header.request_type.store(0, Ordering::Release);
329            return Err(format!("Host did not respond to state save: {}", e));
330        }
331
332        let status = header.request_status.load(Ordering::Acquire);
333        let size = header.scratch_size.load(Ordering::Acquire) as usize;
334        if status != 1 {
335            header.request_type.store(0, Ordering::Release);
336            return Err("State save failed in host".to_string());
337        }
338
339        let scratch = unsafe { scratch_ptr(ptr) };
340        let state = deserialize_vst3_state(scratch, size)?;
341        header.request_type.store(0, Ordering::Release);
342        Ok(state)
343    }
344
345    pub fn restore_state(&self, state: &Vst3PluginState) -> Result<(), String> {
346        let (mapping, events) = match (&self.mapping, &self.events) {
347            (Some(m), Some(e)) => (m, e),
348            _ => return Err("VST3 processor not initialized".to_string()),
349        };
350        let ptr = mapping.as_ptr();
351        let header = unsafe { header_mut(ptr) };
352
353        let scratch = unsafe { scratch_ptr(ptr) };
354        let size = serialize_vst3_state(scratch, state)?;
355        header.scratch_size.store(size as u32, Ordering::Release);
356
357        header.request_type.store(2, Ordering::Release);
358        header.request_status.store(0, Ordering::Release);
359        if let Err(e) = events.signal_host() {
360            header.request_type.store(0, Ordering::Release);
361            return Err(format!("Failed to signal host for state restore: {}", e));
362        }
363
364        if let Err(e) = events.wait_host(Duration::from_secs(5)) {
365            header.request_type.store(0, Ordering::Release);
366            return Err(format!("Host did not respond to state restore: {}", e));
367        }
368
369        let status = header.request_status.load(Ordering::Acquire);
370        header.request_type.store(0, Ordering::Release);
371        if status != 1 {
372            return Err("State restore failed in host".to_string());
373        }
374        Ok(())
375    }
376
377    pub fn process_with_audio_buffers(
378        &self,
379        frames: usize,
380        audio_inputs: &[&[f32]],
381        audio_outputs: &mut [&mut [f32]],
382    ) -> Vec<MidiEvent> {
383        if self.bypassed.load(Ordering::Relaxed) {
384            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
385            return Vec::new();
386        }
387
388        // Safety: the sole RT accessor of `child` is this processor's own
389        // plan task node, which runs exactly once per cycle.
390        let crashed = unsafe {
391            self.with_child(|child| {
392                if let Some(c) = child.as_mut()
393                    && let Ok(Some(status)) = c.try_wait()
394                    && !status.success()
395                {
396                    self.crash_count.fetch_add(1, Ordering::Relaxed);
397                    return true;
398                }
399                false
400            })
401        };
402        if crashed {
403            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
404            return Vec::new();
405        }
406
407        let (mapping, events) = match (&self.mapping, &self.events) {
408            (Some(m), Some(e)) => (m, e),
409            _ => {
410                ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
411                return Vec::new();
412            }
413        };
414
415        let ptr = mapping.as_ptr();
416        let num_in = audio_inputs.len();
417        let num_out = audio_outputs.len();
418        let midi_in_count = self.midi_input_ports.len();
419        let midi_out_count = self.midi_output_ports.len();
420        unsafe {
421            ipc::configure_shm_header(ptr, frames, num_in, num_out, midi_in_count, midi_out_count);
422
423            let t = transport_mut(ptr);
424            t.playhead_sample = 0;
425            t.tempo = 120.0;
426            t.numerator = 4;
427            t.denominator = 4;
428            t.flags = 1;
429
430            ipc::copy_input_slices_to_shm(audio_inputs, ptr, frames);
431
432            for (port_idx, port) in self.midi_input_ports.iter().enumerate() {
433                let buf = midi_in_ring_ptr(ptr, port_idx);
434                let (w, r) = midi_in_indices(ptr, port_idx);
435                let ring = RingBuffer::new(buf, w, r, RING_CAPACITY);
436                // Safety: plan single-writer invariant — this task is the sole
437                // writer of its own ports this cycle; this read is of the
438                // port's own buffer, which no other node touches now
439                // (LOCKLESS.md Phase 3).
440                let port_buffer = port.buffer();
441                for ev in port_buffer {
442                    let data = {
443                        let mut d = [0u8; 3];
444                        for (i, b) in ev.data.iter().enumerate().take(3) {
445                            d[i] = *b;
446                        }
447                        d
448                    };
449                    let _ = ring.push(maolan_plugin_protocol::MidiEvent {
450                        sample_offset: ev.frame,
451                        data,
452                        channel: ev.data.first().copied().unwrap_or(0) & 0x0F,
453                        flags: 0,
454                        _pad: 0,
455                    });
456                }
457                port.mark_finished();
458            }
459        }
460
461        if events.signal_host().is_err() {
462            ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
463            return Vec::new();
464        }
465
466        let timeout = Duration::from_millis(100);
467        match events.wait_host(timeout) {
468            Ok(()) => {}
469            Err(_) => {
470                ipc::bypass_copy_input_slices_to_outputs(audio_inputs, audio_outputs);
471                return Vec::new();
472            }
473        }
474
475        unsafe {
476            ipc::copy_outputs_from_shm_to_slices(audio_outputs, ptr, frames);
477
478            let mut output_events = Vec::new();
479            for (port_idx, port) in self.midi_output_ports.iter().enumerate() {
480                let buf = midi_out_ring_ptr(ptr, port_idx);
481                let (w, r) = midi_out_indices(ptr, port_idx);
482                let ring = RingBuffer::new(buf, w, r, RING_CAPACITY);
483                // Safety: plan single-writer invariant — this task is the sole
484                // writer of its own ports this cycle (LOCKLESS.md Phase 3).
485                let mut port_buffer = port.buffer_mut();
486                port_buffer.clear();
487                while let Some(ev) = ring.pop() {
488                    let event = MidiEvent {
489                        frame: ev.sample_offset,
490                        data: ev.data.to_vec(),
491                    };
492                    port_buffer.push(event.clone());
493                    output_events.push(event);
494                }
495                port.mark_finished();
496            }
497            output_events
498        }
499    }
500
501    pub fn path(&self) -> &str {
502        &self.path
503    }
504
505    pub fn plugin_id(&self) -> &str {
506        &self.plugin_id
507    }
508
509    pub fn name(&self) -> &str {
510        &self.name
511    }
512
513    pub fn take_stderr(&self) -> Option<ChildStderr> {
514        // Control-side only: the EC is the sole accessor, so after `swap`
515        // the `Arc` is unique and `try_unwrap` cannot fail in practice.
516        self.stderr.swap(None).and_then(|s| Arc::try_unwrap(s).ok())
517    }
518
519    pub fn begin_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
520        Ok(())
521    }
522
523    pub fn end_parameter_edit_at(&self, _param_id: u32, _frame: u32) -> Result<(), String> {
524        Ok(())
525    }
526
527    pub fn run_host_callbacks_main_thread(&self) {}
528
529    pub fn reconfigure_ports_if_needed(&self) -> Result<bool, String> {
530        Ok(false)
531    }
532
533    pub fn ui_begin_session(&self) {}
534    pub fn ui_end_session(&self) {}
535    pub fn ui_should_close(&self) -> bool {
536        false
537    }
538    pub fn ui_take_due_timers(&self) -> Vec<u32> {
539        Vec::new()
540    }
541    pub fn ui_take_param_updates(&self) -> Vec<(u32, f64)> {
542        Vec::new()
543    }
544    pub fn ui_take_state_update(&self) -> Option<Vst3PluginState> {
545        None
546    }
547
548    pub fn gui_info(&self) -> Result<crate::plugins::types::Vst3GuiInfo, String> {
549        Err("GUI not yet supported for VST3 plugins".to_string())
550    }
551
552    pub fn gui_create(&self, _platform_type: &str) -> Result<(), String> {
553        Err("GUI not yet supported for VST3 plugins".to_string())
554    }
555
556    pub fn gui_get_size(&self) -> Result<(i32, i32), String> {
557        Err("GUI not yet supported for VST3 plugins".to_string())
558    }
559
560    pub fn gui_set_parent(&self, _window: usize, _platform_type: &str) -> Result<(), String> {
561        Err("GUI not yet supported for VST3 plugins".to_string())
562    }
563
564    pub fn gui_set_floating_mode(&self, floating: bool) -> Result<(), String> {
565        if let Some(ref mapping) = self.mapping {
566            let header = unsafe { header_mut(mapping.as_ptr()) };
567            header.set_gui_mode(if floating {
568                GuiMode::Floating
569            } else {
570                GuiMode::Embedded
571            });
572            if floating {
573                header.set_parent_window(0);
574                header.set_gui_parent_api(maolan_plugin_protocol::protocol::GuiParentApi::None);
575            }
576            return Ok(());
577        }
578        Err("No active host to set GUI mode".to_string())
579    }
580
581    pub fn gui_on_size(&self, _width: i32, _height: i32) -> Result<(), String> {
582        Err("GUI not yet supported for VST3 plugins".to_string())
583    }
584
585    pub fn gui_show(&self) -> Result<(), String> {
586        if let Some(ref mapping) = self.mapping
587            && let Some(ref events) = self.events
588        {
589            let header = unsafe { header_mut(mapping.as_ptr()) };
590            header.request_type.store(3, Ordering::Release);
591            let _ = events.signal_host();
592            return Ok(());
593        }
594        Err("No active host to show GUI".to_string())
595    }
596
597    pub fn gui_hide(&self) {
598        if let Some(ref mapping) = self.mapping
599            && let Some(ref events) = self.events
600        {
601            let header = unsafe { header_mut(mapping.as_ptr()) };
602            header.request_type.store(4, Ordering::Release);
603            let _ = events.signal_host();
604        }
605    }
606
607    pub fn gui_destroy(&self) {}
608
609    pub fn gui_on_main_thread(&self) {}
610
611    pub fn gui_on_timer(&self, _timer_id: u32) {}
612
613    pub fn gui_check_resize(&self) -> Option<(i32, i32)> {
614        None
615    }
616
617    pub fn drain_echoed_parameters(&self) -> Vec<ParameterEvent> {
618        let mut result = Vec::new();
619        if let Some(ref mapping) = self.mapping {
620            let ring = unsafe {
621                let buf = echo_ring_ptr(mapping.as_ptr());
622                let (w, r) = echo_indices(mapping.as_ptr());
623                RingBuffer::new(buf, w, r, RING_CAPACITY)
624            };
625            while let Some(ev) = ring.pop() {
626                result.push(ev);
627            }
628        }
629        result
630    }
631}
632
633impl Drop for Vst3Processor {
634    fn drop(&mut self) {
635        let mapping = self.mapping.take();
636        let events = self.events.take();
637        let child = self.child.get_mut().take();
638        let shm_name = std::mem::take(&mut self.shm_name);
639        ipc::drop_host(mapping, events, child, shm_name);
640    }
641}
642
643fn serialize_vst3_state(scratch: *mut u8, state: &Vst3PluginState) -> Result<usize, String> {
644    let max_len = maolan_plugin_protocol::protocol::SCRATCH_SIZE;
645    let mut offset = 0usize;
646
647    let plugin_id_bytes = state.plugin_id.as_bytes();
648    if offset + 4 > max_len {
649        return Err("scratch overflow".to_string());
650    }
651    unsafe {
652        std::ptr::write_unaligned(
653            scratch.add(offset) as *mut u32,
654            plugin_id_bytes.len() as u32,
655        );
656    }
657    offset += 4;
658    if offset + plugin_id_bytes.len() > max_len {
659        return Err("scratch overflow".to_string());
660    }
661    unsafe {
662        std::ptr::copy_nonoverlapping(
663            plugin_id_bytes.as_ptr(),
664            scratch.add(offset),
665            plugin_id_bytes.len(),
666        );
667    }
668    offset += plugin_id_bytes.len();
669
670    if offset + 4 > max_len {
671        return Err("scratch overflow".to_string());
672    }
673    unsafe {
674        std::ptr::write_unaligned(
675            scratch.add(offset) as *mut u32,
676            state.component_state.len() as u32,
677        );
678    }
679    offset += 4;
680    if offset + state.component_state.len() > max_len {
681        return Err("scratch overflow".to_string());
682    }
683    unsafe {
684        std::ptr::copy_nonoverlapping(
685            state.component_state.as_ptr(),
686            scratch.add(offset),
687            state.component_state.len(),
688        );
689    }
690    offset += state.component_state.len();
691
692    if offset + 4 > max_len {
693        return Err("scratch overflow".to_string());
694    }
695    unsafe {
696        std::ptr::write_unaligned(
697            scratch.add(offset) as *mut u32,
698            state.controller_state.len() as u32,
699        );
700    }
701    offset += 4;
702    if offset + state.controller_state.len() > max_len {
703        return Err("scratch overflow".to_string());
704    }
705    unsafe {
706        std::ptr::copy_nonoverlapping(
707            state.controller_state.as_ptr(),
708            scratch.add(offset),
709            state.controller_state.len(),
710        );
711    }
712    offset += state.controller_state.len();
713
714    Ok(offset)
715}
716
717fn deserialize_vst3_state(scratch: *const u8, size: usize) -> Result<Vst3PluginState, String> {
718    if size < 12 {
719        return Err("scratch too small for VST3 state".to_string());
720    }
721    let mut offset = 0usize;
722
723    let plugin_id_len =
724        unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
725    offset += 4;
726    if offset + plugin_id_len > size {
727        return Err("scratch underflow".to_string());
728    }
729    let mut plugin_id_bytes = vec![0u8; plugin_id_len];
730    unsafe {
731        std::ptr::copy_nonoverlapping(
732            scratch.add(offset),
733            plugin_id_bytes.as_mut_ptr(),
734            plugin_id_len,
735        );
736    }
737    offset += plugin_id_len;
738    let plugin_id = String::from_utf8(plugin_id_bytes).map_err(|e| e.to_string())?;
739
740    let component_state_len =
741        unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
742    offset += 4;
743    if offset + component_state_len > size {
744        return Err("scratch underflow".to_string());
745    }
746    let mut component_state = vec![0u8; component_state_len];
747    unsafe {
748        std::ptr::copy_nonoverlapping(
749            scratch.add(offset),
750            component_state.as_mut_ptr(),
751            component_state_len,
752        );
753    }
754    offset += component_state_len;
755
756    let controller_state_len =
757        unsafe { std::ptr::read_unaligned(scratch.add(offset) as *const u32) } as usize;
758    offset += 4;
759    if offset + controller_state_len > size {
760        return Err("scratch underflow".to_string());
761    }
762    let mut controller_state = vec![0u8; controller_state_len];
763    unsafe {
764        std::ptr::copy_nonoverlapping(
765            scratch.add(offset),
766            controller_state.as_mut_ptr(),
767            controller_state_len,
768        );
769    }
770
771    Ok(Vst3PluginState {
772        plugin_id,
773        component_state,
774        controller_state,
775    })
776}
777
778#[cfg(test)]
779mod tests {
780    use super::*;
781
782    fn find_host_binary() -> PathBuf {
783        ipc::find_plugin_host_binary().expect("maolan-plugin-host binary should be built for tests")
784    }
785
786    #[cfg_attr(
787        all(miri, target_os = "freebsd"),
788        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
789    )]
790    #[test]
791    fn find_host_binary_locates_binary() {
792        let host_bin = find_host_binary();
793        assert!(
794            host_bin.exists(),
795            "plugin-host binary should exist at {}",
796            host_bin.display()
797        );
798    }
799
800    #[test]
801    fn vst3_state_serialization_roundtrip() {
802        let state = Vst3PluginState {
803            plugin_id: "test.plugin.vst3".to_string(),
804            component_state: vec![1, 2, 3, 4, 5],
805            controller_state: vec![10, 20, 30],
806        };
807        let mut scratch = vec![0u8; SCRATCH_SIZE];
808        let size =
809            serialize_vst3_state(scratch.as_mut_ptr(), &state).expect("serialize should succeed");
810        assert!(size > 0);
811        assert!(size < SCRATCH_SIZE);
812
813        let decoded =
814            deserialize_vst3_state(scratch.as_ptr(), size).expect("deserialize should succeed");
815        assert_eq!(decoded.plugin_id, state.plugin_id);
816        assert_eq!(decoded.component_state, state.component_state);
817        assert_eq!(decoded.controller_state, state.controller_state);
818    }
819
820    #[cfg_attr(
821        all(miri, target_os = "freebsd"),
822        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
823    )]
824    #[test]
825    fn vst3_processor_crash_bypass() {
826        let host_bin = find_host_binary();
827
828        let processor = Vst3Processor::new(48000.0, 256, "__crash__", "__crash__", 1, 1, host_bin)
829            .expect("should create VST3 processor for crash test");
830
831        processor.setup_audio_ports();
832
833        let input_buffers = [vec![1.0; 256]];
834        let mut output_buffers = [vec![0.0; 256]];
835        let inputs = input_buffers.iter().map(Vec::as_slice).collect::<Vec<_>>();
836        let mut outputs = output_buffers
837            .iter_mut()
838            .map(Vec::as_mut_slice)
839            .collect::<Vec<_>>();
840        processor.process_with_audio_buffers(256, &inputs, &mut outputs);
841
842        let out_buf = &output_buffers[0];
843        assert!(
844            out_buf.iter().all(|&s| s == 1.0),
845            "after crash, output should be bypass copy of input"
846        );
847    }
848
849    #[cfg_attr(
850        all(miri, target_os = "freebsd"),
851        ignore = "plugin host discovery/runtime uses OS facilities not supported by Miri on FreeBSD"
852    )]
853    #[test]
854    fn vst3_bypass_reports_zero_latency() {
855        let processor = Vst3Processor::new(
856            48000.0,
857            256,
858            "__test__",
859            "__test__",
860            1,
861            1,
862            find_host_binary(),
863        )
864        .expect("should create VST3 processor");
865        let mapping = processor.mapping.as_ref().expect("mapping exists");
866        unsafe {
867            latency_samples_atomic(mapping.as_ptr()).store(128, Ordering::Release);
868        }
869
870        assert_eq!(processor.latency_samples(), 128);
871        processor.set_bypassed(true);
872        assert_eq!(processor.latency_samples(), 0);
873        assert!(processor.take_latency_changed());
874    }
875}