Skip to main content

beamer_core/
sysex_pool.rs

1//! Pre-allocated SysEx output buffer pool for real-time safety.
2//!
3//! This module provides `SysExOutputPool`, which pre-allocates buffer slots
4//! to avoid heap allocation during audio processing.
5
6/// Pre-allocated pool for SysEx output messages.
7///
8/// Avoids heap allocation during audio processing by pre-allocating
9/// a fixed number of buffer slots at initialization time.
10pub struct SysExOutputPool {
11    /// Pre-allocated buffer slots for SysEx data
12    buffers: Vec<Vec<u8>>,
13    /// Length of valid data in each slot
14    lengths: Vec<usize>,
15    /// Maximum number of slots
16    max_slots: usize,
17    /// Maximum buffer size per slot
18    max_buffer_size: usize,
19    /// Next available slot index
20    next_slot: usize,
21    /// Set to true when an allocation fails due to pool exhaustion
22    overflowed: bool,
23    /// Heap-backed fallback buffer for overflow (only when feature enabled).
24    #[cfg(feature = "sysex-heap-fallback")]
25    fallback: Vec<Vec<u8>>,
26}
27
28impl SysExOutputPool {
29    /// Default number of SysEx slots per process block.
30    pub const DEFAULT_SLOTS: usize = 16;
31    /// Default maximum size per SysEx message.
32    pub const DEFAULT_BUFFER_SIZE: usize = 512;
33
34    /// Create a new pool with default capacity.
35    pub fn new() -> Self {
36        Self::with_capacity(Self::DEFAULT_SLOTS, Self::DEFAULT_BUFFER_SIZE)
37    }
38
39    /// Create a new pool with the specified capacity.
40    ///
41    /// Pre-allocates all buffers to avoid heap allocation during process().
42    pub fn with_capacity(slots: usize, buffer_size: usize) -> Self {
43        let mut buffers = Vec::with_capacity(slots);
44        for _ in 0..slots {
45            buffers.push(vec![0u8; buffer_size]);
46        }
47        let lengths = vec![0usize; slots];
48
49        Self {
50            buffers,
51            lengths,
52            max_slots: slots,
53            max_buffer_size: buffer_size,
54            next_slot: 0,
55            overflowed: false,
56            #[cfg(feature = "sysex-heap-fallback")]
57            fallback: Vec::new(),
58        }
59    }
60
61    /// Clear the pool for reuse. O(1) operation.
62    ///
63    /// Note: This does NOT clear the fallback buffer, which is drained separately
64    /// at the start of the next process block.
65    #[inline]
66    pub fn clear(&mut self) {
67        self.next_slot = 0;
68        self.overflowed = false;
69    }
70
71    /// Allocate a slot and copy SysEx data into it.
72    ///
73    /// Returns `Some((pointer, length))` on success, `None` if pool exhausted.
74    /// The pointer is stable until `clear()` is called.
75    ///
76    /// Sets the overflow flag when the pool is exhausted.
77    /// With `sysex-heap-fallback` feature: overflow messages are stored in a
78    /// heap-backed fallback buffer instead of being dropped.
79    pub fn allocate(&mut self, data: &[u8]) -> Option<(*const u8, usize)> {
80        if self.next_slot >= self.max_slots {
81            self.overflowed = true;
82
83            #[cfg(feature = "sysex-heap-fallback")]
84            {
85                let copy_len = data.len().min(self.max_buffer_size);
86                self.fallback.push(data[..copy_len].to_vec());
87            }
88
89            return None;
90        }
91
92        let slot = self.next_slot;
93        self.next_slot += 1;
94
95        let copy_len = data.len().min(self.max_buffer_size);
96        self.buffers[slot][..copy_len].copy_from_slice(&data[..copy_len]);
97        self.lengths[slot] = copy_len;
98
99        Some((self.buffers[slot].as_ptr(), copy_len))
100    }
101
102    /// Allocate and return a slice reference instead of raw pointer.
103    ///
104    /// Safer API for contexts that don't need raw pointers.
105    pub fn allocate_slice(&mut self, data: &[u8]) -> Option<&[u8]> {
106        if self.next_slot >= self.max_slots {
107            self.overflowed = true;
108
109            #[cfg(feature = "sysex-heap-fallback")]
110            {
111                let copy_len = data.len().min(self.max_buffer_size);
112                self.fallback.push(data[..copy_len].to_vec());
113            }
114
115            return None;
116        }
117
118        let slot = self.next_slot;
119        self.next_slot += 1;
120
121        let copy_len = data.len().min(self.max_buffer_size);
122        self.buffers[slot][..copy_len].copy_from_slice(&data[..copy_len]);
123        self.lengths[slot] = copy_len;
124
125        Some(&self.buffers[slot][..copy_len])
126    }
127
128    /// Check if the pool overflowed during this block.
129    #[inline]
130    pub fn has_overflowed(&self) -> bool {
131        self.overflowed
132    }
133
134    /// Get the pool's slot capacity.
135    #[inline]
136    pub fn capacity(&self) -> usize {
137        self.max_slots
138    }
139
140    /// Get number of slots currently used.
141    #[inline]
142    pub fn used(&self) -> usize {
143        self.next_slot
144    }
145
146    /// Check if fallback buffer has pending messages (feature-gated).
147    #[cfg(feature = "sysex-heap-fallback")]
148    #[inline]
149    pub fn has_fallback(&self) -> bool {
150        !self.fallback.is_empty()
151    }
152
153    /// Take ownership of fallback messages (feature-gated).
154    ///
155    /// These messages should be emitted at the start of the current process block.
156    #[cfg(feature = "sysex-heap-fallback")]
157    #[inline]
158    pub fn take_fallback(&mut self) -> Vec<Vec<u8>> {
159        std::mem::take(&mut self.fallback)
160    }
161}
162
163impl Default for SysExOutputPool {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169#[cfg(test)]
170#[allow(clippy::undocumented_unsafe_blocks)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn test_new_pool() {
176        let pool = SysExOutputPool::new();
177        assert_eq!(pool.capacity(), SysExOutputPool::DEFAULT_SLOTS);
178        assert_eq!(pool.used(), 0);
179        assert!(!pool.has_overflowed());
180    }
181
182    #[test]
183    fn test_allocate() {
184        let mut pool = SysExOutputPool::with_capacity(2, 64);
185        let data = [0xF0, 0x41, 0x10, 0xF7];
186
187        let result = pool.allocate(&data);
188        assert!(result.is_some());
189        assert_eq!(pool.used(), 1);
190
191        let (ptr, len) = result.unwrap();
192        assert_eq!(len, 4);
193        let slice = unsafe { std::slice::from_raw_parts(ptr, len) };
194        assert_eq!(slice, &data);
195    }
196
197    #[test]
198    fn test_allocate_slice() {
199        let mut pool = SysExOutputPool::with_capacity(2, 64);
200        let data = [0xF0, 0x41, 0x10, 0xF7];
201
202        let result = pool.allocate_slice(&data);
203        assert!(result.is_some());
204        assert_eq!(result.unwrap(), &data);
205        assert_eq!(pool.used(), 1);
206    }
207
208    #[test]
209    fn test_overflow() {
210        let mut pool = SysExOutputPool::with_capacity(1, 64);
211        let data = [0xF0, 0xF7];
212
213        assert!(pool.allocate(&data).is_some());
214        assert!(!pool.has_overflowed());
215
216        assert!(pool.allocate(&data).is_none());
217        assert!(pool.has_overflowed());
218    }
219
220    #[test]
221    fn test_clear() {
222        let mut pool = SysExOutputPool::with_capacity(1, 64);
223        let data = [0xF0, 0xF7];
224
225        pool.allocate(&data);
226        pool.allocate(&data); // Overflow
227        assert!(pool.has_overflowed());
228        assert_eq!(pool.used(), 1);
229
230        pool.clear();
231        assert!(!pool.has_overflowed());
232        assert_eq!(pool.used(), 0);
233    }
234
235    #[test]
236    fn test_truncation() {
237        let mut pool = SysExOutputPool::with_capacity(1, 4);
238        let data = [0xF0, 0x41, 0x10, 0x42, 0x00, 0xF7]; // 6 bytes
239
240        let result = pool.allocate_slice(&data);
241        assert!(result.is_some());
242        assert_eq!(result.unwrap().len(), 4); // Truncated to buffer size
243    }
244}