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}