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}