Skip to main content

maolan_plugin_protocol/
protocol.rs

1use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
2
3/// Magic number: "MAOL" in big-endian ASCII.
4pub const MAGIC: u32 = 0x4D41_4F4C;
5
6/// Current protocol version.
7/// Version 2: parent_window changed from AtomicU32 to AtomicU64 to support 64-bit HWNDs on Windows.
8/// Version 3: Added MIDI output ring for plugin-generated MIDI events.
9pub const VERSION: u32 = 3;
10
11/// Maximum number of audio channels (main + sidechain combined).
12pub const MAX_CHANNELS: usize = 32;
13
14/// Number of audio buses (main + sidechain).
15pub const NUM_BUSES: usize = 2;
16
17/// Maximum audio block size in samples.
18pub const MAX_BLOCK_SIZE: usize = 4096;
19
20/// Capacity of each ring buffer in slots (power of two).
21pub const RING_CAPACITY: usize = 4096;
22
23// --- Section sizes ---
24pub const HEADER_SIZE: usize = 256;
25pub const CONTROL_SIZE: usize = 256;
26pub const AUDIO_BUFFER_SIZE: usize = MAX_CHANNELS * NUM_BUSES * MAX_BLOCK_SIZE * 4; // f32
27pub const PARAM_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<ParameterEvent>();
28pub const MIDI_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<MidiEvent>();
29pub const TRANSPORT_SIZE: usize = 256;
30pub const SCRATCH_SIZE: usize = 65536;
31
32// --- Offsets into the shared-memory segment ---
33/// Control area starts right after the header.
34pub const CONTROL_OFFSET: usize = HEADER_SIZE;
35/// Audio buffers start after the control area.
36pub const AUDIO_OFFSET: usize = HEADER_SIZE + CONTROL_SIZE;
37/// Parameter ring buffer.
38pub const PARAM_RING_OFFSET: usize = AUDIO_OFFSET + AUDIO_BUFFER_SIZE;
39/// MIDI ring buffer.
40pub const MIDI_RING_OFFSET: usize = PARAM_RING_OFFSET + PARAM_RING_SIZE;
41pub const ECHO_RING_OFFSET: usize = MIDI_RING_OFFSET + MIDI_RING_SIZE;
42pub const ECHO_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<ParameterEvent>();
43pub const MIDI_OUT_RING_OFFSET: usize = ECHO_RING_OFFSET + ECHO_RING_SIZE;
44pub const MIDI_OUT_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<MidiEvent>();
45/// Transport state block (256-byte aligned from here).
46pub const TRANSPORT_OFFSET: usize = {
47    let end = MIDI_OUT_RING_OFFSET + MIDI_OUT_RING_SIZE;
48    // Align up to 256 bytes
49    (end + 255) & !255
50};
51/// State blob scratch area.
52pub const SCRATCH_OFFSET: usize = TRANSPORT_OFFSET + TRANSPORT_SIZE;
53
54/// Total bytes actively used by the protocol layout.
55pub const LAYOUT_SIZE: usize = SCRATCH_OFFSET + SCRATCH_SIZE;
56
57/// Total shared-memory allocation size (4 MiB, page-aligned).
58pub const SHM_SIZE: usize = 4 * 1024 * 1024;
59
60// --- Control-area indices (all 4-byte atomics inside CONTROL_OFFSET..CONTROL_OFFSET+256) ---
61pub const PARAM_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET;
62pub const PARAM_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 4;
63pub const MIDI_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET + 8;
64pub const MIDI_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 12;
65pub const ECHO_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET + 16;
66pub const ECHO_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 20;
67pub const MIDI_OUT_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET + 24;
68pub const MIDI_OUT_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 28;
69
70// --- Structs ---
71
72pub const PARAM_EVENT_VALUE: u32 = 0;
73pub const PARAM_EVENT_MOD: u32 = 1;
74pub const PARAM_EVENT_GESTURE_BEGIN: u32 = 2;
75pub const PARAM_EVENT_GESTURE_END: u32 = 3;
76
77/// Fixed-size parameter change event (16 bytes, 16-byte aligned).
78#[repr(C, align(16))]
79#[derive(Clone, Copy, Debug, Default)]
80pub struct ParameterEvent {
81    pub param_index: u32,
82    pub value: f32,
83    pub sample_offset: u32,
84    pub event_kind: u32,
85}
86
87/// Fixed-size MIDI event (16 bytes, 16-byte aligned).
88#[repr(C, align(16))]
89#[derive(Clone, Copy, Debug, Default)]
90pub struct MidiEvent {
91    pub sample_offset: u32,
92    pub data: [u8; 3],
93    pub channel: u8,
94    pub flags: u16,
95    pub _pad: u16,
96}
97
98/// Transport state block (256 bytes).
99#[repr(C, align(256))]
100#[derive(Clone, Copy, Debug)]
101pub struct TransportState {
102    pub playhead_sample: u64,
103    pub tempo: f64,
104    pub numerator: u32,
105    pub denominator: u32,
106    pub flags: u32,
107    pub sample_rate_hz: f64,
108    _pad: [u8; 256 - 40],
109}
110
111impl Default for TransportState {
112    fn default() -> Self {
113        Self {
114            playhead_sample: 0,
115            tempo: 120.0,
116            numerator: 4,
117            denominator: 4,
118            flags: 0,
119            sample_rate_hz: 0.0,
120            _pad: [0; 256 - 40],
121        }
122    }
123}
124
125/// Shared-memory header (256 bytes).
126#[repr(C, align(256))]
127pub struct ShmHeader {
128    pub magic: u32,
129    pub version: u32,
130    pub flags: u32,
131    pub ready: AtomicU32,
132    pub heartbeat: AtomicU32,
133    pub error_code: u32,
134    pub shutdown_request: AtomicU32,
135    pub tasks_issued: AtomicU32,
136    pub tasks_completed: AtomicU32,
137    pub block_size: AtomicU32,
138    pub num_input_channels: AtomicU32,
139    pub num_output_channels: AtomicU32,
140    /// Request type: 0 = none, 1 = save_state, 2 = restore_state, 3 = gui_show, 4 = gui_hide
141    pub request_type: AtomicU32,
142    /// Request status: 0 = pending, 1 = success, 2 = error
143    pub request_status: AtomicU32,
144    /// Valid bytes in scratch area for state operations
145    pub scratch_size: AtomicU32,
146    /// Parent window ID for GUI embedding (X11 window ID on Unix, HWND on Windows)
147    pub parent_window: AtomicU64,
148    _pad: [u8; 256 - 72],
149}
150
151impl ShmHeader {
152    /// Load parent_window as a `usize` (handles 32- and 64-bit platforms).
153    pub fn parent_window_usize(&self) -> usize {
154        self.parent_window.load(Ordering::Acquire) as usize
155    }
156
157    /// Store a `usize` parent_window (truncates on 32-bit, but HWNDs/XIDs are
158    /// always within 64 bits).
159    pub fn set_parent_window(&self, window: usize) {
160        self.parent_window.store(window as u64, Ordering::Release);
161    }
162}
163
164impl Default for ShmHeader {
165    fn default() -> Self {
166        Self {
167            magic: MAGIC,
168            version: VERSION,
169            flags: 0,
170            ready: AtomicU32::new(0),
171            heartbeat: AtomicU32::new(0),
172            error_code: 0,
173            shutdown_request: AtomicU32::new(0),
174            tasks_issued: AtomicU32::new(0),
175            tasks_completed: AtomicU32::new(0),
176            block_size: AtomicU32::new(0),
177            num_input_channels: AtomicU32::new(0),
178            num_output_channels: AtomicU32::new(0),
179            request_type: AtomicU32::new(0),
180            request_status: AtomicU32::new(0),
181            scratch_size: AtomicU32::new(0),
182            parent_window: AtomicU64::new(0),
183            _pad: [0; 256 - 72],
184        }
185    }
186}
187
188// --- Layout helpers ---
189
190/// Zero-initialize the entire shared-memory region and write the header.
191///
192/// # Safety
193/// `ptr` must be a valid pointer to a memory region of `size` bytes.
194pub unsafe fn init_shm_layout(ptr: *mut u8, size: usize) {
195    unsafe {
196        std::ptr::write_bytes(ptr, 0, size);
197        let header = ptr as *mut ShmHeader;
198        std::ptr::write(header, ShmHeader::default());
199    }
200}
201
202/// Returns a reference to the header at the start of the mapping.
203///
204/// # Safety
205/// `ptr` must point to a valid allocation containing at least `ShmHeader`'s size.
206pub unsafe fn header_ref(ptr: *mut u8) -> &'static ShmHeader {
207    unsafe { &*(ptr as *mut ShmHeader) }
208}
209
210/// Returns a mutable reference to the header.
211///
212/// # Safety
213/// `ptr` must point to a valid allocation containing at least `ShmHeader`'s size.
214pub unsafe fn header_mut(ptr: *mut u8) -> &'static mut ShmHeader {
215    unsafe { &mut *(ptr as *mut ShmHeader) }
216}
217
218/// Returns a pointer to the audio buffer region.
219///
220/// # Safety
221/// `ptr` must point to an allocation large enough to contain the audio buffer.
222pub unsafe fn audio_ptr(ptr: *mut u8) -> *mut f32 {
223    unsafe { ptr.add(AUDIO_OFFSET) as *mut f32 }
224}
225
226/// Returns a pointer to a specific channel/bus plane.
227///
228/// `channel` is 0-based up to `MAX_CHANNELS - 1`.
229/// `bus` is 0 (main) or 1 (sidechain).
230///
231/// # Safety
232/// `ptr` must point to a valid allocation large enough to contain the audio data.
233pub unsafe fn audio_channel_ptr(ptr: *mut u8, channel: usize, bus: usize) -> *mut f32 {
234    let plane_size = MAX_BLOCK_SIZE * std::mem::size_of::<f32>();
235    let offset = AUDIO_OFFSET + (channel * NUM_BUSES + bus) * plane_size;
236    unsafe { ptr.add(offset) as *mut f32 }
237}
238
239/// Returns a pointer to the parameter ring buffer slot array.
240///
241/// # Safety
242/// `ptr` must point to a valid allocation large enough to contain the parameter ring.
243pub unsafe fn param_ring_ptr(ptr: *mut u8) -> *mut ParameterEvent {
244    unsafe { ptr.add(PARAM_RING_OFFSET) as *mut ParameterEvent }
245}
246
247/// Returns pointers to the parameter ring write/read atomics.
248///
249/// # Safety
250/// `ptr` must point to a valid allocation containing the parameter ring atomics.
251pub unsafe fn param_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
252    unsafe {
253        (
254            ptr.add(PARAM_WRITE_IDX_OFFSET) as *mut AtomicU32,
255            ptr.add(PARAM_READ_IDX_OFFSET) as *mut AtomicU32,
256        )
257    }
258}
259
260/// Returns a pointer to the MIDI ring buffer slot array.
261///
262/// # Safety
263/// `ptr` must point to a valid allocation large enough to contain the MIDI ring.
264pub unsafe fn midi_ring_ptr(ptr: *mut u8) -> *mut MidiEvent {
265    unsafe { ptr.add(MIDI_RING_OFFSET) as *mut MidiEvent }
266}
267
268/// Returns pointers to the MIDI ring write/read atomics.
269///
270/// # Safety
271/// `ptr` must point to a valid allocation containing the MIDI ring atomics.
272pub unsafe fn midi_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
273    unsafe {
274        (
275            ptr.add(MIDI_WRITE_IDX_OFFSET) as *mut AtomicU32,
276            ptr.add(MIDI_READ_IDX_OFFSET) as *mut AtomicU32,
277        )
278    }
279}
280
281/// Returns a pointer to the echo ring buffer slot array.
282///
283/// # Safety
284/// `ptr` must point to a valid allocation large enough to contain the echo ring.
285pub unsafe fn echo_ring_ptr(ptr: *mut u8) -> *mut ParameterEvent {
286    unsafe { ptr.add(ECHO_RING_OFFSET) as *mut ParameterEvent }
287}
288
289/// Returns pointers to the echo ring write/read atomics.
290///
291/// # Safety
292/// `ptr` must point to a valid allocation containing the echo ring atomics.
293pub unsafe fn echo_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
294    unsafe {
295        (
296            ptr.add(ECHO_WRITE_IDX_OFFSET) as *mut AtomicU32,
297            ptr.add(ECHO_READ_IDX_OFFSET) as *mut AtomicU32,
298        )
299    }
300}
301
302/// Returns a pointer to the MIDI output ring buffer slot array.
303///
304/// # Safety
305/// `ptr` must point to a valid allocation large enough to contain the MIDI out ring.
306pub unsafe fn midi_out_ring_ptr(ptr: *mut u8) -> *mut MidiEvent {
307    unsafe { ptr.add(MIDI_OUT_RING_OFFSET) as *mut MidiEvent }
308}
309
310/// Returns pointers to the MIDI output ring write/read atomics.
311///
312/// # Safety
313/// `ptr` must point to a valid allocation containing the MIDI out ring atomics.
314pub unsafe fn midi_out_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
315    unsafe {
316        (
317            ptr.add(MIDI_OUT_WRITE_IDX_OFFSET) as *mut AtomicU32,
318            ptr.add(MIDI_OUT_READ_IDX_OFFSET) as *mut AtomicU32,
319        )
320    }
321}
322
323/// Returns a reference to the transport state.
324///
325/// # Safety
326/// `ptr` must point to a valid allocation containing at least `TransportState`'s size.
327pub unsafe fn transport_ref(ptr: *mut u8) -> &'static TransportState {
328    unsafe { &*(ptr.add(TRANSPORT_OFFSET) as *mut TransportState) }
329}
330
331/// Returns a mutable reference to the transport state.
332///
333/// # Safety
334/// `ptr` must point to a valid allocation containing at least `TransportState`'s size.
335pub unsafe fn transport_mut(ptr: *mut u8) -> &'static mut TransportState {
336    unsafe { &mut *(ptr.add(TRANSPORT_OFFSET) as *mut TransportState) }
337}
338
339/// Returns a pointer to the scratch buffer region.
340///
341/// # Safety
342/// `ptr` must point to an allocation large enough to contain the scratch buffer.
343pub unsafe fn scratch_ptr(ptr: *mut u8) -> *mut u8 {
344    unsafe { ptr.add(SCRATCH_OFFSET) }
345}
346
347/// Write a plugin name to the start of the scratch buffer.
348/// The name is encoded as a little-endian u32 length followed by UTF-8 bytes.
349///
350/// # Safety
351/// `ptr` must point to a valid SHM allocation.
352pub unsafe fn write_plugin_name_to_scratch(ptr: *mut u8, name: &str) {
353    unsafe {
354        let scratch = scratch_ptr(ptr);
355        let bytes = name.as_bytes();
356        let len = bytes.len().min(SCRATCH_SIZE - 4);
357        std::ptr::write_unaligned(scratch as *mut u32, len as u32);
358        std::ptr::copy_nonoverlapping(bytes.as_ptr(), scratch.add(4), len);
359    }
360}
361
362/// Read a plugin name from the start of the scratch buffer.
363///
364/// # Safety
365/// `ptr` must point to a valid SHM allocation.
366pub unsafe fn read_plugin_name_from_scratch(ptr: *mut u8) -> Option<String> {
367    unsafe {
368        let scratch = scratch_ptr(ptr);
369        let len = std::ptr::read_unaligned(scratch as *mut u32) as usize;
370        if len == 0 || len > SCRATCH_SIZE - 4 {
371            return None;
372        }
373        let bytes = std::slice::from_raw_parts(scratch.add(4), len);
374        String::from_utf8(bytes.to_vec()).ok()
375    }
376}
377
378/// Magic value written before port counts in scratch.
379pub const PORT_COUNTS_MAGIC: u32 = 0x504F_5254; // "PORT"
380
381/// Offset within scratch where port counts are stored (after plugin name).
382const PORT_COUNTS_OFFSET: usize = 1024;
383
384/// Write audio/MIDI port counts to scratch.
385///
386/// # Safety
387/// `ptr` must point to a valid SHM allocation.
388pub unsafe fn write_port_counts_to_scratch(
389    ptr: *mut u8,
390    audio_in: u32,
391    audio_out: u32,
392    midi_in: u32,
393    midi_out: u32,
394) {
395    unsafe {
396        let dest = scratch_ptr(ptr).add(PORT_COUNTS_OFFSET);
397        std::ptr::write_unaligned(dest as *mut u32, PORT_COUNTS_MAGIC);
398        std::ptr::write_unaligned(dest.add(4) as *mut u32, audio_in);
399        std::ptr::write_unaligned(dest.add(8) as *mut u32, audio_out);
400        std::ptr::write_unaligned(dest.add(12) as *mut u32, midi_in);
401        std::ptr::write_unaligned(dest.add(16) as *mut u32, midi_out);
402    }
403}
404
405/// Read audio/MIDI port counts from scratch.
406///
407/// # Safety
408/// `ptr` must point to a valid SHM allocation.
409pub unsafe fn read_port_counts_from_scratch(ptr: *mut u8) -> Option<(u32, u32, u32, u32)> {
410    unsafe {
411        let src = scratch_ptr(ptr).add(PORT_COUNTS_OFFSET);
412        let magic = std::ptr::read_unaligned(src as *mut u32);
413        if magic != PORT_COUNTS_MAGIC {
414            return None;
415        }
416        let audio_in = std::ptr::read_unaligned(src.add(4) as *mut u32);
417        let audio_out = std::ptr::read_unaligned(src.add(8) as *mut u32);
418        let midi_in = std::ptr::read_unaligned(src.add(12) as *mut u32);
419        let midi_out = std::ptr::read_unaligned(src.add(16) as *mut u32);
420        Some((audio_in, audio_out, midi_in, midi_out))
421    }
422}
423
424// --- Static assertions for sizes ---
425
426const _: () = assert!(std::mem::size_of::<ShmHeader>() == 256);
427const _: () = assert!(std::mem::align_of::<ShmHeader>() == 256);
428const _: () = assert!(std::mem::size_of::<ParameterEvent>() == 16);
429const _: () = assert!(std::mem::align_of::<ParameterEvent>() == 16);
430const _: () = assert!(std::mem::size_of::<MidiEvent>() == 16);
431const _: () = assert!(std::mem::align_of::<MidiEvent>() == 16);
432const _: () = assert!(std::mem::size_of::<TransportState>() == 256);
433const _: () = assert!(std::mem::align_of::<TransportState>() == 256);
434const _: () = assert!(LAYOUT_SIZE <= SHM_SIZE);
435
436/// Wait (spin + yield) until `ready` becomes non-zero or timeout elapses.
437pub fn wait_for_ready(header: &ShmHeader, timeout: std::time::Duration) -> bool {
438    let start = std::time::Instant::now();
439    while header.ready.load(Ordering::Acquire) == 0 {
440        if start.elapsed() >= timeout {
441            return false;
442        }
443        std::thread::yield_now();
444    }
445    true
446}