Skip to main content

pfx_bake/plate/
run.rs

1use std::path::{Path, PathBuf};
2
3use pfx_gpu::Gpu;
4use pfx_gpu::pace::Pacer;
5use pfx_load::Sky;
6use pfx_load::scene::Scene;
7use pfx_materials::Blend;
8use pfx_trace::detail::{Bounces, Lens};
9use pfx_trace::sky::Environment;
10use pfx_trace::stage::Staged;
11use pfx_trace::{Converged, Projection, Trace};
12use sha2::{Digest, Sha256};
13
14use super::codec::{inverse_depth, oct16, rgb9e5};
15use super::{
16    AnchorRecord, Bounds, Depth, IdEntry, Layers, MAX_ID, PassRecord, Plate, PlateCamera,
17    PlateManifest, PlateRecipe, R16_RANGE, SCHEMA_VERSION, Sampling, SunRecord, ViewRecipe,
18    plate_size, read_plate, write_plate,
19};
20
21pub struct StagedPlate {
22    pub staged: Staged<Environment>,
23    pub ids: Vec<u32>,
24    pub objects: Vec<IdEntry>,
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum Pass {
29    Color,
30    Sun,
31    Transfer,
32}
33
34pub fn stage_plate(
35    subset: &Scene,
36    camera: &PlateCamera,
37    size: [u32; 2],
38    pass: Pass,
39) -> Result<StagedPlate, String> {
40    let mut staged = pfx_trace::stage::scene_at(subset, size[0] as f32 / size[1] as f32, 0.0)?;
41    let draws = subset.draws();
42    let mut ids = vec![0; staged.scene.materials.len()];
43    let mut objects: Vec<IdEntry> = Vec::new();
44    for (index, draw) in draws.items.iter().enumerate() {
45        let id = if draw.owner.is_some() { draw.id } else { 0 };
46        if id > MAX_ID {
47            return Err(format!(
48                "object {} has id {id}; a plate holds ids up to {MAX_ID}",
49                draw.object
50            ));
51        }
52        if id != 0 && !objects.iter().any(|entry| entry.id == id) {
53            objects.push(IdEntry {
54                id,
55                object: draw.object.clone(),
56            });
57        }
58        let copy = staged.scene.materials.len() as u32;
59        staged
60            .scene
61            .materials
62            .push(staged.scene.materials[draw.material as usize]);
63        ids.push(id);
64        for triangle in &mut staged.detail.instances[index].triangles {
65            triangle.material = copy;
66        }
67    }
68    objects.sort_by_key(|entry| entry.id);
69    staged.scene.camera = camera.trace_camera();
70    staged.detail.projection = Projection::Perspective;
71    staged.detail.lens = Lens {
72        shift: camera.shift,
73        ..Lens::default()
74    };
75    if pass == Pass::Transfer {
76        for instance in &mut staged.detail.instances {
77            instance.surface.casts_shadow = false;
78        }
79    }
80    if pass != Pass::Color {
81        staged.scene.sky = Environment::Hdr(Sky {
82            width: 1,
83            height: 1,
84            texels: vec![[0.0, 0.0, 0.0, 1.0]],
85        });
86        staged.detail.lights.clear();
87        staged.detail.bounces = Bounces::deep(0);
88        for material in &mut staged.scene.materials {
89            material.emission = [0.0; 3];
90            if material.content_layer.blend == Blend::Emit {
91                material.content_layer.strength = 0.0;
92            }
93        }
94    }
95    Ok(StagedPlate {
96        staged,
97        ids,
98        objects,
99    })
100}
101
102pub struct PlateRun<'a> {
103    pub gpu: &'a Gpu,
104    pub recipe: &'a PlateRecipe,
105    pub scene: &'a Path,
106    pub anchors: &'a [f32],
107    pub seed: u32,
108    pub max_samples: Option<u32>,
109    pub out: &'a Path,
110    pub force: bool,
111    pub between: &'a mut dyn FnMut(f64) -> bool,
112    pub log: &'a mut dyn FnMut(&str),
113}
114
115#[derive(Clone, Debug, PartialEq)]
116pub struct PlateOutcome {
117    pub view: String,
118    pub dir: PathBuf,
119    pub key: String,
120    pub baked: bool,
121    pub width: u32,
122    pub height: u32,
123    pub bytes_per_anchor: u64,
124    pub shared_bytes: u64,
125    pub samples: Vec<AnchorRecord>,
126}
127
128fn hex(bytes: &[u8]) -> String {
129    bytes.iter().map(|b| format!("{b:02x}")).collect()
130}
131
132fn feed(hasher: &mut Sha256, text: impl AsRef<[u8]>) {
133    let text = text.as_ref();
134    hasher.update((text.len() as u64).to_le_bytes());
135    hasher.update(text);
136}
137
138pub fn plate_key(
139    view: &str,
140    camera: &PlateCamera,
141    recipe: &PlateRecipe,
142    sampling: &Sampling,
143    anchors: &[(f32, Scene)],
144) -> Result<String, String> {
145    let mut h = Sha256::new();
146    feed(&mut h, "pito plate");
147    feed(&mut h, super::CODE_VERSION.to_le_bytes());
148    feed(&mut h, env!("CARGO_PKG_VERSION"));
149    feed(&mut h, view);
150    feed(
151        &mut h,
152        serde_json::to_vec(camera).map_err(|e| e.to_string())?,
153    );
154    feed(
155        &mut h,
156        format!(
157            "{:?} {:?} {:?} {:?}",
158            recipe.size,
159            recipe.overscan,
160            recipe.scale,
161            recipe.layers.names()
162        ),
163    );
164    feed(
165        &mut h,
166        serde_json::to_vec(sampling).map_err(|e| e.to_string())?,
167    );
168    feed(&mut h, format!("{:?}", recipe.key));
169    for (hour, scene) in anchors {
170        feed(&mut h, hour.to_le_bytes());
171        feed(&mut h, format!("{:?}", scene.sun));
172        feed(&mut h, scene.sky.as_ref().map_or([0; 32], |sky| sky.hash));
173    }
174    let Some((_, scene)) = anchors.first() else {
175        return Err("a plate needs an anchor".into());
176    };
177    let draws = scene.draws();
178    for draw in &draws.items {
179        feed(&mut h, &draw.object);
180        feed(&mut h, &draw.node);
181        feed(&mut h, draw.geometry.hash);
182        feed(
183            &mut h,
184            format!(
185                "{:?} {} {:?} {} {:?} {} {}",
186                draw.model,
187                draw.id,
188                draw.shadow,
189                draw.two_sided,
190                draw.clip,
191                draw.alpha_cutoff,
192                draw.face_camera
193            ),
194        );
195        feed(
196            &mut h,
197            serde_json::to_vec(&draws.materials[draw.material as usize])
198                .map_err(|e| e.to_string())?,
199        );
200        let content = draw
201            .content
202            .as_ref()
203            .and_then(|name| scene.contents.get(name))
204            .map_or([0; 32], |content| content.hash);
205        feed(&mut h, content);
206    }
207    for (name, text) in &scene.texts {
208        feed(&mut h, name);
209        feed(&mut h, &text.text);
210        feed(&mut h, Sha256::digest(text.font_bytes.as_slice()));
211        feed(
212            &mut h,
213            format!(
214                "{} {:?} {:?} {:?} {}",
215                text.size, text.color, text.at, text.rotate, text.lit
216            ),
217        );
218        if text.place != pfx_load::scene::IDENTITY {
219            feed(&mut h, format!("{:?}", text.place));
220        }
221    }
222    feed(&mut h, format!("{:?}", scene.lights));
223    feed(&mut h, format!("{:?}", scene.emitters));
224    feed(&mut h, format!("{:?}", scene.trace));
225    Ok(hex(&h.finalize()))
226}
227
228fn is_plate_folder(dir: &Path) -> bool {
229    let Ok(entries) = std::fs::read_dir(dir) else {
230        return false;
231    };
232    let names: Vec<String> = entries
233        .filter_map(Result::ok)
234        .map(|entry| entry.file_name().to_string_lossy().into_owned())
235        .collect();
236    if names.is_empty() {
237        return true;
238    }
239    let manifest = std::fs::read(dir.join("manifest.json")).ok();
240    let plate = manifest
241        .and_then(|bytes| serde_json::from_slice::<serde_json::Value>(&bytes).ok())
242        .is_some_and(|value| value.get("format").and_then(|v| v.as_str()) == Some("plate"));
243    plate
244        && names.iter().all(|name| {
245            name == "manifest.json"
246                || name == "manifest.sha256"
247                || (name.ends_with(".bin")
248                    && (name.starts_with("color-")
249                        || name.starts_with("sun-")
250                        || name.starts_with("transfer-")
251                        || name == "depth.bin"
252                        || name == "normal.bin"
253                        || name == "id.bin"))
254        })
255}
256
257fn pass_record(converged: &Converged) -> PassRecord {
258    PassRecord {
259        mean: converged.mean_samples(),
260        max: converged.max_samples(),
261        rounds: converged
262            .rounds
263            .iter()
264            .map(|round| [u64::from(round.samples), round.active])
265            .collect(),
266    }
267}
268
269fn trace_layer(
270    run: &mut PlateRun<'_>,
271    staged: &StagedPlate,
272    size: [u32; 2],
273    sampling: &Sampling,
274    pacer: &mut Pacer,
275) -> Result<(Vec<u32>, PassRecord), String> {
276    let mut trace = Trace::new_detailed(
277        run.gpu,
278        &staged.staged.scene,
279        &staged.staged.detail,
280        size[0],
281        size[1],
282    )
283    .map_err(String::from)?;
284    let between = &mut *run.between;
285    let converged = trace.sample_adaptive(
286        run.gpu,
287        pfx_trace::Adaptive {
288            threshold: sampling.threshold,
289            min_samples: sampling.min_samples,
290            max_samples: sampling.max_samples,
291            growth: sampling.growth,
292        },
293        sampling.seed,
294        pacer,
295        between,
296    )?;
297    let packed = trace
298        .readback_color(run.gpu)?
299        .chunks_exact(16)
300        .map(|texel| {
301            rgb9e5(std::array::from_fn(|k| {
302                f32::from_le_bytes(texel[k * 4..k * 4 + 4].try_into().unwrap())
303            }))
304        })
305        .collect();
306    Ok((packed, pass_record(&converged)))
307}
308
309pub struct Geometry {
310    pub depth: Vec<f32>,
311    pub distance: Vec<f32>,
312    pub material: Vec<u32>,
313    pub normal: Vec<[f32; 3]>,
314}
315
316pub fn probe_plate(
317    gpu: &Gpu,
318    staged: &StagedPlate,
319    camera: &PlateCamera,
320    size: [u32; 2],
321    between: &mut dyn FnMut(f64) -> bool,
322) -> Result<Geometry, String> {
323    let [width, height] = size;
324    let rows = ((2_097_152 / width) / 8 * 8).clamp(8, height.next_multiple_of(8));
325    let mut trace = Trace::new_detailed(
326        gpu,
327        &staged.staged.scene,
328        &staged.staged.detail,
329        width,
330        rows,
331    )
332    .map_err(String::from)?;
333    let texels = width as usize * height as usize;
334    let mut geometry = Geometry {
335        depth: vec![0.0; texels],
336        distance: vec![0.0; texels],
337        material: vec![0; texels],
338        normal: vec![[0.0; 3]; texels],
339    };
340    let mut first = 0;
341    while first < height {
342        let (strip, shift) = camera.strip(size, first, rows);
343        trace.set_camera(strip)?;
344        trace.set_lens(Lens {
345            shift,
346            ..Lens::default()
347        })?;
348        let probe = trace.probe(gpu)?;
349        let kept = rows.min(height - first) as usize;
350        let start = first as usize * width as usize;
351        let count = kept * width as usize;
352        geometry.depth[start..start + count].copy_from_slice(&probe.depth[..count]);
353        geometry.distance[start..start + count].copy_from_slice(&probe.distance[..count]);
354        geometry.material[start..start + count].copy_from_slice(&probe.id[..count]);
355        geometry.normal[start..start + count].copy_from_slice(&probe.normal[..count]);
356        between(0.0);
357        first += rows;
358    }
359    Ok(geometry)
360}
361
362struct Encoded {
363    depth: Depth,
364    depth_near: f32,
365    bounds: Bounds,
366    normal: Option<Vec<u32>>,
367    id: Option<Vec<u16>>,
368}
369
370fn encode_geometry(
371    geometry: &Geometry,
372    staged: &StagedPlate,
373    camera: &PlateCamera,
374    size: [u32; 2],
375    layers: Layers,
376) -> Result<Encoded, String> {
377    let surfaces = || {
378        geometry
379            .depth
380            .iter()
381            .copied()
382            .filter(|depth| *depth > 0.0 && depth.is_finite())
383    };
384    let nearest = surfaces().fold(f32::INFINITY, f32::min);
385    let farthest = surfaces().fold(0.0f32, f32::max);
386    let depth_near = if nearest.is_finite() {
387        nearest * 0.999
388    } else {
389        camera.near
390    };
391    let depth = if nearest.is_finite() && farthest / nearest > R16_RANGE {
392        Depth::Float(
393            geometry
394                .depth
395                .iter()
396                .map(|&depth| if depth > 0.0 { depth } else { 0.0 })
397                .collect(),
398        )
399    } else {
400        Depth::Inverse(
401            geometry
402                .depth
403                .iter()
404                .map(|&depth| inverse_depth(depth, depth_near))
405                .collect(),
406        )
407    };
408    let mut min = [f32::INFINITY; 3];
409    let mut max = [f32::NEG_INFINITY; 3];
410    for (index, &distance) in geometry.distance.iter().enumerate() {
411        if geometry.depth[index] <= 0.0 || geometry.depth[index].is_nan() {
412            continue;
413        }
414        let x = index as u32 % size[0];
415        let y = index as u32 / size[0];
416        let ray = camera.ray([x as f32 + 0.5, y as f32 + 0.5], size);
417        let length = ray.iter().map(|v| v * v).sum::<f32>().sqrt();
418        for k in 0..3 {
419            let point = camera.origin[k] + ray[k] / length * distance;
420            min[k] = min[k].min(point);
421            max[k] = max[k].max(point);
422        }
423    }
424    if !min[0].is_finite() {
425        min = camera.origin;
426        max = camera.origin;
427    }
428    let normal = layers.normal.then(|| {
429        geometry
430            .normal
431            .iter()
432            .zip(&geometry.depth)
433            .map(|(normal, &depth)| if depth > 0.0 { oct16(*normal) } else { 0 })
434            .collect()
435    });
436    let id = if layers.id {
437        let mut ids = Vec::with_capacity(geometry.material.len());
438        for &material in &geometry.material {
439            let id = match material.checked_sub(1) {
440                Some(index) => staged.ids.get(index as usize).copied().unwrap_or(0),
441                None => 0,
442            };
443            ids.push(id as u16);
444        }
445        Some(ids)
446    } else {
447        None
448    };
449    Ok(Encoded {
450        depth,
451        depth_near,
452        bounds: Bounds { min, max },
453        normal,
454        id,
455    })
456}
457
458fn bake_view(
459    run: &mut PlateRun<'_>,
460    view: &ViewRecipe,
461    scenes: &[(f32, Scene)],
462    camera: PlateCamera,
463    size: [u32; 2],
464    sampling: Sampling,
465    key: String,
466) -> Result<Plate, String> {
467    let mut pacer = Pacer::default();
468    let first = stage_plate(&scenes[0].1, &camera, size, Pass::Color)?;
469    let geometry = probe_plate(run.gpu, &first, &camera, size, run.between)?;
470    let Encoded {
471        depth,
472        depth_near,
473        bounds,
474        normal,
475        id,
476    } = encode_geometry(&geometry, &first, &camera, size, run.recipe.layers)?;
477    let objects = first.objects.clone();
478    drop(geometry);
479    let mut color = Vec::new();
480    let mut sun = run.recipe.layers.sun.then(Vec::new);
481    let mut transfer = run.recipe.layers.transfer.then(Vec::new);
482    let mut samples = Vec::new();
483    for (index, (hour, scene)) in scenes.iter().enumerate() {
484        (run.log)(&format!("plate {} at {hour}h: colour", view.name));
485        let staged = if index == 0 {
486            None
487        } else {
488            Some(stage_plate(scene, &camera, size, Pass::Color)?)
489        };
490        let (layer, record) = trace_layer(
491            run,
492            staged.as_ref().unwrap_or(&first),
493            size,
494            &sampling,
495            &mut pacer,
496        )?;
497        color.push(layer);
498        let mut records = [None, None];
499        for ((kind, pass), (layers, record)) in [("sun", Pass::Sun), ("transfer", Pass::Transfer)]
500            .into_iter()
501            .zip(
502                [&mut sun, &mut transfer]
503                    .into_iter()
504                    .zip(records.iter_mut()),
505            )
506        {
507            if let Some(layers) = layers {
508                (run.log)(&format!("plate {} at {hour}h: {kind}", view.name));
509                let staged = stage_plate(scene, &camera, size, pass)?;
510                let (layer, traced) = trace_layer(run, &staged, size, &sampling, &mut pacer)?;
511                layers.push(layer);
512                *record = Some(traced);
513            }
514        }
515        let [sun_record, transfer_record] = records;
516        samples.push(AnchorRecord {
517            hour: *hour,
518            color: record,
519            sun: sun_record,
520            transfer: transfer_record,
521        });
522    }
523    Ok(Plate {
524        manifest: PlateManifest {
525            schema_version: SCHEMA_VERSION,
526            format: super::FORMAT.into(),
527            engine: env!("CARGO_PKG_VERSION").into(),
528            key,
529            caller_key: run.recipe.key.clone(),
530            view: view.name.clone(),
531            size: run.recipe.size,
532            overscan: run.recipe.overscan,
533            scale: run.recipe.scale,
534            width: size[0],
535            height: size[1],
536            camera,
537            depth_near,
538            bounds,
539            anchors: scenes.iter().map(|(hour, _)| *hour).collect(),
540            suns: scenes
541                .iter()
542                .map(|(_, scene)| {
543                    let sun = scene.sun_or_dark();
544                    SunRecord {
545                        direction: sun.direction,
546                        color: sun.color,
547                        intensity: sun.intensity,
548                    }
549                })
550                .collect(),
551            layers: Default::default(),
552            sampling,
553            samples,
554            ids: objects,
555            files: Vec::new(),
556        },
557        depth,
558        normal,
559        id,
560        color,
561        sun,
562        transfer,
563    })
564}
565
566pub fn bake_plates(mut run: PlateRun<'_>) -> Result<Vec<PlateOutcome>, String> {
567    super::check_anchors(run.anchors)?;
568    let mut scenes = Vec::new();
569    for &hour in run.anchors {
570        let scene = Scene::open_at(run.scene, f64::from(hour)).map_err(String::from)?;
571        let mut subset = scene.static_subset();
572        subset.trace = run.recipe.trace(subset.trace);
573        scenes.push((hour, subset));
574    }
575    let mut adaptive = run.recipe.adaptive;
576    if let Some(max) = run.max_samples {
577        adaptive.max_samples = max.clamp(2, 65536);
578        adaptive.min_samples = adaptive.min_samples.min(adaptive.max_samples);
579    }
580    let sampling = Sampling {
581        threshold: adaptive.threshold,
582        min_samples: adaptive.min_samples,
583        max_samples: adaptive.max_samples,
584        growth: adaptive.growth,
585        seed: run.seed,
586    };
587    let mut outcomes = Vec::new();
588    let views = run.recipe.views.clone();
589    for view in &views {
590        let base = scenes[0].1.camera_or_default();
591        let camera = PlateCamera::new(
592            &view.keys.camera(&base)?,
593            run.recipe.size,
594            run.recipe.overscan,
595            run.recipe.scale,
596        );
597        let size = plate_size(run.recipe.size, run.recipe.overscan, run.recipe.scale);
598        let key = plate_key(&view.name, &camera, run.recipe, &sampling, &scenes)?;
599        let target = run.out.join(&view.name);
600        if target.exists() {
601            if !run.force
602                && let Ok(old) = read_plate(&target)
603                && old.manifest.key == key
604            {
605                (run.log)(&format!(
606                    "{} (unchanged)",
607                    target.join("manifest.json").display()
608                ));
609                outcomes.push(PlateOutcome {
610                    view: view.name.clone(),
611                    dir: target.clone(),
612                    key,
613                    baked: false,
614                    width: size[0],
615                    height: size[1],
616                    bytes_per_anchor: old.bytes_per_anchor(),
617                    shared_bytes: old.shared_bytes(),
618                    samples: old.manifest.samples.clone(),
619                });
620                continue;
621            }
622            if !is_plate_folder(&target) {
623                return Err(format!(
624                    "{} holds something that is not a plate; it is never replaced",
625                    target.display()
626                ));
627            }
628        }
629        let plate = bake_view(&mut run, view, &scenes, camera, size, sampling, key.clone())?;
630        let next = run.out.join(format!(".{}.next", view.name));
631        let old = run.out.join(format!(".{}.old", view.name));
632        for stale in [&next, &old] {
633            if stale.exists() {
634                std::fs::remove_dir_all(stale).map_err(|e| format!("{}: {e}", stale.display()))?;
635            }
636        }
637        write_plate(&next, &plate)?;
638        let written = read_plate(&next)?;
639        if written.manifest.key != key {
640            return Err("the written plate does not read back".into());
641        }
642        if target.exists() {
643            std::fs::rename(&target, &old).map_err(|e| format!("{}: {e}", target.display()))?;
644        }
645        std::fs::rename(&next, &target).map_err(|e| format!("{}: {e}", target.display()))?;
646        if old.exists() {
647            std::fs::remove_dir_all(&old).map_err(|e| format!("{}: {e}", old.display()))?;
648        }
649        (run.log)(&format!(
650            "{} ({key})",
651            target.join("manifest.json").display()
652        ));
653        outcomes.push(PlateOutcome {
654            view: view.name.clone(),
655            dir: target,
656            key,
657            baked: true,
658            width: size[0],
659            height: size[1],
660            bytes_per_anchor: plate.bytes_per_anchor(),
661            shared_bytes: plate.shared_bytes(),
662            samples: plate.manifest.samples.clone(),
663        });
664    }
665    Ok(outcomes)
666}