Skip to main content

beamer_core/
buffer_storage.rs

1//! Pre-allocated buffer storage for real-time safe audio processing.
2//!
3//! This module provides [`ProcessBufferStorage`], which pre-allocates capacity
4//! for channel pointers during plugin setup. The storage is then reused for each
5//! render call without allocations.
6//!
7//! # Pattern
8//!
9//! This storage follows a consistent pattern across all plugin formats:
10//! 1. Allocate storage once during setup (non-real-time)
11//! 2. Clear storage at start of each render (O(1), no deallocation)
12//! 3. Push pointers from host buffers (never exceeds capacity)
13//! 4. Build slices from pointers
14//!
15//! # Memory Optimization Strategy
16//!
17//! The allocation strategy is **config-based, not worst-case**:
18//!
19//! - **Channel counts**: Allocates exact number of channels from bus config, not MAX_CHANNELS
20//! - **Bus counts**: Allocates only for buses that exist, not MAX_BUSES
21//! - **Lazy aux allocation**: No heap allocation for aux buses if plugin doesn't use them
22//! - **Asymmetric support**: Mono input can have stereo output (allocates 1 + 2, not 2 + 2)
23//!
24//! Examples:
25//! - Mono plugin (1in/1out): Allocates 2 pointers (16 bytes on 64-bit)
26//! - Stereo plugin (2in/2out): Allocates 4 pointers (32 bytes on 64-bit)
27//! - Stereo with sidechain (2+2in/2out): Allocates 6 pointers (48 bytes on 64-bit)
28//! - Worst-case (32ch x 16 buses): Would be MAX_CHANNELS * MAX_BUSES = 512 pointers (4KB)
29//!
30//! This means simple plugins use **32x less memory** than worst-case allocation.
31//!
32//! # Real-Time Safety
33//!
34//! - `clear()` is O(1) - only sets Vec lengths to 0
35//! - `push()` never allocates - capacity is pre-reserved
36//! - No heap operations during audio processing
37//! - All allocations happen in `allocate_from_config()` (non-real-time)
38
39use crate::bus_config::CachedBusConfig;
40use crate::sample::Sample;
41use std::slice;
42
43/// Pre-allocated storage for audio processing channel pointers.
44///
45/// Stores channel pointers collected from host audio buffers during render.
46/// The Vecs have pre-allocated capacity matching the **actual** bus configuration,
47/// ensuring no allocations occur during audio callbacks while minimizing memory usage.
48///
49/// # Memory Layout
50///
51/// The storage is optimized based on the actual plugin configuration:
52/// - `main_inputs`: Capacity = actual input channel count (e.g., 1 for mono, 2 for stereo)
53/// - `main_outputs`: Capacity = actual output channel count (e.g., 1 for mono, 2 for stereo)
54/// - `aux_inputs`: Only allocated if plugin declares aux input buses
55/// - `aux_outputs`: Only allocated if plugin declares aux output buses
56///
57/// This means a simple stereo plugin uses only 32 bytes (4 pointers x 8 bytes),
58/// not the worst-case 4KB (MAX_CHANNELS x MAX_BUSES x pointer size).
59///
60/// # Type Parameter
61///
62/// `S` is the sample type (`f32` or `f64`).
63#[derive(Clone)]
64pub struct ProcessBufferStorage<S: Sample> {
65    /// Main input channel pointers (capacity = actual channel count).
66    /// Format wrappers may access this directly for performance-critical inline collection.
67    pub main_inputs: Vec<*const S>,
68    /// Main output channel pointers (capacity = actual channel count).
69    /// Format wrappers may access this directly for performance-critical inline collection.
70    pub main_outputs: Vec<*mut S>,
71    /// Auxiliary input buses (only allocated if plugin uses them).
72    /// Format wrappers may access this directly for performance-critical inline collection.
73    pub aux_inputs: Vec<Vec<*const S>>,
74    /// Auxiliary output buses (only allocated if plugin uses them).
75    /// Format wrappers may access this directly for performance-critical inline collection.
76    pub aux_outputs: Vec<Vec<*mut S>>,
77    /// Internal output buffers for instruments (when host provides null pointers).
78    /// Only allocated for plugins with no input buses (instruments/generators).
79    /// When a host provides null output pointers, the format wrapper can use these
80    /// buffers and update the host's buffer list to point to them.
81    pub internal_output_buffers: Option<Vec<Vec<S>>>,
82    /// Max frames for internal buffers (set during allocation).
83    /// Used to validate that num_samples doesn't exceed buffer capacity.
84    pub max_frames: usize,
85}
86
87impl<S: Sample> ProcessBufferStorage<S> {
88    /// Create empty storage (no capacity reserved).
89    pub fn new() -> Self {
90        Self {
91            main_inputs: Vec::new(),
92            main_outputs: Vec::new(),
93            aux_inputs: Vec::new(),
94            aux_outputs: Vec::new(),
95            internal_output_buffers: None,
96            max_frames: 0,
97        }
98    }
99
100    /// Create storage from cached bus configuration (recommended).
101    ///
102    /// This is the preferred way to allocate storage as it automatically
103    /// extracts the correct channel counts from the bus configuration.
104    /// Should be called during plugin setup (non-real-time).
105    ///
106    /// # Memory Optimization
107    ///
108    /// This method implements smart allocation strategies:
109    /// - Allocates only for channels actually present in the config
110    /// - No pre-allocation for aux buses if plugin doesn't use them
111    /// - Uses actual channel counts, not MAX_CHANNELS worst-case
112    /// - Zero heap allocation for simple mono/stereo plugins without aux buses
113    ///
114    /// # Arguments
115    ///
116    /// * `bus_config` - Cached bus configuration
117    /// * `max_frames` - Maximum frames per render call (for internal buffer allocation)
118    ///
119    /// # Example
120    ///
121    /// ```ignore
122    /// let config = extract_bus_config();
123    /// config.validate()?;
124    /// let storage = ProcessBufferStorage::allocate_from_config(&config, 4096);
125    /// ```
126    pub fn allocate_from_config(bus_config: &CachedBusConfig, max_frames: usize) -> Self {
127        // Extract main bus channel counts (bus 0)
128        let main_in_channels = bus_config
129            .input_bus_info(0)
130            .map(|b| b.channel_count)
131            .unwrap_or(0);
132        let main_out_channels = bus_config
133            .output_bus_info(0)
134            .map(|b| b.channel_count)
135            .unwrap_or(0);
136
137        // Count auxiliary buses (all buses except main bus 0)
138        let aux_in_buses = bus_config.input_bus_count.saturating_sub(1);
139        let aux_out_buses = bus_config.output_bus_count.saturating_sub(1);
140
141        // Optimization: Only allocate aux bus storage if actually needed.
142        // For simple plugins (mono/stereo with no aux), this avoids any
143        // heap allocation for the outer Vec containers.
144        let aux_inputs = if aux_in_buses > 0 {
145            let mut vec = Vec::with_capacity(aux_in_buses);
146            for i in 1..=aux_in_buses {
147                let channels = bus_config
148                    .input_bus_info(i)
149                    .map(|b| b.channel_count)
150                    .unwrap_or(0);
151                vec.push(Vec::with_capacity(channels));
152            }
153            vec
154        } else {
155            Vec::new() // Zero-capacity allocation - no heap memory
156        };
157
158        let aux_outputs = if aux_out_buses > 0 {
159            let mut vec = Vec::with_capacity(aux_out_buses);
160            for i in 1..=aux_out_buses {
161                let channels = bus_config
162                    .output_bus_info(i)
163                    .map(|b| b.channel_count)
164                    .unwrap_or(0);
165                vec.push(Vec::with_capacity(channels));
166            }
167            vec
168        } else {
169            Vec::new() // Zero-capacity allocation - no heap memory
170        };
171
172        // For instruments (no input buses), allocate internal output buffers.
173        // Some hosts (Logic Pro, Reaper) may provide null output buffer pointers,
174        // expecting the plugin to use its own buffers.
175        let internal_output_buffers = if main_in_channels == 0 && main_out_channels > 0 {
176            let mut buffers = Vec::with_capacity(main_out_channels);
177            for _ in 0..main_out_channels {
178                buffers.push(vec![S::ZERO; max_frames]);
179            }
180            Some(buffers)
181        } else {
182            None
183        };
184
185        Self {
186            main_inputs: Vec::with_capacity(main_in_channels),
187            main_outputs: Vec::with_capacity(main_out_channels),
188            aux_inputs,
189            aux_outputs,
190            internal_output_buffers,
191            max_frames,
192        }
193    }
194
195    /// Create new storage with pre-allocated capacity (manual).
196    ///
197    /// This is a lower-level method for manual capacity specification.
198    /// Prefer `allocate_from_config()` when possible as it's less error-prone.
199    ///
200    /// # Arguments
201    ///
202    /// * `main_in_channels` - Number of main input channels
203    /// * `main_out_channels` - Number of main output channels
204    /// * `aux_in_buses` - Number of auxiliary input buses
205    /// * `aux_out_buses` - Number of auxiliary output buses
206    /// * `aux_channels` - Channels per aux bus (assumes uniform)
207    pub fn allocate(
208        main_in_channels: usize,
209        main_out_channels: usize,
210        aux_in_buses: usize,
211        aux_out_buses: usize,
212        aux_channels: usize,
213    ) -> Self {
214        let mut aux_inputs = Vec::with_capacity(aux_in_buses);
215        for _ in 0..aux_in_buses {
216            aux_inputs.push(Vec::with_capacity(aux_channels));
217        }
218
219        let mut aux_outputs = Vec::with_capacity(aux_out_buses);
220        for _ in 0..aux_out_buses {
221            aux_outputs.push(Vec::with_capacity(aux_channels));
222        }
223
224        Self {
225            main_inputs: Vec::with_capacity(main_in_channels),
226            main_outputs: Vec::with_capacity(main_out_channels),
227            aux_inputs,
228            aux_outputs,
229            internal_output_buffers: None, // Manual allocation doesn't set up internal buffers
230            max_frames: 0,
231        }
232    }
233
234    /// Clear all pointer storage without deallocating.
235    ///
236    /// This is O(1) - it only sets Vec lengths to 0 while preserving capacity.
237    /// Call this at the start of each render call.
238    #[inline]
239    pub fn clear(&mut self) {
240        self.main_inputs.clear();
241        self.main_outputs.clear();
242        for bus in &mut self.aux_inputs {
243            bus.clear();
244        }
245        for bus in &mut self.aux_outputs {
246            bus.clear();
247        }
248    }
249
250    /// Get the number of input channels collected.
251    #[inline]
252    pub fn input_channel_count(&self) -> usize {
253        self.main_inputs.len()
254    }
255
256    /// Get the number of output channels collected.
257    #[inline]
258    pub fn output_channel_count(&self) -> usize {
259        self.main_outputs.len()
260    }
261
262    /// Get the number of auxiliary input buses.
263    #[inline]
264    pub fn aux_input_bus_count(&self) -> usize {
265        self.aux_inputs.len()
266    }
267
268    /// Get the number of auxiliary output buses.
269    #[inline]
270    pub fn aux_output_bus_count(&self) -> usize {
271        self.aux_outputs.len()
272    }
273
274    /// Get the maximum frames for internal buffers.
275    #[inline]
276    pub fn max_frames(&self) -> usize {
277        self.max_frames
278    }
279
280    /// Check if internal output buffers are available.
281    #[inline]
282    pub fn has_internal_output_buffers(&self) -> bool {
283        self.internal_output_buffers.is_some()
284    }
285
286    /// Push a main input pointer.
287    ///
288    /// # Safety
289    ///
290    /// The pointer must be valid for the duration of the current render call.
291    #[inline]
292    pub unsafe fn push_main_input(&mut self, ptr: *const S) {
293        self.main_inputs.push(ptr);
294    }
295
296    /// Push a main output pointer.
297    ///
298    /// # Safety
299    ///
300    /// The pointer must be valid for the duration of the current render call.
301    #[inline]
302    pub unsafe fn push_main_output(&mut self, ptr: *mut S) {
303        self.main_outputs.push(ptr);
304    }
305
306    /// Push an auxiliary input pointer for a specific bus.
307    ///
308    /// # Safety
309    ///
310    /// The pointer must be valid for the duration of the current render call.
311    #[inline]
312    pub unsafe fn push_aux_input(&mut self, bus_index: usize, ptr: *const S) {
313        if bus_index < self.aux_inputs.len() {
314            self.aux_inputs[bus_index].push(ptr);
315        }
316    }
317
318    /// Push an auxiliary output pointer for a specific bus.
319    ///
320    /// # Safety
321    ///
322    /// The pointer must be valid for the duration of the current render call.
323    #[inline]
324    pub unsafe fn push_aux_output(&mut self, bus_index: usize, ptr: *mut S) {
325        if bus_index < self.aux_outputs.len() {
326            self.aux_outputs[bus_index].push(ptr);
327        }
328    }
329
330    /// Get the main input capacity.
331    #[inline]
332    pub fn main_input_capacity(&self) -> usize {
333        self.main_inputs.capacity()
334    }
335
336    /// Get the main output capacity.
337    #[inline]
338    pub fn main_output_capacity(&self) -> usize {
339        self.main_outputs.capacity()
340    }
341
342    /// Get an auxiliary input bus capacity.
343    #[inline]
344    pub fn aux_input_capacity(&self, bus_index: usize) -> usize {
345        self.aux_inputs.get(bus_index).map(|v| v.capacity()).unwrap_or(0)
346    }
347
348    /// Get an auxiliary output bus capacity.
349    #[inline]
350    pub fn aux_output_capacity(&self, bus_index: usize) -> usize {
351        self.aux_outputs.get(bus_index).map(|v| v.capacity()).unwrap_or(0)
352    }
353
354    /// Build input slices from collected pointers.
355    ///
356    /// # Safety
357    ///
358    /// - Pointers must still be valid (within same render call)
359    /// - num_samples must match what was used in collection
360    #[inline]
361    pub unsafe fn input_slices(&self, num_samples: usize) -> Vec<&[S]> {
362        self.main_inputs
363            .iter()
364            .map(|&ptr| {
365                // SAFETY: Caller guarantees pointers are valid for `num_samples` elements
366                // and remain valid for the lifetime of the returned slices (within render call).
367                unsafe { slice::from_raw_parts(ptr, num_samples) }
368            })
369            .collect()
370    }
371
372    /// Build output slices from collected pointers.
373    ///
374    /// # Safety
375    ///
376    /// - Pointers must still be valid (within same render call)
377    /// - num_samples must match what was used in collection
378    ///
379    /// # Clippy Allow: mut_from_ref
380    ///
381    /// Returns `&mut [S]` from `&self` because we're converting raw pointers stored in the struct,
382    /// not mutating `self`. This is a common and safe FFI pattern where:
383    /// - Raw pointers (`*mut S`) are stored during collection
384    /// - Those pointers are then converted back to safe references
385    /// - The mutable references are to the external buffer memory, not to `self`
386    /// - Host guarantees single-threaded render access, preventing aliasing
387    #[inline]
388    #[allow(clippy::mut_from_ref)]
389    pub unsafe fn output_slices(&self, num_samples: usize) -> Vec<&mut [S]> {
390        self.main_outputs
391            .iter()
392            .map(|&ptr| {
393                // SAFETY: Caller guarantees pointers are valid for `num_samples` elements,
394                // remain valid for the returned slice lifetime, and do not alias.
395                // Host guarantees single-threaded render access.
396                unsafe { slice::from_raw_parts_mut(ptr, num_samples) }
397            })
398            .collect()
399    }
400
401    /// Build auxiliary input slices from collected pointers.
402    ///
403    /// Returns a Vec of buses, where each bus is a Vec of channel slices.
404    ///
405    /// # Safety
406    ///
407    /// - Pointers must still be valid (within same render call)
408    /// - num_samples must match what was used in collection
409    #[inline]
410    pub unsafe fn aux_input_slices(&self, num_samples: usize) -> Vec<Vec<&[S]>> {
411        self.aux_inputs
412            .iter()
413            .map(|bus| {
414                bus.iter()
415                    .map(|&ptr| {
416                        // SAFETY: Caller guarantees pointers are valid for `num_samples` elements
417                        // and remain valid for the lifetime of the returned slices (within render call).
418                        unsafe { slice::from_raw_parts(ptr, num_samples) }
419                    })
420                    .collect()
421            })
422            .collect()
423    }
424
425    /// Build auxiliary output slices from collected pointers.
426    ///
427    /// Returns a Vec of buses, where each bus is a Vec of channel slices.
428    ///
429    /// # Safety
430    ///
431    /// - Pointers must still be valid (within same render call)
432    /// - num_samples must match what was used in collection
433    ///
434    /// # Clippy Allow: mut_from_ref
435    ///
436    /// Same justification as `output_slices` - converts raw pointers to mutable references.
437    #[inline]
438    #[allow(clippy::mut_from_ref)]
439    pub unsafe fn aux_output_slices(&self, num_samples: usize) -> Vec<Vec<&mut [S]>> {
440        self.aux_outputs
441            .iter()
442            .map(|bus| {
443                bus.iter()
444                    .map(|&ptr| {
445                        // SAFETY: Caller guarantees pointers are valid for `num_samples` elements,
446                        // remain valid for the returned slice lifetime, and do not alias.
447                        // Host guarantees single-threaded render access.
448                        unsafe { slice::from_raw_parts_mut(ptr, num_samples) }
449                    })
450                    .collect()
451            })
452            .collect()
453    }
454
455    /// Get a mutable pointer to an internal output buffer channel.
456    ///
457    /// Returns `None` if internal buffers are not allocated or channel is out of range.
458    ///
459    /// # Arguments
460    ///
461    /// * `channel` - The channel index
462    #[inline]
463    pub fn internal_output_buffer_mut(&mut self, channel: usize) -> Option<&mut Vec<S>> {
464        self.internal_output_buffers
465            .as_mut()
466            .and_then(|buffers| buffers.get_mut(channel))
467    }
468
469    /// Get a reference to an internal output buffer channel.
470    ///
471    /// Returns `None` if internal buffers are not allocated or channel is out of range.
472    #[inline]
473    pub fn internal_output_buffer(&self, channel: usize) -> Option<&Vec<S>> {
474        self.internal_output_buffers
475            .as_ref()
476            .and_then(|buffers| buffers.get(channel))
477    }
478
479    /// Get the number of internal output buffer channels.
480    #[inline]
481    pub fn internal_output_buffer_count(&self) -> usize {
482        self.internal_output_buffers
483            .as_ref()
484            .map(|b| b.len())
485            .unwrap_or(0)
486    }
487}
488
489impl<S: Sample> Default for ProcessBufferStorage<S> {
490    fn default() -> Self {
491        Self::new()
492    }
493}
494
495// SAFETY: The raw pointers are only used within a single render call
496// where the host guarantees single-threaded access.
497unsafe impl<S: Sample> Send for ProcessBufferStorage<S> {}
498// SAFETY: The raw pointers are only used within a single render call
499// where the host guarantees single-threaded access.
500unsafe impl<S: Sample> Sync for ProcessBufferStorage<S> {}
501
502#[cfg(test)]
503#[allow(clippy::undocumented_unsafe_blocks)]
504mod tests {
505    use super::*;
506    use crate::bus_config::CachedBusInfo;
507    use crate::plugin::BusType;
508    use crate::types::MAX_CHANNELS;
509
510    #[test]
511    fn test_validate_bus_limits_success() {
512        let config = CachedBusConfig::new(
513            vec![CachedBusInfo::new(2, BusType::Main)],
514            vec![CachedBusInfo::new(2, BusType::Main)],
515        );
516
517        assert!(config.validate().is_ok());
518    }
519
520    #[test]
521    fn test_validate_bus_limits_too_many_channels() {
522        let config = CachedBusConfig::new(
523            vec![CachedBusInfo::new(MAX_CHANNELS + 1, BusType::Main)],
524            vec![CachedBusInfo::new(2, BusType::Main)],
525        );
526
527        assert!(config.validate().is_err());
528    }
529
530    #[test]
531    fn test_allocate_from_config_stereo() {
532        let config = CachedBusConfig::default(); // 2in/2out
533        let storage: ProcessBufferStorage<f32> =
534            ProcessBufferStorage::allocate_from_config(&config, 4096);
535
536        assert_eq!(storage.main_inputs.capacity(), 2);
537        assert_eq!(storage.main_outputs.capacity(), 2);
538        assert_eq!(storage.aux_inputs.len(), 0);
539        assert_eq!(storage.aux_outputs.len(), 0);
540    }
541
542    #[test]
543    fn test_allocate_from_config_with_aux() {
544        let config = CachedBusConfig::new(
545            vec![
546                CachedBusInfo::new(2, BusType::Main),
547                CachedBusInfo::new(2, BusType::Aux),
548            ],
549            vec![CachedBusInfo::new(6, BusType::Main)],
550        );
551
552        let storage: ProcessBufferStorage<f32> =
553            ProcessBufferStorage::allocate_from_config(&config, 4096);
554
555        assert_eq!(storage.main_inputs.capacity(), 2);
556        assert_eq!(storage.main_outputs.capacity(), 6);
557        assert_eq!(storage.aux_inputs.len(), 1);
558        assert_eq!(storage.aux_inputs[0].capacity(), 2);
559        assert_eq!(storage.aux_outputs.len(), 0);
560    }
561
562    #[test]
563    fn test_allocate_and_clear() {
564        let mut storage: ProcessBufferStorage<f32> = ProcessBufferStorage::allocate(2, 2, 1, 0, 2);
565
566        // Verify capacities
567        assert_eq!(storage.main_inputs.capacity(), 2);
568        assert_eq!(storage.main_outputs.capacity(), 2);
569        assert_eq!(storage.aux_inputs.len(), 1);
570        assert_eq!(storage.aux_inputs[0].capacity(), 2);
571
572        // Simulate pushing pointers
573        let dummy: f32 = 0.0;
574        unsafe {
575            storage.push_main_input(&dummy as *const f32);
576            storage.push_main_input(&dummy as *const f32);
577        }
578
579        assert_eq!(storage.main_inputs.len(), 2);
580
581        // Clear should reset length but not capacity
582        storage.clear();
583        assert_eq!(storage.main_inputs.len(), 0);
584        assert_eq!(storage.main_inputs.capacity(), 2);
585    }
586
587    #[test]
588    fn test_allocate_from_config_mono() {
589        // Mono plugin: 1 in, 1 out, no aux buses
590        let config = CachedBusConfig::new(
591            vec![CachedBusInfo::new(1, BusType::Main)],
592            vec![CachedBusInfo::new(1, BusType::Main)],
593        );
594
595        let storage: ProcessBufferStorage<f32> =
596            ProcessBufferStorage::allocate_from_config(&config, 4096);
597
598        assert_eq!(storage.main_inputs.capacity(), 1);
599        assert_eq!(storage.main_outputs.capacity(), 1);
600        assert_eq!(storage.aux_inputs.len(), 0);
601        assert_eq!(storage.aux_outputs.len(), 0);
602    }
603
604    #[test]
605    fn test_instrument_internal_buffers_allocated() {
606        // Instruments have 0 inputs and >0 outputs - should allocate internal buffers
607        let config = CachedBusConfig::new(
608            vec![], // No input buses (instrument)
609            vec![CachedBusInfo::new(2, BusType::Main)],
610        );
611
612        let storage: ProcessBufferStorage<f32> =
613            ProcessBufferStorage::allocate_from_config(&config, 512);
614
615        // Internal buffers should be allocated for instruments
616        assert!(storage.internal_output_buffers.is_some());
617        let internal = storage.internal_output_buffers.as_ref().unwrap();
618        assert_eq!(internal.len(), 2); // Stereo
619        assert_eq!(internal[0].len(), 512); // max_frames
620        assert_eq!(internal[1].len(), 512);
621        assert_eq!(storage.max_frames, 512);
622    }
623
624    #[test]
625    fn test_effect_no_internal_buffers() {
626        // Effects have inputs - should NOT allocate internal buffers
627        let config = CachedBusConfig::default(); // 2in/2out
628
629        let storage: ProcessBufferStorage<f32> =
630            ProcessBufferStorage::allocate_from_config(&config, 512);
631
632        // Effects don't need internal buffers
633        assert!(storage.internal_output_buffers.is_none());
634    }
635
636    #[test]
637    fn test_clear_maintains_capacity() {
638        let config = CachedBusConfig::new(
639            vec![
640                CachedBusInfo::new(2, BusType::Main),
641                CachedBusInfo::new(2, BusType::Aux),
642            ],
643            vec![CachedBusInfo::new(2, BusType::Main)],
644        );
645
646        let mut storage: ProcessBufferStorage<f32> =
647            ProcessBufferStorage::allocate_from_config(&config, 4096);
648
649        // Record initial capacities
650        let main_in_cap = storage.main_inputs.capacity();
651        let main_out_cap = storage.main_outputs.capacity();
652        let aux_in_count = storage.aux_inputs.len();
653        let aux_in_cap = if aux_in_count > 0 {
654            storage.aux_inputs[0].capacity()
655        } else {
656            0
657        };
658
659        // Simulate some usage
660        let dummy: f32 = 0.0;
661        unsafe {
662            storage.push_main_input(&dummy as *const f32);
663            storage.push_main_input(&dummy as *const f32);
664            if aux_in_count > 0 {
665                storage.push_aux_input(0, &dummy as *const f32);
666            }
667        }
668
669        // Clear and verify capacities are unchanged
670        storage.clear();
671
672        assert_eq!(storage.main_inputs.capacity(), main_in_cap);
673        assert_eq!(storage.main_outputs.capacity(), main_out_cap);
674        assert_eq!(storage.aux_inputs.len(), aux_in_count);
675        if aux_in_count > 0 {
676            assert_eq!(storage.aux_inputs[0].capacity(), aux_in_cap);
677        }
678
679        // Verify lengths are reset to 0
680        assert_eq!(storage.main_inputs.len(), 0);
681        assert_eq!(storage.main_outputs.len(), 0);
682        if aux_in_count > 0 {
683            assert_eq!(storage.aux_inputs[0].len(), 0);
684        }
685    }
686}