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