Skip to main content

scirs2_core/gpu/memory_management/
pool.rs

1//! Buffer pool and arena memory management for GPU operations.
2//!
3//! This submodule provides core pool-based allocation types: [`BufferPool`],
4//! [`BufferAllocator`], [`MemoryArena`], and associated error/result types.
5
6use crate::gpu::{GpuBuffer, GpuDataType, GpuError};
7use std::collections::{BTreeMap, HashMap, VecDeque};
8use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
9use std::sync::{Arc, Mutex};
10use std::time::{Duration, Instant};
11use thiserror::Error;
12
13#[derive(Error, Debug)]
14pub enum MemoryError {
15    /// Out of memory
16    #[error("Out of memory: {0}")]
17    OutOfMemory(String),
18
19    /// Invalid allocation size
20    #[error("Invalid allocation size: {0}")]
21    InvalidSize(usize),
22
23    /// Buffer not found
24    #[error("Buffer not found: {0}")]
25    BufferNotFound(u64),
26
27    /// Pool is full
28    #[error("Pool is full")]
29    PoolFull,
30
31    /// Fragmentation threshold exceeded
32    #[error("Fragmentation threshold exceeded: {0:.2}%")]
33    FragmentationExceeded(f64),
34
35    /// GPU error
36    #[error("GPU error: {0}")]
37    GpuError(#[from] GpuError),
38}
39
40/// Result type for memory operations
41pub type MemoryResult<T> = Result<T, MemoryError>;
42
43/// Buffer handle for tracking allocated buffers
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
45pub struct BufferHandle(u64);
46
47impl BufferHandle {
48    /// Create a new buffer handle
49    fn new() -> Self {
50        static COUNTER: AtomicU64 = AtomicU64::new(1);
51        Self(COUNTER.fetch_add(1, Ordering::Relaxed))
52    }
53
54    /// Get the raw handle value
55    pub fn raw(&self) -> u64 {
56        self.0
57    }
58}
59
60/// Buffer metadata for pool management
61#[derive(Debug, Clone)]
62struct BufferMetadata {
63    handle: BufferHandle,
64    size: usize,
65    allocated_at: Instant,
66    last_used: Instant,
67    use_count: usize,
68    is_pinned: bool,
69}
70
71impl BufferMetadata {
72    fn new(handle: BufferHandle, size: usize) -> Self {
73        let now = Instant::now();
74        Self {
75            handle,
76            size,
77            allocated_at: now,
78            last_used: now,
79            use_count: 0,
80            is_pinned: false,
81        }
82    }
83
84    fn mark_used(&mut self) {
85        self.last_used = Instant::now();
86        self.use_count += 1;
87    }
88
89    fn age(&self) -> Duration {
90        self.allocated_at.elapsed()
91    }
92
93    fn idle_time(&self) -> Duration {
94        self.last_used.elapsed()
95    }
96}
97
98/// Buffer pool for reusing GPU buffers
99#[derive(Debug)]
100pub struct BufferPool<T: GpuDataType> {
101    // Size-stratified pools (size -> list of available buffers)
102    pools: Arc<Mutex<BTreeMap<usize, VecDeque<(BufferHandle, Arc<GpuBuffer<T>>)>>>>,
103    // Active buffers being used
104    active_buffers: Arc<Mutex<HashMap<BufferHandle, BufferMetadata>>>,
105    // Statistics
106    total_allocated: Arc<AtomicUsize>,
107    total_reused: Arc<AtomicUsize>,
108    max_pool_size: usize,
109    eviction_policy: EvictionPolicy,
110}
111
112/// Buffer eviction policy
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub enum EvictionPolicy {
115    /// Least Recently Used
116    Lru,
117    /// Least Frequently Used
118    Lfu,
119    /// First In First Out
120    Fifo,
121}
122
123impl<T: GpuDataType> BufferPool<T> {
124    /// Create a new buffer pool
125    pub fn new() -> Self {
126        Self::with_capacity_and_policy(1024, EvictionPolicy::Lru)
127    }
128
129    /// Create a new buffer pool with capacity
130    pub fn with_capacity(max_pool_size: usize) -> Self {
131        Self::with_capacity_and_policy(max_pool_size, EvictionPolicy::Lru)
132    }
133
134    /// Create a new buffer pool with capacity and eviction policy
135    pub fn with_capacity_and_policy(max_pool_size: usize, eviction_policy: EvictionPolicy) -> Self {
136        Self {
137            pools: Arc::new(Mutex::new(BTreeMap::new())),
138            active_buffers: Arc::new(Mutex::new(HashMap::new())),
139            total_allocated: Arc::new(AtomicUsize::new(0)),
140            total_reused: Arc::new(AtomicUsize::new(0)),
141            max_pool_size,
142            eviction_policy,
143        }
144    }
145
146    /// Allocate a buffer from the pool or create a new one
147    pub fn allocate(&self, size: usize) -> MemoryResult<(BufferHandle, Arc<GpuBuffer<T>>)> {
148        if size == 0 {
149            return Err(MemoryError::InvalidSize(size));
150        }
151
152        // Try to reuse an existing buffer
153        let mut pools = self.pools.lock().map_err(|_| {
154            MemoryError::GpuError(GpuError::Other("Failed to lock pools".to_string()))
155        })?;
156
157        // Look for a buffer of the exact size or slightly larger
158        let reusable = pools
159            .range_mut(size..)
160            .next()
161            .and_then(|(_, buffers)| buffers.pop_front());
162
163        if let Some((handle, buffer)) = reusable {
164            self.total_reused.fetch_add(1, Ordering::Relaxed);
165
166            let mut active = self.active_buffers.lock().map_err(|_| {
167                MemoryError::GpuError(GpuError::Other("Failed to lock active buffers".to_string()))
168            })?;
169
170            if let Some(metadata) = active.get_mut(&handle) {
171                metadata.mark_used();
172            } else {
173                let metadata = BufferMetadata::new(handle, size);
174                active.insert(handle, metadata);
175            }
176
177            return Ok((handle, buffer));
178        }
179
180        drop(pools);
181
182        // No reusable buffer found, allocate a new one
183        // Note: In a real implementation, this would call the GPU backend
184        // For now, we create a placeholder buffer
185        self.allocate_new_buffer(size)
186    }
187
188    /// Allocate a new buffer
189    fn allocate_new_buffer(&self, size: usize) -> MemoryResult<(BufferHandle, Arc<GpuBuffer<T>>)> {
190        self.total_allocated.fetch_add(1, Ordering::Relaxed);
191
192        // In a real implementation, this would use the GPU backend to allocate
193        // For now, we create a dummy buffer handle
194        let handle = BufferHandle::new();
195
196        // Since we can't create a real GpuBuffer without a backend,
197        // we'll need to modify this in practice to use the actual GPU context
198        // For the type system, we'll return an error indicating this needs backend support
199        Err(MemoryError::GpuError(GpuError::Other(
200            "Buffer allocation requires GPU backend context".to_string(),
201        )))
202    }
203
204    /// Return a buffer to the pool
205    pub fn deallocate(&self, handle: BufferHandle, buffer: Arc<GpuBuffer<T>>) -> MemoryResult<()> {
206        let mut active = self.active_buffers.lock().map_err(|_| {
207            MemoryError::GpuError(GpuError::Other("Failed to lock active buffers".to_string()))
208        })?;
209
210        let metadata = active
211            .remove(&handle)
212            .ok_or(MemoryError::BufferNotFound(handle.raw()))?;
213
214        drop(active);
215
216        let mut pools = self.pools.lock().map_err(|_| {
217            MemoryError::GpuError(GpuError::Other("Failed to lock pools".to_string()))
218        })?;
219
220        let pool = pools.entry(metadata.size).or_insert_with(VecDeque::new);
221
222        if pool.len() >= self.max_pool_size {
223            // Pool is full, apply eviction policy
224            self.evict_buffer(pool)?;
225        }
226
227        pool.push_back((handle, buffer));
228        Ok(())
229    }
230
231    /// Evict a buffer based on the eviction policy
232    fn evict_buffer(
233        &self,
234        pool: &mut VecDeque<(BufferHandle, Arc<GpuBuffer<T>>)>,
235    ) -> MemoryResult<()> {
236        match self.eviction_policy {
237            EvictionPolicy::Lru | EvictionPolicy::Lfu | EvictionPolicy::Fifo => {
238                // For FIFO and LRU, remove the front
239                pool.pop_front();
240                Ok(())
241            }
242        }
243    }
244
245    /// Get pool statistics
246    pub fn statistics(&self) -> BufferPoolStatistics {
247        let pools = self.pools.lock().expect("Failed to lock pools");
248        let active = self
249            .active_buffers
250            .lock()
251            .expect("Failed to lock active buffers");
252
253        let total_pooled: usize = pools.values().map(|v| v.len()).sum();
254        let total_active = active.len();
255
256        BufferPoolStatistics {
257            total_allocated: self.total_allocated.load(Ordering::Relaxed),
258            total_reused: self.total_reused.load(Ordering::Relaxed),
259            pooled_buffers: total_pooled,
260            active_buffers: total_active,
261            pool_size_distribution: pools
262                .iter()
263                .map(|(size, buffers)| (*size, buffers.len()))
264                .collect(),
265        }
266    }
267
268    /// Clear the pool
269    pub fn clear(&self) -> MemoryResult<()> {
270        let mut pools = self.pools.lock().map_err(|_| {
271            MemoryError::GpuError(GpuError::Other("Failed to lock pools".to_string()))
272        })?;
273        pools.clear();
274        Ok(())
275    }
276
277    /// Get the total number of pooled buffers
278    pub fn pooled_count(&self) -> MemoryResult<usize> {
279        let pools = self.pools.lock().map_err(|_| {
280            MemoryError::GpuError(GpuError::Other("Failed to lock pools".to_string()))
281        })?;
282        Ok(pools.values().map(|v| v.len()).sum())
283    }
284}
285
286impl<T: GpuDataType> Default for BufferPool<T> {
287    fn default() -> Self {
288        Self::new()
289    }
290}
291
292/// Buffer pool statistics
293#[derive(Debug, Clone)]
294pub struct BufferPoolStatistics {
295    pub total_allocated: usize,
296    pub total_reused: usize,
297    pub pooled_buffers: usize,
298    pub active_buffers: usize,
299    pub pool_size_distribution: Vec<(usize, usize)>,
300}
301
302/// Buffer allocator with size stratification
303#[derive(Debug)]
304pub struct BufferAllocator {
305    // Size classes (powers of 2)
306    size_classes: Vec<usize>,
307    // Memory statistics
308    allocated_bytes: Arc<AtomicUsize>,
309    freed_bytes: Arc<AtomicUsize>,
310    peak_usage: Arc<AtomicUsize>,
311    // Fragmentation tracking
312    fragmentation_threshold: f64,
313}
314
315impl BufferAllocator {
316    /// Create a new buffer allocator
317    pub fn new() -> Self {
318        Self::with_size_classes(Self::default_size_classes())
319    }
320
321    /// Create allocator with custom size classes
322    pub fn with_size_classes(size_classes: Vec<usize>) -> Self {
323        Self {
324            size_classes,
325            allocated_bytes: Arc::new(AtomicUsize::new(0)),
326            freed_bytes: Arc::new(AtomicUsize::new(0)),
327            peak_usage: Arc::new(AtomicUsize::new(0)),
328            fragmentation_threshold: 0.3, // 30% fragmentation threshold
329        }
330    }
331
332    /// Get default size classes (powers of 2 from 256 bytes to 1 GB)
333    fn default_size_classes() -> Vec<usize> {
334        let mut classes = Vec::new();
335        let mut size = 256;
336        while size <= 1024 * 1024 * 1024 {
337            classes.push(size);
338            size *= 2;
339        }
340        classes
341    }
342
343    /// Find the appropriate size class for a requested size
344    pub fn size_class_for(&self, size: usize) -> usize {
345        self.size_classes
346            .iter()
347            .find(|&&class_size| class_size >= size)
348            .copied()
349            .unwrap_or_else(|| {
350                // Round up to next power of 2
351                size.next_power_of_two()
352            })
353    }
354
355    /// Record an allocation
356    pub fn record_allocation(&self, size: usize) {
357        let new_allocated = self.allocated_bytes.fetch_add(size, Ordering::Relaxed) + size;
358
359        // Update peak usage
360        let mut peak = self.peak_usage.load(Ordering::Relaxed);
361        while new_allocated > peak {
362            match self.peak_usage.compare_exchange_weak(
363                peak,
364                new_allocated,
365                Ordering::Relaxed,
366                Ordering::Relaxed,
367            ) {
368                Ok(_) => break,
369                Err(current) => peak = current,
370            }
371        }
372    }
373
374    /// Record a deallocation
375    pub fn record_deallocation(&self, size: usize) {
376        self.freed_bytes.fetch_add(size, Ordering::Relaxed);
377    }
378
379    /// Get current allocated bytes
380    pub fn allocated_bytes(&self) -> usize {
381        self.allocated_bytes.load(Ordering::Relaxed)
382    }
383
384    /// Get freed bytes
385    pub fn freed_bytes(&self) -> usize {
386        self.freed_bytes.load(Ordering::Relaxed)
387    }
388
389    /// Get current usage (allocated - freed)
390    pub fn current_usage(&self) -> usize {
391        self.allocated_bytes() - self.freed_bytes()
392    }
393
394    /// Get peak usage
395    pub fn peak_usage(&self) -> usize {
396        self.peak_usage.load(Ordering::Relaxed)
397    }
398
399    /// Calculate fragmentation ratio
400    pub fn fragmentation_ratio(&self) -> f64 {
401        let allocated = self.allocated_bytes() as f64;
402        let current = self.current_usage() as f64;
403
404        if allocated == 0.0 {
405            return 0.0;
406        }
407
408        (allocated - current) / allocated
409    }
410
411    /// Check if defragmentation is needed
412    pub fn needs_defragmentation(&self) -> bool {
413        self.fragmentation_ratio() > self.fragmentation_threshold
414    }
415
416    /// Get allocator statistics
417    pub fn statistics(&self) -> AllocatorStatistics {
418        AllocatorStatistics {
419            allocated_bytes: self.allocated_bytes(),
420            freed_bytes: self.freed_bytes(),
421            current_usage: self.current_usage(),
422            peak_usage: self.peak_usage(),
423            fragmentation_ratio: self.fragmentation_ratio(),
424        }
425    }
426}
427
428impl Default for BufferAllocator {
429    fn default() -> Self {
430        Self::new()
431    }
432}
433
434/// Allocator statistics
435#[derive(Debug, Clone)]
436pub struct AllocatorStatistics {
437    pub allocated_bytes: usize,
438    pub freed_bytes: usize,
439    pub current_usage: usize,
440    pub peak_usage: usize,
441    pub fragmentation_ratio: f64,
442}
443
444/// Memory arena for temporary scratch space
445#[derive(Debug)]
446pub struct MemoryArena<T: GpuDataType> {
447    // Arena buffers
448    buffers: Arc<Mutex<Vec<Arc<GpuBuffer<T>>>>>,
449    // Current offset in the active buffer
450    current_offset: Arc<AtomicUsize>,
451    // Size of each arena buffer
452    buffer_size: usize,
453    // Total allocated in this arena
454    total_allocated: Arc<AtomicUsize>,
455}
456
457impl<T: GpuDataType> MemoryArena<T> {
458    /// Create a new memory arena
459    pub fn new(buffer_size: usize) -> Self {
460        Self {
461            buffers: Arc::new(Mutex::new(Vec::new())),
462            current_offset: Arc::new(AtomicUsize::new(0)),
463            buffer_size,
464            total_allocated: Arc::new(AtomicUsize::new(0)),
465        }
466    }
467
468    /// Allocate temporary space in the arena
469    pub fn allocate_temp(&self, size: usize) -> MemoryResult<ArenaAllocation> {
470        if size > self.buffer_size {
471            return Err(MemoryError::InvalidSize(size));
472        }
473
474        let offset = self.current_offset.fetch_add(size, Ordering::Relaxed);
475
476        // Check if we need a new buffer
477        if offset + size > self.buffer_size {
478            // Would need to allocate a new buffer in the arena
479            // Reset offset and allocate from start of new buffer
480            self.current_offset.store(size, Ordering::Relaxed);
481            self.total_allocated
482                .fetch_add(self.buffer_size, Ordering::Relaxed);
483
484            Ok(ArenaAllocation {
485                buffer_index: 0, // In practice, would track which buffer
486                offset: 0,
487                size,
488            })
489        } else {
490            Ok(ArenaAllocation {
491                buffer_index: 0,
492                offset,
493                size,
494            })
495        }
496    }
497
498    /// Reset the arena (deallocate all temporary allocations)
499    pub fn reset(&self) {
500        self.current_offset.store(0, Ordering::Relaxed);
501    }
502
503    /// Get total allocated bytes
504    pub fn total_allocated(&self) -> usize {
505        self.total_allocated.load(Ordering::Relaxed)
506    }
507
508    /// Get current usage
509    pub fn current_usage(&self) -> usize {
510        self.current_offset.load(Ordering::Relaxed)
511    }
512}
513
514/// Arena allocation handle
515#[derive(Debug, Clone, Copy)]
516pub struct ArenaAllocation {
517    buffer_index: usize,
518    offset: usize,
519    size: usize,
520}
521
522impl ArenaAllocation {
523    /// Get the offset in the buffer
524    pub fn offset(&self) -> usize {
525        self.offset
526    }
527
528    /// Get the size of the allocation
529    pub fn size(&self) -> usize {
530        self.size
531    }
532
533    /// Get the buffer index
534    pub fn buffer_index(&self) -> usize {
535        self.buffer_index
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    fn test_buffer_handle_creation() {
544        let handle1 = BufferHandle::new();
545        let handle2 = BufferHandle::new();
546        assert_ne!(handle1, handle2);
547    }
548
549    #[test]
550    fn test_buffer_metadata() {
551        let handle = BufferHandle::new();
552        let mut metadata = BufferMetadata::new(handle, 1024);
553
554        assert_eq!(metadata.handle, handle);
555        assert_eq!(metadata.size, 1024);
556        assert_eq!(metadata.use_count, 0);
557        assert!(!metadata.is_pinned);
558
559        metadata.mark_used();
560        assert_eq!(metadata.use_count, 1);
561    }
562
563    #[test]
564    fn test_buffer_allocator_size_classes() {
565        let allocator = BufferAllocator::new();
566
567        assert_eq!(allocator.size_class_for(100), 256);
568        assert_eq!(allocator.size_class_for(256), 256);
569        assert_eq!(allocator.size_class_for(257), 512);
570        assert_eq!(allocator.size_class_for(1000), 1024);
571    }
572
573    #[test]
574    fn test_buffer_allocator_statistics() {
575        let allocator = BufferAllocator::new();
576
577        allocator.record_allocation(1024);
578        allocator.record_allocation(2048);
579
580        assert_eq!(allocator.allocated_bytes(), 3072);
581        assert_eq!(allocator.current_usage(), 3072);
582        assert_eq!(allocator.peak_usage(), 3072);
583
584        allocator.record_deallocation(1024);
585        assert_eq!(allocator.current_usage(), 2048);
586        assert_eq!(allocator.freed_bytes(), 1024);
587    }
588
589    #[test]
590    fn test_buffer_allocator_fragmentation() {
591        let allocator = BufferAllocator::new();
592
593        allocator.record_allocation(10000);
594        allocator.record_deallocation(3000);
595
596        let frag = allocator.fragmentation_ratio();
597        assert!(frag > 0.0 && frag < 1.0);
598    }
599
600    #[test]
601    fn test_memory_arena() {
602        let arena = MemoryArena::<f32>::new(4096);
603
604        let alloc1 = arena.allocate_temp(1024).expect("Failed to allocate");
605        assert_eq!(alloc1.size(), 1024);
606        assert_eq!(alloc1.offset(), 0);
607
608        let alloc2 = arena.allocate_temp(512).expect("Failed to allocate");
609        assert_eq!(alloc2.size(), 512);
610        assert_eq!(alloc2.offset(), 1024);
611
612        arena.reset();
613        assert_eq!(arena.current_usage(), 0);
614    }
615
616    #[test]
617    fn test_memory_arena_overflow() {
618        let arena = MemoryArena::<f32>::new(1024);
619
620        let result = arena.allocate_temp(2048);
621        assert!(result.is_err());
622    }
623
624    fn test_eviction_policy() {
625        let pool = BufferPool::<f32>::with_capacity_and_policy(10, EvictionPolicy::Lru);
626        let stats = pool.statistics();
627
628        assert_eq!(stats.pooled_buffers, 0);
629        assert_eq!(stats.active_buffers, 0);
630    }
631}