Skip to main content

maolan_plugin_protocol/
protocol.rs

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