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.
9/// Version 4: Per-port MIDI input/output rings (MAX_MIDI_PORTS each direction).
10/// Version 5: Plugin-reported latency in samples.
11/// Version 6: Added GUI parent API tag for native window handles.
12pub const VERSION: u32 = 6;
13
14/// Maximum number of audio channels (main + sidechain combined).
15pub const MAX_CHANNELS: usize = 32;
16
17/// Number of audio buses (main + sidechain).
18pub const NUM_BUSES: usize = 2;
19
20/// Maximum audio block size in samples.
21pub const MAX_BLOCK_SIZE: usize = 4096;
22
23/// Capacity of each ring buffer in slots (power of two).
24pub const RING_CAPACITY: usize = 4096;
25
26/// Maximum number of MIDI ports per direction.
27/// Runtime counts may be lower; this is the SHM capacity.
28pub const MAX_MIDI_PORTS: usize = 16;
29
30// --- Section sizes ---
31pub const HEADER_SIZE: usize = 256;
32pub const CONTROL_SIZE: usize = 256;
33pub const AUDIO_BUFFER_SIZE: usize = MAX_CHANNELS * NUM_BUSES * MAX_BLOCK_SIZE * 4; // f32
34pub const PARAM_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<ParameterEvent>();
35/// Size of the data area for one MIDI port ring (event slots only).
36pub const MIDI_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<MidiEvent>();
37/// Size of one MIDI port ring area including embedded write/read atomics.
38/// The atomics live at the start of the area, followed by 8 bytes of padding,
39/// then the 16-byte-aligned `MidiEvent` slots.
40pub const MIDI_PORT_RING_SIZE: usize = {
41    let raw = 16 + MIDI_RING_SIZE; // head/tail atomics + padding + event slots
42    (raw + 15) & !15 // align up to 16 bytes for the next port
43};
44pub const TRANSPORT_SIZE: usize = 256;
45pub const SCRATCH_SIZE: usize = 65536;
46
47// --- Offsets into the shared-memory segment ---
48/// Control area starts right after the header.
49pub const CONTROL_OFFSET: usize = HEADER_SIZE;
50/// Audio buffers start after the control area.
51pub const AUDIO_OFFSET: usize = HEADER_SIZE + CONTROL_SIZE;
52/// Parameter ring buffer.
53pub const PARAM_RING_OFFSET: usize = AUDIO_OFFSET + AUDIO_BUFFER_SIZE;
54/// Echo/parameter-change ring buffer.
55pub const ECHO_RING_OFFSET: usize = PARAM_RING_OFFSET + PARAM_RING_SIZE;
56pub const ECHO_RING_SIZE: usize = RING_CAPACITY * std::mem::size_of::<ParameterEvent>();
57/// Per-port MIDI input rings start after the echo ring.
58pub const MIDI_IN_RINGS_OFFSET: usize = {
59    let end = ECHO_RING_OFFSET + ECHO_RING_SIZE;
60    (end + 255) & !255
61};
62pub const MIDI_IN_RINGS_SIZE: usize = MAX_MIDI_PORTS * MIDI_PORT_RING_SIZE;
63/// Per-port MIDI output rings follow the input rings.
64pub const MIDI_OUT_RINGS_OFFSET: usize = MIDI_IN_RINGS_OFFSET + MIDI_IN_RINGS_SIZE;
65pub const MIDI_OUT_RINGS_SIZE: usize = MAX_MIDI_PORTS * MIDI_PORT_RING_SIZE;
66/// Transport state block (256-byte aligned from here).
67pub const TRANSPORT_OFFSET: usize = {
68    let end = MIDI_OUT_RINGS_OFFSET + MIDI_OUT_RINGS_SIZE;
69    // Align up to 256 bytes
70    (end + 255) & !255
71};
72/// State blob scratch area.
73pub const SCRATCH_OFFSET: usize = TRANSPORT_OFFSET + TRANSPORT_SIZE;
74
75/// Total bytes actively used by the protocol layout.
76pub const LAYOUT_SIZE: usize = SCRATCH_OFFSET + SCRATCH_SIZE;
77
78/// Total shared-memory allocation size (4 MiB, page-aligned).
79pub const SHM_SIZE: usize = 4 * 1024 * 1024;
80
81// --- Control-area indices (all 4-byte atomics inside CONTROL_OFFSET..CONTROL_OFFSET+256) ---
82pub const PARAM_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET;
83pub const PARAM_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 4;
84pub const ECHO_WRITE_IDX_OFFSET: usize = CONTROL_OFFSET + 8;
85pub const ECHO_READ_IDX_OFFSET: usize = CONTROL_OFFSET + 12;
86pub const GUI_MODE_OFFSET: usize = CONTROL_OFFSET + 16;
87pub const GUI_PARENT_API_OFFSET: usize = CONTROL_OFFSET + 20;
88
89/// GUI mode requested by the DAW.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
91pub enum GuiMode {
92    /// DAW provides a parent window; plugin UI should be embedded.
93    #[default]
94    Embedded = 0,
95    /// DAW cannot provide a parent window; plugin-host must create a top-level window.
96    Floating = 1,
97}
98
99impl GuiMode {
100    pub fn from_u32(value: u32) -> Self {
101        match value {
102            1 => GuiMode::Floating,
103            _ => GuiMode::Embedded,
104        }
105    }
106
107    pub fn as_u32(self) -> u32 {
108        self as u32
109    }
110}
111
112/// Native window-system API for the GUI parent handle.
113#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
114pub enum GuiParentApi {
115    /// No native parent handle has been provided.
116    #[default]
117    None = 0,
118    /// X11/Xlib window ID.
119    X11 = 1,
120    /// Wayland surface/object handle.
121    Wayland = 2,
122}
123
124impl GuiParentApi {
125    pub fn from_u32(value: u32) -> Self {
126        match value {
127            1 => GuiParentApi::X11,
128            2 => GuiParentApi::Wayland,
129            _ => GuiParentApi::None,
130        }
131    }
132
133    pub fn as_u32(self) -> u32 {
134        self as u32
135    }
136}
137
138// --- Structs ---
139
140pub const PARAM_EVENT_VALUE: u32 = 0;
141pub const PARAM_EVENT_MOD: u32 = 1;
142pub const PARAM_EVENT_GESTURE_BEGIN: u32 = 2;
143pub const PARAM_EVENT_GESTURE_END: u32 = 3;
144
145/// Fixed-size parameter change event (16 bytes, 16-byte aligned).
146#[repr(C, align(16))]
147#[derive(Clone, Copy, Debug, Default)]
148pub struct ParameterEvent {
149    pub param_index: u32,
150    pub value: f32,
151    pub sample_offset: u32,
152    pub event_kind: u32,
153}
154
155/// Fixed-size MIDI event (16 bytes, 16-byte aligned).
156#[repr(C, align(16))]
157#[derive(Clone, Copy, Debug, Default)]
158pub struct MidiEvent {
159    pub sample_offset: u32,
160    pub data: [u8; 3],
161    pub channel: u8,
162    pub flags: u16,
163    pub _pad: u16,
164}
165
166/// Transport state block (256 bytes).
167#[repr(C, align(256))]
168#[derive(Clone, Copy, Debug)]
169pub struct TransportState {
170    pub playhead_sample: u64,
171    pub tempo: f64,
172    pub numerator: u32,
173    pub denominator: u32,
174    pub flags: u32,
175    pub sample_rate_hz: f64,
176    _pad: [u8; 256 - 40],
177}
178
179impl Default for TransportState {
180    fn default() -> Self {
181        Self {
182            playhead_sample: 0,
183            tempo: 120.0,
184            numerator: 4,
185            denominator: 4,
186            flags: 0,
187            sample_rate_hz: 0.0,
188            _pad: [0; 256 - 40],
189        }
190    }
191}
192
193/// Shared-memory header (256 bytes).
194#[repr(C, align(256))]
195pub struct ShmHeader {
196    pub magic: u32,
197    pub version: u32,
198    pub flags: u32,
199    pub ready: AtomicU32,
200    pub heartbeat: AtomicU32,
201    pub error_code: u32,
202    pub shutdown_request: AtomicU32,
203    pub tasks_issued: AtomicU32,
204    pub tasks_completed: AtomicU32,
205    pub block_size: AtomicU32,
206    pub num_input_channels: AtomicU32,
207    pub num_output_channels: AtomicU32,
208    /// Number of MIDI input ports actually used by the plugin (<= MAX_MIDI_PORTS).
209    pub midi_in_port_count: AtomicU32,
210    /// Number of MIDI output ports actually used by the plugin (<= MAX_MIDI_PORTS).
211    pub midi_out_port_count: AtomicU32,
212    /// Request type: 0 = none, 1 = save_state, 2 = restore_state, 3 = gui_show, 4 = gui_hide,
213    /// 5 = set_resource_directory, 6 = enumerate_file_references, 7 = update_file_reference,
214    /// 8 = enumerate_lv2_control_ports, 9 = enumerate_clap_parameters,
215    /// 11 = enumerate_clap_note_names, 12 = enumerate_clap_audio_ports
216    pub request_type: AtomicU32,
217    /// Request status: 0 = pending, 1 = success, 2 = error
218    pub request_status: AtomicU32,
219    /// Valid bytes in scratch area for state operations
220    pub scratch_size: AtomicU32,
221    /// Parent window ID for GUI embedding. See GUI_PARENT_API_OFFSET for Unix API tagging.
222    pub parent_window: AtomicU64,
223    /// Set to 1 by the plugin-host when the plugin calls clap_host_state.mark_dirty()
224    pub state_dirty: AtomicU32,
225    /// Current plugin latency in samples, refreshed by the host.
226    pub latency_samples: AtomicU32,
227    _pad: [u8; 256 - 88],
228}
229
230impl ShmHeader {
231    /// Load parent_window as a `usize` (handles 32- and 64-bit platforms).
232    pub fn parent_window_usize(&self) -> usize {
233        self.parent_window.load(Ordering::Acquire) as usize
234    }
235
236    /// Store a `usize` parent_window (truncates on 32-bit, but HWNDs/XIDs are
237    /// always within 64 bits).
238    pub fn set_parent_window(&self, window: usize) {
239        self.parent_window.store(window as u64, Ordering::Release);
240    }
241
242    fn gui_parent_api_atomic(&self) -> &AtomicU32 {
243        // SAFETY: GUI_PARENT_API_OFFSET is inside the control area, which is
244        // within the header's 256-byte allocation. The offset is aligned to 4 bytes.
245        unsafe {
246            let base = self as *const Self as *const u8;
247            &*(base.add(GUI_PARENT_API_OFFSET) as *const AtomicU32)
248        }
249    }
250
251    /// Load the native API of the GUI parent handle.
252    pub fn gui_parent_api(&self) -> GuiParentApi {
253        GuiParentApi::from_u32(self.gui_parent_api_atomic().load(Ordering::Acquire))
254    }
255
256    /// Store the native API of the GUI parent handle.
257    pub fn set_gui_parent_api(&self, api: GuiParentApi) {
258        self.gui_parent_api_atomic()
259            .store(api.as_u32(), Ordering::Release);
260    }
261
262    fn gui_mode_atomic(&self) -> &AtomicU32 {
263        // SAFETY: GUI_MODE_OFFSET is inside the control area, which is within the
264        // header's 256-byte allocation. The offset is aligned to 4 bytes.
265        unsafe {
266            let base = self as *const Self as *const u8;
267            &*(base.add(GUI_MODE_OFFSET) as *const AtomicU32)
268        }
269    }
270
271    /// Load the requested GUI mode.
272    pub fn gui_mode(&self) -> GuiMode {
273        GuiMode::from_u32(self.gui_mode_atomic().load(Ordering::Acquire))
274    }
275
276    /// Store the requested GUI mode.
277    pub fn set_gui_mode(&self, mode: GuiMode) {
278        self.gui_mode_atomic()
279            .store(mode.as_u32(), Ordering::Release);
280    }
281}
282
283impl Default for ShmHeader {
284    fn default() -> Self {
285        Self {
286            magic: MAGIC,
287            version: VERSION,
288            flags: 0,
289            ready: AtomicU32::new(0),
290            heartbeat: AtomicU32::new(0),
291            error_code: 0,
292            shutdown_request: AtomicU32::new(0),
293            tasks_issued: AtomicU32::new(0),
294            tasks_completed: AtomicU32::new(0),
295            block_size: AtomicU32::new(0),
296            num_input_channels: AtomicU32::new(0),
297            num_output_channels: AtomicU32::new(0),
298            midi_in_port_count: AtomicU32::new(0),
299            midi_out_port_count: AtomicU32::new(0),
300            request_type: AtomicU32::new(0),
301            request_status: AtomicU32::new(0),
302            scratch_size: AtomicU32::new(0),
303            parent_window: AtomicU64::new(0),
304            state_dirty: AtomicU32::new(0),
305            latency_samples: AtomicU32::new(0),
306            _pad: [0; 256 - 88],
307        }
308    }
309}
310
311// --- Layout helpers ---
312
313/// Zero-initialize the entire shared-memory region and write the header.
314///
315/// # Safety
316/// `ptr` must be a valid pointer to a memory region of `size` bytes.
317pub unsafe fn init_shm_layout(ptr: *mut u8, size: usize) {
318    unsafe {
319        std::ptr::write_bytes(ptr, 0, size);
320        let header = ptr as *mut ShmHeader;
321        std::ptr::write(header, ShmHeader::default());
322    }
323}
324
325/// Returns a reference to the header at the start of the mapping.
326///
327/// # Safety
328/// `ptr` must point to a valid allocation containing at least `ShmHeader`'s size.
329pub unsafe fn header_ref(ptr: *mut u8) -> &'static ShmHeader {
330    unsafe { &*(ptr as *mut ShmHeader) }
331}
332
333/// Returns a mutable reference to the header.
334///
335/// # Safety
336/// `ptr` must point to a valid allocation containing at least `ShmHeader`'s size.
337pub unsafe fn header_mut(ptr: *mut u8) -> &'static mut ShmHeader {
338    unsafe { &mut *(ptr as *mut ShmHeader) }
339}
340
341/// Returns a pointer to the audio buffer region.
342///
343/// # Safety
344/// `ptr` must point to an allocation large enough to contain the audio buffer.
345pub unsafe fn audio_ptr(ptr: *mut u8) -> *mut f32 {
346    unsafe { ptr.add(AUDIO_OFFSET) as *mut f32 }
347}
348
349/// Returns a pointer to a specific channel/bus plane.
350///
351/// `channel` is 0-based up to `MAX_CHANNELS - 1`.
352/// `bus` is 0 (main) or 1 (sidechain).
353///
354/// # Safety
355/// `ptr` must point to a valid allocation large enough to contain the audio data.
356pub unsafe fn audio_channel_ptr(ptr: *mut u8, channel: usize, bus: usize) -> *mut f32 {
357    let plane_size = MAX_BLOCK_SIZE * std::mem::size_of::<f32>();
358    let offset = AUDIO_OFFSET + (channel * NUM_BUSES + bus) * plane_size;
359    unsafe { ptr.add(offset) as *mut f32 }
360}
361
362/// Returns a pointer to the parameter ring buffer slot array.
363///
364/// # Safety
365/// `ptr` must point to a valid allocation large enough to contain the parameter ring.
366pub unsafe fn param_ring_ptr(ptr: *mut u8) -> *mut ParameterEvent {
367    unsafe { ptr.add(PARAM_RING_OFFSET) as *mut ParameterEvent }
368}
369
370/// Returns pointers to the parameter ring write/read atomics.
371///
372/// # Safety
373/// `ptr` must point to a valid allocation containing the parameter ring atomics.
374pub unsafe fn param_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
375    unsafe {
376        (
377            ptr.add(PARAM_WRITE_IDX_OFFSET) as *mut AtomicU32,
378            ptr.add(PARAM_READ_IDX_OFFSET) as *mut AtomicU32,
379        )
380    }
381}
382
383/// Returns a pointer to the echo ring buffer slot array.
384///
385/// # Safety
386/// `ptr` must point to a valid allocation large enough to contain the echo ring.
387pub unsafe fn echo_ring_ptr(ptr: *mut u8) -> *mut ParameterEvent {
388    unsafe { ptr.add(ECHO_RING_OFFSET) as *mut ParameterEvent }
389}
390
391/// Returns pointers to the echo ring write/read atomics.
392///
393/// # Safety
394/// `ptr` must point to a valid allocation containing the echo ring atomics.
395pub unsafe fn echo_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
396    unsafe {
397        (
398            ptr.add(ECHO_WRITE_IDX_OFFSET) as *mut AtomicU32,
399            ptr.add(ECHO_READ_IDX_OFFSET) as *mut AtomicU32,
400        )
401    }
402}
403
404const fn midi_port_ring_offset(base_offset: usize, port: usize) -> usize {
405    base_offset + port * MIDI_PORT_RING_SIZE
406}
407
408/// Returns pointers to the embedded write/read atomics for a MIDI input port ring.
409///
410/// # Safety
411/// `ptr` must point to a valid allocation and `port` must be < MAX_MIDI_PORTS.
412pub unsafe fn midi_in_indices(ptr: *mut u8, port: usize) -> (*mut AtomicU32, *mut AtomicU32) {
413    unsafe {
414        let base = ptr.add(midi_port_ring_offset(MIDI_IN_RINGS_OFFSET, port));
415        (base as *mut AtomicU32, base.add(4) as *mut AtomicU32)
416    }
417}
418
419/// Returns a pointer to the MIDI input port ring buffer slot array.
420///
421/// # Safety
422/// `ptr` must point to a valid allocation and `port` must be < MAX_MIDI_PORTS.
423pub unsafe fn midi_in_ring_ptr(ptr: *mut u8, port: usize) -> *mut MidiEvent {
424    unsafe { ptr.add(midi_port_ring_offset(MIDI_IN_RINGS_OFFSET, port) + 16) as *mut MidiEvent }
425}
426
427/// Returns pointers to the embedded write/read atomics for a MIDI output port ring.
428///
429/// # Safety
430/// `ptr` must point to a valid allocation and `port` must be < MAX_MIDI_PORTS.
431pub unsafe fn midi_out_indices(ptr: *mut u8, port: usize) -> (*mut AtomicU32, *mut AtomicU32) {
432    unsafe {
433        let base = ptr.add(midi_port_ring_offset(MIDI_OUT_RINGS_OFFSET, port));
434        (base as *mut AtomicU32, base.add(4) as *mut AtomicU32)
435    }
436}
437
438/// Returns a pointer to the MIDI output port ring buffer slot array.
439///
440/// # Safety
441/// `ptr` must point to a valid allocation and `port` must be < MAX_MIDI_PORTS.
442pub unsafe fn midi_out_ring_ptr(ptr: *mut u8, port: usize) -> *mut MidiEvent {
443    unsafe { ptr.add(midi_port_ring_offset(MIDI_OUT_RINGS_OFFSET, port) + 16) as *mut MidiEvent }
444}
445
446/// Returns a reference to the transport state.
447///
448/// # Safety
449/// `ptr` must point to a valid allocation containing at least `TransportState`'s size.
450pub unsafe fn transport_ref(ptr: *mut u8) -> &'static TransportState {
451    unsafe { &*(ptr.add(TRANSPORT_OFFSET) as *mut TransportState) }
452}
453
454/// Returns a mutable reference to the transport state.
455///
456/// # Safety
457/// `ptr` must point to a valid allocation containing at least `TransportState`'s size.
458pub unsafe fn transport_mut(ptr: *mut u8) -> &'static mut TransportState {
459    unsafe { &mut *(ptr.add(TRANSPORT_OFFSET) as *mut TransportState) }
460}
461
462/// Returns a pointer to the scratch buffer region.
463///
464/// # Safety
465/// `ptr` must point to an allocation large enough to contain the scratch buffer.
466pub unsafe fn scratch_ptr(ptr: *mut u8) -> *mut u8 {
467    unsafe { ptr.add(SCRATCH_OFFSET) }
468}
469
470/// Write a plugin name to the start of the scratch buffer.
471/// The name is encoded as a little-endian u32 length followed by UTF-8 bytes.
472///
473/// # Safety
474/// `ptr` must point to a valid SHM allocation.
475pub unsafe fn write_plugin_name_to_scratch(ptr: *mut u8, name: &str) {
476    unsafe {
477        let scratch = scratch_ptr(ptr);
478        let bytes = name.as_bytes();
479        let len = bytes.len().min(SCRATCH_SIZE - 4);
480        std::ptr::write_unaligned(scratch as *mut u32, len as u32);
481        std::ptr::copy_nonoverlapping(bytes.as_ptr(), scratch.add(4), len);
482    }
483}
484
485/// Read a plugin name from the start of the scratch buffer.
486///
487/// # Safety
488/// `ptr` must point to a valid SHM allocation.
489pub unsafe fn read_plugin_name_from_scratch(ptr: *mut u8) -> Option<String> {
490    unsafe {
491        let scratch = scratch_ptr(ptr);
492        let len = std::ptr::read_unaligned(scratch as *mut u32) as usize;
493        if len == 0 || len > SCRATCH_SIZE - 4 {
494            return None;
495        }
496        let bytes = std::slice::from_raw_parts(scratch.add(4), len);
497        String::from_utf8(bytes.to_vec()).ok()
498    }
499}
500
501/// Magic value written before port counts in scratch.
502pub const PORT_COUNTS_MAGIC: u32 = 0x504F_5254; // "PORT"
503
504/// Offset within scratch where port counts are stored (after plugin name).
505const PORT_COUNTS_OFFSET: usize = 1024;
506
507/// Write audio/MIDI port counts to scratch.
508///
509/// # Safety
510/// `ptr` must point to a valid SHM allocation.
511pub unsafe fn write_port_counts_to_scratch(
512    ptr: *mut u8,
513    audio_in: u32,
514    audio_out: u32,
515    midi_in: u32,
516    midi_out: u32,
517) {
518    unsafe {
519        let dest = scratch_ptr(ptr).add(PORT_COUNTS_OFFSET);
520        std::ptr::write_unaligned(dest as *mut u32, PORT_COUNTS_MAGIC);
521        std::ptr::write_unaligned(dest.add(4) as *mut u32, audio_in);
522        std::ptr::write_unaligned(dest.add(8) as *mut u32, audio_out);
523        std::ptr::write_unaligned(dest.add(12) as *mut u32, midi_in);
524        std::ptr::write_unaligned(dest.add(16) as *mut u32, midi_out);
525    }
526}
527
528/// Read audio/MIDI port counts from scratch.
529///
530/// # Safety
531/// `ptr` must point to a valid SHM allocation.
532pub unsafe fn read_port_counts_from_scratch(ptr: *mut u8) -> Option<(u32, u32, u32, u32)> {
533    unsafe {
534        let src = scratch_ptr(ptr).add(PORT_COUNTS_OFFSET);
535        let magic = std::ptr::read_unaligned(src as *mut u32);
536        if magic != PORT_COUNTS_MAGIC {
537            return None;
538        }
539        let audio_in = std::ptr::read_unaligned(src.add(4) as *mut u32);
540        let audio_out = std::ptr::read_unaligned(src.add(8) as *mut u32);
541        let midi_in = std::ptr::read_unaligned(src.add(12) as *mut u32);
542        let midi_out = std::ptr::read_unaligned(src.add(16) as *mut u32);
543        Some((audio_in, audio_out, midi_in, midi_out))
544    }
545}
546
547/// Magic value written before file-reference string list in scratch.
548pub const FILE_REFS_MAGIC: u32 = 0x4649_4C45; // "FILE"
549
550/// Offset within scratch where file-reference string list is stored.
551const FILE_REFS_OFFSET: usize = 2048;
552
553/// Maximum total bytes available for the file-reference list.
554const FILE_REFS_MAX_SIZE: usize = SCRATCH_SIZE - FILE_REFS_OFFSET;
555
556/// A file reference returned by a plugin, paired with its plugin-side index.
557pub type FileReference = (u32, String);
558
559/// Write a list of file-reference (index, path) pairs to scratch.
560/// Format: magic (u32), count (u32), then for each entry:
561///   index (u32), length (u32) followed by UTF-8 bytes.
562///
563/// # Safety
564/// `ptr` must point to a valid SHM allocation.
565pub unsafe fn write_file_references_to_scratch(
566    ptr: *mut u8,
567    refs: &[FileReference],
568) -> Result<(), String> {
569    unsafe {
570        let mut dest = scratch_ptr(ptr).add(FILE_REFS_OFFSET);
571        let mut remaining = FILE_REFS_MAX_SIZE;
572        if remaining < 8 {
573            return Err("scratch too small for file references".to_string());
574        }
575        std::ptr::write_unaligned(dest as *mut u32, FILE_REFS_MAGIC);
576        dest = dest.add(4);
577        remaining -= 4;
578        let count = refs.len().min(u32::MAX as usize) as u32;
579        std::ptr::write_unaligned(dest as *mut u32, count);
580        dest = dest.add(4);
581        remaining -= 4;
582        for (index, path) in refs.iter().take(count as usize) {
583            if remaining < 8 {
584                return Err("scratch overflow writing file references".to_string());
585            }
586            std::ptr::write_unaligned(dest as *mut u32, *index);
587            dest = dest.add(4);
588            remaining -= 4;
589            let bytes = path.as_bytes();
590            let len = bytes
591                .len()
592                .min(u32::MAX as usize)
593                .min(remaining.saturating_sub(4));
594            if len < bytes.len() {
595                return Err("scratch overflow writing file references".to_string());
596            }
597            std::ptr::write_unaligned(dest as *mut u32, len as u32);
598            dest = dest.add(4);
599            remaining -= 4;
600            std::ptr::copy_nonoverlapping(bytes.as_ptr(), dest, len);
601            dest = dest.add(len);
602            remaining -= len;
603        }
604        Ok(())
605    }
606}
607
608/// Read a list of file-reference (index, path) pairs from scratch.
609///
610/// # Safety
611/// `ptr` must point to a valid SHM allocation.
612pub unsafe fn read_file_references_from_scratch(ptr: *mut u8) -> Option<Vec<FileReference>> {
613    unsafe {
614        let mut src = scratch_ptr(ptr).add(FILE_REFS_OFFSET);
615        let mut remaining = FILE_REFS_MAX_SIZE;
616        if remaining < 8 {
617            return None;
618        }
619        let magic = std::ptr::read_unaligned(src as *mut u32);
620        if magic != FILE_REFS_MAGIC {
621            return None;
622        }
623        src = src.add(4);
624        remaining -= 4;
625        let count = std::ptr::read_unaligned(src as *mut u32) as usize;
626        src = src.add(4);
627        remaining -= 4;
628        let mut refs = Vec::with_capacity(count);
629        for _ in 0..count {
630            if remaining < 8 {
631                return None;
632            }
633            let index = std::ptr::read_unaligned(src as *mut u32);
634            src = src.add(4);
635            remaining -= 4;
636            let len = std::ptr::read_unaligned(src as *mut u32) as usize;
637            src = src.add(4);
638            remaining -= 4;
639            if len > remaining {
640                return None;
641            }
642            let bytes = std::slice::from_raw_parts(src, len);
643            let path = String::from_utf8(bytes.to_vec()).ok()?;
644            refs.push((index, path));
645            src = src.add(len);
646            remaining -= len;
647        }
648        Some(refs)
649    }
650}
651
652/// Write a resource-directory / base-directory path to scratch.
653/// Format: magic (u32), length (u32), UTF-8 bytes.
654///
655/// # Safety
656/// `ptr` must point to a valid SHM allocation.
657pub unsafe fn write_resource_directory_to_scratch(ptr: *mut u8, path: &str) -> Result<(), String> {
658    unsafe {
659        let scratch = scratch_ptr(ptr);
660        let bytes = path.as_bytes();
661        let len = bytes.len().min(SCRATCH_SIZE - 8);
662        if len < bytes.len() {
663            return Err("resource directory path too long".to_string());
664        }
665        std::ptr::write_unaligned(scratch as *mut u32, FILE_REFS_MAGIC);
666        std::ptr::write_unaligned(scratch.add(4) as *mut u32, len as u32);
667        std::ptr::copy_nonoverlapping(bytes.as_ptr(), scratch.add(8), len);
668        Ok(())
669    }
670}
671
672/// Read a resource-directory / base-directory path from scratch.
673///
674/// # Safety
675/// `ptr` must point to a valid SHM allocation.
676pub unsafe fn read_resource_directory_from_scratch(ptr: *mut u8) -> Option<String> {
677    unsafe {
678        let scratch = scratch_ptr(ptr);
679        let magic = std::ptr::read_unaligned(scratch as *mut u32);
680        if magic != FILE_REFS_MAGIC {
681            return None;
682        }
683        let len = std::ptr::read_unaligned(scratch.add(4) as *mut u32) as usize;
684        if len == 0 || len > SCRATCH_SIZE - 8 {
685            return None;
686        }
687        let bytes = std::slice::from_raw_parts(scratch.add(8), len);
688        String::from_utf8(bytes.to_vec()).ok()
689    }
690}
691
692/// Request type: enumerate LV2 control ports (index, name, min, max, value).
693pub const REQUEST_LV2_CONTROL_PORTS: u32 = 8;
694
695/// Request type: enumerate CLAP parameters (id, name, module, min, max, default).
696pub const REQUEST_CLAP_PARAMETERS: u32 = 9;
697
698/// Request type: fetch LV2 midnam note names (MIDI note number -> name).
699pub const REQUEST_LV2_MIDNAM: u32 = 10;
700
701/// Request type: fetch CLAP note names (MIDI note number -> name).
702pub const REQUEST_CLAP_NOTE_NAMES: u32 = 11;
703
704/// Request type: refresh CLAP audio port counts in scratch.
705pub const REQUEST_CLAP_AUDIO_PORTS: u32 = 12;
706
707/// Magic value for a single file-reference update in scratch.
708pub const FILE_REF_UPDATE_MAGIC: u32 = 0x5550_4441; // "UPDA"
709
710/// Write a file-reference update (index + new path) to scratch.
711/// Format: magic (u32), index (u32), length (u32), UTF-8 bytes.
712///
713/// # Safety
714/// `ptr` must point to a valid SHM allocation.
715pub unsafe fn write_file_reference_update_to_scratch(
716    ptr: *mut u8,
717    index: u32,
718    path: &str,
719) -> Result<(), String> {
720    unsafe {
721        let scratch = scratch_ptr(ptr);
722        let bytes = path.as_bytes();
723        let len = bytes.len().min(SCRATCH_SIZE - 12);
724        if len < bytes.len() {
725            return Err("file-reference update path too long".to_string());
726        }
727        std::ptr::write_unaligned(scratch as *mut u32, FILE_REF_UPDATE_MAGIC);
728        std::ptr::write_unaligned(scratch.add(4) as *mut u32, index);
729        std::ptr::write_unaligned(scratch.add(8) as *mut u32, len as u32);
730        std::ptr::copy_nonoverlapping(bytes.as_ptr(), scratch.add(12), len);
731        Ok(())
732    }
733}
734
735/// Read a file-reference update (index + new path) from scratch.
736///
737/// # Safety
738/// `ptr` must point to a valid SHM allocation.
739pub unsafe fn read_file_reference_update_from_scratch(ptr: *mut u8) -> Option<(u32, String)> {
740    unsafe {
741        let scratch = scratch_ptr(ptr);
742        let magic = std::ptr::read_unaligned(scratch as *mut u32);
743        if magic != FILE_REF_UPDATE_MAGIC {
744            return None;
745        }
746        let index = std::ptr::read_unaligned(scratch.add(4) as *mut u32);
747        let len = std::ptr::read_unaligned(scratch.add(8) as *mut u32) as usize;
748        if len == 0 || len > SCRATCH_SIZE - 12 {
749            return None;
750        }
751        let bytes = std::slice::from_raw_parts(scratch.add(12), len);
752        let path = String::from_utf8(bytes.to_vec()).ok()?;
753        Some((index, path))
754    }
755}
756
757// --- Static assertions for sizes ---
758
759const _: () = assert!(std::mem::size_of::<ShmHeader>() == 256);
760const _: () = assert!(std::mem::align_of::<ShmHeader>() == 256);
761const _: () = assert!(std::mem::size_of::<ParameterEvent>() == 16);
762const _: () = assert!(std::mem::align_of::<ParameterEvent>() == 16);
763const _: () = assert!(std::mem::size_of::<MidiEvent>() == 16);
764const _: () = assert!(std::mem::align_of::<MidiEvent>() == 16);
765const _: () = assert!(std::mem::size_of::<TransportState>() == 256);
766const _: () = assert!(std::mem::align_of::<TransportState>() == 256);
767const _: () = assert!(LAYOUT_SIZE <= SHM_SIZE);
768
769/// Wait (spin + yield) until `ready` becomes non-zero or timeout elapses.
770pub fn wait_for_ready(header: &ShmHeader, timeout: std::time::Duration) -> bool {
771    let start = std::time::Instant::now();
772    while header.ready.load(Ordering::Acquire) == 0 {
773        if start.elapsed() >= timeout {
774            return false;
775        }
776        std::thread::yield_now();
777    }
778    true
779}