renderling/atlas/
cpu.rs

1use core::ops::Deref;
2use glam::UVec2;
3use image::RgbaImage;
4use snafu::prelude::*;
5use std::sync::{Arc, RwLock};
6
7use crate::{
8    slab::{Hybrid, IsBuffer, SlabAllocator, UpdatesSlab},
9    texture::Texture,
10};
11
12use super::{
13    atlas_image::{convert_to_rgba8_bytes, AtlasImage},
14    AtlasTexture,
15};
16
17pub(crate) const ATLAS_SUGGESTED_SIZE: u32 = 2048;
18pub(crate) const ATLAS_SUGGESTED_LAYERS: u32 = 8;
19
20/// The count at which a `AtlasFrame` will be automatically removed from the
21/// atlas and recycled.
22///
23/// * +1 for the clone in [`Atlas`]'s layers
24/// * +1 for the clone in [`Stage`]'s [`SlabAllocator`]'s update sources, needed
25///   to update the GPU
26pub const ENTRY_STRONG_COUNT_LOWER_BOUND: usize = 2;
27
28#[derive(Debug, Snafu)]
29pub enum AtlasError {
30    #[snafu(display(
31        "Cannot pack textures.\natlas_size: {size:#?}\nimage_sizes: {image_sizes:#?}"
32    ))]
33    CannotPackTextures {
34        size: wgpu::Extent3d,
35        image_sizes: Vec<UVec2>,
36    },
37
38    #[snafu(display("Missing layer {index}"))]
39    MissingLayer { index: u32, images: Vec<AtlasImage> },
40
41    #[snafu(display("Atlas size is invalid: {size:?}"))]
42    Size { size: wgpu::Extent3d },
43}
44
45pub(crate) fn check_size(size: wgpu::Extent3d) -> Result<(), AtlasError> {
46    let conditions = size.depth_or_array_layers >= 2
47        && size.width == size.height
48        && (size.width & (size.width - 1)) == 0;
49    if !conditions {
50        return SizeSnafu { size }.fail();
51    }
52
53    Ok(())
54}
55
56fn fan_split_n<T>(n: usize, input: impl IntoIterator<Item = T>) -> Vec<Vec<T>> {
57    if n == 0 {
58        return vec![];
59    }
60    let mut output = vec![];
61    for _ in 0..n {
62        output.push(vec![]);
63    }
64    let mut i = 0;
65    for item in input.into_iter() {
66        // UNWRAP: safe because i % n
67        output
68            .get_mut(i)
69            .unwrap_or_else(|| panic!("could not unwrap i:{i} n:{n}"))
70            .push(item);
71        i = (i + 1) % n;
72    }
73    output
74}
75
76/// A texture atlas packing, before it is committed to the GPU.
77#[derive(Clone)]
78enum Packing<'a> {
79    /// A new packing.
80    ///
81    /// This image does not yet live on the GPU
82    Img {
83        /// Index of the layer within the atlas.
84        layer_index: u32,
85        /// Index of the frame within the layer.
86        frame_index: u32,
87        /// Image bytes, etc
88        image: &'a AtlasImage,
89    },
90    /// A previous packing.
91    ///
92    /// This image has already been staged on the GPU.
93    GpuImg(Hybrid<AtlasTexture>),
94}
95
96impl<'a> Packing<'a> {
97    pub fn size(&self) -> UVec2 {
98        match self {
99            Packing::Img { image, .. } => image.size,
100            Packing::GpuImg(entry) => entry.get().size_px,
101        }
102    }
103}
104
105#[derive(Clone, Default, Debug)]
106pub struct Layer {
107    pub frames: Vec<Hybrid<AtlasTexture>>,
108}
109
110/// A texture atlas, used to store all the textures in a scene.
111///
112/// Clones of `Atlas` all point to the same internal data.
113#[derive(Clone)]
114pub struct Atlas {
115    texture_array: Arc<RwLock<Texture>>,
116    layers: Arc<RwLock<Vec<Layer>>>,
117}
118
119impl Atlas {
120    /// Create a new atlas with `size` and `num_layers` layers.
121    ///
122    /// `size` **must be a power of two**.
123    ///
124    /// ## Panics
125    /// Panics if `size` is _not_ a power of two.
126    fn new_with_texture(texture: Texture) -> Self {
127        let num_layers = texture.texture.size().depth_or_array_layers as usize;
128        let layers = vec![Layer::default(); num_layers];
129        log::trace!("created atlas with {num_layers} layers");
130        Atlas {
131            layers: Arc::new(RwLock::new(layers)),
132            texture_array: Arc::new(RwLock::new(texture)),
133        }
134    }
135
136    /// Create the initial texture to use.
137    fn create_texture(
138        device: &wgpu::Device,
139        queue: &wgpu::Queue,
140        size: wgpu::Extent3d,
141    ) -> Result<Texture, AtlasError> {
142        check_size(size)?;
143        let texture = device.create_texture(&wgpu::TextureDescriptor {
144            label: Some("atlas texture"),
145            size,
146            mip_level_count: 1,
147            sample_count: 1,
148            dimension: wgpu::TextureDimension::D2,
149            format: wgpu::TextureFormat::Rgba8Unorm,
150            usage: wgpu::TextureUsages::TEXTURE_BINDING
151                | wgpu::TextureUsages::COPY_DST
152                | wgpu::TextureUsages::COPY_SRC,
153            view_formats: &[],
154        });
155
156        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
157            label: Some("create atlas texture array"),
158        });
159        if device.features().contains(wgpu::Features::CLEAR_TEXTURE) {
160            encoder.clear_texture(
161                &texture,
162                &wgpu::ImageSubresourceRange {
163                    aspect: wgpu::TextureAspect::All,
164                    base_mip_level: 0,
165                    mip_level_count: None,
166                    base_array_layer: 0,
167                    array_layer_count: None,
168                },
169            );
170        }
171        queue.submit(Some(encoder.finish()));
172
173        let sampler_desc = wgpu::SamplerDescriptor {
174            address_mode_u: wgpu::AddressMode::ClampToEdge,
175            address_mode_v: wgpu::AddressMode::ClampToEdge,
176            address_mode_w: wgpu::AddressMode::ClampToEdge,
177            mag_filter: wgpu::FilterMode::Nearest,
178            min_filter: wgpu::FilterMode::Nearest,
179            mipmap_filter: wgpu::FilterMode::Nearest,
180            ..Default::default()
181        };
182
183        Ok(Texture::from_wgpu_tex(
184            device,
185            texture,
186            Some(sampler_desc),
187            None,
188        ))
189    }
190
191    /// Create a new atlas.
192    ///
193    /// Size _must_ be a power of two.
194    ///
195    /// ## Panics
196    /// Panics if `size` is not a power of two.
197    pub fn new(
198        device: &wgpu::Device,
199        queue: &wgpu::Queue,
200        size: wgpu::Extent3d,
201    ) -> Result<Self, AtlasError> {
202        log::trace!("creating new atlas with dimensions {size:?}");
203        let texture = Self::create_texture(device, queue, size)?;
204        Ok(Self::new_with_texture(texture))
205    }
206
207    pub fn len(&self) -> usize {
208        // UNWRAP: panic on purpose
209        let layers = self.layers.read().unwrap();
210        layers.iter().map(|layer| layer.frames.len()).sum::<usize>()
211    }
212
213    pub fn is_empty(&self) -> bool {
214        // UNWRAP: panic on purpose
215        self.len() == 0
216    }
217
218    /// Returns a clone of the current atlas texture array.
219    pub fn get_texture(&self) -> Texture {
220        // UNWRAP: panic on purpose
221        self.texture_array.read().unwrap().clone()
222    }
223
224    pub fn get_layers(&self) -> impl Deref<Target = Vec<Layer>> + '_ {
225        // UNWRAP: panic on purpose
226        self.layers.read().unwrap()
227    }
228
229    /// Reset this atlas with all new images.
230    ///
231    /// Any existing `Hybrid<AtlasTexture>`s will be invalidated.
232    pub fn set_images(
233        &self,
234        device: &wgpu::Device,
235        queue: &wgpu::Queue,
236        slab: &SlabAllocator<impl IsBuffer>,
237        images: impl IntoIterator<Item = impl Into<AtlasImage>>,
238    ) -> Result<Vec<Hybrid<AtlasTexture>>, AtlasError> {
239        log::debug!("setting images");
240        {
241            // UNWRAP: panic on purpose
242            let texture = self.texture_array.read().unwrap();
243            let mut guard = self.layers.write().unwrap();
244            let layers: &mut Vec<_> = guard.as_mut();
245            let new_layers =
246                vec![Layer::default(); texture.texture.size().depth_or_array_layers as usize];
247            let _old_layers = std::mem::replace(layers, new_layers);
248        }
249        self.add_images(device, queue, slab, images)
250    }
251
252    pub fn get_size(&self) -> wgpu::Extent3d {
253        // UNWRAP: POP
254        self.texture_array.read().unwrap().texture.size()
255    }
256
257    pub fn add_images(
258        &self,
259        device: &wgpu::Device,
260        queue: &wgpu::Queue,
261        slab: &SlabAllocator<impl IsBuffer>,
262        images: impl IntoIterator<Item = impl Into<AtlasImage>>,
263    ) -> Result<Vec<Hybrid<AtlasTexture>>, AtlasError> {
264        // UNWRAP: POP
265        let mut layers = self.layers.write().unwrap();
266
267        let mut images = images
268            .into_iter()
269            .enumerate()
270            .map(|(i, img)| {
271                let img = img.into();
272                log::trace!("adding image{i}: {:?}", img.size);
273                (i, img)
274            })
275            .collect::<Vec<_>>();
276        images.sort_by(|a, b| (a.1.size.x * a.1.size.y).cmp(&(b.1.size.x * b.1.size.y)));
277
278        log::trace!("adding {} images to {} layers", images.len(), layers.len());
279        let layer_additions: Vec<Vec<(usize, AtlasImage)>> = fan_split_n(layers.len(), images);
280        log::trace!(
281            "extending the atlas by '{}'",
282            layer_additions
283                .iter()
284                .map(|v| format!("{}", v.len()))
285                .collect::<Vec<_>>()
286                .join(",")
287        );
288
289        // UNWRAP: POP
290        let mut texture_array = self.texture_array.write().unwrap();
291        let extent = texture_array.texture.size();
292        let new_texture_array = Self::create_texture(device, queue, extent)?;
293
294        let mut output = vec![];
295        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
296            label: Some("atlas add images"),
297        });
298
299        // for each new layer addition, attempt to repack that layer
300        for (i, additions) in layer_additions.into_iter().enumerate() {
301            if additions.is_empty() {
302                continue;
303            }
304            log::trace!("repacking layer {i} and adding {} images", additions.len());
305            // UNWRAP: safe because we know this index exists from our calc above
306            let layer = layers.get_mut(i).unwrap();
307            let images_starting_texture_index = layer.frames.len() as u32;
308            let maybe_items = crunch::pack_into_po2(
309                extent.width as usize,
310                layer
311                    .frames
312                    .iter()
313                    .map(|entry| Packing::GpuImg(entry.clone()))
314                    .chain(additions.iter().map(|(j, image)| Packing::Img {
315                        layer_index: i as u32,
316                        frame_index: images_starting_texture_index + *j as u32,
317                        image,
318                    }))
319                    .map(|packing: Packing| {
320                        let size = packing.size();
321                        crunch::Item::new(
322                            packing,
323                            size.x as usize,
324                            size.y as usize,
325                            crunch::Rotation::None,
326                        )
327                    }),
328            )
329            .ok();
330            if let Some(items) = maybe_items {
331                for crunch::PackedItem { data: item, rect } in items.items.into_iter() {
332                    let offset_px = UVec2::new(rect.x as u32, rect.y as u32);
333                    let size_px = UVec2::new(rect.w as u32, rect.h as u32);
334                    // convert into hybrids
335                    match item {
336                        Packing::Img {
337                            layer_index,
338                            frame_index,
339                            image,
340                        } => {
341                            let atlas_texture = AtlasTexture {
342                                offset_px,
343                                size_px,
344                                layer_index,
345                                frame_index,
346                                ..Default::default()
347                            };
348                            let texture = slab.new_value(atlas_texture);
349                            output.push(texture.clone());
350                            layer.frames.push(texture);
351
352                            let bytes = convert_to_rgba8_bytes(
353                                image.pixels.clone(),
354                                image.format,
355                                image.apply_linear_transfer,
356                            );
357
358                            let origin = wgpu::Origin3d {
359                                x: offset_px.x,
360                                y: offset_px.y,
361                                z: layer_index,
362                            };
363                            let size = wgpu::Extent3d {
364                                width: size_px.x,
365                                height: size_px.y,
366                                depth_or_array_layers: 1,
367                            };
368                            log::trace!("  writing image data to frame {frame_index} in layer {layer_index}");
369                            log::trace!("    frame: {atlas_texture:?}");
370                            log::trace!("    origin: {origin:?}");
371                            log::trace!("    size: {size:?}");
372
373                            // write the new image from the CPU to the new texture
374                            queue.write_texture(
375                                wgpu::ImageCopyTextureBase {
376                                    texture: &new_texture_array.texture,
377                                    mip_level: 0,
378                                    origin,
379                                    aspect: wgpu::TextureAspect::All,
380                                },
381                                &bytes,
382                                wgpu::ImageDataLayout {
383                                    offset: 0,
384                                    bytes_per_row: Some(4 * size_px.x),
385                                    rows_per_image: Some(size_px.y),
386                                },
387                                size,
388                            );
389                        }
390                        Packing::GpuImg(texture) => texture.modify(|t| {
391                            debug_assert_eq!(t.size_px, size_px);
392                            log::trace!("  add_images: copying previous frame {t:?}",);
393                            // copy the frame from the old texture to the new texture
394                            // in a new destination
395                            encoder.copy_texture_to_texture(
396                                wgpu::ImageCopyTexture {
397                                    texture: &texture_array.texture,
398                                    mip_level: 0,
399                                    origin: wgpu::Origin3d {
400                                        x: t.offset_px.x,
401                                        y: t.offset_px.y,
402                                        z: t.layer_index,
403                                    },
404                                    aspect: wgpu::TextureAspect::All,
405                                },
406                                wgpu::ImageCopyTexture {
407                                    texture: &new_texture_array.texture,
408                                    mip_level: 0,
409                                    origin: wgpu::Origin3d {
410                                        x: offset_px.x,
411                                        y: offset_px.y,
412                                        z: t.layer_index,
413                                    },
414                                    aspect: wgpu::TextureAspect::All,
415                                },
416                                wgpu::Extent3d {
417                                    width: size_px.x,
418                                    height: size_px.y,
419                                    depth_or_array_layers: 1,
420                                },
421                            );
422
423                            t.offset_px = offset_px;
424                        }),
425                    }
426                }
427            } else {
428                // TODO: create a new layer and retry
429                return CannotPackTexturesSnafu {
430                    size: texture_array.texture.size(),
431                    image_sizes: additions
432                        .iter()
433                        .map(|(_, img)| img.size)
434                        .collect::<Vec<_>>(),
435                }
436                .fail();
437            }
438        }
439        queue.submit(Some(encoder.finish()));
440
441        *texture_array = new_texture_array;
442
443        output.sort_by_key(|a| {
444            // UNWRAP: safe because we created the value as a hybrid above
445            a.get().frame_index
446        });
447        Ok(output)
448    }
449
450    /// Resize the atlas.
451    ///
452    /// This also distributes the images by size among all layers in an effort to reduce
453    /// the likelyhood that packing the atlas may fail.
454    ///
455    /// ## Errors
456    /// Errors if `size` has a width or height that is not a power of two, or are unequal
457    pub fn resize(
458        &self,
459        device: &wgpu::Device,
460        queue: &wgpu::Queue,
461        size: wgpu::Extent3d,
462    ) -> Result<(), AtlasError> {
463        let new_texture_array = Self::create_texture(device, queue, size)?;
464        // UNWRAP: POP
465        let mut texture_array = self.texture_array.write().unwrap();
466        // UNWRAP: POP
467        let mut layers = self.layers.write().unwrap();
468        let old_layers = std::mem::replace(
469            layers.as_mut(),
470            vec![Layer::default(); size.depth_or_array_layers as usize],
471        );
472        log::trace!("repacking all layers due to a resize");
473        let mut all_frames = old_layers
474            .into_iter()
475            .flat_map(|layer| layer.frames)
476            .collect::<Vec<_>>();
477        all_frames.sort_by(|a, b| {
478            let a = a.get();
479            let b = b.get();
480            (a.size_px.x * a.size_px.y).cmp(&(b.size_px.x * b.size_px.y))
481        });
482
483        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
484            label: Some("atlas resize"),
485        });
486        for (i, entries) in fan_split_n(size.depth_or_array_layers as usize, all_frames)
487            .into_iter()
488            .enumerate()
489        {
490            log::trace!("repacking layer {i} with {} frames", entries.len());
491            let items = crunch::pack_into_po2(
492                size.width as usize,
493                entries.iter().map(|atlas_texture| {
494                    let size = atlas_texture.get().size_px;
495                    crunch::Item::new(
496                        atlas_texture,
497                        size.x as usize,
498                        size.y as usize,
499                        crunch::Rotation::None,
500                    )
501                }),
502            )
503            .ok()
504            .context(CannotPackTexturesSnafu {
505                size,
506                image_sizes: entries.iter().map(|t| t.get().size_px).collect::<Vec<_>>(),
507            })?;
508            log::trace!("  packed!");
509
510            // UNWRAP: safe because we know the new layers has this index
511            let layer = layers.get_mut(i).unwrap();
512            for (j, crunch::PackedItem { data: entry, rect }) in items.items.into_iter().enumerate()
513            {
514                let offset_px = UVec2::new(rect.x as u32, rect.y as u32);
515                let size_px = UVec2::new(rect.w as u32, rect.h as u32);
516
517                entry.modify(|t| {
518                    debug_assert_eq!(t.size_px, size_px);
519
520                    log::trace!(
521                        "  resize: copying previous texture {} in layer {}",
522                        t.frame_index,
523                        t.layer_index
524                    );
525                    // copy the frame from the old frame to the new frame
526                    encoder.copy_texture_to_texture(
527                        wgpu::ImageCopyTexture {
528                            texture: &texture_array.texture,
529                            mip_level: 0,
530                            origin: wgpu::Origin3d {
531                                x: t.offset_px.x,
532                                y: t.offset_px.y,
533                                z: t.layer_index,
534                            },
535                            aspect: wgpu::TextureAspect::All,
536                        },
537                        wgpu::ImageCopyTexture {
538                            texture: &new_texture_array.texture,
539                            mip_level: 0,
540                            origin: wgpu::Origin3d {
541                                x: offset_px.x,
542                                y: offset_px.y,
543                                z: t.layer_index,
544                            },
545                            aspect: wgpu::TextureAspect::All,
546                        },
547                        wgpu::Extent3d {
548                            width: size_px.x,
549                            height: size_px.y,
550                            depth_or_array_layers: 1,
551                        },
552                    );
553
554                    t.offset_px = offset_px;
555                    t.layer_index = i as u32;
556                    t.frame_index = j as u32;
557                });
558
559                layer.frames.push(entry.clone());
560            }
561        }
562        queue.submit(Some(encoder.finish()));
563
564        *texture_array = new_texture_array;
565
566        Ok(())
567    }
568
569    /// Perform upkeep on the atlas.
570    ///
571    /// This removes any `TextureFrame`s that have no references and repacks the atlas
572    /// if any were removed.
573    pub fn upkeep(&self, device: &wgpu::Device, queue: &wgpu::Queue) {
574        let mut total_dropped = 0;
575        {
576            let mut layers = self.layers.write().unwrap();
577            for (i, layer) in layers.iter_mut().enumerate() {
578                let mut dropped = 0;
579                layer.frames.retain(|entry| {
580                    let tex_has_references = entry.strong_count() > ENTRY_STRONG_COUNT_LOWER_BOUND;
581                    if tex_has_references {
582                        true
583                    } else {
584                        dropped += 1;
585                        false
586                    }
587                });
588                total_dropped += dropped;
589                if dropped > 0 {
590                    log::trace!("removed {dropped} frames from layer {i}");
591                }
592            }
593
594            layers.len()
595        };
596
597        if total_dropped > 0 {
598            log::trace!("repacking after dropping {total_dropped} frames from the atlas");
599            // UNWRAP: safe because we can only remove frames from the atlas, which should
600            // only make it easier to pack.
601            self.resize(device, queue, self.get_size()).unwrap();
602        }
603    }
604
605    /// Read the atlas image from the GPU.
606    ///
607    /// This is primarily for testing.
608    ///
609    /// The resulting image will be in a **linear** color space.
610    ///
611    /// ## Panics
612    /// Panics if the pixels read from the GPU cannot be converted into an
613    /// `RgbaImage`.
614    pub fn atlas_img(&self, device: &wgpu::Device, queue: &wgpu::Queue, layer: u32) -> RgbaImage {
615        let tex = self.get_texture();
616        let size = tex.texture.size();
617        let buffer = Texture::read_from(
618            &tex.texture,
619            device,
620            queue,
621            size.width as usize,
622            size.height as usize,
623            4,
624            1,
625            0,
626            Some(wgpu::Origin3d {
627                x: 0,
628                y: 0,
629                z: layer,
630            }),
631        );
632        buffer.into_linear_rgba(device).unwrap()
633    }
634}
635
636#[cfg(test)]
637mod test {
638    use crate::{
639        atlas::{AtlasTexture, TextureAddressMode},
640        camera::Camera,
641        pbr::Material,
642        stage::{Renderlet, Vertex},
643        transform::Transform,
644        Context,
645    };
646    use glam::{UVec3, Vec2, Vec3, Vec4};
647
648    use super::*;
649
650    #[test]
651    // Ensures that textures are packed and rendered correctly.
652    fn atlas_uv_mapping() {
653        log::info!("{:?}", std::env::current_dir());
654        let ctx =
655            Context::headless(32, 32).with_default_atlas_texture_size(UVec3::new(1024, 1024, 2));
656        let stage = ctx
657            .new_stage()
658            .with_background_color(Vec3::splat(0.0).extend(1.0))
659            .with_bloom(false);
660        let (projection, view) = crate::camera::default_ortho2d(32.0, 32.0);
661        let camera = stage.new_value(Camera::new(projection, view));
662        let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
663        let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap();
664        let texels = AtlasImage::from_path("../../test_img/atlas/uv_mapping.png").unwrap();
665        log::info!("setting images");
666        let atlas_entries = stage.set_images([dirt, sandstone, texels]).unwrap();
667        log::info!("  done setting images");
668
669        let texels_entry = &atlas_entries[2];
670
671        let material = stage.new_value(Material {
672            albedo_texture_id: texels_entry.id(),
673            has_lighting: false,
674            ..Default::default()
675        });
676        let geometry = stage.new_array({
677            let tl = Vertex::default()
678                .with_position(Vec3::ZERO)
679                .with_uv0(Vec2::ZERO);
680            let tr = Vertex::default()
681                .with_position(Vec3::new(1.0, 0.0, 0.0))
682                .with_uv0(Vec2::new(1.0, 0.0));
683            let bl = Vertex::default()
684                .with_position(Vec3::new(0.0, 1.0, 0.0))
685                .with_uv0(Vec2::new(0.0, 1.0));
686            let br = Vertex::default()
687                .with_position(Vec3::new(1.0, 1.0, 0.0))
688                .with_uv0(Vec2::splat(1.0));
689            [tl, bl, br, tl, br, tr]
690        });
691        let transform = stage.new_value(Transform {
692            scale: Vec3::new(32.0, 32.0, 1.0),
693            ..Default::default()
694        });
695        let renderlet = stage.new_value(Renderlet {
696            camera_id: camera.id(),
697            vertices_array: geometry.array(),
698            transform_id: transform.id(),
699            material_id: material.id(),
700            ..Default::default()
701        });
702        stage.add_renderlet(&renderlet);
703
704        log::info!("rendering");
705        let frame = ctx.get_next_frame().unwrap();
706        stage.render(&frame.view());
707        let img = frame.read_image().unwrap();
708        img_diff::assert_img_eq("atlas/uv_mapping.png", img);
709    }
710
711    #[test]
712    // Ensures that textures with different wrapping modes are rendered correctly.
713    fn uv_wrapping() {
714        let icon_w = 32;
715        let icon_h = 41;
716        let sheet_w = icon_w * 3;
717        let sheet_h = icon_h * 3;
718        let w = sheet_w * 3 + 2;
719        let h = sheet_h;
720        let ctx = Context::headless(w, h);
721        let stage = ctx
722            .new_stage()
723            .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
724        let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);
725        let camera = stage.new_value(Camera::new(projection, view));
726        let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap();
727        let entries = stage.set_images(std::iter::repeat(texels).take(3)).unwrap();
728        let clamp_tex = &entries[0];
729        let repeat_tex = &entries[1];
730        repeat_tex.modify(|t| {
731            t.modes.s = TextureAddressMode::Repeat;
732            t.modes.t = TextureAddressMode::Repeat;
733        });
734        let mirror_tex = &entries[2];
735        mirror_tex.modify(|t| {
736            t.modes.s = TextureAddressMode::MirroredRepeat;
737            t.modes.t = TextureAddressMode::MirroredRepeat;
738        });
739
740        let clamp_material = stage.new_value(Material {
741            albedo_texture_id: clamp_tex.id(),
742            has_lighting: false,
743            ..Default::default()
744        });
745        let repeat_material = stage.new_value(Material {
746            albedo_texture_id: repeat_tex.id(),
747            has_lighting: false,
748            ..Default::default()
749        });
750        let mirror_material = stage.new_value(Material {
751            albedo_texture_id: mirror_tex.id(),
752            has_lighting: false,
753            ..Default::default()
754        });
755
756        let sheet_w = sheet_w as f32;
757        let sheet_h = sheet_h as f32;
758        let geometry = stage.new_array({
759            let tl = Vertex::default()
760                .with_position(Vec3::ZERO)
761                .with_uv0(Vec2::ZERO);
762            let tr = Vertex::default()
763                .with_position(Vec3::new(sheet_w, 0.0, 0.0))
764                .with_uv0(Vec2::new(3.0, 0.0));
765            let bl = Vertex::default()
766                .with_position(Vec3::new(0.0, sheet_h, 0.0))
767                .with_uv0(Vec2::new(0.0, 3.0));
768            let br = Vertex::default()
769                .with_position(Vec3::new(sheet_w, sheet_h, 0.0))
770                .with_uv0(Vec2::splat(3.0));
771            [tl, bl, br, tl, br, tr]
772        });
773        let clamp_prim = stage.new_value(Renderlet {
774            camera_id: camera.id(),
775            vertices_array: geometry.array(),
776            material_id: clamp_material.id(),
777            ..Default::default()
778        });
779        stage.add_renderlet(&clamp_prim);
780
781        let repeat_transform = stage.new_value(Transform {
782            translation: Vec3::new(sheet_w + 1.0, 0.0, 0.0),
783            ..Default::default()
784        });
785        let repeat_prim = stage.new_value(Renderlet {
786            camera_id: camera.id(),
787            vertices_array: geometry.array(),
788            material_id: repeat_material.id(),
789            transform_id: repeat_transform.id(),
790            ..Default::default()
791        });
792        stage.add_renderlet(&repeat_prim);
793
794        let mirror_transform = stage.new_value(Transform {
795            translation: Vec3::new(sheet_w * 2.0 + 2.0, 0.0, 0.0),
796            ..Default::default()
797        });
798        let mirror_prim = stage.new_value(Renderlet {
799            camera_id: camera.id(),
800            vertices_array: geometry.array(),
801            material_id: mirror_material.id(),
802            transform_id: mirror_transform.id(),
803            ..Default::default()
804        });
805        stage.add_renderlet(&mirror_prim);
806
807        let frame = ctx.get_next_frame().unwrap();
808        stage.render(&frame.view());
809        let img = frame.read_image().unwrap();
810        img_diff::assert_img_eq("atlas/uv_wrapping.png", img);
811    }
812
813    #[test]
814    // Ensures that textures with negative uv coords wrap correctly
815    fn negative_uv_wrapping() {
816        let icon_w = 32;
817        let icon_h = 41;
818        let sheet_w = icon_w * 3;
819        let sheet_h = icon_h * 3;
820        let w = sheet_w * 3 + 2;
821        let h = sheet_h;
822        let ctx = Context::headless(w, h);
823        let stage = ctx
824            .new_stage()
825            .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
826
827        let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);
828        let camera = stage.new_value(Camera {
829            projection,
830            view,
831            ..Default::default()
832        });
833
834        let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap();
835        let entries = stage.set_images(std::iter::repeat(texels).take(3)).unwrap();
836
837        let clamp_tex = &entries[0];
838        let repeat_tex = &entries[1];
839        repeat_tex.modify(|t| {
840            t.modes.s = TextureAddressMode::Repeat;
841            t.modes.t = TextureAddressMode::Repeat;
842        });
843
844        let mirror_tex = &entries[2];
845        mirror_tex.modify(|t| {
846            t.modes.s = TextureAddressMode::MirroredRepeat;
847            t.modes.t = TextureAddressMode::MirroredRepeat;
848        });
849
850        let clamp_material = stage.new_value(Material {
851            albedo_texture_id: clamp_tex.id(),
852            has_lighting: false,
853            ..Default::default()
854        });
855
856        let repeat_material = stage.new_value(Material {
857            albedo_texture_id: repeat_tex.id(),
858            has_lighting: false,
859            ..Default::default()
860        });
861
862        let mirror_material = stage.new_value(Material {
863            albedo_texture_id: mirror_tex.id(),
864            has_lighting: false,
865            ..Default::default()
866        });
867
868        let sheet_w = sheet_w as f32;
869        let sheet_h = sheet_h as f32;
870        let geometry = {
871            let tl = Vertex::default()
872                .with_position(Vec3::ZERO)
873                .with_uv0(Vec2::ZERO);
874            let tr = Vertex::default()
875                .with_position(Vec3::new(sheet_w, 0.0, 0.0))
876                .with_uv0(Vec2::new(-3.0, 0.0));
877            let bl = Vertex::default()
878                .with_position(Vec3::new(0.0, sheet_h, 0.0))
879                .with_uv0(Vec2::new(0.0, -3.0));
880            let br = Vertex::default()
881                .with_position(Vec3::new(sheet_w, sheet_h, 0.0))
882                .with_uv0(Vec2::splat(-3.0));
883            stage.new_array([tl, bl, br, tl, br, tr])
884        };
885
886        let clamp_prim = stage.new_value(Renderlet {
887            camera_id: camera.id(),
888            vertices_array: geometry.array(),
889            material_id: clamp_material.id(),
890            ..Default::default()
891        });
892        stage.add_renderlet(&clamp_prim);
893
894        let repeat_transform = stage.new_value(Transform {
895            translation: Vec3::new(sheet_w + 1.0, 0.0, 0.0),
896            ..Default::default()
897        });
898        let repeat_prim = stage.new_value(Renderlet {
899            camera_id: camera.id(),
900            vertices_array: geometry.array(),
901            material_id: repeat_material.id(),
902            transform_id: repeat_transform.id(),
903            ..Default::default()
904        });
905        stage.add_renderlet(&repeat_prim);
906
907        let mirror_transform = stage.new_value(Transform {
908            translation: Vec3::new(sheet_w * 2.0 + 2.0, 0.0, 0.0),
909            ..Default::default()
910        });
911        let mirror_prim = stage.new_value(Renderlet {
912            camera_id: camera.id(),
913            vertices_array: geometry.array(),
914            material_id: mirror_material.id(),
915            transform_id: mirror_transform.id(),
916            ..Default::default()
917        });
918        stage.add_renderlet(&mirror_prim);
919
920        let frame = ctx.get_next_frame().unwrap();
921        stage.render(&frame.view());
922        let img = frame.read_image().unwrap();
923        img_diff::assert_img_eq("atlas/negative_uv_wrapping.png", img);
924    }
925
926    #[test]
927    fn transform_uvs_for_atlas() {
928        let mut tex = AtlasTexture {
929            offset_px: UVec2::ZERO,
930            size_px: UVec2::ONE,
931            ..Default::default()
932        };
933        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(100)));
934        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(1)));
935        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(256)));
936        tex.offset_px = UVec2::splat(10);
937        assert_eq!(
938            Vec2::splat(0.1).extend(0.0),
939            tex.uv(Vec2::ZERO, UVec2::splat(100))
940        );
941    }
942
943    #[test]
944    fn can_load_and_read_atlas_texture_array() {
945        // tests that the atlas lays out textures in the way we expect
946        let ctx =
947            Context::headless(100, 100).with_default_atlas_texture_size(UVec3::new(512, 512, 2));
948        let stage = ctx.new_stage();
949        let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
950        let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap();
951        let cheetah = AtlasImage::from_path("../../img/cheetah.jpg").unwrap();
952        let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap();
953        let _frames = stage
954            .set_images([dirt, sandstone, cheetah, texels])
955            .unwrap();
956
957        let img = stage.atlas.atlas_img(&stage.device, &stage.queue, 0);
958        img_diff::assert_img_eq("atlas/array0.png", img);
959        let img = stage.atlas.atlas_img(&stage.device, &stage.queue, 1);
960        img_diff::assert_img_eq("atlas/array1.png", img);
961    }
962
963    #[test]
964    fn upkeep_trims_the_atlas() {
965        // tests that Atlas::upkeep trims out unused images and repacks the atlas
966        let ctx =
967            Context::headless(100, 100).with_default_atlas_texture_size(UVec3::new(512, 512, 2));
968        let stage = ctx.new_stage();
969        let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
970        let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap();
971        let cheetah = AtlasImage::from_path("../../img/cheetah.jpg").unwrap();
972        let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap();
973        let mut frames = stage
974            .set_images([
975                dirt,
976                sandstone,
977                cheetah,
978                texels.clone(),
979                texels.clone(),
980                texels.clone(),
981                texels.clone(),
982                texels,
983            ])
984            .unwrap();
985        assert_eq!(8, stage.atlas.len());
986
987        frames.pop();
988        frames.pop();
989        frames.pop();
990        frames.pop();
991
992        stage.atlas.upkeep(&stage.device, &stage.queue);
993        assert_eq!(4, stage.atlas.len());
994    }
995}