Skip to main content

renderling/atlas/
cpu.rs

1use core::{ops::Deref, sync::atomic::AtomicUsize};
2use std::sync::{Arc, Mutex, RwLock};
3
4use craballoc::{
5    prelude::{Hybrid, SlabAllocator, WeakHybrid},
6    runtime::WgpuRuntime,
7    slab::SlabBuffer,
8};
9use crabslab::Id;
10use glam::{UVec2, UVec3};
11use image::RgbaImage;
12use snafu::{prelude::*, OptionExt};
13
14use crate::{
15    atlas::{
16        shader::{AtlasBlittingDescriptor, AtlasDescriptor, AtlasTextureDescriptor},
17        TextureModes,
18    },
19    bindgroup::ManagedBindGroup,
20    texture::{self, CopiedTextureBuffer, Texture},
21};
22
23use super::atlas_image::{convert_pixels, AtlasImage};
24
25pub(crate) const ATLAS_SUGGESTED_SIZE: u32 = 2048;
26pub(crate) const ATLAS_SUGGESTED_LAYERS: u32 = 8;
27
28#[derive(Debug, Snafu)]
29pub enum AtlasError {
30    #[snafu(display("Cannot pack textures.\natlas_size: {size:#?}"))]
31    CannotPackTextures { size: wgpu::Extent3d },
32
33    #[snafu(display("Missing layer {index}"))]
34    MissingLayer { index: u32, images: Vec<AtlasImage> },
35
36    #[snafu(display("Atlas size is invalid: {size:?}"))]
37    Size { size: wgpu::Extent3d },
38
39    #[snafu(display("Missing slab during staging"))]
40    StagingMissingSlab,
41
42    #[snafu(display("Missing bindgroup {layer}"))]
43    MissingBindgroup { layer: u32 },
44
45    #[snafu(display("{source}"))]
46    Texture {
47        source: crate::texture::TextureError,
48    },
49}
50
51/// A staged texture in the texture atlas.
52///
53/// An [`AtlasTexture`] can be acquired through:
54///
55/// * [`Atlas::add_image`]
56/// * [`Atlas::add_images`].
57/// * [`Atlas::set_images`]
58///
59/// Clones of this type all point to the same underlying data.
60///
61/// Dropping all clones of this type will cause it to be unloaded from the GPU.
62///
63/// If a value of this type has been given to another staged resource,
64/// like [`Material`](crate::material::Material), this will prevent the
65/// `AtlasTexture` from being dropped and unloaded.
66///
67/// Internally an `AtlasTexture` holds a reference to its descriptor,
68/// [`AtlasTextureDescriptor`].
69#[derive(Clone)]
70pub struct AtlasTexture {
71    pub(crate) descriptor: Hybrid<AtlasTextureDescriptor>,
72}
73
74impl AtlasTexture {
75    /// Get the GPU slab identifier of the underlying descriptor.
76    ///
77    /// This is for internal use.
78    pub fn id(&self) -> Id<AtlasTextureDescriptor> {
79        self.descriptor.id()
80    }
81
82    /// Return a copy of the underlying descriptor.
83    pub fn descriptor(&self) -> AtlasTextureDescriptor {
84        self.descriptor.get()
85    }
86
87    /// Return the texture modes of the underlying descriptor.
88    pub fn modes(&self) -> TextureModes {
89        self.descriptor.get().modes
90    }
91
92    /// Sets the texture modes of the underlying descriptor.
93    ///
94    /// ## Warning
95    ///
96    /// This also sets the modes for all clones of this value.
97    pub fn set_modes(&self, modes: TextureModes) {
98        self.descriptor.modify(|d| d.modes = modes);
99    }
100}
101
102/// Used to track textures internally.
103///
104/// We need a separate struct for tracking textures because the atlas
105/// reorganizes the layout (the packing) of textures each time a new
106/// texture is added.
107///
108/// This means the textures must be updated on the GPU, but we don't
109/// want these internal representations to keep unreferenced textures
110/// from dropping, so we have to maintain a separate representation
111/// here.
112#[derive(Clone, Debug)]
113struct InternalAtlasTexture {
114    /// Cached value.
115    cache: AtlasTextureDescriptor,
116    weak: WeakHybrid<AtlasTextureDescriptor>,
117}
118
119impl InternalAtlasTexture {
120    fn from_hybrid(hat: &Hybrid<AtlasTextureDescriptor>) -> Self {
121        InternalAtlasTexture {
122            cache: hat.get(),
123            weak: WeakHybrid::from_hybrid(hat),
124        }
125    }
126
127    fn has_external_references(&self) -> bool {
128        self.weak.has_external_references()
129    }
130
131    fn set(&mut self, at: AtlasTextureDescriptor) {
132        self.cache = at;
133        if let Some(hy) = self.weak.upgrade() {
134            hy.set(at);
135        } else if let Some(gpu) = self.weak.weak_gpu().upgrade() {
136            gpu.set(at)
137        } else {
138            log::warn!("could not set atlas texture, lost");
139        }
140    }
141}
142
143pub(crate) fn check_size(size: wgpu::Extent3d) {
144    let conditions = size.depth_or_array_layers >= 2
145        && size.width == size.height
146        && (size.width & (size.width - 1)) == 0;
147    if !conditions {
148        log::error!("{}", AtlasError::Size { size });
149    }
150}
151
152fn fan_split_n<T>(n: usize, input: impl IntoIterator<Item = T>) -> Vec<Vec<T>> {
153    if n == 0 {
154        return vec![];
155    }
156    let mut output = vec![];
157    for _ in 0..n {
158        output.push(vec![]);
159    }
160    let mut i = 0;
161    for item in input.into_iter() {
162        // UNWRAP: safe because i % n
163        output
164            .get_mut(i)
165            .unwrap_or_else(|| panic!("could not unwrap i:{i} n:{n}"))
166            .push(item);
167        i = (i + 1) % n;
168    }
169    output
170}
171
172#[derive(Clone)]
173enum AnotherPacking<'a> {
174    Img {
175        original_index: usize,
176        image: &'a AtlasImage,
177    },
178    Internal(InternalAtlasTexture),
179}
180
181impl AnotherPacking<'_> {
182    fn size(&self) -> UVec2 {
183        match self {
184            AnotherPacking::Img {
185                original_index: _,
186                image,
187            } => image.size,
188            AnotherPacking::Internal(tex) => tex.cache.size_px,
189        }
190    }
191}
192
193#[derive(Clone, Default, Debug)]
194pub struct Layer {
195    frames: Vec<InternalAtlasTexture>,
196}
197
198/// A texture atlas, used to store all the textures in a scene.
199///
200/// Clones of `Atlas` all point to the same internal data.
201#[derive(Clone)]
202pub struct Atlas {
203    pub(crate) slab: SlabAllocator<WgpuRuntime>,
204    texture_array: Arc<RwLock<Texture>>,
205    layers: Arc<RwLock<Vec<Layer>>>,
206    label: Option<String>,
207    descriptor: Hybrid<AtlasDescriptor>,
208    /// Used for user updates into the atlas by blit images into specific
209    /// frames.
210    blitter: AtlasBlitter,
211}
212
213impl Atlas {
214    const LABEL: Option<&str> = Some("atlas-texture");
215
216    pub fn device(&self) -> &wgpu::Device {
217        self.slab.device()
218    }
219
220    /// Create the initial texture to use.
221    fn create_texture(
222        runtime: impl AsRef<WgpuRuntime>,
223        size: wgpu::Extent3d,
224        format: Option<wgpu::TextureFormat>,
225        label: Option<&str>,
226        usage: Option<wgpu::TextureUsages>,
227    ) -> Texture {
228        let device = &runtime.as_ref().device;
229        let queue = &runtime.as_ref().queue;
230        check_size(size);
231        let usage = usage.unwrap_or(wgpu::TextureUsages::empty());
232        let texture = device.create_texture(&wgpu::TextureDescriptor {
233            label: Some(label.unwrap_or(Self::LABEL.unwrap())),
234            size,
235            mip_level_count: 1,
236            sample_count: 1,
237            dimension: wgpu::TextureDimension::D2,
238            format: format.unwrap_or(wgpu::TextureFormat::Rgba8Unorm),
239            usage: usage
240                | wgpu::TextureUsages::TEXTURE_BINDING
241                | wgpu::TextureUsages::COPY_DST
242                | wgpu::TextureUsages::COPY_SRC,
243            view_formats: &[],
244        });
245
246        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
247            label: Some("create atlas texture array"),
248        });
249        if device.features().contains(wgpu::Features::CLEAR_TEXTURE) {
250            encoder.clear_texture(
251                &texture,
252                &wgpu::ImageSubresourceRange {
253                    aspect: wgpu::TextureAspect::All,
254                    base_mip_level: 0,
255                    mip_level_count: None,
256                    base_array_layer: 0,
257                    array_layer_count: None,
258                },
259            );
260        }
261        queue.submit(Some(encoder.finish()));
262
263        let sampler_desc = wgpu::SamplerDescriptor {
264            address_mode_u: wgpu::AddressMode::ClampToEdge,
265            address_mode_v: wgpu::AddressMode::ClampToEdge,
266            address_mode_w: wgpu::AddressMode::ClampToEdge,
267            mag_filter: wgpu::FilterMode::Nearest,
268            min_filter: wgpu::FilterMode::Nearest,
269            mipmap_filter: wgpu::FilterMode::Nearest,
270            ..Default::default()
271        };
272
273        Texture::from_wgpu_tex(device, texture, Some(sampler_desc), None)
274    }
275
276    /// Create a new atlas.
277    ///
278    /// Size _must_ be a power of two.
279    ///
280    /// ## Panics
281    /// Panics if `size` is not a power of two.
282    pub fn new(
283        slab: &SlabAllocator<WgpuRuntime>,
284        size: wgpu::Extent3d,
285        format: Option<wgpu::TextureFormat>,
286        label: Option<&str>,
287        usage: Option<wgpu::TextureUsages>,
288    ) -> Self {
289        let texture = Self::create_texture(slab.runtime(), size, format, label, usage);
290        let num_layers = texture.texture.size().depth_or_array_layers as usize;
291        let layers = vec![Layer::default(); num_layers];
292        log::trace!("creating new atlas with dimensions {size:?}, {num_layers} layers");
293        let descriptor = slab.new_value(AtlasDescriptor {
294            size: UVec3::new(size.width, size.height, size.depth_or_array_layers),
295        });
296        let label = label.map(|s| s.to_owned());
297        let blitter = AtlasBlitter::new(
298            slab.device(),
299            texture.texture.format(),
300            wgpu::FilterMode::Linear,
301        );
302        Atlas {
303            slab: slab.clone(),
304            layers: Arc::new(RwLock::new(layers)),
305            descriptor,
306            label,
307            blitter,
308            texture_array: Arc::new(RwLock::new(texture)),
309        }
310    }
311
312    pub fn descriptor_id(&self) -> Id<AtlasDescriptor> {
313        self.descriptor.id()
314    }
315
316    pub fn len(&self) -> usize {
317        // UNWRAP: panic on purpose
318        let layers = self.layers.read().expect("atlas layers read");
319        layers.iter().map(|layer| layer.frames.len()).sum::<usize>()
320    }
321
322    pub fn is_empty(&self) -> bool {
323        // UNWRAP: panic on purpose
324        self.len() == 0
325    }
326
327    /// Returns a reference to the current atlas texture array.
328    pub fn get_texture(&self) -> impl Deref<Target = Texture> + '_ {
329        // UNWRAP: panic on purpose
330        self.texture_array.read().expect("atlas texture_array read")
331    }
332
333    pub fn get_layers(&self) -> impl Deref<Target = Vec<Layer>> + '_ {
334        // UNWRAP: panic on purpose
335        self.layers.read().expect("atlas layers read")
336    }
337
338    /// Reset this atlas with all new images.
339    ///
340    /// Any existing `Hybrid<AtlasTexture>`s will be invalidated.
341    pub fn set_images(&self, images: &[AtlasImage]) -> Result<Vec<AtlasTexture>, AtlasError> {
342        log::debug!("setting images");
343        {
344            // UNWRAP: panic on purpose
345            let texture = self.texture_array.read().expect("atlas texture_array read");
346            let mut guard = self.layers.write().expect("atlas layers write");
347            let layers: &mut Vec<_> = guard.as_mut();
348            let new_layers =
349                vec![Layer::default(); texture.texture.size().depth_or_array_layers as usize];
350            let _old_layers = std::mem::replace(layers, new_layers);
351        }
352        self.add_images(images)
353    }
354
355    pub fn get_size(&self) -> wgpu::Extent3d {
356        // UNWRAP: POP
357        self.texture_array
358            .read()
359            .expect("atlas texture_array read")
360            .texture
361            .size()
362    }
363
364    /// Add the given images
365    pub fn add_images<'a>(
366        &self,
367        images: impl IntoIterator<Item = &'a AtlasImage>,
368    ) -> Result<Vec<AtlasTexture>, AtlasError> {
369        // UNWRAP: POP
370        let mut layers = self.layers.write().expect("atlas layers write");
371        let mut texture_array = self
372            .texture_array
373            .write()
374            .expect("atlas texture_array write");
375        let extent = texture_array.texture.size();
376
377        let newly_packed_layers = pack_images(&layers, images, extent)
378            .context(CannotPackTexturesSnafu { size: extent })?;
379
380        let mut staged = StagedResources::try_staging(
381            self.slab.runtime(),
382            extent,
383            newly_packed_layers,
384            Some(&self.slab),
385            &texture_array,
386            self.label.as_deref(),
387        )?;
388
389        // Commit our newly staged values, now that everything is done.
390        *texture_array = staged.texture;
391        *layers = staged.layers;
392
393        staged.image_additions.sort_by_key(|a| a.0);
394        Ok(staged
395            .image_additions
396            .into_iter()
397            .map(|a| AtlasTexture { descriptor: a.1 })
398            .collect())
399    }
400
401    /// Add one image.
402    ///
403    /// If you have more than one image, you should use [`Atlas::add_images`],
404    /// as every change in images causes a repacking, which might be
405    /// expensive.
406    pub fn add_image(&self, image: &AtlasImage) -> Result<AtlasTexture, AtlasError> {
407        // UNWRAP: safe because we know there's at least one image
408        Ok(self.add_images(Some(image))?.pop().unwrap())
409    }
410
411    /// Resize the atlas.
412    ///
413    /// This also distributes the images by size among all layers in an effort
414    /// to reduce the likelyhood that packing the atlas may fail.
415    ///
416    /// ## Errors
417    /// Errors if `size` has a width or height that is not a power of two, or
418    /// are unequal
419    pub fn resize(
420        &self,
421        runtime: impl AsRef<WgpuRuntime>,
422        extent: wgpu::Extent3d,
423    ) -> Result<(), AtlasError> {
424        let mut layers = self.layers.write().expect("atlas layers write");
425        let mut texture_array = self
426            .texture_array
427            .write()
428            .expect("atlas texture_array write");
429
430        let newly_packed_layers =
431            pack_images(&layers, &[], extent).context(CannotPackTexturesSnafu { size: extent })?;
432
433        let staged = StagedResources::try_staging(
434            runtime,
435            extent,
436            newly_packed_layers,
437            None::<&SlabAllocator<WgpuRuntime>>,
438            &texture_array,
439            self.label.as_deref(),
440        )?;
441
442        // Commit our newly staged values, now that everything is done.
443        *texture_array = staged.texture;
444        *layers = staged.layers;
445
446        Ok(())
447    }
448
449    /// Perform upkeep on the atlas.
450    ///
451    /// This removes any `TextureFrame`s that have no references and repacks the
452    /// atlas if any were removed.
453    ///
454    /// Returns `true` if the atlas texture was recreated.
455    #[must_use]
456    pub fn upkeep(&self, runtime: impl AsRef<WgpuRuntime>) -> bool {
457        let mut total_dropped = 0;
458        {
459            let mut layers = self.layers.write().expect("atlas layers write");
460            for (i, layer) in layers.iter_mut().enumerate() {
461                let mut dropped = 0;
462                layer.frames.retain(|entry| {
463                    if entry.has_external_references() {
464                        true
465                    } else {
466                        dropped += 1;
467                        false
468                    }
469                });
470                total_dropped += dropped;
471                if dropped > 0 {
472                    log::trace!("removed {dropped} frames from layer {i}");
473                }
474            }
475
476            layers.len()
477        };
478
479        if total_dropped > 0 {
480            log::trace!("repacking after dropping {total_dropped} frames from the atlas");
481            // UNWRAP: safe because we can only remove frames from the atlas,
482            // which should only make it easier to pack.
483            self.resize(runtime.as_ref(), self.get_size()).unwrap();
484            true
485        } else {
486            false
487        }
488    }
489
490    /// Read the atlas image from the GPU into a [`CopiedTextureBuffer`].
491    ///
492    /// This is primarily for testing.
493    ///
494    /// ## Panics
495    /// Panics if the pixels read from the GPU cannot be read.
496    pub fn atlas_img_buffer(
497        &self,
498        runtime: impl AsRef<WgpuRuntime>,
499        layer: u32,
500    ) -> CopiedTextureBuffer {
501        let runtime = runtime.as_ref();
502        let tex = self.get_texture();
503        let size = tex.texture.size();
504        let (channels, subpixel_bytes) =
505            crate::texture::wgpu_texture_format_channels_and_subpixel_bytes_todo(
506                tex.texture.format(),
507            );
508        log::info!("atlas_texture_format: {:#?}", tex.texture.format());
509        log::info!("atlas_texture_channels: {channels:#?}");
510        log::info!("atlas_texture_subpixel_bytes: {subpixel_bytes:#?}");
511        CopiedTextureBuffer::read_from(
512            runtime,
513            &tex.texture,
514            size.width as usize,
515            size.height as usize,
516            channels as usize,
517            subpixel_bytes as usize,
518            0,
519            Some(wgpu::Origin3d {
520                x: 0,
521                y: 0,
522                z: layer,
523            }),
524        )
525    }
526
527    /// Read the atlas image from the GPU.
528    ///
529    /// This is primarily for testing.
530    ///
531    /// The resulting image will be in a **linear** color space.
532    ///
533    /// ## Panics
534    /// Panics if the pixels read from the GPU cannot be converted into an
535    /// `RgbaImage`.
536    pub async fn atlas_img(&self, runtime: impl AsRef<WgpuRuntime>, layer: u32) -> RgbaImage {
537        let runtime = runtime.as_ref();
538        let buffer = self.atlas_img_buffer(runtime, layer);
539        buffer.into_linear_rgba(&runtime.device).await.unwrap()
540    }
541
542    // It's ok to hold this lock because this is just for testing.
543    #[allow(clippy::await_holding_lock)]
544    pub async fn read_images(&self, runtime: impl AsRef<WgpuRuntime>) -> Vec<RgbaImage> {
545        let mut images = vec![];
546        for i in 0..self.layers.read().expect("atlas layers read").len() {
547            images.push(self.atlas_img(runtime.as_ref(), i as u32).await);
548        }
549        images
550    }
551
552    /// Update the given [`AtlasTexture`] with a
553    /// [`Texture`](crate::texture::Texture).
554    ///
555    /// This will blit the `Texture` into the frame of the [`Atlas`] pointed to
556    /// by the `AtlasTexture`.
557    ///
558    /// Returns a submission index that can be polled with
559    /// [`wgpu::Device::poll`].
560    pub fn update_texture(
561        &self,
562        atlas_texture: &AtlasTexture,
563        source_texture: &texture::Texture,
564    ) -> Result<wgpu::SubmissionIndex, AtlasError> {
565        self.update_textures(Some((atlas_texture, source_texture)))
566    }
567
568    /// Update the given [`AtlasTexture`]s with
569    /// [`Texture`](crate::texture::Texture)s.
570    ///
571    /// This will blit the `Texture` into the frame of the [`Atlas`] pointed to
572    /// by the `AtlasTexture`.
573    ///
574    /// Returns a submission index that can be polled with
575    /// [`wgpu::Device::poll`].
576    pub fn update_textures<'a>(
577        &self,
578        updates: impl IntoIterator<Item = (&'a AtlasTexture, &'a texture::Texture)>,
579    ) -> Result<wgpu::SubmissionIndex, AtlasError> {
580        let updates = updates.into_iter().collect::<Vec<_>>();
581        let op = AtlasBlittingOperation::new(&self.blitter, self, updates.len());
582        let runtime = self.slab.runtime();
583        let mut encoder = runtime
584            .device
585            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
586                label: Some("Atlas::update_texture"),
587            });
588        for (i, (atlas_texture, source_texture)) in updates.into_iter().enumerate() {
589            op.run(
590                runtime,
591                &mut encoder,
592                source_texture,
593                i as u32,
594                self,
595                atlas_texture,
596            )?;
597        }
598        Ok(runtime.queue.submit(Some(encoder.finish())))
599    }
600
601    /// Update the given [`AtlasTexture`]s with new data.
602    ///
603    /// This will blit the image data into the frame of the [`Atlas`] pointed to
604    /// by the `AtlasTexture`.
605    ///
606    /// Returns a submission index that can be polled with
607    /// [`wgpu::Device::poll`].
608    pub fn update_images<'a>(
609        &self,
610        updates: impl IntoIterator<Item = (&'a AtlasTexture, impl Into<AtlasImage>)>,
611    ) -> Result<wgpu::SubmissionIndex, AtlasError> {
612        let (atlas_textures, images): (Vec<_>, Vec<_>) = updates.into_iter().unzip();
613        let mut textures = vec![];
614        for image in images.into_iter() {
615            let image: AtlasImage = image.into();
616            let atlas_format = self.get_texture().texture.format();
617            let bytes = super::atlas_image::convert_pixels(
618                image.pixels,
619                image.format,
620                atlas_format,
621                image.apply_linear_transfer,
622            );
623            let (channels, subpixel_bytes) =
624                texture::wgpu_texture_format_channels_and_subpixel_bytes(atlas_format)
625                    .context(TextureSnafu)?;
626            let texture = texture::Texture::new_with(
627                self.slab.runtime(),
628                Some("atlas-image-update"),
629                Some(wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST),
630                None,
631                atlas_format,
632                channels,
633                subpixel_bytes,
634                image.size.x,
635                image.size.y,
636                1,
637                &bytes,
638            );
639            textures.push(texture);
640        }
641        self.update_textures(atlas_textures.into_iter().zip(textures.iter()))
642    }
643
644    /// Update the given [`AtlasTexture`]s with new data.
645    ///
646    /// This will blit the image data into the frame of the [`Atlas`] pointed to
647    /// by the `AtlasTexture`.
648    ///
649    /// Returns a submission index that can be polled with
650    /// [`wgpu::Device::poll`].
651    pub fn update_image(
652        &self,
653        atlas_texture: &AtlasTexture,
654        source_image: impl Into<AtlasImage>,
655    ) -> Result<wgpu::SubmissionIndex, AtlasError> {
656        self.update_images(Some((atlas_texture, source_image)))
657    }
658}
659
660fn pack_images<'a>(
661    layers: &[Layer],
662    images: impl IntoIterator<Item = &'a AtlasImage>,
663    extent: wgpu::Extent3d,
664) -> Option<Vec<crunch::PackedItems<AnotherPacking<'a>>>> {
665    let mut new_packing: Vec<AnotherPacking> = {
666        let layers: Vec<_> = layers.to_vec();
667        layers
668            .into_iter()
669            .flat_map(|layer| layer.frames)
670            // Filter out any textures that have been completely dropped
671            // by the user.
672            .filter_map(|tex| {
673                if tex.has_external_references() {
674                    Some(AnotherPacking::Internal(tex))
675                } else {
676                    None
677                }
678            })
679            .chain(
680                images
681                    .into_iter()
682                    .enumerate()
683                    .map(|(i, image)| AnotherPacking::Img {
684                        original_index: i,
685                        image,
686                    }),
687            )
688            .collect()
689    };
690    new_packing.sort_by_key(|a| a.size().length_squared());
691    let total_images = new_packing.len();
692    let new_packing_layers: Vec<Vec<AnotherPacking>> =
693        fan_split_n(extent.depth_or_array_layers as usize, new_packing);
694    log::trace!(
695        "packing {total_images} textures into {} layers",
696        new_packing_layers.len()
697    );
698    let mut newly_packed_layers: Vec<crunch::PackedItems<_>> = vec![];
699    for (i, new_layer) in new_packing_layers.into_iter().enumerate() {
700        log::trace!("  packing layer {i} into power of 2 {}", extent.width);
701        let packed = crunch::pack_into_po2(
702            extent.width as usize,
703            new_layer.into_iter().map(|p| {
704                let size = p.size();
705                crunch::Item::new(p, size.x as usize, size.y as usize, crunch::Rotation::None)
706            }),
707        )
708        .ok()?;
709        log::trace!("  layer {i} packed with {} textures", packed.items.len());
710        newly_packed_layers.push(packed);
711    }
712    Some(newly_packed_layers)
713}
714
715/// Internal atlas resources.
716struct StagedResources {
717    texture: Texture,
718    image_additions: Vec<(usize, Hybrid<AtlasTextureDescriptor>)>,
719    layers: Vec<Layer>,
720}
721
722impl StagedResources {
723    /// Stage the packed images, copying them to the next texture.
724    fn try_staging(
725        runtime: impl AsRef<WgpuRuntime>,
726        extent: wgpu::Extent3d,
727        newly_packed_layers: Vec<crunch::PackedItems<AnotherPacking>>,
728        slab: Option<&SlabAllocator<WgpuRuntime>>,
729        old_texture_array: &Texture,
730        label: Option<&str>,
731    ) -> Result<Self, AtlasError> {
732        let runtime = runtime.as_ref();
733        let new_texture_array = Atlas::create_texture(
734            runtime,
735            extent,
736            Some(old_texture_array.texture.format()),
737            label,
738            Some(old_texture_array.texture.usage()),
739        );
740        let mut output = vec![];
741        let mut encoder = runtime
742            .device
743            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
744                label: Some("atlas staging"),
745            });
746        let mut temporary_layers = vec![Layer::default(); extent.depth_or_array_layers as usize];
747        for (layer_index, packed_items) in newly_packed_layers.into_iter().enumerate() {
748            if packed_items.items.is_empty() {
749                continue;
750            }
751            // UNWRAP: safe because we know this index exists because we created
752            // it above
753            let layer = temporary_layers.get_mut(layer_index).unwrap();
754            for (frame_index, crunch::PackedItem { data: item, rect }) in
755                packed_items.items.into_iter().enumerate()
756            {
757                let offset_px = UVec2::new(rect.x as u32, rect.y as u32);
758                let size_px = UVec2::new(rect.w as u32, rect.h as u32);
759
760                match item {
761                    AnotherPacking::Img {
762                        original_index,
763                        image,
764                    } => {
765                        let atlas_texture = AtlasTextureDescriptor {
766                            offset_px,
767                            size_px,
768                            frame_index: frame_index as u32,
769                            layer_index: layer_index as u32,
770                            ..Default::default()
771                        };
772                        let texture = slab
773                            .context(StagingMissingSlabSnafu)?
774                            .new_value(atlas_texture);
775                        layer
776                            .frames
777                            .push(InternalAtlasTexture::from_hybrid(&texture));
778                        output.push((original_index, texture));
779
780                        let bytes = convert_pixels(
781                            image.pixels.clone(),
782                            image.format,
783                            old_texture_array.texture.format(),
784                            image.apply_linear_transfer,
785                        );
786
787                        let origin = wgpu::Origin3d {
788                            x: offset_px.x,
789                            y: offset_px.y,
790                            z: layer_index as u32,
791                        };
792                        let size = wgpu::Extent3d {
793                            width: size_px.x,
794                            height: size_px.y,
795                            depth_or_array_layers: 1,
796                        };
797                        log::trace!(
798                            "  writing image data to frame {frame_index} in layer {layer_index}"
799                        );
800                        log::trace!("    frame: {atlas_texture:?}");
801                        log::trace!("    origin: {origin:?}");
802                        log::trace!("    size: {size:?}");
803
804                        // write the new image from the CPU to the new texture
805                        runtime.queue.write_texture(
806                            wgpu::TexelCopyTextureInfo {
807                                texture: &new_texture_array.texture,
808                                mip_level: 0,
809                                origin,
810                                aspect: wgpu::TextureAspect::All,
811                            },
812                            &bytes,
813                            wgpu::TexelCopyBufferLayout {
814                                offset: 0,
815                                bytes_per_row: Some(4 * size_px.x),
816                                rows_per_image: Some(size_px.y),
817                            },
818                            size,
819                        );
820                    }
821                    AnotherPacking::Internal(mut texture) => {
822                        let prev_t = texture.cache;
823                        let mut t = texture.cache;
824                        debug_assert_eq!(t.size_px, size_px);
825                        // copy the frame from the old texture to the new
826                        // texture in a new destination
827                        encoder.copy_texture_to_texture(
828                            wgpu::TexelCopyTextureInfo {
829                                texture: &old_texture_array.texture,
830                                mip_level: 0,
831                                origin: t.origin(),
832                                aspect: wgpu::TextureAspect::All,
833                            },
834                            wgpu::TexelCopyTextureInfo {
835                                texture: &new_texture_array.texture,
836                                mip_level: 0,
837                                origin: wgpu::Origin3d {
838                                    x: offset_px.x,
839                                    y: offset_px.y,
840                                    z: layer_index as u32,
841                                },
842                                aspect: wgpu::TextureAspect::All,
843                            },
844                            t.size_as_extent(),
845                        );
846
847                        t.layer_index = layer_index as u32;
848                        t.frame_index = frame_index as u32;
849                        t.offset_px = offset_px;
850
851                        log::trace!(
852                            "  copied previous frame {}",
853                            pretty_assertions::Comparison::new(&prev_t, &t)
854                        );
855
856                        texture.set(t);
857                        layer.frames.push(texture);
858                    }
859                }
860            }
861        }
862        runtime.queue.submit(Some(encoder.finish()));
863
864        Ok(Self {
865            texture: new_texture_array,
866            image_additions: output,
867            layers: temporary_layers,
868        })
869    }
870}
871
872/// A reusable blitting operation that copies a source texture into a specific
873/// frame of an [`Atlas`].
874#[derive(Clone)]
875pub struct AtlasBlittingOperation {
876    atlas_slab_buffer: Arc<Mutex<SlabBuffer<wgpu::Buffer>>>,
877    pipeline: Arc<wgpu::RenderPipeline>,
878    bindgroups: Arc<Vec<ManagedBindGroup>>,
879    bindgroup_layout: Arc<wgpu::BindGroupLayout>,
880    sampler: Arc<wgpu::Sampler>,
881    from_texture_id: Arc<AtomicUsize>,
882    pub(crate) desc: Hybrid<AtlasBlittingDescriptor>,
883}
884
885impl AtlasBlittingOperation {
886    pub fn new(
887        blitter: &AtlasBlitter,
888        into_atlas: &Atlas,
889        source_layers: usize,
890    ) -> AtlasBlittingOperation {
891        AtlasBlittingOperation {
892            desc: into_atlas
893                .slab
894                .new_value(AtlasBlittingDescriptor::default()),
895            atlas_slab_buffer: Arc::new(Mutex::new(into_atlas.slab.commit())),
896            bindgroups: {
897                let mut bgs = vec![];
898                for _ in 0..source_layers {
899                    bgs.push(ManagedBindGroup::default());
900                }
901                Arc::new(bgs)
902            },
903            pipeline: blitter.pipeline.clone(),
904            sampler: blitter.sampler.clone(),
905            bindgroup_layout: blitter.bind_group_layout.clone(),
906            from_texture_id: Default::default(),
907        }
908    }
909
910    /// Copies the data from texture this [`AtlasBlittingOperation`] was created
911    /// with into the atlas.
912    ///
913    /// The original items used to create the inner bind group are required
914    /// here, to determine whether or not the bind group needs to be
915    /// invalidated.
916    pub fn run(
917        &self,
918        runtime: impl AsRef<WgpuRuntime>,
919        encoder: &mut wgpu::CommandEncoder,
920        from_texture: &crate::texture::Texture,
921        from_layer: u32,
922        to_atlas: &Atlas,
923        atlas_texture: &AtlasTexture,
924    ) -> Result<(), AtlasError> {
925        let runtime = runtime.as_ref();
926
927        // update the descriptor
928        self.desc.set(AtlasBlittingDescriptor {
929            atlas_texture_id: atlas_texture.id(),
930            atlas_desc_id: to_atlas.descriptor_id(),
931        });
932        // sync the update
933        let _ = to_atlas.slab.commit();
934
935        let to_atlas_texture = to_atlas.get_texture();
936        let mut atlas_slab_buffer = self
937            .atlas_slab_buffer
938            .lock()
939            .expect("atlas slab buffer lock");
940        let atlas_slab_invalid = atlas_slab_buffer.update_if_invalid();
941        let from_texture_has_been_replaced = {
942            let prev_id = self
943                .from_texture_id
944                .swap(from_texture.id(), std::sync::atomic::Ordering::Relaxed);
945            from_texture.id() != prev_id
946        };
947        let should_invalidate = atlas_slab_invalid || from_texture_has_been_replaced;
948        let view = from_texture
949            .texture
950            .create_view(&wgpu::TextureViewDescriptor {
951                label: Some("atlas-blitting"),
952                base_array_layer: from_layer,
953                array_layer_count: Some(1),
954                dimension: Some(wgpu::TextureViewDimension::D2),
955                ..Default::default()
956            });
957        let bindgroup = self
958            .bindgroups
959            .get(from_layer as usize)
960            .context(MissingBindgroupSnafu { layer: from_layer })?
961            .get(should_invalidate, || {
962                runtime
963                    .device
964                    .create_bind_group(&wgpu::BindGroupDescriptor {
965                        label: Some("atlas-blitting"),
966                        layout: &self.bindgroup_layout,
967                        entries: &[
968                            wgpu::BindGroupEntry {
969                                binding: 0,
970                                resource: wgpu::BindingResource::Buffer(
971                                    atlas_slab_buffer.deref().as_entire_buffer_binding(),
972                                ),
973                            },
974                            wgpu::BindGroupEntry {
975                                binding: 1,
976                                resource: wgpu::BindingResource::TextureView(&view),
977                            },
978                            wgpu::BindGroupEntry {
979                                binding: 2,
980                                resource: wgpu::BindingResource::Sampler(&self.sampler),
981                            },
982                        ],
983                    })
984            });
985
986        let atlas_texture = atlas_texture.descriptor();
987        let atlas_view = to_atlas_texture
988            .texture
989            .create_view(&wgpu::TextureViewDescriptor {
990                label: Some("atlas-blitting"),
991                format: None,
992                dimension: Some(wgpu::TextureViewDimension::D2),
993                usage: None,
994                aspect: wgpu::TextureAspect::All,
995                base_mip_level: 0,
996                mip_level_count: None,
997                base_array_layer: atlas_texture.layer_index,
998                array_layer_count: Some(1),
999            });
1000        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1001            label: Some("atlas-blitter"),
1002            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1003                view: &atlas_view,
1004                resolve_target: None,
1005                ops: wgpu::Operations {
1006                    load: wgpu::LoadOp::Load,
1007                    store: wgpu::StoreOp::Store,
1008                },
1009                depth_slice: None,
1010            })],
1011            depth_stencil_attachment: None,
1012            timestamp_writes: None,
1013            occlusion_query_set: None,
1014        });
1015        pass.set_pipeline(&self.pipeline);
1016        pass.set_bind_group(0, Some(bindgroup.as_ref()), &[]);
1017        let id = self.desc.id();
1018        pass.draw(0..6, id.inner()..id.inner() + 1);
1019        Ok(())
1020    }
1021}
1022
1023/// A texture blitting utility.
1024///
1025/// [`AtlasBlitter`] copies textures to specific frames within the texture
1026/// atlas.
1027#[derive(Clone)]
1028pub struct AtlasBlitter {
1029    pipeline: Arc<wgpu::RenderPipeline>,
1030    bind_group_layout: Arc<wgpu::BindGroupLayout>,
1031    sampler: Arc<wgpu::Sampler>,
1032}
1033
1034impl AtlasBlitter {
1035    /// Creates a new [`AtlasBlitter`].
1036    ///
1037    /// # Arguments
1038    /// - `device` - A [`wgpu::Device`]
1039    /// - `format` - The [`wgpu::TextureFormat`] of the atlas being updated.
1040    /// - `mag_filter` - The filtering algorithm to use when magnifying. This is
1041    ///   used when the input source is smaller than the destination.
1042    pub fn new(
1043        device: &wgpu::Device,
1044        format: wgpu::TextureFormat,
1045        mag_filter: wgpu::FilterMode,
1046    ) -> Self {
1047        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1048            label: Some("atlas-blitter"),
1049            address_mode_u: wgpu::AddressMode::ClampToEdge,
1050            address_mode_v: wgpu::AddressMode::ClampToEdge,
1051            address_mode_w: wgpu::AddressMode::ClampToEdge,
1052            mag_filter,
1053            ..Default::default()
1054        });
1055
1056        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1057            label: Some("atlas-blitter"),
1058            entries: &[
1059                wgpu::BindGroupLayoutEntry {
1060                    binding: 0,
1061                    visibility: wgpu::ShaderStages::VERTEX,
1062                    ty: wgpu::BindingType::Buffer {
1063                        ty: wgpu::BufferBindingType::Storage { read_only: true },
1064                        has_dynamic_offset: false,
1065                        min_binding_size: None,
1066                    },
1067                    count: None,
1068                },
1069                wgpu::BindGroupLayoutEntry {
1070                    binding: 1,
1071                    visibility: wgpu::ShaderStages::FRAGMENT,
1072                    ty: wgpu::BindingType::Texture {
1073                        sample_type: wgpu::TextureSampleType::Float {
1074                            filterable: mag_filter == wgpu::FilterMode::Linear,
1075                        },
1076                        view_dimension: wgpu::TextureViewDimension::D2,
1077                        multisampled: false,
1078                    },
1079                    count: None,
1080                },
1081                wgpu::BindGroupLayoutEntry {
1082                    binding: 2,
1083                    visibility: wgpu::ShaderStages::FRAGMENT,
1084                    ty: wgpu::BindingType::Sampler(
1085                        if mag_filter == wgpu::FilterMode::Linear {
1086                            wgpu::SamplerBindingType::Filtering
1087                        } else {
1088                            wgpu::SamplerBindingType::NonFiltering
1089                        },
1090                    ),
1091                    count: None,
1092                },
1093            ],
1094        });
1095
1096        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1097            label: Some("atlas-blitter"),
1098            bind_group_layouts: &[&bind_group_layout],
1099            push_constant_ranges: &[],
1100        });
1101
1102        let vertex = crate::linkage::atlas_blit_vertex::linkage(device);
1103        let fragment = crate::linkage::atlas_blit_fragment::linkage(device);
1104        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1105            label: Some("atlas-blitter"),
1106            layout: Some(&pipeline_layout),
1107            vertex: wgpu::VertexState {
1108                module: &vertex.module,
1109                entry_point: Some(vertex.entry_point),
1110                compilation_options: wgpu::PipelineCompilationOptions::default(),
1111                buffers: &[],
1112            },
1113            primitive: wgpu::PrimitiveState {
1114                topology: wgpu::PrimitiveTopology::TriangleList,
1115                strip_index_format: None,
1116                front_face: wgpu::FrontFace::Ccw,
1117                cull_mode: None,
1118                unclipped_depth: false,
1119                polygon_mode: wgpu::PolygonMode::Fill,
1120                conservative: false,
1121            },
1122            depth_stencil: None,
1123            multisample: wgpu::MultisampleState::default(),
1124            fragment: Some(wgpu::FragmentState {
1125                module: &fragment.module,
1126                entry_point: Some(fragment.entry_point),
1127                compilation_options: wgpu::PipelineCompilationOptions::default(),
1128                targets: &[Some(wgpu::ColorTargetState {
1129                    format,
1130                    blend: None,
1131                    write_mask: wgpu::ColorWrites::ALL,
1132                })],
1133            }),
1134            multiview: None,
1135            cache: None,
1136        });
1137
1138        Self {
1139            pipeline: pipeline.into(),
1140            bind_group_layout: bind_group_layout.into(),
1141            sampler: sampler.into(),
1142        }
1143    }
1144}
1145
1146#[cfg(test)]
1147mod test {
1148    use crate::{
1149        atlas::{shader::AtlasTextureDescriptor, TextureAddressMode},
1150        context::Context,
1151        geometry::Vertex,
1152        material::Materials,
1153        test::BlockOnFuture,
1154    };
1155    use glam::{UVec3, Vec2, Vec3, Vec4};
1156
1157    use super::*;
1158
1159    #[test]
1160    // Ensures that textures are packed and rendered correctly.
1161    fn atlas_uv_mapping() {
1162        log::info!("{:?}", std::env::current_dir());
1163        let ctx = Context::headless(32, 32)
1164            .block()
1165            .with_default_atlas_texture_size(UVec3::new(1024, 1024, 2));
1166        let stage = ctx
1167            .new_stage()
1168            .with_background_color(Vec3::splat(0.0).extend(1.0))
1169            .with_bloom(false);
1170        let (projection, view) = crate::camera::default_ortho2d(32.0, 32.0);
1171        let _camera = stage
1172            .new_camera()
1173            .with_projection_and_view(projection, view);
1174        let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
1175        let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap();
1176        let texels = AtlasImage::from_path("../../test_img/atlas/uv_mapping.png").unwrap();
1177        log::info!("setting images");
1178        let atlas_entries = stage.set_images([dirt, sandstone, texels]).unwrap();
1179        log::info!("  done setting images");
1180
1181        let texels_entry = &atlas_entries[2];
1182
1183        let _rez = stage
1184            .new_primitive()
1185            .with_material(
1186                stage
1187                    .new_material()
1188                    .with_albedo_texture(texels_entry)
1189                    .with_has_lighting(false),
1190            )
1191            .with_vertices(stage.new_vertices({
1192                let tl = Vertex::default()
1193                    .with_position(Vec3::ZERO)
1194                    .with_uv0(Vec2::ZERO);
1195                let tr = Vertex::default()
1196                    .with_position(Vec3::new(1.0, 0.0, 0.0))
1197                    .with_uv0(Vec2::new(1.0, 0.0));
1198                let bl = Vertex::default()
1199                    .with_position(Vec3::new(0.0, 1.0, 0.0))
1200                    .with_uv0(Vec2::new(0.0, 1.0));
1201                let br = Vertex::default()
1202                    .with_position(Vec3::new(1.0, 1.0, 0.0))
1203                    .with_uv0(Vec2::splat(1.0));
1204                [tl, bl, br, tl, br, tr]
1205            }))
1206            .with_transform(stage.new_transform().with_scale(Vec3::new(32.0, 32.0, 1.0)));
1207
1208        log::info!("rendering");
1209        let frame = ctx.get_next_frame().unwrap();
1210        stage.render(&frame.view());
1211        let img = frame.read_image().block().unwrap();
1212        img_diff::assert_img_eq("atlas/uv_mapping.png", img);
1213    }
1214
1215    #[test]
1216    // Ensures that textures with different wrapping modes are rendered
1217    // correctly.
1218    fn uv_wrapping() {
1219        let icon_w = 32;
1220        let icon_h = 41;
1221        let sheet_w = icon_w * 3;
1222        let sheet_h = icon_h * 3;
1223        let w = sheet_w * 3 + 2;
1224        let h = sheet_h;
1225        let ctx = Context::headless(w, h).block();
1226        let stage = ctx
1227            .new_stage()
1228            .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
1229        let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);
1230        let _camera = stage
1231            .new_camera()
1232            .with_projection_and_view(projection, view);
1233        let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap();
1234        let entries = stage.set_images(std::iter::repeat_n(texels, 3)).unwrap();
1235        let clamp_tex = &entries[0];
1236        let repeat_tex = &entries[1];
1237        repeat_tex.set_modes(TextureModes {
1238            s: TextureAddressMode::Repeat,
1239            t: TextureAddressMode::Repeat,
1240        });
1241        let mirror_tex = &entries[2];
1242        mirror_tex.set_modes(TextureModes {
1243            s: TextureAddressMode::MirroredRepeat,
1244            t: TextureAddressMode::MirroredRepeat,
1245        });
1246
1247        let sheet_w = sheet_w as f32;
1248        let sheet_h = sheet_h as f32;
1249        let geometry = stage.new_vertices({
1250            let tl = Vertex::default()
1251                .with_position(Vec3::ZERO)
1252                .with_uv0(Vec2::ZERO);
1253            let tr = Vertex::default()
1254                .with_position(Vec3::new(sheet_w, 0.0, 0.0))
1255                .with_uv0(Vec2::new(3.0, 0.0));
1256            let bl = Vertex::default()
1257                .with_position(Vec3::new(0.0, sheet_h, 0.0))
1258                .with_uv0(Vec2::new(0.0, 3.0));
1259            let br = Vertex::default()
1260                .with_position(Vec3::new(sheet_w, sheet_h, 0.0))
1261                .with_uv0(Vec2::splat(3.0));
1262            [tl, bl, br, tl, br, tr]
1263        });
1264        let _clamp_rez = stage
1265            .new_primitive()
1266            .with_vertices(&geometry)
1267            .with_material(
1268                stage
1269                    .new_material()
1270                    .with_albedo_texture(clamp_tex)
1271                    .with_has_lighting(false),
1272            );
1273
1274        let _repeat_rez = stage
1275            .new_primitive()
1276            .with_transform(stage.new_transform().with_translation(Vec3::new(
1277                sheet_w + 1.0,
1278                0.0,
1279                0.0,
1280            )))
1281            .with_material(
1282                stage
1283                    .new_material()
1284                    .with_albedo_texture(repeat_tex)
1285                    .with_has_lighting(false),
1286            )
1287            .with_vertices(&geometry);
1288
1289        let _mirror_rez = stage
1290            .new_primitive()
1291            .with_transform(stage.new_transform().with_translation(Vec3::new(
1292                sheet_w * 2.0 + 2.0,
1293                0.0,
1294                0.0,
1295            )))
1296            .with_material(
1297                stage
1298                    .new_material()
1299                    .with_albedo_texture(mirror_tex)
1300                    .with_has_lighting(false),
1301            )
1302            .with_vertices(geometry);
1303
1304        let frame = ctx.get_next_frame().unwrap();
1305        stage.render(&frame.view());
1306        let img = frame.read_image().block().unwrap();
1307        img_diff::assert_img_eq("atlas/uv_wrapping.png", img);
1308    }
1309
1310    #[test]
1311    // Ensures that textures with negative uv coords wrap correctly
1312    fn negative_uv_wrapping() {
1313        let icon_w = 32;
1314        let icon_h = 41;
1315        let sheet_w = icon_w * 3;
1316        let sheet_h = icon_h * 3;
1317        let w = sheet_w * 3 + 2;
1318        let h = sheet_h;
1319        let ctx = Context::headless(w, h).block();
1320        let stage = ctx
1321            .new_stage()
1322            .with_background_color(Vec4::new(1.0, 1.0, 0.0, 1.0));
1323
1324        let (projection, view) = crate::camera::default_ortho2d(w as f32, h as f32);
1325        let _camera = stage
1326            .new_camera()
1327            .with_projection_and_view(projection, view);
1328
1329        let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap();
1330        let entries = stage.set_images(std::iter::repeat_n(texels, 3)).unwrap();
1331
1332        let clamp_tex = &entries[0];
1333        let repeat_tex = &entries[1];
1334        repeat_tex.set_modes(TextureModes {
1335            s: TextureAddressMode::Repeat,
1336            t: TextureAddressMode::Repeat,
1337        });
1338
1339        let mirror_tex = &entries[2];
1340        mirror_tex.set_modes(TextureModes {
1341            s: TextureAddressMode::MirroredRepeat,
1342            t: TextureAddressMode::MirroredRepeat,
1343        });
1344
1345        let sheet_w = sheet_w as f32;
1346        let sheet_h = sheet_h as f32;
1347        let geometry = stage.new_vertices({
1348            let tl = Vertex::default()
1349                .with_position(Vec3::ZERO)
1350                .with_uv0(Vec2::ZERO);
1351            let tr = Vertex::default()
1352                .with_position(Vec3::new(sheet_w, 0.0, 0.0))
1353                .with_uv0(Vec2::new(-3.0, 0.0));
1354            let bl = Vertex::default()
1355                .with_position(Vec3::new(0.0, sheet_h, 0.0))
1356                .with_uv0(Vec2::new(0.0, -3.0));
1357            let br = Vertex::default()
1358                .with_position(Vec3::new(sheet_w, sheet_h, 0.0))
1359                .with_uv0(Vec2::splat(-3.0));
1360            [tl, bl, br, tl, br, tr]
1361        });
1362        let _clamp_prim = stage
1363            .new_primitive()
1364            .with_vertices(&geometry)
1365            .with_material(
1366                stage
1367                    .new_material()
1368                    .with_albedo_texture(clamp_tex)
1369                    .with_has_lighting(false),
1370            );
1371
1372        let _repeat_rez = stage
1373            .new_primitive()
1374            .with_material(
1375                stage
1376                    .new_material()
1377                    .with_albedo_texture(repeat_tex)
1378                    .with_has_lighting(false),
1379            )
1380            .with_transform(stage.new_transform().with_translation(Vec3::new(
1381                sheet_w + 1.0,
1382                0.0,
1383                0.0,
1384            )))
1385            .with_vertices(&geometry);
1386
1387        let _mirror_rez = stage
1388            .new_primitive()
1389            .with_material(
1390                stage
1391                    .new_material()
1392                    .with_albedo_texture(mirror_tex)
1393                    .with_has_lighting(false),
1394            )
1395            .with_transform(stage.new_transform().with_translation(Vec3::new(
1396                sheet_w * 2.0 + 2.0,
1397                0.0,
1398                0.0,
1399            )))
1400            .with_vertices(&geometry);
1401
1402        let frame = ctx.get_next_frame().unwrap();
1403        stage.render(&frame.view());
1404        let img = frame.read_image().block().unwrap();
1405        img_diff::assert_img_eq("atlas/negative_uv_wrapping.png", img);
1406    }
1407
1408    #[test]
1409    fn transform_uvs_for_atlas() {
1410        let mut tex = AtlasTextureDescriptor {
1411            offset_px: UVec2::ZERO,
1412            size_px: UVec2::ONE,
1413            ..Default::default()
1414        };
1415        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(100)));
1416        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(1)));
1417        assert_eq!(Vec3::ZERO, tex.uv(Vec2::ZERO, UVec2::splat(256)));
1418        tex.offset_px = UVec2::splat(10);
1419        assert_eq!(
1420            Vec2::splat(0.1).extend(0.0),
1421            tex.uv(Vec2::ZERO, UVec2::splat(100))
1422        );
1423    }
1424
1425    #[test]
1426    fn can_load_and_read_atlas_texture_array() {
1427        // tests that the atlas lays out textures in the way we expect
1428        let ctx = Context::headless(100, 100)
1429            .block()
1430            .with_default_atlas_texture_size(UVec3::new(512, 512, 2));
1431        let stage = ctx.new_stage();
1432        let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
1433        let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap();
1434        let cheetah = AtlasImage::from_path("../../img/cheetah.jpg").unwrap();
1435        let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap();
1436        let _frames = stage
1437            .set_images([dirt, sandstone, cheetah, texels])
1438            .unwrap();
1439        let materials: &Materials = stage.as_ref();
1440        let img = materials.atlas().atlas_img(&ctx, 0).block();
1441        img_diff::assert_img_eq("atlas/array0.png", img);
1442        let img = materials.atlas().atlas_img(&ctx, 1).block();
1443        img_diff::assert_img_eq("atlas/array1.png", img);
1444    }
1445
1446    #[test]
1447    fn upkeep_trims_the_atlas() {
1448        // tests that Atlas::upkeep trims out unused images and repacks the
1449        // atlas
1450        let ctx = Context::headless(100, 100)
1451            .block()
1452            .with_default_atlas_texture_size(UVec3::new(512, 512, 2));
1453        let stage = ctx.new_stage();
1454        let dirt = AtlasImage::from_path("../../img/dirt.jpg").unwrap();
1455        let sandstone = AtlasImage::from_path("../../img/sandstone.png").unwrap();
1456        let cheetah = AtlasImage::from_path("../../img/cheetah.jpg").unwrap();
1457        let texels = AtlasImage::from_path("../../img/happy_mac.png").unwrap();
1458        let mut frames = stage
1459            .add_images([
1460                dirt,
1461                sandstone,
1462                cheetah,
1463                texels.clone(),
1464                texels.clone(),
1465                texels.clone(),
1466                texels.clone(),
1467                texels,
1468            ])
1469            .unwrap();
1470        let materials: &Materials = stage.as_ref();
1471        assert_eq!(8, materials.atlas().len());
1472
1473        frames.pop();
1474        frames.pop();
1475        frames.pop();
1476        frames.pop();
1477
1478        let _ = materials.atlas().upkeep(&ctx);
1479        assert_eq!(4, materials.atlas().len());
1480    }
1481}