1#![allow(clippy::cast_precision_loss)]
9
10use std::time::Instant;
11
12pub struct GpuBuffer {
18 pub id: u64,
20 pub size_bytes: usize,
22 pub alignment: usize,
24 data: Vec<u8>,
26 pub(crate) in_use: bool,
28 pub(crate) created_at: Instant,
30 pub(crate) last_released_at: Option<Instant>,
32}
33
34impl GpuBuffer {
35 #[must_use]
42 pub fn new(id: u64, size: usize, alignment: usize) -> Self {
43 let effective_alignment = alignment.max(1);
44 Self {
45 id,
46 size_bytes: size,
47 alignment: effective_alignment,
48 data: vec![0u8; size],
49 in_use: false,
50 created_at: Instant::now(),
51 last_released_at: None,
52 }
53 }
54
55 #[must_use]
57 pub fn as_slice(&self) -> &[u8] {
58 &self.data
59 }
60
61 #[must_use]
63 pub fn as_mut_slice(&mut self) -> &mut [u8] {
64 &mut self.data
65 }
66
67 pub fn fill(&mut self, value: u8) {
69 self.data.fill(value);
70 }
71
72 #[must_use]
74 pub fn is_in_use(&self) -> bool {
75 self.in_use
76 }
77}
78
79impl std::fmt::Debug for GpuBuffer {
80 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81 f.debug_struct("GpuBuffer")
82 .field("id", &self.id)
83 .field("size_bytes", &self.size_bytes)
84 .field("alignment", &self.alignment)
85 .field("in_use", &self.in_use)
86 .finish()
87 }
88}
89
90#[derive(Debug, Clone)]
96pub struct PoolStats {
97 pub total_buffers: usize,
99 pub in_use_buffers: usize,
101 pub available_buffers: usize,
103 pub total_allocated_bytes: usize,
105 pub reuse_rate: f64,
107}
108
109pub struct BufferPool {
119 buffers: Vec<GpuBuffer>,
120 next_id: u64,
121 total_allocated: usize,
122 max_pool_bytes: usize,
123 reuse_count: u64,
125 alloc_count: u64,
127}
128
129impl BufferPool {
130 #[must_use]
133 pub fn new(max_pool_bytes: usize) -> Self {
134 Self {
135 buffers: Vec::new(),
136 next_id: 1,
137 total_allocated: 0,
138 max_pool_bytes,
139 reuse_count: 0,
140 alloc_count: 0,
141 }
142 }
143
144 pub fn acquire(&mut self, size_bytes: usize, alignment: usize) -> Option<u64> {
157 self.alloc_count += 1;
158
159 let best_idx = self
161 .buffers
162 .iter()
163 .enumerate()
164 .filter(|(_, b)| {
165 !b.in_use && b.size_bytes >= size_bytes && b.alignment >= alignment.max(1)
166 })
167 .min_by_key(|(_, b)| b.size_bytes)
168 .map(|(idx, _)| idx);
169
170 if let Some(idx) = best_idx {
171 self.buffers[idx].in_use = true;
172 self.buffers[idx].created_at = Instant::now();
173 self.reuse_count += 1;
174 return Some(self.buffers[idx].id);
175 }
176
177 let effective_alignment = alignment.max(1);
179 let new_size = self.total_allocated + size_bytes;
180 if new_size > self.max_pool_bytes {
181 return None; }
183
184 let id = self.next_id;
185 self.next_id += 1;
186
187 let mut buf = GpuBuffer::new(id, size_bytes, effective_alignment);
188 buf.in_use = true;
189 self.total_allocated += size_bytes;
190 self.buffers.push(buf);
191
192 Some(id)
193 }
194
195 pub fn release(&mut self, id: u64) -> bool {
204 if let Some(buf) = self.buffers.iter_mut().find(|b| b.id == id) {
205 buf.in_use = false;
206 buf.last_released_at = Some(Instant::now());
207 true
208 } else {
209 false
210 }
211 }
212
213 #[must_use]
219 pub fn get(&self, id: u64) -> Option<&GpuBuffer> {
220 self.buffers.iter().find(|b| b.id == id)
221 }
222
223 #[must_use]
225 pub fn get_mut(&mut self, id: u64) -> Option<&mut GpuBuffer> {
226 self.buffers.iter_mut().find(|b| b.id == id)
227 }
228
229 pub fn defragment(&mut self) {
237 let now = Instant::now();
238 let eviction_threshold = std::time::Duration::from_secs(60);
239
240 let mut bytes_freed = 0usize;
241 self.buffers.retain(|buf| {
242 if buf.in_use {
243 return true; }
245 let idle_since = buf.last_released_at.unwrap_or(buf.created_at);
246 if now.duration_since(idle_since) > eviction_threshold {
247 bytes_freed += buf.size_bytes;
248 false } else {
250 true
251 }
252 });
253 self.total_allocated = self.total_allocated.saturating_sub(bytes_freed);
254 }
255
256 #[must_use]
262 pub fn stats(&self) -> PoolStats {
263 let in_use = self.buffers.iter().filter(|b| b.in_use).count();
264 let available = self.buffers.len() - in_use;
265 let reuse_rate = if self.alloc_count == 0 {
266 0.0
267 } else {
268 self.reuse_count as f64 / self.alloc_count as f64
269 };
270 PoolStats {
271 total_buffers: self.buffers.len(),
272 in_use_buffers: in_use,
273 available_buffers: available,
274 total_allocated_bytes: self.total_allocated,
275 reuse_rate,
276 }
277 }
278
279 #[must_use]
281 pub fn total_allocated_bytes(&self) -> usize {
282 self.total_allocated
283 }
284
285 #[must_use]
287 pub fn max_pool_bytes(&self) -> usize {
288 self.max_pool_bytes
289 }
290}
291
292impl std::fmt::Debug for BufferPool {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 f.debug_struct("BufferPool")
295 .field("buffers", &self.buffers.len())
296 .field("total_allocated", &self.total_allocated)
297 .field("max_pool_bytes", &self.max_pool_bytes)
298 .field("alloc_count", &self.alloc_count)
299 .field("reuse_count", &self.reuse_count)
300 .finish()
301 }
302}
303
304#[derive(Debug, Clone, PartialEq, Eq)]
310pub struct SubAllocation {
311 pub id: u64,
313 pub offset: u64,
315 pub size: u64,
317}
318
319pub struct SubAllocator {
326 backing_buffer_size: u64,
328 current_offset: u64,
330 allocations: Vec<SubAllocation>,
332 alignment: u64,
334 next_id: u64,
336 freed_ids: std::collections::HashSet<u64>,
338}
339
340impl SubAllocator {
341 #[must_use]
346 pub fn new(backing_size: u64, alignment: u64) -> Self {
347 let alignment = alignment.max(1);
348 Self {
349 backing_buffer_size: backing_size,
350 current_offset: 0,
351 allocations: Vec::new(),
352 alignment,
353 next_id: 1,
354 freed_ids: std::collections::HashSet::new(),
355 }
356 }
357
358 pub fn alloc(&mut self, size: u64) -> Option<SubAllocation> {
363 if size == 0 {
364 return None;
365 }
366
367 let aligned_offset = Self::align_up(self.current_offset, self.alignment);
369 let end = aligned_offset.checked_add(size)?;
370
371 if end > self.backing_buffer_size {
372 return None; }
374
375 let id = self.next_id;
376 self.next_id += 1;
377 self.current_offset = end;
378
379 let alloc = SubAllocation {
380 id,
381 offset: aligned_offset,
382 size,
383 };
384 self.allocations.push(alloc.clone());
385 Some(alloc)
386 }
387
388 pub fn free(&mut self, id: u64) {
392 if let Some(pos) = self.allocations.iter().position(|a| a.id == id) {
393 self.freed_ids.insert(id);
394 self.allocations.remove(pos);
395 }
396 }
397
398 pub fn defrag(&mut self) {
404 self.allocations.retain(|a| !self.freed_ids.contains(&a.id));
406 self.freed_ids.clear();
407
408 let mut cursor: u64 = 0;
410 for alloc in &mut self.allocations {
411 let aligned = Self::align_up(cursor, self.alignment);
412 alloc.offset = aligned;
413 cursor = aligned + alloc.size;
414 }
415 self.current_offset = cursor;
416 }
417
418 #[must_use]
421 pub fn utilization(&self) -> f64 {
422 if self.backing_buffer_size == 0 {
423 return 0.0;
424 }
425 let live_bytes: u64 = self.allocations.iter().map(|a| a.size).sum();
426 live_bytes as f64 / self.backing_buffer_size as f64
427 }
428
429 #[must_use]
431 pub fn allocation_count(&self) -> usize {
432 self.allocations.len()
433 }
434
435 #[must_use]
437 pub fn current_offset(&self) -> u64 {
438 self.current_offset
439 }
440
441 #[must_use]
443 pub fn capacity(&self) -> u64 {
444 self.backing_buffer_size
445 }
446
447 #[must_use]
449 pub fn alignment(&self) -> u64 {
450 self.alignment
451 }
452
453 fn align_up(offset: u64, alignment: u64) -> u64 {
457 if alignment <= 1 {
458 return offset;
459 }
460 let rem = offset % alignment;
461 if rem == 0 {
462 offset
463 } else {
464 offset + (alignment - rem)
465 }
466 }
467}
468
469impl std::fmt::Debug for SubAllocator {
470 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
471 f.debug_struct("SubAllocator")
472 .field("capacity", &self.backing_buffer_size)
473 .field("current_offset", &self.current_offset)
474 .field("live_allocs", &self.allocations.len())
475 .field("alignment", &self.alignment)
476 .finish()
477 }
478}
479
480#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[test]
491 fn test_gpu_buffer_new() {
492 let buf = GpuBuffer::new(1, 1024, 64);
493 assert_eq!(buf.id, 1);
494 assert_eq!(buf.size_bytes, 1024);
495 assert_eq!(buf.alignment, 64);
496 assert_eq!(buf.as_slice().len(), 1024);
497 assert!(!buf.is_in_use());
498 }
499
500 #[test]
501 fn test_gpu_buffer_fill() {
502 let mut buf = GpuBuffer::new(2, 16, 4);
503 buf.fill(0xAB);
504 assert!(buf.as_slice().iter().all(|&b| b == 0xAB));
505 }
506
507 #[test]
508 fn test_gpu_buffer_as_mut_slice() {
509 let mut buf = GpuBuffer::new(3, 8, 1);
510 buf.as_mut_slice()[0] = 42;
511 assert_eq!(buf.as_slice()[0], 42);
512 }
513
514 #[test]
517 fn test_pool_new_empty() {
518 let pool = BufferPool::new(1024 * 1024);
519 let stats = pool.stats();
520 assert_eq!(stats.total_buffers, 0);
521 assert_eq!(stats.reuse_rate, 0.0);
522 }
523
524 #[test]
527 fn test_pool_acquire_and_release() {
528 let mut pool = BufferPool::new(1024 * 1024);
529 let id = pool.acquire(256, 4).expect("acquire failed");
530 assert!(pool.get(id).expect("missing").is_in_use());
531
532 let released = pool.release(id);
533 assert!(released, "release should succeed");
534 assert!(!pool.get(id).expect("missing").is_in_use());
535 }
536
537 #[test]
538 fn test_pool_reuse() {
539 let mut pool = BufferPool::new(1024 * 1024);
540 let id1 = pool.acquire(512, 4).expect("first acquire");
541 pool.release(id1);
542 let id2 = pool.acquire(512, 4).expect("second acquire");
543 assert_eq!(id1, id2, "expected buffer reuse");
545 let stats = pool.stats();
546 assert!(stats.reuse_rate > 0.0);
547 }
548
549 #[test]
550 fn test_pool_smallest_compatible_preferred() {
551 let mut pool = BufferPool::new(4 * 1024 * 1024);
552 let big = pool.acquire(4096, 4).expect("big");
554 let small = pool.acquire(256, 4).expect("small");
555 pool.release(big);
556 pool.release(small);
557 let id = pool.acquire(128, 4).expect("reacquire");
559 assert_eq!(id, small, "should prefer smaller buffer");
560 }
561
562 #[test]
563 fn test_pool_budget_exceeded() {
564 let mut pool = BufferPool::new(100);
565 let id = pool.acquire(80, 1).expect("first");
567 let result = pool.acquire(80, 1);
569 assert!(result.is_none(), "should fail over budget");
570 pool.release(id);
571 }
572
573 #[test]
574 fn test_pool_release_unknown_id() {
575 let mut pool = BufferPool::new(1024);
576 assert!(
577 !pool.release(9999),
578 "releasing unknown id should return false"
579 );
580 }
581
582 #[test]
583 fn test_pool_get_missing() {
584 let pool = BufferPool::new(1024);
585 assert!(pool.get(42).is_none());
586 }
587
588 #[test]
591 fn test_pool_get_mut_write() {
592 let mut pool = BufferPool::new(1024 * 1024);
593 let id = pool.acquire(64, 1).expect("acquire");
594 {
595 let buf = pool.get_mut(id).expect("get_mut");
596 buf.as_mut_slice()[0] = 0xFF;
597 }
598 assert_eq!(pool.get(id).expect("get").as_slice()[0], 0xFF);
599 }
600
601 #[test]
604 fn test_pool_stats_in_use_count() {
605 let mut pool = BufferPool::new(1024 * 1024);
606 let id1 = pool.acquire(128, 1).expect("a1");
607 let _id2 = pool.acquire(128, 1).expect("a2");
608 pool.release(id1);
609 let stats = pool.stats();
610 assert_eq!(stats.total_buffers, 2);
611 assert_eq!(stats.in_use_buffers, 1);
612 assert_eq!(stats.available_buffers, 1);
613 }
614
615 #[test]
618 fn test_pool_defragment_keeps_in_use() {
619 let mut pool = BufferPool::new(1024 * 1024);
620 let id = pool.acquire(64, 1).expect("acquire");
621 pool.defragment();
623 assert!(
624 pool.get(id).is_some(),
625 "in-use buffer should not be evicted"
626 );
627 }
628
629 #[test]
630 fn test_pool_defragment_recently_released_kept() {
631 let mut pool = BufferPool::new(1024 * 1024);
632 let id = pool.acquire(64, 1).expect("acquire");
633 pool.release(id);
634 pool.defragment();
636 assert!(
637 pool.get(id).is_some(),
638 "recently released buffer should survive"
639 );
640 }
641
642 #[test]
645 fn test_sub_alloc_basic() {
646 let mut sa = SubAllocator::new(1024, 4);
647 let a = sa.alloc(64).expect("alloc 64 bytes");
648 assert_eq!(a.offset, 0);
649 assert_eq!(a.size, 64);
650 assert_eq!(sa.allocation_count(), 1);
651 }
652
653 #[test]
654 fn test_sub_alloc_fills_buffer() {
655 let mut sa = SubAllocator::new(128, 1);
656 sa.alloc(128).expect("should fill exactly");
657 assert!(sa.alloc(1).is_none(), "buffer exhausted");
659 }
660
661 #[test]
662 fn test_sub_alloc_alignment_respected() {
663 let alignment = 16u64;
664 let mut sa = SubAllocator::new(4096, alignment);
665 let a1 = sa.alloc(1).expect("first alloc");
667 assert_eq!(a1.offset % alignment, 0, "offset must be aligned");
668 let a2 = sa.alloc(1).expect("second alloc");
670 assert_eq!(
671 a2.offset, 16,
672 "second alloc should start at aligned offset 16"
673 );
674 assert_eq!(a2.offset % alignment, 0, "all offsets must be aligned");
675 }
676
677 #[test]
678 fn test_sub_alloc_free_reduces_count() {
679 let mut sa = SubAllocator::new(1024, 4);
680 let a1 = sa.alloc(100).expect("alloc 1");
681 let a2 = sa.alloc(100).expect("alloc 2");
682 assert_eq!(sa.allocation_count(), 2);
683 sa.free(a1.id);
684 assert_eq!(sa.allocation_count(), 1);
685 sa.free(a2.id);
686 assert_eq!(sa.allocation_count(), 0);
687 }
688
689 #[test]
690 fn test_sub_alloc_defrag_reclaims_space() {
691 let mut sa = SubAllocator::new(200, 1);
692 let a1 = sa.alloc(100).expect("a1");
693 let _a2 = sa.alloc(100).expect("a2");
694 assert!(sa.alloc(1).is_none(), "should be full before defrag");
696 sa.free(a1.id);
698 sa.defrag();
699 let a3 = sa.alloc(100).expect("a3 after defrag");
701 assert!(a3.offset < 200, "a3 offset must be within backing buffer");
702 }
703
704 #[test]
705 fn test_sub_alloc_defrag_zeroes_utilization_when_all_freed() {
706 let mut sa = SubAllocator::new(512, 8);
707 let a1 = sa.alloc(100).expect("a1");
708 let a2 = sa.alloc(100).expect("a2");
709 assert!(sa.utilization() > 0.0);
710 sa.free(a1.id);
711 sa.free(a2.id);
712 sa.defrag();
713 assert_eq!(
714 sa.utilization(),
715 0.0,
716 "utilization must be 0 after all freed + defrag"
717 );
718 assert_eq!(sa.current_offset(), 0);
719 }
720
721 #[test]
722 fn test_sub_alloc_utilization_rises_and_falls() {
723 let mut sa = SubAllocator::new(1000, 1);
724 assert_eq!(sa.utilization(), 0.0);
725 let a = sa.alloc(500).expect("alloc 500");
726 assert!((sa.utilization() - 0.5).abs() < 1e-9);
728 sa.free(a.id);
729 sa.defrag();
730 assert_eq!(sa.utilization(), 0.0);
731 }
732
733 #[test]
734 fn test_sub_alloc_zero_size_returns_none() {
735 let mut sa = SubAllocator::new(1024, 4);
736 assert!(sa.alloc(0).is_none(), "zero-size alloc must return None");
737 }
738
739 #[test]
740 fn test_sub_alloc_ids_are_unique() {
741 let mut sa = SubAllocator::new(4096, 4);
742 let a1 = sa.alloc(10).expect("a1");
743 let a2 = sa.alloc(10).expect("a2");
744 let a3 = sa.alloc(10).expect("a3");
745 assert_ne!(a1.id, a2.id);
746 assert_ne!(a2.id, a3.id);
747 }
748
749 #[test]
750 fn test_sub_alloc_capacity_and_alignment_accessors() {
751 let sa = SubAllocator::new(8192, 64);
752 assert_eq!(sa.capacity(), 8192);
753 assert_eq!(sa.alignment(), 64);
754 }
755
756 #[test]
757 fn test_sub_alloc_debug_fmt() {
758 let sa = SubAllocator::new(1024, 4);
759 let s = format!("{sa:?}");
760 assert!(s.contains("SubAllocator"));
761 }
762
763 #[test]
766 fn test_buffer_pool_alloc_free_100_cycles() {
767 let mut pool = BufferPool::new(100 * 1024 * 1024); let mut ids = Vec::with_capacity(100);
769 for _ in 0..100 {
770 let id = pool.acquire(1024, 8).expect("acquire in cycle");
771 ids.push(id);
772 }
773 for id in &ids {
774 pool.release(*id);
775 }
776 let stats = pool.stats();
777 assert_eq!(
778 stats.in_use_buffers, 0,
779 "all buffers must be freed after release"
780 );
781 }
782
783 #[test]
784 fn test_buffer_pool_alloc_free_alloc_reuse() {
785 let mut pool = BufferPool::new(1024 * 1024);
786 let id1 = pool.acquire(512, 4).expect("first alloc");
787 pool.release(id1);
788 let id2 = pool.acquire(512, 4).expect("second alloc after free");
789 assert_eq!(id1, id2, "should reuse freed buffer");
790 assert!(pool.stats().reuse_rate > 0.0);
791 pool.release(id2);
792 }
793
794 #[test]
795 fn test_buffer_pool_alloc_1000_then_free_all() {
796 let budget = 1000 * 64 + 1024; let mut pool = BufferPool::new(budget);
798 let mut ids = Vec::with_capacity(1000);
799 for _ in 0..1000 {
800 let id = pool.acquire(64, 1).expect("acquire 64 bytes");
801 ids.push(id);
802 }
803 for id in &ids {
804 pool.release(*id);
805 }
806 let stats = pool.stats();
807 assert_eq!(stats.in_use_buffers, 0);
808 assert!(stats.total_allocated_bytes <= budget);
810 }
811
812 #[test]
813 fn test_sub_alloc_alloc_free_cycle_many() {
814 let mut sa = SubAllocator::new(1024 * 1024, 16);
815 for _ in 0..100 {
816 let a = sa.alloc(256).expect("alloc in cycle");
817 sa.free(a.id);
818 sa.defrag();
819 }
820 assert_eq!(sa.allocation_count(), 0);
821 assert_eq!(sa.utilization(), 0.0);
822 }
823}