1use astrelis_core::profiling::profile_function;
29
30use crate::extension::AsWgpu;
31use crate::types::GpuTexture;
32use crate::GraphicsContext;
33use ahash::HashMap;
34use std::sync::Arc;
35
36#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct AtlasRect {
43 pub x: f32,
44 pub y: f32,
45 pub width: f32,
46 pub height: f32,
47}
48
49impl AtlasRect {
50 pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
52 Self { x, y, width, height }
53 }
54}
55
56type Rect = AtlasRect;
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub struct AtlasKey(u64);
62
63impl AtlasKey {
64 pub fn new(s: &str) -> Self {
66 use std::collections::hash_map::DefaultHasher;
67 use std::hash::{Hash, Hasher};
68
69 let mut hasher = DefaultHasher::new();
70 s.hash(&mut hasher);
71 Self(hasher.finish())
72 }
73
74 pub fn from_u64(id: u64) -> Self {
76 Self(id)
77 }
78
79 pub fn as_u64(&self) -> u64 {
81 self.0
82 }
83}
84
85#[derive(Debug, Clone, Copy)]
87pub struct AtlasEntry {
88 pub rect: Rect,
90 pub uv_rect: Rect,
92}
93
94impl AtlasEntry {
95 pub fn new(rect: Rect, atlas_size: f32) -> Self {
97 let uv_rect = Rect {
98 x: rect.x / atlas_size,
99 y: rect.y / atlas_size,
100 width: rect.width / atlas_size,
101 height: rect.height / atlas_size,
102 };
103
104 Self { rect, uv_rect }
105 }
106}
107
108#[derive(Debug, Clone)]
110#[allow(dead_code)]
111enum PackerNode {
112 Empty {
114 rect: Rect,
115 },
116 Filled {
118 rect: Rect,
119 key: AtlasKey,
120 },
121 Split {
123 rect: Rect,
124 left: Box<PackerNode>,
125 right: Box<PackerNode>,
126 },
127}
128
129impl PackerNode {
130 fn new(rect: Rect) -> Self {
132 Self::Empty { rect }
133 }
134
135 fn insert(&mut self, key: AtlasKey, width: f32, height: f32) -> Option<Rect> {
137 match self {
138 PackerNode::Empty { rect } => {
139 if width > rect.width || height > rect.height {
141 return None;
142 }
143
144 if width == rect.width && height == rect.height {
146 let result = *rect;
147 *self = PackerNode::Filled { rect: *rect, key };
148 return Some(result);
149 }
150
151 let rect_copy = *rect;
153
154 let horizontal_waste = rect.width - width;
156 let vertical_waste = rect.height - height;
157
158 let (left_rect, right_rect) = if horizontal_waste > vertical_waste {
159 (
161 Rect {
162 x: rect.x,
163 y: rect.y,
164 width,
165 height: rect.height,
166 },
167 Rect {
168 x: rect.x + width,
169 y: rect.y,
170 width: rect.width - width,
171 height: rect.height,
172 },
173 )
174 } else {
175 (
177 Rect {
178 x: rect.x,
179 y: rect.y,
180 width: rect.width,
181 height,
182 },
183 Rect {
184 x: rect.x,
185 y: rect.y + height,
186 width: rect.width,
187 height: rect.height - height,
188 },
189 )
190 };
191
192 let mut left = Box::new(PackerNode::new(left_rect));
193 let right = Box::new(PackerNode::new(right_rect));
194
195 let result = left.insert(key, width, height);
197
198 *self = PackerNode::Split {
199 rect: rect_copy,
200 left,
201 right,
202 };
203
204 result
205 }
206 PackerNode::Filled { .. } => None,
207 PackerNode::Split { left, right, .. } => {
208 left.insert(key, width, height)
210 .or_else(|| right.insert(key, width, height))
211 }
212 }
213 }
214}
215
216pub struct TextureAtlas {
218 texture: GpuTexture,
220 entries: HashMap<AtlasKey, AtlasEntry>,
221 packer: PackerNode,
222 context: Arc<GraphicsContext>,
223 pending_uploads: Vec<(AtlasKey, Vec<u8>, Rect)>,
225 dirty: bool,
226}
227
228impl TextureAtlas {
229 pub fn new(context: Arc<GraphicsContext>, size: u32, format: wgpu::TextureFormat) -> Self {
237 profile_function!();
238 let texture = GpuTexture::new_2d(
239 context.device(),
240 Some("TextureAtlas"),
241 size,
242 size,
243 format,
244 wgpu::TextureUsages::TEXTURE_BINDING
245 | wgpu::TextureUsages::COPY_DST
246 | wgpu::TextureUsages::RENDER_ATTACHMENT,
247 );
248
249 let packer = PackerNode::new(Rect {
250 x: 0.0,
251 y: 0.0,
252 width: size as f32,
253 height: size as f32,
254 });
255
256 Self {
257 texture,
258 entries: HashMap::default(),
259 packer,
260 context,
261 pending_uploads: Vec::new(),
262 dirty: false,
263 }
264 }
265
266 pub fn insert(
277 &mut self,
278 key: AtlasKey,
279 image_data: &[u8],
280 width: u32,
281 height: u32,
282 ) -> Option<AtlasEntry> {
283 profile_function!();
284 if let Some(entry) = self.entries.get(&key) {
286 return Some(*entry);
287 }
288
289 let rect = self.packer.insert(key, width as f32, height as f32)?;
291
292 let entry = AtlasEntry::new(rect, self.texture.width() as f32);
294 self.entries.insert(key, entry);
295
296 self.pending_uploads
298 .push((key, image_data.to_vec(), rect));
299 self.dirty = true;
300
301 Some(entry)
302 }
303
304 pub fn get(&self, key: &AtlasKey) -> Option<&AtlasEntry> {
306 self.entries.get(key)
307 }
308
309 pub fn contains(&self, key: &AtlasKey) -> bool {
311 self.entries.contains_key(key)
312 }
313
314 pub fn upload(&mut self) {
316 profile_function!();
317 if !self.dirty {
318 return;
319 }
320
321 let format = self.texture.format();
322 for (_, data, rect) in &self.pending_uploads {
323 let bytes_per_pixel = match format {
324 wgpu::TextureFormat::Rgba8UnormSrgb | wgpu::TextureFormat::Rgba8Unorm => 4,
325 wgpu::TextureFormat::Bgra8UnormSrgb | wgpu::TextureFormat::Bgra8Unorm => 4,
326 wgpu::TextureFormat::R8Unorm => 1,
327 _ => 4, };
329
330 self.context.queue().write_texture(
331 wgpu::TexelCopyTextureInfo {
332 texture: self.texture.as_wgpu(),
333 mip_level: 0,
334 origin: wgpu::Origin3d {
335 x: rect.x as u32,
336 y: rect.y as u32,
337 z: 0,
338 },
339 aspect: wgpu::TextureAspect::All,
340 },
341 data,
342 wgpu::TexelCopyBufferLayout {
343 offset: 0,
344 bytes_per_row: Some(rect.width as u32 * bytes_per_pixel),
345 rows_per_image: Some(rect.height as u32),
346 },
347 wgpu::Extent3d {
348 width: rect.width as u32,
349 height: rect.height as u32,
350 depth_or_array_layers: 1,
351 },
352 );
353 }
354
355 self.pending_uploads.clear();
356 self.dirty = false;
357 }
358
359 pub fn texture_view(&self) -> &wgpu::TextureView {
361 self.texture.view()
362 }
363
364 pub fn texture(&self) -> &wgpu::Texture {
366 self.texture.as_wgpu()
367 }
368
369 pub fn size(&self) -> u32 {
371 self.texture.width()
372 }
373
374 pub fn format(&self) -> wgpu::TextureFormat {
376 self.texture.format()
377 }
378
379 pub fn len(&self) -> usize {
381 self.entries.len()
382 }
383
384 pub fn is_empty(&self) -> bool {
386 self.entries.is_empty()
387 }
388
389 pub fn clear(&mut self) {
391 self.entries.clear();
392 self.pending_uploads.clear();
393 let size = self.texture.width();
394 self.packer = PackerNode::new(Rect {
395 x: 0.0,
396 y: 0.0,
397 width: size as f32,
398 height: size as f32,
399 });
400 self.dirty = false;
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 #[test]
409 fn test_atlas_key() {
410 let key1 = AtlasKey::new("test");
411 let key2 = AtlasKey::new("test");
412 let key3 = AtlasKey::new("other");
413
414 assert_eq!(key1, key2);
415 assert_ne!(key1, key3);
416 }
417
418 #[test]
419 fn test_atlas_entry_uv() {
420 let rect = Rect {
421 x: 0.0,
422 y: 0.0,
423 width: 64.0,
424 height: 64.0,
425 };
426 let entry = AtlasEntry::new(rect, 256.0);
427
428 assert_eq!(entry.uv_rect.x, 0.0);
429 assert_eq!(entry.uv_rect.y, 0.0);
430 assert_eq!(entry.uv_rect.width, 0.25);
431 assert_eq!(entry.uv_rect.height, 0.25);
432 }
433
434 #[test]
435 fn test_packer_insertion() {
436 let mut packer = PackerNode::new(Rect {
437 x: 0.0,
438 y: 0.0,
439 width: 256.0,
440 height: 256.0,
441 });
442
443 let key1 = AtlasKey::new("rect1");
444 let rect1 = packer.insert(key1, 64.0, 64.0);
445 assert!(rect1.is_some());
446
447 let key2 = AtlasKey::new("rect2");
448 let rect2 = packer.insert(key2, 32.0, 32.0);
449 assert!(rect2.is_some());
450
451 let key3 = AtlasKey::new("rect3");
453 let rect3 = packer.insert(key3, 512.0, 512.0);
454 assert!(rect3.is_none());
455 }
456
457 #[test]
458 fn test_atlas_basic() {
459 let context = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
460 let mut atlas = TextureAtlas::new(context, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
461
462 assert_eq!(atlas.size(), 256);
463 assert_eq!(atlas.len(), 0);
464 assert!(atlas.is_empty());
465
466 let mut image_data = vec![0u8; 32 * 32 * 4];
468 for i in 0..(32 * 32) {
469 image_data[i * 4] = 255; image_data[i * 4 + 1] = 0; image_data[i * 4 + 2] = 0; image_data[i * 4 + 3] = 255; }
474
475 let key = AtlasKey::new("red_square");
476 let entry = atlas.insert(key, &image_data, 32, 32);
477 assert!(entry.is_some());
478 assert_eq!(atlas.len(), 1);
479
480 let retrieved = atlas.get(&key);
482 assert!(retrieved.is_some());
483
484 atlas.upload();
486 }
487
488 #[test]
489 fn test_atlas_multiple_inserts() {
490 let context = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
491 let mut atlas = TextureAtlas::new(context, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
492
493 for i in 0..10 {
495 let image_data = vec![0u8; 16 * 16 * 4];
496 let key = AtlasKey::new(&format!("image_{}", i));
497 let entry = atlas.insert(key, &image_data, 16, 16);
498 assert!(entry.is_some());
499 }
500
501 assert_eq!(atlas.len(), 10);
502 atlas.upload();
503 }
504
505 #[test]
506 fn test_atlas_duplicate_key() {
507 let context = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
508 let mut atlas = TextureAtlas::new(context, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
509
510 let image_data = vec![0u8; 32 * 32 * 4];
511 let key = AtlasKey::new("duplicate");
512
513 let entry1 = atlas.insert(key, &image_data, 32, 32);
514 assert!(entry1.is_some());
515
516 let entry2 = atlas.insert(key, &image_data, 32, 32);
517 assert!(entry2.is_some());
518
519 assert_eq!(atlas.len(), 1);
521 }
522
523 #[test]
524 fn test_atlas_clear() {
525 let context = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
526 let mut atlas = TextureAtlas::new(context, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
527
528 let image_data = vec![0u8; 32 * 32 * 4];
529 let key = AtlasKey::new("test");
530 atlas.insert(key, &image_data, 32, 32);
531
532 assert_eq!(atlas.len(), 1);
533
534 atlas.clear();
535
536 assert_eq!(atlas.len(), 0);
537 assert!(atlas.is_empty());
538 }
539}