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    /// Set to 1 by the plugin-host when the plugin calls clap_host_state.mark_dirty()
150    pub state_dirty: AtomicU32,
151    _pad: [u8; 256 - 76],
152}
153
154impl ShmHeader {
155    /// Load parent_window as a `usize` (handles 32- and 64-bit platforms).
156    pub fn parent_window_usize(&self) -> usize {
157        self.parent_window.load(Ordering::Acquire) as usize
158    }
159
160    /// Store a `usize` parent_window (truncates on 32-bit, but HWNDs/XIDs are
161    /// always within 64 bits).
162    pub fn set_parent_window(&self, window: usize) {
163        self.parent_window.store(window as u64, Ordering::Release);
164    }
165}
166
167impl Default for ShmHeader {
168    fn default() -> Self {
169        Self {
170            magic: MAGIC,
171            version: VERSION,
172            flags: 0,
173            ready: AtomicU32::new(0),
174            heartbeat: AtomicU32::new(0),
175            error_code: 0,
176            shutdown_request: AtomicU32::new(0),
177            tasks_issued: AtomicU32::new(0),
178            tasks_completed: AtomicU32::new(0),
179            block_size: AtomicU32::new(0),
180            num_input_channels: AtomicU32::new(0),
181            num_output_channels: AtomicU32::new(0),
182            request_type: AtomicU32::new(0),
183            request_status: AtomicU32::new(0),
184            scratch_size: AtomicU32::new(0),
185            parent_window: AtomicU64::new(0),
186            state_dirty: AtomicU32::new(0),
187            _pad: [0; 256 - 76],
188        }
189    }
190}
191
192// --- Layout helpers ---
193
194/// Zero-initialize the entire shared-memory region and write the header.
195///
196/// # Safety
197/// `ptr` must be a valid pointer to a memory region of `size` bytes.
198pub unsafe fn init_shm_layout(ptr: *mut u8, size: usize) {
199    unsafe {
200        std::ptr::write_bytes(ptr, 0, size);
201        let header = ptr as *mut ShmHeader;
202        std::ptr::write(header, ShmHeader::default());
203    }
204}
205
206/// Returns a reference to the header at the start of the mapping.
207///
208/// # Safety
209/// `ptr` must point to a valid allocation containing at least `ShmHeader`'s size.
210pub unsafe fn header_ref(ptr: *mut u8) -> &'static ShmHeader {
211    unsafe { &*(ptr as *mut ShmHeader) }
212}
213
214/// Returns a mutable reference to the header.
215///
216/// # Safety
217/// `ptr` must point to a valid allocation containing at least `ShmHeader`'s size.
218pub unsafe fn header_mut(ptr: *mut u8) -> &'static mut ShmHeader {
219    unsafe { &mut *(ptr as *mut ShmHeader) }
220}
221
222/// Returns a pointer to the audio buffer region.
223///
224/// # Safety
225/// `ptr` must point to an allocation large enough to contain the audio buffer.
226pub unsafe fn audio_ptr(ptr: *mut u8) -> *mut f32 {
227    unsafe { ptr.add(AUDIO_OFFSET) as *mut f32 }
228}
229
230/// Returns a pointer to a specific channel/bus plane.
231///
232/// `channel` is 0-based up to `MAX_CHANNELS - 1`.
233/// `bus` is 0 (main) or 1 (sidechain).
234///
235/// # Safety
236/// `ptr` must point to a valid allocation large enough to contain the audio data.
237pub unsafe fn audio_channel_ptr(ptr: *mut u8, channel: usize, bus: usize) -> *mut f32 {
238    let plane_size = MAX_BLOCK_SIZE * std::mem::size_of::<f32>();
239    let offset = AUDIO_OFFSET + (channel * NUM_BUSES + bus) * plane_size;
240    unsafe { ptr.add(offset) as *mut f32 }
241}
242
243/// Returns a pointer to the parameter ring buffer slot array.
244///
245/// # Safety
246/// `ptr` must point to a valid allocation large enough to contain the parameter ring.
247pub unsafe fn param_ring_ptr(ptr: *mut u8) -> *mut ParameterEvent {
248    unsafe { ptr.add(PARAM_RING_OFFSET) as *mut ParameterEvent }
249}
250
251/// Returns pointers to the parameter ring write/read atomics.
252///
253/// # Safety
254/// `ptr` must point to a valid allocation containing the parameter ring atomics.
255pub unsafe fn param_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
256    unsafe {
257        (
258            ptr.add(PARAM_WRITE_IDX_OFFSET) as *mut AtomicU32,
259            ptr.add(PARAM_READ_IDX_OFFSET) as *mut AtomicU32,
260        )
261    }
262}
263
264/// Returns a pointer to the MIDI ring buffer slot array.
265///
266/// # Safety
267/// `ptr` must point to a valid allocation large enough to contain the MIDI ring.
268pub unsafe fn midi_ring_ptr(ptr: *mut u8) -> *mut MidiEvent {
269    unsafe { ptr.add(MIDI_RING_OFFSET) as *mut MidiEvent }
270}
271
272/// Returns pointers to the MIDI ring write/read atomics.
273///
274/// # Safety
275/// `ptr` must point to a valid allocation containing the MIDI ring atomics.
276pub unsafe fn midi_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
277    unsafe {
278        (
279            ptr.add(MIDI_WRITE_IDX_OFFSET) as *mut AtomicU32,
280            ptr.add(MIDI_READ_IDX_OFFSET) as *mut AtomicU32,
281        )
282    }
283}
284
285/// Returns a pointer to the echo ring buffer slot array.
286///
287/// # Safety
288/// `ptr` must point to a valid allocation large enough to contain the echo ring.
289pub unsafe fn echo_ring_ptr(ptr: *mut u8) -> *mut ParameterEvent {
290    unsafe { ptr.add(ECHO_RING_OFFSET) as *mut ParameterEvent }
291}
292
293/// Returns pointers to the echo ring write/read atomics.
294///
295/// # Safety
296/// `ptr` must point to a valid allocation containing the echo ring atomics.
297pub unsafe fn echo_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
298    unsafe {
299        (
300            ptr.add(ECHO_WRITE_IDX_OFFSET) as *mut AtomicU32,
301            ptr.add(ECHO_READ_IDX_OFFSET) as *mut AtomicU32,
302        )
303    }
304}
305
306/// Returns a pointer to the MIDI output ring buffer slot array.
307///
308/// # Safety
309/// `ptr` must point to a valid allocation large enough to contain the MIDI out ring.
310pub unsafe fn midi_out_ring_ptr(ptr: *mut u8) -> *mut MidiEvent {
311    unsafe { ptr.add(MIDI_OUT_RING_OFFSET) as *mut MidiEvent }
312}
313
314/// Returns pointers to the MIDI output ring write/read atomics.
315///
316/// # Safety
317/// `ptr` must point to a valid allocation containing the MIDI out ring atomics.
318pub unsafe fn midi_out_indices(ptr: *mut u8) -> (*mut AtomicU32, *mut AtomicU32) {
319    unsafe {
320        (
321            ptr.add(MIDI_OUT_WRITE_IDX_OFFSET) as *mut AtomicU32,
322            ptr.add(MIDI_OUT_READ_IDX_OFFSET) as *mut AtomicU32,
323        )
324    }
325}
326
327/// Returns a reference to the transport state.
328///
329/// # Safety
330/// `ptr` must point to a valid allocation containing at least `TransportState`'s size.
331pub unsafe fn transport_ref(ptr: *mut u8) -> &'static TransportState {
332    unsafe { &*(ptr.add(TRANSPORT_OFFSET) as *mut TransportState) }
333}
334
335/// Returns a mutable reference to the transport state.
336///
337/// # Safety
338/// `ptr` must point to a valid allocation containing at least `TransportState`'s size.
339pub unsafe fn transport_mut(ptr: *mut u8) -> &'static mut TransportState {
340    unsafe { &mut *(ptr.add(TRANSPORT_OFFSET) as *mut TransportState) }
341}
342
343/// Returns a pointer to the scratch buffer region.
344///
345/// # Safety
346/// `ptr` must point to an allocation large enough to contain the scratch buffer.
347pub unsafe fn scratch_ptr(ptr: *mut u8) -> *mut u8 {
348    unsafe { ptr.add(SCRATCH_OFFSET) }
349}
350
351/// Write a plugin name to the start of the scratch buffer.
352/// The name is encoded as a little-endian u32 length followed by UTF-8 bytes.
353///
354/// # Safety
355/// `ptr` must point to a valid SHM allocation.
356pub unsafe fn write_plugin_name_to_scratch(ptr: *mut u8, name: &str) {
357    unsafe {
358        let scratch = scratch_ptr(ptr);
359        let bytes = name.as_bytes();
360        let len = bytes.len().min(SCRATCH_SIZE - 4);
361        std::ptr::write_unaligned(scratch as *mut u32, len as u32);
362        std::ptr::copy_nonoverlapping(bytes.as_ptr(), scratch.add(4), len);
363    }
364}
365
366/// Read a plugin name from the start of the scratch buffer.
367///
368/// # Safety
369/// `ptr` must point to a valid SHM allocation.
370pub unsafe fn read_plugin_name_from_scratch(ptr: *mut u8) -> Option<String> {
371    unsafe {
372        let scratch = scratch_ptr(ptr);
373        let len = std::ptr::read_unaligned(scratch as *mut u32) as usize;
374        if len == 0 || len > SCRATCH_SIZE - 4 {
375            return None;
376        }
377        let bytes = std::slice::from_raw_parts(scratch.add(4), len);
378        String::from_utf8(bytes.to_vec()).ok()
379    }
380}
381
382/// Magic value written before port counts in scratch.
383pub const PORT_COUNTS_MAGIC: u32 = 0x504F_5254; // "PORT"
384
385/// Offset within scratch where port counts are stored (after plugin name).
386const PORT_COUNTS_OFFSET: usize = 1024;
387
388/// Write audio/MIDI port counts to scratch.
389///
390/// # Safety
391/// `ptr` must point to a valid SHM allocation.
392pub unsafe fn write_port_counts_to_scratch(
393    ptr: *mut u8,
394    audio_in: u32,
395    audio_out: u32,
396    midi_in: u32,
397    midi_out: u32,
398) {
399    unsafe {
400        let dest = scratch_ptr(ptr).add(PORT_COUNTS_OFFSET);
401        std::ptr::write_unaligned(dest as *mut u32, PORT_COUNTS_MAGIC);
402        std::ptr::write_unaligned(dest.add(4) as *mut u32, audio_in);
403        std::ptr::write_unaligned(dest.add(8) as *mut u32, audio_out);
404        std::ptr::write_unaligned(dest.add(12) as *mut u32, midi_in);
405        std::ptr::write_unaligned(dest.add(16) as *mut u32, midi_out);
406    }
407}
408
409/// Read audio/MIDI port counts from scratch.
410///
411/// # Safety
412/// `ptr` must point to a valid SHM allocation.
413pub unsafe fn read_port_counts_from_scratch(ptr: *mut u8) -> Option<(u32, u32, u32, u32)> {
414    unsafe {
415        let src = scratch_ptr(ptr).add(PORT_COUNTS_OFFSET);
416        let magic = std::ptr::read_unaligned(src as *mut u32);
417        if magic != PORT_COUNTS_MAGIC {
418            return None;
419        }
420        let audio_in = std::ptr::read_unaligned(src.add(4) as *mut u32);
421        let audio_out = std::ptr::read_unaligned(src.add(8) as *mut u32);
422        let midi_in = std::ptr::read_unaligned(src.add(12) as *mut u32);
423        let midi_out = std::ptr::read_unaligned(src.add(16) as *mut u32);
424        Some((audio_in, audio_out, midi_in, midi_out))
425    }
426}
427
428/// Magic value written before file-reference string list in scratch.
429pub const FILE_REFS_MAGIC: u32 = 0x4649_4C45; // "FILE"
430
431/// Offset within scratch where file-reference string list is stored.
432const FILE_REFS_OFFSET: usize = 2048;
433
434/// Maximum total bytes available for the file-reference list.
435const FILE_REFS_MAX_SIZE: usize = SCRATCH_SIZE - FILE_REFS_OFFSET;
436
437/// A file reference returned by a plugin, paired with its plugin-side index.
438pub type FileReference = (u32, String);
439
440/// Write a list of file-reference (index, path) pairs to scratch.
441/// Format: magic (u32), count (u32), then for each entry:
442///   index (u32), length (u32) followed by UTF-8 bytes.
443///
444/// # Safety
445/// `ptr` must point to a valid SHM allocation.
446pub unsafe fn write_file_references_to_scratch(
447    ptr: *mut u8,
448    refs: &[FileReference],
449) -> Result<(), String> {
450    unsafe {
451        let mut dest = scratch_ptr(ptr).add(FILE_REFS_OFFSET);
452        let mut remaining = FILE_REFS_MAX_SIZE;
453        if remaining < 8 {
454            return Err("scratch too small for file references".to_string());
455        }
456        std::ptr::write_unaligned(dest as *mut u32, FILE_REFS_MAGIC);
457        dest = dest.add(4);
458        remaining -= 4;
459        let count = refs.len().min(u32::MAX as usize) as u32;
460        std::ptr::write_unaligned(dest as *mut u32, count);
461        dest = dest.add(4);
462        remaining -= 4;
463        for (index, path) in refs.iter().take(count as usize) {
464            if remaining < 8 {
465                return Err("scratch overflow writing file references".to_string());
466            }
467            std::ptr::write_unaligned(dest as *mut u32, *index);
468            dest = dest.add(4);
469            remaining -= 4;
470            let bytes = path.as_bytes();
471            let len = bytes
472                .len()
473                .min(u32::MAX as usize)
474                .min(remaining.saturating_sub(4));
475            if len < bytes.len() {
476                return Err("scratch overflow writing file references".to_string());
477            }
478            std::ptr::write_unaligned(dest as *mut u32, len as u32);
479            dest = dest.add(4);
480            remaining -= 4;
481            std::ptr::copy_nonoverlapping(bytes.as_ptr(), dest, len);
482            dest = dest.add(len);
483            remaining -= len;
484        }
485        Ok(())
486    }
487}
488
489/// Read a list of file-reference (index, path) pairs from scratch.
490///
491/// # Safety
492/// `ptr` must point to a valid SHM allocation.
493pub unsafe fn read_file_references_from_scratch(ptr: *mut u8) -> Option<Vec<FileReference>> {
494    unsafe {
495        let mut src = scratch_ptr(ptr).add(FILE_REFS_OFFSET);
496        let mut remaining = FILE_REFS_MAX_SIZE;
497        if remaining < 8 {
498            return None;
499        }
500        let magic = std::ptr::read_unaligned(src as *mut u32);
501        if magic != FILE_REFS_MAGIC {
502            return None;
503        }
504        src = src.add(4);
505        remaining -= 4;
506        let count = std::ptr::read_unaligned(src as *mut u32) as usize;
507        src = src.add(4);
508        remaining -= 4;
509        let mut refs = Vec::with_capacity(count);
510        for _ in 0..count {
511            if remaining < 8 {
512                return None;
513            }
514            let index = std::ptr::read_unaligned(src as *mut u32);
515            src = src.add(4);
516            remaining -= 4;
517            let len = std::ptr::read_unaligned(src as *mut u32) as usize;
518            src = src.add(4);
519            remaining -= 4;
520            if len > remaining {
521                return None;
522            }
523            let bytes = std::slice::from_raw_parts(src, len);
524            let path = String::from_utf8(bytes.to_vec()).ok()?;
525            refs.push((index, path));
526            src = src.add(len);
527            remaining -= len;
528        }
529        Some(refs)
530    }
531}
532
533/// Write a resource-directory / base-directory path to scratch.
534/// Format: magic (u32), length (u32), UTF-8 bytes.
535///
536/// # Safety
537/// `ptr` must point to a valid SHM allocation.
538pub unsafe fn write_resource_directory_to_scratch(ptr: *mut u8, path: &str) -> Result<(), String> {
539    unsafe {
540        let scratch = scratch_ptr(ptr);
541        let bytes = path.as_bytes();
542        let len = bytes.len().min(SCRATCH_SIZE - 8);
543        if len < bytes.len() {
544            return Err("resource directory path too long".to_string());
545        }
546        std::ptr::write_unaligned(scratch as *mut u32, FILE_REFS_MAGIC);
547        std::ptr::write_unaligned(scratch.add(4) as *mut u32, len as u32);
548        std::ptr::copy_nonoverlapping(bytes.as_ptr(), scratch.add(8), len);
549        Ok(())
550    }
551}
552
553/// Read a resource-directory / base-directory path from scratch.
554///
555/// # Safety
556/// `ptr` must point to a valid SHM allocation.
557pub unsafe fn read_resource_directory_from_scratch(ptr: *mut u8) -> Option<String> {
558    unsafe {
559        let scratch = scratch_ptr(ptr);
560        let magic = std::ptr::read_unaligned(scratch as *mut u32);
561        if magic != FILE_REFS_MAGIC {
562            return None;
563        }
564        let len = std::ptr::read_unaligned(scratch.add(4) as *mut u32) as usize;
565        if len == 0 || len > SCRATCH_SIZE - 8 {
566            return None;
567        }
568        let bytes = std::slice::from_raw_parts(scratch.add(8), len);
569        String::from_utf8(bytes.to_vec()).ok()
570    }
571}
572
573/// Magic value for a single file-reference update in scratch.
574pub const FILE_REF_UPDATE_MAGIC: u32 = 0x5550_4441; // "UPDA"
575
576/// Write a file-reference update (index + new path) to scratch.
577/// Format: magic (u32), index (u32), length (u32), UTF-8 bytes.
578///
579/// # Safety
580/// `ptr` must point to a valid SHM allocation.
581pub unsafe fn write_file_reference_update_to_scratch(
582    ptr: *mut u8,
583    index: u32,
584    path: &str,
585) -> Result<(), String> {
586    unsafe {
587        let scratch = scratch_ptr(ptr);
588        let bytes = path.as_bytes();
589        let len = bytes.len().min(SCRATCH_SIZE - 12);
590        if len < bytes.len() {
591            return Err("file-reference update path too long".to_string());
592        }
593        std::ptr::write_unaligned(scratch as *mut u32, FILE_REF_UPDATE_MAGIC);
594        std::ptr::write_unaligned(scratch.add(4) as *mut u32, index);
595        std::ptr::write_unaligned(scratch.add(8) as *mut u32, len as u32);
596        std::ptr::copy_nonoverlapping(bytes.as_ptr(), scratch.add(12), len);
597        Ok(())
598    }
599}
600
601/// Read a file-reference update (index + new path) from scratch.
602///
603/// # Safety
604/// `ptr` must point to a valid SHM allocation.
605pub unsafe fn read_file_reference_update_from_scratch(ptr: *mut u8) -> Option<(u32, String)> {
606    unsafe {
607        let scratch = scratch_ptr(ptr);
608        let magic = std::ptr::read_unaligned(scratch as *mut u32);
609        if magic != FILE_REF_UPDATE_MAGIC {
610            return None;
611        }
612        let index = std::ptr::read_unaligned(scratch.add(4) as *mut u32);
613        let len = std::ptr::read_unaligned(scratch.add(8) as *mut u32) as usize;
614        if len == 0 || len > SCRATCH_SIZE - 12 {
615            return None;
616        }
617        let bytes = std::slice::from_raw_parts(scratch.add(12), len);
618        let path = String::from_utf8(bytes.to_vec()).ok()?;
619        Some((index, path))
620    }
621}
622
623// --- Static assertions for sizes ---
624
625const _: () = assert!(std::mem::size_of::<ShmHeader>() == 256);
626const _: () = assert!(std::mem::align_of::<ShmHeader>() == 256);
627const _: () = assert!(std::mem::size_of::<ParameterEvent>() == 16);
628const _: () = assert!(std::mem::align_of::<ParameterEvent>() == 16);
629const _: () = assert!(std::mem::size_of::<MidiEvent>() == 16);
630const _: () = assert!(std::mem::align_of::<MidiEvent>() == 16);
631const _: () = assert!(std::mem::size_of::<TransportState>() == 256);
632const _: () = assert!(std::mem::align_of::<TransportState>() == 256);
633const _: () = assert!(LAYOUT_SIZE <= SHM_SIZE);
634
635/// Wait (spin + yield) until `ready` becomes non-zero or timeout elapses.
636pub fn wait_for_ready(header: &ShmHeader, timeout: std::time::Duration) -> bool {
637    let start = std::time::Instant::now();
638    while header.ready.load(Ordering::Acquire) == 0 {
639        if start.elapsed() >= timeout {
640            return false;
641        }
642        std::thread::yield_now();
643    }
644    true
645}