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