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