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