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#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
315 fn test_gpu_buffer_new() {
316 let buf = GpuBuffer::new(1, 1024, 64);
317 assert_eq!(buf.id, 1);
318 assert_eq!(buf.size_bytes, 1024);
319 assert_eq!(buf.alignment, 64);
320 assert_eq!(buf.as_slice().len(), 1024);
321 assert!(!buf.is_in_use());
322 }
323
324 #[test]
325 fn test_gpu_buffer_fill() {
326 let mut buf = GpuBuffer::new(2, 16, 4);
327 buf.fill(0xAB);
328 assert!(buf.as_slice().iter().all(|&b| b == 0xAB));
329 }
330
331 #[test]
332 fn test_gpu_buffer_as_mut_slice() {
333 let mut buf = GpuBuffer::new(3, 8, 1);
334 buf.as_mut_slice()[0] = 42;
335 assert_eq!(buf.as_slice()[0], 42);
336 }
337
338 #[test]
341 fn test_pool_new_empty() {
342 let pool = BufferPool::new(1024 * 1024);
343 let stats = pool.stats();
344 assert_eq!(stats.total_buffers, 0);
345 assert_eq!(stats.reuse_rate, 0.0);
346 }
347
348 #[test]
351 fn test_pool_acquire_and_release() {
352 let mut pool = BufferPool::new(1024 * 1024);
353 let id = pool.acquire(256, 4).expect("acquire failed");
354 assert!(pool.get(id).expect("missing").is_in_use());
355
356 let released = pool.release(id);
357 assert!(released, "release should succeed");
358 assert!(!pool.get(id).expect("missing").is_in_use());
359 }
360
361 #[test]
362 fn test_pool_reuse() {
363 let mut pool = BufferPool::new(1024 * 1024);
364 let id1 = pool.acquire(512, 4).expect("first acquire");
365 pool.release(id1);
366 let id2 = pool.acquire(512, 4).expect("second acquire");
367 assert_eq!(id1, id2, "expected buffer reuse");
369 let stats = pool.stats();
370 assert!(stats.reuse_rate > 0.0);
371 }
372
373 #[test]
374 fn test_pool_smallest_compatible_preferred() {
375 let mut pool = BufferPool::new(4 * 1024 * 1024);
376 let big = pool.acquire(4096, 4).expect("big");
378 let small = pool.acquire(256, 4).expect("small");
379 pool.release(big);
380 pool.release(small);
381 let id = pool.acquire(128, 4).expect("reacquire");
383 assert_eq!(id, small, "should prefer smaller buffer");
384 }
385
386 #[test]
387 fn test_pool_budget_exceeded() {
388 let mut pool = BufferPool::new(100);
389 let id = pool.acquire(80, 1).expect("first");
391 let result = pool.acquire(80, 1);
393 assert!(result.is_none(), "should fail over budget");
394 pool.release(id);
395 }
396
397 #[test]
398 fn test_pool_release_unknown_id() {
399 let mut pool = BufferPool::new(1024);
400 assert!(
401 !pool.release(9999),
402 "releasing unknown id should return false"
403 );
404 }
405
406 #[test]
407 fn test_pool_get_missing() {
408 let pool = BufferPool::new(1024);
409 assert!(pool.get(42).is_none());
410 }
411
412 #[test]
415 fn test_pool_get_mut_write() {
416 let mut pool = BufferPool::new(1024 * 1024);
417 let id = pool.acquire(64, 1).expect("acquire");
418 {
419 let buf = pool.get_mut(id).expect("get_mut");
420 buf.as_mut_slice()[0] = 0xFF;
421 }
422 assert_eq!(pool.get(id).expect("get").as_slice()[0], 0xFF);
423 }
424
425 #[test]
428 fn test_pool_stats_in_use_count() {
429 let mut pool = BufferPool::new(1024 * 1024);
430 let id1 = pool.acquire(128, 1).expect("a1");
431 let _id2 = pool.acquire(128, 1).expect("a2");
432 pool.release(id1);
433 let stats = pool.stats();
434 assert_eq!(stats.total_buffers, 2);
435 assert_eq!(stats.in_use_buffers, 1);
436 assert_eq!(stats.available_buffers, 1);
437 }
438
439 #[test]
442 fn test_pool_defragment_keeps_in_use() {
443 let mut pool = BufferPool::new(1024 * 1024);
444 let id = pool.acquire(64, 1).expect("acquire");
445 pool.defragment();
447 assert!(
448 pool.get(id).is_some(),
449 "in-use buffer should not be evicted"
450 );
451 }
452
453 #[test]
454 fn test_pool_defragment_recently_released_kept() {
455 let mut pool = BufferPool::new(1024 * 1024);
456 let id = pool.acquire(64, 1).expect("acquire");
457 pool.release(id);
458 pool.defragment();
460 assert!(
461 pool.get(id).is_some(),
462 "recently released buffer should survive"
463 );
464 }
465}