Skip to main content

pfx_bake/
plate.rs

1pub mod codec;
2mod run;
3
4use std::collections::BTreeMap;
5use std::path::Path;
6
7use pfx_load::scene::{Camera as SceneCamera, Projection as SceneProjection, TraceSettings};
8use pfx_trace::Adaptive;
9use serde::{Deserialize, Serialize};
10use sha2::{Digest, Sha256};
11
12pub use run::{
13    Geometry, Pass, PlateOutcome, PlateRun, StagedPlate, bake_plates, plate_key, probe_plate,
14    stage_plate,
15};
16
17pub const SCHEMA_VERSION: u32 = 1;
18pub const FORMAT: &str = "plate";
19pub const CODE_VERSION: u32 = 1;
20pub const MAX_ID: u32 = u16::MAX as u32;
21pub const R16_RANGE: f32 = 64.0;
22pub const DEFAULT_VIEW: &str = "main";
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
25pub struct Layers {
26    pub normal: bool,
27    pub id: bool,
28    pub sun: bool,
29    pub transfer: bool,
30}
31
32impl Layers {
33    pub const ALL: Layers = Layers {
34        normal: true,
35        id: true,
36        sun: true,
37        transfer: true,
38    };
39
40    pub fn names(&self) -> Vec<&'static str> {
41        let mut names = vec!["color", "depth"];
42        for (on, name) in [
43            (self.normal, "normal"),
44            (self.id, "id"),
45            (self.sun, "sun"),
46            (self.transfer, "transfer"),
47        ] {
48            if on {
49                names.push(name);
50            }
51        }
52        names
53    }
54}
55
56#[derive(Clone, Debug, PartialEq, Default)]
57pub struct ViewKeys {
58    pub at: Option<[f32; 3]>,
59    pub look_at: Option<[f32; 3]>,
60    pub up: Option<[f32; 3]>,
61    pub fov: Option<f32>,
62    pub focal: Option<f32>,
63    pub sensor: Option<f32>,
64    pub shift: Option<[f32; 2]>,
65    pub near: Option<f32>,
66    pub far: Option<f32>,
67}
68
69#[derive(Clone, Debug, PartialEq)]
70pub struct ViewRecipe {
71    pub name: String,
72    pub keys: ViewKeys,
73}
74
75#[derive(Clone, Debug, PartialEq)]
76pub struct PlateRecipe {
77    pub scene: String,
78    pub size: [u32; 2],
79    pub overscan: f32,
80    pub scale: f32,
81    pub anchors: Option<Vec<f32>>,
82    pub layers: Layers,
83    pub adaptive: Adaptive,
84    pub seed: Option<u32>,
85    pub key: Option<String>,
86    pub clamp_indirect: Option<f32>,
87    pub filter_glossy: Option<f32>,
88    pub views: Vec<ViewRecipe>,
89}
90
91#[derive(Deserialize)]
92struct Outer {
93    plates: Option<toml::Value>,
94}
95
96#[derive(Deserialize)]
97#[serde(deny_unknown_fields)]
98struct RawPlates {
99    scene: String,
100    size: [u32; 2],
101    overscan: Option<f32>,
102    scale: Option<f32>,
103    anchors: Option<Vec<f32>>,
104    layers: Option<Vec<String>>,
105    threshold: Option<f32>,
106    min_samples: Option<u32>,
107    max_samples: Option<u32>,
108    growth: Option<f32>,
109    seed: Option<u32>,
110    key: Option<String>,
111    clamp_indirect: Option<f32>,
112    filter_glossy: Option<f32>,
113    #[serde(default)]
114    view: Vec<RawView>,
115}
116
117#[derive(Deserialize)]
118#[serde(deny_unknown_fields)]
119struct RawView {
120    name: String,
121    at: Option<[f32; 3]>,
122    look_at: Option<[f32; 3]>,
123    up: Option<[f32; 3]>,
124    fov: Option<f32>,
125    focal: Option<f32>,
126    sensor: Option<f32>,
127    shift: Option<[f32; 2]>,
128    near: Option<f32>,
129    far: Option<f32>,
130}
131
132fn label(name: &str) -> bool {
133    !name.is_empty()
134        && name.len() <= 64
135        && name
136            .bytes()
137            .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
138}
139
140pub fn check_anchors(anchors: &[f32]) -> Result<(), String> {
141    if anchors.is_empty()
142        || anchors
143            .iter()
144            .any(|hour| !hour.is_finite() || !(0.0..=24.0).contains(hour))
145        || anchors.windows(2).any(|pair| pair[0] >= pair[1])
146    {
147        return Err("plate anchors must be strictly increasing hours from 0 through 24".into());
148    }
149    Ok(())
150}
151
152pub fn parse_plates(bytes: &[u8]) -> Result<Option<PlateRecipe>, String> {
153    let text = std::str::from_utf8(bytes).map_err(|e| format!("bake.toml: {e}"))?;
154    let outer: Outer = toml::from_str(text).map_err(|e| format!("bake.toml: {e}"))?;
155    let Some(value) = outer.plates else {
156        return Ok(None);
157    };
158    let raw: RawPlates = value
159        .try_into()
160        .map_err(|e: toml::de::Error| format!("bake.toml [plates]: {}", e.message()))?;
161    let [width, height] = raw.size;
162    if !(16..=16384).contains(&width) || !(16..=16384).contains(&height) {
163        return Err("plates size must be 16 to 16384 pixels on each side".into());
164    }
165    let overscan = raw.overscan.unwrap_or(0.1);
166    if !(0.0..=1.0).contains(&overscan) {
167        return Err("plates overscan must be 0 to 1".into());
168    }
169    let scale = raw.scale.unwrap_or(1.0);
170    if !(0.25..=4.0).contains(&scale) {
171        return Err("plates scale must be 0.25 to 4".into());
172    }
173    if let Some(anchors) = &raw.anchors {
174        check_anchors(anchors)?;
175    }
176    let mut layers = Layers::default();
177    let mut seen = Vec::new();
178    for name in raw.layers.unwrap_or_else(|| {
179        ["color", "depth", "normal", "id", "sun", "transfer"]
180            .map(String::from)
181            .to_vec()
182    }) {
183        if seen.contains(&name) {
184            return Err(format!("plates layer {name} is named twice"));
185        }
186        match name.as_str() {
187            "color" | "depth" => {}
188            "normal" => layers.normal = true,
189            "id" => layers.id = true,
190            "sun" => layers.sun = true,
191            "transfer" => layers.transfer = true,
192            other => {
193                return Err(format!(
194                    "plates layer {other} is unknown; layers are color, depth, normal, id, sun and transfer"
195                ));
196            }
197        }
198        seen.push(name);
199    }
200    if !seen.iter().any(|name| name == "color") || !seen.iter().any(|name| name == "depth") {
201        return Err("plates layers need color and depth".into());
202    }
203    if layers.transfer && !(layers.sun && layers.normal) {
204        return Err("plates layer transfer needs sun and normal".into());
205    }
206    let adaptive = Adaptive {
207        threshold: raw.threshold.unwrap_or(0.01),
208        min_samples: raw.min_samples.unwrap_or(16),
209        max_samples: raw.max_samples.unwrap_or(1024),
210        growth: raw.growth.unwrap_or(2.0),
211    };
212    if !adaptive.valid() || adaptive.max_samples > 65536 {
213        return Err("plates sampling needs a positive threshold, a growth over 1 and 2 to 65536 samples, min_samples at most max_samples".into());
214    }
215    TraceSettings {
216        clamp_indirect: raw.clamp_indirect.unwrap_or(0.0),
217        filter_glossy: raw.filter_glossy.unwrap_or(0.0),
218        ..TraceSettings::default()
219    }
220    .check()
221    .map_err(|message| format!("plates {message}"))?;
222    let mut views = Vec::new();
223    for view in raw.view {
224        if !label(&view.name) {
225            return Err(format!(
226                "plates view name {:?} must be an ASCII label",
227                view.name
228            ));
229        }
230        if views
231            .iter()
232            .any(|other: &ViewRecipe| other.name == view.name)
233        {
234            return Err(format!("plates view {} is named twice", view.name));
235        }
236        if view.fov.is_some() && (view.focal.is_some() || view.sensor.is_some()) {
237            return Err(format!(
238                "plates view {} takes fov or focal and sensor, not both",
239                view.name
240            ));
241        }
242        views.push(ViewRecipe {
243            name: view.name,
244            keys: ViewKeys {
245                at: view.at,
246                look_at: view.look_at,
247                up: view.up,
248                fov: view.fov,
249                focal: view.focal,
250                sensor: view.sensor,
251                shift: view.shift,
252                near: view.near,
253                far: view.far,
254            },
255        });
256    }
257    if views.is_empty() {
258        views.push(ViewRecipe {
259            name: DEFAULT_VIEW.into(),
260            keys: ViewKeys::default(),
261        });
262    }
263    Ok(Some(PlateRecipe {
264        scene: raw.scene,
265        size: raw.size,
266        overscan,
267        scale,
268        anchors: raw.anchors,
269        layers,
270        adaptive,
271        seed: raw.seed,
272        key: raw.key,
273        clamp_indirect: raw.clamp_indirect,
274        filter_glossy: raw.filter_glossy,
275        views,
276    }))
277}
278
279impl PlateRecipe {
280    pub fn trace(&self, scene: TraceSettings) -> TraceSettings {
281        TraceSettings {
282            clamp_indirect: self.clamp_indirect.unwrap_or(scene.clamp_indirect),
283            filter_glossy: self.filter_glossy.unwrap_or(scene.filter_glossy),
284            ..scene
285        }
286    }
287}
288
289impl ViewKeys {
290    pub fn camera(&self, base: &SceneCamera) -> Result<SceneCamera, String> {
291        let mut camera = *base;
292        if let Some(at) = self.at {
293            camera.at = at;
294        }
295        if let Some(look_at) = self.look_at {
296            camera.look_at = look_at;
297        }
298        if let Some(up) = self.up {
299            camera.up = up;
300        }
301        if let Some(shift) = self.shift {
302            camera.shift = shift;
303        }
304        if let Some(near) = self.near {
305            camera.near = near;
306        }
307        if let Some(far) = self.far {
308            camera.far = far;
309        }
310        if let Some(fov) = self.fov {
311            camera.projection = SceneProjection::Perspective { fov };
312        } else if let Some(focal) = self.focal {
313            let sensor = self.sensor.unwrap_or(24.0);
314            camera.projection = SceneProjection::Perspective {
315                fov: (2.0 * (sensor * 0.5 / focal).atan()).to_degrees(),
316            };
317        }
318        let finite = |values: &[f32]| values.iter().all(|value| value.is_finite());
319        if !finite(&camera.at) || !finite(&camera.look_at) || !finite(&camera.up) {
320            return Err("plate view camera is not finite".into());
321        }
322        if !(camera.near > 0.0 && camera.far > camera.near) {
323            return Err("plate view needs 0 < near < far".into());
324        }
325        match camera.projection {
326            SceneProjection::Perspective { fov } if fov > 0.0 && fov < 179.0 => Ok(camera),
327            SceneProjection::Perspective { .. } => {
328                Err("plate view fov must be between 0 and 179 degrees".into())
329            }
330            SceneProjection::Orthographic { .. } => {
331                Err("a plate is perspective; an orthographic camera is refused".into())
332            }
333        }
334    }
335}
336
337#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
338pub struct PlateCamera {
339    pub origin: [f32; 3],
340    pub forward: [f32; 3],
341    pub right: [f32; 3],
342    pub up: [f32; 3],
343    pub shift: [f32; 2],
344    pub near: f32,
345    pub far: f32,
346}
347
348pub fn plate_size(size: [u32; 2], overscan: f32, scale: f32) -> [u32; 2] {
349    size.map(|side| 2 * ((side as f32 * scale * (1.0 + overscan) * 0.5).round() as u32).max(1))
350}
351
352fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
353    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
354}
355
356impl PlateCamera {
357    pub fn new(camera: &SceneCamera, size: [u32; 2], overscan: f32, scale: f32) -> Self {
358        let [width, height] = plate_size(size, overscan, scale);
359        let tan = camera.tan_half_height().unwrap_or(1.0);
360        let aspect = size[0] as f32 / size[1] as f32;
361        let grow = [
362            width as f32 / (size[0] as f32 * scale),
363            height as f32 / (size[1] as f32 * scale),
364        ];
365        let (forward, right, up) = camera.axes();
366        Self {
367            origin: camera.at,
368            forward,
369            right: right.map(|v| v * tan * aspect * grow[0]),
370            up: up.map(|v| v * tan * grow[1]),
371            shift: [camera.shift[0] / grow[0], camera.shift[1] / grow[1]],
372            near: camera.near,
373            far: camera.far,
374        }
375    }
376
377    pub fn ray(&self, texel: [f32; 2], size: [u32; 2]) -> [f32; 3] {
378        let x = texel[0] / size[0] as f32 * 2.0 - 1.0;
379        let y = texel[1] / size[1] as f32 * 2.0 - 1.0;
380        std::array::from_fn(|k| {
381            self.forward[k]
382                + self.right[k] * (x + self.shift[0])
383                + self.up[k] * (-y + self.shift[1])
384        })
385    }
386
387    pub fn project(&self, world: [f32; 3], size: [u32; 2]) -> Option<([f32; 2], f32)> {
388        let v = [
389            world[0] - self.origin[0],
390            world[1] - self.origin[1],
391            world[2] - self.origin[2],
392        ];
393        let z = dot(v, self.forward);
394        if z <= 0.0 || z.is_nan() {
395            return None;
396        }
397        let a = dot(v, self.right) / (dot(self.right, self.right) * z);
398        let b = dot(v, self.up) / (dot(self.up, self.up) * z);
399        let x = a - self.shift[0];
400        let y = self.shift[1] - b;
401        Some((
402            [
403                (x + 1.0) * 0.5 * size[0] as f32,
404                (y + 1.0) * 0.5 * size[1] as f32,
405            ],
406            z,
407        ))
408    }
409
410    pub fn trace_camera(&self) -> pfx_trace::Camera {
411        pfx_trace::Camera {
412            origin: self.origin,
413            forward: self.forward,
414            right: self.right,
415            up: self.up,
416        }
417    }
418
419    pub fn strip(&self, size: [u32; 2], first: u32, rows: u32) -> (pfx_trace::Camera, [f32; 2]) {
420        let k = rows as f32 / size[1] as f32;
421        let centre = (2.0 * first as f32 + rows as f32) / size[1] as f32 - 1.0;
422        (
423            pfx_trace::Camera {
424                origin: self.origin,
425                forward: self.forward,
426                right: self.right,
427                up: self.up.map(|v| v * k),
428            },
429            [self.shift[0], (self.shift[1] - centre) / k],
430        )
431    }
432}
433
434#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
435pub struct Bounds {
436    pub min: [f32; 3],
437    pub max: [f32; 3],
438}
439
440#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
441pub struct Sampling {
442    pub threshold: f32,
443    pub min_samples: u32,
444    pub max_samples: u32,
445    pub growth: f32,
446    pub seed: u32,
447}
448
449#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
450pub struct PassRecord {
451    pub mean: f64,
452    pub max: u32,
453    pub rounds: Vec<[u64; 2]>,
454}
455
456#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
457pub struct AnchorRecord {
458    pub hour: f32,
459    pub color: PassRecord,
460    pub sun: Option<PassRecord>,
461    pub transfer: Option<PassRecord>,
462}
463
464#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
465pub struct SunRecord {
466    pub direction: [f32; 3],
467    pub color: [f32; 3],
468    pub intensity: f32,
469}
470
471impl SunRecord {
472    pub fn light(&self) -> [f32; 3] {
473        self.color.map(|channel| channel * self.intensity)
474    }
475}
476
477#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
478pub struct IdEntry {
479    pub id: u32,
480    pub object: String,
481}
482
483#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
484pub struct PlateFile {
485    pub name: String,
486    pub layer: String,
487    pub anchor: Option<usize>,
488    pub bytes: u64,
489    pub sha256: String,
490}
491
492#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
493pub struct PlateManifest {
494    pub schema_version: u32,
495    pub format: String,
496    pub engine: String,
497    pub key: String,
498    pub caller_key: Option<String>,
499    pub view: String,
500    pub size: [u32; 2],
501    pub overscan: f32,
502    pub scale: f32,
503    pub width: u32,
504    pub height: u32,
505    pub camera: PlateCamera,
506    pub depth_near: f32,
507    pub bounds: Bounds,
508    pub anchors: Vec<f32>,
509    pub suns: Vec<SunRecord>,
510    pub layers: BTreeMap<String, String>,
511    pub sampling: Sampling,
512    pub samples: Vec<AnchorRecord>,
513    pub ids: Vec<IdEntry>,
514    pub files: Vec<PlateFile>,
515}
516
517#[derive(Clone, Debug, PartialEq)]
518pub enum Depth {
519    Inverse(Vec<u16>),
520    Float(Vec<f32>),
521}
522
523impl Depth {
524    pub fn at(&self, index: usize, near: f32) -> Option<f32> {
525        match self {
526            Self::Inverse(codes) => codec::from_inverse_depth(codes[index], near),
527            Self::Float(depths) => (depths[index] > 0.0).then_some(depths[index]),
528        }
529    }
530
531    pub fn format(&self) -> &'static str {
532        match self {
533            Self::Inverse(_) => "r16-inverse",
534            Self::Float(_) => "r32f",
535        }
536    }
537
538    fn bytes(&self) -> Vec<u8> {
539        match self {
540            Self::Inverse(codes) => codes.iter().flat_map(|code| code.to_le_bytes()).collect(),
541            Self::Float(depths) => depths
542                .iter()
543                .flat_map(|depth| depth.to_le_bytes())
544                .collect(),
545        }
546    }
547}
548
549#[derive(Clone, Debug, PartialEq)]
550pub struct Plate {
551    pub manifest: PlateManifest,
552    pub depth: Depth,
553    pub normal: Option<Vec<u32>>,
554    pub id: Option<Vec<u16>>,
555    pub color: Vec<Vec<u32>>,
556    pub sun: Option<Vec<Vec<u32>>>,
557    pub transfer: Option<Vec<Vec<u32>>>,
558}
559
560impl Plate {
561    pub fn texels(&self) -> usize {
562        self.manifest.width as usize * self.manifest.height as usize
563    }
564
565    pub fn bytes_per_anchor(&self) -> u64 {
566        let layers = 1 + u64::from(self.sun.is_some()) + u64::from(self.transfer.is_some());
567        self.texels() as u64 * 4 * layers
568    }
569
570    pub fn shared_bytes(&self) -> u64 {
571        let depth = match self.depth {
572            Depth::Inverse(_) => 2,
573            Depth::Float(_) => 4,
574        };
575        let normal = if self.normal.is_some() { 4 } else { 0 };
576        let id = if self.id.is_some() { 2 } else { 0 };
577        self.texels() as u64 * (depth + normal + id)
578    }
579
580    pub fn depth_at(&self, x: u32, y: u32) -> Option<f32> {
581        let index = y as usize * self.manifest.width as usize + x as usize;
582        self.depth.at(index, self.manifest.depth_near)
583    }
584
585    pub fn blend(&self, hour: f32) -> (usize, usize, f32) {
586        anchor_blend(&self.manifest.anchors, hour)
587    }
588}
589
590pub fn anchor_blend(anchors: &[f32], hour: f32) -> (usize, usize, f32) {
591    let last = anchors.len().saturating_sub(1);
592    if anchors.is_empty() || !hour.is_finite() || hour <= anchors[0] {
593        return (0, 0, 0.0);
594    }
595    if hour >= anchors[last] {
596        return (last, last, 0.0);
597    }
598    let next = anchors
599        .iter()
600        .position(|&anchor| anchor > hour)
601        .unwrap_or(last);
602    let before = next - 1;
603    let t = (hour - anchors[before]) / (anchors[next] - anchors[before]);
604    (before, next, t)
605}
606
607fn hex(bytes: &[u8]) -> String {
608    bytes.iter().map(|b| format!("{b:02x}")).collect()
609}
610
611fn sha(bytes: &[u8]) -> String {
612    hex(&Sha256::digest(bytes))
613}
614
615fn words<T: Copy>(bytes: &[u8], size: usize, read: impl Fn(&[u8]) -> T) -> Vec<T> {
616    bytes.chunks_exact(size).map(read).collect()
617}
618
619pub fn write_plate(dir: &Path, plate: &Plate) -> Result<PlateManifest, String> {
620    let mut manifest = plate.manifest.clone();
621    let mut files = Vec::new();
622    let mut out: Vec<(String, Vec<u8>)> = Vec::new();
623    let mut add = |name: String, layer: &str, anchor: Option<usize>, bytes: Vec<u8>| {
624        files.push(PlateFile {
625            name: name.clone(),
626            layer: layer.into(),
627            anchor,
628            bytes: bytes.len() as u64,
629            sha256: sha(&bytes),
630        });
631        out.push((name, bytes));
632    };
633    let mut layers = BTreeMap::new();
634    layers.insert("color".to_string(), "rgb9e5".to_string());
635    layers.insert("depth".to_string(), plate.depth.format().to_string());
636    add("depth.bin".into(), "depth", None, plate.depth.bytes());
637    if let Some(normal) = &plate.normal {
638        layers.insert("normal".into(), "oct16".into());
639        add(
640            "normal.bin".into(),
641            "normal",
642            None,
643            normal.iter().flat_map(|v| v.to_le_bytes()).collect(),
644        );
645    }
646    if let Some(id) = &plate.id {
647        layers.insert("id".into(), "r16".into());
648        add(
649            "id.bin".into(),
650            "id",
651            None,
652            id.iter().flat_map(|v| v.to_le_bytes()).collect(),
653        );
654    }
655    for (anchor, color) in plate.color.iter().enumerate() {
656        add(
657            format!("color-{anchor:03}.bin"),
658            "color",
659            Some(anchor),
660            color.iter().flat_map(|v| v.to_le_bytes()).collect(),
661        );
662    }
663    for (name, layer) in [("sun", &plate.sun), ("transfer", &plate.transfer)] {
664        if let Some(anchors) = layer {
665            layers.insert(name.into(), "rgb9e5".into());
666            for (anchor, values) in anchors.iter().enumerate() {
667                add(
668                    format!("{name}-{anchor:03}.bin"),
669                    name,
670                    Some(anchor),
671                    values.iter().flat_map(|v| v.to_le_bytes()).collect(),
672                );
673            }
674        }
675    }
676    manifest.layers = layers;
677    manifest.files = files;
678    check(&manifest, plate)?;
679    std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
680    for (name, bytes) in &out {
681        std::fs::write(dir.join(name), bytes).map_err(|e| format!("{name}: {e}"))?;
682    }
683    let json = serde_json::to_vec_pretty(&manifest).map_err(|e| e.to_string())?;
684    std::fs::write(dir.join("manifest.json"), &json).map_err(|e| e.to_string())?;
685    std::fs::write(dir.join("manifest.sha256"), format!("{}\n", sha(&json)))
686        .map_err(|e| e.to_string())?;
687    Ok(manifest)
688}
689
690fn check(manifest: &PlateManifest, plate: &Plate) -> Result<(), String> {
691    let texels = plate.texels();
692    let anchors = manifest.anchors.len();
693    let depth = match &plate.depth {
694        Depth::Inverse(codes) => codes.len(),
695        Depth::Float(depths) => depths.len(),
696    };
697    if texels == 0
698        || depth != texels
699        || manifest.suns.len() != anchors
700        || plate.color.len() != anchors
701        || plate.color.iter().any(|layer| layer.len() != texels)
702        || plate.normal.as_ref().is_some_and(|v| v.len() != texels)
703        || plate.id.as_ref().is_some_and(|v| v.len() != texels)
704        || [&plate.sun, &plate.transfer].iter().any(|layer| {
705            layer.as_ref().is_some_and(|layer| {
706                layer.len() != anchors || layer.iter().any(|v| v.len() != texels)
707            })
708        })
709    {
710        return Err("plate layers do not match its size and anchors".into());
711    }
712    check_anchors(&manifest.anchors)
713}
714
715pub fn read_plate(dir: &Path) -> Result<Plate, String> {
716    read_plate_from(&|name| std::fs::read(dir.join(name)).ok())
717}
718
719pub fn read_plate_from(get: &dyn Fn(&str) -> Option<Vec<u8>>) -> Result<Plate, String> {
720    let fetch = |name: &str| get(name).ok_or_else(|| format!("missing plate file {name}"));
721    let json = fetch("manifest.json")?;
722    let recorded = fetch("manifest.sha256")?;
723    if String::from_utf8_lossy(&recorded).trim() != sha(&json) {
724        return Err("plate manifest.sha256 does not match manifest.json".into());
725    }
726    let manifest: PlateManifest =
727        serde_json::from_slice(&json).map_err(|e| format!("plate manifest: {e}"))?;
728    if manifest.schema_version != SCHEMA_VERSION || manifest.format != FORMAT {
729        return Err(format!(
730            "plate manifest is {} schema {}; this engine reads {FORMAT} schema {SCHEMA_VERSION}",
731            manifest.format, manifest.schema_version
732        ));
733    }
734    let mut loaded: BTreeMap<String, Vec<u8>> = BTreeMap::new();
735    for file in &manifest.files {
736        let bytes = fetch(&file.name)?;
737        if bytes.len() as u64 != file.bytes || sha(&bytes) != file.sha256 {
738            return Err(format!(
739                "plate file {} does not match its manifest",
740                file.name
741            ));
742        }
743        loaded.insert(file.name.clone(), bytes);
744    }
745    let mut take = |name: &str| loaded.remove(name);
746    let u32s = |bytes: Vec<u8>| words(&bytes, 4, |c| u32::from_le_bytes(c.try_into().unwrap()));
747    let u16s = |bytes: Vec<u8>| words(&bytes, 2, |c| u16::from_le_bytes(c.try_into().unwrap()));
748    let layer = |name: &str| manifest.layers.get(name).map(String::as_str);
749    let depth_bytes = take("depth.bin").ok_or("missing plate file depth.bin")?;
750    let depth = match layer("depth") {
751        Some("r16-inverse") => Depth::Inverse(u16s(depth_bytes)),
752        Some("r32f") => Depth::Float(words(&depth_bytes, 4, |c| {
753            f32::from_le_bytes(c.try_into().unwrap())
754        })),
755        other => return Err(format!("plate depth format {other:?} is unknown")),
756    };
757    if layer("color") != Some("rgb9e5") {
758        return Err("plate colour must be rgb9e5".into());
759    }
760    let anchors = manifest.anchors.len();
761    let mut color = Vec::with_capacity(anchors);
762    for anchor in 0..anchors {
763        let name = format!("color-{anchor:03}.bin");
764        color.push(u32s(
765            take(&name).ok_or(format!("missing plate file {name}"))?,
766        ));
767    }
768    let mut per_anchor = |kind: &str| -> Result<Option<Vec<Vec<u32>>>, String> {
769        match layer(kind) {
770            None => Ok(None),
771            Some("rgb9e5") => {
772                let mut layers = Vec::with_capacity(anchors);
773                for anchor in 0..anchors {
774                    let name = format!("{kind}-{anchor:03}.bin");
775                    layers.push(u32s(
776                        take(&name).ok_or(format!("missing plate file {name}"))?,
777                    ));
778                }
779                Ok(Some(layers))
780            }
781            Some(other) => Err(format!("plate {kind} format {other} is unknown")),
782        }
783    };
784    let sun = per_anchor("sun")?;
785    let transfer = per_anchor("transfer")?;
786    let normal = match layer("normal") {
787        None => None,
788        Some("oct16") => Some(u32s(
789            take("normal.bin").ok_or("missing plate file normal.bin")?,
790        )),
791        Some(other) => return Err(format!("plate normal format {other} is unknown")),
792    };
793    let id = match layer("id") {
794        None => None,
795        Some("r16") => Some(u16s(take("id.bin").ok_or("missing plate file id.bin")?)),
796        Some(other) => return Err(format!("plate id format {other} is unknown")),
797    };
798    let plate = Plate {
799        manifest,
800        depth,
801        normal,
802        id,
803        color,
804        sun,
805        transfer,
806    };
807    check(&plate.manifest, &plate)?;
808    Ok(plate)
809}
810
811#[cfg(test)]
812mod tests;