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_or_panic();
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 crate::extension::AsWgpu;
29use crate::types::GpuTexture;
30use crate::GraphicsContext;
31use ahash::HashMap;
32use std::sync::Arc;
33
34/// Rectangle for atlas packing (not coordinate-space aware).
35///
36/// This is a simple rectangle type used internally for texture atlas packing.
37/// It does not distinguish between logical/physical coordinates since atlas
38/// operations work in texture-local pixel space.
39#[derive(Debug, Clone, Copy, PartialEq)]
40pub struct AtlasRect {
41    pub x: f32,
42    pub y: f32,
43    pub width: f32,
44    pub height: f32,
45}
46
47impl AtlasRect {
48    /// Create a new atlas rectangle.
49    pub const fn new(x: f32, y: f32, width: f32, height: f32) -> Self {
50        Self { x, y, width, height }
51    }
52}
53
54/// Rectangle type for atlas packing (internal alias).
55type Rect = AtlasRect;
56
57/// Unique key for an atlas entry.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59pub struct AtlasKey(u64);
60
61impl AtlasKey {
62    /// Create a new atlas key from a string.
63    pub fn new(s: &str) -> Self {
64        use std::collections::hash_map::DefaultHasher;
65        use std::hash::{Hash, Hasher};
66
67        let mut hasher = DefaultHasher::new();
68        s.hash(&mut hasher);
69        Self(hasher.finish())
70    }
71
72    /// Create an atlas key from a u64.
73    pub fn from_u64(id: u64) -> Self {
74        Self(id)
75    }
76
77    /// Get the raw u64 value.
78    pub fn as_u64(&self) -> u64 {
79        self.0
80    }
81}
82
83/// An entry in the texture atlas.
84#[derive(Debug, Clone, Copy)]
85pub struct AtlasEntry {
86    /// Rectangle in pixel coordinates within the atlas.
87    pub rect: Rect,
88    /// Rectangle in normalized UV coordinates (0.0 to 1.0).
89    pub uv_rect: Rect,
90}
91
92impl AtlasEntry {
93    /// Create a new atlas entry.
94    pub fn new(rect: Rect, atlas_size: f32) -> Self {
95        let uv_rect = Rect {
96            x: rect.x / atlas_size,
97            y: rect.y / atlas_size,
98            width: rect.width / atlas_size,
99            height: rect.height / atlas_size,
100        };
101
102        Self { rect, uv_rect }
103    }
104}
105
106/// Rectangle packing algorithm.
107#[derive(Debug, Clone)]
108enum PackerNode {
109    /// Empty node that can be split.
110    Empty {
111        rect: Rect,
112    },
113    /// Filled node with an entry.
114    Filled {
115        rect: Rect,
116        key: AtlasKey,
117    },
118    /// Split node with two children.
119    Split {
120        rect: Rect,
121        left: Box<PackerNode>,
122        right: Box<PackerNode>,
123    },
124}
125
126impl PackerNode {
127    /// Create a new empty node.
128    fn new(rect: Rect) -> Self {
129        Self::Empty { rect }
130    }
131
132    /// Try to insert a rectangle into this node.
133    fn insert(&mut self, key: AtlasKey, width: f32, height: f32) -> Option<Rect> {
134        match self {
135            PackerNode::Empty { rect } => {
136                // Check if the rectangle fits
137                if width > rect.width || height > rect.height {
138                    return None;
139                }
140
141                // Perfect fit
142                if width == rect.width && height == rect.height {
143                    let result = *rect;
144                    *self = PackerNode::Filled { rect: *rect, key };
145                    return Some(result);
146                }
147
148                // Split the node
149                let rect_copy = *rect;
150
151                // Decide whether to split horizontally or vertically
152                let horizontal_waste = rect.width - width;
153                let vertical_waste = rect.height - height;
154
155                let (left_rect, right_rect) = if horizontal_waste > vertical_waste {
156                    // Split horizontally (left/right)
157                    (
158                        Rect {
159                            x: rect.x,
160                            y: rect.y,
161                            width,
162                            height: rect.height,
163                        },
164                        Rect {
165                            x: rect.x + width,
166                            y: rect.y,
167                            width: rect.width - width,
168                            height: rect.height,
169                        },
170                    )
171                } else {
172                    // Split vertically (top/bottom)
173                    (
174                        Rect {
175                            x: rect.x,
176                            y: rect.y,
177                            width: rect.width,
178                            height,
179                        },
180                        Rect {
181                            x: rect.x,
182                            y: rect.y + height,
183                            width: rect.width,
184                            height: rect.height - height,
185                        },
186                    )
187                };
188
189                let mut left = Box::new(PackerNode::new(left_rect));
190                let right = Box::new(PackerNode::new(right_rect));
191
192                // Insert into the left node
193                let result = left.insert(key, width, height);
194
195                *self = PackerNode::Split {
196                    rect: rect_copy,
197                    left,
198                    right,
199                };
200
201                result
202            }
203            PackerNode::Filled { .. } => None,
204            PackerNode::Split { left, right, .. } => {
205                // Try left first, then right
206                left.insert(key, width, height)
207                    .or_else(|| right.insert(key, width, height))
208            }
209        }
210    }
211}
212
213/// Texture atlas with dynamic rectangle packing.
214pub struct TextureAtlas {
215    /// GPU texture with cached view and metadata.
216    texture: GpuTexture,
217    entries: HashMap<AtlasKey, AtlasEntry>,
218    packer: PackerNode,
219    context: Arc<GraphicsContext>,
220    /// Pending uploads (key, data, rect)
221    pending_uploads: Vec<(AtlasKey, Vec<u8>, Rect)>,
222    dirty: bool,
223}
224
225impl TextureAtlas {
226    /// Create a new texture atlas.
227    ///
228    /// # Arguments
229    ///
230    /// * `context` - Graphics context
231    /// * `size` - Size of the atlas texture (must be power of 2)
232    /// * `format` - Texture format
233    pub fn new(context: Arc<GraphicsContext>, size: u32, format: wgpu::TextureFormat) -> Self {
234        let texture = GpuTexture::new_2d(
235            &context.device,
236            Some("TextureAtlas"),
237            size,
238            size,
239            format,
240            wgpu::TextureUsages::TEXTURE_BINDING
241                | wgpu::TextureUsages::COPY_DST
242                | wgpu::TextureUsages::RENDER_ATTACHMENT,
243        );
244
245        let packer = PackerNode::new(Rect {
246            x: 0.0,
247            y: 0.0,
248            width: size as f32,
249            height: size as f32,
250        });
251
252        Self {
253            texture,
254            entries: HashMap::default(),
255            packer,
256            context,
257            pending_uploads: Vec::new(),
258            dirty: false,
259        }
260    }
261
262    /// Insert an image into the atlas.
263    ///
264    /// Returns the atlas entry if the image was successfully inserted.
265    /// Returns None if there's no space in the atlas.
266    ///
267    /// # Arguments
268    ///
269    /// * `key` - Unique key for this image
270    /// * `image_data` - Raw pixel data (must match atlas format)
271    /// * `size` - Size of the image in pixels
272    pub fn insert(
273        &mut self,
274        key: AtlasKey,
275        image_data: &[u8],
276        width: u32,
277        height: u32,
278    ) -> Option<AtlasEntry> {
279        // Check if already exists
280        if let Some(entry) = self.entries.get(&key) {
281            return Some(*entry);
282        }
283
284        // Try to pack the rectangle
285        let rect = self.packer.insert(key, width as f32, height as f32)?;
286
287        // Create entry
288        let entry = AtlasEntry::new(rect, self.texture.width() as f32);
289        self.entries.insert(key, entry);
290
291        // Queue upload
292        self.pending_uploads
293            .push((key, image_data.to_vec(), rect));
294        self.dirty = true;
295
296        Some(entry)
297    }
298
299    /// Get an atlas entry by key.
300    pub fn get(&self, key: &AtlasKey) -> Option<&AtlasEntry> {
301        self.entries.get(key)
302    }
303
304    /// Check if the atlas contains a key.
305    pub fn contains(&self, key: &AtlasKey) -> bool {
306        self.entries.contains_key(key)
307    }
308
309    /// Upload all pending data to the GPU.
310    pub fn upload(&mut self) {
311        if !self.dirty {
312            return;
313        }
314
315        let format = self.texture.format();
316        for (_, data, rect) in &self.pending_uploads {
317            let bytes_per_pixel = match format {
318                wgpu::TextureFormat::Rgba8UnormSrgb | wgpu::TextureFormat::Rgba8Unorm => 4,
319                wgpu::TextureFormat::Bgra8UnormSrgb | wgpu::TextureFormat::Bgra8Unorm => 4,
320                wgpu::TextureFormat::R8Unorm => 1,
321                _ => 4, // Default to 4 bytes
322            };
323
324            self.context.queue.write_texture(
325                wgpu::TexelCopyTextureInfo {
326                    texture: self.texture.as_wgpu(),
327                    mip_level: 0,
328                    origin: wgpu::Origin3d {
329                        x: rect.x as u32,
330                        y: rect.y as u32,
331                        z: 0,
332                    },
333                    aspect: wgpu::TextureAspect::All,
334                },
335                data,
336                wgpu::TexelCopyBufferLayout {
337                    offset: 0,
338                    bytes_per_row: Some(rect.width as u32 * bytes_per_pixel),
339                    rows_per_image: Some(rect.height as u32),
340                },
341                wgpu::Extent3d {
342                    width: rect.width as u32,
343                    height: rect.height as u32,
344                    depth_or_array_layers: 1,
345                },
346            );
347        }
348
349        self.pending_uploads.clear();
350        self.dirty = false;
351    }
352
353    /// Get the texture view for binding.
354    pub fn texture_view(&self) -> &wgpu::TextureView {
355        self.texture.view()
356    }
357
358    /// Get the texture for advanced use cases.
359    pub fn texture(&self) -> &wgpu::Texture {
360        self.texture.as_wgpu()
361    }
362
363    /// Get the size of the atlas.
364    pub fn size(&self) -> u32 {
365        self.texture.width()
366    }
367
368    /// Get the texture format.
369    pub fn format(&self) -> wgpu::TextureFormat {
370        self.texture.format()
371    }
372
373    /// Get the number of entries in the atlas.
374    pub fn len(&self) -> usize {
375        self.entries.len()
376    }
377
378    /// Check if the atlas is empty.
379    pub fn is_empty(&self) -> bool {
380        self.entries.is_empty()
381    }
382
383    /// Clear all entries from the atlas.
384    pub fn clear(&mut self) {
385        self.entries.clear();
386        self.pending_uploads.clear();
387        let size = self.texture.width();
388        self.packer = PackerNode::new(Rect {
389            x: 0.0,
390            y: 0.0,
391            width: size as f32,
392            height: size as f32,
393        });
394        self.dirty = false;
395    }
396}
397
398#[cfg(test)]
399mod tests {
400    use super::*;
401
402    #[test]
403    fn test_atlas_key() {
404        let key1 = AtlasKey::new("test");
405        let key2 = AtlasKey::new("test");
406        let key3 = AtlasKey::new("other");
407
408        assert_eq!(key1, key2);
409        assert_ne!(key1, key3);
410    }
411
412    #[test]
413    fn test_atlas_entry_uv() {
414        let rect = Rect {
415            x: 0.0,
416            y: 0.0,
417            width: 64.0,
418            height: 64.0,
419        };
420        let entry = AtlasEntry::new(rect, 256.0);
421
422        assert_eq!(entry.uv_rect.x, 0.0);
423        assert_eq!(entry.uv_rect.y, 0.0);
424        assert_eq!(entry.uv_rect.width, 0.25);
425        assert_eq!(entry.uv_rect.height, 0.25);
426    }
427
428    #[test]
429    fn test_packer_insertion() {
430        let mut packer = PackerNode::new(Rect {
431            x: 0.0,
432            y: 0.0,
433            width: 256.0,
434            height: 256.0,
435        });
436
437        let key1 = AtlasKey::new("rect1");
438        let rect1 = packer.insert(key1, 64.0, 64.0);
439        assert!(rect1.is_some());
440
441        let key2 = AtlasKey::new("rect2");
442        let rect2 = packer.insert(key2, 32.0, 32.0);
443        assert!(rect2.is_some());
444
445        // Try to insert something too large
446        let key3 = AtlasKey::new("rect3");
447        let rect3 = packer.insert(key3, 512.0, 512.0);
448        assert!(rect3.is_none());
449    }
450
451    #[test]
452    fn test_atlas_basic() {
453        let context = GraphicsContext::new_owned_sync_or_panic();
454        let mut atlas = TextureAtlas::new(context, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
455
456        assert_eq!(atlas.size(), 256);
457        assert_eq!(atlas.len(), 0);
458        assert!(atlas.is_empty());
459
460        // Create a 32x32 red square
461        let mut image_data = vec![0u8; 32 * 32 * 4];
462        for i in 0..(32 * 32) {
463            image_data[i * 4] = 255; // R
464            image_data[i * 4 + 1] = 0; // G
465            image_data[i * 4 + 2] = 0; // B
466            image_data[i * 4 + 3] = 255; // A
467        }
468
469        let key = AtlasKey::new("red_square");
470        let entry = atlas.insert(key, &image_data, 32, 32);
471        assert!(entry.is_some());
472        assert_eq!(atlas.len(), 1);
473
474        // Check retrieval
475        let retrieved = atlas.get(&key);
476        assert!(retrieved.is_some());
477
478        // Upload to GPU
479        atlas.upload();
480    }
481
482    #[test]
483    fn test_atlas_multiple_inserts() {
484        let context = GraphicsContext::new_owned_sync_or_panic();
485        let mut atlas = TextureAtlas::new(context, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
486
487        // Insert multiple images
488        for i in 0..10 {
489            let image_data = vec![0u8; 16 * 16 * 4];
490            let key = AtlasKey::new(&format!("image_{}", i));
491            let entry = atlas.insert(key, &image_data, 16, 16);
492            assert!(entry.is_some());
493        }
494
495        assert_eq!(atlas.len(), 10);
496        atlas.upload();
497    }
498
499    #[test]
500    fn test_atlas_duplicate_key() {
501        let context = GraphicsContext::new_owned_sync_or_panic();
502        let mut atlas = TextureAtlas::new(context, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
503
504        let image_data = vec![0u8; 32 * 32 * 4];
505        let key = AtlasKey::new("duplicate");
506
507        let entry1 = atlas.insert(key, &image_data, 32, 32);
508        assert!(entry1.is_some());
509
510        let entry2 = atlas.insert(key, &image_data, 32, 32);
511        assert!(entry2.is_some());
512
513        // Should only have one entry
514        assert_eq!(atlas.len(), 1);
515    }
516
517    #[test]
518    fn test_atlas_clear() {
519        let context = GraphicsContext::new_owned_sync_or_panic();
520        let mut atlas = TextureAtlas::new(context, 256, wgpu::TextureFormat::Rgba8UnormSrgb);
521
522        let image_data = vec![0u8; 32 * 32 * 4];
523        let key = AtlasKey::new("test");
524        atlas.insert(key, &image_data, 32, 32);
525
526        assert_eq!(atlas.len(), 1);
527
528        atlas.clear();
529
530        assert_eq!(atlas.len(), 0);
531        assert!(atlas.is_empty());
532    }
533}