Skip to main content

pfx_live/
content_atlas.rs

1use crate::frame::{Frame, InstanceSurface};
2use crate::maps::{ContentFormat, MipRegion, check_write, mip_count, mip_regions};
3use pfx_gpu::floor::TEXTURE_SIDE;
4use pfx_gpu::pace::{Pacer, Slice, Stats, Turned, Work};
5use pfx_gpu::wgpu;
6use std::fmt;
7
8pub const DEFAULT_BLEED_LEVEL: u32 = 4;
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub enum AtlasError {
12    BadSide {
13        width: u32,
14        height: u32,
15        limit: u32,
16    },
17    BadLevel {
18        level: u32,
19        largest: u32,
20    },
21    EmptyImage,
22    BadTexels {
23        width: u32,
24        height: u32,
25        expected: usize,
26        got: usize,
27    },
28    TooLarge {
29        cell: [u32; 2],
30        atlas: [u32; 2],
31    },
32    Full {
33        cell: [u32; 2],
34        atlas: [u32; 2],
35        images: usize,
36        used: u64,
37    },
38}
39
40impl fmt::Display for AtlasError {
41    fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
42        match self {
43            Self::BadSide {
44                width,
45                height,
46                limit,
47            } => write!(
48                out,
49                "a {width}x{height} atlas must have sides from 1 to {limit}"
50            ),
51            Self::BadLevel { level, largest } => write!(
52                out,
53                "gutters for mip level {level} do not fit this atlas, whose largest level is {largest}"
54            ),
55            Self::EmptyImage => write!(out, "an atlas image has no texels"),
56            Self::BadTexels {
57                width,
58                height,
59                expected,
60                got,
61            } => write!(
62                out,
63                "a {width}x{height} atlas image needs {expected} bytes, got {got}"
64            ),
65            Self::TooLarge { cell, atlas } => write!(
66                out,
67                "an image with its gutters takes {}x{} texels, more than the {}x{} atlas",
68                cell[0], cell[1], atlas[0], atlas[1]
69            ),
70            Self::Full {
71                cell,
72                atlas,
73                images,
74                used,
75            } => write!(
76                out,
77                "the {}x{} atlas is full: {images} images hold {used} texels and no shelf takes another {}x{} cell",
78                atlas[0], atlas[1], cell[0], cell[1]
79            ),
80        }
81    }
82}
83
84impl std::error::Error for AtlasError {}
85
86impl From<AtlasError> for String {
87    fn from(error: AtlasError) -> Self {
88        error.to_string()
89    }
90}
91
92#[derive(Clone, Debug, PartialEq, Eq)]
93struct Shelf {
94    y: u32,
95    height: u32,
96    cursor: u32,
97}
98
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub struct Packer {
101    width: u32,
102    height: u32,
103    shelves: Vec<Shelf>,
104}
105
106impl Packer {
107    pub fn new(width: u32, height: u32) -> Self {
108        Self {
109            width,
110            height,
111            shelves: Vec::new(),
112        }
113    }
114
115    pub fn size(&self) -> [u32; 2] {
116        [self.width, self.height]
117    }
118
119    pub fn place(&mut self, width: u32, height: u32) -> Option<[u32; 2]> {
120        if width == 0 || height == 0 || width > self.width || height > self.height {
121            return None;
122        }
123        let best = self
124            .shelves
125            .iter()
126            .enumerate()
127            .filter(|(_, shelf)| shelf.height >= height && shelf.cursor + width <= self.width)
128            .min_by_key(|(index, shelf)| (shelf.height, *index))
129            .map(|(index, _)| index);
130        if let Some(index) = best {
131            let shelf = &mut self.shelves[index];
132            let at = [shelf.cursor, shelf.y];
133            shelf.cursor += width;
134            return Some(at);
135        }
136        let y = self
137            .shelves
138            .last()
139            .map_or(0, |shelf| shelf.y + shelf.height);
140        if y.checked_add(height).is_none_or(|end| end > self.height) {
141            return None;
142        }
143        self.shelves.push(Shelf {
144            y,
145            height,
146            cursor: width,
147        });
148        Some([0, y])
149    }
150}
151
152pub fn texture_bytes(width: u32, height: u32, format: ContentFormat) -> u64 {
153    (0..mip_count(width, height))
154        .map(|level| {
155            u64::from((width >> level).max(1))
156                * u64::from((height >> level).max(1))
157                * u64::from(format.texel_bytes())
158        })
159        .sum()
160}
161
162pub fn gutter(level: u32) -> u32 {
163    1 << level
164}
165
166pub fn cell_size(image: [u32; 2], level: u32) -> [u32; 2] {
167    let block = gutter(level);
168    image.map(|side| side.div_ceil(block) * block + 2 * block)
169}
170
171pub fn cards_that_fit(atlas: [u32; 2], image: [u32; 2], level: u32) -> u32 {
172    let cell = cell_size(image, level);
173    (atlas[0] / cell[0]) * (atlas[1] / cell[1])
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
177pub struct Placement {
178    pub rect: [u32; 4],
179    pub cell: [u32; 4],
180}
181
182impl Placement {
183    pub fn uv_offset(&self, atlas: [u32; 2]) -> [f32; 2] {
184        [
185            self.rect[0] as f32 / atlas[0] as f32,
186            self.rect[1] as f32 / atlas[1] as f32,
187        ]
188    }
189
190    pub fn uv_scale(&self, atlas: [u32; 2]) -> [f32; 2] {
191        [
192            self.rect[2] as f32 / atlas[0] as f32,
193            self.rect[3] as f32 / atlas[1] as f32,
194        ]
195    }
196
197    pub fn crop(&self, atlas: [u32; 2]) -> [f32; 4] {
198        let [x, y, width, height] = self.rect;
199        [
200            x as f32 / atlas[0] as f32,
201            y as f32 / atlas[1] as f32,
202            (x + width) as f32 / atlas[0] as f32,
203            (y + height) as f32 / atlas[1] as f32,
204        ]
205    }
206
207    pub fn surface(&self, atlas: [u32; 2]) -> InstanceSurface {
208        InstanceSurface {
209            uv_offset: self.uv_offset(atlas),
210            uv_scale: self.uv_scale(atlas),
211            crop: self.crop(atlas),
212            ..InstanceSurface::default()
213        }
214    }
215}
216
217#[derive(Clone, Copy, Debug)]
218pub struct AtlasImage<'a> {
219    pub width: u32,
220    pub height: u32,
221    pub texels: &'a [u8],
222}
223
224pub fn padded_cell(
225    image: &AtlasImage<'_>,
226    format: ContentFormat,
227    level: u32,
228) -> Result<Vec<u8>, AtlasError> {
229    if image.width == 0 || image.height == 0 {
230        return Err(AtlasError::EmptyImage);
231    }
232    let texel = format.texel_bytes() as usize;
233    let expected = image.width as usize * image.height as usize * texel;
234    if image.texels.len() != expected {
235        return Err(AtlasError::BadTexels {
236            width: image.width,
237            height: image.height,
238            expected,
239            got: image.texels.len(),
240        });
241    }
242    let block = gutter(level);
243    let [cell_width, cell_height] = cell_size([image.width, image.height], level);
244    let mut cell = Vec::with_capacity(cell_width as usize * cell_height as usize * texel);
245    for row in 0..cell_height {
246        let y = row.saturating_sub(block).min(image.height - 1) as usize;
247        let source =
248            &image.texels[y * image.width as usize * texel..][..image.width as usize * texel];
249        for column in 0..cell_width {
250            let x = column.saturating_sub(block).min(image.width - 1) as usize;
251            cell.extend_from_slice(&source[x * texel..][..texel]);
252        }
253    }
254    Ok(cell)
255}
256
257pub struct ContentAtlas {
258    packer: Packer,
259    format: ContentFormat,
260    level: u32,
261    placements: Vec<Placement>,
262    pending: Vec<(usize, Vec<u8>)>,
263    used: u64,
264}
265
266impl ContentAtlas {
267    pub fn new(
268        width: u32,
269        height: u32,
270        format: ContentFormat,
271        level: u32,
272    ) -> Result<Self, AtlasError> {
273        if width == 0 || height == 0 || width > TEXTURE_SIDE || height > TEXTURE_SIDE {
274            return Err(AtlasError::BadSide {
275                width,
276                height,
277                limit: TEXTURE_SIDE,
278            });
279        }
280        let largest = mip_count(width, height) - 1;
281        if level > largest || 4 * gutter(level) > width.min(height) {
282            return Err(AtlasError::BadLevel { level, largest });
283        }
284        Ok(Self {
285            packer: Packer::new(width, height),
286            format,
287            level,
288            placements: Vec::new(),
289            pending: Vec::new(),
290            used: 0,
291        })
292    }
293
294    pub fn size(&self) -> [u32; 2] {
295        self.packer.size()
296    }
297
298    pub fn format(&self) -> ContentFormat {
299        self.format
300    }
301
302    pub fn level(&self) -> u32 {
303        self.level
304    }
305
306    pub fn len(&self) -> usize {
307        self.placements.len()
308    }
309
310    pub fn is_empty(&self) -> bool {
311        self.placements.is_empty()
312    }
313
314    pub fn pending(&self) -> usize {
315        self.pending.len()
316    }
317
318    pub fn vram_bytes(&self) -> u64 {
319        let [width, height] = self.size();
320        texture_bytes(width, height, self.format)
321    }
322
323    pub fn placement(&self, id: usize) -> Option<&Placement> {
324        self.placements.get(id)
325    }
326
327    pub fn crop(&self, id: usize) -> Option<[f32; 4]> {
328        self.placement(id).map(|place| place.crop(self.size()))
329    }
330
331    pub fn surface(&self, id: usize) -> Option<InstanceSurface> {
332        self.placement(id).map(|place| place.surface(self.size()))
333    }
334
335    pub fn add(&mut self, image: &AtlasImage<'_>) -> Result<usize, AtlasError> {
336        let cell = padded_cell(image, self.format, self.level)?;
337        let [cell_width, cell_height] = cell_size([image.width, image.height], self.level);
338        let atlas = self.size();
339        if cell_width > atlas[0] || cell_height > atlas[1] {
340            return Err(AtlasError::TooLarge {
341                cell: [cell_width, cell_height],
342                atlas,
343            });
344        }
345        let Some([x, y]) = self.packer.place(cell_width, cell_height) else {
346            return Err(AtlasError::Full {
347                cell: [cell_width, cell_height],
348                atlas,
349                images: self.placements.len(),
350                used: self.used,
351            });
352        };
353        let block = gutter(self.level);
354        let id = self.placements.len();
355        self.placements.push(Placement {
356            rect: [x + block, y + block, image.width, image.height],
357            cell: [x, y, cell_width, cell_height],
358        });
359        self.pending.push((id, cell));
360        self.used += u64::from(cell_width) * u64::from(cell_height);
361        Ok(id)
362    }
363
364    pub fn add_all(&mut self, images: &[AtlasImage<'_>]) -> Result<Vec<usize>, AtlasError> {
365        let mut order: Vec<usize> = (0..images.len()).collect();
366        order.sort_by_key(|&index| (std::cmp::Reverse(images[index].height), index));
367        let saved = (
368            self.packer.clone(),
369            self.placements.len(),
370            self.pending.len(),
371            self.used,
372        );
373        let first = self.placements.len();
374        let mut ids = vec![0; images.len()];
375        for index in order {
376            match self.add(&images[index]) {
377                Ok(id) => ids[index] = id,
378                Err(error) => {
379                    self.packer = saved.0;
380                    self.placements.truncate(saved.1);
381                    self.pending.truncate(saved.2);
382                    self.used = saved.3;
383                    return Err(error);
384                }
385            }
386        }
387        debug_assert!(ids.iter().all(|&id| id >= first));
388        Ok(ids)
389    }
390
391    pub fn allocate(&self, frame: &Frame, slot: usize) -> Result<(), String> {
392        let [width, height] = self.size();
393        frame.set_content(slot, width, height, self.format)
394    }
395
396    fn check_target(&self, frame: &Frame, slot: usize) -> Result<(), String> {
397        let atlas = self.size();
398        let content = frame.content();
399        let target = content
400            .get(slot)
401            .ok_or_else(|| format!("content slot {slot} is not allocated"))?;
402        if [target.width, target.height] != atlas || target.format != self.format {
403            return Err(format!(
404                "content slot {slot} holds {}x{} {:?}, not this {}x{} {:?} atlas",
405                target.width, target.height, target.format, atlas[0], atlas[1], self.format
406            ));
407        }
408        for (id, cell) in &self.pending {
409            check_write(atlas, self.format, self.placements[*id].cell, cell.len())?;
410        }
411        Ok(())
412    }
413
414    fn write_cell(&self, frame: &Frame, slot: usize, index: usize) {
415        let content = frame.content();
416        let Some(target) = content.get(slot) else {
417            return;
418        };
419        let (id, cell) = &self.pending[index];
420        let rect = self.placements[*id].cell;
421        frame.gpu.queue.write_texture(
422            wgpu::TexelCopyTextureInfo {
423                texture: &target.texture,
424                mip_level: 0,
425                origin: wgpu::Origin3d {
426                    x: rect[0],
427                    y: rect[1],
428                    z: 0,
429                },
430                aspect: wgpu::TextureAspect::All,
431            },
432            cell,
433            wgpu::TexelCopyBufferLayout {
434                offset: 0,
435                bytes_per_row: Some(rect[2] * self.format.texel_bytes()),
436                rows_per_image: Some(rect[3]),
437            },
438            wgpu::Extent3d {
439                width: rect[2],
440                height: rect[3],
441                depth_or_array_layers: 1,
442            },
443        );
444    }
445
446    fn pending_regions(&self) -> Vec<MipRegion> {
447        let size = self.size();
448        let mut regions: Vec<MipRegion> = self
449            .pending
450            .iter()
451            .flat_map(|(id, _)| mip_regions(size, self.placements[*id].cell))
452            .collect();
453        regions.sort_by_key(|region| region.level);
454        regions
455    }
456
457    pub fn upload(&mut self, frame: &Frame, slot: usize) -> Result<usize, String> {
458        if self.pending.is_empty() {
459            return Ok(0);
460        }
461        self.check_target(frame, slot)?;
462        for index in 0..self.pending.len() {
463            self.write_cell(frame, slot, index);
464        }
465        let regions = self.pending_regions();
466        let mut encoder =
467            frame
468                .gpu
469                .device
470                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
471                    label: Some("content atlas mips"),
472                });
473        frame.regenerate_content_regions(&mut encoder, slot, &regions)?;
474        frame.gpu.queue.submit(Some(encoder.finish()));
475        let written = self.pending.len();
476        self.pending.clear();
477        Ok(written)
478    }
479
480    pub fn upload_paced<T: Turned>(
481        &mut self,
482        frame: &Frame,
483        slot: usize,
484        pacer: &mut Pacer,
485        between: impl FnMut(f64) -> T,
486    ) -> Result<(usize, Stats), String> {
487        if self.pending.is_empty() {
488            return Ok((0, Stats::default()));
489        }
490        self.check_target(frame, slot)?;
491        let regions = self.pending_regions();
492        let writes = self.pending.len();
493        let mut failure: Option<String> = None;
494        let stats = pacer.run(
495            "content atlas mips",
496            &frame.gpu.device,
497            &frame.gpu.queue,
498            Work::Dispatches {
499                count: (writes + regions.len()) as u32,
500            },
501            |encoder, slice| {
502                let Slice::Dispatches { start, count } = slice else {
503                    return;
504                };
505                let (start, end) = (start as usize, (start + count) as usize);
506                for index in start..end.min(writes) {
507                    self.write_cell(frame, slot, index);
508                }
509                let first = start.saturating_sub(writes);
510                let last = end.saturating_sub(writes);
511                if first < last
512                    && failure.is_none()
513                    && let Err(error) =
514                        frame.regenerate_content_regions(encoder, slot, &regions[first..last])
515                {
516                    failure = Some(error);
517                }
518            },
519            between,
520        )?;
521        if let Some(error) = failure {
522            return Err(error);
523        }
524        self.pending.clear();
525        Ok((writes, stats))
526    }
527}
528
529#[cfg(test)]
530mod tests {
531    use super::*;
532    use crate::frame::{
533        Camera, Instance, Matrix, MeshData, Scene, SceneWind, Sun, multiply, transform,
534    };
535    use pfx_gpu::Gpu;
536    use pfx_materials::{Content, ContentLayer, Material};
537
538    fn flat(width: u32, height: u32, rgba: [u8; 4]) -> Vec<u8> {
539        rgba.repeat(width as usize * height as usize)
540    }
541
542    fn marked(width: u32, height: u32) -> Vec<u8> {
543        (0..width * height)
544            .flat_map(|texel| {
545                let (x, y) = (texel % width, texel / width);
546                [x as u8, y as u8, (x ^ y) as u8, 255]
547            })
548            .collect()
549    }
550
551    fn overlap(a: [u32; 4], b: [u32; 4]) -> bool {
552        a[0] < b[0] + b[2] && b[0] < a[0] + a[2] && a[1] < b[1] + b[3] && b[1] < a[1] + a[3]
553    }
554
555    fn sizes() -> Vec<[u32; 2]> {
556        (0..40)
557            .map(|i| [20 + (i * 37) % 90, 12 + (i * 53) % 110])
558            .collect()
559    }
560
561    fn pack(level: u32) -> (ContentAtlas, Vec<usize>) {
562        let data: Vec<Vec<u8>> = sizes().iter().map(|s| marked(s[0], s[1])).collect();
563        let images: Vec<AtlasImage> = sizes()
564            .iter()
565            .zip(&data)
566            .map(|(s, t)| AtlasImage {
567                width: s[0],
568                height: s[1],
569                texels: t,
570            })
571            .collect();
572        let mut atlas = ContentAtlas::new(1024, 1024, ContentFormat::Srgb8, level).unwrap();
573        let ids = atlas.add_all(&images).unwrap();
574        (atlas, ids)
575    }
576
577    #[test]
578    fn cells_never_overlap_and_stay_inside_the_atlas() {
579        for level in [0, 2, 4] {
580            let (atlas, ids) = pack(level);
581            assert_eq!(ids.len(), 40);
582            let cells: Vec<[u32; 4]> = ids
583                .iter()
584                .map(|&id| atlas.placement(id).unwrap().cell)
585                .collect();
586            for (i, a) in cells.iter().enumerate() {
587                assert!(a[0] + a[2] <= 1024 && a[1] + a[3] <= 1024, "{a:?}");
588                for b in &cells[i + 1..] {
589                    assert!(!overlap(*a, *b), "{a:?} overlaps {b:?}");
590                }
591            }
592            for (&id, size) in ids.iter().zip(sizes()) {
593                let place = atlas.placement(id).unwrap();
594                let block = gutter(level);
595                assert_eq!([place.rect[2], place.rect[3]], size);
596                assert_eq!(place.rect[0] % block, 0);
597                assert_eq!(place.rect[1] % block, 0);
598                assert_eq!(place.cell[0] % block, 0);
599                assert_eq!(place.cell[2], cell_size(size, level)[0]);
600                assert!(place.rect[0] >= place.cell[0] + block);
601                assert!(place.rect[0] + place.rect[2] + block <= place.cell[0] + place.cell[2]);
602                assert!(place.rect[1] + place.rect[3] + block <= place.cell[1] + place.cell[3]);
603            }
604        }
605    }
606
607    #[test]
608    fn the_same_images_in_the_same_order_land_in_the_same_places() {
609        let (a, ids_a) = pack(3);
610        let (b, ids_b) = pack(3);
611        assert_eq!(ids_a, ids_b);
612        for id in ids_a {
613            assert_eq!(a.placement(id), b.placement(id));
614        }
615        let (c, _) = pack(4);
616        assert_ne!(a.placement(0), c.placement(0));
617    }
618
619    #[test]
620    fn a_batch_returns_ids_in_input_order_whatever_the_packing_order() {
621        let (atlas, ids) = pack(2);
622        let mut distinct = ids.clone();
623        distinct.sort();
624        assert_eq!(distinct, (0..40).collect::<Vec<_>>());
625        for (&id, size) in ids.iter().zip(sizes().iter()) {
626            let place = atlas.placement(id).unwrap();
627            assert_eq!([place.rect[2], place.rect[3]], *size);
628        }
629    }
630
631    #[test]
632    fn gutters_hold_the_images_own_edge_texels() {
633        for level in [0, 1, 3] {
634            let (width, height) = (13, 9);
635            let texels = marked(width, height);
636            let cell = padded_cell(
637                &AtlasImage {
638                    width,
639                    height,
640                    texels: &texels,
641                },
642                ContentFormat::Srgb8,
643                level,
644            )
645            .unwrap();
646            let block = gutter(level);
647            let [cell_width, cell_height] = cell_size([width, height], level);
648            assert_eq!(cell.len(), (cell_width * cell_height * 4) as usize);
649            for row in 0..cell_height {
650                for column in 0..cell_width {
651                    let x = column.saturating_sub(block).min(width - 1);
652                    let y = row.saturating_sub(block).min(height - 1);
653                    let want = &texels[((y * width + x) * 4) as usize..][..4];
654                    let got = &cell[((row * cell_width + column) * 4) as usize..][..4];
655                    assert_eq!(got, want, "level {level} at {column},{row}");
656                }
657            }
658            let inside =
659                |column: u32, row: u32| &cell[((row * cell_width + column) * 4) as usize..][..4];
660            assert_eq!(inside(block, block), &texels[0..4]);
661            assert_eq!(inside(0, 0), &texels[0..4]);
662            assert_eq!(
663                inside(cell_width - 1, cell_height - 1),
664                &texels[(((height - 1) * width + width - 1) * 4) as usize..][..4]
665            );
666        }
667    }
668
669    #[test]
670    fn sixteen_bit_gutters_copy_whole_texels() {
671        let texels: Vec<u8> = (0..3 * 2 * 8).map(|i| i as u8).collect();
672        let cell = padded_cell(
673            &AtlasImage {
674                width: 3,
675                height: 2,
676                texels: &texels,
677            },
678            ContentFormat::Linear16,
679            0,
680        )
681        .unwrap();
682        let [width, height] = cell_size([3, 2], 0);
683        assert_eq!(cell.len(), (width * height * 8) as usize);
684        assert_eq!(&cell[..8], &texels[..8]);
685        assert_eq!(&cell[8..16], &texels[..8]);
686        let last = ((height - 1) * width + width - 1) as usize * 8;
687        assert_eq!(&cell[last..last + 8], &texels[5 * 8..6 * 8]);
688    }
689
690    #[test]
691    fn crop_maths_matches_the_instance_surface() {
692        let (atlas, ids) = pack(3);
693        let size = atlas.size();
694        for id in ids {
695            let place = *atlas.placement(id).unwrap();
696            let surface = atlas.surface(id).unwrap();
697            assert_eq!(surface.crop, atlas.crop(id).unwrap());
698            assert_eq!(surface.uv_offset, place.uv_offset(size));
699            assert_eq!(surface.uv_scale, place.uv_scale(size));
700            let [x, y, width, height] = place.rect;
701            let texel = |u: f32, v: f32| {
702                let at = surface.content_uv([u, v]).unwrap();
703                [at[0] * size[0] as f32, at[1] * size[1] as f32]
704            };
705            let corner = texel(0.0, 0.0);
706            assert!((corner[0] - x as f32).abs() < 1e-3 && (corner[1] - y as f32).abs() < 1e-3);
707            let far = texel(1.0, 1.0);
708            assert!((far[0] - (x + width) as f32).abs() < 1e-2);
709            assert!((far[1] - (y + height) as f32).abs() < 1e-2);
710            let middle = texel(0.5, 0.5);
711            assert!((middle[0] - (x as f32 + width as f32 / 2.0)).abs() < 1e-2);
712            assert!(surface.content_uv([-0.05, 0.5]).is_none());
713            assert!(surface.content_uv([0.5, 1.05]).is_none());
714        }
715    }
716
717    #[test]
718    fn adding_to_a_live_atlas_moves_nothing_until_it_is_full() {
719        let mut atlas = ContentAtlas::new(256, 256, ContentFormat::Srgb8, 2).unwrap();
720        let tile = flat(40, 40, [9, 9, 9, 255]);
721        let image = AtlasImage {
722            width: 40,
723            height: 40,
724            texels: &tile,
725        };
726        let mut seen = Vec::new();
727        let error = loop {
728            match atlas.add(&image) {
729                Ok(id) => {
730                    seen.push(*atlas.placement(id).unwrap());
731                    for (before, now) in seen.iter().zip((0..=id).map(|i| atlas.placement(i))) {
732                        assert_eq!(Some(before), now);
733                    }
734                }
735                Err(error) => break error,
736            }
737        };
738        assert_eq!(seen.len(), 25);
739        assert_eq!(
740            error,
741            AtlasError::Full {
742                cell: [48, 48],
743                atlas: [256, 256],
744                images: 25,
745                used: 25 * 48 * 48,
746            }
747        );
748        assert!(error.to_string().contains("full"));
749        assert_eq!(atlas.len(), 25);
750        assert_eq!(atlas.add(&image).unwrap_err(), error);
751    }
752
753    #[test]
754    fn a_later_small_image_fills_a_gap_without_moving_the_first() {
755        let mut atlas = ContentAtlas::new(128, 128, ContentFormat::Srgb8, 0).unwrap();
756        let wide = flat(100, 20, [1, 1, 1, 255]);
757        let first = atlas
758            .add(&AtlasImage {
759                width: 100,
760                height: 20,
761                texels: &wide,
762            })
763            .unwrap();
764        let before = *atlas.placement(first).unwrap();
765        let small = flat(10, 10, [2, 2, 2, 255]);
766        let second = atlas
767            .add(&AtlasImage {
768                width: 10,
769                height: 10,
770                texels: &small,
771            })
772            .unwrap();
773        assert_eq!(*atlas.placement(first).unwrap(), before);
774        assert!(!overlap(
775            atlas.placement(first).unwrap().cell,
776            atlas.placement(second).unwrap().cell
777        ));
778    }
779
780    #[test]
781    fn a_failed_batch_leaves_the_atlas_as_it_was() {
782        let mut atlas = ContentAtlas::new(128, 128, ContentFormat::Srgb8, 1).unwrap();
783        let tile = flat(50, 50, [5, 5, 5, 255]);
784        let image = AtlasImage {
785            width: 50,
786            height: 50,
787            texels: &tile,
788        };
789        atlas.add(&image).unwrap();
790        let before = *atlas.placement(0).unwrap();
791        let used = atlas.pending();
792        assert!(atlas.add_all(&[image, image, image, image, image]).is_err());
793        assert_eq!(atlas.len(), 1);
794        assert_eq!(atlas.pending(), used);
795        assert_eq!(*atlas.placement(0).unwrap(), before);
796        let ids = atlas.add_all(&[image, image]).unwrap();
797        assert_eq!(ids, vec![1, 2]);
798    }
799
800    #[test]
801    fn bad_images_and_sides_are_refused_with_a_reason() {
802        assert!(ContentAtlas::new(0, 64, ContentFormat::Srgb8, 0).is_err());
803        assert!(ContentAtlas::new(16385, 64, ContentFormat::Srgb8, 0).is_err());
804        assert!(ContentAtlas::new(64, 64, ContentFormat::Srgb8, 5).is_err());
805        assert!(ContentAtlas::new(16384, 16384, ContentFormat::Srgb8, 14).is_err());
806        assert!(ContentAtlas::new(16384, 16384, ContentFormat::Srgb8, 4).is_ok());
807        let mut atlas = ContentAtlas::new(64, 64, ContentFormat::Srgb8, 2).unwrap();
808        let short = AtlasImage {
809            width: 4,
810            height: 4,
811            texels: &[0; 10],
812        };
813        assert!(matches!(
814            atlas.add(&short),
815            Err(AtlasError::BadTexels {
816                expected: 64,
817                got: 10,
818                ..
819            })
820        ));
821        let empty = AtlasImage {
822            width: 0,
823            height: 4,
824            texels: &[],
825        };
826        assert_eq!(atlas.add(&empty), Err(AtlasError::EmptyImage));
827        let big = flat(60, 8, [0; 4]);
828        let big = AtlasImage {
829            width: 60,
830            height: 8,
831            texels: &big,
832        };
833        assert!(matches!(atlas.add(&big), Err(AtlasError::TooLarge { .. })));
834        assert!(atlas.is_empty());
835    }
836
837    #[test]
838    fn five_hundred_twelve_cards_fit_in_the_stated_numbers() {
839        let card = [512, 512];
840        assert_eq!(cell_size(card, DEFAULT_BLEED_LEVEL), [544, 544]);
841        assert_eq!(cards_that_fit([8192, 8192], card, DEFAULT_BLEED_LEVEL), 225);
842        assert_eq!(
843            cards_that_fit([16384, 16384], card, DEFAULT_BLEED_LEVEL),
844            900
845        );
846        assert_eq!(cards_that_fit([8192, 8192], card, 0), 15 * 15);
847        assert_eq!(cards_that_fit([16384, 16384], card, 0), 31 * 31);
848        let mib = |bytes: u64| bytes as f64 / (1024.0 * 1024.0);
849        assert!((mib(texture_bytes(8192, 8192, ContentFormat::Srgb8)) - 341.33).abs() < 0.01);
850        assert!((mib(texture_bytes(16384, 16384, ContentFormat::Srgb8)) - 1365.33).abs() < 0.01);
851        assert!((mib(texture_bytes(8192, 8192, ContentFormat::Linear16)) - 682.67).abs() < 0.01);
852        for (side, want) in [(8192u32, 225usize), (16384, 900)] {
853            let mut packer = Packer::new(side, side);
854            let mut placed = Vec::new();
855            while let Some(at) = packer.place(544, 544) {
856                placed.push(at);
857            }
858            assert_eq!(placed.len(), want);
859            assert_eq!(placed[0], [0, 0]);
860            assert_eq!(placed[1], [544, 0]);
861        }
862    }
863
864    #[test]
865    fn the_packer_is_deterministic_and_best_fits_shelves() {
866        let mut a = Packer::new(100, 100);
867        let mut b = Packer::new(100, 100);
868        let asks = [[40, 30], [40, 50], [20, 28], [60, 50], [10, 10], [50, 20]];
869        let placed_a: Vec<_> = asks.iter().map(|s| a.place(s[0], s[1])).collect();
870        let placed_b: Vec<_> = asks.iter().map(|s| b.place(s[0], s[1])).collect();
871        assert_eq!(placed_a, placed_b);
872        assert_eq!(
873            placed_a,
874            [
875                Some([0, 0]),
876                Some([0, 30]),
877                Some([40, 0]),
878                Some([40, 30]),
879                Some([60, 0]),
880                Some([0, 80]),
881            ]
882        );
883        let placed: Vec<[u32; 4]> = asks
884            .iter()
885            .zip(&placed_a)
886            .filter_map(|(s, at)| at.map(|at| [at[0], at[1], s[0], s[1]]))
887            .collect();
888        for (i, p) in placed.iter().enumerate() {
889            for q in &placed[i + 1..] {
890                assert!(!overlap(*p, *q));
891            }
892        }
893        assert_eq!(Packer::new(10, 10).place(11, 1), None);
894        assert_eq!(Packer::new(10, 10).place(0, 1), None);
895    }
896
897    fn card_texels(card: usize, side: u32) -> Vec<u8> {
898        let mut texels = Vec::with_capacity((side * side * 4) as usize);
899        for y in 0..side {
900            for x in 0..side {
901                texels.extend(quadrant_colour(
902                    card,
903                    usize::from(x >= side / 2) + 2 * usize::from(y >= side / 2),
904                ));
905            }
906        }
907        texels
908    }
909
910    fn quadrant_colour(card: usize, quadrant: usize) -> [u8; 4] {
911        let k = card * 4 + quadrant;
912        [
913            ((k * 53 + 17) % 256) as u8,
914            ((k * 97 + 101) % 256) as u8,
915            ((k * 193 + 7) % 256) as u8,
916            255,
917        ]
918    }
919
920    fn linear(value: u8) -> f32 {
921        pfx_materials::linear_channel(f32::from(value) / 255.0)
922    }
923
924    fn half(value: u16) -> f32 {
925        half::f16::from_bits(value).to_f32()
926    }
927
928    fn identity() -> Matrix {
929        let mut m = [[0.0; 4]; 4];
930        for (i, column) in m.iter_mut().enumerate() {
931            column[i] = 1.0;
932        }
933        m
934    }
935
936    fn perspective() -> Matrix {
937        let (near, far) = (0.1, 100.0);
938        let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
939        [
940            [f, 0.0, 0.0, 0.0],
941            [0.0, f, 0.0, 0.0],
942            [0.0, 0.0, far / (near - far), -1.0],
943            [0.0, 0.0, far * near / (near - far), 0.0],
944        ]
945    }
946
947    fn look_down(height: f32) -> Matrix {
948        [
949            [1.0, 0.0, 0.0, 0.0],
950            [0.0, 0.0, 1.0, 0.0],
951            [0.0, -1.0, 0.0, 0.0],
952            [0.0, 0.0, -height, 1.0],
953        ]
954    }
955
956    struct Sheet {
957        frame_px: u32,
958        card_px: u32,
959        image: u32,
960        atlas: u32,
961        level: u32,
962    }
963
964    #[derive(Debug)]
965    struct Worst {
966        error: f32,
967        card: usize,
968        quadrant: usize,
969        at: [u32; 2],
970        pixels: usize,
971        seen: [f32; 3],
972    }
973
974    impl Worst {
975        fn describe(&self) -> String {
976            format!(
977                "worst {:.5}/255 on card {} quadrant {} at pixel {:?} of {} checked, got/want {:?}",
978                self.error * 255.0,
979                self.card,
980                self.quadrant,
981                self.at,
982                self.pixels,
983                self.seen
984            )
985        }
986    }
987
988    fn render_sheet(sheet: &Sheet) -> Worst {
989        let Sheet {
990            frame_px,
991            card_px,
992            image,
993            atlas: atlas_side,
994            level,
995        } = *sheet;
996        let per_side = (frame_px / card_px) as usize;
997        let cards = per_side * per_side;
998        assert_eq!(cards, 64);
999        let gpu = pollster::block_on(Gpu::headless()).unwrap();
1000        let mut frame = Frame::new(gpu, frame_px, frame_px).unwrap();
1001        let data: Vec<Vec<u8>> = (0..cards).map(|card| card_texels(card, image)).collect();
1002        let images: Vec<AtlasImage> = data
1003            .iter()
1004            .map(|texels| AtlasImage {
1005                width: image,
1006                height: image,
1007                texels,
1008            })
1009            .collect();
1010        let mut atlas =
1011            ContentAtlas::new(atlas_side, atlas_side, ContentFormat::Srgb8, level).unwrap();
1012        atlas.allocate(&frame, 0).unwrap();
1013        let mut ids = atlas.add_all(&images[..cards / 2]).unwrap();
1014        assert_eq!(atlas.upload(&frame, 0).unwrap(), cards / 2);
1015        let before: Vec<Placement> = ids
1016            .iter()
1017            .map(|&id| *atlas.placement(id).unwrap())
1018            .collect();
1019        ids.extend(atlas.add_all(&images[cards / 2..]).unwrap());
1020        assert_eq!(atlas.upload(&frame, 0).unwrap(), cards / 2);
1021        assert_eq!(atlas.upload(&frame, 0).unwrap(), 0);
1022        for (id, place) in ids.iter().zip(&before) {
1023            assert_eq!(atlas.placement(*id).unwrap(), place, "adding moved a card");
1024        }
1025
1026        let quad = frame
1027            .upload_mesh(MeshData {
1028                positions: &[
1029                    [-0.5, 0.0, -0.5],
1030                    [0.5, 0.0, -0.5],
1031                    [-0.5, 0.0, 0.5],
1032                    [0.5, 0.0, 0.5],
1033                ],
1034                normals: &[[0.0, 1.0, 0.0]; 4],
1035                tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
1036                uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
1037                uvs1: None,
1038                alpha: None,
1039                indices: &[0, 2, 1, 1, 2, 3],
1040            })
1041            .unwrap();
1042        let height = 6.0;
1043        let view = look_down(height);
1044        let projection = perspective();
1045        let view_projection = multiply(projection, view);
1046        let pitch = 2.0 * height * 25.0_f32.to_radians().tan() / frame_px as f32;
1047        let span = pitch * card_px as f32;
1048        let centre = |index: usize| {
1049            [
1050                ((index % per_side) as f32 - (per_side as f32 - 1.0) / 2.0) * span,
1051                ((index / per_side) as f32 - (per_side as f32 - 1.0) / 2.0) * span,
1052            ]
1053        };
1054        let instances: Vec<Instance> = (0..cards)
1055            .map(|index| {
1056                let [x, z] = centre(index);
1057                let mut model = identity();
1058                model[0][0] = span;
1059                model[2][2] = span;
1060                model[3] = [x, 0.0, z, 1.0];
1061                Instance::new(quad, model, 1, index as u32 + 1)
1062            })
1063            .collect();
1064        let surfaces: Vec<InstanceSurface> = (0..cards)
1065            .map(|index| atlas.surface(ids[index]).unwrap())
1066            .collect();
1067        let dark = Material {
1068            base: [0.0; 3],
1069            roughness: 0.3,
1070            ..Default::default()
1071        };
1072        let lit = Material {
1073            content_layer: ContentLayer::from_kind(
1074                Content::Screen,
1075                1.0,
1076                0,
1077                &pfx_materials::ContentLook {
1078                    screen_gain: 1.5,
1079                    ..Default::default()
1080                },
1081            ),
1082            ..dark
1083        };
1084        let camera = Camera {
1085            view,
1086            projection,
1087            previous_view_projection: view_projection,
1088            position: [0.0, height, 0.0],
1089        };
1090        let sun = Sun {
1091            direction: [0.0, 1.0, 0.0],
1092            colour: [1.0; 3],
1093            intensity: 0.0,
1094        };
1095        let shoot =
1096            |frame: &mut Frame, materials: &[Material], surfaces: &[InstanceSurface], used: u32| {
1097                let scene_instances: Vec<Instance> = instances
1098                    .iter()
1099                    .map(|instance| Instance {
1100                        material: used,
1101                        ..*instance
1102                    })
1103                    .collect();
1104                frame.set_surfaces(surfaces).unwrap();
1105                frame
1106                    .render(&Scene {
1107                        camera,
1108                        time: 0.0,
1109                        seed: 7,
1110                        sun,
1111                        instances: &scene_instances,
1112                        materials,
1113                        deformers: &[],
1114                        wind: SceneWind::default(),
1115                    })
1116                    .unwrap();
1117                frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap()
1118            };
1119        let materials = [dark, lit];
1120        let on = shoot(&mut frame, &materials, &surfaces, 1);
1121        let black = [0u8, 0, 0, 255].repeat((image * image) as usize);
1122        let blanks: Vec<AtlasImage> = (0..cards)
1123            .map(|_| AtlasImage {
1124                width: image,
1125                height: image,
1126                texels: &black,
1127            })
1128            .collect();
1129        let mut blank_atlas =
1130            ContentAtlas::new(atlas_side, atlas_side, ContentFormat::Srgb8, level).unwrap();
1131        blank_atlas.allocate(&frame, 0).unwrap();
1132        let blank_ids = blank_atlas.add_all(&blanks).unwrap();
1133        assert_eq!(blank_ids, ids);
1134        blank_atlas.upload(&frame, 0).unwrap();
1135        let zero = shoot(&mut frame, &materials, &surfaces, 1);
1136
1137        let pixel = |point: [f32; 3]| {
1138            let clip = transform(view_projection, [point[0], point[1], point[2], 1.0]);
1139            [
1140                (clip[0] / clip[3] * 0.5 + 0.5) * frame_px as f32,
1141                (0.5 - clip[1] / clip[3] * 0.5) * frame_px as f32,
1142            ]
1143        };
1144        let cross = 2.0 * card_px as f32 / 32.0;
1145        let mut worst = Worst {
1146            error: 0.0,
1147            card: 0,
1148            quadrant: 0,
1149            at: [0, 0],
1150            pixels: 0,
1151            seen: [0.0; 3],
1152        };
1153        for index in 0..cards {
1154            let [x, z] = centre(index);
1155            let a = pixel([x - span / 2.0, 0.0, z - span / 2.0]);
1156            let bu = pixel([x + span / 2.0, 0.0, z - span / 2.0]);
1157            let bv = pixel([x - span / 2.0, 0.0, z + span / 2.0]);
1158            let du = [bu[0] - a[0], bu[1] - a[1]];
1159            let dv = [bv[0] - a[0], bv[1] - a[1]];
1160            for py in 0..frame_px {
1161                for px in 0..frame_px {
1162                    let c = [px as f32 + 0.5 - a[0], py as f32 + 0.5 - a[1]];
1163                    let u = (c[0] * du[0] + c[1] * du[1]) / (du[0] * du[0] + du[1] * du[1]);
1164                    let v = (c[0] * dv[0] + c[1] * dv[1]) / (dv[0] * dv[0] + dv[1] * dv[1]);
1165                    if !(0.0..1.0).contains(&u) || !(0.0..1.0).contains(&v) {
1166                        continue;
1167                    }
1168                    if (u - 0.5).abs() * (card_px as f32) < cross
1169                        || (v - 0.5).abs() * (card_px as f32) < cross
1170                    {
1171                        continue;
1172                    }
1173                    let at = ((py * frame_px + px) * 4) as usize;
1174                    if zero[at..at + 3].iter().any(|v| half(*v) > 0.05) {
1175                        continue;
1176                    }
1177                    let quadrant = usize::from(u >= 0.5) + 2 * usize::from(v >= 0.5);
1178                    let want = quadrant_colour(index, quadrant);
1179                    worst.pixels += 1;
1180                    for channel in 0..3 {
1181                        let got = (half(on[at + channel]) - half(zero[at + channel])) / 1.5;
1182                        let error = (got - linear(want[channel])).abs();
1183                        if error > worst.error {
1184                            worst = Worst {
1185                                error,
1186                                card: index,
1187                                quadrant,
1188                                at: [px, py],
1189                                pixels: worst.pixels,
1190                                seen: [channel as f32, got, linear(want[channel])],
1191                            };
1192                        }
1193                    }
1194                }
1195            }
1196        }
1197        worst
1198    }
1199
1200    #[test]
1201    #[ignore = "needs a GPU; run under pgpu"]
1202    fn sixty_four_cards_show_their_own_image_at_mip_zero() {
1203        let worst = render_sheet(&Sheet {
1204            frame_px: 512,
1205            card_px: 64,
1206            image: 64,
1207            atlas: 1024,
1208            level: DEFAULT_BLEED_LEVEL,
1209        });
1210        println!("1:1, gutter level 4: {}", worst.describe());
1211        assert!(worst.pixels > 64 * 3000, "{}", worst.describe());
1212        assert!(worst.error <= 1.0 / 255.0, "{}", worst.describe());
1213    }
1214
1215    #[test]
1216    #[ignore = "needs a GPU; run under pgpu"]
1217    fn minified_sixteen_times_the_gutters_keep_neighbours_out() {
1218        let sheet = |level| Sheet {
1219            frame_px: 256,
1220            card_px: 32,
1221            image: 512,
1222            atlas: 8192,
1223            level,
1224        };
1225        let held = render_sheet(&sheet(DEFAULT_BLEED_LEVEL));
1226        println!("16x minified, gutter level 4: {}", held.describe());
1227        assert!(held.pixels > 64 * 600, "{}", held.describe());
1228        assert!(held.error <= 1.0 / 255.0, "{}", held.describe());
1229        let bare = render_sheet(&sheet(0));
1230        println!("16x minified, gutter level 0: {}", bare.describe());
1231        assert!(
1232            bare.error > 4.0 / 255.0,
1233            "a bare gutter should bleed: {}",
1234            bare.describe()
1235        );
1236    }
1237
1238    fn reads(from: u32, to: u32, texel: u32) -> [u32; 2] {
1239        let at = (f64::from(texel) + 0.5) * f64::from(from) / f64::from(to) - 0.5;
1240        let first = at.floor() as i64;
1241        let last = i64::from(from) - 1;
1242        [
1243            first.clamp(0, last) as u32,
1244            (first + 1).clamp(0, last) as u32,
1245        ]
1246    }
1247
1248    fn side(size: u32, level: u32) -> u32 {
1249        (size >> level).max(1)
1250    }
1251
1252    #[test]
1253    fn every_mip_texel_that_reads_a_changed_texel_lies_inside_its_region() {
1254        let sizes = [
1255            [1000u32, 616],
1256            [1024, 1024],
1257            [257, 9],
1258            [1, 64],
1259            [16384, 16384],
1260        ];
1261        for size in sizes {
1262            let rects = [
1263                [0u32, 0, 24, 24],
1264                [8, 4, 40, 36],
1265                [size[0] / 2, size[1] / 2, 1, 1],
1266                [size[0].saturating_sub(5), size[1].saturating_sub(3), 5, 3],
1267                [0, 0, size[0], size[1]],
1268            ];
1269            for rect in rects {
1270                let (x, y) = (rect[0].min(size[0] - 1), rect[1].min(size[1] - 1));
1271                let rect = [x, y, rect[2].min(size[0] - x), rect[3].min(size[1] - y)];
1272                let regions = mip_regions(size, rect);
1273                assert_eq!(regions.len() as u32 + 1, mip_count(size[0], size[1]));
1274                let mut changed = [[rect[0], rect[0] + rect[2]], [rect[1], rect[1] + rect[3]]];
1275                for region in &regions {
1276                    let covered = [
1277                        [region.rect[0], region.rect[0] + region.rect[2]],
1278                        [region.rect[1], region.rect[1] + region.rect[3]],
1279                    ];
1280                    for axis in 0..2 {
1281                        let from = side(size[axis], region.level - 1);
1282                        let to = side(size[axis], region.level);
1283                        assert!(covered[axis][0] < covered[axis][1] && covered[axis][1] <= to);
1284                        for texel in 0..to {
1285                            let [a, b] = reads(from, to, texel);
1286                            let touches = a < changed[axis][1] && changed[axis][0] <= b;
1287                            let inside = covered[axis][0] <= texel && texel < covered[axis][1];
1288                            assert!(
1289                                inside || !touches,
1290                                "{size:?} {rect:?} level {} axis {axis} texel {texel}",
1291                                region.level
1292                            );
1293                        }
1294                    }
1295                    changed = covered;
1296                }
1297            }
1298        }
1299    }
1300
1301    #[test]
1302    fn a_cell_region_stays_close_to_its_footprint() {
1303        let size = [16384, 16384];
1304        let cell = [544u32, 1088, 544, 544];
1305        for region in mip_regions(size, cell) {
1306            let shrink = 1u32 << region.level;
1307            assert!(
1308                region.rect[2] <= cell[2] / shrink + 8 && region.rect[3] <= cell[3] / shrink + 8,
1309                "{region:?}"
1310            );
1311        }
1312    }
1313
1314    fn marked16(width: u32, height: u32) -> Vec<u8> {
1315        (0..width * height)
1316            .flat_map(|texel| {
1317                let (x, y) = (texel % width, texel / width);
1318                [
1319                    x as f32 / width as f32,
1320                    y as f32 / height as f32,
1321                    ((x ^ y) % 16) as f32 / 16.0,
1322                    1.0,
1323                ]
1324            })
1325            .flat_map(|value| half::f16::from_f32(value).to_le_bytes())
1326            .collect()
1327    }
1328
1329    fn texels_for(format: ContentFormat, width: u32, height: u32) -> Vec<u8> {
1330        match format {
1331            ContentFormat::Srgb8 => marked(width, height),
1332            ContentFormat::Linear16 => marked16(width, height),
1333        }
1334    }
1335
1336    fn read_levels(frame: &Frame, slot: usize) -> Vec<Vec<u8>> {
1337        let gpu = &frame.gpu;
1338        let content = frame.content();
1339        let target = content.get(slot).unwrap();
1340        let texel = target.format.texel_bytes();
1341        (0..target.mips)
1342            .map(|level| {
1343                let width = (target.width >> level).max(1);
1344                let height = (target.height >> level).max(1);
1345                let raw = width * texel;
1346                let row = raw.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
1347                    * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
1348                let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1349                    label: Some("atlas level readback"),
1350                    size: u64::from(row) * u64::from(height),
1351                    usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
1352                    mapped_at_creation: false,
1353                });
1354                let mut encoder = gpu
1355                    .device
1356                    .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
1357                encoder.copy_texture_to_buffer(
1358                    wgpu::TexelCopyTextureInfo {
1359                        texture: &target.texture,
1360                        mip_level: level,
1361                        origin: wgpu::Origin3d::ZERO,
1362                        aspect: wgpu::TextureAspect::All,
1363                    },
1364                    wgpu::TexelCopyBufferInfo {
1365                        buffer: &buffer,
1366                        layout: wgpu::TexelCopyBufferLayout {
1367                            offset: 0,
1368                            bytes_per_row: Some(row),
1369                            rows_per_image: Some(height),
1370                        },
1371                    },
1372                    wgpu::Extent3d {
1373                        width,
1374                        height,
1375                        depth_or_array_layers: 1,
1376                    },
1377                );
1378                gpu.queue.submit(Some(encoder.finish()));
1379                let (sender, receiver) = std::sync::mpsc::channel();
1380                buffer
1381                    .slice(..)
1382                    .map_async(wgpu::MapMode::Read, move |result| {
1383                        let _ = sender.send(result);
1384                    });
1385                gpu.device
1386                    .poll(wgpu::PollType::wait_indefinitely())
1387                    .unwrap();
1388                receiver.recv().unwrap().unwrap();
1389                let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
1390                let mut bytes = Vec::with_capacity((raw * height) as usize);
1391                for padded in mapped.chunks_exact(row as usize) {
1392                    bytes.extend_from_slice(&padded[..raw as usize]);
1393                }
1394                drop(mapped);
1395                buffer.unmap();
1396                bytes
1397            })
1398            .collect()
1399    }
1400
1401    fn assert_same_levels(a: &[Vec<u8>], b: &[Vec<u8>], what: &str) {
1402        assert_eq!(a.len(), b.len());
1403        for (level, (left, right)) in a.iter().zip(b).enumerate() {
1404            let differ = left.iter().zip(right).filter(|(x, y)| x != y).count();
1405            assert_eq!(differ, 0, "{what}: level {level} differs in {differ} bytes");
1406        }
1407    }
1408
1409    const ATLAS: [u32; 2] = [1001, 777];
1410
1411    fn batches(format: ContentFormat) -> [Vec<Vec<u8>>; 2] {
1412        let all: Vec<Vec<u8>> = sizes()
1413            .iter()
1414            .map(|size| texels_for(format, size[0], size[1]))
1415            .collect();
1416        [all[..20].to_vec(), all[20..].to_vec()]
1417    }
1418
1419    fn add_batch(atlas: &mut ContentAtlas, data: &[Vec<u8>], first: usize) {
1420        let all = sizes();
1421        let images: Vec<AtlasImage> = data
1422            .iter()
1423            .enumerate()
1424            .map(|(index, texels)| AtlasImage {
1425                width: all[first + index][0],
1426                height: all[first + index][1],
1427                texels,
1428            })
1429            .collect();
1430        atlas.add_all(&images).unwrap();
1431    }
1432
1433    fn headless_frame() -> Frame {
1434        let gpu = pollster::block_on(Gpu::headless()).unwrap();
1435        Frame::new(gpu, 64, 64).unwrap()
1436    }
1437
1438    #[test]
1439    #[ignore = "needs a GPU; run under pgpu"]
1440    fn regional_mips_equal_a_full_rebuild_byte_for_byte() {
1441        for format in [ContentFormat::Srgb8, ContentFormat::Linear16] {
1442            let frame = headless_frame();
1443            let [first, second] = batches(format);
1444            let mut atlas = ContentAtlas::new(ATLAS[0], ATLAS[1], format, 2).unwrap();
1445            atlas.allocate(&frame, 0).unwrap();
1446            add_batch(&mut atlas, &first, 0);
1447            assert_eq!(atlas.upload(&frame, 0).unwrap(), 20);
1448            add_batch(&mut atlas, &second, 20);
1449            assert_eq!(atlas.upload(&frame, 0).unwrap(), 20);
1450            let regional = read_levels(&frame, 0);
1451            assert!(regional.len() > 5);
1452            assert!(regional.last().unwrap().iter().any(|byte| *byte != 0));
1453            let mut encoder = frame
1454                .gpu
1455                .device
1456                .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
1457            frame.regenerate_content(&mut encoder, 0).unwrap();
1458            frame.gpu.queue.submit(Some(encoder.finish()));
1459            let full = read_levels(&frame, 0);
1460            assert_same_levels(
1461                &regional,
1462                &full,
1463                &format!("{format:?} regional against full"),
1464            );
1465        }
1466    }
1467
1468    #[test]
1469    #[ignore = "needs a GPU; run under pgpu"]
1470    fn paced_mips_equal_unpaced_byte_for_byte() {
1471        for format in [ContentFormat::Srgb8, ContentFormat::Linear16] {
1472            let frame = headless_frame();
1473            let [first, second] = batches(format);
1474            let mut plain = ContentAtlas::new(ATLAS[0], ATLAS[1], format, 2).unwrap();
1475            let mut paced = ContentAtlas::new(ATLAS[0], ATLAS[1], format, 2).unwrap();
1476            plain.allocate(&frame, 0).unwrap();
1477            paced.allocate(&frame, 1).unwrap();
1478            let mut pacer = Pacer::default();
1479            let mut turns = pfx_gpu::pace::Turns::default();
1480            let mut slices = 0;
1481            for (data, start) in [(&first, 0), (&second, 20)] {
1482                add_batch(&mut plain, data, start);
1483                add_batch(&mut paced, data, start);
1484                plain.upload(&frame, 0).unwrap();
1485                let (written, stats) = paced
1486                    .upload_paced(&frame, 1, &mut pacer, |ms| turns.add(ms))
1487                    .unwrap();
1488                assert_eq!(written, 20);
1489                assert_eq!(paced.pending(), 0);
1490                slices += stats.count();
1491            }
1492            assert!(slices >= 2, "{format:?} ran in {slices} submissions");
1493            assert_same_levels(
1494                &read_levels(&frame, 0),
1495                &read_levels(&frame, 1),
1496                &format!("{format:?} paced against unpaced"),
1497            );
1498        }
1499    }
1500
1501    #[test]
1502    #[ignore = "needs a GPU; run under pgpu"]
1503    fn sixty_four_new_cards_at_sixteen_thousand_pixels_upload_in_short_submissions() {
1504        let frame = headless_frame();
1505        let side = 512;
1506        let mut atlas =
1507            ContentAtlas::new(16384, 16384, ContentFormat::Srgb8, DEFAULT_BLEED_LEVEL).unwrap();
1508        atlas.allocate(&frame, 0).unwrap();
1509        let data: Vec<Vec<u8>> = (0..72).map(|card| card_texels(card, side)).collect();
1510        let images: Vec<AtlasImage> = data
1511            .iter()
1512            .map(|texels| AtlasImage {
1513                width: side,
1514                height: side,
1515                texels,
1516            })
1517            .collect();
1518        atlas.add_all(&images[..8]).unwrap();
1519        assert_eq!(atlas.upload(&frame, 0).unwrap(), 8);
1520        atlas.add_all(&images[8..]).unwrap();
1521        let mut pacer = Pacer::default();
1522        let mut turns = pfx_gpu::pace::Turns::default();
1523        let (written, stats) = atlas
1524            .upload_paced(&frame, 0, &mut pacer, |ms| turns.add(ms))
1525            .unwrap();
1526        assert_eq!(written, 64);
1527        println!(
1528            "16384x16384 Srgb8, 64 new 512x512 cards: {} submissions, longest {:.3} ms, median {:.3} ms, {:?} timing, wall {:.1} ms",
1529            stats.count(),
1530            stats.longest_ms(),
1531            stats.median_ms(),
1532            stats.source(),
1533            stats.wall_ms
1534        );
1535        assert!(stats.count() > 1);
1536        assert!(
1537            stats.longest_ms() < 50.0,
1538            "longest {:.3} ms",
1539            stats.longest_ms()
1540        );
1541        let began = std::time::Instant::now();
1542        let mut encoder = frame
1543            .gpu
1544            .device
1545            .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
1546        frame.regenerate_content(&mut encoder, 0).unwrap();
1547        frame.gpu.queue.submit(Some(encoder.finish()));
1548        frame
1549            .gpu
1550            .device
1551            .poll(wgpu::PollType::wait_indefinitely())
1552            .unwrap();
1553        println!(
1554            "one full rebuild of the same slot: {:.3} ms",
1555            began.elapsed().as_secs_f64() * 1000.0
1556        );
1557    }
1558}