Skip to main content

dsfb_computer_graphics/
unreal_native.rs

1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt::Write as _;
3use std::fs;
4use std::path::{Path, PathBuf};
5use std::process::Command;
6use std::time::{SystemTime, UNIX_EPOCH};
7
8use serde::{Deserialize, Serialize};
9
10use crate::config::DemoConfig;
11use crate::error::{Error, Result};
12use crate::external::{
13    load_bool_buffer_from_reference, load_color_buffer_from_reference,
14    load_scalar_buffer_from_reference, load_vec2_buffer_from_reference,
15    load_vec3_buffer_from_reference, BufferReference, ExternalBufferSet, ExternalCaptureEntry,
16    ExternalCaptureManifest, ExternalCaptureMetadata, ExternalCaptureSource, ExternalNormalization,
17    EXTERNAL_CAPTURE_FORMAT_VERSION,
18};
19use crate::external_validation::{
20    capture_reference_frame_and_metric_source, roi_mask_for_capture, run_external_validation_bundle,
21    CANONICAL_HEADLINE_STATEMENT, ExternalDemoAMetrics, ExternalDemoBMetrics,
22    ExternalGpuMetrics, ExternalScalingMetrics, PURE_DSFB_LIMITATION_STATEMENT,
23    ROI_AGGREGATION_MIN_CAPTURES, ROI_CONTRACT_BASELINE_METHOD_ID, ROI_CONTRACT_SOURCE,
24    ROI_CONTRACT_STATEMENT, ROI_HONESTY_STATEMENT,
25};
26use crate::frame::{save_scalar_field_png, Color, ImageFrame, ScalarField};
27use crate::host::{
28    default_host_realistic_profile, supervise_temporal_reuse, HostSupervisionOutputs,
29};
30use crate::scene::{MotionVector, Normal3};
31
32pub const UNREAL_NATIVE_SCHEMA_VERSION: &str = "dsfb_unreal_native_v1";
33pub const UNREAL_NATIVE_DATASET_KIND: &str = "unreal_native";
34pub const UNREAL_NATIVE_PROVENANCE_LABEL: &str = "unreal_native";
35pub const UNREAL_NATIVE_PDF_FILE_NAME: &str = "artifacts_bundle.pdf";
36pub const UNREAL_NATIVE_ZIP_FILE_NAME: &str = "artifacts_bundle.zip";
37pub const UNREAL_NATIVE_EXECUTIVE_SHEET_FILE_NAME: &str = "executive_evidence_sheet.png";
38pub const UNREAL_NATIVE_EVIDENCE_MANIFEST_FILE_NAME: &str = "evidence_bundle_manifest.json";
39const UNREAL_NATIVE_CANONICAL_METRIC_SHEET_FILE_NAME: &str = "canonical_metric_sheet.md";
40const UNREAL_NATIVE_AGGREGATION_SUMMARY_FILE_NAME: &str = "aggregation_summary.md";
41
42#[derive(Clone, Debug)]
43pub struct UnrealNativeArtifacts {
44    pub run_dir: PathBuf,
45    pub materialized_manifest_path: PathBuf,
46    pub summary_path: PathBuf,
47    pub metrics_csv_path: PathBuf,
48    pub metrics_summary_path: PathBuf,
49    pub comparison_summary_path: PathBuf,
50    pub provenance_path: PathBuf,
51    pub failure_modes_path: PathBuf,
52    pub notebook_manifest_path: PathBuf,
53    pub executive_sheet_path: PathBuf,
54    pub pdf_path: PathBuf,
55    pub zip_path: PathBuf,
56}
57
58#[derive(Clone, Debug, Serialize, Deserialize)]
59pub struct UnrealNativeManifest {
60    pub schema_version: String,
61    pub dataset_kind: String,
62    pub provenance_label: String,
63    pub dataset_id: String,
64    pub description: String,
65    pub engine: UnrealEngineInfo,
66    pub contract: UnrealCaptureContract,
67    pub frames: Vec<UnrealFrameEntry>,
68    #[serde(default)]
69    pub notes: Vec<String>,
70}
71
72#[derive(Clone, Debug, Serialize, Deserialize)]
73pub struct UnrealEngineInfo {
74    pub engine_name: String,
75    pub engine_version: String,
76    pub capture_tool: String,
77    #[serde(default)]
78    pub project_path: Option<String>,
79    #[serde(default)]
80    pub capture_script: Option<String>,
81    #[serde(default)]
82    pub real_engine_capture: bool,
83}
84
85#[derive(Clone, Debug, Serialize, Deserialize)]
86pub struct UnrealCaptureContract {
87    pub color_space: String,
88    pub tonemap: String,
89    pub depth_convention: String,
90    pub normal_space: String,
91    pub motion_vector_convention: String,
92    pub coordinate_space: String,
93    #[serde(default)]
94    pub history_source: String,
95    #[serde(default)]
96    pub notes: Vec<String>,
97}
98
99#[derive(Clone, Debug, Serialize, Deserialize)]
100pub struct UnrealFrameEntry {
101    pub label: String,
102    pub frame_index: usize,
103    pub history_frame_index: usize,
104    pub buffers: UnrealFrameBuffers,
105    #[serde(default)]
106    pub notes: Vec<String>,
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize)]
110pub struct UnrealFrameBuffers {
111    pub current_color: BufferReference,
112    pub previous_color: BufferReference,
113    #[serde(default)]
114    pub history_color: Option<BufferReference>,
115    pub motion_vectors: BufferReference,
116    pub current_depth: BufferReference,
117    pub previous_depth: BufferReference,
118    #[serde(default)]
119    pub history_depth: Option<BufferReference>,
120    pub current_normals: BufferReference,
121    pub previous_normals: BufferReference,
122    #[serde(default)]
123    pub history_normals: Option<BufferReference>,
124    pub metadata: BufferReference,
125    #[serde(default)]
126    pub host_output: Option<BufferReference>,
127    #[serde(default)]
128    pub reference_color: Option<BufferReference>,
129    #[serde(default)]
130    pub roi_mask: Option<BufferReference>,
131    #[serde(default)]
132    pub disocclusion_mask: Option<BufferReference>,
133    #[serde(default)]
134    pub reactive_mask: Option<BufferReference>,
135}
136
137#[derive(Clone, Debug, Serialize, Deserialize)]
138pub struct UnrealFrameMetadata {
139    pub frame_index: usize,
140    pub history_frame_index: usize,
141    pub width: usize,
142    pub height: usize,
143    pub source_kind: String,
144    #[serde(default)]
145    pub externally_validated: bool,
146    #[serde(default)]
147    pub real_external_data: bool,
148    #[serde(default)]
149    pub data_description: Option<String>,
150    #[serde(default)]
151    pub provenance_label: Option<String>,
152    #[serde(default)]
153    pub scene_name: Option<String>,
154    #[serde(default)]
155    pub shot_name: Option<String>,
156    #[serde(default)]
157    pub exposure: Option<String>,
158    #[serde(default)]
159    pub tonemap: Option<String>,
160    #[serde(default)]
161    pub camera: Option<UnrealCameraMetadata>,
162    #[serde(default)]
163    pub notes: Vec<String>,
164}
165
166#[derive(Clone, Debug, Serialize, Deserialize)]
167pub struct UnrealCameraMetadata {
168    #[serde(default)]
169    pub name: Option<String>,
170    #[serde(default)]
171    pub position: Option<[f32; 3]>,
172    #[serde(default)]
173    pub forward: Option<[f32; 3]>,
174    #[serde(default)]
175    pub fov_degrees: Option<f32>,
176    #[serde(default)]
177    pub jitter_pixels: Option<[f32; 2]>,
178}
179
180#[derive(Clone, Debug, Serialize)]
181struct ProvenanceRecord {
182    schema_version: String,
183    dataset_kind: String,
184    provenance_label: String,
185    dataset_id: String,
186    manifest_path: String,
187    materialized_manifest_path: String,
188    run_name: String,
189    run_dir: String,
190    timestamp_epoch_seconds: u64,
191    git_commit: Option<String>,
192    cli_args: Vec<String>,
193}
194
195#[derive(Clone, Debug, Serialize)]
196struct SummaryRecord {
197    schema_version: String,
198    dataset_kind: String,
199    provenance_label: String,
200    dataset_id: String,
201    run_name: String,
202    run_dir: String,
203    capture_count: usize,
204    classification_counts: ClassificationCounts,
205    executive_capture_label: String,
206    pdf_file_name: String,
207    zip_file_name: String,
208    notes: Vec<String>,
209}
210
211#[derive(Clone, Debug, Serialize)]
212struct MetricsSummaryRecord {
213    dataset_id: String,
214    provenance_label: String,
215    capture_count: usize,
216    roi_statement: String,
217    classification_counts: ClassificationCounts,
218    captures: Vec<CaptureSummaryRecord>,
219}
220
221#[derive(Clone, Debug, Default, Serialize)]
222struct ClassificationCounts {
223    dsfb_helpful: usize,
224    dsfb_neutral: usize,
225    heuristic_favorable: usize,
226    richer_cues_required: usize,
227}
228
229#[derive(Clone, Debug, Serialize)]
230struct CaptureSummaryRecord {
231    capture_label: String,
232    scene_name: String,
233    shot_name: String,
234    frame_index: usize,
235    classification: String,
236    roi_source: String,
237    roi_coverage: f32,
238    reference_source: String,
239    metric_source: String,
240    dsfb_full_frame_mae: f32,
241    dsfb_roi_mae: f32,
242    dsfb_max_error: f32,
243    dsfb_plus_heuristic_full_frame_mae: f32,
244    dsfb_plus_heuristic_roi_mae: f32,
245    dsfb_plus_heuristic_max_error: f32,
246    strong_heuristic_full_frame_mae: f32,
247    strong_heuristic_roi_mae: f32,
248    strong_heuristic_max_error: f32,
249    fixed_alpha_full_frame_mae: f32,
250    fixed_alpha_roi_mae: f32,
251    fixed_alpha_max_error: f32,
252    dsfb_mean_trust: f32,
253    dsfb_mean_alpha: f32,
254    dsfb_intervention_rate: f32,
255    roi_residual_mean: f32,
256    instability_fraction: f32,
257    gpu_total_ms: Option<f64>,
258}
259
260#[derive(Clone, Debug, Serialize)]
261struct EvidenceBundleManifest {
262    dataset_id: String,
263    provenance_label: String,
264    run_name: String,
265    pdf_file_name: String,
266    zip_file_name: String,
267    executive_sheet_file_name: String,
268    summary_file_name: String,
269    metrics_summary_file_name: String,
270    comparison_summary_file_name: String,
271    failure_modes_file_name: String,
272    frames: Vec<EvidenceFrameManifest>,
273}
274
275#[derive(Clone, Debug, Serialize)]
276struct EvidenceFrameManifest {
277    label: String,
278    scene_name: String,
279    shot_name: String,
280    frame_index: usize,
281    classification: String,
282    explanation: EvidenceExplanation,
283    key_metrics: Vec<KeyMetric>,
284    current_frame_path: String,
285    baseline_frame_path: String,
286    trust_map_path: String,
287    alpha_map_path: String,
288    intervention_map_path: String,
289    residual_map_path: String,
290    instability_overlay_path: String,
291    roi_overlay_path: String,
292    output_panel_path: String,
293}
294
295#[derive(Clone, Debug, Serialize)]
296struct EvidenceExplanation {
297    what_went_wrong: String,
298    what_dsfb_detected: String,
299    what_dsfb_changed: String,
300    overhead_and_caveat: String,
301}
302
303#[derive(Clone, Debug, Serialize)]
304struct KeyMetric {
305    label: String,
306    value: String,
307}
308
309#[derive(Clone, Debug, Serialize)]
310struct NotebookManifest {
311    dataset_id: String,
312    provenance_label: String,
313    run_name: String,
314    run_dir_name: String,
315    executive_sheet_file_name: String,
316    pdf_bundle_file_name: String,
317    zip_bundle_file_name: String,
318    comparison_summary_file_name: String,
319    primary_panel_file_name: String,
320}
321
322#[derive(Clone, Debug)]
323struct MaterializedRun {
324    materialized_manifest_path: PathBuf,
325    captures: Vec<MaterializedCapture>,
326}
327
328#[derive(Clone, Debug)]
329struct MaterializedCapture {
330    label: String,
331    frame_index: usize,
332    scene_name: String,
333    shot_name: String,
334    host_output: Option<ImageFrame>,
335    roi_mask: Option<Vec<bool>>,
336    disocclusion_mask: Option<Vec<bool>>,
337}
338
339#[derive(Clone, Debug)]
340struct FrameArtifacts {
341    label: String,
342    scene_name: String,
343    shot_name: String,
344    frame_index: usize,
345    classification: String,
346    roi_source: String,
347    roi_pixels: usize,
348    roi_coverage: f32,
349    reference_source: String,
350    current_frame_path: PathBuf,
351    baseline_frame_path: PathBuf,
352    roi_mask_path: PathBuf,
353    trust_map_path: PathBuf,
354    alpha_map_path: PathBuf,
355    intervention_map_path: PathBuf,
356    residual_map_path: PathBuf,
357    instability_overlay_path: PathBuf,
358    roi_overlay_path: PathBuf,
359    output_panel_path: PathBuf,
360    metric_source: String,
361    dsfb_full_frame_mae: f32,
362    dsfb_roi_mae: f32,
363    dsfb_max_error: f32,
364    dsfb_plus_heuristic_full_frame_mae: f32,
365    dsfb_plus_heuristic_roi_mae: f32,
366    dsfb_plus_heuristic_max_error: f32,
367    strong_heuristic_full_frame_mae: f32,
368    strong_heuristic_roi_mae: f32,
369    strong_heuristic_max_error: f32,
370    fixed_alpha_full_frame_mae: f32,
371    fixed_alpha_roi_mae: f32,
372    fixed_alpha_max_error: f32,
373    dsfb_mean_trust: f32,
374    dsfb_mean_alpha: f32,
375    dsfb_intervention_rate: f32,
376    roi_residual_mean: f32,
377    instability_fraction: f32,
378    gpu_total_ms: Option<f64>,
379    explanation: EvidenceExplanation,
380}
381
382#[derive(Clone, Debug)]
383struct DemoAMethodSelection<'a> {
384    metric_source: &'a str,
385    roi_source: &'a str,
386    roi_coverage: f32,
387    reference_source: &'a str,
388    fixed_alpha: &'a crate::external_validation::ExternalDemoAMethodMetrics,
389    strong_heuristic: &'a crate::external_validation::ExternalDemoAMethodMetrics,
390    dsfb: &'a crate::external_validation::ExternalDemoAMethodMetrics,
391    dsfb_plus_heuristic: &'a crate::external_validation::ExternalDemoAMethodMetrics,
392}
393
394pub fn run_unreal_native(
395    config: &DemoConfig,
396    manifest_path: &Path,
397    output_root: &Path,
398    run_name_override: Option<&str>,
399    cli_args: &[String],
400) -> Result<UnrealNativeArtifacts> {
401    let manifest = load_and_validate_manifest(manifest_path)?;
402    let run_name = run_name_override
403        .map(|value| value.to_string())
404        .unwrap_or_else(default_run_name);
405    let run_dir = create_run_dir(output_root, &run_name)?;
406
407    let materialized = materialize_unreal_manifest(&manifest, manifest_path, &run_dir)?;
408    run_external_validation_bundle(config, &materialized.materialized_manifest_path, &run_dir)?;
409
410    let demo_a: ExternalDemoAMetrics =
411        read_json(&run_dir.join("demo_a_external_metrics.json"))?;
412    let demo_b: ExternalDemoBMetrics =
413        read_json(&run_dir.join("demo_b_external_metrics.json"))?;
414    let gpu: ExternalGpuMetrics = read_json(&run_dir.join("gpu_execution_metrics.json"))?;
415    let scaling: ExternalScalingMetrics = read_json(&run_dir.join("scaling_metrics.json"))?;
416
417    let per_frame_dir = run_dir.join("per_frame");
418    fs::create_dir_all(&per_frame_dir)?;
419    let frame_artifacts = generate_per_frame_artifacts(
420        config,
421        &materialized.materialized_manifest_path,
422        &materialized.captures,
423        &demo_a,
424        &gpu,
425        &per_frame_dir,
426    )?;
427
428    let comparison_summary_path = run_dir.join("comparison_summary.md");
429    let canonical_metric_sheet_path =
430        run_dir.join(UNREAL_NATIVE_CANONICAL_METRIC_SHEET_FILE_NAME);
431    let aggregation_summary_path = run_dir.join(UNREAL_NATIVE_AGGREGATION_SUMMARY_FILE_NAME);
432    let metrics_csv_path = run_dir.join("metrics.csv");
433    let metrics_summary_path = run_dir.join("metrics_summary.json");
434    let failure_modes_path = run_dir.join("failure_modes.md");
435    let provenance_path = run_dir.join("provenance.json");
436    let run_manifest_path = run_dir.join("run_manifest.json");
437    let summary_path = run_dir.join("summary.json");
438    let evidence_manifest_path = run_dir.join(UNREAL_NATIVE_EVIDENCE_MANIFEST_FILE_NAME);
439    let notebook_manifest_path = run_dir.join("notebook_manifest.json");
440
441    write_run_manifest(&run_manifest_path, manifest_path, &manifest, &materialized)?;
442
443    let counts = classification_counts(&frame_artifacts);
444    let capture_summaries = frame_artifacts
445        .iter()
446        .map(|frame| CaptureSummaryRecord {
447            capture_label: frame.label.clone(),
448            scene_name: frame.scene_name.clone(),
449            shot_name: frame.shot_name.clone(),
450            frame_index: frame.frame_index,
451            classification: frame.classification.clone(),
452            roi_source: frame.roi_source.clone(),
453            roi_coverage: frame.roi_coverage,
454            reference_source: frame.reference_source.clone(),
455            metric_source: frame.metric_source.clone(),
456            dsfb_full_frame_mae: frame.dsfb_full_frame_mae,
457            dsfb_roi_mae: frame.dsfb_roi_mae,
458            dsfb_max_error: frame.dsfb_max_error,
459            dsfb_plus_heuristic_full_frame_mae: frame.dsfb_plus_heuristic_full_frame_mae,
460            dsfb_plus_heuristic_roi_mae: frame.dsfb_plus_heuristic_roi_mae,
461            dsfb_plus_heuristic_max_error: frame.dsfb_plus_heuristic_max_error,
462            strong_heuristic_full_frame_mae: frame.strong_heuristic_full_frame_mae,
463            strong_heuristic_roi_mae: frame.strong_heuristic_roi_mae,
464            strong_heuristic_max_error: frame.strong_heuristic_max_error,
465            fixed_alpha_full_frame_mae: frame.fixed_alpha_full_frame_mae,
466            fixed_alpha_roi_mae: frame.fixed_alpha_roi_mae,
467            fixed_alpha_max_error: frame.fixed_alpha_max_error,
468            dsfb_mean_trust: frame.dsfb_mean_trust,
469            dsfb_mean_alpha: frame.dsfb_mean_alpha,
470            dsfb_intervention_rate: frame.dsfb_intervention_rate,
471            roi_residual_mean: frame.roi_residual_mean,
472            instability_fraction: frame.instability_fraction,
473            gpu_total_ms: frame.gpu_total_ms,
474        })
475        .collect::<Vec<_>>();
476
477    write_metrics_csv(&metrics_csv_path, &capture_summaries, &demo_b)?;
478    let metrics_summary = MetricsSummaryRecord {
479        dataset_id: manifest.dataset_id.clone(),
480        provenance_label: manifest.provenance_label.clone(),
481        capture_count: capture_summaries.len(),
482        roi_statement: ROI_CONTRACT_STATEMENT.to_string(),
483        classification_counts: counts.clone(),
484        captures: capture_summaries.clone(),
485    };
486    fs::write(
487        &metrics_summary_path,
488        serde_json::to_string_pretty(&metrics_summary)?,
489    )?;
490
491    write_canonical_metric_sheet(&canonical_metric_sheet_path, &capture_summaries)?;
492    write_aggregation_summary(&aggregation_summary_path, &capture_summaries)?;
493    write_comparison_summary(&comparison_summary_path, &manifest, &frame_artifacts, &demo_b)?;
494    write_failure_modes(&failure_modes_path, &manifest, &materialized.captures, &scaling)?;
495
496    let timestamp_epoch_seconds = SystemTime::now()
497        .duration_since(UNIX_EPOCH)
498        .unwrap_or_default()
499        .as_secs();
500    let provenance = ProvenanceRecord {
501        schema_version: manifest.schema_version.clone(),
502        dataset_kind: manifest.dataset_kind.clone(),
503        provenance_label: manifest.provenance_label.clone(),
504        dataset_id: manifest.dataset_id.clone(),
505        manifest_path: manifest_path.display().to_string(),
506        materialized_manifest_path: materialized.materialized_manifest_path.display().to_string(),
507        run_name: run_name.clone(),
508        run_dir: run_dir.display().to_string(),
509        timestamp_epoch_seconds,
510        git_commit: git_commit_hash(),
511        cli_args: cli_args.to_vec(),
512    };
513    fs::write(&provenance_path, serde_json::to_string_pretty(&provenance)?)?;
514
515    let executive_frame = select_executive_frame(&frame_artifacts)?;
516    let evidence_manifest = EvidenceBundleManifest {
517        dataset_id: manifest.dataset_id.clone(),
518        provenance_label: manifest.provenance_label.clone(),
519        run_name: run_name.clone(),
520        pdf_file_name: UNREAL_NATIVE_PDF_FILE_NAME.to_string(),
521        zip_file_name: UNREAL_NATIVE_ZIP_FILE_NAME.to_string(),
522        executive_sheet_file_name: UNREAL_NATIVE_EXECUTIVE_SHEET_FILE_NAME.to_string(),
523        summary_file_name: "summary.json".to_string(),
524        metrics_summary_file_name: "metrics_summary.json".to_string(),
525        comparison_summary_file_name: "comparison_summary.md".to_string(),
526        failure_modes_file_name: "failure_modes.md".to_string(),
527        frames: frame_artifacts
528            .iter()
529            .map(|frame| EvidenceFrameManifest {
530                label: frame.label.clone(),
531                scene_name: frame.scene_name.clone(),
532                shot_name: frame.shot_name.clone(),
533                frame_index: frame.frame_index,
534                classification: frame.classification.clone(),
535                explanation: frame.explanation.clone(),
536                key_metrics: vec![
537                    KeyMetric {
538                        label: "DSFB ROI MAE".to_string(),
539                        value: format!("{:.5}", frame.dsfb_roi_mae),
540                    },
541                    KeyMetric {
542                        label: "Strong heuristic ROI MAE".to_string(),
543                        value: format!("{:.5}", frame.strong_heuristic_roi_mae),
544                    },
545                    KeyMetric {
546                        label: "DSFB + heuristic ROI MAE".to_string(),
547                        value: format!("{:.5}", frame.dsfb_plus_heuristic_roi_mae),
548                    },
549                    KeyMetric {
550                        label: "Mean trust".to_string(),
551                        value: format!("{:.4}", frame.dsfb_mean_trust),
552                    },
553                    KeyMetric {
554                        label: "Intervention rate".to_string(),
555                        value: format!("{:.4}", frame.dsfb_intervention_rate),
556                    },
557                    KeyMetric {
558                        label: "GPU total ms".to_string(),
559                        value: frame
560                            .gpu_total_ms
561                            .map(|value| format!("{value:.4}"))
562                            .unwrap_or_else(|| "n/a".to_string()),
563                    },
564                ],
565                current_frame_path: relative_path_string(&frame.current_frame_path, &run_dir),
566                baseline_frame_path: relative_path_string(&frame.baseline_frame_path, &run_dir),
567                trust_map_path: relative_path_string(&frame.trust_map_path, &run_dir),
568                alpha_map_path: relative_path_string(&frame.alpha_map_path, &run_dir),
569                intervention_map_path: relative_path_string(&frame.intervention_map_path, &run_dir),
570                residual_map_path: relative_path_string(&frame.residual_map_path, &run_dir),
571                instability_overlay_path: relative_path_string(
572                    &frame.instability_overlay_path,
573                    &run_dir,
574                ),
575                roi_overlay_path: relative_path_string(&frame.roi_overlay_path, &run_dir),
576                output_panel_path: relative_path_string(&frame.output_panel_path, &run_dir),
577            })
578            .collect(),
579    };
580    fs::write(
581        &evidence_manifest_path,
582        serde_json::to_string_pretty(&evidence_manifest)?,
583    )?;
584
585    let summary = SummaryRecord {
586        schema_version: manifest.schema_version.clone(),
587        dataset_kind: manifest.dataset_kind.clone(),
588        provenance_label: manifest.provenance_label.clone(),
589        dataset_id: manifest.dataset_id.clone(),
590        run_name: run_name.clone(),
591        run_dir: run_dir.display().to_string(),
592        capture_count: frame_artifacts.len(),
593        classification_counts: counts,
594        executive_capture_label: executive_frame.label.clone(),
595        pdf_file_name: UNREAL_NATIVE_PDF_FILE_NAME.to_string(),
596        zip_file_name: UNREAL_NATIVE_ZIP_FILE_NAME.to_string(),
597        notes: vec![
598            "Engine-native empirical replay executed on a strict Unreal-native manifest.".to_string(),
599            "No synthetic fallback is available in this mode.".to_string(),
600            "Any missing required Unreal-native buffer is a hard failure.".to_string(),
601            ROI_CONTRACT_STATEMENT.to_string(),
602            format!(
603                "This checked-in manifest provides {} real Unreal-native captures; aggregation {}.",
604                capture_summaries.len(),
605                if capture_summaries.len() >= ROI_AGGREGATION_MIN_CAPTURES {
606                    "is emitted in aggregation_summary.md"
607                } else {
608                    "remains blocked because fewer than the required real captures are available"
609                }
610            ),
611        ],
612    };
613    fs::write(&summary_path, serde_json::to_string_pretty(&summary)?)?;
614
615    run_bundle_builder(&run_dir)?;
616
617    let notebook_manifest = NotebookManifest {
618        dataset_id: manifest.dataset_id.clone(),
619        provenance_label: manifest.provenance_label.clone(),
620        run_name: run_name.clone(),
621        run_dir_name: run_name.clone(),
622        executive_sheet_file_name: UNREAL_NATIVE_EXECUTIVE_SHEET_FILE_NAME.to_string(),
623        pdf_bundle_file_name: UNREAL_NATIVE_PDF_FILE_NAME.to_string(),
624        zip_bundle_file_name: UNREAL_NATIVE_ZIP_FILE_NAME.to_string(),
625        comparison_summary_file_name: "comparison_summary.md".to_string(),
626        primary_panel_file_name: executive_frame
627            .output_panel_path
628            .file_name()
629            .and_then(|value| value.to_str())
630            .unwrap_or("boardroom_panel.png")
631            .to_string(),
632    };
633    fs::write(
634        &notebook_manifest_path,
635        serde_json::to_string_pretty(&notebook_manifest)?,
636    )?;
637
638    let executive_sheet_path = run_dir.join(UNREAL_NATIVE_EXECUTIVE_SHEET_FILE_NAME);
639    let pdf_path = run_dir.join(UNREAL_NATIVE_PDF_FILE_NAME);
640    let zip_path = run_dir.join(UNREAL_NATIVE_ZIP_FILE_NAME);
641    validate_unreal_native_artifacts(
642        &run_dir,
643        &frame_artifacts,
644        &comparison_summary_path,
645        &canonical_metric_sheet_path,
646        &aggregation_summary_path,
647    )?;
648
649    Ok(UnrealNativeArtifacts {
650        run_dir,
651        materialized_manifest_path: materialized.materialized_manifest_path,
652        summary_path,
653        metrics_csv_path,
654        metrics_summary_path,
655        comparison_summary_path,
656        provenance_path,
657        failure_modes_path,
658        notebook_manifest_path,
659        executive_sheet_path,
660        pdf_path,
661        zip_path,
662    })
663}
664
665fn load_and_validate_manifest(path: &Path) -> Result<UnrealNativeManifest> {
666    let manifest: UnrealNativeManifest = read_json(path)?;
667    if manifest.schema_version != UNREAL_NATIVE_SCHEMA_VERSION {
668        return Err(Error::Message(format!(
669            "unreal-native manifest {} used schema_version `{}` but `{}` is required",
670            path.display(),
671            manifest.schema_version,
672            UNREAL_NATIVE_SCHEMA_VERSION
673        )));
674    }
675    if manifest.dataset_kind != UNREAL_NATIVE_DATASET_KIND {
676        return Err(Error::Message(format!(
677            "unreal-native manifest {} used dataset_kind `{}` but `{}` is required",
678            path.display(),
679            manifest.dataset_kind,
680            UNREAL_NATIVE_DATASET_KIND
681        )));
682    }
683    if manifest.provenance_label != UNREAL_NATIVE_PROVENANCE_LABEL {
684        return Err(Error::Message(format!(
685            "unreal-native manifest {} used provenance_label `{}` but `{}` is required",
686            path.display(),
687            manifest.provenance_label,
688            UNREAL_NATIVE_PROVENANCE_LABEL
689        )));
690    }
691    if manifest.engine.engine_name != "unreal_engine" {
692        return Err(Error::Message(format!(
693            "unreal-native manifest {} declared engine_name `{}`; only `unreal_engine` is accepted",
694            path.display(),
695            manifest.engine.engine_name
696        )));
697    }
698    if !manifest.engine.real_engine_capture {
699        return Err(Error::Message(format!(
700            "unreal-native manifest {} is not marked as a real Unreal capture; this mode refuses pending, proxy, or synthetic provenance",
701            path.display()
702        )));
703    }
704    if manifest.frames.is_empty() {
705        return Err(Error::Message(format!(
706            "unreal-native manifest {} contained no frames",
707            path.display()
708        )));
709    }
710    validate_contract(&manifest.contract)?;
711    validate_unique_frames(&manifest.frames)?;
712    Ok(manifest)
713}
714
715fn validate_contract(contract: &UnrealCaptureContract) -> Result<()> {
716    if !contract.color_space.contains("linear") {
717        return Err(Error::Message(
718            "unreal-native contract must declare a linear color space".to_string(),
719        ));
720    }
721    if !matches!(
722        contract.tonemap.as_str(),
723        "disabled" | "pre_tonemap_capture" | "scene_capture_png_linearized"
724    ) {
725        return Err(Error::Message(format!(
726            "unreal-native contract tonemap `{}` is unsupported; use `disabled`, `pre_tonemap_capture`, or `scene_capture_png_linearized`",
727            contract.tonemap
728        )));
729    }
730    if !matches!(contract.normal_space.as_str(), "view_space_unit" | "world_space_unit") {
731        return Err(Error::Message(format!(
732            "unreal-native contract normal_space `{}` is unsupported; use `view_space_unit` or `world_space_unit`",
733            contract.normal_space
734        )));
735    }
736    if !matches!(
737        contract.depth_convention.as_str(),
738        "monotonic_linear_depth" | "monotonic_visualized_depth"
739    ) {
740        return Err(Error::Message(format!(
741            "unreal-native contract depth_convention `{}` is unsupported; use `monotonic_linear_depth` or `monotonic_visualized_depth`",
742            contract.depth_convention
743        )));
744    }
745    if !matches!(
746        contract.motion_vector_convention.as_str(),
747        "pixel_offset_to_prev" | "ndc_to_prev"
748    ) {
749        return Err(Error::Message(format!(
750            "unreal-native contract motion_vector_convention `{}` is unsupported",
751            contract.motion_vector_convention
752        )));
753    }
754    Ok(())
755}
756
757fn validate_unique_frames(frames: &[UnrealFrameEntry]) -> Result<()> {
758    let mut labels = BTreeSet::new();
759    let mut indices = BTreeSet::new();
760    for frame in frames {
761        if !labels.insert(frame.label.clone()) {
762            return Err(Error::Message(format!(
763                "duplicate unreal-native capture label `{}`",
764                frame.label
765            )));
766        }
767        if !indices.insert(frame.frame_index) {
768            return Err(Error::Message(format!(
769                "duplicate unreal-native frame_index `{}`",
770                frame.frame_index
771            )));
772        }
773        if frame.history_frame_index >= frame.frame_index {
774            return Err(Error::Message(format!(
775                "capture `{}` must provide history_frame_index < frame_index",
776                frame.label
777            )));
778        }
779    }
780    Ok(())
781}
782
783fn create_run_dir(output_root: &Path, run_name: &str) -> Result<PathBuf> {
784    fs::create_dir_all(output_root)?;
785    let run_dir = output_root.join(run_name);
786    if run_dir.exists() {
787        return Err(Error::Message(format!(
788            "refusing to overwrite existing unreal-native run directory {}",
789            run_dir.display()
790        )));
791    }
792    fs::create_dir_all(&run_dir)?;
793    Ok(run_dir)
794}
795
796fn default_run_name() -> String {
797    let seconds = SystemTime::now()
798        .duration_since(UNIX_EPOCH)
799        .unwrap_or_default()
800        .as_secs();
801    format!("unreal_native_{seconds}")
802}
803
804fn materialize_unreal_manifest(
805    manifest: &UnrealNativeManifest,
806    manifest_path: &Path,
807    run_dir: &Path,
808) -> Result<MaterializedRun> {
809    let base_dir = manifest_path.parent().ok_or_else(|| {
810        Error::Message(format!(
811            "unreal-native manifest {} had no parent directory",
812            manifest_path.display()
813        ))
814    })?;
815    let materialized_dir = run_dir.join("materialized_external");
816    fs::create_dir_all(&materialized_dir)?;
817
818    let mut sorted_frames = manifest.frames.clone();
819    sorted_frames.sort_by_key(|frame| frame.frame_index);
820
821    let mut external_entries = Vec::with_capacity(sorted_frames.len());
822    let mut captures = Vec::with_capacity(sorted_frames.len());
823
824    for frame in &sorted_frames {
825        let metadata = load_unreal_frame_metadata(base_dir, &frame.buffers.metadata)?;
826        validate_frame_metadata(frame, &metadata)?;
827
828        let current_color = load_color_buffer_from_reference(
829            base_dir,
830            &frame.buffers.current_color,
831            metadata.width,
832            metadata.height,
833        )?;
834        let previous_color = load_color_buffer_from_reference(
835            base_dir,
836            &frame.buffers.previous_color,
837            metadata.width,
838            metadata.height,
839        )?;
840        let (_, _, mut motion_data) = load_vec2_buffer_from_reference(
841            base_dir,
842            &frame.buffers.motion_vectors,
843            metadata.width,
844            metadata.height,
845        )?;
846        normalize_motion_vectors(
847            &mut motion_data,
848            metadata.width,
849            metadata.height,
850            &manifest.contract.motion_vector_convention,
851        )?;
852        let motion_vectors = motion_data
853            .iter()
854            .map(|value| MotionVector {
855                to_prev_x: value[0],
856                to_prev_y: value[1],
857            })
858            .collect::<Vec<_>>();
859
860        let (_, _, current_depth) = load_scalar_buffer_from_reference(
861            base_dir,
862            &frame.buffers.current_depth,
863            metadata.width,
864            metadata.height,
865        )?;
866        let (_, _, previous_depth) = load_scalar_buffer_from_reference(
867            base_dir,
868            &frame.buffers.previous_depth,
869            metadata.width,
870            metadata.height,
871        )?;
872        let (_, _, current_normal_data) = load_vec3_buffer_from_reference(
873            base_dir,
874            &frame.buffers.current_normals,
875            metadata.width,
876            metadata.height,
877        )?;
878        let (_, _, previous_normal_data) = load_vec3_buffer_from_reference(
879            base_dir,
880            &frame.buffers.previous_normals,
881            metadata.width,
882            metadata.height,
883        )?;
884        let current_normals = current_normal_data
885            .into_iter()
886            .map(|value| Normal3::new(value[0], value[1], value[2]).normalized())
887            .collect::<Vec<_>>();
888        let previous_normals = previous_normal_data
889            .into_iter()
890            .map(|value| Normal3::new(value[0], value[1], value[2]).normalized())
891            .collect::<Vec<_>>();
892
893        let history_color = match &frame.buffers.history_color {
894            Some(reference) => load_color_buffer_from_reference(
895                base_dir,
896                reference,
897                metadata.width,
898                metadata.height,
899            )?,
900            None => reproject_image(&previous_color, &motion_vectors),
901        };
902        let history_depth = match &frame.buffers.history_depth {
903            Some(reference) => load_scalar_buffer_from_reference(
904                base_dir,
905                reference,
906                metadata.width,
907                metadata.height,
908            )?
909            .2,
910            None => reproject_scalar(&previous_depth, metadata.width, metadata.height, &motion_vectors),
911        };
912        let history_normals = match &frame.buffers.history_normals {
913            Some(reference) => load_vec3_buffer_from_reference(
914                base_dir,
915                reference,
916                metadata.width,
917                metadata.height,
918            )?
919            .2
920            .into_iter()
921            .map(|value| Normal3::new(value[0], value[1], value[2]).normalized())
922            .collect(),
923            None => reproject_normals(
924                &previous_normals,
925                metadata.width,
926                metadata.height,
927                &motion_vectors,
928            ),
929        };
930
931        let host_output = frame
932            .buffers
933            .host_output
934            .as_ref()
935            .map(|reference| {
936                load_color_buffer_from_reference(base_dir, reference, metadata.width, metadata.height)
937            })
938            .transpose()?;
939        let reference_color = frame
940            .buffers
941            .reference_color
942            .as_ref()
943            .map(|reference| {
944                load_color_buffer_from_reference(base_dir, reference, metadata.width, metadata.height)
945            })
946            .transpose()?;
947        let roi_mask = frame
948            .buffers
949            .roi_mask
950            .as_ref()
951            .map(|reference| load_mask_any(base_dir, reference, metadata.width, metadata.height))
952            .transpose()?;
953        let disocclusion_mask = frame
954            .buffers
955            .disocclusion_mask
956            .as_ref()
957            .map(|reference| load_mask_any(base_dir, reference, metadata.width, metadata.height))
958            .transpose()?;
959
960        let capture_dir = materialized_dir.join(&frame.label);
961        fs::create_dir_all(&capture_dir)?;
962
963        let current_color_path = capture_dir.join("current_color.png");
964        let history_color_path = capture_dir.join("reprojected_history.png");
965        let motion_vectors_path = capture_dir.join("motion_vectors.json");
966        let current_depth_path = capture_dir.join("current_depth.json");
967        let history_depth_path = capture_dir.join("reprojected_depth.json");
968        let current_normals_path = capture_dir.join("current_normals.json");
969        let history_normals_path = capture_dir.join("reprojected_normals.json");
970        let metadata_path = capture_dir.join("metadata.json");
971        let roi_mask_path = capture_dir.join("roi_mask.json");
972        let reference_path = capture_dir.join("reference_color.png");
973
974        current_color.save_png(&current_color_path)?;
975        history_color.save_png(&history_color_path)?;
976        write_json(&motion_vectors_path, &Vec2Json {
977            width: metadata.width,
978            height: metadata.height,
979            data: motion_vectors
980                .iter()
981                .map(|motion| [motion.to_prev_x, motion.to_prev_y])
982                .collect(),
983        })?;
984        write_json(&current_depth_path, &ScalarJson {
985            width: metadata.width,
986            height: metadata.height,
987            data: current_depth.clone(),
988        })?;
989        write_json(&history_depth_path, &ScalarJson {
990            width: metadata.width,
991            height: metadata.height,
992            data: history_depth.clone(),
993        })?;
994        write_json(&current_normals_path, &Vec3Json {
995            width: metadata.width,
996            height: metadata.height,
997            data: current_normals.iter().map(|normal| [normal.x, normal.y, normal.z]).collect(),
998        })?;
999        write_json(&history_normals_path, &Vec3Json {
1000            width: metadata.width,
1001            height: metadata.height,
1002            data: history_normals.iter().map(|normal| [normal.x, normal.y, normal.z]).collect(),
1003        })?;
1004
1005        let external_metadata = ExternalCaptureMetadata {
1006            scenario_id: metadata.scene_name.clone(),
1007            frame_index: metadata.frame_index,
1008            history_frame_index: metadata.history_frame_index,
1009            width: metadata.width,
1010            height: metadata.height,
1011            source_kind: UNREAL_NATIVE_DATASET_KIND.to_string(),
1012            externally_validated: true,
1013            real_external_data: true,
1014            data_description: Some(
1015                "Unreal Engine exported frame pair materialized into the DSFB external replay contract"
1016                    .to_string(),
1017            ),
1018            notes: metadata.notes.clone(),
1019        };
1020        write_json(&metadata_path, &external_metadata)?;
1021
1022        let optional_mask = if let Some(mask) = &roi_mask {
1023            write_json(&roi_mask_path, &BoolJson {
1024                width: metadata.width,
1025                height: metadata.height,
1026                data: mask.clone(),
1027            })?;
1028            Some(BufferReference {
1029                path: relative_path_string(&roi_mask_path, run_dir),
1030                format: "json_mask_bool".to_string(),
1031                semantic: "roi_mask".to_string(),
1032                width: Some(metadata.width),
1033                height: Some(metadata.height),
1034                channels: Some(1),
1035            })
1036        } else {
1037            None
1038        };
1039
1040        let optional_reference = if let Some(reference) = &reference_color {
1041            reference.save_png(&reference_path)?;
1042            Some(BufferReference {
1043                path: relative_path_string(&reference_path, run_dir),
1044                format: "png_rgb8".to_string(),
1045                semantic: "reference_color".to_string(),
1046                width: Some(metadata.width),
1047                height: Some(metadata.height),
1048                channels: Some(3),
1049            })
1050        } else {
1051            None
1052        };
1053
1054        let buffers = ExternalBufferSet {
1055            current_color: BufferReference {
1056                path: relative_path_string(&current_color_path, run_dir),
1057                format: "png_rgb8".to_string(),
1058                semantic: "current_color".to_string(),
1059                width: Some(metadata.width),
1060                height: Some(metadata.height),
1061                channels: Some(3),
1062            },
1063            reprojected_history: BufferReference {
1064                path: relative_path_string(&history_color_path, run_dir),
1065                format: "png_rgb8".to_string(),
1066                semantic: "reprojected_history".to_string(),
1067                width: Some(metadata.width),
1068                height: Some(metadata.height),
1069                channels: Some(3),
1070            },
1071            motion_vectors: BufferReference {
1072                path: relative_path_string(&motion_vectors_path, run_dir),
1073                format: "json_vec2_f32".to_string(),
1074                semantic: "motion_vectors".to_string(),
1075                width: Some(metadata.width),
1076                height: Some(metadata.height),
1077                channels: Some(2),
1078            },
1079            current_depth: BufferReference {
1080                path: relative_path_string(&current_depth_path, run_dir),
1081                format: "json_scalar_f32".to_string(),
1082                semantic: "current_depth".to_string(),
1083                width: Some(metadata.width),
1084                height: Some(metadata.height),
1085                channels: Some(1),
1086            },
1087            reprojected_depth: BufferReference {
1088                path: relative_path_string(&history_depth_path, run_dir),
1089                format: "json_scalar_f32".to_string(),
1090                semantic: "reprojected_depth".to_string(),
1091                width: Some(metadata.width),
1092                height: Some(metadata.height),
1093                channels: Some(1),
1094            },
1095            current_normals: BufferReference {
1096                path: relative_path_string(&current_normals_path, run_dir),
1097                format: "json_vec3_f32".to_string(),
1098                semantic: "current_normals".to_string(),
1099                width: Some(metadata.width),
1100                height: Some(metadata.height),
1101                channels: Some(3),
1102            },
1103            reprojected_normals: BufferReference {
1104                path: relative_path_string(&history_normals_path, run_dir),
1105                format: "json_vec3_f32".to_string(),
1106                semantic: "reprojected_normals".to_string(),
1107                width: Some(metadata.width),
1108                height: Some(metadata.height),
1109                channels: Some(3),
1110            },
1111            metadata: BufferReference {
1112                path: relative_path_string(&metadata_path, run_dir),
1113                format: "json_metadata".to_string(),
1114                semantic: "metadata".to_string(),
1115                width: None,
1116                height: None,
1117                channels: None,
1118            },
1119            optional_mask,
1120            optional_reference,
1121            optional_ground_truth: None,
1122            optional_variance: None,
1123        };
1124
1125        external_entries.push(ExternalCaptureEntry {
1126            label: frame.label.clone(),
1127            buffers,
1128        });
1129
1130        captures.push(MaterializedCapture {
1131            label: frame.label.clone(),
1132            frame_index: frame.frame_index,
1133            scene_name: metadata
1134                .scene_name
1135                .clone()
1136                .unwrap_or_else(|| manifest.dataset_id.clone()),
1137            shot_name: metadata
1138                .shot_name
1139                .clone()
1140                .unwrap_or_else(|| "shot_000".to_string()),
1141            host_output,
1142            roi_mask,
1143            disocclusion_mask,
1144        });
1145    }
1146
1147    let external_manifest = ExternalCaptureManifest {
1148        format_version: EXTERNAL_CAPTURE_FORMAT_VERSION.to_string(),
1149        description: format!(
1150            "Materialized external replay manifest generated from the strict Unreal-native dataset `{}`",
1151            manifest.dataset_id
1152        ),
1153        source: ExternalCaptureSource::EngineNative {
1154            engine_type: "unreal".to_string(),
1155            engine_version: Some(manifest.engine.engine_version.clone()),
1156            capture_tool: Some(manifest.engine.capture_tool.clone()),
1157            capture_note: Some(
1158                "real Unreal capture materialized into reprojected replay inputs with no synthetic fallback"
1159                    .to_string(),
1160            ),
1161        },
1162        buffers: None,
1163        captures: external_entries,
1164        normalization: ExternalNormalization {
1165            color: manifest.contract.color_space.clone(),
1166            motion_vectors: format!(
1167                "{}; normalized into pixel offsets to the previous frame",
1168                manifest.contract.motion_vector_convention
1169            ),
1170            depth: manifest.contract.depth_convention.clone(),
1171            normals: manifest.contract.normal_space.clone(),
1172        },
1173        notes: vec![
1174            "provenance_label=unreal_native".to_string(),
1175            "real_engine_capture=true".to_string(),
1176            "no synthetic fallback is implemented in this path".to_string(),
1177        ],
1178    };
1179    let materialized_manifest_path = run_dir.join("materialized_unreal_external_manifest.json");
1180    fs::write(
1181        &materialized_manifest_path,
1182        serde_json::to_string_pretty(&external_manifest)?,
1183    )?;
1184
1185    Ok(MaterializedRun {
1186        materialized_manifest_path,
1187        captures,
1188    })
1189}
1190
1191fn generate_per_frame_artifacts(
1192    config: &DemoConfig,
1193    materialized_manifest_path: &Path,
1194    materialized_captures: &[MaterializedCapture],
1195    demo_a: &ExternalDemoAMetrics,
1196    gpu: &ExternalGpuMetrics,
1197    per_frame_dir: &Path,
1198) -> Result<Vec<FrameArtifacts>> {
1199    let bundle = crate::external::load_external_capture_bundle(
1200        config,
1201        materialized_manifest_path,
1202        per_frame_dir,
1203    )?;
1204    let profile =
1205        default_host_realistic_profile(config.dsfb_alpha_range.min, config.dsfb_alpha_range.max);
1206    let gpu_by_label = gpu
1207        .captures
1208        .iter()
1209        .map(|capture| (capture.capture_label.clone(), capture.total_ms))
1210        .collect::<BTreeMap<_, _>>();
1211
1212    let mut frames = Vec::with_capacity(bundle.captures.len());
1213    for capture in &bundle.captures {
1214        let materialized = materialized_captures
1215            .iter()
1216            .find(|candidate| candidate.label == capture.label)
1217            .ok_or_else(|| {
1218                Error::Message(format!(
1219                    "materialized capture `{}` was missing during per-frame artifact generation",
1220                    capture.label
1221                ))
1222            })?;
1223        let methods = find_demo_a_methods(demo_a, &capture.label)?;
1224        let outputs = supervise_temporal_reuse(&capture.inputs.borrow(), &profile);
1225        let _dsfb_resolved = resolve_with_alpha(
1226            &capture.inputs.reprojected_history,
1227            &capture.inputs.current_color,
1228            &outputs.alpha,
1229        );
1230        let (_strong_resolved, _, _strong_response) = run_strong_heuristic(config, capture);
1231        let fixed_alpha_field = constant_field(
1232            capture.inputs.width(),
1233            capture.inputs.height(),
1234            config.baseline.fixed_alpha,
1235        );
1236        let fixed_resolved = resolve_with_alpha(
1237            &capture.inputs.reprojected_history,
1238            &capture.inputs.current_color,
1239            &fixed_alpha_field,
1240        );
1241        let (reference_frame, reference_source, _) =
1242            capture_reference_frame_and_metric_source(capture);
1243        let (roi_mask, roi_source, roi_coverage) =
1244            roi_mask_for_capture(capture, &fixed_resolved, reference_frame);
1245        let roi_pixels = roi_mask.iter().filter(|value| **value).count();
1246        let instability_mask = materialized
1247            .disocclusion_mask
1248            .clone()
1249            .unwrap_or_else(|| {
1250                derive_instability_mask(
1251                    &outputs,
1252                    &capture.inputs.current_color,
1253                    &capture.inputs.reprojected_history,
1254                )
1255            });
1256        let baseline = fixed_resolved.clone();
1257
1258        let capture_dir = per_frame_dir.join(&capture.label);
1259        fs::create_dir_all(&capture_dir)?;
1260        let current_frame_path = capture_dir.join("current_frame.png");
1261        let baseline_frame_path = capture_dir.join("baseline_or_host_output.png");
1262        let roi_mask_path = capture_dir.join("roi_mask.json");
1263        let trust_map_path = capture_dir.join("trust_map.png");
1264        let alpha_map_path = capture_dir.join("alpha_map.png");
1265        let intervention_map_path = capture_dir.join("intervention_map.png");
1266        let residual_map_path = capture_dir.join("residual_map.png");
1267        let instability_overlay_path = capture_dir.join("instability_overlay.png");
1268        let roi_overlay_path = capture_dir.join("roi_overlay.png");
1269        let output_panel_path = capture_dir.join(format!("boardroom_panel_{}.png", capture.label));
1270
1271        capture.inputs.current_color.save_png(&current_frame_path)?;
1272        baseline.save_png(&baseline_frame_path)?;
1273        write_json(
1274            &roi_mask_path,
1275            &BoolJson {
1276                width: capture.inputs.width(),
1277                height: capture.inputs.height(),
1278                data: roi_mask.clone(),
1279            },
1280        )?;
1281        save_scalar_field_png(&outputs.trust, &trust_map_path, heatmap_blue)?;
1282        save_scalar_field_png(&outputs.alpha, &alpha_map_path, heatmap_orange)?;
1283        save_scalar_field_png(
1284            &outputs.intervention,
1285            &intervention_map_path,
1286            heatmap_red,
1287        )?;
1288        let residual_field =
1289            residual_field(&capture.inputs.current_color, &capture.inputs.reprojected_history);
1290        save_scalar_field_png(&residual_field, &residual_map_path, heatmap_residual)?;
1291        overlay_mask(
1292            &capture.inputs.current_color,
1293            &instability_mask,
1294            Color::rgb(1.0, 0.1, 0.1),
1295            0.45,
1296        )
1297        .save_png(&instability_overlay_path)?;
1298        overlay_mask(
1299            &capture.inputs.current_color,
1300            &roi_mask,
1301            Color::rgb(0.12, 1.0, 0.24),
1302            0.45,
1303        )
1304        .save_png(&roi_overlay_path)?;
1305
1306        let classification = classify_capture(&methods);
1307        let roi_residual_mean = residual_field.mean_over_mask(&roi_mask);
1308        let instability_fraction = instability_mask
1309            .iter()
1310            .filter(|value| **value)
1311            .count() as f32
1312            / instability_mask.len().max(1) as f32;
1313        let explanation = build_explanation(
1314            &classification,
1315            materialized,
1316            &methods,
1317            roi_residual_mean,
1318            instability_fraction,
1319        );
1320
1321        frames.push(FrameArtifacts {
1322            label: capture.label.clone(),
1323            scene_name: materialized.scene_name.clone(),
1324            shot_name: materialized.shot_name.clone(),
1325            frame_index: materialized.frame_index,
1326            classification,
1327            roi_source,
1328            roi_pixels,
1329            roi_coverage,
1330            reference_source: reference_source.to_string(),
1331            current_frame_path,
1332            baseline_frame_path,
1333            roi_mask_path,
1334            trust_map_path,
1335            alpha_map_path,
1336            intervention_map_path,
1337            residual_map_path,
1338            instability_overlay_path,
1339            roi_overlay_path,
1340            output_panel_path,
1341            metric_source: methods.metric_source.to_string(),
1342            dsfb_full_frame_mae: methods.dsfb.overall_mae,
1343            dsfb_roi_mae: methods.dsfb.roi_mae,
1344            dsfb_max_error: methods.dsfb.max_error,
1345            dsfb_plus_heuristic_full_frame_mae: methods.dsfb_plus_heuristic.overall_mae,
1346            dsfb_plus_heuristic_roi_mae: methods.dsfb_plus_heuristic.roi_mae,
1347            dsfb_plus_heuristic_max_error: methods.dsfb_plus_heuristic.max_error,
1348            strong_heuristic_full_frame_mae: methods.strong_heuristic.overall_mae,
1349            strong_heuristic_roi_mae: methods.strong_heuristic.roi_mae,
1350            strong_heuristic_max_error: methods.strong_heuristic.max_error,
1351            fixed_alpha_full_frame_mae: methods.fixed_alpha.overall_mae,
1352            fixed_alpha_roi_mae: methods.fixed_alpha.roi_mae,
1353            fixed_alpha_max_error: methods.fixed_alpha.max_error,
1354            dsfb_mean_trust: outputs.trust.mean(),
1355            dsfb_mean_alpha: outputs.alpha.mean(),
1356            dsfb_intervention_rate: outputs.intervention.mean(),
1357            roi_residual_mean,
1358            instability_fraction,
1359            gpu_total_ms: gpu_by_label.get(&capture.label).copied().flatten(),
1360            explanation,
1361        });
1362    }
1363
1364    Ok(frames)
1365}
1366
1367fn write_run_manifest(
1368    path: &Path,
1369    manifest_path: &Path,
1370    manifest: &UnrealNativeManifest,
1371    materialized: &MaterializedRun,
1372) -> Result<()> {
1373    let payload = serde_json::json!({
1374        "schema_version": manifest.schema_version,
1375        "dataset_kind": manifest.dataset_kind,
1376        "provenance_label": manifest.provenance_label,
1377        "dataset_id": manifest.dataset_id,
1378        "manifest_path": manifest_path.display().to_string(),
1379        "materialized_manifest_path": materialized.materialized_manifest_path.display().to_string(),
1380        "capture_count": materialized.captures.len(),
1381        "engine": manifest.engine,
1382        "contract": manifest.contract,
1383        "notes": manifest.notes,
1384    });
1385    fs::write(path, serde_json::to_string_pretty(&payload)?)?;
1386    Ok(())
1387}
1388
1389fn classification_counts(frames: &[FrameArtifacts]) -> ClassificationCounts {
1390    let mut counts = ClassificationCounts::default();
1391    for frame in frames {
1392        match frame.classification.as_str() {
1393            "dsfb_helpful" => counts.dsfb_helpful += 1,
1394            "dsfb_neutral" => counts.dsfb_neutral += 1,
1395            "heuristic_favorable" => counts.heuristic_favorable += 1,
1396            _ => counts.richer_cues_required += 1,
1397        }
1398    }
1399    counts
1400}
1401
1402fn write_metrics_csv(
1403    path: &Path,
1404    capture_summaries: &[CaptureSummaryRecord],
1405    demo_b: &ExternalDemoBMetrics,
1406) -> Result<()> {
1407    let mut csv = String::new();
1408    let _ = writeln!(
1409        csv,
1410        "record_type,capture_label,scene_name,shot_name,frame_index,classification,roi_source,roi_coverage,reference_source,metric_source,dsfb_full_frame_mae,dsfb_roi_mae,dsfb_max_error,dsfb_plus_heuristic_full_frame_mae,dsfb_plus_heuristic_roi_mae,dsfb_plus_heuristic_max_error,strong_heuristic_full_frame_mae,strong_heuristic_roi_mae,strong_heuristic_max_error,fixed_alpha_full_frame_mae,fixed_alpha_roi_mae,fixed_alpha_max_error,dsfb_mean_trust,dsfb_mean_alpha,dsfb_intervention_rate,roi_residual_mean,instability_fraction,gpu_total_ms"
1411    );
1412    for capture in capture_summaries {
1413        let _ = writeln!(
1414            csv,
1415            "demo_a,{capture_label},{scene_name},{shot_name},{frame_index},{classification},{roi_source},{roi_coverage:.5},{reference_source},{metric_source},{dsfb_full_frame_mae:.5},{dsfb_roi_mae:.5},{dsfb_max_error:.5},{dsfb_plus_heuristic_full_frame_mae:.5},{dsfb_plus_heuristic_roi_mae:.5},{dsfb_plus_heuristic_max_error:.5},{strong_heuristic_full_frame_mae:.5},{strong_heuristic_roi_mae:.5},{strong_heuristic_max_error:.5},{fixed_alpha_full_frame_mae:.5},{fixed_alpha_roi_mae:.5},{fixed_alpha_max_error:.5},{dsfb_mean_trust:.5},{dsfb_mean_alpha:.5},{dsfb_intervention_rate:.5},{roi_residual_mean:.5},{instability_fraction:.5},{gpu_total_ms}",
1416            capture_label = capture.capture_label,
1417            scene_name = capture.scene_name,
1418            shot_name = capture.shot_name,
1419            frame_index = capture.frame_index,
1420            classification = capture.classification,
1421            roi_source = capture.roi_source,
1422            roi_coverage = capture.roi_coverage,
1423            reference_source = capture.reference_source,
1424            metric_source = capture.metric_source,
1425            dsfb_full_frame_mae = capture.dsfb_full_frame_mae,
1426            dsfb_roi_mae = capture.dsfb_roi_mae,
1427            dsfb_max_error = capture.dsfb_max_error,
1428            dsfb_plus_heuristic_full_frame_mae = capture.dsfb_plus_heuristic_full_frame_mae,
1429            dsfb_plus_heuristic_roi_mae = capture.dsfb_plus_heuristic_roi_mae,
1430            dsfb_plus_heuristic_max_error = capture.dsfb_plus_heuristic_max_error,
1431            strong_heuristic_full_frame_mae = capture.strong_heuristic_full_frame_mae,
1432            strong_heuristic_roi_mae = capture.strong_heuristic_roi_mae,
1433            strong_heuristic_max_error = capture.strong_heuristic_max_error,
1434            fixed_alpha_full_frame_mae = capture.fixed_alpha_full_frame_mae,
1435            fixed_alpha_roi_mae = capture.fixed_alpha_roi_mae,
1436            fixed_alpha_max_error = capture.fixed_alpha_max_error,
1437            dsfb_mean_trust = capture.dsfb_mean_trust,
1438            dsfb_mean_alpha = capture.dsfb_mean_alpha,
1439            dsfb_intervention_rate = capture.dsfb_intervention_rate,
1440            roi_residual_mean = capture.roi_residual_mean,
1441            instability_fraction = capture.instability_fraction,
1442            gpu_total_ms = capture
1443                .gpu_total_ms
1444                .map(|value| format!("{value:.5}"))
1445                .unwrap_or_else(|| "n/a".to_string()),
1446        );
1447    }
1448    for capture in &demo_b.captures {
1449        for policy in &capture.policies {
1450            let _ = writeln!(
1451                csv,
1452                "demo_b,{capture_label},,,,,{roi_source},{roi_coverage:.5},{reference_source},{metric_source},,{roi_mae:.5},,,,,,,,,,{roi_mean_spp:.5},{non_roi_mean_spp:.5},{overall_mae:.5},,,",
1453                capture_label = capture.capture_label,
1454                roi_source = capture.roi_source,
1455                roi_coverage = capture.roi_coverage,
1456                reference_source = capture.reference_source,
1457                metric_source = capture.metric_source,
1458                roi_mae = policy.roi_mae,
1459                roi_mean_spp = policy.roi_mean_spp,
1460                non_roi_mean_spp = policy.non_roi_mean_spp,
1461                overall_mae = policy.overall_mae,
1462            );
1463        }
1464    }
1465    fs::write(path, csv)?;
1466    Ok(())
1467}
1468
1469fn write_comparison_summary(
1470    path: &Path,
1471    manifest: &UnrealNativeManifest,
1472    frames: &[FrameArtifacts],
1473    demo_b: &ExternalDemoBMetrics,
1474) -> Result<()> {
1475    let mut markdown = String::new();
1476    let roi_coverages = frames
1477        .iter()
1478        .map(|frame| frame.roi_coverage)
1479        .collect::<Vec<_>>();
1480    let fixed_roi = frames
1481        .iter()
1482        .map(|frame| frame.fixed_alpha_roi_mae)
1483        .collect::<Vec<_>>();
1484    let strong_roi = frames
1485        .iter()
1486        .map(|frame| frame.strong_heuristic_roi_mae)
1487        .collect::<Vec<_>>();
1488    let dsfb_roi = frames
1489        .iter()
1490        .map(|frame| frame.dsfb_roi_mae)
1491        .collect::<Vec<_>>();
1492    let hybrid_roi = frames
1493        .iter()
1494        .map(|frame| frame.dsfb_plus_heuristic_roi_mae)
1495        .collect::<Vec<_>>();
1496    let (roi_coverage_mean, roi_coverage_std) = mean_and_std(&roi_coverages);
1497    let (fixed_roi_mean, fixed_roi_std) = mean_and_std(&fixed_roi);
1498    let (strong_roi_mean, strong_roi_std) = mean_and_std(&strong_roi);
1499    let (dsfb_roi_mean, dsfb_roi_std) = mean_and_std(&dsfb_roi);
1500    let (hybrid_roi_mean, hybrid_roi_std) = mean_and_std(&hybrid_roi);
1501    let heuristic_favorable_everywhere =
1502        !frames.is_empty() && frames.iter().all(|frame| frame.classification == "heuristic_favorable");
1503    let hybrid_beats_strong = !frames.is_empty() && hybrid_roi_mean + 1e-6 < strong_roi_mean;
1504    let onset_frame = frames.iter().min_by_key(|frame| frame.frame_index);
1505    let peak_roi_frame = frames
1506        .iter()
1507        .max_by(|left, right| left.roi_coverage.total_cmp(&right.roi_coverage));
1508    let recovery_frame = frames.iter().max_by_key(|frame| frame.frame_index);
1509    let mean_demo_b_policy = |policy_id: &str| -> Option<f32> {
1510        let values = demo_b
1511            .captures
1512            .iter()
1513            .map(|capture| {
1514                capture
1515                    .policies
1516                    .iter()
1517                    .find(|policy| policy.policy_id == policy_id)
1518                    .map(|policy| policy.roi_mae)
1519            })
1520            .collect::<Option<Vec<_>>>()?;
1521        Some(values.iter().copied().sum::<f32>() / values.len() as f32)
1522    };
1523    let _ = writeln!(markdown, "# Unreal-Native Comparison Summary");
1524    let _ = writeln!(markdown);
1525    let _ = writeln!(
1526        markdown,
1527        "Dataset `{}` is labeled `{}` and was executed through the strict Unreal-native replay path.",
1528        manifest.dataset_id, manifest.provenance_label
1529    );
1530    let _ = writeln!(markdown);
1531    let _ = writeln!(markdown, "## Frozen Benchmark Contract");
1532    let _ = writeln!(markdown);
1533    let _ = writeln!(markdown, "- {ROI_CONTRACT_STATEMENT}");
1534    let _ = writeln!(
1535        markdown,
1536        "- Canonical baseline ladder: `fixed_alpha`, `strong_heuristic`, `dsfb_host_minimum`, `dsfb_plus_strong_heuristic`."
1537    );
1538    let _ = writeln!(
1539        markdown,
1540        "- Fixed capture count in this run: `{}` real Unreal-native capture(s).",
1541        frames.len()
1542    );
1543    if frames.len() >= 2 {
1544        let _ = writeln!(
1545            markdown,
1546            "- Trust diagnostics generated for the canonical run: `figures/trust_histogram.svg`, `figures/trust_vs_error.svg`, `figures/trust_conditioned_error_map.png`, `figures/trust_temporal_trajectory.svg`."
1547        );
1548    } else {
1549        let _ = writeln!(
1550            markdown,
1551            "- Trust diagnostics generated for the canonical run: `figures/trust_histogram.svg`, `figures/trust_vs_error.svg`, `figures/trust_conditioned_error_map.png`."
1552        );
1553    }
1554    let _ = writeln!(markdown);
1555    let _ = writeln!(markdown, "## Current Result Posture");
1556    let _ = writeln!(markdown);
1557    if hybrid_beats_strong {
1558        let _ = writeln!(markdown, "- {CANONICAL_HEADLINE_STATEMENT}");
1559    }
1560    if heuristic_favorable_everywhere {
1561        let _ = writeln!(markdown, "- {PURE_DSFB_LIMITATION_STATEMENT}");
1562    } else if !frames.is_empty() {
1563        let _ = writeln!(
1564            markdown,
1565            "- Pure DSFB does beat the strong heuristic on at least one capture in this run, so no pure-DSFB limitation claim is emitted for this manifest."
1566        );
1567    }
1568    if (roi_coverage_mean - 0.5).abs() <= 0.1 {
1569        let _ = writeln!(markdown, "- {ROI_HONESTY_STATEMENT}");
1570    } else {
1571        let _ = writeln!(
1572            markdown,
1573            "- The ROI definition captures {:.2}% of the frame under the fixed baseline-relative threshold, so it is not being treated as a tiny artifact-only mask in this run.",
1574            roi_coverage_mean * 100.0
1575        );
1576    }
1577    let _ = writeln!(
1578        markdown,
1579        "- Demo A ROI MAE mean ± std is {:.5} ± {:.5} for `fixed_alpha`, {:.5} ± {:.5} for `strong_heuristic`, {:.5} ± {:.5} for `dsfb_host_minimum`, and {:.5} ± {:.5} for `dsfb_plus_strong_heuristic`.",
1580        fixed_roi_mean,
1581        fixed_roi_std,
1582        strong_roi_mean,
1583        strong_roi_std,
1584        dsfb_roi_mean,
1585        dsfb_roi_std,
1586        hybrid_roi_mean,
1587        hybrid_roi_std
1588    );
1589    let _ = writeln!(
1590        markdown,
1591        "- ROI coverage mean ± std is {:.2}% ± {:.2}% across the fixed capture family.",
1592        roi_coverage_mean * 100.0,
1593        roi_coverage_std * 100.0
1594    );
1595    if let (Some(onset), Some(peak), Some(recovery)) = (onset_frame, peak_roi_frame, recovery_frame) {
1596        let _ = writeln!(
1597            markdown,
1598            "- Trust trajectory facts for this run: onset `{}`, peak ROI `{}`, recovery-side `{}`; mean trust = {:.5} -> {:.5} -> {:.5}; intervention rate = {:.5} -> {:.5} -> {:.5}.",
1599            onset.label,
1600            peak.label,
1601            recovery.label,
1602            onset.dsfb_mean_trust,
1603            peak.dsfb_mean_trust,
1604            recovery.dsfb_mean_trust,
1605            onset.dsfb_intervention_rate,
1606            peak.dsfb_intervention_rate,
1607            recovery.dsfb_intervention_rate
1608        );
1609    }
1610    if let (Some(imported), Some(combined), Some(uniform)) = (
1611        mean_demo_b_policy("imported_trust"),
1612        mean_demo_b_policy("combined_heuristic"),
1613        mean_demo_b_policy("uniform"),
1614    ) {
1615        let _ = writeln!(
1616            markdown,
1617            "- Demo B mean ROI error is {:.5} for imported trust, {:.5} for the combined heuristic, and {:.5} for uniform allocation.",
1618            imported,
1619            combined,
1620            uniform
1621        );
1622    }
1623    let _ = writeln!(markdown);
1624    let _ = writeln!(markdown, "## Capture Classification");
1625    let _ = writeln!(markdown);
1626    for frame in frames {
1627        let _ = writeln!(
1628            markdown,
1629            "- `{}` ({}/{} frame {}): `{}`. ROI pixels = {}, ROI coverage = {:.2}%. DSFB ROI MAE = {:.5}, DSFB + heuristic ROI MAE = {:.5}, strong heuristic ROI MAE = {:.5}, fixed alpha ROI MAE = {:.5}.",
1630            frame.label,
1631            frame.scene_name,
1632            frame.shot_name,
1633            frame.frame_index,
1634            frame.classification,
1635            frame.roi_pixels,
1636            frame.roi_coverage * 100.0,
1637            frame.dsfb_roi_mae,
1638            frame.dsfb_plus_heuristic_roi_mae,
1639            frame.strong_heuristic_roi_mae,
1640            frame.fixed_alpha_roi_mae
1641        );
1642    }
1643    let _ = writeln!(markdown);
1644    let _ = writeln!(markdown, "## Demo B Policy Posture");
1645    let _ = writeln!(markdown);
1646    for capture in &demo_b.captures {
1647        let imported = capture
1648            .policies
1649            .iter()
1650            .find(|policy| policy.policy_id == "imported_trust");
1651        let combined = capture
1652            .policies
1653            .iter()
1654            .find(|policy| policy.policy_id == "combined_heuristic");
1655        let uniform = capture
1656            .policies
1657            .iter()
1658            .find(|policy| policy.policy_id == "uniform");
1659        if let (Some(imported), Some(combined), Some(uniform)) = (imported, combined, uniform) {
1660            let winner = if imported.roi_mae + 1e-4 < combined.roi_mae {
1661                "DSFB-helpful allocation case"
1662            } else if (imported.roi_mae - combined.roi_mae).abs() <= 1e-4 {
1663                "DSFB-neutral allocation case"
1664            } else {
1665                "heuristic-favorable allocation case"
1666            };
1667            let _ = writeln!(
1668                markdown,
1669                "- `{}`: {}. Imported trust ROI error = {:.5}, combined heuristic ROI error = {:.5}, uniform ROI error = {:.5}.",
1670                capture.capture_label,
1671                winner,
1672                imported.roi_mae,
1673                combined.roi_mae,
1674                uniform.roi_mae
1675            );
1676        }
1677    }
1678    let _ = writeln!(markdown);
1679    let _ = writeln!(markdown, "## Boundaries");
1680    let _ = writeln!(markdown);
1681    let _ = writeln!(
1682        markdown,
1683        "- This is evidence consistent with reduced temporal artifact risk in bounded cases, not a claim of universal outperformance."
1684    );
1685    if frames.len() < ROI_AGGREGATION_MIN_CAPTURES {
1686        let _ = writeln!(
1687            markdown,
1688            "- Aggregated mean ± std claims are blocked until at least {} real captures are available under unchanged parameters and code.",
1689            ROI_AGGREGATION_MIN_CAPTURES
1690        );
1691    } else {
1692        let _ = writeln!(
1693            markdown,
1694            "- Aggregated mean ± std claims are emitted in `aggregation_summary.md` because this run contains {} unchanged-code real captures.",
1695            frames.len()
1696        );
1697    }
1698    let _ = writeln!(
1699        markdown,
1700        "- Demo B remains an advisory allocation proxy unless a live renderer budget path is exported."
1701    );
1702    let _ = writeln!(
1703        markdown,
1704        "- The crate is acting as a supervisory trust / admissibility / intervention layer, not a renderer replacement."
1705    );
1706    fs::write(path, markdown)?;
1707    Ok(())
1708}
1709
1710fn write_failure_modes(
1711    path: &Path,
1712    manifest: &UnrealNativeManifest,
1713    captures: &[MaterializedCapture],
1714    scaling: &ExternalScalingMetrics,
1715) -> Result<()> {
1716    let missing_optional = captures
1717        .iter()
1718        .filter(|capture| capture.roi_mask.is_none() || capture.disocclusion_mask.is_none())
1719        .map(|capture| capture.label.clone())
1720        .collect::<Vec<_>>();
1721    let mut markdown = String::new();
1722    let _ = writeln!(markdown, "# Unreal-Native Failure Modes");
1723    let _ = writeln!(markdown);
1724    let _ = writeln!(
1725        markdown,
1726        "This file is first-class evidence for where the Unreal-native replay path should remain bounded or advisory."
1727    );
1728    let _ = writeln!(markdown);
1729    let _ = writeln!(markdown, "## Structural Limits");
1730    let _ = writeln!(markdown);
1731    let _ = writeln!(
1732        markdown,
1733        "- Residual-only evidence weakens when the host output already tracks the current frame closely."
1734    );
1735    let _ = writeln!(
1736        markdown,
1737        "- Canonical ROI is always recomputed from the fixed-alpha baseline and the reference/proxy frame using `{}`; optional manifest ROI masks are audit inputs only.",
1738        ROI_CONTRACT_SOURCE
1739    );
1740    let _ = writeln!(
1741        markdown,
1742        "- Transparency, particles, UI, post effects, and specular-only motion can violate the view-space normal and monotonic-depth assumptions."
1743    );
1744    let _ = writeln!(
1745        markdown,
1746        "- If motion vectors are noisy or encoded in a convention that does not match the manifest, the run fails rather than silently downgrading."
1747    );
1748    let _ = writeln!(
1749        markdown,
1750        "- Where a host heuristic already performs strongly, DSFB should be interpreted as a bounded monitor or advisory layer."
1751    );
1752    let _ = writeln!(markdown);
1753    let _ = writeln!(markdown, "## Export-Specific Notes");
1754    let _ = writeln!(markdown);
1755    let _ = writeln!(
1756        markdown,
1757        "- Dataset `{}` uses motion_vector_convention = `{}` and history_source = `{}`.",
1758        manifest.dataset_id,
1759        manifest.contract.motion_vector_convention,
1760        manifest.contract.history_source
1761    );
1762    if !missing_optional.is_empty() {
1763        let _ = writeln!(
1764            markdown,
1765            "- Optional overlays were missing for: {}.",
1766            missing_optional.join(", ")
1767        );
1768    }
1769    let _ = writeln!(markdown);
1770    let _ = writeln!(markdown, "## Scaling Limits");
1771    let _ = writeln!(markdown);
1772    let _ = writeln!(
1773        markdown,
1774        "- Scaling measurement kind: `{}`. Coverage status: `{}`.",
1775        scaling.measurement_kind, scaling.coverage.coverage_status
1776    );
1777    for entry in &scaling.entries {
1778        if let Some(reason) = &entry.unavailable_reason {
1779            let _ = writeln!(
1780                markdown,
1781                "- `{}` {}x{} unavailable: {}",
1782                entry.label, entry.width, entry.height, reason
1783            );
1784        }
1785    }
1786    fs::write(path, markdown)?;
1787    Ok(())
1788}
1789
1790fn select_executive_frame<'a>(frames: &'a [FrameArtifacts]) -> Result<&'a FrameArtifacts> {
1791    frames
1792        .iter()
1793        .max_by(|left, right| {
1794            let left_gain = left
1795                .strong_heuristic_roi_mae
1796                - left.dsfb_roi_mae.min(left.dsfb_plus_heuristic_roi_mae);
1797            let right_gain = right
1798                .strong_heuristic_roi_mae
1799                - right.dsfb_roi_mae.min(right.dsfb_plus_heuristic_roi_mae);
1800            left_gain.total_cmp(&right_gain)
1801        })
1802        .ok_or_else(|| Error::Message("no per-frame artifacts were generated".to_string()))
1803}
1804
1805fn run_bundle_builder(run_dir: &Path) -> Result<()> {
1806    let script = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
1807        .join("colab")
1808        .join("build_unreal_native_bundle.py");
1809    let status = Command::new("python3")
1810        .arg(&script)
1811        .arg("--run-dir")
1812        .arg(run_dir)
1813        .status()
1814        .map_err(|error| {
1815            Error::Message(format!(
1816                "failed to launch unreal-native bundle builder {}: {error}",
1817                script.display()
1818            ))
1819        })?;
1820    if !status.success() {
1821        return Err(Error::Message(format!(
1822            "unreal-native bundle builder {} exited with status {}",
1823            script.display(),
1824            status
1825        )));
1826    }
1827    Ok(())
1828}
1829
1830fn find_demo_a_methods<'a>(
1831    demo_a: &'a ExternalDemoAMetrics,
1832    capture_label: &str,
1833) -> Result<DemoAMethodSelection<'a>> {
1834    let capture = demo_a
1835        .captures
1836        .iter()
1837        .find(|capture| capture.capture_label == capture_label)
1838        .ok_or_else(|| {
1839            Error::Message(format!(
1840                "Demo A metrics did not contain capture `{capture_label}`"
1841            ))
1842        })?;
1843    let fixed_alpha = capture
1844        .methods
1845        .iter()
1846        .find(|method| method.method_id == "fixed_alpha")
1847        .ok_or_else(|| Error::Message(format!("capture `{capture_label}` missing fixed_alpha")))?;
1848    let strong_heuristic = capture
1849        .methods
1850        .iter()
1851        .find(|method| method.method_id == "strong_heuristic")
1852        .ok_or_else(|| {
1853            Error::Message(format!(
1854                "capture `{capture_label}` missing strong_heuristic"
1855            ))
1856        })?;
1857    let dsfb = capture
1858        .methods
1859        .iter()
1860        .find(|method| method.method_id == "dsfb_host_minimum")
1861        .ok_or_else(|| {
1862            Error::Message(format!(
1863                "capture `{capture_label}` missing dsfb_host_minimum"
1864            ))
1865        })?;
1866    let dsfb_plus_heuristic = capture
1867        .methods
1868        .iter()
1869        .find(|method| method.method_id == "dsfb_plus_strong_heuristic")
1870        .ok_or_else(|| {
1871            Error::Message(format!(
1872                "capture `{capture_label}` missing dsfb_plus_strong_heuristic"
1873            ))
1874        })?;
1875    Ok(DemoAMethodSelection {
1876        metric_source: &capture.metric_source,
1877        roi_source: &capture.roi_source,
1878        roi_coverage: capture.roi_coverage,
1879        reference_source: &capture.reference_source,
1880        fixed_alpha,
1881        strong_heuristic,
1882        dsfb,
1883        dsfb_plus_heuristic,
1884    })
1885}
1886
1887fn classify_capture(methods: &DemoAMethodSelection<'_>) -> String {
1888    let epsilon = 1e-4;
1889    if methods.dsfb.roi_mae + epsilon
1890        < methods
1891            .strong_heuristic
1892            .roi_mae
1893            .min(methods.fixed_alpha.roi_mae)
1894    {
1895        "dsfb_helpful".to_string()
1896    } else if (methods.dsfb.roi_mae - methods.strong_heuristic.roi_mae).abs() <= epsilon {
1897        "dsfb_neutral".to_string()
1898    } else if methods.strong_heuristic.roi_mae + epsilon < methods.dsfb.roi_mae {
1899        "heuristic_favorable".to_string()
1900    } else {
1901        "richer_cues_required".to_string()
1902    }
1903}
1904
1905fn build_explanation(
1906    classification: &str,
1907    materialized: &MaterializedCapture,
1908    methods: &DemoAMethodSelection<'_>,
1909    roi_residual_mean: f32,
1910    instability_fraction: f32,
1911) -> EvidenceExplanation {
1912    let what_went_wrong = format!(
1913        "Temporal reuse risk was concentrated in scene `{}` / shot `{}` frame {} with ROI residual {:.5} and instability coverage {:.3}.",
1914        materialized.scene_name, materialized.shot_name, materialized.frame_index, roi_residual_mean, instability_fraction
1915    );
1916    let what_dsfb_detected = format!(
1917        "DSFB concentrated low trust and intervention in the exported Unreal-native ROI, with ROI MAE {:.5}, strong heuristic ROI MAE {:.5}, and DSFB + heuristic ROI MAE {:.5}.",
1918        methods.dsfb.roi_mae, methods.strong_heuristic.roi_mae, methods.dsfb_plus_heuristic.roi_mae
1919    );
1920    let what_dsfb_changed = match classification {
1921        "dsfb_helpful" => "The supervisory layer would route this region toward higher alpha / intervention and away from blind temporal reuse.".to_string(),
1922        "dsfb_neutral" => "The supervisory layer agreed with the strongest host heuristic closely enough that this should be treated as a bounded monitor result, not a large behavioral delta.".to_string(),
1923        "heuristic_favorable" if methods.dsfb_plus_heuristic.roi_mae + 1e-4 < methods.strong_heuristic.roi_mae =>
1924            "The strongest host heuristic outperformed pure DSFB on this frame, but the explicit DSFB + strong heuristic hybrid recovered that miss and is reported separately rather than relabeled as DSFB.".to_string(),
1925        "heuristic_favorable" => "The strongest host heuristic outperformed DSFB on this frame; the evidence is surfaced directly rather than hidden.".to_string(),
1926        _ => "The current observability is not rich enough to claim a strong DSFB advantage on this frame.".to_string(),
1927    };
1928    let host_output_note = if materialized.host_output.is_some() {
1929        "Exported host output was preserved as an audit-only artifact and is not the canonical benchmark baseline."
1930    } else {
1931        "No exported host output was available; the canonical benchmark baseline remains fixed alpha."
1932    };
1933    let overhead_and_caveat = format!(
1934        "GPU measurement is advisory and environment-dependent; canonical ROI source is `{}` with {:.2}% coverage against reference source `{}` and canonical baseline is `{}`. {} This is not a renderer replacement claim.",
1935        methods.roi_source,
1936        methods.roi_coverage * 100.0,
1937        methods.reference_source,
1938        ROI_CONTRACT_BASELINE_METHOD_ID,
1939        host_output_note
1940    );
1941    EvidenceExplanation {
1942        what_went_wrong,
1943        what_dsfb_detected,
1944        what_dsfb_changed,
1945        overhead_and_caveat,
1946    }
1947}
1948
1949fn load_unreal_frame_metadata(base_dir: &Path, reference: &BufferReference) -> Result<UnrealFrameMetadata> {
1950    if reference.format != "json_metadata" {
1951        return Err(Error::Message(format!(
1952            "unreal-native metadata {} must use json_metadata format",
1953            reference.path
1954        )));
1955    }
1956    let path = resolve_path(base_dir, &reference.path);
1957    read_json(&path)
1958}
1959
1960fn validate_frame_metadata(frame: &UnrealFrameEntry, metadata: &UnrealFrameMetadata) -> Result<()> {
1961    if metadata.width == 0 || metadata.height == 0 {
1962        return Err(Error::Message(format!(
1963            "capture `{}` declared zero-sized extent {}x{}",
1964            frame.label, metadata.width, metadata.height
1965        )));
1966    }
1967    if metadata.frame_index != frame.frame_index {
1968        return Err(Error::Message(format!(
1969            "capture `{}` manifest frame_index {} did not match metadata frame_index {}",
1970            frame.label, frame.frame_index, metadata.frame_index
1971        )));
1972    }
1973    if metadata.history_frame_index != frame.history_frame_index {
1974        return Err(Error::Message(format!(
1975            "capture `{}` manifest history_frame_index {} did not match metadata history_frame_index {}",
1976            frame.label, frame.history_frame_index, metadata.history_frame_index
1977        )));
1978    }
1979    if metadata.source_kind != UNREAL_NATIVE_DATASET_KIND {
1980        return Err(Error::Message(format!(
1981            "capture `{}` metadata source_kind `{}` is invalid; `{}` is required",
1982            frame.label, metadata.source_kind, UNREAL_NATIVE_DATASET_KIND
1983        )));
1984    }
1985    if metadata.provenance_label.as_deref() != Some(UNREAL_NATIVE_PROVENANCE_LABEL) {
1986        return Err(Error::Message(format!(
1987            "capture `{}` metadata provenance_label must be `{}`",
1988            frame.label, UNREAL_NATIVE_PROVENANCE_LABEL
1989        )));
1990    }
1991    if !metadata.real_external_data {
1992        return Err(Error::Message(format!(
1993            "capture `{}` metadata real_external_data=false; unreal-native mode refuses synthetic or proxy provenance",
1994            frame.label
1995        )));
1996    }
1997    Ok(())
1998}
1999
2000fn normalize_motion_vectors(
2001    values: &mut [[f32; 2]],
2002    width: usize,
2003    height: usize,
2004    convention: &str,
2005) -> Result<()> {
2006    match convention {
2007        "pixel_offset_to_prev" => {}
2008        "ndc_to_prev" => {
2009            let scale_x = width as f32 / 2.0;
2010            let scale_y = height as f32 / 2.0;
2011            for value in values {
2012                value[0] *= scale_x;
2013                value[1] *= scale_y;
2014            }
2015        }
2016        other => {
2017            return Err(Error::Message(format!(
2018                "unsupported unreal-native motion vector convention `{other}`"
2019            )))
2020        }
2021    }
2022    Ok(())
2023}
2024
2025fn reproject_image(previous: &ImageFrame, motion_vectors: &[MotionVector]) -> ImageFrame {
2026    let mut output = ImageFrame::new(previous.width(), previous.height());
2027    for y in 0..previous.height() {
2028        for x in 0..previous.width() {
2029            let motion = motion_vectors[y * previous.width() + x];
2030            output.set(
2031                x,
2032                y,
2033                previous.sample_bilinear_clamped(x as f32 + motion.to_prev_x, y as f32 + motion.to_prev_y),
2034            );
2035        }
2036    }
2037    output
2038}
2039
2040fn reproject_scalar(
2041    previous: &[f32],
2042    width: usize,
2043    height: usize,
2044    motion_vectors: &[MotionVector],
2045) -> Vec<f32> {
2046    let mut output = vec![0.0; width * height];
2047    for y in 0..height {
2048        for x in 0..width {
2049            let motion = motion_vectors[y * width + x];
2050            output[y * width + x] = sample_scalar(previous, width, height, x as f32 + motion.to_prev_x, y as f32 + motion.to_prev_y);
2051        }
2052    }
2053    output
2054}
2055
2056fn reproject_normals(
2057    previous: &[Normal3],
2058    width: usize,
2059    height: usize,
2060    motion_vectors: &[MotionVector],
2061) -> Vec<Normal3> {
2062    let mut output = vec![Normal3::new(0.0, 0.0, 1.0); width * height];
2063    for y in 0..height {
2064        for x in 0..width {
2065            let motion = motion_vectors[y * width + x];
2066            output[y * width + x] = sample_normal(previous, width, height, x as f32 + motion.to_prev_x, y as f32 + motion.to_prev_y);
2067        }
2068    }
2069    output
2070}
2071
2072fn sample_scalar(values: &[f32], width: usize, height: usize, x: f32, y: f32) -> f32 {
2073    let x0 = x.floor();
2074    let y0 = y.floor();
2075    let x1 = x0 + 1.0;
2076    let y1 = y0 + 1.0;
2077    let tx = (x - x0).clamp(0.0, 1.0);
2078    let ty = (y - y0).clamp(0.0, 1.0);
2079    let sample = |sx: f32, sy: f32| -> f32 {
2080        let ix = sx.clamp(0.0, width.saturating_sub(1) as f32) as usize;
2081        let iy = sy.clamp(0.0, height.saturating_sub(1) as f32) as usize;
2082        values[iy * width + ix]
2083    };
2084    let top = sample(x0, y0) * (1.0 - tx) + sample(x1, y0) * tx;
2085    let bottom = sample(x0, y1) * (1.0 - tx) + sample(x1, y1) * tx;
2086    top * (1.0 - ty) + bottom * ty
2087}
2088
2089fn sample_normal(values: &[Normal3], width: usize, height: usize, x: f32, y: f32) -> Normal3 {
2090    let x0 = x.floor();
2091    let y0 = y.floor();
2092    let x1 = x0 + 1.0;
2093    let y1 = y0 + 1.0;
2094    let tx = (x - x0).clamp(0.0, 1.0);
2095    let ty = (y - y0).clamp(0.0, 1.0);
2096    let sample = |sx: f32, sy: f32| -> Normal3 {
2097        let ix = sx.clamp(0.0, width.saturating_sub(1) as f32) as usize;
2098        let iy = sy.clamp(0.0, height.saturating_sub(1) as f32) as usize;
2099        values[iy * width + ix]
2100    };
2101    let lerp = |a: Normal3, b: Normal3, t: f32| {
2102        Normal3::new(
2103            a.x + (b.x - a.x) * t,
2104            a.y + (b.y - a.y) * t,
2105            a.z + (b.z - a.z) * t,
2106        )
2107    };
2108    lerp(lerp(sample(x0, y0), sample(x1, y0), tx), lerp(sample(x0, y1), sample(x1, y1), tx), ty).normalized()
2109}
2110
2111fn load_mask_any(
2112    base_dir: &Path,
2113    reference: &BufferReference,
2114    expected_width: usize,
2115    expected_height: usize,
2116) -> Result<Vec<bool>> {
2117    match reference.format.as_str() {
2118        "json_mask_bool" | "raw_mask_u8" => {
2119            Ok(load_bool_buffer_from_reference(base_dir, reference, expected_width, expected_height)?.2)
2120        }
2121        "json_scalar_f32" | "raw_r32f" | "exr_r32f" => {
2122            let values =
2123                load_scalar_buffer_from_reference(base_dir, reference, expected_width, expected_height)?.2;
2124            Ok(values.into_iter().map(|value| value >= 0.5).collect())
2125        }
2126        other => Err(Error::Message(format!(
2127            "unsupported unreal-native mask format `{other}` for {}",
2128            reference.path
2129        ))),
2130    }
2131}
2132
2133fn resolve_with_alpha(
2134    history: &ImageFrame,
2135    current: &ImageFrame,
2136    alpha: &ScalarField,
2137) -> ImageFrame {
2138    let mut output = ImageFrame::new(current.width(), current.height());
2139    for y in 0..current.height() {
2140        for x in 0..current.width() {
2141            output.set(
2142                x,
2143                y,
2144                history.get(x, y).lerp(current.get(x, y), alpha.get(x, y)),
2145            );
2146        }
2147    }
2148    output
2149}
2150
2151fn run_strong_heuristic(
2152    config: &DemoConfig,
2153    capture: &crate::external::ExternalLoadedCapture,
2154) -> (ImageFrame, ScalarField, ScalarField) {
2155    let width = capture.inputs.width();
2156    let height = capture.inputs.height();
2157    let mut resolved = ImageFrame::new(width, height);
2158    let mut alpha = ScalarField::new(width, height);
2159    let mut response = ScalarField::new(width, height);
2160
2161    for y in 0..height {
2162        for x in 0..width {
2163            let index = y * width + x;
2164            let current = capture.inputs.current_color.get(x, y);
2165            let history = capture.inputs.reprojected_history.get(x, y);
2166            let clamped = clamp_to_current_neighborhood(&capture.inputs.current_color, history, x, y);
2167            let clamp_distance = clamped.abs_diff(history);
2168            let residual_gate = smoothstep(
2169                config.baseline.residual_threshold.low,
2170                config.baseline.residual_threshold.high,
2171                current.abs_diff(clamped),
2172            );
2173            let depth_gate = smoothstep(
2174                config.baseline.depth_disagreement.low,
2175                config.baseline.depth_disagreement.high,
2176                (capture.inputs.current_depth[index] - capture.inputs.reprojected_depth[index]).abs(),
2177            );
2178            let normal_gate = smoothstep(
2179                config.baseline.normal_disagreement.low,
2180                config.baseline.normal_disagreement.high,
2181                1.0 - capture.inputs.current_normals[index]
2182                    .dot(capture.inputs.reprojected_normals[index])
2183                    .clamp(-1.0, 1.0),
2184            );
2185            let neighborhood_gate = smoothstep(
2186                config.baseline.neighborhood_distance.low,
2187                config.baseline.neighborhood_distance.high,
2188                clamp_distance,
2189            );
2190            let trigger = residual_gate
2191                .max(depth_gate)
2192                .max(normal_gate)
2193                .max(neighborhood_gate);
2194            let pixel_alpha = config.baseline.residual_alpha_range.min
2195                + (config.baseline.residual_alpha_range.max
2196                    - config.baseline.residual_alpha_range.min)
2197                    * trigger;
2198            alpha.set(x, y, pixel_alpha);
2199            response.set(x, y, trigger);
2200            resolved.set(x, y, clamped.lerp(current, pixel_alpha));
2201        }
2202    }
2203
2204    (resolved, alpha, response)
2205}
2206
2207fn clamp_to_current_neighborhood(
2208    current: &ImageFrame,
2209    history: Color,
2210    x: usize,
2211    y: usize,
2212) -> Color {
2213    let mut min_r = f32::INFINITY;
2214    let mut min_g = f32::INFINITY;
2215    let mut min_b = f32::INFINITY;
2216    let mut max_r = f32::NEG_INFINITY;
2217    let mut max_g = f32::NEG_INFINITY;
2218    let mut max_b = f32::NEG_INFINITY;
2219    for dy in -1i32..=1 {
2220        for dx in -1i32..=1 {
2221            let sample = current.sample_clamped(x as i32 + dx, y as i32 + dy);
2222            min_r = min_r.min(sample.r);
2223            min_g = min_g.min(sample.g);
2224            min_b = min_b.min(sample.b);
2225            max_r = max_r.max(sample.r);
2226            max_g = max_g.max(sample.g);
2227            max_b = max_b.max(sample.b);
2228        }
2229    }
2230    Color::rgb(
2231        history.r.clamp(min_r, max_r),
2232        history.g.clamp(min_g, max_g),
2233        history.b.clamp(min_b, max_b),
2234    )
2235}
2236
2237fn smoothstep(low: f32, high: f32, value: f32) -> f32 {
2238    let span = (high - low).max(1e-6);
2239    let t = ((value - low) / span).clamp(0.0, 1.0);
2240    t * t * (3.0 - 2.0 * t)
2241}
2242
2243fn residual_field(current: &ImageFrame, history: &ImageFrame) -> ScalarField {
2244    let mut field = ScalarField::new(current.width(), current.height());
2245    for y in 0..current.height() {
2246        for x in 0..current.width() {
2247            field.set(x, y, (current.get(x, y).abs_diff(history.get(x, y)) / 0.25).clamp(0.0, 1.0));
2248        }
2249    }
2250    field
2251}
2252
2253fn derive_instability_mask(
2254    outputs: &HostSupervisionOutputs,
2255    current: &ImageFrame,
2256    history: &ImageFrame,
2257) -> Vec<bool> {
2258    let residual = residual_field(current, history);
2259    let mut mask = vec![false; current.width() * current.height()];
2260    for y in 0..current.height() {
2261        for x in 0..current.width() {
2262            let index = y * current.width() + x;
2263            mask[index] = outputs.intervention.get(x, y) > 0.45
2264                || outputs.trust.get(x, y) < 0.35
2265                || residual.get(x, y) > 0.35;
2266        }
2267    }
2268    mask
2269}
2270
2271fn overlay_mask(frame: &ImageFrame, mask: &[bool], overlay: Color, strength: f32) -> ImageFrame {
2272    let mut output = frame.clone();
2273    for y in 0..frame.height() {
2274        for x in 0..frame.width() {
2275            let index = y * frame.width() + x;
2276            if mask[index] {
2277                output.set(x, y, frame.get(x, y).lerp(overlay, strength));
2278            }
2279        }
2280    }
2281    output
2282}
2283
2284fn constant_field(width: usize, height: usize, value: f32) -> ScalarField {
2285    ScalarField::from_values(width, height, vec![value; width * height])
2286}
2287
2288fn resolve_path(base_dir: &Path, relative_or_absolute: &str) -> PathBuf {
2289    let candidate = Path::new(relative_or_absolute);
2290    if candidate.is_absolute() {
2291        candidate.to_path_buf()
2292    } else {
2293        base_dir.join(candidate)
2294    }
2295}
2296
2297fn relative_path_string(path: &Path, base_dir: &Path) -> String {
2298    path.strip_prefix(base_dir)
2299        .unwrap_or(path)
2300        .to_string_lossy()
2301        .replace('\\', "/")
2302}
2303
2304fn write_canonical_metric_sheet(
2305    path: &Path,
2306    capture_summaries: &[CaptureSummaryRecord],
2307) -> Result<()> {
2308    let mut markdown = String::new();
2309    let _ = writeln!(markdown, "# Canonical Metric Sheet");
2310    let _ = writeln!(markdown);
2311    let _ = writeln!(markdown, "{ROI_CONTRACT_STATEMENT}");
2312    let _ = writeln!(markdown);
2313    let _ = writeln!(
2314        markdown,
2315        "Strong baseline: `strong_heuristic` (named strong heuristic clamp). Canonical baseline: `{}`.",
2316        ROI_CONTRACT_BASELINE_METHOD_ID
2317    );
2318    let _ = writeln!(markdown);
2319    let _ = writeln!(
2320        markdown,
2321        "| Capture set | Metric | Baseline | Strong heuristic | DSFB | DSFB + heuristic | Winner |"
2322    );
2323    let _ = writeln!(markdown, "| --- | --- | ---: | ---: | ---: | ---: | --- |");
2324    for capture in capture_summaries {
2325        let _ = writeln!(
2326            markdown,
2327            "| {} | ROI MAE | {:.5} | {:.5} | {:.5} | {:.5} | {} |",
2328            capture.capture_label,
2329            capture.fixed_alpha_roi_mae,
2330            capture.strong_heuristic_roi_mae,
2331            capture.dsfb_roi_mae,
2332            capture.dsfb_plus_heuristic_roi_mae,
2333            winner_label(
2334                capture.fixed_alpha_roi_mae,
2335                capture.strong_heuristic_roi_mae,
2336                capture.dsfb_roi_mae,
2337                Some(capture.dsfb_plus_heuristic_roi_mae),
2338            )
2339        );
2340        let _ = writeln!(
2341            markdown,
2342            "| {} | Full-frame MAE | {:.5} | {:.5} | {:.5} | {:.5} | {} |",
2343            capture.capture_label,
2344            capture.fixed_alpha_full_frame_mae,
2345            capture.strong_heuristic_full_frame_mae,
2346            capture.dsfb_full_frame_mae,
2347            capture.dsfb_plus_heuristic_full_frame_mae,
2348            winner_label(
2349                capture.fixed_alpha_full_frame_mae,
2350                capture.strong_heuristic_full_frame_mae,
2351                capture.dsfb_full_frame_mae,
2352                Some(capture.dsfb_plus_heuristic_full_frame_mae),
2353            )
2354        );
2355        let _ = writeln!(
2356            markdown,
2357            "| {} | Max error | {:.5} | {:.5} | {:.5} | {:.5} | {} |",
2358            capture.capture_label,
2359            capture.fixed_alpha_max_error,
2360            capture.strong_heuristic_max_error,
2361            capture.dsfb_max_error,
2362            capture.dsfb_plus_heuristic_max_error,
2363            winner_label(
2364                capture.fixed_alpha_max_error,
2365                capture.strong_heuristic_max_error,
2366                capture.dsfb_max_error,
2367                Some(capture.dsfb_plus_heuristic_max_error),
2368            )
2369        );
2370        let _ = writeln!(
2371            markdown,
2372            "| {} | ROI coverage | {:.2}% | {:.2}% | {:.2}% | {:.2}% | fixed ROI mask |",
2373            capture.capture_label,
2374            capture.roi_coverage * 100.0,
2375            capture.roi_coverage * 100.0,
2376            capture.roi_coverage * 100.0,
2377            capture.roi_coverage * 100.0,
2378        );
2379    }
2380    fs::write(path, markdown)?;
2381    Ok(())
2382}
2383
2384fn write_aggregation_summary(path: &Path, capture_summaries: &[CaptureSummaryRecord]) -> Result<()> {
2385    let mut markdown = String::new();
2386    let _ = writeln!(markdown, "# Aggregation Summary");
2387    let _ = writeln!(markdown);
2388    let _ = writeln!(markdown, "{ROI_CONTRACT_STATEMENT}");
2389    let _ = writeln!(markdown);
2390    let _ = writeln!(
2391        markdown,
2392        "Real capture count in this run: `{}`. Mean ± std claims require at least `{}` real captures under unchanged code and parameters.",
2393        capture_summaries.len(),
2394        ROI_AGGREGATION_MIN_CAPTURES
2395    );
2396    let _ = writeln!(markdown);
2397    if capture_summaries.len() < ROI_AGGREGATION_MIN_CAPTURES {
2398        let _ = writeln!(
2399            markdown,
2400            "Status: blocked by missing real captures. The crate-local canonical path currently has fewer than {} real Unreal-native captures, so no distribution claim is emitted.",
2401            ROI_AGGREGATION_MIN_CAPTURES
2402        );
2403    } else {
2404        let fixed = mean_and_std(
2405            &capture_summaries
2406                .iter()
2407                .map(|capture| capture.fixed_alpha_roi_mae)
2408                .collect::<Vec<_>>(),
2409        );
2410        let strong = mean_and_std(
2411            &capture_summaries
2412                .iter()
2413                .map(|capture| capture.strong_heuristic_roi_mae)
2414                .collect::<Vec<_>>(),
2415        );
2416        let dsfb = mean_and_std(
2417            &capture_summaries
2418                .iter()
2419                .map(|capture| capture.dsfb_roi_mae)
2420                .collect::<Vec<_>>(),
2421        );
2422        let hybrid = mean_and_std(
2423            &capture_summaries
2424                .iter()
2425                .map(|capture| capture.dsfb_plus_heuristic_roi_mae)
2426                .collect::<Vec<_>>(),
2427        );
2428        let _ = writeln!(
2429            markdown,
2430            "| Metric | Baseline mean ± std | Strong heuristic mean ± std | DSFB mean ± std | DSFB + heuristic mean ± std | Winner |"
2431        );
2432        let _ = writeln!(markdown, "| --- | ---: | ---: | ---: | ---: | --- |");
2433        let _ = writeln!(
2434            markdown,
2435            "| ROI MAE | {:.5} ± {:.5} | {:.5} ± {:.5} | {:.5} ± {:.5} | {:.5} ± {:.5} | {} |",
2436            fixed.0,
2437            fixed.1,
2438            strong.0,
2439            strong.1,
2440            dsfb.0,
2441            dsfb.1,
2442            hybrid.0,
2443            hybrid.1,
2444            winner_label(fixed.0, strong.0, dsfb.0, Some(hybrid.0))
2445        );
2446    }
2447    fs::write(path, markdown)?;
2448    Ok(())
2449}
2450
2451fn validate_unreal_native_artifacts(
2452    run_dir: &Path,
2453    frames: &[FrameArtifacts],
2454    comparison_summary_path: &Path,
2455    canonical_metric_sheet_path: &Path,
2456    aggregation_summary_path: &Path,
2457) -> Result<()> {
2458    let current_status_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("CURRENT_STATUS.md");
2459    if !current_status_path.exists() {
2460        return Err(Error::Message(format!(
2461            "required crate status file is missing: {}",
2462            current_status_path.display()
2463        )));
2464    }
2465    let current_status = fs::read_to_string(&current_status_path)?;
2466    for required in [
2467        CANONICAL_HEADLINE_STATEMENT,
2468        PURE_DSFB_LIMITATION_STATEMENT,
2469        ROI_HONESTY_STATEMENT,
2470    ] {
2471        if !current_status.contains(required) {
2472            return Err(Error::Message(format!(
2473                "crate status file {} is missing required canonical statement: {}",
2474                current_status_path.display(),
2475                required
2476            )));
2477        }
2478    }
2479
2480    let comparison_summary = fs::read_to_string(comparison_summary_path)?;
2481    if !comparison_summary.contains(ROI_CONTRACT_STATEMENT) {
2482        return Err(Error::Message(format!(
2483            "comparison summary {} is missing the ROI contract statement",
2484            comparison_summary_path.display()
2485        )));
2486    }
2487
2488    for report_name in [
2489        "demo_a_external_report.md",
2490        "demo_b_external_report.md",
2491        "external_validation_report.md",
2492    ] {
2493        let report_path = run_dir.join(report_name);
2494        let report = fs::read_to_string(&report_path)?;
2495        if !report.contains(ROI_CONTRACT_STATEMENT) {
2496            return Err(Error::Message(format!(
2497                "report {} is missing the ROI contract statement",
2498                report_path.display()
2499            )));
2500        }
2501    }
2502
2503    let canonical_metric_sheet = fs::read_to_string(canonical_metric_sheet_path)?;
2504    if !canonical_metric_sheet.contains("Strong heuristic") {
2505        return Err(Error::Message(format!(
2506            "canonical metric sheet {} is missing the strong baseline column",
2507            canonical_metric_sheet_path.display()
2508        )));
2509    }
2510    if !canonical_metric_sheet.contains("DSFB + heuristic") {
2511        return Err(Error::Message(format!(
2512            "canonical metric sheet {} is missing the DSFB + heuristic column",
2513            canonical_metric_sheet_path.display()
2514        )));
2515    }
2516    if !canonical_metric_sheet.contains(ROI_CONTRACT_STATEMENT) {
2517        return Err(Error::Message(format!(
2518            "canonical metric sheet {} is missing the ROI contract statement",
2519            canonical_metric_sheet_path.display()
2520        )));
2521    }
2522
2523    let aggregation_summary = fs::read_to_string(aggregation_summary_path)?;
2524    if frames.len() < ROI_AGGREGATION_MIN_CAPTURES
2525        && !aggregation_summary.contains("blocked by missing real captures")
2526    {
2527        return Err(Error::Message(format!(
2528            "aggregation summary {} emitted distribution output without enough real captures",
2529            aggregation_summary_path.display()
2530        )));
2531    }
2532
2533    for relative_path in [
2534        "figures/trust_histogram.svg",
2535        "figures/trust_vs_error.svg",
2536        "figures/trust_conditioned_error_map.png",
2537    ] {
2538        let artifact_path = run_dir.join(relative_path);
2539        if !artifact_path.exists() {
2540            return Err(Error::Message(format!(
2541                "required trust artifact is missing: {}",
2542                artifact_path.display()
2543            )));
2544        }
2545    }
2546    if frames.len() >= 2 {
2547        for relative_path in [
2548            "figures/trust_temporal_trajectory.svg",
2549            "figures/trust_temporal_trajectory.json",
2550        ] {
2551            let artifact_path = run_dir.join(relative_path);
2552            if !artifact_path.exists() {
2553                return Err(Error::Message(format!(
2554                    "required temporal trust artifact is missing: {}",
2555                    artifact_path.display()
2556                )));
2557            }
2558        }
2559    }
2560
2561    for frame in frames {
2562        if !frame.roi_mask_path.exists() {
2563            return Err(Error::Message(format!(
2564                "required ROI mask artifact is missing: {}",
2565                frame.roi_mask_path.display()
2566            )));
2567        }
2568    }
2569
2570    Ok(())
2571}
2572
2573fn winner_label(baseline: f32, strong: f32, dsfb: f32, hybrid: Option<f32>) -> &'static str {
2574    let best = hybrid
2575        .map(|hybrid| baseline.min(strong).min(dsfb).min(hybrid))
2576        .unwrap_or_else(|| baseline.min(strong).min(dsfb));
2577    if (best - baseline).abs() <= 1e-6 {
2578        "Baseline"
2579    } else if (best - strong).abs() <= 1e-6 {
2580        "Strong heuristic"
2581    } else if hybrid.is_some_and(|value| (best - value).abs() <= 1e-6) {
2582        "DSFB + heuristic"
2583    } else {
2584        "DSFB"
2585    }
2586}
2587
2588fn mean_and_std(values: &[f32]) -> (f32, f32) {
2589    if values.is_empty() {
2590        return (0.0, 0.0);
2591    }
2592    let mean = values.iter().copied().sum::<f32>() / values.len() as f32;
2593    let variance = values
2594        .iter()
2595        .map(|value| {
2596            let delta = *value - mean;
2597            delta * delta
2598        })
2599        .sum::<f32>()
2600        / values.len() as f32;
2601    (mean, variance.sqrt())
2602}
2603
2604fn git_commit_hash() -> Option<String> {
2605    Command::new("git")
2606        .arg("rev-parse")
2607        .arg("HEAD")
2608        .output()
2609        .ok()
2610        .filter(|output| output.status.success())
2611        .and_then(|output| String::from_utf8(output.stdout).ok())
2612        .map(|value| value.trim().to_string())
2613        .filter(|value| !value.is_empty())
2614}
2615
2616fn read_json<T: for<'de> Deserialize<'de>>(path: &Path) -> Result<T> {
2617    let text = fs::read_to_string(path)?;
2618    Ok(serde_json::from_str(&text)?)
2619}
2620
2621fn write_json<T: Serialize>(path: &Path, value: &T) -> Result<()> {
2622    fs::write(path, serde_json::to_string_pretty(value)?)?;
2623    Ok(())
2624}
2625
2626#[derive(Clone, Debug, Serialize)]
2627struct ScalarJson {
2628    width: usize,
2629    height: usize,
2630    data: Vec<f32>,
2631}
2632
2633#[derive(Clone, Debug, Serialize)]
2634struct Vec2Json {
2635    width: usize,
2636    height: usize,
2637    data: Vec<[f32; 2]>,
2638}
2639
2640#[derive(Clone, Debug, Serialize)]
2641struct Vec3Json {
2642    width: usize,
2643    height: usize,
2644    data: Vec<[f32; 3]>,
2645}
2646
2647#[derive(Clone, Debug, Serialize)]
2648struct BoolJson {
2649    width: usize,
2650    height: usize,
2651    data: Vec<bool>,
2652}
2653
2654fn heatmap_blue(value: f32) -> [u8; 4] {
2655    let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
2656    [v / 4, v / 2, 255, 255]
2657}
2658
2659fn heatmap_orange(value: f32) -> [u8; 4] {
2660    let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
2661    [255, v, 32, 255]
2662}
2663
2664fn heatmap_red(value: f32) -> [u8; 4] {
2665    let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
2666    [255, 24, v / 2, 255]
2667}
2668
2669fn heatmap_residual(value: f32) -> [u8; 4] {
2670    let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
2671    [255, v / 3, 16, 255]
2672}