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}
121
122impl TexturePool {
123 #[must_use]
125 pub fn new(max_gb: f64) -> Self {
126 Self {
127 descriptors: Vec::new(),
128 allocated_bytes: 0,
129 max_bytes: (max_gb * 1024.0 * 1024.0 * 1024.0) as usize,
130 }
131 }
132
133 pub fn allocate(&mut self, desc: TextureDescriptor) -> Option<usize> {
138 let bytes = desc.size_bytes();
139 if self.allocated_bytes + bytes > self.max_bytes {
140 return None;
141 }
142 if let Some(idx) = self
144 .descriptors
145 .iter()
146 .position(std::option::Option::is_none)
147 {
148 self.descriptors[idx] = Some(desc);
149 self.allocated_bytes += bytes;
150 return Some(idx);
151 }
152 let idx = self.descriptors.len();
153 self.descriptors.push(Some(desc));
154 self.allocated_bytes += bytes;
155 Some(idx)
156 }
157
158 pub fn free(&mut self, id: usize) {
160 if let Some(slot) = self.descriptors.get_mut(id) {
161 if let Some(desc) = slot.take() {
162 let bytes = desc.size_bytes();
163 self.allocated_bytes = self.allocated_bytes.saturating_sub(bytes);
164 }
165 }
166 }
167
168 #[must_use]
170 pub fn utilization(&self) -> f64 {
171 if self.max_bytes == 0 {
172 return 0.0;
173 }
174 self.allocated_bytes as f64 / self.max_bytes as f64
175 }
176
177 #[must_use]
179 pub fn live_count(&self) -> usize {
180 self.descriptors.iter().filter(|s| s.is_some()).count()
181 }
182
183 #[must_use]
185 pub fn allocated_bytes(&self) -> usize {
186 self.allocated_bytes
187 }
188
189 #[must_use]
191 pub fn max_bytes(&self) -> usize {
192 self.max_bytes
193 }
194}
195
196#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_rgba8_bytes_per_pixel() {
205 assert!((TextureFormat::Rgba8.bytes_per_pixel() - 4.0).abs() < f32::EPSILON);
206 }
207
208 #[test]
209 fn test_yuv_formats_are_yuv() {
210 assert!(TextureFormat::Yuv420.is_yuv());
211 assert!(TextureFormat::Nv12.is_yuv());
212 assert!(!TextureFormat::Rgba8.is_yuv());
213 }
214
215 #[test]
216 fn test_channel_counts() {
217 assert_eq!(TextureFormat::R8.channel_count(), 1);
218 assert_eq!(TextureFormat::Rg8.channel_count(), 2);
219 assert_eq!(TextureFormat::Rgba8.channel_count(), 4);
220 assert_eq!(TextureFormat::Yuv420.channel_count(), 3);
221 }
222
223 #[test]
224 fn test_descriptor_new_defaults() {
225 let d = TextureDescriptor::new(1920, 1080, TextureFormat::Rgba8);
226 assert_eq!(d.mip_levels, 1);
227 assert_eq!(d.array_layers, 1);
228 }
229
230 #[test]
231 fn test_descriptor_total_pixels() {
232 let d = TextureDescriptor::new(100, 200, TextureFormat::R8);
233 assert_eq!(d.total_pixels(), 20_000);
234 }
235
236 #[test]
237 fn test_descriptor_size_bytes_rgba8() {
238 let d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
239 assert_eq!(d.size_bytes(), 64);
241 }
242
243 #[test]
244 fn test_descriptor_size_bytes_with_mips() {
245 let mut d = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
247 d.mip_levels = 3;
248 assert_eq!(d.size_bytes(), 84);
249 }
250
251 #[test]
252 fn test_pool_basic_allocation() {
253 let mut pool = TexturePool::new(1.0);
254 let desc = TextureDescriptor::new(64, 64, TextureFormat::Rgba8);
255 let handle = pool.allocate(desc);
256 assert!(handle.is_some());
257 assert_eq!(pool.live_count(), 1);
258 }
259
260 #[test]
261 fn test_pool_free_reduces_bytes() {
262 let mut pool = TexturePool::new(1.0);
263 let desc = TextureDescriptor::new(4, 4, TextureFormat::Rgba8);
264 let handle = pool.allocate(desc).unwrap();
265 let before = pool.allocated_bytes();
266 pool.free(handle);
267 assert!(pool.allocated_bytes() < before);
268 assert_eq!(pool.live_count(), 0);
269 }
270
271 #[test]
272 fn test_pool_reuses_freed_slot() {
273 let mut pool = TexturePool::new(1.0);
274 let d1 = TextureDescriptor::new(4, 4, TextureFormat::R8);
275 let h1 = pool.allocate(d1).unwrap();
276 pool.free(h1);
277 let d2 = TextureDescriptor::new(4, 4, TextureFormat::R8);
278 let h2 = pool.allocate(d2).unwrap();
279 assert_eq!(h1, h2);
280 }
281
282 #[test]
283 fn test_pool_budget_exceeded_returns_none() {
284 let mut pool = TexturePool::new(0.0);
286 pool.max_bytes = 1;
287 let desc = TextureDescriptor::new(1920, 1080, TextureFormat::Rgba8);
288 assert!(pool.allocate(desc).is_none());
289 }
290
291 #[test]
292 fn test_pool_utilization_after_alloc() {
293 let mut pool = TexturePool::new(0.0);
294 let desc = TextureDescriptor::new(4, 4, TextureFormat::Rgba8); pool.max_bytes = 128;
297 pool.allocate(desc).unwrap();
298 let util = pool.utilization();
299 assert!((util - 0.5).abs() < 1e-6, "expected 0.5, got {util}");
300 }
301}