Skip to main content

astrelis_render/
atlas.rs

1//! Texture atlas with non-uniform rectangle packing.
2//!
3//! Provides efficient texture packing for UI elements, sprites, and other 2D graphics.
4//!
5//! # Example
6//!
7//! ```ignore
8//! use astrelis_render::{TextureAtlas, GraphicsContext};
9//!
10//! let context = GraphicsContext::new_owned_sync().expect("Failed to create graphics context");
11//! let mut atlas = TextureAtlas::new(context.clone(), 512, wgpu::TextureFormat::Rgba8UnormSrgb);
12//!
13//! // Insert images
14//! let key1 = AtlasKey::new("icon1");
15//! if let Some(entry) = atlas.insert(key1, &image_data, Vec2::new(32.0, 32.0)) {
16//!     println!("Inserted at UV: {:?}", entry.uv_rect);
17//! }
18//!
19//! // Upload to GPU
20//! atlas.upload(&context);
21//!
22//! // Retrieve UV coordinates
23//! if let Some(entry) = atlas.get(&key1) {
24//!     // Use entry.uv_rect for rendering
25//! }
26//! ```
27
28use 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/// Rectangle for atlas packing (not coordinate-space aware).
37///
38/// This is a simple rectangle type used internally for texture atlas packing.
39/// It does not distinguish between logical/physical coordinates since atlas
40/// operations work in texture-local pixel space.
41#[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    /// Create a new atlas rectangle.
51    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
52        Self { x, y, width, height }
53    }
54}
55
56/// Rectangle type for atlas packing (internal alias).
57type Rect = AtlasRect;
58
59/// Unique key for an atlas entry.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61pub struct AtlasKey(u64);
62
63impl AtlasKey {
64    /// Create a new atlas key from a string.
65    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    /// Create an atlas key from a u64.
75    pub fn from_u64(id: u64) -> Self {
76        Self(id)
77    }
78
79    /// Get the raw u64 value.
80    pub fn as_u64(&self) -> u64 {
81        self.0
82    }
83}
84
85/// An entry in the texture atlas.
86#[derive(Debug, Clone, Copy)]
87pub struct AtlasEntry {
88    /// Rectangle in pixel coordinates within the atlas.
89    pub rect: Rect,
90    /// Rectangle in normalized UV coordinates (0.0 to 1.0).
91    pub uv_rect: Rect,
92}
93
94impl AtlasEntry {
95    /// Create a new atlas entry.
96    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/// Rectangle packing algorithm.
109#[derive(Debug, Clone)]
110#[allow(dead_code)]
111enum PackerNode {
112    /// Empty node that can be split.
113    Empty {
114        rect: Rect,
115    },
116    /// Filled node with an entry.
117    Filled {
118        rect: Rect,
119        key: AtlasKey,
120    },
121    /// Split node with two children.
122    Split {
123        rect: Rect,
124        left: Box<PackerNode>,
125        right: Box<PackerNode>,
126    },
127}
128
129impl PackerNode {
130    /// Create a new empty node.
131    fn new(rect: Rect) -> Self {
132        Self::Empty { rect }
133    }
134
135    /// Try to insert a rectangle into this node.
136    fn insert(&mut self, key: AtlasKey, width: f32, height: f32) -> Option<Rect> {
137        match self {
138            PackerNode::Empty { rect } => {
139                // Check if the rectangle fits
140                if width > rect.width || height > rect.height {
141                    return None;
142                }
143
144                // Perfect fit
145                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                // Split the node
152                let rect_copy = *rect;
153
154                // Decide whether to split horizontally or vertically
155                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                    // Split horizontally (left/right)
160                    (
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                    // Split vertically (top/bottom)
176                    (
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                // Insert into the left node
196                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                // Try left first, then right
209                left.insert(key, width, height)
210                    .or_else(|| right.insert(key, width, height))
211            }
212        }
213    }
214}
215
216/// Texture atlas with dynamic rectangle packing.
217pub struct TextureAtlas {
218    /// GPU texture with cached view and metadata.
219    texture: GpuTexture,
220    entries: HashMap<AtlasKey, AtlasEntry>,
221    packer: PackerNode,
222    context: Arc<GraphicsContext>,
223    /// Pending uploads (key, data, rect)
224    pending_uploads: Vec<(AtlasKey, Vec<u8>, Rect)>,
225    dirty: bool,
226}
227
228impl TextureAtlas {
229    /// Create a new texture atlas.
230    ///
231    /// # Arguments
232    ///
233    /// * `context` - Graphics context
234    /// * `size` - Size of the atlas texture (must be power of 2)
235    /// * `format` - Texture format
236    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    /// Insert an image into the atlas.
267    ///
268    /// Returns the atlas entry if the image was successfully inserted.
269    /// Returns None if there's no space in the atlas.
270    ///
271    /// # Arguments
272    ///
273    /// * `key` - Unique key for this image
274    /// * `image_data` - Raw pixel data (must match atlas format)
275    /// * `size` - Size of the image in pixels
276    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        // Check if already exists
285        if let Some(entry) = self.entries.get(&key) {
286            return Some(*entry);
287        }
288
289        // Try to pack the rectangle
290        let rect = self.packer.insert(key, width as f32, height as f32)?;
291
292        // Create entry
293        let entry = AtlasEntry::new(rect, self.texture.width() as f32);
294        self.entries.insert(key, entry);
295
296        // Queue upload
297        self.pending_uploads
298            .push((key, image_data.to_vec(), rect));
299        self.dirty = true;
300
301        Some(entry)
302    }
303
304    /// Get an atlas entry by key.
305    pub fn get(&self, key: &AtlasKey) -> Option<&AtlasEntry> {
306        self.entries.get(key)
307    }
308
309    /// Check if the atlas contains a key.
310    pub fn contains(&self, key: &AtlasKey) -> bool {
311        self.entries.contains_key(key)
312    }
313
314    /// Upload all pending data to the GPU.
315    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, // Default to 4 bytes
328            };
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    /// Get the texture view for binding.
360    pub fn texture_view(&self) -> &wgpu::TextureView {
361        self.texture.view()
362    }
363
364    /// Get the texture for advanced use cases.
365    pub fn texture(&self) -> &wgpu::Texture {
366        self.texture.as_wgpu()
367    }
368
369    /// Get the size of the atlas.
370    pub fn size(&self) -> u32 {
371        self.texture.width()
372    }
373
374    /// Get the texture format.
375    pub fn format(&self) -> wgpu::TextureFormat {
376        self.texture.format()
377    }
378
379    /// Get the number of entries in the atlas.
380    pub fn len(&self) -> usize {
381        self.entries.len()
382    }
383
384    /// Check if the atlas is empty.
385    pub fn is_empty(&self) -> bool {
386        self.entries.is_empty()
387    }
388
389    /// Clear all entries from the atlas.
390    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        // Try to insert something too large
452        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        // Create a 32x32 red square
467        let mut image_data = vec![0u8; 32 * 32 * 4];
468        for i in 0..(32 * 32) {
469            image_data[i * 4] = 255; // R
470            image_data[i * 4 + 1] = 0; // G
471            image_data[i * 4 + 2] = 0; // B
472            image_data[i * 4 + 3] = 255; // A
473        }
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        // Check retrieval
481        let retrieved = atlas.get(&key);
482        assert!(retrieved.is_some());
483
484        // Upload to GPU
485        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        // Insert multiple images
494        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        // Should only have one entry
520        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}