1use std::fs;
2use std::io::Read;
3use std::path::{Path, PathBuf};
4
5use serde::{Deserialize, Serialize};
6
7use crate::config::DemoConfig;
8use crate::error::{Error, Result};
9use crate::frame::{save_scalar_field_png, Color, ImageFrame, ScalarField};
10use crate::host::{
11 default_host_realistic_profile, supervise_temporal_reuse, HostSupervisionOutputs,
12 HostTemporalInputs,
13};
14use crate::report::EXPERIMENT_SENTENCE;
15use crate::scene::{
16 generate_sequence_for_definition, scenario_by_id, MotionVector, Normal3, ScenarioId,
17 SceneFrame, SceneSequence, SurfaceTag,
18};
19use crate::taa::run_fixed_alpha_baseline;
20
21pub const EXTERNAL_CAPTURE_FORMAT_VERSION: &str = "dsfb_external_capture_v1";
22pub const NO_REAL_EXTERNAL_DATA_PROVIDED: &str = "NO REAL EXTERNAL DATA PROVIDED";
23
24#[derive(Clone, Debug)]
25pub struct OwnedHostTemporalInputs {
26 pub current_color: ImageFrame,
27 pub reprojected_history: ImageFrame,
28 pub motion_vectors: Vec<MotionVector>,
29 pub current_depth: Vec<f32>,
30 pub reprojected_depth: Vec<f32>,
31 pub current_normals: Vec<Normal3>,
32 pub reprojected_normals: Vec<Normal3>,
33 pub visibility_hint: Option<Vec<bool>>,
34 pub thin_hint: Option<ScalarField>,
35}
36
37impl OwnedHostTemporalInputs {
38 pub fn width(&self) -> usize {
39 self.current_color.width()
40 }
41
42 pub fn height(&self) -> usize {
43 self.current_color.height()
44 }
45
46 pub fn borrow(&self) -> HostTemporalInputs<'_> {
47 HostTemporalInputs {
48 current_color: &self.current_color,
49 reprojected_history: &self.reprojected_history,
50 motion_vectors: &self.motion_vectors,
51 current_depth: &self.current_depth,
52 reprojected_depth: &self.reprojected_depth,
53 current_normals: &self.current_normals,
54 reprojected_normals: &self.reprojected_normals,
55 visibility_hint: self.visibility_hint.as_deref(),
56 thin_hint: self.thin_hint.as_ref(),
57 }
58 }
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct BufferReference {
63 pub path: String,
64 pub format: String,
65 pub semantic: String,
66 #[serde(default)]
67 pub width: Option<usize>,
68 #[serde(default)]
69 pub height: Option<usize>,
70 #[serde(default)]
71 pub channels: Option<usize>,
72}
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
75pub struct ExternalBufferSet {
76 pub current_color: BufferReference,
77 #[serde(rename = "history_color", alias = "reprojected_history")]
78 pub reprojected_history: BufferReference,
79 pub motion_vectors: BufferReference,
80 pub current_depth: BufferReference,
81 #[serde(rename = "history_depth", alias = "reprojected_depth")]
82 pub reprojected_depth: BufferReference,
83 pub current_normals: BufferReference,
84 #[serde(rename = "history_normals", alias = "reprojected_normals")]
85 pub reprojected_normals: BufferReference,
86 pub metadata: BufferReference,
87 pub optional_mask: Option<BufferReference>,
88 #[serde(default)]
89 pub optional_reference: Option<BufferReference>,
90 #[serde(default)]
91 pub optional_ground_truth: Option<BufferReference>,
92 #[serde(default)]
93 pub optional_variance: Option<BufferReference>,
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize)]
97pub struct ExternalCaptureEntry {
98 pub label: String,
99 pub buffers: ExternalBufferSet,
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize)]
103#[serde(tag = "kind", rename_all = "snake_case")]
104pub enum ExternalCaptureSource {
105 Files,
106 SyntheticCompat {
107 scenario_id: String,
108 frame_index: Option<usize>,
109 },
110 EngineNative {
115 engine_type: String,
116 #[serde(default)]
117 engine_version: Option<String>,
118 #[serde(default)]
119 capture_tool: Option<String>,
120 #[serde(default)]
121 capture_note: Option<String>,
122 },
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct ExternalNormalization {
127 pub color: String,
128 pub motion_vectors: String,
129 pub depth: String,
130 pub normals: String,
131}
132
133#[derive(Clone, Debug, Serialize, Deserialize)]
134pub struct ExternalCaptureManifest {
135 pub format_version: String,
136 pub description: String,
137 pub source: ExternalCaptureSource,
138 #[serde(default)]
139 pub buffers: Option<ExternalBufferSet>,
140 #[serde(default)]
141 pub captures: Vec<ExternalCaptureEntry>,
142 pub normalization: ExternalNormalization,
143 pub notes: Vec<String>,
144}
145
146#[derive(Clone, Debug, Serialize, Deserialize)]
147struct ScalarBufferFile {
148 width: usize,
149 height: usize,
150 data: Vec<f32>,
151}
152
153#[derive(Clone, Debug, Serialize, Deserialize)]
154struct Vec2BufferFile {
155 width: usize,
156 height: usize,
157 data: Vec<[f32; 2]>,
158}
159
160#[derive(Clone, Debug, Serialize, Deserialize)]
161struct Vec3BufferFile {
162 width: usize,
163 height: usize,
164 data: Vec<[f32; 3]>,
165}
166
167#[derive(Clone, Debug, Serialize, Deserialize)]
168struct BoolBufferFile {
169 width: usize,
170 height: usize,
171 data: Vec<bool>,
172}
173
174#[derive(Clone, Debug, Serialize, Deserialize)]
175struct ColorBufferFile {
176 width: usize,
177 height: usize,
178 data: Vec<[f32; 3]>,
179}
180
181#[derive(Clone, Debug, Serialize, Deserialize)]
182pub struct ExternalCaptureMetadata {
183 pub scenario_id: Option<String>,
184 pub frame_index: usize,
185 pub history_frame_index: usize,
186 pub width: usize,
187 pub height: usize,
188 pub source_kind: String,
189 pub externally_validated: bool,
190 #[serde(default)]
191 pub real_external_data: bool,
192 #[serde(default)]
193 pub data_description: Option<String>,
194 pub notes: Vec<String>,
195}
196
197#[derive(Clone, Debug, Serialize, Deserialize)]
198pub struct ExternalHandoffMetrics {
199 pub measurement_kind: String,
200 pub external_capable: bool,
201 pub externally_validated: bool,
202 pub real_external_data_provided: bool,
203 pub no_real_external_data_provided: bool,
204 pub source_kind: String,
205 pub scenario_id: Option<String>,
206 pub frame_index: usize,
207 pub history_frame_index: usize,
208 pub width: usize,
209 pub height: usize,
210 pub capture_count: usize,
211 pub imported_formats: Vec<String>,
212 pub required_buffers: Vec<String>,
213 pub normalization_notes: Vec<String>,
214 pub roi_source: String,
215 pub ground_truth_available: bool,
216 pub variance_available: bool,
217 pub mean_trust: f32,
218 pub mean_alpha: f32,
219 pub intervention_rate: f32,
220 pub notes: Vec<String>,
221}
222
223#[derive(Clone, Debug)]
224pub struct ExternalImportArtifacts {
225 pub report_path: PathBuf,
226 pub metrics: ExternalHandoffMetrics,
227 pub resolved_manifest_path: PathBuf,
228}
229
230#[derive(Clone, Debug)]
231pub struct ExternalLoadedCapture {
232 pub label: String,
233 pub inputs: OwnedHostTemporalInputs,
234 pub metadata: ExternalCaptureMetadata,
235 pub mask: Option<Vec<bool>>,
236 pub reference: Option<ImageFrame>,
237 pub variance: Option<ScalarField>,
238}
239
240#[derive(Clone, Debug)]
241pub struct ExternalCaptureBundle {
242 pub manifest: ExternalCaptureManifest,
243 pub captures: Vec<ExternalLoadedCapture>,
244 pub real_external_data_provided: bool,
245 pub no_real_external_data_provided: bool,
246}
247
248pub fn example_manifest() -> ExternalCaptureManifest {
249 ExternalCaptureManifest {
250 format_version: EXTERNAL_CAPTURE_FORMAT_VERSION.to_string(),
251 description: "Synthetic compatibility example that exports one frame pair into the stable external buffer schema and re-imports it through the same file-based path.".to_string(),
252 source: ExternalCaptureSource::SyntheticCompat {
253 scenario_id: "motion_bias_band".to_string(),
254 frame_index: None,
255 },
256 buffers: Some(ExternalBufferSet {
257 current_color: BufferReference {
258 path: "external_capture/current_color.png".to_string(),
259 format: "png_rgb8".to_string(),
260 semantic: "current color, normalized [0,1] RGB".to_string(),
261 width: None,
262 height: None,
263 channels: None,
264 },
265 reprojected_history: BufferReference {
266 path: "external_capture/reprojected_history.png".to_string(),
267 format: "png_rgb8".to_string(),
268 semantic: "reprojected history color, normalized [0,1] RGB".to_string(),
269 width: None,
270 height: None,
271 channels: None,
272 },
273 motion_vectors: BufferReference {
274 path: "external_capture/motion_vectors.json".to_string(),
275 format: "json_vec2_f32".to_string(),
276 semantic: "per-pixel motion vector to previous frame in pixel units".to_string(),
277 width: None,
278 height: None,
279 channels: None,
280 },
281 current_depth: BufferReference {
282 path: "external_capture/current_depth.json".to_string(),
283 format: "json_scalar_f32".to_string(),
284 semantic: "current frame depth".to_string(),
285 width: None,
286 height: None,
287 channels: None,
288 },
289 reprojected_depth: BufferReference {
290 path: "external_capture/reprojected_depth.json".to_string(),
291 format: "json_scalar_f32".to_string(),
292 semantic: "reprojected depth from previous frame".to_string(),
293 width: None,
294 height: None,
295 channels: None,
296 },
297 current_normals: BufferReference {
298 path: "external_capture/current_normals.json".to_string(),
299 format: "json_vec3_f32".to_string(),
300 semantic: "current frame normals in view space, unit length".to_string(),
301 width: None,
302 height: None,
303 channels: None,
304 },
305 reprojected_normals: BufferReference {
306 path: "external_capture/reprojected_normals.json".to_string(),
307 format: "json_vec3_f32".to_string(),
308 semantic: "reprojected normals from previous frame".to_string(),
309 width: None,
310 height: None,
311 channels: None,
312 },
313 metadata: BufferReference {
314 path: "external_capture/metadata.json".to_string(),
315 format: "json_metadata".to_string(),
316 semantic: "capture metadata and provenance".to_string(),
317 width: None,
318 height: None,
319 channels: None,
320 },
321 optional_mask: Some(BufferReference {
322 path: "external_capture/optional_mask.json".to_string(),
323 format: "json_mask_bool".to_string(),
324 semantic: "optional ROI-like disclosure or debug mask".to_string(),
325 width: None,
326 height: None,
327 channels: None,
328 }),
329 optional_reference: Some(BufferReference {
330 path: "external_capture/optional_reference.png".to_string(),
331 format: "png_rgb8".to_string(),
332 semantic: "optional reference frame for evaluator-side error checks; synthetic in the bundled example".to_string(),
333 width: None,
334 height: None,
335 channels: None,
336 }),
337 optional_ground_truth: None,
338 optional_variance: None,
339 }),
340 captures: Vec::new(),
341 normalization: ExternalNormalization {
342 color: "linear RGB in [0,1]".to_string(),
343 motion_vectors: "pixel offsets to the previous frame; positive x samples from a pixel further right in history".to_string(),
344 depth: "monotonic depth with larger disagreement indicating less trust".to_string(),
345 normals: "unit vectors in a consistent view-space basis".to_string(),
346 },
347 notes: vec![
348 "Switch source.kind from synthetic_compat to files when real engine exports are available.".to_string(),
349 "The example capture is external-capable but not externally validated.".to_string(),
350 NO_REAL_EXTERNAL_DATA_PROVIDED.to_string(),
351 ],
352 }
353}
354
355pub fn write_example_manifest(path: &Path) -> Result<()> {
356 if let Some(parent) = path.parent() {
357 fs::create_dir_all(parent)?;
358 }
359 fs::write(path, serde_json::to_string_pretty(&example_manifest())?)?;
360 Ok(())
361}
362
363pub fn build_owned_inputs_from_sequence(
364 sequence: &SceneSequence,
365 frame_index: usize,
366 previous_history: Option<&ImageFrame>,
367) -> Result<OwnedHostTemporalInputs> {
368 let frame_index = frame_index.min(sequence.frames.len().saturating_sub(1));
369 if frame_index == 0 {
370 return Err(Error::Message(
371 "external capture requires a frame index after the first frame".to_string(),
372 ));
373 }
374
375 let scene_frame = &sequence.frames[frame_index];
376 let previous_scene = &sequence.frames[frame_index - 1];
377 let history_source = previous_history.unwrap_or(&previous_scene.ground_truth);
378 let reprojected_history = reproject_frame(history_source, scene_frame);
379 let reprojected_depth = reproject_depth(previous_scene, scene_frame);
380 let reprojected_normals = reproject_normals(previous_scene, scene_frame);
381
382 Ok(OwnedHostTemporalInputs {
383 current_color: scene_frame.ground_truth.clone(),
384 reprojected_history,
385 motion_vectors: scene_frame.motion.clone(),
386 current_depth: scene_frame.depth.clone(),
387 reprojected_depth,
388 current_normals: scene_frame.normals.clone(),
389 reprojected_normals,
390 visibility_hint: None,
391 thin_hint: None,
392 })
393}
394
395pub fn run_external_import_from_manifest(
396 config: &DemoConfig,
397 manifest_path: &Path,
398 output_dir: &Path,
399) -> Result<ExternalImportArtifacts> {
400 fs::create_dir_all(output_dir)?;
401 let bundle = load_external_capture_bundle(config, manifest_path, output_dir)?;
402 let first_capture = bundle
403 .captures
404 .first()
405 .ok_or_else(|| Error::Message("external capture bundle had no captures".to_string()))?;
406 let first_buffers = first_capture_buffer_set(&bundle.manifest)?;
407 let resolved_manifest_path = output_dir.join("resolved_external_capture_manifest.json");
408 fs::write(
409 &resolved_manifest_path,
410 serde_json::to_string_pretty(&bundle.manifest)?,
411 )?;
412
413 let profile =
414 default_host_realistic_profile(config.dsfb_alpha_range.min, config.dsfb_alpha_range.max);
415 let outputs = supervise_temporal_reuse(&first_capture.inputs.borrow(), &profile);
416 write_external_outputs(output_dir, &first_capture.inputs, &outputs)?;
417
418 let no_real_external_data_provided =
419 bundle.no_real_external_data_provided || !bundle.real_external_data_provided;
420 let mut notes = first_capture.metadata.notes.clone();
421 if no_real_external_data_provided {
422 notes.push(NO_REAL_EXTERNAL_DATA_PROVIDED.to_string());
423 }
424
425 let metrics = ExternalHandoffMetrics {
426 measurement_kind: if bundle.real_external_data_provided {
427 "external_buffer_import_real".to_string()
428 } else {
429 "external_buffer_import_external_ready".to_string()
430 },
431 external_capable: true,
432 externally_validated: first_capture.metadata.externally_validated,
433 real_external_data_provided: bundle.real_external_data_provided,
434 no_real_external_data_provided,
435 source_kind: first_capture.metadata.source_kind.clone(),
436 scenario_id: first_capture.metadata.scenario_id.clone(),
437 frame_index: first_capture.metadata.frame_index,
438 history_frame_index: first_capture.metadata.history_frame_index,
439 width: first_capture.metadata.width,
440 height: first_capture.metadata.height,
441 capture_count: bundle.captures.len(),
442 imported_formats: vec![
443 first_buffers.current_color.format.clone(),
444 first_buffers.reprojected_history.format.clone(),
445 first_buffers.motion_vectors.format.clone(),
446 first_buffers.current_depth.format.clone(),
447 first_buffers.current_normals.format.clone(),
448 ],
449 required_buffers: vec![
450 "current_color".to_string(),
451 "reprojected_history".to_string(),
452 "motion_vectors".to_string(),
453 "current_depth".to_string(),
454 "reprojected_depth".to_string(),
455 "current_normals".to_string(),
456 "reprojected_normals".to_string(),
457 ],
458 normalization_notes: vec![
459 bundle.manifest.normalization.color.clone(),
460 bundle.manifest.normalization.motion_vectors.clone(),
461 bundle.manifest.normalization.depth.clone(),
462 bundle.manifest.normalization.normals.clone(),
463 ],
464 roi_source: if first_capture.mask.is_some() {
465 "manifest_mask".to_string()
466 } else {
467 "derived_proxy_mask".to_string()
468 },
469 ground_truth_available: first_capture.reference.is_some(),
470 variance_available: first_capture.variance.is_some(),
471 mean_trust: outputs.trust.mean(),
472 mean_alpha: outputs.alpha.mean(),
473 intervention_rate: outputs.intervention.mean(),
474 notes,
475 };
476
477 let report_path = output_dir.join("external_replay_report.md");
478 let handoff_alias_path = output_dir.join("external_handoff_report.md");
479 write_external_replay_report(&report_path, &metrics, &bundle.manifest)?;
480 fs::copy(&report_path, &handoff_alias_path)?;
481
482 Ok(ExternalImportArtifacts {
483 report_path,
484 metrics,
485 resolved_manifest_path,
486 })
487}
488
489pub fn load_external_capture_bundle(
490 config: &DemoConfig,
491 manifest_path: &Path,
492 output_dir: &Path,
493) -> Result<ExternalCaptureBundle> {
494 let manifest_text = fs::read_to_string(manifest_path)?;
495 let manifest: ExternalCaptureManifest = serde_json::from_str(&manifest_text)?;
496 if manifest.format_version != EXTERNAL_CAPTURE_FORMAT_VERSION {
497 return Err(Error::Message(format!(
498 "unsupported external capture format version {}",
499 manifest.format_version
500 )));
501 }
502
503 let resolved_manifest = match &manifest.source {
504 ExternalCaptureSource::Files => manifest.clone(),
505 ExternalCaptureSource::SyntheticCompat {
506 scenario_id,
507 frame_index,
508 } => {
509 let scenario_id = parse_scenario_id(scenario_id)?;
510 let definition = scenario_by_id(&config.scene, scenario_id).ok_or_else(|| {
511 Error::Message(format!(
512 "synthetic compat scenario {scenario_id:?} not found"
513 ))
514 })?;
515 let sequence = generate_sequence_for_definition(&definition);
516 let export_frame_index = frame_index.unwrap_or(
517 definition
518 .onset_frame
519 .min(sequence.frames.len().saturating_sub(1)),
520 );
521 let fixed_alpha = run_fixed_alpha_baseline(&sequence, config.baseline.fixed_alpha);
522 let previous_history = fixed_alpha.taa.resolved_frames.get(export_frame_index - 1);
523 let inputs =
524 build_owned_inputs_from_sequence(&sequence, export_frame_index, previous_history)?;
525 let metadata = ExternalCaptureMetadata {
526 scenario_id: Some(sequence.scenario_id.as_str().to_string()),
527 frame_index: export_frame_index,
528 history_frame_index: export_frame_index.saturating_sub(1),
529 width: inputs.width(),
530 height: inputs.height(),
531 source_kind: "synthetic_compat".to_string(),
532 externally_validated: false,
533 real_external_data: false,
534 data_description: Some(
535 "Deterministic synthetic compatibility export generated inside the crate"
536 .to_string(),
537 ),
538 notes: vec![
539 "The example external capture was generated from the crate's deterministic synthetic suite.".to_string(),
540 "Replace source.kind with files and point the buffer paths at real engine exports to move beyond synthetic compatibility.".to_string(),
541 NO_REAL_EXTERNAL_DATA_PROVIDED.to_string(),
542 ],
543 };
544 let capture_mask =
545 compute_external_compatible_mask(&sequence.frames[export_frame_index]);
546 materialize_capture_bundle(
547 &manifest,
548 output_dir,
549 &inputs,
550 &metadata,
551 Some(&capture_mask),
552 Some(&inputs.current_color),
553 )?
554 }
555 ExternalCaptureSource::EngineNative { .. } => manifest.clone(),
557 };
558
559 let captures = load_capture_entries(&resolved_manifest, manifest_path, output_dir)?;
560 let real_external_data_provided = captures
561 .iter()
562 .any(|capture| capture.metadata.real_external_data);
563 Ok(ExternalCaptureBundle {
564 manifest: resolved_manifest,
565 captures,
566 real_external_data_provided,
567 no_real_external_data_provided: !real_external_data_provided,
568 })
569}
570
571fn materialize_capture_bundle(
572 manifest: &ExternalCaptureManifest,
573 base_dir: &Path,
574 inputs: &OwnedHostTemporalInputs,
575 metadata: &ExternalCaptureMetadata,
576 optional_mask: Option<&[bool]>,
577 optional_reference: Option<&ImageFrame>,
578) -> Result<ExternalCaptureManifest> {
579 let buffer_set = manifest.buffers.clone().ok_or_else(|| {
580 Error::Message("synthetic compat manifest requires a top-level buffers block".to_string())
581 })?;
582 write_color_buffer(base_dir, &buffer_set.current_color, &inputs.current_color)?;
583 write_color_buffer(
584 base_dir,
585 &buffer_set.reprojected_history,
586 &inputs.reprojected_history,
587 )?;
588 write_vec2_buffer(
589 base_dir,
590 &buffer_set.motion_vectors,
591 &inputs.motion_vectors,
592 inputs.width(),
593 inputs.height(),
594 )?;
595 write_scalar_buffer(
596 base_dir,
597 &buffer_set.current_depth,
598 &inputs.current_depth,
599 inputs.width(),
600 inputs.height(),
601 )?;
602 write_scalar_buffer(
603 base_dir,
604 &buffer_set.reprojected_depth,
605 &inputs.reprojected_depth,
606 inputs.width(),
607 inputs.height(),
608 )?;
609 write_vec3_buffer(
610 base_dir,
611 &buffer_set.current_normals,
612 &inputs.current_normals,
613 inputs.width(),
614 inputs.height(),
615 )?;
616 write_vec3_buffer(
617 base_dir,
618 &buffer_set.reprojected_normals,
619 &inputs.reprojected_normals,
620 inputs.width(),
621 inputs.height(),
622 )?;
623 if let Some(mask_ref) = &buffer_set.optional_mask {
624 let fallback_mask;
625 let mask_values = if let Some(values) = optional_mask {
626 values
627 } else {
628 fallback_mask = vec![false; inputs.width() * inputs.height()];
629 &fallback_mask
630 };
631 write_bool_buffer(
632 base_dir,
633 mask_ref,
634 mask_values,
635 inputs.width(),
636 inputs.height(),
637 )?;
638 }
639 if let Some(reference_ref) = &buffer_set
640 .optional_ground_truth
641 .as_ref()
642 .or(buffer_set.optional_reference.as_ref())
643 {
644 if let Some(reference_frame) = optional_reference {
645 write_color_buffer(base_dir, reference_ref, reference_frame)?;
646 }
647 }
648 let metadata_path = base_dir.join(&buffer_set.metadata.path);
649 if let Some(parent) = metadata_path.parent() {
650 fs::create_dir_all(parent)?;
651 }
652 fs::write(&metadata_path, serde_json::to_string_pretty(metadata)?)?;
653 let mut resolved = manifest.clone();
654 resolved.buffers = Some(buffer_set);
655 Ok(resolved)
656}
657
658fn load_capture_entries(
659 manifest: &ExternalCaptureManifest,
660 manifest_path: &Path,
661 synthetic_base_dir: &Path,
662) -> Result<Vec<ExternalLoadedCapture>> {
663 let entries = capture_entries(manifest)?;
664 match &manifest.source {
665 ExternalCaptureSource::Files => {
666 let base_dir = manifest_path.parent().ok_or_else(|| {
667 Error::Message("manifest path had no parent directory".to_string())
668 })?;
669 entries
670 .iter()
671 .map(|(label, buffers)| {
672 load_single_capture(base_dir, label, buffers, &manifest.normalization)
673 })
674 .collect()
675 }
676 ExternalCaptureSource::SyntheticCompat { .. } => entries
677 .iter()
678 .map(|(label, buffers)| {
679 load_single_capture(synthetic_base_dir, label, buffers, &manifest.normalization)
680 })
681 .collect(),
682 ExternalCaptureSource::EngineNative { .. } => {
684 let base_dir = manifest_path.parent().ok_or_else(|| {
685 Error::Message("manifest path had no parent directory".to_string())
686 })?;
687 entries
688 .iter()
689 .map(|(label, buffers)| {
690 load_single_capture(base_dir, label, buffers, &manifest.normalization)
691 })
692 .collect()
693 }
694 }
695}
696
697fn capture_entries(manifest: &ExternalCaptureManifest) -> Result<Vec<(String, ExternalBufferSet)>> {
698 if !manifest.captures.is_empty() {
699 return Ok(manifest
700 .captures
701 .iter()
702 .map(|entry| (entry.label.clone(), entry.buffers.clone()))
703 .collect());
704 }
705 if let Some(buffers) = &manifest.buffers {
706 return Ok(vec![("capture_0".to_string(), buffers.clone())]);
707 }
708 Err(Error::Message(
709 "external capture manifest must provide either `buffers` for one frame pair or `captures` for a short sequence".to_string(),
710 ))
711}
712
713fn first_capture_buffer_set(manifest: &ExternalCaptureManifest) -> Result<ExternalBufferSet> {
714 capture_entries(manifest)?
715 .into_iter()
716 .next()
717 .map(|(_, buffers)| buffers)
718 .ok_or_else(|| Error::Message("external manifest had no buffer set".to_string()))
719}
720
721fn load_single_capture(
722 base_dir: &Path,
723 label: &str,
724 buffers: &ExternalBufferSet,
725 normalization: &ExternalNormalization,
726) -> Result<ExternalLoadedCapture> {
727 let metadata = load_metadata(base_dir, &buffers.metadata, false)?;
728 let inputs = load_owned_inputs(buffers, base_dir, metadata.width, metadata.height)?;
729 if metadata.width != inputs.width() || metadata.height != inputs.height() {
730 return Err(Error::Message(format!(
731 "metadata extent {}x{} did not match imported buffers {}x{}",
732 metadata.width,
733 metadata.height,
734 inputs.width(),
735 inputs.height()
736 )));
737 }
738 validate_normalization(label, &inputs, normalization)?;
739 let mask = buffers
740 .optional_mask
741 .as_ref()
742 .map(|reference| load_bool_buffer(base_dir, reference, metadata.width, metadata.height))
743 .transpose()?
744 .map(|file| file.data);
745 let reference = buffers
746 .optional_ground_truth
747 .as_ref()
748 .or(buffers.optional_reference.as_ref())
749 .map(|reference| load_color_buffer(base_dir, reference, metadata.width, metadata.height))
750 .transpose()?;
751 let variance = buffers
752 .optional_variance
753 .as_ref()
754 .map(|reference| {
755 load_scalar_buffer(base_dir, reference, metadata.width, metadata.height)
756 .map(|file| ScalarField::from_values(metadata.width, metadata.height, file.data))
757 })
758 .transpose()?;
759
760 Ok(ExternalLoadedCapture {
761 label: label.to_string(),
762 inputs,
763 metadata,
764 mask,
765 reference,
766 variance,
767 })
768}
769
770fn write_external_outputs(
771 output_dir: &Path,
772 inputs: &OwnedHostTemporalInputs,
773 outputs: &HostSupervisionOutputs,
774) -> Result<()> {
775 inputs
776 .current_color
777 .save_png(&output_dir.join("external_current_color.png"))?;
778 inputs
779 .reprojected_history
780 .save_png(&output_dir.join("external_reprojected_history.png"))?;
781 save_scalar_field_png(
782 &outputs.trust,
783 &output_dir.join("external_trust.png"),
784 |value| {
785 let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
786 [v, v, 255, 255]
787 },
788 )?;
789 save_scalar_field_png(
790 &outputs.alpha,
791 &output_dir.join("external_alpha.png"),
792 |value| {
793 let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
794 [255, v, 0, 255]
795 },
796 )?;
797 save_scalar_field_png(
798 &outputs.intervention,
799 &output_dir.join("external_intervention.png"),
800 |value| {
801 let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
802 [255, 32, v, 255]
803 },
804 )?;
805 Ok(())
806}
807
808fn write_external_replay_report(
809 path: &Path,
810 metrics: &ExternalHandoffMetrics,
811 manifest: &ExternalCaptureManifest,
812) -> Result<()> {
813 let mut markdown = String::new();
814 markdown.push_str("# External Replay Report\n\n");
815 markdown.push_str(EXPERIMENT_SENTENCE);
816 markdown.push_str("\n\n");
817 markdown.push_str("This report covers the file-based external buffer replay path. It demonstrates that the crate is external-capable, not externally validated.\n\n");
818 markdown.push_str(&format!(
819 "Source kind: `{}`. Externally validated: `{}`. Real external data provided: `{}`.\n\n",
820 metrics.source_kind, metrics.externally_validated, metrics.real_external_data_provided
821 ));
822 if metrics.no_real_external_data_provided {
823 markdown.push_str(NO_REAL_EXTERNAL_DATA_PROVIDED);
824 markdown.push_str("\n\n");
825 }
826 markdown.push_str("## Required Buffers\n\n");
827 for buffer in &metrics.required_buffers {
828 markdown.push_str(&format!("- `{buffer}`\n"));
829 }
830 markdown.push_str("\n## Accepted Formats\n\n");
831 markdown.push_str("- `png_rgb8`\n");
832 markdown.push_str("- `json_rgb_f32`\n");
833 markdown.push_str("- `exr_rgb32f`\n");
834 markdown.push_str("- `json_scalar_f32`\n");
835 markdown.push_str("- `exr_r32f`\n");
836 markdown.push_str("- `raw_r32f` with inline width/height/channels = 1\n");
837 markdown.push_str("- `json_vec2_f32`\n");
838 markdown.push_str("- `exr_rg32f`\n");
839 markdown.push_str("- `raw_rg32f` with inline width/height/channels >= 2\n");
840 markdown.push_str("- `json_vec3_f32`\n");
841 markdown.push_str("- `raw_rgb32f` with inline width/height/channels >= 3\n");
842 markdown.push_str("- `json_mask_bool`\n");
843 markdown.push_str("- `raw_mask_u8` with inline width/height/channels = 1\n");
844 markdown.push_str("- `json_metadata`\n\n");
845 markdown.push_str("## Normalization Conventions\n\n");
846 for note in &metrics.normalization_notes {
847 markdown.push_str(&format!("- {note}\n"));
848 }
849 markdown.push_str("\n## Imported Capture Summary\n\n");
850 markdown.push_str(&format!(
851 "- Resolution: {}x{}\n- Frame index: {}\n- History frame index: {}\n- Mean trust: {:.4}\n- Mean alpha: {:.4}\n- Mean intervention: {:.4}\n",
852 metrics.width,
853 metrics.height,
854 metrics.frame_index,
855 metrics.history_frame_index,
856 metrics.mean_trust,
857 metrics.mean_alpha,
858 metrics.intervention_rate
859 ));
860 markdown.push_str("\n## How An Engine Team Would Use This\n\n");
861 markdown.push_str("- Export one frame pair using the buffer names and normalization described in the manifest.\n");
862 markdown.push_str(
863 "- Set `source.kind` to `files` and point the buffer paths at the exported assets.\n",
864 );
865 markdown.push_str("- Run `cargo run --release -- run-external-replay --manifest <manifest> --output <dir>`.\n");
866 markdown.push_str(
867 "- Alias: `cargo run --release -- replay-external --manifest <manifest> --output <dir>`.\n",
868 );
869 markdown.push_str("- Inspect `external_trust.png`, `external_alpha.png`, and `external_intervention.png` plus the generated report.\n\n");
870 markdown.push_str("## What Is Not Proven\n\n");
871 markdown.push_str("- This report does not claim any real engine capture has been validated unless the metadata says so.\n");
872 markdown.push_str("- The example manifest included in the crate is synthetic compatibility data, not field data.\n\n");
873 markdown.push_str("## Remaining Blockers\n\n");
874 markdown.push_str("- A real renderer still needs to export buffers into this schema.\n");
875 markdown.push_str("- Real production captures and engine motion vectors are still required for external validation.\n");
876 markdown.push_str("- If the GPU external report is unmeasured on the evaluator machine, imported-capture GPU timing still remains future work there.\n\n");
877 markdown.push_str("## Manifest Notes\n\n");
878 for note in &manifest.notes {
879 markdown.push_str(&format!("- {note}\n"));
880 }
881 fs::write(path, markdown)?;
882 Ok(())
883}
884
885fn load_owned_inputs(
886 buffers: &ExternalBufferSet,
887 base_dir: &Path,
888 expected_width: usize,
889 expected_height: usize,
890) -> Result<OwnedHostTemporalInputs> {
891 let current_color = load_color_buffer(
892 base_dir,
893 &buffers.current_color,
894 expected_width,
895 expected_height,
896 )?;
897 let reprojected_history = load_color_buffer(
898 base_dir,
899 &buffers.reprojected_history,
900 expected_width,
901 expected_height,
902 )?;
903 let width = current_color.width();
904 let height = current_color.height();
905 let motion_vectors = load_vec2_buffer(
906 base_dir,
907 &buffers.motion_vectors,
908 expected_width,
909 expected_height,
910 )?;
911 validate_buffer_extent(
912 "motion_vectors",
913 motion_vectors.width,
914 motion_vectors.height,
915 width,
916 height,
917 )?;
918 let current_depth = load_scalar_buffer(
919 base_dir,
920 &buffers.current_depth,
921 expected_width,
922 expected_height,
923 )?;
924 validate_buffer_extent(
925 "current_depth",
926 current_depth.width,
927 current_depth.height,
928 width,
929 height,
930 )?;
931 let reprojected_depth = load_scalar_buffer(
932 base_dir,
933 &buffers.reprojected_depth,
934 expected_width,
935 expected_height,
936 )?;
937 validate_buffer_extent(
938 "reprojected_depth",
939 reprojected_depth.width,
940 reprojected_depth.height,
941 width,
942 height,
943 )?;
944 let current_normals = load_vec3_buffer(
945 base_dir,
946 &buffers.current_normals,
947 expected_width,
948 expected_height,
949 )?;
950 validate_buffer_extent(
951 "current_normals",
952 current_normals.width,
953 current_normals.height,
954 width,
955 height,
956 )?;
957 let reprojected_normals = load_vec3_buffer(
958 base_dir,
959 &buffers.reprojected_normals,
960 expected_width,
961 expected_height,
962 )?;
963 validate_buffer_extent(
964 "reprojected_normals",
965 reprojected_normals.width,
966 reprojected_normals.height,
967 width,
968 height,
969 )?;
970 let optional_mask = buffers
971 .optional_mask
972 .as_ref()
973 .map(|reference| load_bool_buffer(base_dir, reference, expected_width, expected_height))
974 .transpose()?;
975 if let Some(mask) = &optional_mask {
976 validate_buffer_extent("optional_mask", mask.width, mask.height, width, height)?;
977 }
978 if let Some(reference) = buffers
979 .optional_ground_truth
980 .as_ref()
981 .or(buffers.optional_reference.as_ref())
982 {
983 let optional_reference =
984 load_color_buffer(base_dir, reference, expected_width, expected_height)?;
985 validate_buffer_extent(
986 "optional_reference",
987 optional_reference.width(),
988 optional_reference.height(),
989 width,
990 height,
991 )?;
992 }
993 Ok(OwnedHostTemporalInputs {
994 current_color,
995 reprojected_history,
996 motion_vectors: motion_vectors
997 .data
998 .into_iter()
999 .map(|value| MotionVector {
1000 to_prev_x: value[0],
1001 to_prev_y: value[1],
1002 })
1003 .collect(),
1004 current_depth: current_depth.data,
1005 reprojected_depth: reprojected_depth.data,
1006 current_normals: current_normals
1007 .data
1008 .into_iter()
1009 .map(|value| Normal3::new(value[0], value[1], value[2]).normalized())
1010 .collect(),
1011 reprojected_normals: reprojected_normals
1012 .data
1013 .into_iter()
1014 .map(|value| Normal3::new(value[0], value[1], value[2]).normalized())
1015 .collect(),
1016 visibility_hint: optional_mask.map(|mask| mask.data),
1017 thin_hint: None,
1018 })
1019}
1020
1021fn load_metadata(
1022 base_dir: &Path,
1023 reference: &BufferReference,
1024 externally_validated: bool,
1025) -> Result<ExternalCaptureMetadata> {
1026 if reference.format != "json_metadata" {
1027 return Err(Error::Message(format!(
1028 "metadata buffer {} must use json_metadata format",
1029 reference.path
1030 )));
1031 }
1032 let path = base_dir.join(&reference.path);
1033 let text = fs::read_to_string(path)?;
1034 let mut metadata: ExternalCaptureMetadata = serde_json::from_str(&text)?;
1035 metadata.externally_validated = externally_validated || metadata.externally_validated;
1036 if metadata.width == 0 || metadata.height == 0 {
1037 return Err(Error::Message(format!(
1038 "metadata {} declared zero-sized capture {}x{}",
1039 reference.path, metadata.width, metadata.height
1040 )));
1041 }
1042 if metadata.history_frame_index > metadata.frame_index {
1043 return Err(Error::Message(format!(
1044 "metadata {} had history_frame_index {} after frame_index {}",
1045 reference.path, metadata.history_frame_index, metadata.frame_index
1046 )));
1047 }
1048 Ok(metadata)
1049}
1050
1051fn load_color_buffer(
1052 base_dir: &Path,
1053 reference: &BufferReference,
1054 expected_width: usize,
1055 expected_height: usize,
1056) -> Result<ImageFrame> {
1057 let path = base_dir.join(&reference.path);
1058 let frame = match reference.format.as_str() {
1059 "png_rgb8" => ImageFrame::load_png(&path)?,
1060 "exr_rgb32f" => load_exr_color(&path)?,
1061 "raw_rgb32f" => load_raw_color(&path, reference, expected_width, expected_height)?,
1062 "json_rgb_f32" => {
1063 let file: ColorBufferFile = serde_json::from_str(&fs::read_to_string(path)?)?;
1064 ImageFrame::from_pixels(
1065 file.width,
1066 file.height,
1067 file.data
1068 .into_iter()
1069 .map(|rgb| Color::rgb(rgb[0], rgb[1], rgb[2]))
1070 .collect(),
1071 )
1072 }
1073 other => {
1074 return Err(Error::Message(format!(
1075 "unsupported color buffer format {other}"
1076 )))
1077 }
1078 };
1079 validate_buffer_extent(
1080 "color_buffer",
1081 frame.width(),
1082 frame.height(),
1083 expected_width,
1084 expected_height,
1085 )?;
1086 Ok(frame)
1087}
1088
1089pub(crate) fn load_color_buffer_from_reference(
1090 base_dir: &Path,
1091 reference: &BufferReference,
1092 expected_width: usize,
1093 expected_height: usize,
1094) -> Result<ImageFrame> {
1095 load_color_buffer(base_dir, reference, expected_width, expected_height)
1096}
1097
1098fn load_scalar_buffer(
1099 base_dir: &Path,
1100 reference: &BufferReference,
1101 expected_width: usize,
1102 expected_height: usize,
1103) -> Result<ScalarBufferFile> {
1104 let path = base_dir.join(&reference.path);
1105 let file = match reference.format.as_str() {
1106 "json_scalar_f32" => serde_json::from_str(&fs::read_to_string(path)?)?,
1107 "exr_r32f" => load_exr_scalar(&path)?,
1108 "raw_r32f" => load_raw_scalar(&path, reference, expected_width, expected_height)?,
1109 other => {
1110 return Err(Error::Message(format!(
1111 "unsupported scalar buffer format {other}"
1112 )))
1113 }
1114 };
1115 validate_element_count("scalar_buffer", file.width, file.height, file.data.len())?;
1116 validate_buffer_extent(
1117 "scalar_buffer",
1118 file.width,
1119 file.height,
1120 expected_width,
1121 expected_height,
1122 )?;
1123 Ok(file)
1124}
1125
1126pub(crate) fn load_scalar_buffer_from_reference(
1127 base_dir: &Path,
1128 reference: &BufferReference,
1129 expected_width: usize,
1130 expected_height: usize,
1131) -> Result<(usize, usize, Vec<f32>)> {
1132 let file = load_scalar_buffer(base_dir, reference, expected_width, expected_height)?;
1133 Ok((file.width, file.height, file.data))
1134}
1135
1136fn load_vec2_buffer(
1137 base_dir: &Path,
1138 reference: &BufferReference,
1139 expected_width: usize,
1140 expected_height: usize,
1141) -> Result<Vec2BufferFile> {
1142 let path = base_dir.join(&reference.path);
1143 let file = match reference.format.as_str() {
1144 "json_vec2_f32" => serde_json::from_str(&fs::read_to_string(path)?)?,
1145 "exr_rg32f" => load_exr_vec2(&path)?,
1146 "raw_rg32f" => load_raw_vec2(&path, reference, expected_width, expected_height)?,
1147 other => {
1148 return Err(Error::Message(format!(
1149 "unsupported vec2 buffer format {other}"
1150 )))
1151 }
1152 };
1153 validate_element_count("motion_vectors", file.width, file.height, file.data.len())?;
1154 validate_buffer_extent(
1155 "motion_vectors",
1156 file.width,
1157 file.height,
1158 expected_width,
1159 expected_height,
1160 )?;
1161 Ok(file)
1162}
1163
1164pub(crate) fn load_vec2_buffer_from_reference(
1165 base_dir: &Path,
1166 reference: &BufferReference,
1167 expected_width: usize,
1168 expected_height: usize,
1169) -> Result<(usize, usize, Vec<[f32; 2]>)> {
1170 let file = load_vec2_buffer(base_dir, reference, expected_width, expected_height)?;
1171 Ok((file.width, file.height, file.data))
1172}
1173
1174fn load_vec3_buffer(
1175 base_dir: &Path,
1176 reference: &BufferReference,
1177 expected_width: usize,
1178 expected_height: usize,
1179) -> Result<Vec3BufferFile> {
1180 let path = base_dir.join(&reference.path);
1181 let file = match reference.format.as_str() {
1182 "json_vec3_f32" => serde_json::from_str(&fs::read_to_string(path)?)?,
1183 "exr_rgb32f" => load_exr_vec3(&path)?,
1184 "raw_rgb32f" => load_raw_vec3(&path, reference, expected_width, expected_height)?,
1185 other => {
1186 return Err(Error::Message(format!(
1187 "unsupported vec3 buffer format {other}"
1188 )))
1189 }
1190 };
1191 validate_element_count("normal_buffer", file.width, file.height, file.data.len())?;
1192 validate_buffer_extent(
1193 "normal_buffer",
1194 file.width,
1195 file.height,
1196 expected_width,
1197 expected_height,
1198 )?;
1199 Ok(file)
1200}
1201
1202pub(crate) fn load_vec3_buffer_from_reference(
1203 base_dir: &Path,
1204 reference: &BufferReference,
1205 expected_width: usize,
1206 expected_height: usize,
1207) -> Result<(usize, usize, Vec<[f32; 3]>)> {
1208 let file = load_vec3_buffer(base_dir, reference, expected_width, expected_height)?;
1209 Ok((file.width, file.height, file.data))
1210}
1211
1212fn load_bool_buffer(
1213 base_dir: &Path,
1214 reference: &BufferReference,
1215 expected_width: usize,
1216 expected_height: usize,
1217) -> Result<BoolBufferFile> {
1218 let path = base_dir.join(&reference.path);
1219 let file = match reference.format.as_str() {
1220 "json_mask_bool" => serde_json::from_str(&fs::read_to_string(path)?)?,
1221 "raw_mask_u8" => load_raw_mask(&path, reference, expected_width, expected_height)?,
1222 other => {
1223 return Err(Error::Message(format!(
1224 "unsupported mask buffer format {other}"
1225 )))
1226 }
1227 };
1228 validate_element_count("optional_mask", file.width, file.height, file.data.len())?;
1229 validate_buffer_extent(
1230 "optional_mask",
1231 file.width,
1232 file.height,
1233 expected_width,
1234 expected_height,
1235 )?;
1236 Ok(file)
1237}
1238
1239pub(crate) fn load_bool_buffer_from_reference(
1240 base_dir: &Path,
1241 reference: &BufferReference,
1242 expected_width: usize,
1243 expected_height: usize,
1244) -> Result<(usize, usize, Vec<bool>)> {
1245 let file = load_bool_buffer(base_dir, reference, expected_width, expected_height)?;
1246 Ok((file.width, file.height, file.data))
1247}
1248
1249fn validate_buffer_extent(
1250 label: &str,
1251 width: usize,
1252 height: usize,
1253 expected_width: usize,
1254 expected_height: usize,
1255) -> Result<()> {
1256 if width != expected_width || height != expected_height {
1257 return Err(Error::Message(format!(
1258 "{label} extent {width}x{height} did not match expected {expected_width}x{expected_height}"
1259 )));
1260 }
1261 Ok(())
1262}
1263
1264fn validate_element_count(label: &str, width: usize, height: usize, count: usize) -> Result<()> {
1265 let expected = width.saturating_mul(height);
1266 if count != expected {
1267 return Err(Error::Message(format!(
1268 "{label} had {count} elements but expected {expected} for {width}x{height}"
1269 )));
1270 }
1271 Ok(())
1272}
1273
1274fn validate_normalization(
1275 label: &str,
1276 inputs: &OwnedHostTemporalInputs,
1277 normalization: &ExternalNormalization,
1278) -> Result<()> {
1279 if normalization.color.contains("[0,1]") || normalization.color.contains("linear RGB") {
1280 for (index, pixel) in inputs.current_color.pixels().iter().enumerate() {
1281 for channel in [pixel.r, pixel.g, pixel.b] {
1282 if !(-0.01..=1.01).contains(&channel) {
1283 return Err(Error::Message(format!(
1284 "{label} current_color pixel {index} violated normalized color expectations"
1285 )));
1286 }
1287 }
1288 }
1289 }
1290
1291 for (index, depth) in inputs.current_depth.iter().enumerate() {
1292 if !depth.is_finite() {
1293 return Err(Error::Message(format!(
1294 "{label} current_depth[{index}] was non-finite"
1295 )));
1296 }
1297 }
1298 for (index, depth) in inputs.reprojected_depth.iter().enumerate() {
1299 if !depth.is_finite() {
1300 return Err(Error::Message(format!(
1301 "{label} reprojected_depth[{index}] was non-finite"
1302 )));
1303 }
1304 }
1305 for (index, motion) in inputs.motion_vectors.iter().enumerate() {
1306 if !motion.to_prev_x.is_finite()
1307 || !motion.to_prev_y.is_finite()
1308 || motion.to_prev_x.abs() > inputs.width() as f32 * 4.0
1309 || motion.to_prev_y.abs() > inputs.height() as f32 * 4.0
1310 {
1311 return Err(Error::Message(format!(
1312 "{label} motion_vectors[{index}] violated finite/range validation"
1313 )));
1314 }
1315 }
1316 if normalization.normals.contains("unit") {
1317 for (index, normal) in inputs.current_normals.iter().enumerate() {
1318 let norm = (normal.x * normal.x + normal.y * normal.y + normal.z * normal.z).sqrt();
1319 if !norm.is_finite() || (norm - 1.0).abs() > 0.05 {
1320 return Err(Error::Message(format!(
1321 "{label} current_normals[{index}] violated unit-normal validation"
1322 )));
1323 }
1324 }
1325 }
1326 Ok(())
1327}
1328
1329fn load_exr_color(path: &Path) -> Result<ImageFrame> {
1330 let image = image::open(path)?.to_rgb32f();
1331 let width = image.width() as usize;
1332 let height = image.height() as usize;
1333 let pixels = image
1334 .pixels()
1335 .map(|pixel| Color::rgb(pixel[0], pixel[1], pixel[2]))
1336 .collect();
1337 Ok(ImageFrame::from_pixels(width, height, pixels))
1338}
1339
1340fn load_exr_scalar(path: &Path) -> Result<ScalarBufferFile> {
1341 let image = image::open(path)?.to_rgba32f();
1342 let width = image.width() as usize;
1343 let height = image.height() as usize;
1344 let data = image.pixels().map(|pixel| pixel[0]).collect();
1345 Ok(ScalarBufferFile {
1346 width,
1347 height,
1348 data,
1349 })
1350}
1351
1352fn load_exr_vec2(path: &Path) -> Result<Vec2BufferFile> {
1353 let image = image::open(path)?.to_rgba32f();
1354 let width = image.width() as usize;
1355 let height = image.height() as usize;
1356 let data = image.pixels().map(|pixel| [pixel[0], pixel[1]]).collect();
1357 Ok(Vec2BufferFile {
1358 width,
1359 height,
1360 data,
1361 })
1362}
1363
1364fn load_exr_vec3(path: &Path) -> Result<Vec3BufferFile> {
1365 let image = image::open(path)?.to_rgb32f();
1366 let width = image.width() as usize;
1367 let height = image.height() as usize;
1368 let data = image
1369 .pixels()
1370 .map(|pixel| [pixel[0], pixel[1], pixel[2]])
1371 .collect();
1372 Ok(Vec3BufferFile {
1373 width,
1374 height,
1375 data,
1376 })
1377}
1378
1379fn load_raw_color(
1380 path: &Path,
1381 reference: &BufferReference,
1382 expected_width: usize,
1383 expected_height: usize,
1384) -> Result<ImageFrame> {
1385 let (width, height) = extent_from_reference(reference, expected_width, expected_height)?;
1386 let channels = channel_count(reference, 3)?;
1387 if channels < 3 {
1388 return Err(Error::Message(format!(
1389 "raw_rgb32f buffer {} must provide at least 3 channels",
1390 reference.path
1391 )));
1392 }
1393 let values = read_raw_f32_values(path)?;
1394 validate_raw_value_count("raw_rgb32f", width, height, channels, values.len())?;
1395 let mut pixels = Vec::with_capacity(width * height);
1396 for chunk in values.chunks_exact(channels) {
1397 pixels.push(Color::rgb(chunk[0], chunk[1], chunk[2]));
1398 }
1399 Ok(ImageFrame::from_pixels(width, height, pixels))
1400}
1401
1402fn load_raw_scalar(
1403 path: &Path,
1404 reference: &BufferReference,
1405 expected_width: usize,
1406 expected_height: usize,
1407) -> Result<ScalarBufferFile> {
1408 let (width, height) = extent_from_reference(reference, expected_width, expected_height)?;
1409 let channels = channel_count(reference, 1)?;
1410 if channels != 1 {
1411 return Err(Error::Message(format!(
1412 "raw_r32f buffer {} must declare channels = 1",
1413 reference.path
1414 )));
1415 }
1416 let values = read_raw_f32_values(path)?;
1417 validate_raw_value_count("raw_r32f", width, height, channels, values.len())?;
1418 Ok(ScalarBufferFile {
1419 width,
1420 height,
1421 data: values,
1422 })
1423}
1424
1425fn load_raw_vec2(
1426 path: &Path,
1427 reference: &BufferReference,
1428 expected_width: usize,
1429 expected_height: usize,
1430) -> Result<Vec2BufferFile> {
1431 let (width, height) = extent_from_reference(reference, expected_width, expected_height)?;
1432 let channels = channel_count(reference, 2)?;
1433 if channels < 2 {
1434 return Err(Error::Message(format!(
1435 "raw_rg32f buffer {} must provide at least 2 channels",
1436 reference.path
1437 )));
1438 }
1439 let values = read_raw_f32_values(path)?;
1440 validate_raw_value_count("raw_rg32f", width, height, channels, values.len())?;
1441 let data = values
1442 .chunks_exact(channels)
1443 .map(|chunk| [chunk[0], chunk[1]])
1444 .collect();
1445 Ok(Vec2BufferFile {
1446 width,
1447 height,
1448 data,
1449 })
1450}
1451
1452fn load_raw_vec3(
1453 path: &Path,
1454 reference: &BufferReference,
1455 expected_width: usize,
1456 expected_height: usize,
1457) -> Result<Vec3BufferFile> {
1458 let (width, height) = extent_from_reference(reference, expected_width, expected_height)?;
1459 let channels = channel_count(reference, 3)?;
1460 if channels < 3 {
1461 return Err(Error::Message(format!(
1462 "raw_rgb32f buffer {} must provide at least 3 channels",
1463 reference.path
1464 )));
1465 }
1466 let values = read_raw_f32_values(path)?;
1467 validate_raw_value_count("raw_rgb32f", width, height, channels, values.len())?;
1468 let data = values
1469 .chunks_exact(channels)
1470 .map(|chunk| [chunk[0], chunk[1], chunk[2]])
1471 .collect();
1472 Ok(Vec3BufferFile {
1473 width,
1474 height,
1475 data,
1476 })
1477}
1478
1479fn load_raw_mask(
1480 path: &Path,
1481 reference: &BufferReference,
1482 expected_width: usize,
1483 expected_height: usize,
1484) -> Result<BoolBufferFile> {
1485 let (width, height) = extent_from_reference(reference, expected_width, expected_height)?;
1486 let channels = channel_count(reference, 1)?;
1487 if channels != 1 {
1488 return Err(Error::Message(format!(
1489 "raw_mask_u8 buffer {} must declare channels = 1",
1490 reference.path
1491 )));
1492 }
1493 let bytes = fs::read(path)?;
1494 let expected = width.saturating_mul(height);
1495 if bytes.len() != expected {
1496 return Err(Error::Message(format!(
1497 "raw_mask_u8 buffer {} had {} bytes but expected {} for {}x{}",
1498 reference.path,
1499 bytes.len(),
1500 expected,
1501 width,
1502 height
1503 )));
1504 }
1505 Ok(BoolBufferFile {
1506 width,
1507 height,
1508 data: bytes.into_iter().map(|value| value != 0).collect(),
1509 })
1510}
1511
1512fn extent_from_reference(
1513 reference: &BufferReference,
1514 expected_width: usize,
1515 expected_height: usize,
1516) -> Result<(usize, usize)> {
1517 let width = reference.width.unwrap_or(expected_width);
1518 let height = reference.height.unwrap_or(expected_height);
1519 if width == 0 || height == 0 {
1520 return Err(Error::Message(format!(
1521 "buffer {} must declare positive width/height either in metadata or inline",
1522 reference.path
1523 )));
1524 }
1525 Ok((width, height))
1526}
1527
1528fn channel_count(reference: &BufferReference, default_channels: usize) -> Result<usize> {
1529 let channels = reference.channels.unwrap_or(default_channels);
1530 if channels == 0 {
1531 return Err(Error::Message(format!(
1532 "buffer {} declared zero channels",
1533 reference.path
1534 )));
1535 }
1536 Ok(channels)
1537}
1538
1539fn validate_raw_value_count(
1540 label: &str,
1541 width: usize,
1542 height: usize,
1543 channels: usize,
1544 value_count: usize,
1545) -> Result<()> {
1546 let expected = width
1547 .checked_mul(height)
1548 .and_then(|pixels| pixels.checked_mul(channels))
1549 .ok_or_else(|| Error::Message(format!("{label} extent overflowed")))?;
1550 if value_count != expected {
1551 return Err(Error::Message(format!(
1552 "{label} had {value_count} float values but expected {expected} for {width}x{height}x{channels}"
1553 )));
1554 }
1555 Ok(())
1556}
1557
1558fn read_raw_f32_values(path: &Path) -> Result<Vec<f32>> {
1559 let mut file = fs::File::open(path)?;
1560 let mut bytes = Vec::new();
1561 file.read_to_end(&mut bytes)?;
1562 if bytes.len() % 4 != 0 {
1563 return Err(Error::Message(format!(
1564 "raw float buffer {} had {} bytes, which is not divisible by 4",
1565 path.display(),
1566 bytes.len()
1567 )));
1568 }
1569 Ok(bytes
1570 .chunks_exact(4)
1571 .map(|chunk| f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
1572 .collect())
1573}
1574
1575fn write_color_buffer(
1576 base_dir: &Path,
1577 reference: &BufferReference,
1578 frame: &ImageFrame,
1579) -> Result<()> {
1580 let path = base_dir.join(&reference.path);
1581 if let Some(parent) = path.parent() {
1582 fs::create_dir_all(parent)?;
1583 }
1584 match reference.format.as_str() {
1585 "png_rgb8" => frame.save_png(&path),
1586 "json_rgb_f32" => {
1587 let file = ColorBufferFile {
1588 width: frame.width(),
1589 height: frame.height(),
1590 data: frame
1591 .pixels()
1592 .iter()
1593 .map(|pixel| [pixel.r, pixel.g, pixel.b])
1594 .collect(),
1595 };
1596 fs::write(path, serde_json::to_string_pretty(&file)?)?;
1597 Ok(())
1598 }
1599 other => Err(Error::Message(format!(
1600 "unsupported color output format {other}"
1601 ))),
1602 }
1603}
1604
1605fn write_scalar_buffer(
1606 base_dir: &Path,
1607 reference: &BufferReference,
1608 values: &[f32],
1609 width: usize,
1610 height: usize,
1611) -> Result<()> {
1612 let path = base_dir.join(&reference.path);
1613 if let Some(parent) = path.parent() {
1614 fs::create_dir_all(parent)?;
1615 }
1616 let file = ScalarBufferFile {
1617 width,
1618 height,
1619 data: values.to_vec(),
1620 };
1621 fs::write(path, serde_json::to_string_pretty(&file)?)?;
1622 Ok(())
1623}
1624
1625fn write_vec2_buffer(
1626 base_dir: &Path,
1627 reference: &BufferReference,
1628 values: &[MotionVector],
1629 width: usize,
1630 height: usize,
1631) -> Result<()> {
1632 let path = base_dir.join(&reference.path);
1633 if let Some(parent) = path.parent() {
1634 fs::create_dir_all(parent)?;
1635 }
1636 let file = Vec2BufferFile {
1637 width,
1638 height,
1639 data: values
1640 .iter()
1641 .map(|value| [value.to_prev_x, value.to_prev_y])
1642 .collect(),
1643 };
1644 fs::write(path, serde_json::to_string_pretty(&file)?)?;
1645 Ok(())
1646}
1647
1648fn write_vec3_buffer(
1649 base_dir: &Path,
1650 reference: &BufferReference,
1651 values: &[Normal3],
1652 width: usize,
1653 height: usize,
1654) -> Result<()> {
1655 let path = base_dir.join(&reference.path);
1656 if let Some(parent) = path.parent() {
1657 fs::create_dir_all(parent)?;
1658 }
1659 let file = Vec3BufferFile {
1660 width,
1661 height,
1662 data: values
1663 .iter()
1664 .map(|value| [value.x, value.y, value.z])
1665 .collect(),
1666 };
1667 fs::write(path, serde_json::to_string_pretty(&file)?)?;
1668 Ok(())
1669}
1670
1671fn write_bool_buffer(
1672 base_dir: &Path,
1673 reference: &BufferReference,
1674 values: &[bool],
1675 width: usize,
1676 height: usize,
1677) -> Result<()> {
1678 let path = base_dir.join(&reference.path);
1679 if let Some(parent) = path.parent() {
1680 fs::create_dir_all(parent)?;
1681 }
1682 let file = BoolBufferFile {
1683 width,
1684 height,
1685 data: values.to_vec(),
1686 };
1687 fs::write(path, serde_json::to_string_pretty(&file)?)?;
1688 Ok(())
1689}
1690
1691pub fn parse_scenario_id(value: &str) -> Result<ScenarioId> {
1692 match value {
1693 "thin_reveal" => Ok(ScenarioId::ThinReveal),
1694 "fast_pan" => Ok(ScenarioId::FastPan),
1695 "diagonal_reveal" => Ok(ScenarioId::DiagonalReveal),
1696 "reveal_band" => Ok(ScenarioId::RevealBand),
1697 "motion_bias_band" => Ok(ScenarioId::MotionBiasBand),
1698 "layered_slats" => Ok(ScenarioId::LayeredSlats),
1699 "noisy_reprojection" => Ok(ScenarioId::NoisyReprojection),
1700 "heuristic_friendly_pan" => Ok(ScenarioId::HeuristicFriendlyPan),
1701 "contrast_pulse" => Ok(ScenarioId::ContrastPulse),
1702 "stability_holdout" => Ok(ScenarioId::StabilityHoldout),
1703 other => Err(Error::Message(format!("unknown scenario id {other}"))),
1704 }
1705}
1706
1707pub fn build_example_external_capture(
1708 config: &DemoConfig,
1709 output_dir: &Path,
1710) -> Result<ExternalImportArtifacts> {
1711 let manifest_path = output_dir.join("example_external_capture_manifest.json");
1712 write_example_manifest(&manifest_path)?;
1713 run_external_import_from_manifest(config, &manifest_path, output_dir)
1714}
1715
1716fn reproject_frame(previous_resolved: &ImageFrame, scene_frame: &SceneFrame) -> ImageFrame {
1717 let mut reprojected = ImageFrame::new(
1718 scene_frame.ground_truth.width(),
1719 scene_frame.ground_truth.height(),
1720 );
1721 for y in 0..scene_frame.ground_truth.height() {
1722 for x in 0..scene_frame.ground_truth.width() {
1723 let motion = scene_frame.motion[y * scene_frame.ground_truth.width() + x];
1724 reprojected.set(
1725 x,
1726 y,
1727 previous_resolved.sample_bilinear_clamped(
1728 x as f32 + motion.to_prev_x,
1729 y as f32 + motion.to_prev_y,
1730 ),
1731 );
1732 }
1733 }
1734 reprojected
1735}
1736
1737fn reproject_depth(previous_scene_frame: &SceneFrame, scene_frame: &SceneFrame) -> Vec<f32> {
1738 reproject_scalar_buffer(
1739 &previous_scene_frame.depth,
1740 scene_frame.ground_truth.width(),
1741 scene_frame.ground_truth.height(),
1742 &scene_frame.motion,
1743 )
1744}
1745
1746fn reproject_normals(previous_scene_frame: &SceneFrame, scene_frame: &SceneFrame) -> Vec<Normal3> {
1747 let width = scene_frame.ground_truth.width();
1748 let height = scene_frame.ground_truth.height();
1749 let mut reprojected = vec![Normal3::new(0.0, 0.0, 1.0); width * height];
1750 for y in 0..height {
1751 for x in 0..width {
1752 let index = y * width + x;
1753 let motion = scene_frame.motion[index];
1754 reprojected[index] = sample_normal_bilinear_clamped(
1755 &previous_scene_frame.normals,
1756 width,
1757 height,
1758 x as f32 + motion.to_prev_x,
1759 y as f32 + motion.to_prev_y,
1760 );
1761 }
1762 }
1763 reprojected
1764}
1765
1766fn reproject_scalar_buffer(
1767 previous_values: &[f32],
1768 width: usize,
1769 height: usize,
1770 motion: &[MotionVector],
1771) -> Vec<f32> {
1772 let mut reprojected = vec![0.0; width * height];
1773 for y in 0..height {
1774 for x in 0..width {
1775 let index = y * width + x;
1776 let vector = motion[index];
1777 reprojected[index] = sample_scalar_bilinear_clamped(
1778 previous_values,
1779 width,
1780 height,
1781 x as f32 + vector.to_prev_x,
1782 y as f32 + vector.to_prev_y,
1783 );
1784 }
1785 }
1786 reprojected
1787}
1788
1789fn sample_scalar_bilinear_clamped(
1790 values: &[f32],
1791 width: usize,
1792 height: usize,
1793 x: f32,
1794 y: f32,
1795) -> f32 {
1796 let x0 = x.floor();
1797 let y0 = y.floor();
1798 let x1 = x0 + 1.0;
1799 let y1 = y0 + 1.0;
1800 let tx = (x - x0).clamp(0.0, 1.0);
1801 let ty = (y - y0).clamp(0.0, 1.0);
1802 let sample = |sample_x: f32, sample_y: f32| {
1803 let sx = sample_x.clamp(0.0, width.saturating_sub(1) as f32) as usize;
1804 let sy = sample_y.clamp(0.0, height.saturating_sub(1) as f32) as usize;
1805 values[sy * width + sx]
1806 };
1807 let top = sample(x0, y0) * (1.0 - tx) + sample(x1, y0) * tx;
1808 let bottom = sample(x0, y1) * (1.0 - tx) + sample(x1, y1) * tx;
1809 top * (1.0 - ty) + bottom * ty
1810}
1811
1812fn sample_normal_bilinear_clamped(
1813 values: &[Normal3],
1814 width: usize,
1815 height: usize,
1816 x: f32,
1817 y: f32,
1818) -> Normal3 {
1819 let x0 = x.floor();
1820 let y0 = y.floor();
1821 let x1 = x0 + 1.0;
1822 let y1 = y0 + 1.0;
1823 let tx = (x - x0).clamp(0.0, 1.0);
1824 let ty = (y - y0).clamp(0.0, 1.0);
1825 let sample = |sample_x: f32, sample_y: f32| {
1826 let sx = sample_x.clamp(0.0, width.saturating_sub(1) as f32) as usize;
1827 let sy = sample_y.clamp(0.0, height.saturating_sub(1) as f32) as usize;
1828 values[sy * width + sx]
1829 };
1830 let mix = |a: Normal3, b: Normal3, t: f32| {
1831 Normal3::new(
1832 a.x + (b.x - a.x) * t,
1833 a.y + (b.y - a.y) * t,
1834 a.z + (b.z - a.z) * t,
1835 )
1836 };
1837 mix(
1838 mix(sample(x0, y0), sample(x1, y0), tx),
1839 mix(sample(x0, y1), sample(x1, y1), tx),
1840 ty,
1841 )
1842 .normalized()
1843}
1844
1845pub fn compute_external_compatible_mask(scene_frame: &SceneFrame) -> Vec<bool> {
1846 scene_frame
1847 .layers
1848 .iter()
1849 .zip(scene_frame.disocclusion_mask.iter().copied())
1850 .map(|(layer, disoccluded)| disoccluded && !matches!(*layer, SurfaceTag::ForegroundObject))
1851 .collect()
1852}