Skip to main content

pfx_bake/detail/
atlas.rs

1use std::fs;
2use std::path::Path;
3
4use block_compression::{BC7Settings, CompressionVariant, decode, encode};
5use pfx_materials::encode_channel;
6use serde_json::{Value, json};
7
8use super::{Crop, Region, digest};
9
10pub const ALIGN: u32 = 16;
11pub const LEVELS: u32 = 5;
12pub const PAGE: u32 = 2048;
13pub const PAGE_LIMIT: u32 = pfx_gpu::floor::TEXTURE_SIDE;
14pub const SCHEMA: u64 = 2;
15pub const SCHEMA_UV1: u64 = 3;
16pub const KIND: &str = "detail-atlas";
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Format {
20    Rgba8,
21    Rgba8Srgb,
22    Bc4,
23    Bc5,
24    Bc7Srgb,
25}
26
27impl Format {
28    pub fn block(self) -> u32 {
29        match self {
30            Self::Rgba8 | Self::Rgba8Srgb => 1,
31            Self::Bc4 | Self::Bc5 | Self::Bc7Srgb => 4,
32        }
33    }
34
35    pub fn block_bytes(self) -> u32 {
36        match self {
37            Self::Rgba8 | Self::Rgba8Srgb => 4,
38            Self::Bc4 => 8,
39            Self::Bc5 | Self::Bc7Srgb => 16,
40        }
41    }
42
43    pub fn level_bytes(self, width: u32, height: u32) -> usize {
44        width.div_ceil(self.block()) as usize
45            * height.div_ceil(self.block()) as usize
46            * self.block_bytes() as usize
47    }
48
49    pub fn compressed(self) -> bool {
50        self.block() == 4
51    }
52
53    pub fn vk(self) -> u32 {
54        match self {
55            Self::Rgba8 => 37,
56            Self::Rgba8Srgb => 43,
57            Self::Bc4 => 139,
58            Self::Bc5 => 141,
59            Self::Bc7Srgb => 146,
60        }
61    }
62
63    pub fn from_vk(value: u32) -> Option<Self> {
64        [
65            Self::Rgba8,
66            Self::Rgba8Srgb,
67            Self::Bc4,
68            Self::Bc5,
69            Self::Bc7Srgb,
70        ]
71        .into_iter()
72        .find(|format| format.vk() == value)
73    }
74
75    pub fn name(self) -> &'static str {
76        match self {
77            Self::Rgba8 => "rgba8",
78            Self::Rgba8Srgb => "rgba8-srgb",
79            Self::Bc4 => "bc4",
80            Self::Bc5 => "bc5",
81            Self::Bc7Srgb => "bc7-srgb",
82        }
83    }
84
85    fn variant(self) -> Option<CompressionVariant> {
86        match self {
87            Self::Rgba8 | Self::Rgba8Srgb => None,
88            Self::Bc4 => Some(CompressionVariant::BC4),
89            Self::Bc5 => Some(CompressionVariant::BC5),
90            Self::Bc7Srgb => Some(CompressionVariant::BC7(BC7Settings::opaque_slow())),
91        }
92    }
93}
94
95#[derive(Clone, Copy, Debug, PartialEq, Eq)]
96pub enum Kind {
97    Base,
98    Normal,
99    Roughness,
100}
101
102impl Kind {
103    pub const ALL: [Self; 3] = [Self::Base, Self::Normal, Self::Roughness];
104
105    pub fn name(self) -> &'static str {
106        match self {
107            Self::Base => "base",
108            Self::Normal => "normal",
109            Self::Roughness => "roughness",
110        }
111    }
112
113    pub fn plain(self) -> Format {
114        match self {
115            Self::Base => Format::Rgba8Srgb,
116            Self::Normal | Self::Roughness => Format::Rgba8,
117        }
118    }
119
120    pub fn packed(self) -> Format {
121        match self {
122            Self::Base => Format::Bc7Srgb,
123            Self::Normal => Format::Bc5,
124            Self::Roughness => Format::Bc4,
125        }
126    }
127
128    fn channels(self) -> &'static [usize] {
129        match self {
130            Self::Base => &[0, 1, 2],
131            Self::Normal => &[0, 1],
132            Self::Roughness => &[0],
133        }
134    }
135}
136
137pub fn level_size(width: u32, height: u32, level: u32) -> [u32; 2] {
138    [(width >> level).max(1), (height >> level).max(1)]
139}
140
141#[derive(Clone, Debug, PartialEq, Eq)]
142pub struct Texture {
143    pub format: Format,
144    pub width: u32,
145    pub height: u32,
146    pub levels: Vec<Vec<u8>>,
147}
148
149impl Texture {
150    pub fn check(&self) -> Result<(), String> {
151        if self.width == 0 || self.height == 0 || self.levels.is_empty() {
152            return Err("detail page is empty".into());
153        }
154        if self.format.compressed()
155            && (!self.width.is_multiple_of(4) || !self.height.is_multiple_of(4))
156        {
157            return Err("a block-compressed detail page needs sides in fours".into());
158        }
159        for (level, bytes) in self.levels.iter().enumerate() {
160            let [width, height] = level_size(self.width, self.height, level as u32);
161            if bytes.len() != self.format.level_bytes(width, height) {
162                return Err(format!("detail page level {level} has the wrong size"));
163            }
164        }
165        Ok(())
166    }
167
168    pub fn bytes(&self) -> u64 {
169        self.levels.iter().map(|level| level.len() as u64).sum()
170    }
171
172    pub fn compress(&self, kind: Kind) -> Result<Self, String> {
173        if self.format != kind.plain() {
174            return Err(format!(
175                "{} pages compress from {}",
176                kind.name(),
177                kind.plain().name()
178            ));
179        }
180        let format = kind.packed();
181        let levels = self
182            .levels
183            .iter()
184            .enumerate()
185            .map(|(level, bytes)| {
186                let [width, height] = level_size(self.width, self.height, level as u32);
187                encode_level(format, width, height, bytes)
188            })
189            .collect();
190        Ok(Self {
191            format,
192            width: self.width,
193            height: self.height,
194            levels,
195        })
196    }
197
198    pub fn decode(&self) -> Self {
199        let format = match self.format {
200            Format::Bc7Srgb => Format::Rgba8Srgb,
201            Format::Bc4 | Format::Bc5 => Format::Rgba8,
202            plain => return self.clone().with_format(plain),
203        };
204        let levels = self
205            .levels
206            .iter()
207            .enumerate()
208            .map(|(level, bytes)| {
209                let [width, height] = level_size(self.width, self.height, level as u32);
210                decode_level(self.format, width, height, bytes)
211            })
212            .collect();
213        Self {
214            format,
215            width: self.width,
216            height: self.height,
217            levels,
218        }
219    }
220
221    fn with_format(mut self, format: Format) -> Self {
222        self.format = format;
223        self
224    }
225}
226
227fn padded(width: u32, height: u32, rgba: &[u8]) -> (u32, u32, Vec<u8>) {
228    let wide = width.div_ceil(4) * 4;
229    let tall = height.div_ceil(4) * 4;
230    let mut out = vec![0; (wide * tall * 4) as usize];
231    for y in 0..tall {
232        let sy = y.min(height - 1);
233        for x in 0..wide {
234            let sx = x.min(width - 1);
235            let from = ((sy * width + sx) * 4) as usize;
236            let to = ((y * wide + x) * 4) as usize;
237            out[to..to + 4].copy_from_slice(&rgba[from..from + 4]);
238        }
239    }
240    (wide, tall, out)
241}
242
243pub fn encode_level(format: Format, width: u32, height: u32, rgba: &[u8]) -> Vec<u8> {
244    let Some(variant) = format.variant() else {
245        return rgba.to_vec();
246    };
247    let (wide, tall, source) = padded(width, height, rgba);
248    let mut out = vec![0; format.level_bytes(width, height)];
249    let rows = (tall / 4) as usize;
250    let row_bytes = format.level_bytes(wide, 4);
251    let threads = std::thread::available_parallelism()
252        .map_or(1, usize::from)
253        .clamp(1, rows);
254    let per = rows.div_ceil(threads);
255    std::thread::scope(|scope| {
256        for (chunk, blocks) in out.chunks_mut(per * row_bytes).enumerate() {
257            let first = chunk * per;
258            let count = blocks.len() / row_bytes;
259            let start = first * 4 * wide as usize * 4;
260            let end = start + count * 4 * wide as usize * 4;
261            let source = &source[start..end];
262            scope.spawn(move || {
263                encode::compress_rgba8(variant, source, blocks, wide, count as u32 * 4, wide * 4);
264            });
265        }
266    });
267    out
268}
269
270pub fn decode_level(format: Format, width: u32, height: u32, blocks: &[u8]) -> Vec<u8> {
271    let Some(variant) = format.variant() else {
272        return blocks.to_vec();
273    };
274    let wide = width.div_ceil(4) * 4;
275    let tall = height.div_ceil(4) * 4;
276    let mut full = vec![0; (wide * tall * 4) as usize];
277    decode::decompress_blocks_as_rgba8(variant, wide, tall, blocks, &mut full);
278    let mut out = Vec::with_capacity((width * height * 4) as usize);
279    for y in 0..height {
280        for x in 0..width {
281            let at = ((y * wide + x) * 4) as usize;
282            let texel = &full[at..at + 4];
283            out.extend_from_slice(&match format {
284                Format::Bc4 => [texel[0], 0, 0, 255],
285                Format::Bc5 => [texel[0], texel[1], normal_z(texel[0], texel[1]), 255],
286                _ => [texel[0], texel[1], texel[2], texel[3]],
287            });
288        }
289    }
290    out
291}
292
293pub fn normal_z(x: u8, y: u8) -> u8 {
294    let x = x as f32 / 255.0 * 2.0 - 1.0;
295    let y = y as f32 / 255.0 * 2.0 - 1.0;
296    let z = (1.0 - x * x - y * y).max(0.0).sqrt();
297    ((z * 0.5 + 0.5) * 255.0).round() as u8
298}
299
300fn quantize(level: &[[f32; 4]], kind: Kind) -> Vec<u8> {
301    let byte = |value: f32| (value.clamp(0.0, 1.0) * 255.0).round() as u8;
302    level
303        .iter()
304        .flat_map(|&texel| {
305            let rgba = match kind {
306                Kind::Base => {
307                    let a = texel[3];
308                    let rgb: [f32; 3] = std::array::from_fn(|c| {
309                        if a > 0.0 {
310                            encode_channel((texel[c] / a).clamp(0.0, 1.0))
311                        } else {
312                            0.0
313                        }
314                    });
315                    [rgb[0], rgb[1], rgb[2], a]
316                }
317                Kind::Normal | Kind::Roughness => texel,
318            };
319            rgba.map(byte)
320        })
321        .collect()
322}
323
324pub fn chain(kind: Kind, width: u32, height: u32, linear: &[u8], levels: u32) -> Vec<Vec<u8>> {
325    let mut level: Vec<[f32; 4]> = linear
326        .chunks_exact(4)
327        .map(|p| {
328            let rgba: [f32; 4] = std::array::from_fn(|c| p[c] as f32 / 255.0);
329            match kind {
330                Kind::Base => {
331                    let a = rgba[3];
332                    [rgba[0] * a, rgba[1] * a, rgba[2] * a, a]
333                }
334                Kind::Normal | Kind::Roughness => rgba,
335            }
336        })
337        .collect();
338    let (mut width, mut height) = (width, height);
339    let mut out = vec![quantize(&level, kind)];
340    while (out.len() as u32) < levels && (width > 1 || height > 1) {
341        let next_width = (width / 2).max(1);
342        let next_height = (height / 2).max(1);
343        let mut next = Vec::with_capacity(next_width as usize * next_height as usize);
344        for y in 0..next_height {
345            for x in 0..next_width {
346                let mut total = [0.0; 4];
347                for dy in 0..2 {
348                    for dx in 0..2 {
349                        let sx = (2 * x + dx).min(width - 1);
350                        let sy = (2 * y + dy).min(height - 1);
351                        let texel = level[(sy * width + sx) as usize];
352                        for c in 0..4 {
353                            total[c] += texel[c] * 0.25;
354                        }
355                    }
356                }
357                if kind == Kind::Normal {
358                    let normal: [f32; 3] = std::array::from_fn(|c| total[c] * 2.0 - 1.0);
359                    let length = normal.iter().map(|v| v * v).sum::<f32>().sqrt().max(1e-6);
360                    for c in 0..3 {
361                        total[c] = normal[c] / length * 0.5 + 0.5;
362                    }
363                }
364                next.push(total);
365            }
366        }
367        out.push(quantize(&next, kind));
368        level = next;
369        width = next_width;
370        height = next_height;
371    }
372    out
373}
374
375#[derive(Clone, Debug, PartialEq, Eq)]
376pub struct Packing {
377    pub width: u32,
378    pub height: u32,
379    pub pages: u32,
380    pub places: Places,
381}
382
383type Places = Vec<(u32, [u32; 2])>;
384
385fn shelves(sizes: &[[u32; 2]], width: u32, height: u32) -> Option<(u32, Places)> {
386    if sizes.iter().any(|size| size[0] > width || size[1] > height) {
387        return None;
388    }
389    let mut order: Vec<usize> = (0..sizes.len()).collect();
390    order.sort_by_key(|&index| {
391        (
392            std::cmp::Reverse(sizes[index][1]),
393            std::cmp::Reverse(sizes[index][0]),
394            index,
395        )
396    });
397    struct Shelf {
398        page: u32,
399        y: u32,
400        height: u32,
401        x: u32,
402    }
403    let mut shelves: Vec<Shelf> = Vec::new();
404    let mut pages = 0u32;
405    let mut floor = 0u32;
406    let mut places = vec![(0, [0, 0]); sizes.len()];
407    for index in order {
408        let [w, h] = sizes[index];
409        if let Some(shelf) = shelves
410            .iter_mut()
411            .find(|shelf| shelf.height >= h && shelf.x + w <= width)
412        {
413            places[index] = (shelf.page, [shelf.x, shelf.y]);
414            shelf.x += w;
415            continue;
416        }
417        if pages == 0 || floor + h > height {
418            pages += 1;
419            floor = 0;
420        }
421        places[index] = (pages - 1, [0, floor]);
422        shelves.push(Shelf {
423            page: pages - 1,
424            y: floor,
425            height: h,
426            x: w,
427        });
428        floor += h;
429    }
430    Some((pages.max(1), places))
431}
432
433fn extent(sizes: &[[u32; 2]], places: &[(u32, [u32; 2])]) -> [u32; 2] {
434    let used = |axis: usize| {
435        places
436            .iter()
437            .zip(sizes)
438            .map(|((_, at), size)| at[axis] + size[axis])
439            .max()
440            .unwrap_or(ALIGN)
441            .next_power_of_two()
442    };
443    [used(0), used(1)]
444}
445
446pub fn pack(sizes: &[[u32; 2]], page: u32) -> Result<Packing, String> {
447    if !page.is_power_of_two() || page < ALIGN {
448        return Err(format!(
449            "a detail page side must be a power of two of at least {ALIGN}"
450        ));
451    }
452    if let Some(size) = sizes
453        .iter()
454        .find(|size| size[0] == 0 || size[1] == 0 || size[0] > page || size[1] > page)
455    {
456        return Err(format!(
457            "a {}×{} detail crop does not fit a {page}² page",
458            size[0], size[1]
459        ));
460    }
461    let (pages, places) = shelves(sizes, page, page).ok_or("detail crops do not fit")?;
462    if pages > 1 {
463        return Ok(Packing {
464            width: page,
465            height: page,
466            pages,
467            places,
468        });
469    }
470    let mut best = (extent(sizes, &places), places);
471    let mut width = page / 2;
472    while width >= ALIGN {
473        match shelves(sizes, width, page) {
474            Some((1, places)) => {
475                let size = extent(sizes, &places);
476                if u64::from(size[0]) * u64::from(size[1])
477                    < u64::from(best.0[0]) * u64::from(best.0[1])
478                {
479                    best = (size, places);
480                }
481            }
482            _ => break,
483        }
484        width /= 2;
485    }
486    Ok(Packing {
487        width: best.0[0],
488        height: best.0[1],
489        pages: 1,
490        places: best.1,
491    })
492}
493
494#[derive(Clone, Debug, PartialEq, Eq)]
495pub struct Piece {
496    pub index: u32,
497    pub node: String,
498    pub input_hash: String,
499    pub region: Region,
500}
501
502#[derive(Clone, Debug, PartialEq, Eq)]
503pub struct Placed {
504    pub index: u32,
505    pub node: String,
506    pub density: u32,
507    pub crop: Crop,
508    pub page: u32,
509    pub at: [u32; 2],
510    pub input_hash: String,
511    pub uv_set: u32,
512}
513
514#[derive(Clone, Debug, PartialEq, Eq)]
515pub struct Atlas {
516    pub width: u32,
517    pub height: u32,
518    pub parts: Vec<Placed>,
519    pub base: Vec<Texture>,
520    pub normal: Vec<Texture>,
521    pub roughness: Vec<Texture>,
522}
523
524pub fn page_side(pieces: &[Piece], page: u32) -> u32 {
525    let largest = pieces
526        .iter()
527        .map(|piece| piece.region.crop.width.max(piece.region.crop.height))
528        .max()
529        .unwrap_or(0);
530    page.max(largest.next_power_of_two())
531}
532
533pub fn assemble(pieces: Vec<Piece>, page: u32) -> Result<Atlas, String> {
534    if pieces.is_empty() {
535        return Err("a detail atlas needs at least one part".into());
536    }
537    for piece in &pieces {
538        let crop = piece.region.crop;
539        if !crop.x.is_multiple_of(ALIGN)
540            || !crop.y.is_multiple_of(ALIGN)
541            || !crop.width.is_multiple_of(ALIGN)
542            || !crop.height.is_multiple_of(ALIGN)
543        {
544            return Err(format!("detail crops must be aligned to {ALIGN} texels"));
545        }
546        let count = (crop.width * crop.height * 4) as usize;
547        if [
548            &piece.region.base,
549            &piece.region.normal,
550            &piece.region.roughness,
551        ]
552        .iter()
553        .any(|map| map.len() != count)
554        {
555            return Err("a detail crop has the wrong number of texels".into());
556        }
557    }
558    let side = page_side(&pieces, page);
559    if side > PAGE_LIMIT {
560        return Err(format!("detail pages cannot exceed {PAGE_LIMIT}²"));
561    }
562    let sizes: Vec<[u32; 2]> = pieces
563        .iter()
564        .map(|piece| [piece.region.crop.width, piece.region.crop.height])
565        .collect();
566    let packing = pack(&sizes, side)?;
567    let (width, height) = (packing.width, packing.height);
568    let mut atlas = Atlas {
569        width,
570        height,
571        parts: Vec::with_capacity(pieces.len()),
572        base: Vec::new(),
573        normal: Vec::new(),
574        roughness: Vec::new(),
575    };
576    let page_bytes = (width * height * 4) as usize;
577    let mut linear = vec![
578        [
579            vec![0u8; page_bytes],
580            vec![0u8; page_bytes],
581            vec![0u8; page_bytes]
582        ];
583        packing.pages as usize
584    ];
585    for (piece, &(page, at)) in pieces.into_iter().zip(&packing.places) {
586        let crop = piece.region.crop;
587        let row = (crop.width * 4) as usize;
588        for (kind, source) in [
589            &piece.region.base,
590            &piece.region.normal,
591            &piece.region.roughness,
592        ]
593        .into_iter()
594        .enumerate()
595        {
596            let target = &mut linear[page as usize][kind];
597            for y in 0..crop.height {
598                let from = (y * crop.width * 4) as usize;
599                let to = (((at[1] + y) * width + at[0]) * 4) as usize;
600                target[to..to + row].copy_from_slice(&source[from..from + row]);
601            }
602        }
603        atlas.parts.push(Placed {
604            index: piece.index,
605            node: piece.node,
606            density: piece.region.size,
607            crop,
608            page,
609            at,
610            input_hash: piece.input_hash,
611            uv_set: piece.region.uv_set,
612        });
613    }
614    for maps in linear {
615        for (kind, bytes) in Kind::ALL.into_iter().zip(maps) {
616            let texture = Texture {
617                format: kind.plain(),
618                width,
619                height,
620                levels: chain(kind, width, height, &bytes, LEVELS),
621            };
622            atlas.pages_mut(kind).push(texture);
623        }
624    }
625    Ok(atlas)
626}
627
628impl Atlas {
629    pub fn pages(&self) -> u32 {
630        self.base.len() as u32
631    }
632
633    pub fn pages_of(&self, kind: Kind) -> &[Texture] {
634        match kind {
635            Kind::Base => &self.base,
636            Kind::Normal => &self.normal,
637            Kind::Roughness => &self.roughness,
638        }
639    }
640
641    fn pages_mut(&mut self, kind: Kind) -> &mut Vec<Texture> {
642        match kind {
643            Kind::Base => &mut self.base,
644            Kind::Normal => &mut self.normal,
645            Kind::Roughness => &mut self.roughness,
646        }
647    }
648
649    pub fn schema(&self) -> u64 {
650        if self.parts.iter().any(|part| part.uv_set != 0) {
651            SCHEMA_UV1
652        } else {
653            SCHEMA
654        }
655    }
656
657    pub fn transform(&self, part: &Placed) -> [f32; 4] {
658        let width = f64::from(self.width);
659        let height = f64::from(self.height);
660        let density = f64::from(part.density);
661        [
662            (density / width) as f32,
663            (density / height) as f32,
664            ((f64::from(part.at[0]) - f64::from(part.crop.x)) / width) as f32,
665            ((f64::from(part.at[1]) - f64::from(part.crop.y)) / height) as f32,
666        ]
667    }
668
669    pub fn compressed(&self) -> bool {
670        Kind::ALL.iter().all(|&kind| {
671            self.pages_of(kind)
672                .iter()
673                .all(|page| page.format == kind.packed())
674        })
675    }
676
677    pub fn compress(&self) -> Result<Self, String> {
678        let mut out = Self {
679            width: self.width,
680            height: self.height,
681            parts: self.parts.clone(),
682            base: Vec::new(),
683            normal: Vec::new(),
684            roughness: Vec::new(),
685        };
686        for kind in Kind::ALL {
687            for page in self.pages_of(kind) {
688                let packed = page.compress(kind)?;
689                out.pages_mut(kind).push(packed);
690            }
691        }
692        Ok(out)
693    }
694
695    pub fn decode(&self) -> Self {
696        Self {
697            width: self.width,
698            height: self.height,
699            parts: self.parts.clone(),
700            base: self.base.iter().map(Texture::decode).collect(),
701            normal: self.normal.iter().map(Texture::decode).collect(),
702            roughness: self.roughness.iter().map(Texture::decode).collect(),
703        }
704    }
705
706    pub fn vram_bytes(&self) -> u64 {
707        Kind::ALL
708            .iter()
709            .flat_map(|&kind| self.pages_of(kind))
710            .map(Texture::bytes)
711            .sum()
712    }
713
714    pub fn decoded_vram_bytes(&self) -> u64 {
715        Kind::ALL
716            .iter()
717            .flat_map(|&kind| self.pages_of(kind))
718            .map(|page| {
719                (0..page.levels.len() as u32)
720                    .map(|level| {
721                        let [width, height] = level_size(page.width, page.height, level);
722                        u64::from(width) * u64::from(height) * 4
723                    })
724                    .sum::<u64>()
725            })
726            .sum()
727    }
728
729    pub fn part_bytes(&self, part: &Placed) -> u64 {
730        Kind::ALL
731            .iter()
732            .filter_map(|&kind| self.pages_of(kind).first())
733            .map(|page| {
734                (0..page.levels.len() as u32)
735                    .map(|level| {
736                        let width = (part.crop.width >> level).max(1);
737                        let height = (part.crop.height >> level).max(1);
738                        page.format.level_bytes(width, height) as u64
739                    })
740                    .sum::<u64>()
741            })
742            .sum()
743    }
744
745    fn check(&self) -> Result<(), String> {
746        let pages = self.base.len();
747        if pages == 0 || self.normal.len() != pages || self.roughness.len() != pages {
748            return Err("detail atlas needs the same pages for every map".into());
749        }
750        for kind in Kind::ALL {
751            for page in self.pages_of(kind) {
752                page.check()?;
753                if page.width != self.width
754                    || page.height != self.height
755                    || ![kind.plain(), kind.packed()].contains(&page.format)
756                    || page.levels.len() != self.base[0].levels.len()
757                {
758                    return Err(format!(
759                        "detail {} page differs from the atlas",
760                        kind.name()
761                    ));
762                }
763            }
764        }
765        for part in &self.parts {
766            if part.page as usize >= pages
767                || part.at[0] + part.crop.width > self.width
768                || part.at[1] + part.crop.height > self.height
769                || part.crop.x + part.crop.width > part.density
770                || part.crop.y + part.crop.height > part.density
771            {
772                return Err(format!("detail part {} lies outside its page", part.node));
773            }
774            if part.uv_set > 1 {
775                return Err(format!(
776                    "detail part {} names UV set {}; only TEXCOORD_0 and TEXCOORD_1 bake",
777                    part.node, part.uv_set
778                ));
779            }
780        }
781        Ok(())
782    }
783}
784
785pub fn psnr(plain: &Atlas, packed: &Atlas, kind: Kind) -> Option<f64> {
786    let decoded = packed.decode();
787    let mut error = 0.0f64;
788    let mut count = 0u64;
789    for part in &plain.parts {
790        let a = &plain.pages_of(kind)[part.page as usize].levels[0];
791        let b = &decoded.pages_of(kind)[part.page as usize].levels[0];
792        for y in part.at[1]..part.at[1] + part.crop.height {
793            for x in part.at[0]..part.at[0] + part.crop.width {
794                let at = ((y * plain.width + x) * 4) as usize;
795                for &channel in kind.channels() {
796                    let difference = f64::from(a[at + channel]) - f64::from(b[at + channel]);
797                    error += difference * difference;
798                    count += 1;
799                }
800            }
801        }
802    }
803    if count == 0 || error == 0.0 {
804        return None;
805    }
806    let mean = error / count as f64;
807    Some(10.0 * (255.0f64 * 255.0 / mean).log10())
808}
809
810#[derive(Clone, Debug, PartialEq)]
811pub struct Summary {
812    pub input_hash: String,
813    pub padding: u32,
814    pub psnr: [Option<f64>; 3],
815    pub total_bytes: u64,
816    pub vram_bytes: u64,
817}
818
819fn file_name(page: usize, kind: Kind) -> String {
820    format!("page-{page:03}-{}.ktx2", kind.name())
821}
822
823pub fn file_names(pages: usize) -> Vec<String> {
824    (0..pages)
825        .flat_map(|page| Kind::ALL.map(|kind| file_name(page, kind)))
826        .collect()
827}
828
829fn round(value: f64) -> f64 {
830    (value * 1000.0).round() / 1000.0
831}
832
833pub fn write(
834    path: &Path,
835    atlas: &Atlas,
836    input_hash: &str,
837    padding: u32,
838    psnr: [Option<f64>; 3],
839) -> Result<Summary, String> {
840    atlas.check()?;
841    if path.exists() {
842        return Err(format!("{} already exists", path.display()));
843    }
844    fs::create_dir_all(path).map_err(|e| e.to_string())?;
845    let mut files = Vec::new();
846    let mut total = 0u64;
847    for page in 0..atlas.pages() as usize {
848        for kind in Kind::ALL {
849            let name = file_name(page, kind);
850            let bytes = super::ktx2::encode(&atlas.pages_of(kind)[page])?;
851            fs::write(path.join(&name), &bytes).map_err(|e| e.to_string())?;
852            total += bytes.len() as u64;
853            files.push(json!({"path": name, "bytes": bytes.len(), "sha256": digest(&bytes)}));
854        }
855    }
856    let parts: Vec<Value> = atlas
857        .parts
858        .iter()
859        .map(|part| {
860            let transform = atlas.transform(part);
861            json!({
862                "part": part.index,
863                "node": part.node,
864                "size": part.density,
865                "crop": [part.crop.x, part.crop.y, part.crop.width, part.crop.height],
866                "page": part.page,
867                "at": part.at,
868                "scale": [transform[0], transform[1]],
869                "offset": [transform[2], transform[3]],
870                "bytes": atlas.part_bytes(part),
871                "input_hash": part.input_hash,
872                "uv_set": part.uv_set,
873            })
874        })
875        .collect();
876    let formats: Vec<&str> = Kind::ALL
877        .iter()
878        .map(|&kind| atlas.pages_of(kind)[0].format.name())
879        .collect();
880    let psnr = psnr.map(|value| value.map(round));
881    let summary = Summary {
882        input_hash: input_hash.to_owned(),
883        padding,
884        psnr,
885        total_bytes: total,
886        vram_bytes: atlas.vram_bytes(),
887    };
888    let manifest = serde_json::to_vec_pretty(&json!({
889        "schema_version": atlas.schema(),
890        "format": KIND,
891        "input_hash": input_hash,
892        "padding": padding,
893        "align": ALIGN,
894        "page": {
895            "width": atlas.width,
896            "height": atlas.height,
897            "count": atlas.pages(),
898            "levels": atlas.base[0].levels.len(),
899        },
900        "maps": {"base": formats[0], "normal": formats[1], "roughness": formats[2]},
901        "total_bytes": total,
902        "vram_bytes": summary.vram_bytes,
903        "decoded_vram_bytes": atlas.decoded_vram_bytes(),
904        "psnr": {
905            "base": psnr[0],
906            "normal": psnr[1],
907            "roughness": psnr[2],
908        },
909        "files": files,
910        "parts": parts,
911    }))
912    .map_err(|e| e.to_string())?;
913    fs::write(path.join("manifest.json"), &manifest).map_err(|e| e.to_string())?;
914    fs::write(path.join("manifest.sha256"), digest(&manifest)).map_err(|e| e.to_string())?;
915    Ok(summary)
916}
917
918fn manifest_bytes(files: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<Value, String> {
919    let fetch = |name: &str| files(name).ok_or_else(|| format!("missing detail file {name}"));
920    let manifest = fetch("manifest.json")?;
921    let expected = String::from_utf8(fetch("manifest.sha256")?).map_err(|e| e.to_string())?;
922    if expected != digest(&manifest) {
923        return Err("detail manifest checksum differs".into());
924    }
925    serde_json::from_slice(&manifest).map_err(|e| e.to_string())
926}
927
928fn manifest(path: &Path) -> Result<Value, String> {
929    manifest_bytes(&|name| fs::read(path.join(name)).ok())
930}
931
932fn number(value: &Value, name: &str) -> Result<u32, String> {
933    value
934        .as_u64()
935        .and_then(|value| u32::try_from(value).ok())
936        .ok_or_else(|| format!("detail {name} is missing or invalid"))
937}
938
939fn pair(value: &Value, name: &str) -> Result<[u32; 2], String> {
940    Ok([number(&value[0], name)?, number(&value[1], name)?])
941}
942
943pub fn read(path: &Path) -> Result<(Summary, Atlas), String> {
944    read_from(&|name| fs::read(path.join(name)).ok())
945}
946
947pub fn read_from(files: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<(Summary, Atlas), String> {
948    let value = manifest_bytes(files)?;
949    let fetch = |name: &str| files(name).ok_or_else(|| format!("missing detail file {name}"));
950    let schema = value["schema_version"].as_u64();
951    if !matches!(schema, Some(SCHEMA | SCHEMA_UV1)) || value["format"] != KIND {
952        return Err("unsupported detail atlas".into());
953    }
954    let page = &value["page"];
955    let width = number(&page["width"], "page width")?;
956    let height = number(&page["height"], "page height")?;
957    let count = number(&page["count"], "page count")? as usize;
958    let entries = value["files"]
959        .as_array()
960        .ok_or("detail files are missing")?;
961    let names = file_names(count);
962    if entries.len() != names.len() {
963        return Err("detail atlas lists the wrong files".into());
964    }
965    let mut atlas = Atlas {
966        width,
967        height,
968        parts: Vec::new(),
969        base: Vec::new(),
970        normal: Vec::new(),
971        roughness: Vec::new(),
972    };
973    let mut total = 0u64;
974    for (index, (entry, name)) in entries.iter().zip(&names).enumerate() {
975        if entry["path"] != name.as_str() {
976            return Err("detail page name differs".into());
977        }
978        let bytes = fetch(name)?;
979        if entry["bytes"].as_u64() != Some(bytes.len() as u64)
980            || entry["sha256"].as_str() != Some(digest(&bytes).as_str())
981        {
982            return Err(format!("detail {name} checksum differs"));
983        }
984        total += bytes.len() as u64;
985        let texture = super::ktx2::decode(&bytes)?;
986        atlas.pages_mut(Kind::ALL[index % 3]).push(texture);
987    }
988    for part in value["parts"]
989        .as_array()
990        .ok_or("detail parts are missing")?
991    {
992        let crop = &part["crop"];
993        atlas.parts.push(Placed {
994            index: number(&part["part"], "part")?,
995            node: part["node"]
996                .as_str()
997                .ok_or("detail node is missing")?
998                .to_owned(),
999            density: number(&part["size"], "size")?,
1000            crop: Crop {
1001                x: number(&crop[0], "crop")?,
1002                y: number(&crop[1], "crop")?,
1003                width: number(&crop[2], "crop")?,
1004                height: number(&crop[3], "crop")?,
1005            },
1006            page: number(&part["page"], "page")?,
1007            at: pair(&part["at"], "placement")?,
1008            input_hash: part["input_hash"]
1009                .as_str()
1010                .ok_or("detail input hash is missing")?
1011                .to_owned(),
1012            uv_set: match &part["uv_set"] {
1013                Value::Null => 0,
1014                set => number(set, "UV set")?,
1015            },
1016        });
1017    }
1018    atlas.check()?;
1019    if schema != Some(atlas.schema()) {
1020        return Err("detail atlas schema differs from its parts' UV sets".into());
1021    }
1022    if total != value["total_bytes"].as_u64().unwrap_or(u64::MAX) {
1023        return Err("detail total bytes differ".into());
1024    }
1025    let psnr = Kind::ALL.map(|kind| value["psnr"][kind.name()].as_f64());
1026    Ok((
1027        Summary {
1028            input_hash: value["input_hash"]
1029                .as_str()
1030                .ok_or("detail input hash is missing")?
1031                .to_owned(),
1032            padding: number(&value["padding"], "padding")?,
1033            psnr,
1034            total_bytes: total,
1035            vram_bytes: atlas.vram_bytes(),
1036        },
1037        atlas,
1038    ))
1039}
1040
1041pub fn covered_crop(size: u32, base: &[u8]) -> Option<Crop> {
1042    let mut low = [u32::MAX; 2];
1043    let mut high = [0u32; 2];
1044    for (index, texel) in base.chunks_exact(4).enumerate() {
1045        if texel[3] == 0 {
1046            continue;
1047        }
1048        let at = [index as u32 % size, index as u32 / size];
1049        for axis in 0..2 {
1050            low[axis] = low[axis].min(at[axis]);
1051            high[axis] = high[axis].max(at[axis]);
1052        }
1053    }
1054    if low[0] > high[0] {
1055        return None;
1056    }
1057    let start = low.map(|value| value / ALIGN * ALIGN);
1058    let end = high.map(|value| ((value + 1).div_ceil(ALIGN) * ALIGN).min(size));
1059    Some(Crop {
1060        x: start[0],
1061        y: start[1],
1062        width: end[0] - start[0],
1063        height: end[1] - start[1],
1064    })
1065}
1066
1067pub fn cut(maps: &super::Maps, crop: Crop) -> Region {
1068    let take = |map: &[u8]| {
1069        let mut out = Vec::with_capacity((crop.width * crop.height * 4) as usize);
1070        for y in crop.y..crop.y + crop.height {
1071            let from = ((y * maps.size + crop.x) * 4) as usize;
1072            out.extend_from_slice(&map[from..from + (crop.width * 4) as usize]);
1073        }
1074        out
1075    };
1076    Region {
1077        size: maps.size,
1078        crop,
1079        uv_set: 0,
1080        base: take(&maps.base),
1081        roughness: take(&maps.roughness),
1082        normal: take(&maps.normal),
1083    }
1084}
1085
1086pub fn load(path: &Path) -> Result<Atlas, String> {
1087    let value = manifest(path)?;
1088    match value["schema_version"].as_u64() {
1089        Some(SCHEMA | SCHEMA_UV1) => read(path).map(|(_, atlas)| atlas),
1090        Some(1) => {
1091            let parts = value["parts"]
1092                .as_array()
1093                .ok_or("detail parts are missing")?;
1094            let mut pieces = Vec::with_capacity(parts.len());
1095            for (position, part) in parts.iter().enumerate() {
1096                let folder = part["path"].as_str().ok_or("detail part path is missing")?;
1097                if folder.is_empty() || folder.contains(['/', '\\']) || folder.starts_with('.') {
1098                    return Err(format!("detail part path {folder:?} is not a part folder"));
1099                }
1100                let (hash, maps) = super::read(&path.join(folder))?;
1101                if part["input_hash"].as_str() != Some(hash.as_str())
1102                    || part["size"].as_u64() != Some(u64::from(maps.size))
1103                {
1104                    return Err(format!("detail {folder} differs from the manifest"));
1105                }
1106                let crop = covered_crop(maps.size, &maps.base)
1107                    .ok_or_else(|| format!("detail {folder} covers nothing"))?;
1108                pieces.push(Piece {
1109                    index: folder
1110                        .strip_prefix("part-")
1111                        .and_then(|number| number.parse().ok())
1112                        .unwrap_or(position as u32),
1113                    node: part["node"].as_str().unwrap_or_default().to_owned(),
1114                    input_hash: hash,
1115                    region: cut(&maps, crop),
1116                });
1117            }
1118            assemble(pieces, PAGE)
1119        }
1120        _ => Err("unsupported detail folder".into()),
1121    }
1122}
1123
1124pub fn hash(pieces: &[(u32, &str, &str)], page: u32, padding: u32) -> String {
1125    use sha2::{Digest, Sha256};
1126    let mut h = Sha256::new();
1127    h.update(b"pito-engine-detail-atlas-v2");
1128    h.update(env!("CARGO_PKG_VERSION"));
1129    h.update(include_str!("atlas.rs"));
1130    h.update(include_str!("ktx2.rs"));
1131    h.update(page.to_le_bytes());
1132    h.update(padding.to_le_bytes());
1133    for (index, node, input) in pieces {
1134        h.update(index.to_le_bytes());
1135        h.update((node.len() as u64).to_le_bytes());
1136        h.update(node.as_bytes());
1137        h.update(input.as_bytes());
1138    }
1139    h.finalize()
1140        .iter()
1141        .map(|byte| format!("{byte:02x}"))
1142        .collect()
1143}
1144
1145#[cfg(test)]
1146mod tests {
1147    use super::*;
1148
1149    fn sizes(count: usize, seed: u32) -> Vec<[u32; 2]> {
1150        let mut state = seed;
1151        let mut next = || {
1152            state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1153            (state >> 16) % 12 + 1
1154        };
1155        (0..count)
1156            .map(|_| [next() * ALIGN, next() * ALIGN])
1157            .collect()
1158    }
1159
1160    fn overlap(a: ([u32; 2], [u32; 2]), b: ([u32; 2], [u32; 2])) -> bool {
1161        a.0[0] < b.0[0] + b.1[0]
1162            && b.0[0] < a.0[0] + a.1[0]
1163            && a.0[1] < b.0[1] + b.1[1]
1164            && b.0[1] < a.0[1] + a.1[1]
1165    }
1166
1167    #[test]
1168    fn packing_places_every_crop_once_without_overlap_and_the_same_way_each_time() {
1169        let sizes = sizes(60, 7);
1170        let packing = pack(&sizes, 256).unwrap();
1171        assert_eq!(packing, pack(&sizes, 256).unwrap());
1172        assert!(packing.pages > 1);
1173        assert_eq!((packing.width, packing.height), (256, 256));
1174        for (index, (&(page, at), size)) in packing.places.iter().zip(&sizes).enumerate() {
1175            assert!(page < packing.pages);
1176            assert!(at[0] + size[0] <= 256 && at[1] + size[1] <= 256);
1177            assert!(at[0].is_multiple_of(ALIGN) && at[1].is_multiple_of(ALIGN));
1178            for (other, (&(other_page, other_at), other_size)) in
1179                packing.places.iter().zip(&sizes).enumerate()
1180            {
1181                if other != index && other_page == page {
1182                    assert!(
1183                        !overlap((at, *size), (other_at, *other_size)),
1184                        "{index} and {other} overlap"
1185                    );
1186                }
1187            }
1188        }
1189        let area: u32 = sizes.iter().map(|size| size[0] * size[1]).sum();
1190        assert!(
1191            area * 10 > packing.pages * 256 * 256 * 6,
1192            "packing wastes too much"
1193        );
1194        let one = pack(&[[64, 32], [32, 32], [16, 16]], 2048).unwrap();
1195        assert_eq!((one.pages, one.width, one.height), (1, 128, 32));
1196        assert_eq!(one.places, vec![(0, [0, 0]), (0, [64, 0]), (0, [96, 0])]);
1197        let tall = pack(&[[64, 64], [64, 64], [64, 64], [64, 64], [64, 64]], 2048).unwrap();
1198        assert_eq!((tall.width, tall.height), (512, 64));
1199        assert!(pack(&[[512, 16]], 256).is_err());
1200        assert!(pack(&[[0, 16]], 256).is_err());
1201        assert!(pack(&[[16, 16]], 96).is_err());
1202    }
1203
1204    fn region(width: u32, height: u32, seed: u32) -> Region {
1205        let mut state = seed;
1206        let mut map = |alpha: bool| {
1207            (0..width * height)
1208                .flat_map(|index| {
1209                    let x = index % width;
1210                    let y = index / width;
1211                    state = state.wrapping_mul(22_695_477).wrapping_add(1);
1212                    let noise = (state >> 24) as u8 / 16;
1213                    let value = |c: u32| ((x * (3 + c) + y * (5 + c)) % 200) as u8 + noise;
1214                    [
1215                        value(0),
1216                        value(1),
1217                        value(2),
1218                        if alpha { 255 } else { value(3) },
1219                    ]
1220                })
1221                .collect::<Vec<u8>>()
1222        };
1223        Region {
1224            size: 256,
1225            crop: Crop {
1226                x: 32,
1227                y: 64,
1228                width,
1229                height,
1230            },
1231            uv_set: 0,
1232            base: map(true),
1233            roughness: map(true),
1234            normal: map(true),
1235        }
1236    }
1237
1238    fn atlas() -> Atlas {
1239        assemble(
1240            vec![
1241                Piece {
1242                    index: 4,
1243                    node: "planks".into(),
1244                    input_hash: "a".repeat(64),
1245                    region: region(48, 32, 1),
1246                },
1247                Piece {
1248                    index: 9,
1249                    node: "grime".into(),
1250                    input_hash: "b".repeat(64),
1251                    region: region(32, 64, 2),
1252                },
1253            ],
1254            128,
1255        )
1256        .unwrap()
1257    }
1258
1259    #[test]
1260    fn the_encoder_gives_the_same_blocks_each_time_and_for_any_split() {
1261        let plain = atlas();
1262        let first = plain.compress().unwrap();
1263        assert_eq!(first, plain.compress().unwrap());
1264        for kind in Kind::ALL {
1265            let page = &plain.pages_of(kind)[0];
1266            let [width, height] = [page.width, page.height];
1267            let mut whole = vec![0; kind.packed().level_bytes(width, height)];
1268            encode::compress_rgba8(
1269                kind.packed().variant().unwrap(),
1270                &page.levels[0],
1271                &mut whole,
1272                width,
1273                height,
1274                width * 4,
1275            );
1276            assert_eq!(first.pages_of(kind)[0].levels[0], whole);
1277            assert_eq!(first.pages_of(kind)[0].format, kind.packed());
1278        }
1279        let levels = &first.base[0].levels;
1280        assert_eq!(levels.len() as u32, LEVELS);
1281        assert_eq!(levels[4].len(), Format::Bc7Srgb.level_bytes(8, 4));
1282    }
1283
1284    #[test]
1285    fn block_compression_keeps_the_maps_close() {
1286        let plain = atlas();
1287        let packed = plain.compress().unwrap();
1288        for kind in Kind::ALL {
1289            let quality = psnr(&plain, &packed, kind).unwrap();
1290            assert!(quality > 30.0, "{} psnr {quality}", kind.name());
1291        }
1292        let decoded = packed.decode();
1293        assert_eq!(decoded.normal[0].format, Format::Rgba8);
1294        assert_eq!(decoded.base[0].format, Format::Rgba8Srgb);
1295        assert!(
1296            decoded.roughness[0].levels[0]
1297                .chunks_exact(4)
1298                .all(|t| t[1] == 0 && t[2] == 0 && t[3] == 255)
1299        );
1300        assert_eq!(decoded.decoded_vram_bytes(), plain.vram_bytes());
1301    }
1302
1303    #[test]
1304    fn the_manifest_and_pages_round_trip_and_refuse_a_changed_byte() {
1305        let plain = atlas();
1306        assert_eq!((plain.width, plain.height, plain.pages()), (128, 64, 1));
1307        let packed = plain.compress().unwrap();
1308        let psnr = Kind::ALL.map(|kind| psnr(&plain, &packed, kind));
1309        let root = Path::new("tmp").join(format!("detail-atlas-test-{}", std::process::id()));
1310        if root.exists() {
1311            fs::remove_dir_all(&root).unwrap();
1312        }
1313        let written = write(&root, &packed, "c0ffee", 8, psnr).unwrap();
1314        assert!(write(&root, &packed, "c0ffee", 8, psnr).is_err());
1315        let (summary, read_back) = read(&root).unwrap();
1316        assert_eq!(read_back, packed);
1317        let mut stored = std::collections::BTreeMap::new();
1318        for name in ["manifest.json", "manifest.sha256"]
1319            .into_iter()
1320            .map(str::to_owned)
1321            .chain(file_names(read_back.pages() as usize))
1322        {
1323            stored.insert(name.clone(), fs::read(root.join(&name)).unwrap());
1324        }
1325        let from_bytes = read_from(&|name| stored.get(name).cloned()).unwrap();
1326        assert_eq!(from_bytes.0, summary);
1327        assert_eq!(from_bytes.1, read_back);
1328        let mut changed = stored.clone();
1329        let page_name = file_names(1)[0].clone();
1330        let page_bytes = changed.get_mut(&page_name).unwrap();
1331        let last = page_bytes.len() - 1;
1332        page_bytes[last] ^= 1;
1333        assert!(read_from(&|name| changed.get(name).cloned()).is_err());
1334        assert_eq!(summary.input_hash, "c0ffee");
1335        assert_eq!(summary.total_bytes, written.total_bytes);
1336        assert_eq!(summary.vram_bytes, packed.vram_bytes());
1337        assert_eq!(summary.psnr, written.psnr);
1338        assert!(summary.psnr.iter().all(Option::is_some));
1339        assert_eq!(load(&root).unwrap(), packed);
1340        for (name, kind) in file_names(1).iter().zip(Kind::ALL) {
1341            let bytes = fs::read(root.join(name)).unwrap();
1342            let reader = ::ktx2::Reader::new(&bytes[..]).unwrap();
1343            let header = reader.header();
1344            assert_eq!(header.pixel_width, 128);
1345            assert_eq!(header.pixel_height, 64);
1346            assert_eq!(header.level_count, LEVELS);
1347            assert_eq!(header.format.map(|f| f.value()), Some(kind.packed().vk()));
1348            let levels: Vec<&[u8]> = reader.levels().map(|level| level.data).collect();
1349            assert_eq!(levels.len(), packed.pages_of(kind)[0].levels.len());
1350            for (level, data) in levels.iter().enumerate() {
1351                assert_eq!(*data, &packed.pages_of(kind)[0].levels[level][..]);
1352            }
1353            assert!(!reader.dfd_blocks().is_empty());
1354        }
1355        let page = root.join(&file_names(1)[1]);
1356        let mut bytes = fs::read(&page).unwrap();
1357        let last = bytes.len() - 1;
1358        bytes[last] ^= 1;
1359        fs::write(&page, bytes).unwrap();
1360        assert!(read(&root).is_err());
1361        fs::remove_dir_all(root).unwrap();
1362    }
1363
1364    #[test]
1365    fn transforms_map_a_tile_uv_onto_its_place_in_the_page() {
1366        let plain = atlas();
1367        for part in &plain.parts {
1368            let [su, sv, ou, ov] = plain.transform(part);
1369            let corner = [
1370                part.crop.x as f32 / part.density as f32,
1371                part.crop.y as f32 / part.density as f32,
1372            ];
1373            let placed = [corner[0] * su + ou, corner[1] * sv + ov];
1374            assert!((placed[0] * plain.width as f32 - part.at[0] as f32).abs() < 1e-3);
1375            assert!((placed[1] * plain.height as f32 - part.at[1] as f32).abs() < 1e-3);
1376        }
1377    }
1378
1379    #[test]
1380    fn the_manifest_records_each_parts_uv_set_and_old_readers_refuse_a_second_set() {
1381        let plain = atlas();
1382        assert_eq!(plain.schema(), SCHEMA);
1383        assert!(plain.parts.iter().all(|part| part.uv_set == 0));
1384        let mut second = region(48, 32, 1);
1385        second.uv_set = 1;
1386        let mixed = assemble(
1387            vec![
1388                Piece {
1389                    index: 4,
1390                    node: "planks".into(),
1391                    input_hash: "a".repeat(64),
1392                    region: second,
1393                },
1394                Piece {
1395                    index: 9,
1396                    node: "grime".into(),
1397                    input_hash: "b".repeat(64),
1398                    region: region(32, 64, 2),
1399                },
1400            ],
1401            128,
1402        )
1403        .unwrap();
1404        assert_eq!(mixed.schema(), SCHEMA_UV1);
1405        let sets: Vec<(&str, u32)> = mixed
1406            .parts
1407            .iter()
1408            .map(|part| (part.node.as_str(), part.uv_set))
1409            .collect();
1410        assert!(sets.contains(&("planks", 1)) && sets.contains(&("grime", 0)));
1411        assert_eq!(plain.base, mixed.base);
1412        let root = Path::new("tmp").join(format!("detail-atlas-uv-test-{}", std::process::id()));
1413        if root.exists() {
1414            fs::remove_dir_all(&root).unwrap();
1415        }
1416        let first = root.join("first");
1417        let second = root.join("second");
1418        write(&first, &plain, "c0ffee", 8, [None; 3]).unwrap();
1419        write(&second, &mixed, "c0ffee", 8, [None; 3]).unwrap();
1420        let text = |path: &Path| -> Value {
1421            serde_json::from_slice(&fs::read(path.join("manifest.json")).unwrap()).unwrap()
1422        };
1423        let one = text(&first);
1424        assert_eq!(one["schema_version"], SCHEMA);
1425        assert!(
1426            one["parts"]
1427                .as_array()
1428                .unwrap()
1429                .iter()
1430                .all(|part| part["uv_set"] == 0)
1431        );
1432        let two = text(&second);
1433        assert_eq!(two["schema_version"], SCHEMA_UV1);
1434        assert_eq!(read(&second).unwrap().1, mixed);
1435        let mut second_files = std::collections::BTreeMap::new();
1436        for entry in fs::read_dir(&second).unwrap() {
1437            let entry = entry.unwrap();
1438            let name = entry.file_name().into_string().unwrap();
1439            second_files.insert(name, fs::read(entry.path()).unwrap());
1440        }
1441        assert_eq!(
1442            read_from(&|name| second_files.get(name).cloned())
1443                .unwrap()
1444                .1,
1445            mixed
1446        );
1447        assert_eq!(load(&second).unwrap(), mixed);
1448        assert_eq!(read(&first).unwrap().1, plain);
1449
1450        let rewrite = |path: &Path, value: &Value| {
1451            let bytes = serde_json::to_vec_pretty(value).unwrap();
1452            fs::write(path.join("manifest.json"), &bytes).unwrap();
1453            fs::write(path.join("manifest.sha256"), digest(&bytes)).unwrap();
1454        };
1455        let mut unmarked = one.clone();
1456        for part in unmarked["parts"].as_array_mut().unwrap() {
1457            part.as_object_mut().unwrap().remove("uv_set");
1458        }
1459        rewrite(&first, &unmarked);
1460        assert_eq!(read(&first).unwrap().1, plain);
1461        let mut downgraded = two.clone();
1462        downgraded["schema_version"] = json!(SCHEMA);
1463        rewrite(&second, &downgraded);
1464        assert!(read(&second).unwrap_err().contains("schema"));
1465        let mut third = two.clone();
1466        third["parts"][0]["uv_set"] = json!(2);
1467        rewrite(&second, &third);
1468        assert!(read(&second).unwrap_err().contains("UV set 2"));
1469        fs::remove_dir_all(root).unwrap();
1470    }
1471}