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