Skip to main content

maolan_plugin_protocol/
protocol.rs

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