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