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, ®ions)?;
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, ®ions[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 ®ions {
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 ®ional,
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}