1#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Alignment {
11 Bytes4 = 4,
13 Bytes16 = 16,
15 Bytes64 = 64,
17 Bytes256 = 256,
19 Bytes4096 = 4096,
21}
22
23impl Alignment {
24 #[allow(dead_code)]
26 #[must_use]
27 pub const fn as_usize(self) -> usize {
28 self as usize
29 }
30
31 #[allow(dead_code)]
33 #[must_use]
34 pub const fn align_up(self, offset: usize) -> usize {
35 let align = self as usize;
36 (offset + align - 1) & !(align - 1)
37 }
38}
39
40#[allow(dead_code)]
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct AllocationHandle {
44 pub block_index: usize,
46 pub offset: usize,
48 pub size: usize,
50 pub alignment: usize,
52 pub id: u64,
54}
55
56#[allow(dead_code)]
58#[derive(Debug)]
59struct FreeRange {
60 offset: usize,
61 size: usize,
62}
63
64#[allow(dead_code)]
66#[derive(Debug)]
67struct Block {
68 capacity: usize,
70 free_ranges: Vec<FreeRange>,
72 live_count: usize,
74}
75
76impl Block {
77 fn new(capacity: usize) -> Self {
78 Self {
79 capacity,
80 free_ranges: vec![FreeRange {
81 offset: 0,
82 size: capacity,
83 }],
84 live_count: 0,
85 }
86 }
87
88 fn try_alloc(&mut self, size: usize, alignment: usize) -> Option<usize> {
91 for range in &mut self.free_ranges {
92 let aligned_offset = (range.offset + alignment - 1) & !(alignment - 1);
93 let waste = aligned_offset - range.offset;
94 if range.size >= waste + size {
95 let result_offset = aligned_offset;
96 range.offset += waste + size;
97 range.size -= waste + size;
98 self.live_count += 1;
99 return Some(result_offset);
100 }
101 }
102 self.free_ranges.retain(|r| r.size > 0);
104 None
105 }
106
107 fn free(&mut self, offset: usize, size: usize) {
109 self.free_ranges.push(FreeRange { offset, size });
110 if self.live_count > 0 {
111 self.live_count -= 1;
112 }
113 self.coalesce();
115 }
116
117 fn coalesce(&mut self) {
118 self.free_ranges.sort_by_key(|r| r.offset);
119 let mut i = 0;
120 while i + 1 < self.free_ranges.len() {
121 let end = self.free_ranges[i].offset + self.free_ranges[i].size;
122 if end >= self.free_ranges[i + 1].offset {
123 let merged_size = self.free_ranges[i + 1].offset + self.free_ranges[i + 1].size
125 - self.free_ranges[i].offset;
126 self.free_ranges[i].size = merged_size;
127 self.free_ranges.remove(i + 1);
128 } else {
129 i += 1;
130 }
131 }
132 }
133
134 fn free_bytes(&self) -> usize {
136 self.free_ranges.iter().map(|r| r.size).sum()
137 }
138}
139
140#[allow(dead_code)]
142#[derive(Debug, Clone, Default)]
143pub struct PoolStats {
144 pub total_reserved: usize,
146 pub total_allocated: usize,
148 pub block_count: usize,
150 pub alloc_count: u64,
152 pub free_count: u64,
154 pub failures: u64,
156}
157
158impl PoolStats {
159 #[allow(dead_code)]
161 #[must_use]
162 pub fn free_bytes(&self) -> usize {
163 self.total_reserved.saturating_sub(self.total_allocated)
164 }
165
166 #[allow(dead_code)]
168 #[must_use]
169 pub fn utilisation(&self) -> f64 {
170 if self.total_reserved == 0 {
171 0.0
172 } else {
173 self.total_allocated as f64 / self.total_reserved as f64
174 }
175 }
176}
177
178#[allow(dead_code)]
180pub struct GpuMemoryPool {
181 block_size: usize,
183 blocks: Vec<Block>,
185 stats: PoolStats,
187 next_id: u64,
189}
190
191impl GpuMemoryPool {
192 #[allow(dead_code)]
196 #[must_use]
197 pub fn new(block_size: usize) -> Self {
198 assert!(block_size > 0, "block_size must be > 0");
199 Self {
200 block_size,
201 blocks: Vec::new(),
202 stats: PoolStats::default(),
203 next_id: 0,
204 }
205 }
206
207 #[allow(dead_code)]
212 pub fn alloc(&mut self, size: usize, alignment: Alignment) -> Option<AllocationHandle> {
213 if size == 0 {
214 return None;
215 }
216 let align = alignment.as_usize();
217
218 for (i, block) in self.blocks.iter_mut().enumerate() {
220 if let Some(offset) = block.try_alloc(size, align) {
221 let id = self.next_id;
222 self.next_id += 1;
223 self.stats.alloc_count += 1;
224 self.stats.total_allocated += size;
225 return Some(AllocationHandle {
226 block_index: i,
227 offset,
228 size,
229 alignment: align,
230 id,
231 });
232 }
233 }
234
235 let new_block_size = self.block_size.max(size + align);
237 let mut block = Block::new(new_block_size);
238 if let Some(offset) = block.try_alloc(size, align) {
239 self.stats.total_reserved += new_block_size;
240 self.stats.block_count += 1;
241 let block_index = self.blocks.len();
242 self.blocks.push(block);
243
244 let id = self.next_id;
245 self.next_id += 1;
246 self.stats.alloc_count += 1;
247 self.stats.total_allocated += size;
248 Some(AllocationHandle {
249 block_index,
250 offset,
251 size,
252 alignment: align,
253 id,
254 })
255 } else {
256 self.stats.failures += 1;
257 None
258 }
259 }
260
261 #[allow(dead_code)]
263 pub fn free(&mut self, handle: &AllocationHandle) {
264 if handle.block_index < self.blocks.len() {
265 self.blocks[handle.block_index].free(handle.offset, handle.size);
266 self.stats.total_allocated = self.stats.total_allocated.saturating_sub(handle.size);
267 self.stats.free_count += 1;
268 }
269 }
270
271 #[allow(dead_code)]
273 #[must_use]
274 pub fn stats(&self) -> &PoolStats {
275 &self.stats
276 }
277
278 #[allow(dead_code)]
280 #[must_use]
281 pub fn block_count(&self) -> usize {
282 self.blocks.len()
283 }
284
285 #[allow(dead_code)]
287 #[must_use]
288 pub fn free_bytes(&self) -> usize {
289 self.blocks.iter().map(Block::free_bytes).sum()
290 }
291
292 #[allow(dead_code)]
294 pub fn reset(&mut self) {
295 self.blocks.clear();
296 self.stats = PoolStats::default();
297 self.next_id = 0;
298 }
299
300 #[allow(dead_code)]
308 pub fn defragment(&mut self) -> DefragResult {
309 let mut ranges_coalesced = 0u64;
310 let bytes_before_free = self.free_bytes();
311
312 for block in &mut self.blocks {
314 let ranges_before = block.free_ranges.len();
315 block.coalesce();
316 let ranges_after = block.free_ranges.len();
317 if ranges_before > ranges_after {
318 ranges_coalesced += (ranges_before - ranges_after) as u64;
319 }
320 }
321
322 let blocks_before = self.blocks.len();
324 self.blocks.retain(|b| b.live_count > 0);
325 let blocks_after = self.blocks.len();
326 let blocks_removed = (blocks_before - blocks_after) as u64;
327
328 if blocks_removed > 0 {
330 self.stats.block_count = self.blocks.len();
331 self.stats.total_reserved = self.blocks.iter().map(|b| b.capacity).sum();
332 }
333
334 let bytes_after_free = self.free_bytes();
335
336 DefragResult {
337 ranges_coalesced,
338 blocks_removed,
339 bytes_recovered: bytes_after_free.saturating_sub(bytes_before_free),
340 fragmentation_ratio: self.fragmentation_ratio(),
341 }
342 }
343
344 #[allow(dead_code)]
353 pub fn compact(&mut self) -> Option<CompactionPlan> {
354 let defrag = self.defragment();
356
357 if self.blocks.len() <= 1 {
358 return Some(CompactionPlan {
359 migrations: Vec::new(),
360 defrag_result: defrag,
361 });
362 }
363
364 let mut sparse_blocks: Vec<usize> = Vec::new();
366 for (i, block) in self.blocks.iter().enumerate() {
367 let used = block.capacity - block.free_bytes();
368 let util = if block.capacity > 0 {
369 used as f64 / block.capacity as f64
370 } else {
371 1.0
372 };
373 if util < 0.5 && block.live_count > 0 {
374 sparse_blocks.push(i);
375 }
376 }
377
378 if sparse_blocks.is_empty() {
379 return Some(CompactionPlan {
380 migrations: Vec::new(),
381 defrag_result: defrag,
382 });
383 }
384
385 let migrations: Vec<MigrationEntry> = sparse_blocks
388 .iter()
389 .map(|&block_idx| {
390 let used = self.blocks[block_idx].capacity - self.blocks[block_idx].free_bytes();
391 MigrationEntry {
392 source_block: block_idx,
393 estimated_bytes: used,
394 }
395 })
396 .collect();
397
398 Some(CompactionPlan {
399 migrations,
400 defrag_result: defrag,
401 })
402 }
403
404 #[allow(dead_code)]
409 #[must_use]
410 pub fn fragmentation_ratio(&self) -> f64 {
411 let total_free = self.free_bytes();
412 if total_free == 0 {
413 return 0.0;
414 }
415
416 let largest_contiguous: usize = self
417 .blocks
418 .iter()
419 .flat_map(|b| b.free_ranges.iter().map(|r| r.size))
420 .max()
421 .unwrap_or(0);
422
423 if total_free == 0 {
424 0.0
425 } else {
426 1.0 - (largest_contiguous as f64 / total_free as f64)
427 }
428 }
429
430 #[allow(dead_code)]
434 pub fn shrink(&mut self) -> usize {
435 let before = self.blocks.len();
436 self.blocks.retain(|b| b.live_count > 0);
437 let removed = before - self.blocks.len();
438 if removed > 0 {
439 self.stats.block_count = self.blocks.len();
440 self.stats.total_reserved = self.blocks.iter().map(|b| b.capacity).sum();
441 }
442 removed
443 }
444}
445
446#[allow(dead_code)]
448#[derive(Debug, Clone)]
449pub struct DefragResult {
450 pub ranges_coalesced: u64,
452 pub blocks_removed: u64,
454 pub bytes_recovered: usize,
456 pub fragmentation_ratio: f64,
458}
459
460#[allow(dead_code)]
462#[derive(Debug, Clone)]
463pub struct MigrationEntry {
464 pub source_block: usize,
466 pub estimated_bytes: usize,
468}
469
470#[allow(dead_code)]
472#[derive(Debug, Clone)]
473pub struct CompactionPlan {
474 pub migrations: Vec<MigrationEntry>,
476 pub defrag_result: DefragResult,
478}
479
480#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[test]
489 fn test_alignment_align_up() {
490 assert_eq!(Alignment::Bytes16.align_up(0), 0);
491 assert_eq!(Alignment::Bytes16.align_up(1), 16);
492 assert_eq!(Alignment::Bytes16.align_up(16), 16);
493 assert_eq!(Alignment::Bytes16.align_up(17), 32);
494 }
495
496 #[test]
497 fn test_alignment_as_usize() {
498 assert_eq!(Alignment::Bytes4.as_usize(), 4);
499 assert_eq!(Alignment::Bytes256.as_usize(), 256);
500 }
501
502 #[test]
503 fn test_simple_alloc() {
504 let mut pool = GpuMemoryPool::new(1024);
505 let handle = pool.alloc(64, Alignment::Bytes16);
506 assert!(handle.is_some());
507 let h = handle.expect("handle should be valid");
508 assert_eq!(h.size, 64);
509 assert_eq!(h.offset % 16, 0);
510 }
511
512 #[test]
513 fn test_zero_size_alloc_returns_none() {
514 let mut pool = GpuMemoryPool::new(1024);
515 assert!(pool.alloc(0, Alignment::Bytes4).is_none());
516 }
517
518 #[test]
519 fn test_alloc_and_free_stats() {
520 let mut pool = GpuMemoryPool::new(1024);
521 let h = pool
522 .alloc(100, Alignment::Bytes4)
523 .expect("allocation should succeed");
524 assert_eq!(pool.stats().total_allocated, 100);
525 pool.free(&h);
526 assert_eq!(pool.stats().total_allocated, 0);
527 }
528
529 #[test]
530 fn test_multiple_allocs_same_block() {
531 let mut pool = GpuMemoryPool::new(4096);
532 let h1 = pool
533 .alloc(128, Alignment::Bytes64)
534 .expect("allocation should succeed");
535 let h2 = pool
536 .alloc(128, Alignment::Bytes64)
537 .expect("allocation should succeed");
538 assert_eq!(h1.block_index, h2.block_index);
539 assert_eq!(pool.block_count(), 1);
540 }
541
542 #[test]
543 fn test_new_block_created_when_full() {
544 let mut pool = GpuMemoryPool::new(64);
545 let _h1 = pool
547 .alloc(64, Alignment::Bytes4)
548 .expect("allocation should succeed");
549 let h2 = pool
551 .alloc(64, Alignment::Bytes4)
552 .expect("allocation should succeed");
553 assert!(h2.block_index >= 1 || pool.block_count() == 2);
554 }
555
556 #[test]
557 fn test_pool_stats_utilisation() {
558 let mut pool = GpuMemoryPool::new(1000);
559 pool.alloc(500, Alignment::Bytes4);
560 let util = pool.stats().utilisation();
561 assert!(util > 0.0 && util <= 1.0);
562 }
563
564 #[test]
565 fn test_free_bytes_decreases_after_alloc() {
566 let mut pool = GpuMemoryPool::new(1024);
567 pool.alloc(256, Alignment::Bytes4);
568 assert!(pool.free_bytes() < 1024);
569 }
570
571 #[test]
572 fn test_reset_clears_all() {
573 let mut pool = GpuMemoryPool::new(512);
574 pool.alloc(100, Alignment::Bytes4);
575 pool.reset();
576 assert_eq!(pool.block_count(), 0);
577 assert_eq!(pool.stats().alloc_count, 0);
578 }
579
580 #[test]
581 fn test_alloc_id_increments() {
582 let mut pool = GpuMemoryPool::new(1024);
583 let h1 = pool
584 .alloc(10, Alignment::Bytes4)
585 .expect("allocation should succeed");
586 let h2 = pool
587 .alloc(10, Alignment::Bytes4)
588 .expect("allocation should succeed");
589 assert!(h2.id > h1.id);
590 }
591
592 #[test]
593 fn test_block_coalescing_after_free() {
594 let mut pool = GpuMemoryPool::new(256);
595 let h1 = pool
596 .alloc(64, Alignment::Bytes4)
597 .expect("allocation should succeed");
598 let h2 = pool
599 .alloc(64, Alignment::Bytes4)
600 .expect("allocation should succeed");
601 pool.free(&h1);
602 pool.free(&h2);
603 let h3 = pool.alloc(100, Alignment::Bytes4);
605 assert!(h3.is_some());
606 }
607
608 #[test]
609 fn test_stats_free_bytes() {
610 let mut stats = PoolStats {
611 total_reserved: 1000,
612 total_allocated: 400,
613 ..Default::default()
614 };
615 assert_eq!(stats.free_bytes(), 600);
616 stats.total_allocated = 1000;
617 assert_eq!(stats.free_bytes(), 0);
618 }
619
620 #[test]
623 fn test_defragment_empty_pool() {
624 let mut pool = GpuMemoryPool::new(1024);
625 let result = pool.defragment();
626 assert_eq!(result.ranges_coalesced, 0);
627 assert_eq!(result.blocks_removed, 0);
628 }
629
630 #[test]
631 fn test_defragment_removes_empty_blocks() {
632 let mut pool = GpuMemoryPool::new(256);
633 let h1 = pool.alloc(256, Alignment::Bytes4).expect("alloc 1");
635 let _h2 = pool.alloc(256, Alignment::Bytes4).expect("alloc 2");
636 assert_eq!(pool.block_count(), 2);
637
638 pool.free(&h1);
640 let result = pool.defragment();
641 assert_eq!(result.blocks_removed, 1);
642 assert_eq!(pool.block_count(), 1);
643 }
644
645 #[test]
646 fn test_defragment_coalesces_ranges() {
647 let mut pool = GpuMemoryPool::new(1024);
648 let h1 = pool.alloc(64, Alignment::Bytes4).expect("alloc 1");
649 let h2 = pool.alloc(64, Alignment::Bytes4).expect("alloc 2");
650 let h3 = pool.alloc(64, Alignment::Bytes4).expect("alloc 3");
651
652 pool.free(&h1);
654 pool.free(&h3);
655
656 let frag_before = pool.fragmentation_ratio();
657
658 pool.free(&h2);
660 let result = pool.defragment();
661
662 assert!(result.ranges_coalesced > 0 || result.blocks_removed > 0);
664 let frag_after = pool.fragmentation_ratio();
665 assert!(
666 frag_after <= frag_before || frag_after == 0.0,
667 "fragmentation should not increase: before={frag_before}, after={frag_after}"
668 );
669 }
670
671 #[test]
672 fn test_fragmentation_ratio_no_fragmentation() {
673 let mut pool = GpuMemoryPool::new(1024);
674 pool.alloc(100, Alignment::Bytes4);
676 let ratio = pool.fragmentation_ratio();
677 assert!(ratio == 0.0, "expected 0.0 fragmentation, got {ratio}");
679 }
680
681 #[test]
682 fn test_fragmentation_ratio_with_holes() {
683 let mut pool = GpuMemoryPool::new(1024);
684 let h1 = pool.alloc(100, Alignment::Bytes4).expect("alloc 1");
685 let _h2 = pool.alloc(100, Alignment::Bytes4).expect("alloc 2");
686 let h3 = pool.alloc(100, Alignment::Bytes4).expect("alloc 3");
687
688 pool.free(&h1);
690 pool.free(&h3);
691
692 let ratio = pool.fragmentation_ratio();
693 assert!(ratio > 0.0, "should have fragmentation, got {ratio}");
694 assert!(ratio <= 1.0);
695 }
696
697 #[test]
698 fn test_shrink_removes_empty_blocks() {
699 let mut pool = GpuMemoryPool::new(128);
700 let h1 = pool.alloc(128, Alignment::Bytes4).expect("alloc 1");
701 let _h2 = pool.alloc(128, Alignment::Bytes4).expect("alloc 2");
702 pool.free(&h1);
703
704 let removed = pool.shrink();
705 assert_eq!(removed, 1);
706 assert_eq!(pool.block_count(), 1);
707 }
708
709 #[test]
710 fn test_compact_returns_plan() {
711 let mut pool = GpuMemoryPool::new(1024);
712 let h1 = pool.alloc(100, Alignment::Bytes4).expect("alloc 1");
713 let _ = h1; let plan = pool.compact();
716 assert!(plan.is_some());
717 }
718
719 #[test]
720 fn test_compact_identifies_sparse_blocks() {
721 let mut pool = GpuMemoryPool::new(1024);
722 let h1 = pool.alloc(50, Alignment::Bytes4).expect("alloc");
724 let _h2 = pool.alloc(1024, Alignment::Bytes4).expect("alloc 2");
726
727 let _ = h1;
728 let plan = pool.compact().expect("compact should succeed");
729 let has_sparse = plan.migrations.iter().any(|m| m.source_block == 0);
731 assert!(has_sparse, "block 0 should be identified as sparse");
732 }
733
734 #[test]
735 fn test_defragment_updates_stats() {
736 let mut pool = GpuMemoryPool::new(256);
737 let h1 = pool.alloc(256, Alignment::Bytes4).expect("alloc 1");
738 let _h2 = pool.alloc(256, Alignment::Bytes4).expect("alloc 2");
739 pool.free(&h1);
740
741 let result = pool.defragment();
742 assert_eq!(result.blocks_removed, 1);
743 assert_eq!(pool.stats().block_count, 1);
744 }
745}