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}