1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum TextureFormat {
12 Rgba8,
14 Rgba16f,
16 Rgb10A2,
18 R8,
20 Rg8,
22 Yuv420,
24 Nv12,
26}
27
28impl TextureFormat {
29 #[must_use]
31 pub fn bytes_per_pixel(&self) -> f32 {
32 match self {
33 Self::Rgba8 | Self::Rgb10A2 => 4.0,
34 Self::Rgba16f => 8.0,
35 Self::R8 => 1.0,
36 Self::Rg8 => 2.0,
37 Self::Yuv420 | Self::Nv12 => 1.5,
38 }
39 }
40
41 #[must_use]
43 pub fn is_yuv(&self) -> bool {
44 matches!(self, Self::Yuv420 | Self::Nv12)
45 }
46
47 #[must_use]
49 pub fn channel_count(&self) -> u8 {
50 match self {
51 Self::R8 => 1,
52 Self::Rg8 => 2,
53 Self::Rgba8 | Self::Rgba16f | Self::Rgb10A2 => 4,
54 Self::Yuv420 | Self::Nv12 => 3,
55 }
56 }
57}
58
59#[derive(Debug, Clone)]
61pub struct TextureDescriptor {
62 pub width: u32,
64 pub height: u32,
66 pub format: TextureFormat,
68 pub mip_levels: u8,
70 pub array_layers: u16,
72}
73
74impl TextureDescriptor {
75 #[must_use]
77 pub fn new(width: u32, height: u32, format: TextureFormat) -> Self {
78 Self {
79 width,
80 height,
81 format,
82 mip_levels: 1,
83 array_layers: 1,
84 }
85 }
86
87 #[must_use]
92 pub fn size_bytes(&self) -> usize {
93 let bpp = self.format.bytes_per_pixel();
94 let layers = self.array_layers as usize;
95 let mut total_pixels: f64 = 0.0;
96 let (mut w, mut h) = (f64::from(self.width), f64::from(self.height));
97 for _ in 0..self.mip_levels {
98 total_pixels += w * h;
99 w = (w / 2.0).max(1.0);
100 h = (h / 2.0).max(1.0);
101 }
102 (total_pixels * f64::from(bpp) * layers as f64) as usize
103 }
104
105 #[must_use]
107 pub fn total_pixels(&self) -> u64 {
108 u64::from(self.width) * u64::from(self.height)
109 }
110}
111
112pub struct TexturePool {
115 descriptors: Vec<Option<TextureDescriptor>>,
117 allocated_bytes: usize,
119 pub(crate) max_bytes: usize,
121 max_textures: usize,
123 access_clock: u64,
125 last_access: Vec<u64>,
127}
128
129impl TexturePool {
130 #[must_use]
132 pub fn new(max_gb: f64) -> Self {
133 Self {
134 descriptors: Vec::new(),
135 allocated_bytes: 0,
136 max_bytes: (max_gb * 1024.0 * 1024.0 * 1024.0) as usize,
137 max_textures: 0,
138 access_clock: 0,
139 last_access: Vec::new(),
140 }
141 }
142
143 #[must_use]
149 pub fn with_capacity(max: usize) -> Self {
150 Self {
151 descriptors: Vec::with_capacity(max),
152 allocated_bytes: 0,
153 max_bytes: usize::MAX,
154 max_textures: max,
155 access_clock: 0,
156 last_access: Vec::with_capacity(max),
157 }
158 }
159
160 pub fn evict_lru(&mut self) -> usize {
165 let mut evicted = 0usize;
166 while self.max_textures > 0 && self.live_count() > self.max_textures {
167 match self.lru_handle() {
168 Some(h) => {
169 self.free(h);
170 evicted += 1;
171 }
172 None => break,
173 }
174 }
175 evicted
176 }
177
178 pub fn allocate(&mut self, desc: TextureDescriptor) -> Option<usize> {
183 let bytes = desc.size_bytes();
184 if self.allocated_bytes + bytes > self.max_bytes {
185 return None;
186 }
187 if self.max_textures > 0 && self.live_count() >= self.max_textures {
188 return None;
189 }
190 self.access_clock += 1;
192 let ts = self.access_clock;
193 if let Some(idx) = self
194 .descriptors
195 .iter()
196 .position(std::option::Option::is_none)
197 {
198 self.descriptors[idx] = Some(desc);
199 self.last_access[idx] = ts;
200 self.allocated_bytes += bytes;
201 return Some(idx);
202 }
203 let idx = self.descriptors.len();
204 self.descriptors.push(Some(desc));
205 self.last_access.push(ts);
206 self.allocated_bytes += bytes;
207 Some(idx)
208 }
209
210 pub fn allocate_with_lru_eviction(&mut self, desc: TextureDescriptor) -> Option<usize> {
217 let bytes = desc.size_bytes();
218 let count_ok = self.max_textures == 0 || self.live_count() < self.max_textures;
220 if self.allocated_bytes + bytes <= self.max_bytes && count_ok {
221 return self.allocate(desc);
222 }
223 loop {
225 let bytes_ok = self.allocated_bytes + bytes <= self.max_bytes;
226 let cnt_ok = self.max_textures == 0 || self.live_count() < self.max_textures;
227 if bytes_ok && cnt_ok {
228 return self.allocate(desc);
229 }
230 let lru = self.lru_handle()?;
231 self.free(lru);
232 }
233 }
234
235 #[must_use]
238 pub fn lru_handle(&self) -> Option<usize> {
239 self.descriptors
240 .iter()
241 .enumerate()
242 .filter_map(|(i, slot)| slot.as_ref().map(|_| i))
243 .min_by_key(|&i| self.last_access[i])
244 }
245
246 pub fn touch(&mut self, handle: usize) {
248 if handle < self.descriptors.len() && self.descriptors[handle].is_some() {
249 self.access_clock += 1;
250 self.last_access[handle] = self.access_clock;
251 }
252 }
253
254 pub fn free(&mut self, id: usize) {
256 if let Some(slot) = self.descriptors.get_mut(id) {
257 if let Some(desc) = slot.take() {
258 let bytes = desc.size_bytes();
259 self.allocated_bytes = self.allocated_bytes.saturating_sub(bytes);
260 self.last_access[id] = 0;
261 }
262 }
263 }
264
265 #[must_use]
267 pub fn utilization(&self) -> f64 {
268 if self.max_bytes == 0 {
269 return 0.0;
270 }
271 self.allocated_bytes as f64 / self.max_bytes as f64
272 }
273
274 #[must_use]
276 pub fn live_count(&self) -> usize {
277 self.descriptors.iter().filter(|s| s.is_some()).count()
278 }
279
280 #[must_use]
282 pub fn allocated_bytes(&self) -> usize {
283 self.allocated_bytes
284 }
285
286 #[must_use]
288 pub fn max_bytes(&self) -> usize {
289 self.max_bytes
290 }
291}
292
293#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_rgba8_bytes_per_pixel() {
302 assert!((TextureFormat::Rgba8.bytes_per_pixel() - 4.0).abs() < f32::EPSILON);
303 }
304
305 #[test]
306 fn test_yuv_formats_are_yuv() {
307 assert!(TextureFormat::Yuv420.is_yuv());
308 assert!(TextureFormat::Nv12.is_yuv());
309 assert!(!TextureFormat::Rgba8.is_yuv());
310 }
311
312 #[test]
313 fn test_channel_counts() {
314 assert_eq!(TextureFormat::R8.channel_count(), 1);
315 assert_eq!(TextureFormat::Rg8.channel_count(), 2);
316 assert_eq!(TextureFormat::Rgba8.channel_count(), 4);
317 assert_eq!(TextureFormat::Yuv420.channel_count(), 3);
318 }
319
320 #[test]
321 fn test_descriptor_new_defaults() {
322 let d = TextureDescriptor::new(1920, 1080, TextureFormat::Rgba8);
323 assert_eq!(d.mip_levels, 1);
324 assert_eq!(d.array_layers, 1);
325 }
326
327 #[test]
328 fn test_descriptor_total_pixels() {
329 let d = TextureDescriptor::new(100, 200, TextureFormat::R8);
330 assert_eq!(d.total_pixels(), 20_000);
331 }
332
333 #[test]
334 fn test_descriptor_size_bytes_rgba8() {
335 let d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
336 assert_eq!(d.size_bytes(), 64);
338 }
339
340 #[test]
341 fn test_descriptor_size_bytes_with_mips() {
342 let mut d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
344 d.mip_levels = 3;
345 assert_eq!(d.size_bytes(), 84);
346 }
347
348 #[test]
349 fn test_pool_basic_allocation() {
350 let mut pool = TexturePool::new(1.0);
351 let desc = TextureDescriptor::new(64, 64, TextureFormat::Rgba8);
352 let handle = pool.allocate(desc);
353 assert!(handle.is_some());
354 assert_eq!(pool.live_count(), 1);
355 }
356
357 #[test]
358 fn test_pool_free_reduces_bytes() {
359 let mut pool = TexturePool::new(1.0);
360 let desc = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
361 let handle = pool.allocate(desc).expect("allocation should succeed");
362 let before = pool.allocated_bytes();
363 pool.free(handle);
364 assert!(pool.allocated_bytes() < before);
365 assert_eq!(pool.live_count(), 0);
366 }
367
368 #[test]
369 fn test_pool_reuses_freed_slot() {
370 let mut pool = TexturePool::new(1.0);
371 let d1 = TextureDescriptor::new(4, 4, TextureFormat::R8);
372 let h1 = pool.allocate(d1).expect("allocation should succeed");
373 pool.free(h1);
374 let d2 = TextureDescriptor::new(4, 4, TextureFormat::R8);
375 let h2 = pool.allocate(d2).expect("allocation should succeed");
376 assert_eq!(h1, h2);
377 }
378
379 #[test]
380 fn test_pool_budget_exceeded_returns_none() {
381 let mut pool = TexturePool::new(0.0);
383 pool.max_bytes = 1;
384 let desc = TextureDescriptor::new(1920, 1080, TextureFormat::Rgba8);
385 assert!(pool.allocate(desc).is_none());
386 }
387
388 #[test]
389 fn test_pool_utilization_after_alloc() {
390 let mut pool = TexturePool::new(0.0);
391 let desc = TextureDescriptor::new(4, 4, TextureFormat::Rgba8); pool.max_bytes = 128;
394 pool.allocate(desc).expect("allocation should succeed");
395 let util = pool.utilization();
396 assert!((util - 0.5).abs() < 1e-6, "expected 0.5, got {util}");
397 }
398
399 #[test]
402 fn test_lru_handle_on_empty_pool() {
403 let pool = TexturePool::new(1.0);
404 assert!(pool.lru_handle().is_none());
405 }
406
407 #[test]
408 fn test_lru_handle_returns_oldest() {
409 let mut pool = TexturePool::new(1.0);
410 let h0 = pool
411 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
412 .expect("alloc");
413 let h1 = pool
414 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
415 .expect("alloc");
416 assert_eq!(pool.lru_handle(), Some(h0));
418 pool.touch(h0);
420 assert_eq!(pool.lru_handle(), Some(h1));
421 let _ = h1; }
423
424 #[test]
425 fn test_touch_updates_lru_order() {
426 let mut pool = TexturePool::new(1.0);
427 let h0 = pool
428 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
429 .expect("alloc");
430 let h1 = pool
431 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
432 .expect("alloc");
433 let h2 = pool
434 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
435 .expect("alloc");
436 assert_eq!(pool.lru_handle(), Some(h0));
438 pool.touch(h0);
439 pool.touch(h1);
440 assert_eq!(pool.lru_handle(), Some(h2));
442 }
443
444 #[test]
445 fn test_allocate_with_lru_eviction_makes_space() {
446 let mut pool = TexturePool::new(0.0);
447 pool.max_bytes = 64;
449 let h0 = pool
450 .allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
451 .expect("first alloc should succeed");
452 assert_eq!(pool.live_count(), 1);
453 assert!(pool
455 .allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
456 .is_none());
457 let h1 = pool
459 .allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
460 .expect("lru eviction alloc should succeed");
461 assert_eq!(pool.live_count(), 1);
462 assert_eq!(h0, h1);
464 }
465
466 #[test]
467 fn test_allocate_with_lru_eviction_preserves_mru() {
468 let mut pool = TexturePool::new(0.0);
469 pool.max_bytes = 128;
471 let h0 = pool
472 .allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
473 .expect("alloc h0");
474 let _h1 = pool
475 .allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
476 .expect("alloc h1");
477 pool.touch(h0);
479 let h2 = pool
481 .allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
482 .expect("lru eviction");
483 assert_eq!(h2, _h1);
485 assert_eq!(pool.live_count(), 2);
487 }
488
489 #[test]
490 fn test_lru_eviction_returns_none_when_budget_impossible() {
491 let mut pool = TexturePool::new(0.0);
492 pool.max_bytes = 16;
494 pool.allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
496 .expect("alloc small");
497 let result =
499 pool.allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8));
500 assert!(result.is_none());
501 }
502
503 #[test]
506 fn test_with_capacity_rejects_when_full() {
507 let mut pool = TexturePool::with_capacity(2);
508 let d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
509 assert!(
510 pool.allocate(d.clone()).is_some(),
511 "first alloc should succeed"
512 );
513 assert!(
514 pool.allocate(d.clone()).is_some(),
515 "second alloc should succeed"
516 );
517 assert!(
519 pool.allocate(d.clone()).is_none(),
520 "third alloc must fail (capacity = 2)"
521 );
522 }
523
524 #[test]
525 fn test_evict_lru_reduces_count_to_capacity() {
526 let mut pool = TexturePool::with_capacity(2);
527 let d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
528 pool.max_textures = 0; pool.allocate(d.clone()).expect("alloc 1");
531 pool.allocate(d.clone()).expect("alloc 2");
532 pool.allocate(d.clone()).expect("alloc 3");
533 assert_eq!(pool.live_count(), 3);
534
535 pool.max_textures = 2; let evicted = pool.evict_lru();
537 assert_eq!(
538 evicted, 1,
539 "one texture should be evicted to reach capacity 2"
540 );
541 assert_eq!(pool.live_count(), 2);
542 }
543
544 #[test]
545 fn test_evict_lru_correct_order() {
546 let mut pool = TexturePool::with_capacity(3);
547 let d = TextureDescriptor::new(4, 4, TextureFormat::R8);
548 pool.max_textures = 0;
550 let h0 = pool.allocate(d.clone()).expect("h0");
551 let h1 = pool.allocate(d.clone()).expect("h1");
552 let h2 = pool.allocate(d.clone()).expect("h2");
553 pool.touch(h0);
555 pool.touch(h1);
556 pool.max_textures = 2;
557 let evicted = pool.evict_lru();
558 assert_eq!(evicted, 1, "one eviction expected");
559 assert!(
561 pool.descriptors[h2].is_none(),
562 "h2 should have been evicted (LRU)"
563 );
564 assert!(pool.descriptors[h0].is_some(), "h0 should still be alive");
565 assert!(pool.descriptors[h1].is_some(), "h1 should still be alive");
566 }
567
568 #[test]
569 fn test_evict_lru_noop_when_under_capacity() {
570 let mut pool = TexturePool::with_capacity(5);
571 let d = TextureDescriptor::new(4, 4, TextureFormat::R8);
572 pool.allocate(d.clone()).expect("alloc");
573 pool.allocate(d.clone()).expect("alloc");
574 let evicted = pool.evict_lru();
576 assert_eq!(evicted, 0, "no eviction expected when under capacity");
577 }
578
579 #[test]
580 fn test_evict_lru_on_empty_pool() {
581 let mut pool = TexturePool::with_capacity(2);
582 let evicted = pool.evict_lru();
583 assert_eq!(evicted, 0, "no eviction on empty pool");
584 }
585
586 #[test]
587 fn test_with_capacity_allocate_after_evict() {
588 let mut pool = TexturePool::with_capacity(1);
589 let d = TextureDescriptor::new(4, 4, TextureFormat::R8);
590 let h0 = pool.allocate(d.clone()).expect("first alloc");
591 assert!(pool.allocate(d.clone()).is_none());
593 let h1 = pool
595 .allocate_with_lru_eviction(d.clone())
596 .expect("evict+alloc");
597 assert_eq!(pool.live_count(), 1, "still 1 live after evict+alloc");
598 assert_eq!(h0, h1, "freed slot should be reused");
600 }
601}