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 {
114 descriptors: Vec<Option<TextureDescriptor>>,
116 allocated_bytes: usize,
118 max_bytes: usize,
120 access_clock: u64,
122 last_access: Vec<u64>,
124}
125
126impl TexturePool {
127 #[must_use]
129 pub fn new(max_gb: f64) -> Self {
130 Self {
131 descriptors: Vec::new(),
132 allocated_bytes: 0,
133 max_bytes: (max_gb * 1024.0 * 1024.0 * 1024.0) as usize,
134 access_clock: 0,
135 last_access: Vec::new(),
136 }
137 }
138
139 pub fn allocate(&mut self, desc: TextureDescriptor) -> Option<usize> {
144 let bytes = desc.size_bytes();
145 if self.allocated_bytes + bytes > self.max_bytes {
146 return None;
147 }
148 self.access_clock += 1;
150 let ts = self.access_clock;
151 if let Some(idx) = self
152 .descriptors
153 .iter()
154 .position(std::option::Option::is_none)
155 {
156 self.descriptors[idx] = Some(desc);
157 self.last_access[idx] = ts;
158 self.allocated_bytes += bytes;
159 return Some(idx);
160 }
161 let idx = self.descriptors.len();
162 self.descriptors.push(Some(desc));
163 self.last_access.push(ts);
164 self.allocated_bytes += bytes;
165 Some(idx)
166 }
167
168 pub fn allocate_with_lru_eviction(&mut self, desc: TextureDescriptor) -> Option<usize> {
173 let bytes = desc.size_bytes();
174 if self.allocated_bytes + bytes <= self.max_bytes {
176 return self.allocate(desc);
177 }
178 loop {
180 if self.allocated_bytes + bytes <= self.max_bytes {
181 return self.allocate(desc);
182 }
183 let lru = self.lru_handle()?;
184 self.free(lru);
185 }
186 }
187
188 #[must_use]
191 pub fn lru_handle(&self) -> Option<usize> {
192 self.descriptors
193 .iter()
194 .enumerate()
195 .filter_map(|(i, slot)| slot.as_ref().map(|_| i))
196 .min_by_key(|&i| self.last_access[i])
197 }
198
199 pub fn touch(&mut self, handle: usize) {
201 if handle < self.descriptors.len() && self.descriptors[handle].is_some() {
202 self.access_clock += 1;
203 self.last_access[handle] = self.access_clock;
204 }
205 }
206
207 pub fn free(&mut self, id: usize) {
209 if let Some(slot) = self.descriptors.get_mut(id) {
210 if let Some(desc) = slot.take() {
211 let bytes = desc.size_bytes();
212 self.allocated_bytes = self.allocated_bytes.saturating_sub(bytes);
213 self.last_access[id] = 0;
214 }
215 }
216 }
217
218 #[must_use]
220 pub fn utilization(&self) -> f64 {
221 if self.max_bytes == 0 {
222 return 0.0;
223 }
224 self.allocated_bytes as f64 / self.max_bytes as f64
225 }
226
227 #[must_use]
229 pub fn live_count(&self) -> usize {
230 self.descriptors.iter().filter(|s| s.is_some()).count()
231 }
232
233 #[must_use]
235 pub fn allocated_bytes(&self) -> usize {
236 self.allocated_bytes
237 }
238
239 #[must_use]
241 pub fn max_bytes(&self) -> usize {
242 self.max_bytes
243 }
244}
245
246#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn test_rgba8_bytes_per_pixel() {
255 assert!((TextureFormat::Rgba8.bytes_per_pixel() - 4.0).abs() < f32::EPSILON);
256 }
257
258 #[test]
259 fn test_yuv_formats_are_yuv() {
260 assert!(TextureFormat::Yuv420.is_yuv());
261 assert!(TextureFormat::Nv12.is_yuv());
262 assert!(!TextureFormat::Rgba8.is_yuv());
263 }
264
265 #[test]
266 fn test_channel_counts() {
267 assert_eq!(TextureFormat::R8.channel_count(), 1);
268 assert_eq!(TextureFormat::Rg8.channel_count(), 2);
269 assert_eq!(TextureFormat::Rgba8.channel_count(), 4);
270 assert_eq!(TextureFormat::Yuv420.channel_count(), 3);
271 }
272
273 #[test]
274 fn test_descriptor_new_defaults() {
275 let d = TextureDescriptor::new(1920, 1080, TextureFormat::Rgba8);
276 assert_eq!(d.mip_levels, 1);
277 assert_eq!(d.array_layers, 1);
278 }
279
280 #[test]
281 fn test_descriptor_total_pixels() {
282 let d = TextureDescriptor::new(100, 200, TextureFormat::R8);
283 assert_eq!(d.total_pixels(), 20_000);
284 }
285
286 #[test]
287 fn test_descriptor_size_bytes_rgba8() {
288 let d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
289 assert_eq!(d.size_bytes(), 64);
291 }
292
293 #[test]
294 fn test_descriptor_size_bytes_with_mips() {
295 let mut d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
297 d.mip_levels = 3;
298 assert_eq!(d.size_bytes(), 84);
299 }
300
301 #[test]
302 fn test_pool_basic_allocation() {
303 let mut pool = TexturePool::new(1.0);
304 let desc = TextureDescriptor::new(64, 64, TextureFormat::Rgba8);
305 let handle = pool.allocate(desc);
306 assert!(handle.is_some());
307 assert_eq!(pool.live_count(), 1);
308 }
309
310 #[test]
311 fn test_pool_free_reduces_bytes() {
312 let mut pool = TexturePool::new(1.0);
313 let desc = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
314 let handle = pool.allocate(desc).expect("allocation should succeed");
315 let before = pool.allocated_bytes();
316 pool.free(handle);
317 assert!(pool.allocated_bytes() < before);
318 assert_eq!(pool.live_count(), 0);
319 }
320
321 #[test]
322 fn test_pool_reuses_freed_slot() {
323 let mut pool = TexturePool::new(1.0);
324 let d1 = TextureDescriptor::new(4, 4, TextureFormat::R8);
325 let h1 = pool.allocate(d1).expect("allocation should succeed");
326 pool.free(h1);
327 let d2 = TextureDescriptor::new(4, 4, TextureFormat::R8);
328 let h2 = pool.allocate(d2).expect("allocation should succeed");
329 assert_eq!(h1, h2);
330 }
331
332 #[test]
333 fn test_pool_budget_exceeded_returns_none() {
334 let mut pool = TexturePool::new(0.0);
336 pool.max_bytes = 1;
337 let desc = TextureDescriptor::new(1920, 1080, TextureFormat::Rgba8);
338 assert!(pool.allocate(desc).is_none());
339 }
340
341 #[test]
342 fn test_pool_utilization_after_alloc() {
343 let mut pool = TexturePool::new(0.0);
344 let desc = TextureDescriptor::new(4, 4, TextureFormat::Rgba8); pool.max_bytes = 128;
347 pool.allocate(desc).expect("allocation should succeed");
348 let util = pool.utilization();
349 assert!((util - 0.5).abs() < 1e-6, "expected 0.5, got {util}");
350 }
351
352 #[test]
355 fn test_lru_handle_on_empty_pool() {
356 let pool = TexturePool::new(1.0);
357 assert!(pool.lru_handle().is_none());
358 }
359
360 #[test]
361 fn test_lru_handle_returns_oldest() {
362 let mut pool = TexturePool::new(1.0);
363 let h0 = pool
364 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
365 .expect("alloc");
366 let h1 = pool
367 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
368 .expect("alloc");
369 assert_eq!(pool.lru_handle(), Some(h0));
371 pool.touch(h0);
373 assert_eq!(pool.lru_handle(), Some(h1));
374 let _ = h1; }
376
377 #[test]
378 fn test_touch_updates_lru_order() {
379 let mut pool = TexturePool::new(1.0);
380 let h0 = pool
381 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
382 .expect("alloc");
383 let h1 = pool
384 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
385 .expect("alloc");
386 let h2 = pool
387 .allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
388 .expect("alloc");
389 assert_eq!(pool.lru_handle(), Some(h0));
391 pool.touch(h0);
392 pool.touch(h1);
393 assert_eq!(pool.lru_handle(), Some(h2));
395 }
396
397 #[test]
398 fn test_allocate_with_lru_eviction_makes_space() {
399 let mut pool = TexturePool::new(0.0);
400 pool.max_bytes = 64;
402 let h0 = pool
403 .allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
404 .expect("first alloc should succeed");
405 assert_eq!(pool.live_count(), 1);
406 assert!(pool
408 .allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
409 .is_none());
410 let h1 = pool
412 .allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
413 .expect("lru eviction alloc should succeed");
414 assert_eq!(pool.live_count(), 1);
415 assert_eq!(h0, h1);
417 }
418
419 #[test]
420 fn test_allocate_with_lru_eviction_preserves_mru() {
421 let mut pool = TexturePool::new(0.0);
422 pool.max_bytes = 128;
424 let h0 = pool
425 .allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
426 .expect("alloc h0");
427 let _h1 = pool
428 .allocate(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
429 .expect("alloc h1");
430 pool.touch(h0);
432 let h2 = pool
434 .allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8))
435 .expect("lru eviction");
436 assert_eq!(h2, _h1);
438 assert_eq!(pool.live_count(), 2);
440 }
441
442 #[test]
443 fn test_lru_eviction_returns_none_when_budget_impossible() {
444 let mut pool = TexturePool::new(0.0);
445 pool.max_bytes = 16;
447 pool.allocate(TextureDescriptor::new(4, 4, TextureFormat::R8))
449 .expect("alloc small");
450 let result =
452 pool.allocate_with_lru_eviction(TextureDescriptor::new(4, 4, TextureFormat::Rgba8));
453 assert!(result.is_none());
454 }
455}