Skip to main content

dsfb_computer_graphics/
external_validation.rs

1use std::fmt::Write as _;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::Command;
5
6use serde::{Deserialize, Serialize};
7
8use crate::config::DemoConfig;
9use crate::error::{Error, Result};
10use crate::external::{
11    load_external_capture_bundle, run_external_import_from_manifest, ExternalCaptureBundle,
12    ExternalHandoffMetrics, ExternalLoadedCapture, OwnedHostTemporalInputs,
13    NO_REAL_EXTERNAL_DATA_PROVIDED,
14};
15use crate::frame::{
16    mean_abs_error, mean_abs_error_over_mask, save_scalar_field_png, Color, ImageFrame, ScalarField,
17};
18use crate::gpu::try_execute_host_minimum_kernel;
19use crate::host::{
20    default_host_realistic_profile, supervise_temporal_reuse,
21};
22use crate::parameters::SmoothstepThreshold;
23use crate::report::EXPERIMENT_SENTENCE;
24use crate::sampling::{
25    build_count_field, combine_fields, gradient_field, guided_allocation, invert_trust,
26    local_contrast_field, mean_count_over_mask, AllocationPolicyId, BudgetCurve, BudgetCurvePoint,
27    DemoBPolicyMetrics,
28};
29use crate::scene::{MotionVector, Normal3};
30pub const ROI_CONTRACT_ALPHA: f32 = 0.15;
31pub const ROI_CONTRACT_BASELINE_METHOD_ID: &str = "fixed_alpha";
32pub const ROI_CONTRACT_SOURCE: &str = "fixed_alpha_local_contrast_0p15";
33pub const ROI_CONTRACT_STATEMENT: &str =
34    "ROI is defined as pixels where baseline error exceeds 15% of local contrast. The mask is computed once from the baseline and held fixed across all methods. DSFB does not influence ROI selection.";
35pub const CANONICAL_HEADLINE_STATEMENT: &str =
36    "DSFB improves strong temporal heuristics via structural supervision.";
37pub const PURE_DSFB_LIMITATION_STATEMENT: &str =
38    "DSFB alone does not outperform strong heuristic baselines in the current evaluation.";
39pub const ROI_HONESTY_STATEMENT: &str =
40    "The ROI definition captures approximately 50% of the frame under the fixed baseline-relative threshold, making the metric closer to a global structural error measure than a sparse artifact mask.";
41pub const ROI_AGGREGATION_MIN_CAPTURES: usize = 3;
42
43#[derive(Clone, Debug)]
44pub struct ExternalValidationArtifacts {
45    pub replay_report_path: PathBuf,
46    pub handoff_report_path: PathBuf,
47    pub validation_report_path: PathBuf,
48    pub gpu_report_path: PathBuf,
49    pub gpu_metrics_path: PathBuf,
50    pub demo_a_report_path: PathBuf,
51    pub demo_b_report_path: PathBuf,
52    pub demo_b_metrics_path: PathBuf,
53    pub scaling_report_path: PathBuf,
54    pub scaling_metrics_path: PathBuf,
55    pub memory_bandwidth_report_path: PathBuf,
56    pub integration_scaling_report_path: PathBuf,
57    pub resolved_manifest_path: PathBuf,
58    pub figures_dir: PathBuf,
59    pub handoff_metrics: ExternalHandoffMetrics,
60}
61
62#[derive(Clone, Debug, Serialize, Deserialize)]
63pub struct ExternalGpuCaptureMetrics {
64    pub capture_label: String,
65    pub measured_gpu: bool,
66    pub adapter: Option<String>,
67    pub backend: Option<String>,
68    pub resolution: [usize; 2],
69    pub kernel: String,
70    pub total_ms: Option<f64>,
71    pub dispatch_ms: Option<f64>,
72    pub readback_ms: Option<f64>,
73    pub mean_abs_trust_delta_vs_cpu: Option<f32>,
74    pub mean_abs_alpha_delta_vs_cpu: Option<f32>,
75    pub mean_abs_intervention_delta_vs_cpu: Option<f32>,
76    pub notes: Vec<String>,
77}
78
79#[derive(Clone, Debug, Serialize, Deserialize)]
80pub struct ExternalGpuMetrics {
81    pub measurement_kind: String,
82    pub measured_gpu: bool,
83    pub actual_real_external_data: bool,
84    pub kernel: String,
85    pub captures: Vec<ExternalGpuCaptureMetrics>,
86    pub notes: Vec<String>,
87}
88
89#[derive(Clone, Debug, Serialize, Deserialize)]
90pub struct ExternalDemoAMethodMetrics {
91    pub method_id: String,
92    pub label: String,
93    pub metric_source: String,
94    pub overall_mae: f32,
95    pub roi_mae: f32,
96    pub non_roi_mae: f32,
97    pub max_error: f32,
98    pub temporal_error_accumulation: f32,
99    pub intervention_rate: f32,
100}
101
102#[derive(Clone, Debug, Serialize, Deserialize)]
103pub struct ExternalDemoACaptureMetrics {
104    pub capture_label: String,
105    pub roi_source: String,
106    pub roi_pixels: usize,
107    pub total_pixels: usize,
108    pub roi_coverage: f32,
109    pub roi_statement: String,
110    pub baseline_method_id: String,
111    pub reference_source: String,
112    pub ground_truth_available: bool,
113    pub metric_source: String,
114    pub methods: Vec<ExternalDemoAMethodMetrics>,
115}
116
117#[derive(Clone, Debug, Serialize, Deserialize)]
118pub struct ExternalDemoAMetrics {
119    pub real_external_data_provided: bool,
120    pub no_real_external_data_provided: bool,
121    pub captures: Vec<ExternalDemoACaptureMetrics>,
122    pub notes: Vec<String>,
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize)]
126pub struct ExternalDemoBCaptureMetrics {
127    pub capture_label: String,
128    pub regime: String,
129    pub metric_source: String,
130    pub roi_source: String,
131    pub roi_pixels: usize,
132    pub total_pixels: usize,
133    pub roi_coverage: f32,
134    pub roi_statement: String,
135    pub baseline_method_id: String,
136    pub reference_source: String,
137    pub ground_truth_available: bool,
138    pub budget_total_samples: usize,
139    pub fixed_budget_equal: bool,
140    pub policies: Vec<DemoBPolicyMetrics>,
141    pub budget_curves: Vec<BudgetCurve>,
142}
143
144#[derive(Clone, Debug, Serialize, Deserialize)]
145pub struct ExternalDemoBMetrics {
146    pub real_external_data_provided: bool,
147    pub no_real_external_data_provided: bool,
148    pub captures: Vec<ExternalDemoBCaptureMetrics>,
149    pub notes: Vec<String>,
150}
151
152#[derive(Clone, Debug, Serialize, Deserialize)]
153pub struct ExternalScalingEntry {
154    pub label: String,
155    pub source: String,
156    pub width: usize,
157    pub height: usize,
158    pub attempted: bool,
159    pub measured_gpu: bool,
160    pub total_ms: Option<f64>,
161    pub dispatch_ms: Option<f64>,
162    pub readback_ms: Option<f64>,
163    pub ms_per_megapixel: Option<f64>,
164    pub scaling_ratio_vs_native: Option<f64>,
165    pub pixel_ratio_vs_native: f64,
166    pub approximately_linear: Option<bool>,
167    pub unavailable_reason: Option<String>,
168}
169
170#[derive(Clone, Debug, Serialize, Deserialize)]
171pub struct ExternalCoverageSummary {
172    pub realism_stress_case: bool,
173    pub larger_roi_case: bool,
174    pub mixed_regime_case: bool,
175    pub coverage_status: String,
176    pub missing: Vec<String>,
177}
178
179#[derive(Clone, Debug, Serialize, Deserialize)]
180pub struct ExternalScalingMetrics {
181    pub measurement_kind: String,
182    pub kernel: String,
183    pub native_capture_label: String,
184    pub native_resolution: [usize; 2],
185    pub attempted_1080p: bool,
186    pub attempted_4k: bool,
187    pub entries: Vec<ExternalScalingEntry>,
188    pub coverage: ExternalCoverageSummary,
189    pub notes: Vec<String>,
190}
191
192#[derive(Clone, Debug, Serialize)]
193struct TemporalTrustTrajectoryPoint {
194    capture_label: String,
195    frame_index: usize,
196    mean_trust: f32,
197    mean_alpha: f32,
198    intervention_rate: f32,
199    roi_coverage: f32,
200    dsfb_roi_mae: f32,
201    hybrid_roi_mae: f32,
202}
203
204#[derive(Clone, Debug, Serialize)]
205struct TemporalTrustTrajectoryReport {
206    onset_capture_label: String,
207    peak_roi_capture_label: String,
208    recovery_capture_label: String,
209    points: Vec<TemporalTrustTrajectoryPoint>,
210}
211
212pub fn run_external_validation_bundle(
213    config: &DemoConfig,
214    manifest_path: &Path,
215    output_dir: &Path,
216) -> Result<ExternalValidationArtifacts> {
217    fs::create_dir_all(output_dir)?;
218
219    let import = run_external_import_from_manifest(config, manifest_path, output_dir)?;
220    let replay_metrics_path = output_dir.join("replay_metrics.json");
221    fs::write(
222        &replay_metrics_path,
223        serde_json::to_string_pretty(&import.metrics)?,
224    )?;
225    let bundle = load_external_capture_bundle(config, manifest_path, output_dir)?;
226    let figures_dir = output_dir.join("figures");
227    fs::create_dir_all(&figures_dir)?;
228
229    let gpu_metrics = run_external_gpu_metrics_safe(config, manifest_path, output_dir, &bundle)?;
230    let gpu_metrics_path = output_dir.join("gpu_execution_metrics.json");
231    fs::write(
232        &gpu_metrics_path,
233        serde_json::to_string_pretty(&gpu_metrics)?,
234    )?;
235    let gpu_report_path = output_dir.join("gpu_execution_report.md");
236    write_gpu_external_report(&gpu_report_path, &gpu_metrics, &bundle)?;
237    fs::copy(
238        &gpu_metrics_path,
239        output_dir.join("gpu_external_metrics.json"),
240    )?;
241    fs::copy(&gpu_report_path, output_dir.join("gpu_external_report.md"))?;
242
243    let demo_a_metrics = run_demo_a_external_metrics(config, &bundle, &figures_dir)?;
244    let demo_a_metrics_path = output_dir.join("demo_a_external_metrics.json");
245    fs::write(
246        &demo_a_metrics_path,
247        serde_json::to_string_pretty(&demo_a_metrics)?,
248    )?;
249    let demo_a_report_path = output_dir.join("demo_a_external_report.md");
250    write_demo_a_external_report(&demo_a_report_path, &demo_a_metrics, &bundle)?;
251
252    let demo_b_metrics = run_demo_b_external_metrics(config, &bundle, &figures_dir)?;
253    let demo_b_metrics_path = output_dir.join("demo_b_external_metrics.json");
254    fs::write(
255        &demo_b_metrics_path,
256        serde_json::to_string_pretty(&demo_b_metrics)?,
257    )?;
258    let demo_b_report_path = output_dir.join("demo_b_external_report.md");
259    write_demo_b_external_report(&demo_b_report_path, &demo_b_metrics, &bundle)?;
260
261    let scaling_metrics = run_external_scaling_study(
262        config,
263        manifest_path,
264        output_dir,
265        &bundle,
266        &gpu_metrics,
267        &demo_a_metrics,
268        &demo_b_metrics,
269    )?;
270    let scaling_metrics_path = output_dir.join("scaling_metrics.json");
271    fs::write(
272        &scaling_metrics_path,
273        serde_json::to_string_pretty(&scaling_metrics)?,
274    )?;
275    let scaling_report_path = output_dir.join("scaling_report.md");
276    write_external_scaling_report(&scaling_report_path, &scaling_metrics, &bundle)?;
277    let memory_bandwidth_report_path = output_dir.join("memory_bandwidth_report.md");
278    write_memory_bandwidth_report(&memory_bandwidth_report_path, &scaling_metrics)?;
279    let integration_scaling_report_path = output_dir.join("integration_scaling_report.md");
280    write_integration_scaling_report(&integration_scaling_report_path, &scaling_metrics, &bundle)?;
281
282    let validation_report_path = output_dir.join("external_validation_report.md");
283    write_external_validation_report(
284        &validation_report_path,
285        &bundle,
286        &import.metrics,
287        &gpu_metrics,
288        &demo_a_metrics,
289        &demo_b_metrics,
290        &scaling_metrics,
291    )?;
292    copy_representative_figure_aliases(output_dir, &figures_dir)?;
293
294    Ok(ExternalValidationArtifacts {
295        replay_report_path: import.report_path,
296        handoff_report_path: output_dir.join("external_handoff_report.md"),
297        validation_report_path,
298        gpu_report_path,
299        gpu_metrics_path,
300        demo_a_report_path,
301        demo_b_report_path,
302        demo_b_metrics_path,
303        scaling_report_path,
304        scaling_metrics_path,
305        memory_bandwidth_report_path,
306        integration_scaling_report_path,
307        resolved_manifest_path: import.resolved_manifest_path,
308        figures_dir,
309        handoff_metrics: import.metrics,
310    })
311}
312
313pub fn probe_external_gpu_only(
314    config: &DemoConfig,
315    manifest_path: &Path,
316    output_dir: &Path,
317    capture_label: Option<&str>,
318    scaled_resolution: Option<(usize, usize)>,
319) -> Result<PathBuf> {
320    fs::create_dir_all(output_dir)?;
321    let bundle = load_external_capture_bundle(config, manifest_path, output_dir)?;
322    let bundle = if capture_label.is_some() || scaled_resolution.is_some() {
323        build_gpu_probe_bundle(bundle, capture_label, scaled_resolution)?
324    } else {
325        bundle
326    };
327    let metrics = run_external_gpu_metrics(config, &bundle)?;
328    let path = output_dir.join("gpu_probe_metrics.json");
329    fs::write(&path, serde_json::to_string_pretty(&metrics)?)?;
330    Ok(path)
331}
332
333fn build_gpu_probe_bundle(
334    bundle: ExternalCaptureBundle,
335    capture_label: Option<&str>,
336    scaled_resolution: Option<(usize, usize)>,
337) -> Result<ExternalCaptureBundle> {
338    let capture = if let Some(label) = capture_label {
339        bundle
340            .captures
341            .iter()
342            .find(|capture| capture.label == label)
343            .cloned()
344            .ok_or_else(|| Error::Message(format!("capture `{label}` was missing from the bundle")))?
345    } else {
346        bundle
347            .captures
348            .first()
349            .cloned()
350            .ok_or_else(|| Error::Message("external capture bundle had no captures".to_string()))?
351    };
352    let capture = if let Some((width, height)) = scaled_resolution {
353        scale_external_capture(&capture, width, height)?
354    } else {
355        capture
356    };
357    Ok(ExternalCaptureBundle {
358        manifest: bundle.manifest,
359        captures: vec![capture],
360        real_external_data_provided: bundle.real_external_data_provided,
361        no_real_external_data_provided: bundle.no_real_external_data_provided,
362    })
363}
364
365fn run_external_gpu_metrics(
366    config: &DemoConfig,
367    bundle: &ExternalCaptureBundle,
368) -> Result<ExternalGpuMetrics> {
369    let profile =
370        default_host_realistic_profile(config.dsfb_alpha_range.min, config.dsfb_alpha_range.max);
371    let mut captures = Vec::with_capacity(bundle.captures.len());
372    let mut measured_gpu = false;
373
374    for capture in &bundle.captures {
375        let cpu = supervise_temporal_reuse(&capture.inputs.borrow(), &profile);
376        match try_execute_host_minimum_kernel(&capture.inputs, profile.parameters)? {
377            Some(gpu) => {
378                measured_gpu = true;
379                captures.push(ExternalGpuCaptureMetrics {
380                    capture_label: capture.label.clone(),
381                    measured_gpu: true,
382                    adapter: Some(gpu.adapter_name),
383                    backend: Some(gpu.backend),
384                    resolution: [capture.inputs.width(), capture.inputs.height()],
385                    kernel: "dsfb_host_minimum".to_string(),
386                    total_ms: Some(gpu.total_ms),
387                    dispatch_ms: Some(gpu.dispatch_ms),
388                    readback_ms: Some(gpu.readback_ms),
389                    mean_abs_trust_delta_vs_cpu: Some(mean_abs_delta(cpu.trust.values(), &gpu.trust)),
390                    mean_abs_alpha_delta_vs_cpu: Some(mean_abs_delta(cpu.alpha.values(), &gpu.alpha)),
391                    mean_abs_intervention_delta_vs_cpu: Some(mean_abs_delta(
392                        cpu.intervention.values(),
393                        &gpu.intervention,
394                    )),
395                    notes: vec![
396                        "Measured on imported external buffers using the same minimum host-realistic kernel as the synthetic GPU study.".to_string(),
397                    ],
398                });
399            }
400            None => {
401                captures.push(ExternalGpuCaptureMetrics {
402                    capture_label: capture.label.clone(),
403                    measured_gpu: false,
404                    adapter: None,
405                    backend: None,
406                    resolution: [capture.inputs.width(), capture.inputs.height()],
407                    kernel: "dsfb_host_minimum".to_string(),
408                    total_ms: None,
409                    dispatch_ms: None,
410                    readback_ms: None,
411                    mean_abs_trust_delta_vs_cpu: None,
412                    mean_abs_alpha_delta_vs_cpu: None,
413                    mean_abs_intervention_delta_vs_cpu: None,
414                    notes: vec![
415                        "The GPU path is implemented, but no usable adapter was available in the current environment for this imported capture.".to_string(),
416                    ],
417                });
418            }
419        }
420    }
421
422    Ok(ExternalGpuMetrics {
423        measurement_kind: if measured_gpu {
424            "measured_gpu".to_string()
425        } else {
426            "gpu_path_unmeasured".to_string()
427        },
428        measured_gpu,
429        actual_real_external_data: bundle.real_external_data_provided,
430        kernel: "dsfb_host_minimum".to_string(),
431        captures,
432        notes: vec![
433            "This report covers imported external buffers rather than the synthetic internal suite.".to_string(),
434            if bundle.no_real_external_data_provided {
435                NO_REAL_EXTERNAL_DATA_PROVIDED.to_string()
436            } else {
437                "Real external data was supplied through the file schema.".to_string()
438            },
439        ],
440    })
441}
442
443fn run_external_gpu_metrics_safe(
444    config: &DemoConfig,
445    manifest_path: &Path,
446    output_dir: &Path,
447    bundle: &ExternalCaptureBundle,
448) -> Result<ExternalGpuMetrics> {
449    if let Some(metrics) = try_gpu_subprocess_probe(manifest_path, output_dir, bundle)? {
450        return Ok(metrics);
451    }
452    run_external_gpu_metrics(config, bundle)
453}
454
455fn try_gpu_subprocess_probe(
456    manifest_path: &Path,
457    output_dir: &Path,
458    bundle: &ExternalCaptureBundle,
459) -> Result<Option<ExternalGpuMetrics>> {
460    let Some(executable) = gpu_probe_executable()? else {
461        return Ok(None);
462    };
463
464    let probe_dir = output_dir.join("gpu_probe");
465    fs::create_dir_all(&probe_dir)?;
466    let status = Command::new(&executable)
467        .arg("probe-external-gpu")
468        .arg("--manifest")
469        .arg(manifest_path)
470        .arg("--output")
471        .arg(&probe_dir)
472        .status()?;
473    let metrics_path = probe_dir.join("gpu_probe_metrics.json");
474    if status.success() && metrics_path.exists() {
475        let metrics: ExternalGpuMetrics = serde_json::from_str(&fs::read_to_string(metrics_path)?)?;
476        return Ok(Some(metrics));
477    }
478
479    let crash_note = if let Some(code) = status.code() {
480        format!("GPU subprocess attempt exited with status {code}")
481    } else {
482        "GPU subprocess attempt terminated by signal".to_string()
483    };
484    Ok(Some(unmeasured_gpu_metrics(bundle, vec![
485        "GPU execution was attempted in a subprocess because some drivers abort the process on shader JIT failure.".to_string(),
486        crash_note,
487    ])))
488}
489
490fn unmeasured_gpu_metrics(bundle: &ExternalCaptureBundle, notes: Vec<String>) -> ExternalGpuMetrics {
491    ExternalGpuMetrics {
492        measurement_kind: "gpu_probe_failed".to_string(),
493        measured_gpu: false,
494        actual_real_external_data: bundle.real_external_data_provided,
495        kernel: "dsfb_host_minimum".to_string(),
496        captures: bundle
497            .captures
498            .iter()
499            .map(|capture| ExternalGpuCaptureMetrics {
500                capture_label: capture.label.clone(),
501                measured_gpu: false,
502                adapter: None,
503                backend: None,
504                resolution: [capture.inputs.width(), capture.inputs.height()],
505                kernel: "dsfb_host_minimum".to_string(),
506                total_ms: None,
507                dispatch_ms: None,
508                readback_ms: None,
509                mean_abs_trust_delta_vs_cpu: None,
510                mean_abs_alpha_delta_vs_cpu: None,
511                mean_abs_intervention_delta_vs_cpu: None,
512                notes: vec![
513                    "The GPU path was attempted, but the driver/runtime could not complete the shader execution path safely in-process.".to_string(),
514                ],
515            })
516            .collect(),
517        notes,
518    }
519}
520
521fn run_demo_a_external_metrics(
522    config: &DemoConfig,
523    bundle: &ExternalCaptureBundle,
524    figures_dir: &Path,
525) -> Result<ExternalDemoAMetrics> {
526    let profile =
527        default_host_realistic_profile(config.dsfb_alpha_range.min, config.dsfb_alpha_range.max);
528    let mut captures = Vec::with_capacity(bundle.captures.len());
529    let mut trajectory_points = Vec::with_capacity(bundle.captures.len());
530
531    for (capture_index, capture) in bundle.captures.iter().enumerate() {
532        let outputs = supervise_temporal_reuse(&capture.inputs.borrow(), &profile);
533        let fixed_alpha_field = constant_field(
534            capture.inputs.width(),
535            capture.inputs.height(),
536            config.baseline.fixed_alpha,
537        );
538        let fixed_resolved = resolve_with_alpha(
539            &capture.inputs.reprojected_history,
540            &capture.inputs.current_color,
541            &fixed_alpha_field,
542        );
543        let (reference_frame, reference_source, metric_source) =
544            capture_reference_frame_and_metric_source(capture);
545        let (roi_mask, roi_source, roi_coverage) =
546            roi_mask_for_capture(capture, &fixed_resolved, reference_frame);
547        let dsfb_resolved = resolve_with_alpha(
548            &capture.inputs.reprojected_history,
549            &capture.inputs.current_color,
550            &outputs.alpha,
551        );
552        let (strong_resolved, strong_alpha, strong_response) =
553            run_external_strong_heuristic(config, capture);
554        let (hybrid_resolved, _hybrid_alpha, hybrid_response) =
555            run_external_dsfb_plus_strong_heuristic(
556                capture,
557                &outputs.alpha,
558                &outputs.intervention,
559                &strong_alpha,
560                &strong_response,
561            );
562        let fixed_response = ScalarField::new(capture.inputs.width(), capture.inputs.height());
563
564        let methods = vec![
565            build_demo_a_method_metrics(
566                "fixed_alpha",
567                "Fixed alpha baseline",
568                &fixed_resolved,
569                reference_frame,
570                metric_source,
571                &roi_mask,
572                &fixed_response,
573            ),
574            build_demo_a_method_metrics(
575                "strong_heuristic",
576                "Strong heuristic clamp",
577                &strong_resolved,
578                reference_frame,
579                metric_source,
580                &roi_mask,
581                &strong_response,
582            ),
583            build_demo_a_method_metrics(
584                "dsfb_host_minimum",
585                "DSFB host minimum",
586                &dsfb_resolved,
587                reference_frame,
588                metric_source,
589                &roi_mask,
590                &outputs.intervention,
591            ),
592            build_demo_a_method_metrics(
593                "dsfb_plus_strong_heuristic",
594                "DSFB + strong heuristic",
595                &hybrid_resolved,
596                reference_frame,
597                metric_source,
598                &roi_mask,
599                &hybrid_response,
600            ),
601        ];
602        let dsfb_roi_mae = methods
603            .iter()
604            .find(|method| method.method_id == "dsfb_host_minimum")
605            .map(|method| method.roi_mae)
606            .ok_or_else(|| {
607                Error::Message(format!(
608                    "capture `{}` did not retain dsfb_host_minimum metrics",
609                    capture.label
610                ))
611            })?;
612        let hybrid_roi_mae = methods
613            .iter()
614            .find(|method| method.method_id == "dsfb_plus_strong_heuristic")
615            .map(|method| method.roi_mae)
616            .ok_or_else(|| {
617                Error::Message(format!(
618                    "capture `{}` did not retain dsfb_plus_strong_heuristic metrics",
619                    capture.label
620                ))
621            })?;
622
623        if capture_index == 0 {
624            capture
625                .inputs
626                .current_color
627                .save_png(&figures_dir.join("current_color.png"))?;
628            capture
629                .inputs
630                .reprojected_history
631                .save_png(&figures_dir.join("reprojected_history.png"))?;
632            fixed_resolved.save_png(&figures_dir.join("demo_a_fixed_alpha.png"))?;
633            strong_resolved.save_png(&figures_dir.join("demo_a_strong_heuristic.png"))?;
634            dsfb_resolved.save_png(&figures_dir.join("demo_a_dsfb.png"))?;
635            hybrid_resolved.save_png(
636                &figures_dir.join("demo_a_dsfb_plus_strong_heuristic.png"),
637            )?;
638            save_scalar_field_png(
639                &outputs.trust,
640                &figures_dir.join("trust_map.png"),
641                heatmap_blue,
642            )?;
643            save_scalar_field_png(
644                &outputs.alpha,
645                &figures_dir.join("alpha_map.png"),
646                heatmap_orange,
647            )?;
648            save_scalar_field_png(
649                &outputs.intervention,
650                &figures_dir.join("intervention_map.png"),
651                heatmap_red,
652            )?;
653            overlay_roi_mask(&capture.inputs.current_color, &roi_mask)
654                .save_png(&figures_dir.join("roi_overlay.png"))?;
655            save_scalar_field_png(
656                &strong_alpha,
657                &figures_dir.join("strong_alpha_map.png"),
658                heatmap_orange,
659            )?;
660            let trust_error_field = absolute_error_field(&dsfb_resolved, reference_frame);
661            write_trust_histogram_figure(
662                &outputs.trust,
663                &figures_dir.join("trust_histogram.svg"),
664            )?;
665            write_trust_vs_error_figure(
666                &outputs.trust,
667                &trust_error_field,
668                metric_source,
669                &figures_dir.join("trust_vs_error.svg"),
670            )?;
671            save_trust_conditioned_error_map(
672                &outputs.trust,
673                &trust_error_field,
674                &figures_dir.join("trust_conditioned_error_map.png"),
675            )?;
676        }
677
678        trajectory_points.push(TemporalTrustTrajectoryPoint {
679            capture_label: capture.label.clone(),
680            frame_index: capture.metadata.frame_index,
681            mean_trust: outputs.trust.mean(),
682            mean_alpha: outputs.alpha.mean(),
683            intervention_rate: outputs.intervention.mean(),
684            roi_coverage,
685            dsfb_roi_mae,
686            hybrid_roi_mae,
687        });
688
689        captures.push(ExternalDemoACaptureMetrics {
690            capture_label: capture.label.clone(),
691            roi_source,
692            roi_pixels: roi_mask.iter().filter(|value| **value).count(),
693            total_pixels: capture.inputs.width() * capture.inputs.height(),
694            roi_coverage,
695            roi_statement: ROI_CONTRACT_STATEMENT.to_string(),
696            baseline_method_id: ROI_CONTRACT_BASELINE_METHOD_ID.to_string(),
697            reference_source: reference_source.to_string(),
698            ground_truth_available: capture.reference.is_some(),
699            metric_source: metric_source.to_string(),
700            methods,
701        });
702    }
703
704    let all_have_reference = captures.iter().all(|capture| capture.ground_truth_available);
705    let reference_note = if all_have_reference {
706        "The current bundle measures against exported `reference_color` on every capture; that reference is a higher-resolution Unreal proxy rather than a path-traced ground truth.".to_string()
707    } else {
708        "If no optional reference frame is supplied, the current frame is used as the explicit proxy reference for ROI and full-frame error.".to_string()
709    };
710
711    let metrics = ExternalDemoAMetrics {
712        real_external_data_provided: bundle.real_external_data_provided,
713        no_real_external_data_provided: bundle.no_real_external_data_provided,
714        captures,
715        notes: vec![
716            "Demo A external replay uses the same host-minimum supervisory logic as the internal suite.".to_string(),
717            ROI_CONTRACT_STATEMENT.to_string(),
718            reference_note,
719        ],
720    };
721
722    if trajectory_points.len() >= 2 {
723        write_temporal_trust_trajectory_outputs(&trajectory_points, figures_dir)?;
724    }
725
726    Ok(metrics)
727}
728
729fn run_demo_b_external_metrics(
730    config: &DemoConfig,
731    bundle: &ExternalCaptureBundle,
732    figures_dir: &Path,
733) -> Result<ExternalDemoBMetrics> {
734    let profile =
735        default_host_realistic_profile(config.dsfb_alpha_range.min, config.dsfb_alpha_range.max);
736    let mut captures = Vec::with_capacity(bundle.captures.len());
737
738    for (capture_index, capture) in bundle.captures.iter().enumerate() {
739        let outputs = supervise_temporal_reuse(&capture.inputs.borrow(), &profile);
740        let fixed_alpha_field = constant_field(
741            capture.inputs.width(),
742            capture.inputs.height(),
743            config.baseline.fixed_alpha,
744        );
745        let fixed_resolved = resolve_with_alpha(
746            &capture.inputs.reprojected_history,
747            &capture.inputs.current_color,
748            &fixed_alpha_field,
749        );
750        let (reference_frame, reference_source, _) = capture_reference_frame_and_metric_source(capture);
751        let (roi_mask, roi_source, roi_coverage) =
752            roi_mask_for_capture(capture, &fixed_resolved, reference_frame);
753        let width = capture.inputs.width();
754        let height = capture.inputs.height();
755        let total_pixels = width * height;
756        let total_samples = config.demo_b_uniform_spp * total_pixels;
757        let variance_field = capture
758            .variance
759            .clone()
760            .map(normalize_field)
761            .unwrap_or_else(|| {
762                temporal_variance_proxy(
763                    &capture.inputs.current_color,
764                    &capture.inputs.reprojected_history,
765                )
766            });
767        let gradient = gradient_field(&capture.inputs.current_color);
768        let contrast = local_contrast_field(&capture.inputs.current_color);
769        let imported_trust = invert_trust(&outputs.trust);
770        let combined = combine_fields(
771            &[
772                (&gradient, 0.30),
773                (&contrast, 0.25),
774                (&variance_field, 0.45),
775            ],
776            width,
777            height,
778        );
779        let hybrid = combine_fields(
780            &[(&imported_trust, 0.55), (&variance_field, 0.45)],
781            width,
782            height,
783        );
784        let base_error = combine_fields(
785            &[
786                (&gradient, 0.20),
787                (&contrast, 0.20),
788                (&variance_field, 0.35),
789                (&imported_trust, 0.25),
790            ],
791            width,
792            height,
793        );
794
795        let policies = vec![
796            (
797                AllocationPolicyId::Uniform,
798                "Uniform",
799                vec![config.demo_b_uniform_spp; total_pixels],
800            ),
801            (
802                AllocationPolicyId::EdgeGuided,
803                "Gradient magnitude",
804                guided_allocation(&gradient, total_samples, 1, config.demo_b_max_spp)?,
805            ),
806            (
807                AllocationPolicyId::ContrastGuided,
808                "Contrast-based",
809                guided_allocation(&contrast, total_samples, 1, config.demo_b_max_spp)?,
810            ),
811            (
812                AllocationPolicyId::VarianceGuided,
813                "Variance proxy",
814                guided_allocation(&variance_field, total_samples, 1, config.demo_b_max_spp)?,
815            ),
816            (
817                AllocationPolicyId::CombinedHeuristic,
818                "Combined heuristic",
819                guided_allocation(&combined, total_samples, 1, config.demo_b_max_spp)?,
820            ),
821            (
822                AllocationPolicyId::ImportedTrust,
823                "DSFB imported trust",
824                guided_allocation(&imported_trust, total_samples, 1, config.demo_b_max_spp)?,
825            ),
826            (
827                AllocationPolicyId::HybridTrustVariance,
828                "Hybrid trust + variance",
829                guided_allocation(&hybrid, total_samples, 1, config.demo_b_max_spp)?,
830            ),
831        ];
832
833        let fixed_budget_equal = policies
834            .iter()
835            .all(|(_, _, counts)| counts.iter().sum::<usize>() == total_samples);
836        let uniform_roi_mae =
837            predicted_error_over_mask(&base_error, &policies[0].2, Some(&roi_mask), false);
838        let regime = classify_external_demo_b_regime(&gradient, &variance_field, &contrast);
839
840        let mut policy_metrics = Vec::new();
841        let uniform_roi_mean_spp = mean_count_over_mask(&policies[0].2, &roi_mask);
842        let non_roi_mask = invert_mask(&roi_mask);
843        for (policy_index, (policy_id, label, counts)) in policies.iter().enumerate() {
844            let counts_field = build_count_field(counts, width, height);
845            if capture_index == 0 && policy_index < 4 {
846                save_scalar_field_png(
847                    &counts_field,
848                    &figures_dir.join(format!("demo_b_allocation_{}.png", policy_id.as_str())),
849                    heatmap_blue,
850                )?;
851            }
852            let overall_mae = predicted_error_over_mask(&base_error, counts, None, false);
853            let roi_mae = predicted_error_over_mask(&base_error, counts, Some(&roi_mask), false);
854            let non_roi_mae =
855                predicted_error_over_mask(&base_error, counts, Some(&non_roi_mask), false);
856            let overall_rmse = predicted_error_over_mask(&base_error, counts, None, true);
857            let roi_rmse = predicted_error_over_mask(&base_error, counts, Some(&roi_mask), true);
858            let non_roi_rmse =
859                predicted_error_over_mask(&base_error, counts, Some(&non_roi_mask), true);
860            let roi_mean_spp = mean_count_over_mask(counts, &roi_mask);
861            let non_roi_mean_spp = mean_count_over_mask(counts, &non_roi_mask);
862            let max_spp = counts.iter().copied().max().unwrap_or(0);
863            let allocation_concentration = counts_field.mean();
864            let extra_roi_samples_vs_uniform = roi_mean_spp - uniform_roi_mean_spp;
865            let roi_error_reduction_per_extra_roi_sample =
866                if extra_roi_samples_vs_uniform.abs() <= f32::EPSILON {
867                    0.0
868                } else {
869                    (uniform_roi_mae - roi_mae) / extra_roi_samples_vs_uniform.max(1e-6)
870                };
871            policy_metrics.push(DemoBPolicyMetrics {
872                policy_id: policy_id.as_str().to_string(),
873                label: label.to_string(),
874                total_samples,
875                overall_mae,
876                overall_rmse,
877                roi_mae,
878                roi_rmse,
879                non_roi_mae,
880                non_roi_rmse,
881                roi_mean_spp,
882                non_roi_mean_spp,
883                max_spp,
884                allocation_concentration,
885                extra_roi_samples_vs_uniform,
886                roi_error_reduction_per_extra_roi_sample,
887            });
888        }
889
890        let mut budget_curves = Vec::new();
891        for (policy_id, field) in [
892            (AllocationPolicyId::Uniform, None),
893            (AllocationPolicyId::CombinedHeuristic, Some(&combined)),
894            (AllocationPolicyId::ImportedTrust, Some(&imported_trust)),
895            (AllocationPolicyId::HybridTrustVariance, Some(&hybrid)),
896        ] {
897            let mut points = Vec::new();
898            for average_spp in [1usize, 2, 4, 8] {
899                let budget = average_spp * total_pixels;
900                let counts = if let Some(difficulty) = field {
901                    guided_allocation(difficulty, budget, 1, config.demo_b_max_spp)?
902                } else {
903                    vec![average_spp; total_pixels]
904                };
905                points.push(BudgetCurvePoint {
906                    average_spp: average_spp as f32,
907                    roi_mae: predicted_error_over_mask(
908                        &base_error,
909                        &counts,
910                        Some(&roi_mask),
911                        false,
912                    ),
913                });
914            }
915            budget_curves.push(BudgetCurve {
916                scenario_id: capture.label.clone(),
917                policy_id: policy_id.as_str().to_string(),
918                points,
919            });
920        }
921
922        captures.push(ExternalDemoBCaptureMetrics {
923            capture_label: capture.label.clone(),
924            regime,
925            metric_source: if capture.reference.is_some() {
926                "allocation_proxy_with_optional_reference".to_string()
927            } else {
928                "allocation_proxy_without_reference".to_string()
929            },
930            roi_source,
931            roi_pixels: roi_mask.iter().filter(|value| **value).count(),
932            total_pixels,
933            roi_coverage,
934            roi_statement: ROI_CONTRACT_STATEMENT.to_string(),
935            baseline_method_id: ROI_CONTRACT_BASELINE_METHOD_ID.to_string(),
936            reference_source: reference_source.to_string(),
937            ground_truth_available: capture.reference.is_some(),
938            budget_total_samples: total_samples,
939            fixed_budget_equal,
940            policies: policy_metrics,
941            budget_curves,
942        });
943    }
944
945    Ok(ExternalDemoBMetrics {
946        real_external_data_provided: bundle.real_external_data_provided,
947        no_real_external_data_provided: bundle.no_real_external_data_provided,
948        captures,
949        notes: vec![
950            "External Demo B is an allocation proxy, not a live renderer replay, because imported captures do not contain per-sample shading or multi-budget re-renders.".to_string(),
951            "The proxy still enforces identical total budgets across all policies and is intended to guide the next engine-side experiment rather than replace it.".to_string(),
952            ROI_CONTRACT_STATEMENT.to_string(),
953        ],
954    })
955}
956
957fn run_external_scaling_study(
958    _config: &DemoConfig,
959    manifest_path: &Path,
960    output_dir: &Path,
961    bundle: &ExternalCaptureBundle,
962    gpu: &ExternalGpuMetrics,
963    demo_a: &ExternalDemoAMetrics,
964    demo_b: &ExternalDemoBMetrics,
965) -> Result<ExternalScalingMetrics> {
966    let capture = bundle.captures.first().ok_or_else(|| {
967        Error::Message("external scaling study requires at least one capture".to_string())
968    })?;
969    let native_width = capture.inputs.width();
970    let native_height = capture.inputs.height();
971    let native_pixels = (native_width * native_height) as f64;
972    let coverage = coverage_summary(bundle, demo_a, demo_b);
973    let native_capture = gpu
974        .captures
975        .iter()
976        .find(|candidate| candidate.capture_label == capture.label)
977        .or_else(|| gpu.captures.first());
978    let native_total_ms = native_capture.and_then(|metrics| metrics.total_ms);
979    let mut entries = vec![ExternalScalingEntry {
980        label: "native_imported".to_string(),
981        source: "native_imported".to_string(),
982        width: native_width,
983        height: native_height,
984        attempted: true,
985        measured_gpu: native_capture.map(|metrics| metrics.measured_gpu).unwrap_or(false),
986        total_ms: native_capture.and_then(|metrics| metrics.total_ms),
987        dispatch_ms: native_capture.and_then(|metrics| metrics.dispatch_ms),
988        readback_ms: native_capture.and_then(|metrics| metrics.readback_ms),
989        ms_per_megapixel: native_capture
990            .and_then(|metrics| metrics.total_ms)
991            .map(|total_ms| total_ms / (native_pixels / 1_000_000.0).max(1e-6)),
992        scaling_ratio_vs_native: native_total_ms.map(|_| 1.0),
993        pixel_ratio_vs_native: 1.0,
994        approximately_linear: native_total_ms.map(|_| true),
995        unavailable_reason: native_capture
996            .filter(|metrics| !metrics.measured_gpu || metrics.total_ms.is_none())
997            .map(|metrics| metrics.notes.join(" ")),
998    }];
999
1000    let executable = gpu_probe_executable()?;
1001    for (label, width, height) in [("scaled_1080p", 1920usize, 1080usize), ("scaled_4k", 3840usize, 2160usize)] {
1002        entries.push(if let Some(executable) = &executable {
1003            probe_scaled_gpu_entry(
1004                executable,
1005                manifest_path,
1006                output_dir,
1007                &capture.label,
1008                label,
1009                width,
1010                height,
1011                native_pixels,
1012                native_total_ms,
1013            )?
1014        } else {
1015            unavailable_scaling_entry(
1016                label,
1017                width,
1018                height,
1019                native_pixels,
1020                false,
1021                "scaled probe requires the standalone dsfb-computer-graphics binary; library/test invocation cannot safely isolate GPU shader-JIT failures"
1022                    .to_string(),
1023            )
1024        });
1025    }
1026
1027    let measured_scaled = entries
1028        .iter()
1029        .skip(1)
1030        .any(|entry| entry.measured_gpu && entry.total_ms.is_some());
1031    let measurement_kind = if measured_scaled {
1032        "gpu_scaled_probe_measured".to_string()
1033    } else if entries.iter().any(|entry| entry.measured_gpu) {
1034        "native_gpu_only".to_string()
1035    } else {
1036        "gpu_path_unmeasured".to_string()
1037    };
1038
1039    Ok(ExternalScalingMetrics {
1040        measurement_kind,
1041        kernel: "dsfb_host_minimum".to_string(),
1042        native_capture_label: capture.label.clone(),
1043        native_resolution: [native_width, native_height],
1044        attempted_1080p: true,
1045        attempted_4k: true,
1046        entries,
1047        coverage,
1048        notes: if measured_scaled {
1049            vec![
1050                "Scaled GPU timings were measured in isolated subprocess probes to avoid driver-specific shader-JIT crashes from corrupting the canonical run.".to_string(),
1051                "The native imported timing reuses the same minimum-kernel path as gpu_execution_metrics.json.".to_string(),
1052            ]
1053        } else {
1054            vec![
1055                "Scaled GPU probes were attempted only when the standalone binary was available for isolated subprocess execution.".to_string(),
1056                "When a scaled row is unavailable, the run fails closed rather than guessing a scaling claim.".to_string(),
1057            ]
1058        },
1059    })
1060}
1061
1062fn gpu_probe_executable() -> Result<Option<PathBuf>> {
1063    let executable = std::env::current_exe()?;
1064    let executable_name = executable
1065        .file_name()
1066        .and_then(|name| name.to_str())
1067        .unwrap_or_default();
1068    if executable_name.contains("dsfb-computer-graphics") {
1069        Ok(Some(executable))
1070    } else {
1071        Ok(None)
1072    }
1073}
1074
1075fn probe_scaled_gpu_entry(
1076    executable: &Path,
1077    manifest_path: &Path,
1078    output_dir: &Path,
1079    capture_label: &str,
1080    label: &str,
1081    width: usize,
1082    height: usize,
1083    native_pixels: f64,
1084    native_total_ms: Option<f64>,
1085) -> Result<ExternalScalingEntry> {
1086    let pixel_ratio = (width * height) as f64 / native_pixels.max(1.0);
1087    let probe_dir = output_dir.join("scaling_gpu_probe").join(label);
1088    fs::create_dir_all(&probe_dir)?;
1089    let status = Command::new(executable)
1090        .arg("probe-external-gpu")
1091        .arg("--manifest")
1092        .arg(manifest_path)
1093        .arg("--output")
1094        .arg(&probe_dir)
1095        .arg("--capture-label")
1096        .arg(capture_label)
1097        .arg("--width")
1098        .arg(width.to_string())
1099        .arg("--height")
1100        .arg(height.to_string())
1101        .status()?;
1102    let metrics_path = probe_dir.join("gpu_probe_metrics.json");
1103    if status.success() && metrics_path.exists() {
1104        let metrics: ExternalGpuMetrics = serde_json::from_str(&fs::read_to_string(metrics_path)?)?;
1105        let capture = metrics.captures.first().ok_or_else(|| {
1106            Error::Message(format!(
1107                "scaled GPU probe `{label}` did not emit any capture metrics"
1108            ))
1109        })?;
1110        return Ok(build_scaling_entry(
1111            label,
1112            "scaled_subprocess_probe",
1113            width,
1114            height,
1115            pixel_ratio,
1116            capture,
1117            native_total_ms,
1118        ));
1119    }
1120
1121    let reason = if let Some(code) = status.code() {
1122        format!("scaled GPU subprocess exited with status {code}")
1123    } else {
1124        "scaled GPU subprocess terminated by signal".to_string()
1125    };
1126    Ok(unavailable_scaling_entry(
1127        label,
1128        width,
1129        height,
1130        native_pixels,
1131        true,
1132        reason,
1133    ))
1134}
1135
1136fn build_scaling_entry(
1137    label: &str,
1138    source: &str,
1139    width: usize,
1140    height: usize,
1141    pixel_ratio: f64,
1142    capture: &ExternalGpuCaptureMetrics,
1143    native_total_ms: Option<f64>,
1144) -> ExternalScalingEntry {
1145    let total_ms = capture.total_ms;
1146    let scaling_ratio_vs_native = native_total_ms
1147        .zip(total_ms)
1148        .map(|(native, total)| total / native.max(1e-9));
1149    let approximately_linear = scaling_ratio_vs_native.map(|scaling_ratio| {
1150        let normalized = scaling_ratio / pixel_ratio.max(1e-9);
1151        (normalized - 1.0).abs() <= 0.20
1152    });
1153    ExternalScalingEntry {
1154        label: label.to_string(),
1155        source: source.to_string(),
1156        width,
1157        height,
1158        attempted: true,
1159        measured_gpu: capture.measured_gpu,
1160        total_ms,
1161        dispatch_ms: capture.dispatch_ms,
1162        readback_ms: capture.readback_ms,
1163        ms_per_megapixel: total_ms.map(|value| value / (((width * height) as f64) / 1_000_000.0).max(1e-6)),
1164        scaling_ratio_vs_native,
1165        pixel_ratio_vs_native: pixel_ratio,
1166        approximately_linear,
1167        unavailable_reason: if capture.measured_gpu && total_ms.is_some() {
1168            None
1169        } else if capture.notes.is_empty() {
1170            Some("scaled GPU probe did not return a usable timing".to_string())
1171        } else {
1172            Some(capture.notes.join(" "))
1173        },
1174    }
1175}
1176
1177fn unavailable_scaling_entry(
1178    label: &str,
1179    width: usize,
1180    height: usize,
1181    native_pixels: f64,
1182    attempted: bool,
1183    reason: String,
1184) -> ExternalScalingEntry {
1185    ExternalScalingEntry {
1186        label: label.to_string(),
1187        source: "scaled_subprocess_probe".to_string(),
1188        width,
1189        height,
1190        attempted,
1191        measured_gpu: false,
1192        total_ms: None,
1193        dispatch_ms: None,
1194        readback_ms: None,
1195        ms_per_megapixel: None,
1196        scaling_ratio_vs_native: None,
1197        pixel_ratio_vs_native: (width * height) as f64 / native_pixels.max(1.0),
1198        approximately_linear: None,
1199        unavailable_reason: Some(reason),
1200    }
1201}
1202
1203fn coverage_summary(
1204    bundle: &ExternalCaptureBundle,
1205    demo_a: &ExternalDemoAMetrics,
1206    demo_b: &ExternalDemoBMetrics,
1207) -> ExternalCoverageSummary {
1208    let larger_roi_case = demo_a
1209        .captures
1210        .iter()
1211        .any(|capture| capture.roi_pixels > 50);
1212    let mixed_regime_case = demo_b
1213        .captures
1214        .iter()
1215        .any(|capture| capture.regime == "mixed_regime");
1216    let realism_stress_case = bundle.captures.iter().any(is_realism_stress_capture);
1217    let mut missing = Vec::new();
1218    if !realism_stress_case {
1219        missing.push("realism_stress_case".to_string());
1220    }
1221    if !larger_roi_case {
1222        missing.push("larger_roi_case".to_string());
1223    }
1224    if !mixed_regime_case {
1225        missing.push("mixed_regime_case".to_string());
1226    }
1227    ExternalCoverageSummary {
1228        realism_stress_case,
1229        larger_roi_case,
1230        mixed_regime_case,
1231        coverage_status: if missing.is_empty() {
1232            "complete".to_string()
1233        } else {
1234            "partial".to_string()
1235        },
1236        missing,
1237    }
1238}
1239
1240fn scale_external_capture(
1241    capture: &ExternalLoadedCapture,
1242    width: usize,
1243    height: usize,
1244) -> Result<ExternalLoadedCapture> {
1245    if width == 0 || height == 0 {
1246        return Err(Error::Message(
1247            "scaled GPU probe requires non-zero width and height".to_string(),
1248        ));
1249    }
1250    let source_width = capture.inputs.width();
1251    let source_height = capture.inputs.height();
1252    if width == source_width && height == source_height {
1253        return Ok(capture.clone());
1254    }
1255
1256    let inputs = OwnedHostTemporalInputs {
1257        current_color: scale_image_frame(&capture.inputs.current_color, width, height),
1258        reprojected_history: scale_image_frame(&capture.inputs.reprojected_history, width, height),
1259        motion_vectors: scale_motion_vectors(
1260            &capture.inputs.motion_vectors,
1261            source_width,
1262            source_height,
1263            width,
1264            height,
1265        ),
1266        current_depth: scale_scalar_buffer(
1267            &capture.inputs.current_depth,
1268            source_width,
1269            source_height,
1270            width,
1271            height,
1272        ),
1273        reprojected_depth: scale_scalar_buffer(
1274            &capture.inputs.reprojected_depth,
1275            source_width,
1276            source_height,
1277            width,
1278            height,
1279        ),
1280        current_normals: scale_normal_buffer(
1281            &capture.inputs.current_normals,
1282            source_width,
1283            source_height,
1284            width,
1285            height,
1286        ),
1287        reprojected_normals: scale_normal_buffer(
1288            &capture.inputs.reprojected_normals,
1289            source_width,
1290            source_height,
1291            width,
1292            height,
1293        ),
1294        visibility_hint: capture
1295            .inputs
1296            .visibility_hint
1297            .as_ref()
1298            .map(|mask| scale_bool_buffer(mask, source_width, source_height, width, height)),
1299        thin_hint: capture
1300            .inputs
1301            .thin_hint
1302            .as_ref()
1303            .map(|field| scale_scalar_field(field, width, height)),
1304    };
1305    let mut metadata = capture.metadata.clone();
1306    metadata.width = width;
1307    metadata.height = height;
1308    metadata.notes.push(format!(
1309        "This capture was scaled in-memory from {}x{} to {}x{} for an isolated GPU scaling probe.",
1310        source_width, source_height, width, height
1311    ));
1312
1313    Ok(ExternalLoadedCapture {
1314        label: capture.label.clone(),
1315        inputs,
1316        metadata,
1317        mask: capture
1318            .mask
1319            .as_ref()
1320            .map(|mask| scale_bool_buffer(mask, source_width, source_height, width, height)),
1321        reference: capture
1322            .reference
1323            .as_ref()
1324            .map(|reference| scale_image_frame(reference, width, height)),
1325        variance: capture
1326            .variance
1327            .as_ref()
1328            .map(|variance| scale_scalar_field(variance, width, height)),
1329    })
1330}
1331
1332fn scale_image_frame(frame: &ImageFrame, width: usize, height: usize) -> ImageFrame {
1333    let mut scaled = ImageFrame::new(width, height);
1334    for y in 0..height {
1335        for x in 0..width {
1336            let sample_x = scaled_sample_coordinate(x, frame.width(), width);
1337            let sample_y = scaled_sample_coordinate(y, frame.height(), height);
1338            scaled.set(x, y, frame.sample_bilinear_clamped(sample_x, sample_y));
1339        }
1340    }
1341    scaled
1342}
1343
1344fn scale_scalar_field(field: &ScalarField, width: usize, height: usize) -> ScalarField {
1345    ScalarField::from_values(
1346        width,
1347        height,
1348        scale_scalar_buffer(field.values(), field.width(), field.height(), width, height),
1349    )
1350}
1351
1352fn scale_scalar_buffer(
1353    values: &[f32],
1354    source_width: usize,
1355    source_height: usize,
1356    width: usize,
1357    height: usize,
1358) -> Vec<f32> {
1359    let mut scaled = Vec::with_capacity(width * height);
1360    for y in 0..height {
1361        for x in 0..width {
1362            let sample_x = scaled_sample_coordinate(x, source_width, width);
1363            let sample_y = scaled_sample_coordinate(y, source_height, height);
1364            scaled.push(sample_scalar_bilinear(
1365                values,
1366                source_width,
1367                source_height,
1368                sample_x,
1369                sample_y,
1370            ));
1371        }
1372    }
1373    scaled
1374}
1375
1376fn scale_bool_buffer(
1377    values: &[bool],
1378    source_width: usize,
1379    source_height: usize,
1380    width: usize,
1381    height: usize,
1382) -> Vec<bool> {
1383    let mut scaled = Vec::with_capacity(width * height);
1384    for y in 0..height {
1385        for x in 0..width {
1386            let sample_x = scaled_sample_coordinate(x, source_width, width)
1387                .round()
1388                .clamp(0.0, (source_width.saturating_sub(1)) as f32) as usize;
1389            let sample_y = scaled_sample_coordinate(y, source_height, height)
1390                .round()
1391                .clamp(0.0, (source_height.saturating_sub(1)) as f32) as usize;
1392            scaled.push(values[sample_y * source_width + sample_x]);
1393        }
1394    }
1395    scaled
1396}
1397
1398fn scale_motion_vectors(
1399    values: &[MotionVector],
1400    source_width: usize,
1401    source_height: usize,
1402    width: usize,
1403    height: usize,
1404) -> Vec<MotionVector> {
1405    let scale_x = width as f32 / source_width.max(1) as f32;
1406    let scale_y = height as f32 / source_height.max(1) as f32;
1407    let mut scaled = Vec::with_capacity(width * height);
1408    for y in 0..height {
1409        for x in 0..width {
1410            let sample_x = scaled_sample_coordinate(x, source_width, width);
1411            let sample_y = scaled_sample_coordinate(y, source_height, height);
1412            let motion = sample_motion_bilinear(values, source_width, source_height, sample_x, sample_y);
1413            scaled.push(MotionVector {
1414                to_prev_x: motion.to_prev_x * scale_x,
1415                to_prev_y: motion.to_prev_y * scale_y,
1416            });
1417        }
1418    }
1419    scaled
1420}
1421
1422fn scale_normal_buffer(
1423    values: &[Normal3],
1424    source_width: usize,
1425    source_height: usize,
1426    width: usize,
1427    height: usize,
1428) -> Vec<Normal3> {
1429    let mut scaled = Vec::with_capacity(width * height);
1430    for y in 0..height {
1431        for x in 0..width {
1432            let sample_x = scaled_sample_coordinate(x, source_width, width);
1433            let sample_y = scaled_sample_coordinate(y, source_height, height);
1434            scaled.push(sample_normal_bilinear(
1435                values,
1436                source_width,
1437                source_height,
1438                sample_x,
1439                sample_y,
1440            ));
1441        }
1442    }
1443    scaled
1444}
1445
1446fn scaled_sample_coordinate(index: usize, source_extent: usize, scaled_extent: usize) -> f32 {
1447    ((index as f32 + 0.5) * source_extent as f32 / scaled_extent.max(1) as f32) - 0.5
1448}
1449
1450fn sample_scalar_bilinear(
1451    values: &[f32],
1452    width: usize,
1453    height: usize,
1454    x: f32,
1455    y: f32,
1456) -> f32 {
1457    let x0 = x.floor();
1458    let y0 = y.floor();
1459    let x1 = x0 + 1.0;
1460    let y1 = y0 + 1.0;
1461    let tx = (x - x0).clamp(0.0, 1.0);
1462    let ty = (y - y0).clamp(0.0, 1.0);
1463    let p00 = scalar_at(values, width, height, x0 as i32, y0 as i32);
1464    let p10 = scalar_at(values, width, height, x1 as i32, y0 as i32);
1465    let p01 = scalar_at(values, width, height, x0 as i32, y1 as i32);
1466    let p11 = scalar_at(values, width, height, x1 as i32, y1 as i32);
1467    let top = p00 * (1.0 - tx) + p10 * tx;
1468    let bottom = p01 * (1.0 - tx) + p11 * tx;
1469    top * (1.0 - ty) + bottom * ty
1470}
1471
1472fn scalar_at(values: &[f32], width: usize, height: usize, x: i32, y: i32) -> f32 {
1473    let clamped_x = x.clamp(0, width as i32 - 1) as usize;
1474    let clamped_y = y.clamp(0, height as i32 - 1) as usize;
1475    values[clamped_y * width + clamped_x]
1476}
1477
1478fn sample_motion_bilinear(
1479    values: &[MotionVector],
1480    width: usize,
1481    height: usize,
1482    x: f32,
1483    y: f32,
1484) -> MotionVector {
1485    let x0 = x.floor();
1486    let y0 = y.floor();
1487    let x1 = x0 + 1.0;
1488    let y1 = y0 + 1.0;
1489    let tx = (x - x0).clamp(0.0, 1.0);
1490    let ty = (y - y0).clamp(0.0, 1.0);
1491    let p00 = motion_at(values, width, height, x0 as i32, y0 as i32);
1492    let p10 = motion_at(values, width, height, x1 as i32, y0 as i32);
1493    let p01 = motion_at(values, width, height, x0 as i32, y1 as i32);
1494    let p11 = motion_at(values, width, height, x1 as i32, y1 as i32);
1495    let top_x = p00.to_prev_x * (1.0 - tx) + p10.to_prev_x * tx;
1496    let top_y = p00.to_prev_y * (1.0 - tx) + p10.to_prev_y * tx;
1497    let bottom_x = p01.to_prev_x * (1.0 - tx) + p11.to_prev_x * tx;
1498    let bottom_y = p01.to_prev_y * (1.0 - tx) + p11.to_prev_y * tx;
1499    MotionVector {
1500        to_prev_x: top_x * (1.0 - ty) + bottom_x * ty,
1501        to_prev_y: top_y * (1.0 - ty) + bottom_y * ty,
1502    }
1503}
1504
1505fn motion_at(values: &[MotionVector], width: usize, height: usize, x: i32, y: i32) -> MotionVector {
1506    let clamped_x = x.clamp(0, width as i32 - 1) as usize;
1507    let clamped_y = y.clamp(0, height as i32 - 1) as usize;
1508    values[clamped_y * width + clamped_x]
1509}
1510
1511fn sample_normal_bilinear(
1512    values: &[Normal3],
1513    width: usize,
1514    height: usize,
1515    x: f32,
1516    y: f32,
1517) -> Normal3 {
1518    let x0 = x.floor();
1519    let y0 = y.floor();
1520    let x1 = x0 + 1.0;
1521    let y1 = y0 + 1.0;
1522    let tx = (x - x0).clamp(0.0, 1.0);
1523    let ty = (y - y0).clamp(0.0, 1.0);
1524    let p00 = normal_at(values, width, height, x0 as i32, y0 as i32);
1525    let p10 = normal_at(values, width, height, x1 as i32, y0 as i32);
1526    let p01 = normal_at(values, width, height, x0 as i32, y1 as i32);
1527    let p11 = normal_at(values, width, height, x1 as i32, y1 as i32);
1528    let top = Normal3::new(
1529        p00.x * (1.0 - tx) + p10.x * tx,
1530        p00.y * (1.0 - tx) + p10.y * tx,
1531        p00.z * (1.0 - tx) + p10.z * tx,
1532    );
1533    let bottom = Normal3::new(
1534        p01.x * (1.0 - tx) + p11.x * tx,
1535        p01.y * (1.0 - tx) + p11.y * tx,
1536        p01.z * (1.0 - tx) + p11.z * tx,
1537    );
1538    Normal3::new(
1539        top.x * (1.0 - ty) + bottom.x * ty,
1540        top.y * (1.0 - ty) + bottom.y * ty,
1541        top.z * (1.0 - ty) + bottom.z * ty,
1542    )
1543    .normalized()
1544}
1545
1546fn normal_at(values: &[Normal3], width: usize, height: usize, x: i32, y: i32) -> Normal3 {
1547    let clamped_x = x.clamp(0, width as i32 - 1) as usize;
1548    let clamped_y = y.clamp(0, height as i32 - 1) as usize;
1549    values[clamped_y * width + clamped_x]
1550}
1551
1552fn is_realism_stress_capture(capture: &ExternalLoadedCapture) -> bool {
1553    if let Some(scenario_id) = capture.metadata.scenario_id.as_deref() {
1554        if matches!(
1555            scenario_id,
1556            "motion_bias_band" | "noisy_reprojection" | "fast_pan" | "heuristic_friendly_pan"
1557        ) {
1558            return true;
1559        }
1560    }
1561    let motion_mean = capture
1562        .inputs
1563        .motion_vectors
1564        .iter()
1565        .map(|motion| {
1566            (motion.to_prev_x * motion.to_prev_x + motion.to_prev_y * motion.to_prev_y).sqrt()
1567        })
1568        .sum::<f32>()
1569        / capture.inputs.motion_vectors.len().max(1) as f32;
1570    let depth_disagreement = capture
1571        .inputs
1572        .current_depth
1573        .iter()
1574        .zip(capture.inputs.reprojected_depth.iter())
1575        .map(|(current, history)| (current - history).abs())
1576        .sum::<f32>()
1577        / capture.inputs.current_depth.len().max(1) as f32;
1578    motion_mean > 0.25 || depth_disagreement > 0.02
1579}
1580
1581fn write_external_scaling_report(
1582    path: &Path,
1583    metrics: &ExternalScalingMetrics,
1584    bundle: &ExternalCaptureBundle,
1585) -> Result<()> {
1586    if let Some(parent) = path.parent() {
1587        fs::create_dir_all(parent)?;
1588    }
1589    let mut markdown = String::new();
1590    let _ = writeln!(markdown, "# External Scaling Report");
1591    let _ = writeln!(markdown);
1592    let _ = writeln!(markdown, "{EXPERIMENT_SENTENCE}");
1593    let _ = writeln!(markdown);
1594    if bundle.no_real_external_data_provided {
1595        let _ = writeln!(markdown, "{NO_REAL_EXTERNAL_DATA_PROVIDED}");
1596        let _ = writeln!(markdown);
1597    }
1598    let _ = writeln!(
1599        markdown,
1600        "Native imported capture: `{}` at {}x{}.",
1601        metrics.native_capture_label, metrics.native_resolution[0], metrics.native_resolution[1]
1602    );
1603    let _ = writeln!(markdown);
1604    let _ = writeln!(
1605        markdown,
1606        "| Label | Source | Resolution | Attempted | Measured GPU | total_ms | dispatch_ms | readback_ms | ms/MPixel | scaling ratio vs native | approx linear |"
1607    );
1608    let _ = writeln!(
1609        markdown,
1610        "| --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | --- |"
1611    );
1612    for entry in &metrics.entries {
1613        let _ = writeln!(
1614            markdown,
1615            "| {} | {} | {}x{} | {} | {} | {} | {} | {} | {} | {} | {} |",
1616            entry.label,
1617            entry.source,
1618            entry.width,
1619            entry.height,
1620            entry.attempted,
1621            entry.measured_gpu,
1622            format_f64(entry.total_ms),
1623            format_f64(entry.dispatch_ms),
1624            format_f64(entry.readback_ms),
1625            format_f64(entry.ms_per_megapixel),
1626            format_f64(entry.scaling_ratio_vs_native),
1627            entry
1628                .approximately_linear
1629                .map(|value| value.to_string())
1630                .unwrap_or_else(|| "unknown".to_string()),
1631        );
1632        if let Some(reason) = &entry.unavailable_reason {
1633            let _ = writeln!(markdown, "  - unavailable: {reason}");
1634        }
1635    }
1636    let approximate_linearity = metrics
1637        .entries
1638        .iter()
1639        .skip(1)
1640        .filter_map(|entry| entry.approximately_linear)
1641        .all(|value| value);
1642    let _ = writeln!(markdown);
1643    let _ = writeln!(
1644        markdown,
1645        "Cost appears approximately linear with resolution: `{}`.",
1646        if metrics
1647            .entries
1648            .iter()
1649            .any(|entry| entry.approximately_linear.is_some())
1650        {
1651            approximate_linearity
1652        } else {
1653            false
1654        }
1655    );
1656    if !metrics
1657        .entries
1658        .iter()
1659        .any(|entry| entry.approximately_linear.is_some())
1660    {
1661        let _ = writeln!(
1662            markdown,
1663            "Approximate linearity could not be classified because no scaled GPU timing was measured in the current environment."
1664        );
1665    }
1666    let _ = writeln!(markdown);
1667    let _ = writeln!(markdown, "## Coverage");
1668    let _ = writeln!(markdown);
1669    let _ = writeln!(
1670        markdown,
1671        "- realism_stress_case: `{}`",
1672        metrics.coverage.realism_stress_case
1673    );
1674    let _ = writeln!(
1675        markdown,
1676        "- larger_roi_case: `{}`",
1677        metrics.coverage.larger_roi_case
1678    );
1679    let _ = writeln!(
1680        markdown,
1681        "- mixed_regime_case: `{}`",
1682        metrics.coverage.mixed_regime_case
1683    );
1684    let _ = writeln!(
1685        markdown,
1686        "- coverage_status: `{}`",
1687        metrics.coverage.coverage_status
1688    );
1689    if !metrics.coverage.missing.is_empty() {
1690        let _ = writeln!(
1691            markdown,
1692            "- missing coverage labels: {}",
1693            metrics.coverage.missing.join(", ")
1694        );
1695    }
1696    let _ = writeln!(markdown);
1697    let _ = writeln!(markdown, "## What Is Not Proven");
1698    let _ = writeln!(markdown);
1699    let _ = writeln!(
1700        markdown,
1701        "- This scaling report does not replace full engine-side profiling on real exported captures."
1702    );
1703    let _ = writeln!(
1704        markdown,
1705        "- When a row is marked unavailable, the corresponding scaling point was attempted but not measured in the current environment."
1706    );
1707    let _ = writeln!(markdown);
1708    let _ = writeln!(markdown, "## Remaining Blockers");
1709    let _ = writeln!(markdown);
1710    let _ = writeln!(
1711        markdown,
1712        "- Imported-buffer scaling does not replace full in-engine profiling on the final evaluator hardware and renderer integration point."
1713    );
1714    fs::write(path, markdown)?;
1715    Ok(())
1716}
1717
1718fn write_memory_bandwidth_report(path: &Path, metrics: &ExternalScalingMetrics) -> Result<()> {
1719    if let Some(parent) = path.parent() {
1720        fs::create_dir_all(parent)?;
1721    }
1722    let current_color_bytes_per_read = 16usize;
1723    let current_color_reads = 19usize;
1724    let bytes_read_per_pixel = current_color_bytes_per_read * current_color_reads + 16 + 8 + 8 + 32;
1725    let bytes_written_per_pixel = 12usize;
1726    let validation_readback_bytes_per_pixel = 12usize;
1727    let reads_per_pixel = 23usize;
1728    let writes_per_pixel = 3usize;
1729
1730    let mut markdown = String::new();
1731    let _ = writeln!(markdown, "# Memory Bandwidth Report");
1732    let _ = writeln!(markdown);
1733    let _ = writeln!(markdown, "{EXPERIMENT_SENTENCE}");
1734    let _ = writeln!(markdown);
1735    let _ = writeln!(
1736        markdown,
1737        "| Label | Resolution | bytes read / px | bytes written / px | validation readback / px | estimated memory traffic MB | reads / px | writes / px | readback required in production |"
1738    );
1739    let _ = writeln!(
1740        markdown,
1741        "| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | --- |"
1742    );
1743    for entry in &metrics.entries {
1744        let pixels = entry.width.saturating_mul(entry.height);
1745        let total_bytes = pixels
1746            * (bytes_read_per_pixel
1747                + bytes_written_per_pixel
1748                + validation_readback_bytes_per_pixel);
1749        let _ = writeln!(
1750            markdown,
1751            "| {} | {}x{} | {} | {} | {} | {:.2} | {} | {} | false |",
1752            entry.label,
1753            entry.width,
1754            entry.height,
1755            bytes_read_per_pixel,
1756            bytes_written_per_pixel,
1757            validation_readback_bytes_per_pixel,
1758            total_bytes as f64 / (1024.0 * 1024.0),
1759            reads_per_pixel,
1760            writes_per_pixel,
1761        );
1762    }
1763    let _ = writeln!(markdown);
1764    let _ = writeln!(markdown, "Readback required in production: `false`.");
1765    let _ = writeln!(
1766        markdown,
1767        "Readback was used here only for validation, numerical delta checks, and report generation."
1768    );
1769    let _ = writeln!(markdown);
1770    let _ = writeln!(markdown, "## Memory Access / Coherence Analysis");
1771    let _ = writeln!(markdown);
1772    let _ = writeln!(
1773        markdown,
1774        "- Buffer access pattern: linear per-pixel reads for current color, reprojected history, motion, depth pairs, and normal pairs, plus three scalar output writes."
1775    );
1776    let _ = writeln!(
1777        markdown,
1778        "- Neighborhood reads: the kernel performs two 3x3 neighborhood traversals over current color, one for local contrast and one for neighborhood-hull gating."
1779    );
1780    let _ = writeln!(
1781        markdown,
1782        "- Coherence expectation: adjacent threads in the 8x8 workgroup read strongly overlapping 3x3 neighborhoods, so the access pattern is locally coherent even though current color is revisited many times."
1783    );
1784    let _ = writeln!(
1785        markdown,
1786        "- Cache-friendliness: the minimum kernel avoids scattered history gathers because reprojection is precomputed before dispatch. That keeps the kernel more cache-friendly than a motion-indirected gather path."
1787    );
1788    let _ = writeln!(
1789        markdown,
1790        "- Cache risk: repeated 3x3 reads from a storage buffer still raise bandwidth pressure on current color, so profiling should confirm whether a texture path or shared-memory staging would reduce traffic materially."
1791    );
1792    let _ = writeln!(
1793        markdown,
1794        "- Optional path impact: any future motion-augmented kernel that reintroduces motion-neighborhood disagreement or in-kernel reprojection will increase cache pressure materially and should be treated as non-minimum."
1795    );
1796    let _ = writeln!(markdown);
1797    let _ = writeln!(markdown, "## What Is Not Proven");
1798    let _ = writeln!(markdown);
1799    let _ = writeln!(
1800        markdown,
1801        "- This report is analytical accounting based on the implemented kernel and does not replace external validation with hardware-counter collection."
1802    );
1803    let _ = writeln!(
1804        markdown,
1805        "- Reported traffic is sufficient for reviewer diligence, but not a substitute for per-architecture cache analysis."
1806    );
1807    let _ = writeln!(markdown);
1808    let _ = writeln!(markdown, "## Remaining Blockers");
1809    let _ = writeln!(markdown);
1810    let _ = writeln!(
1811        markdown,
1812        "- External validation still needs hardware counters and vendor-specific bandwidth profiling on imported real captures."
1813    );
1814    fs::write(path, markdown)?;
1815    Ok(())
1816}
1817
1818fn write_integration_scaling_report(
1819    path: &Path,
1820    metrics: &ExternalScalingMetrics,
1821    bundle: &ExternalCaptureBundle,
1822) -> Result<()> {
1823    if let Some(parent) = path.parent() {
1824        fs::create_dir_all(parent)?;
1825    }
1826    let mut markdown = String::new();
1827    let _ = writeln!(markdown, "# Integration Scaling Report");
1828    let _ = writeln!(markdown);
1829    let _ = writeln!(markdown, "{EXPERIMENT_SENTENCE}");
1830    let _ = writeln!(markdown);
1831    if bundle.no_real_external_data_provided {
1832        let _ = writeln!(markdown, "{NO_REAL_EXTERNAL_DATA_PROVIDED}");
1833        let _ = writeln!(markdown);
1834    }
1835    let _ = writeln!(markdown, "## Pipeline Insertion");
1836    let _ = writeln!(markdown);
1837    let _ = writeln!(
1838        markdown,
1839        "- The minimum kernel executes after history reprojection and after motion/depth/normal buffers are available, but before temporal resolve consumes per-pixel alpha or intervention."
1840    );
1841    let _ = writeln!(
1842        markdown,
1843        "- Alpha modulation is consumed by the temporal accumulation pass; trust and intervention are optional debug or allocator-driving side products."
1844    );
1845    let _ = writeln!(
1846        markdown,
1847        "- Production readback is not required. The current reports read buffers back only for validation and CPU/GPU delta checks."
1848    );
1849    let _ = writeln!(markdown);
1850    let _ = writeln!(markdown, "## Async-Compute Feasibility");
1851    let _ = writeln!(markdown);
1852    let _ = writeln!(
1853        markdown,
1854        "- Async execution is feasible if reprojected history, depth, and normals are already materialized and the downstream TAA resolve can wait on a GPU-side signal rather than CPU readback."
1855    );
1856    let _ = writeln!(
1857        markdown,
1858        "- The minimum kernel has no scattered history gather and no CPU dependency, so the main async-compute risk is overlap contention on memory bandwidth rather than synchronization correctness."
1859    );
1860    let _ = writeln!(
1861        markdown,
1862        "- Profiling still needs to confirm that the 3x3 current-color neighborhood reads do not stall other post or denoise passes when overlapped."
1863    );
1864    let _ = writeln!(markdown);
1865    let _ = writeln!(markdown, "## Hazards / Barriers / Transitions");
1866    let _ = writeln!(markdown);
1867    let _ = writeln!(
1868        markdown,
1869        "- Inputs should be transitioned to shader-read / storage-read state before dispatch."
1870    );
1871    let _ = writeln!(
1872        markdown,
1873        "- Trust, alpha, and intervention outputs should be transitioned from UAV/storage-write into the state required by the temporal resolve or any downstream debug visualization."
1874    );
1875    let _ = writeln!(
1876        markdown,
1877        "- A production integration should avoid CPU fences; only GPU barriers and queue synchronization should be required."
1878    );
1879    let _ = writeln!(markdown);
1880    let _ = writeln!(markdown, "## Pipeline Compatibility");
1881    let _ = writeln!(markdown);
1882    let _ = writeln!(
1883        markdown,
1884        "- The minimum kernel is compatible with tiled, deferred, and post-lighting temporal pipelines because it consumes already-aligned per-pixel buffers and writes only local trust/alpha/intervention fields."
1885    );
1886    let _ = writeln!(
1887        markdown,
1888        "- The current design remains compatible with tiled or asynchronous execution because it does not require CPU-side intervention in production."
1889    );
1890    let _ = writeln!(markdown);
1891    let _ = writeln!(markdown, "## Scaling Interpretation");
1892    let _ = writeln!(markdown);
1893    for entry in &metrics.entries {
1894        let _ = writeln!(
1895            markdown,
1896            "- {} {}x{}: measured_gpu = `{}`, ms/MPixel = {}, approx_linear = {}",
1897            entry.label,
1898            entry.width,
1899            entry.height,
1900            entry.measured_gpu,
1901            format_f64(entry.ms_per_megapixel),
1902            entry
1903                .approximately_linear
1904                .map(|value| value.to_string())
1905                .unwrap_or_else(|| "unknown".to_string())
1906        );
1907    }
1908    let _ = writeln!(markdown);
1909    let _ = writeln!(markdown, "## What Is Not Proven");
1910    let _ = writeln!(markdown);
1911    let _ = writeln!(
1912        markdown,
1913        "- This integration note is implementation-specific analysis, not a substitute for engine-side trace profiling."
1914    );
1915    let _ = writeln!(markdown);
1916    let _ = writeln!(markdown, "## Remaining Blockers");
1917    let _ = writeln!(markdown);
1918    let _ = writeln!(
1919        markdown,
1920        "- Async overlap, queue contention, and barrier cost still need confirmation inside a real renderer."
1921    );
1922    fs::write(path, markdown)?;
1923    Ok(())
1924}
1925
1926fn write_gpu_external_report(
1927    path: &Path,
1928    metrics: &ExternalGpuMetrics,
1929    bundle: &ExternalCaptureBundle,
1930) -> Result<()> {
1931    if let Some(parent) = path.parent() {
1932        fs::create_dir_all(parent)?;
1933    }
1934    let mut markdown = String::new();
1935    let _ = writeln!(markdown, "# GPU Execution Report");
1936    let _ = writeln!(markdown);
1937    let _ = writeln!(markdown, "{EXPERIMENT_SENTENCE}");
1938    let _ = writeln!(markdown);
1939    let _ = writeln!(markdown, "measured_gpu: `{}`", metrics.measured_gpu);
1940    let _ = writeln!(markdown, "measurement_kind: `{}`", metrics.measurement_kind);
1941    let _ = writeln!(markdown, "kernel: `{}`", metrics.kernel);
1942    let _ = writeln!(markdown);
1943    if bundle.no_real_external_data_provided {
1944        let _ = writeln!(markdown, "{NO_REAL_EXTERNAL_DATA_PROVIDED}");
1945        let _ = writeln!(markdown);
1946    }
1947    let _ = writeln!(
1948        markdown,
1949        "| Capture | measured_gpu | adapter | backend | resolution | total_ms | dispatch_ms | readback_ms | trust_delta_vs_cpu | alpha_delta_vs_cpu | intervention_delta_vs_cpu |"
1950    );
1951    let _ = writeln!(
1952        markdown,
1953        "| --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |"
1954    );
1955    for capture in &metrics.captures {
1956        let _ = writeln!(
1957            markdown,
1958            "| {} | {} | {} | {} | {}x{} | {} | {} | {} | {} | {} | {} |",
1959            capture.capture_label,
1960            capture.measured_gpu,
1961            capture.adapter.as_deref().unwrap_or("unavailable"),
1962            capture.backend.as_deref().unwrap_or("unavailable"),
1963            capture.resolution[0],
1964            capture.resolution[1],
1965            format_f64(capture.total_ms),
1966            format_f64(capture.dispatch_ms),
1967            format_f64(capture.readback_ms),
1968            format_f32(capture.mean_abs_trust_delta_vs_cpu),
1969            format_f32(capture.mean_abs_alpha_delta_vs_cpu),
1970            format_f32(capture.mean_abs_intervention_delta_vs_cpu),
1971        );
1972    }
1973    let _ = writeln!(markdown);
1974    let _ = writeln!(markdown, "## What Is Proven");
1975    let _ = writeln!(markdown);
1976    let _ = writeln!(
1977        markdown,
1978        "- The imported external buffers can execute through the same minimum host-realistic GPU kernel as the internal study."
1979    );
1980    let _ = writeln!(
1981        markdown,
1982        "- GPU-vs-CPU numerical deltas are recorded whenever a GPU adapter is available."
1983    );
1984    let _ = writeln!(markdown);
1985    let _ = writeln!(markdown, "## What Is Not Proven");
1986    let _ = writeln!(markdown);
1987    let _ = writeln!(
1988        markdown,
1989        "- This file does not prove production renderer integration or full engine-side GPU cost."
1990    );
1991    let _ = writeln!(
1992        markdown,
1993        "- If `measured_gpu` is `false`, the path is implemented but unmeasured in the current environment."
1994    );
1995    let _ = writeln!(markdown);
1996    let _ = writeln!(markdown, "## Remaining Blockers");
1997    let _ = writeln!(markdown);
1998    let _ = writeln!(
1999        markdown,
2000        "- Engine-exported captures on the target evaluation hardware still need GPU-side profiling."
2001    );
2002    fs::write(path, markdown)?;
2003    Ok(())
2004}
2005
2006fn write_demo_a_external_report(
2007    path: &Path,
2008    metrics: &ExternalDemoAMetrics,
2009    bundle: &ExternalCaptureBundle,
2010) -> Result<()> {
2011    if let Some(parent) = path.parent() {
2012        fs::create_dir_all(parent)?;
2013    }
2014    let mut markdown = String::new();
2015    let _ = writeln!(markdown, "# Demo A External Report");
2016    let _ = writeln!(markdown);
2017    let _ = writeln!(markdown, "{EXPERIMENT_SENTENCE}");
2018    let _ = writeln!(markdown);
2019    if bundle.no_real_external_data_provided {
2020        let _ = writeln!(markdown, "{NO_REAL_EXTERNAL_DATA_PROVIDED}");
2021        let _ = writeln!(markdown);
2022    }
2023    let has_sequence = metrics.captures.len() >= 2;
2024    let all_have_reference = metrics.captures.iter().all(|capture| capture.ground_truth_available);
2025    let _ = writeln!(
2026        markdown,
2027        "Point-vs-region disclosure is explicit per capture via the `point_vs_region` line below."
2028    );
2029    let _ = writeln!(markdown);
2030    let _ = writeln!(markdown, "{ROI_CONTRACT_STATEMENT}");
2031    let _ = writeln!(markdown);
2032    for capture in &metrics.captures {
2033        let source_capture = bundle
2034            .captures
2035            .iter()
2036            .find(|candidate| candidate.label == capture.capture_label)
2037            .ok_or_else(|| {
2038                Error::Message(format!(
2039                    "Demo A capture {} was missing from the loaded bundle",
2040                    capture.capture_label
2041                ))
2042            })?;
2043        let total_pixels = source_capture.inputs.width() * source_capture.inputs.height();
2044        let roi_ratio = capture.roi_pixels as f32 / total_pixels.max(1) as f32;
2045        let _ = writeln!(markdown, "## Capture `{}`", capture.capture_label);
2046        let _ = writeln!(markdown);
2047        let _ = writeln!(markdown, "- ROI source: `{}`", capture.roi_source);
2048        let _ = writeln!(markdown, "- ROI pixels: `{}`", capture.roi_pixels);
2049        let _ = writeln!(markdown, "- ROI coverage: `{:.2}%`", capture.roi_coverage * 100.0);
2050        let _ = writeln!(
2051            markdown,
2052            "- ROI baseline method: `{}`",
2053            capture.baseline_method_id
2054        );
2055        let _ = writeln!(
2056            markdown,
2057            "- reference_source: `{}`",
2058            capture.reference_source
2059        );
2060        let _ = writeln!(
2061            markdown,
2062            "- ground_truth_available: `{}`",
2063            capture.ground_truth_available
2064        );
2065        let _ = writeln!(markdown, "- metric_source: `{}`", capture.metric_source);
2066        let _ = writeln!(
2067            markdown,
2068            "- point_vs_region: `{}`",
2069            if roi_ratio < 0.05 {
2070                "point_like"
2071            } else {
2072                "region_like"
2073            }
2074        );
2075        let _ = writeln!(
2076            markdown,
2077            "- realism_stress_note: `{}`",
2078            if is_realism_stress_capture(source_capture) {
2079                "realism_stress_case"
2080            } else {
2081                "not_classified_as_realism_stress"
2082            }
2083        );
2084        let _ = writeln!(
2085            markdown,
2086            "- larger_roi_note: `{}`",
2087            if roi_ratio > 0.10 {
2088                "larger_roi_case"
2089            } else {
2090                "not_a_larger_roi_case"
2091            }
2092        );
2093        if !capture.ground_truth_available {
2094            let _ = writeln!(markdown, "- ground truth unavailable -> proxy metrics used");
2095        }
2096        let _ = writeln!(markdown);
2097        let _ = writeln!(
2098            markdown,
2099            "| Method | full-frame MAE | ROI MAE | non-ROI MAE | max error | temporal error accumulation | intervention rate |"
2100        );
2101        let _ = writeln!(markdown, "| --- | ---: | ---: | ---: | ---: | ---: | ---: |");
2102        for method in &capture.methods {
2103            let _ = writeln!(
2104                markdown,
2105                "| {} | {:.5} | {:.5} | {:.5} | {:.5} | {:.5} | {:.5} |",
2106                method.label,
2107                method.overall_mae,
2108                method.roi_mae,
2109                method.non_roi_mae,
2110                method.max_error,
2111                method.temporal_error_accumulation,
2112                method.intervention_rate
2113            );
2114        }
2115        let _ = writeln!(markdown);
2116    }
2117    let _ = writeln!(markdown, "## What Is Proven");
2118    let _ = writeln!(markdown);
2119    let _ = writeln!(
2120        markdown,
2121        "- The same DSFB host-minimum supervisory layer runs on imported buffers and can be compared against fixed alpha, a strong heuristic baseline, and an explicit DSFB + strong heuristic hybrid."
2122    );
2123    let _ = writeln!(
2124        markdown,
2125        "- ROI and non-ROI behavior remain separated on imported data."
2126    );
2127    if has_sequence {
2128        let _ = writeln!(
2129            markdown,
2130            "- The current real Unreal-native package also emits `figures/trust_temporal_trajectory.svg` and `figures/trust_temporal_trajectory.json` over the ordered capture sequence."
2131        );
2132    }
2133    let _ = writeln!(markdown);
2134    let _ = writeln!(markdown, "## What Is Not Proven");
2135    let _ = writeln!(markdown);
2136    if all_have_reference {
2137        let _ = writeln!(
2138            markdown,
2139            "- The current bundle measures against `reference_color`, but that reference is a higher-resolution exported Unreal proxy rather than a path-traced ground truth."
2140        );
2141    } else {
2142        let _ = writeln!(
2143            markdown,
2144            "- Without an optional reference frame, error is measured against the current-frame proxy rather than a high-spp reconstruction."
2145        );
2146    }
2147    let _ = writeln!(
2148        markdown,
2149        "- Even with a reference frame, this does not replace longer engine-side sequences."
2150    );
2151    if has_sequence {
2152        let _ = writeln!(
2153            markdown,
2154            "- The current bundle generates `figures/trust_histogram.svg`, `figures/trust_vs_error.svg`, `figures/trust_conditioned_error_map.png`, and `figures/trust_temporal_trajectory.svg`; these are calibration artifacts over a short five-frame sequence, not a broad temporal generalization claim."
2155        );
2156    } else {
2157        let _ = writeln!(
2158            markdown,
2159            "- The current bundle generates `figures/trust_histogram.svg`, `figures/trust_vs_error.svg`, and `figures/trust_conditioned_error_map.png`; for a single frame pair these are calibration diagnostics, not temporal claims."
2160        );
2161    }
2162    let _ = writeln!(markdown);
2163    let _ = writeln!(markdown, "## Remaining Blockers");
2164    let _ = writeln!(markdown);
2165    let _ = writeln!(
2166        markdown,
2167        "- Real engine capture sequences and longer temporal windows still need evaluation."
2168    );
2169    fs::write(path, markdown)?;
2170    Ok(())
2171}
2172
2173fn write_demo_b_external_report(
2174    path: &Path,
2175    metrics: &ExternalDemoBMetrics,
2176    bundle: &ExternalCaptureBundle,
2177) -> Result<()> {
2178    if let Some(parent) = path.parent() {
2179        fs::create_dir_all(parent)?;
2180    }
2181    let mut markdown = String::new();
2182    let _ = writeln!(markdown, "# Demo B External Report");
2183    let _ = writeln!(markdown);
2184    let _ = writeln!(markdown, "{EXPERIMENT_SENTENCE}");
2185    let _ = writeln!(markdown);
2186    if bundle.no_real_external_data_provided {
2187        let _ = writeln!(markdown, "{NO_REAL_EXTERNAL_DATA_PROVIDED}");
2188        let _ = writeln!(markdown);
2189    }
2190    let _ = writeln!(
2191        markdown,
2192        "Regime labels used in this report: `aliasing_limited`, `variance_limited`, `mixed_regime`."
2193    );
2194    let _ = writeln!(markdown);
2195    let _ = writeln!(markdown, "{ROI_CONTRACT_STATEMENT}");
2196    let _ = writeln!(markdown);
2197    for capture in &metrics.captures {
2198        let _ = writeln!(markdown, "## Capture `{}`", capture.capture_label);
2199        let _ = writeln!(markdown);
2200        let _ = writeln!(markdown, "- regime: `{}`", capture.regime);
2201        let _ = writeln!(markdown, "- metric_source: `{}`", capture.metric_source);
2202        let _ = writeln!(
2203            markdown,
2204            "- fixed_budget_equal: `{}`",
2205            capture.fixed_budget_equal
2206        );
2207        let _ = writeln!(markdown, "- ROI source: `{}`", capture.roi_source);
2208        let _ = writeln!(markdown, "- ROI pixels: `{}`", capture.roi_pixels);
2209        let _ = writeln!(markdown, "- ROI coverage: `{:.2}%`", capture.roi_coverage * 100.0);
2210        let _ = writeln!(
2211            markdown,
2212            "- ROI baseline method: `{}`",
2213            capture.baseline_method_id
2214        );
2215        let _ = writeln!(
2216            markdown,
2217            "- reference_source: `{}`",
2218            capture.reference_source
2219        );
2220        let _ = writeln!(markdown);
2221        let _ = writeln!(
2222            markdown,
2223            "| Policy | total samples | ROI error | global error | ROI mean spp | non-ROI mean spp |"
2224        );
2225        let _ = writeln!(markdown, "| --- | ---: | ---: | ---: | ---: | ---: |");
2226        for policy in &capture.policies {
2227            let _ = writeln!(
2228                markdown,
2229                "| {} | {} | {:.5} | {:.5} | {:.3} | {:.3} |",
2230                policy.label,
2231                policy.total_samples,
2232                policy.roi_mae,
2233                policy.overall_mae,
2234                policy.roi_mean_spp,
2235                policy.non_roi_mean_spp
2236            );
2237        }
2238        let uniform = capture
2239            .policies
2240            .iter()
2241            .find(|policy| policy.policy_id == AllocationPolicyId::Uniform.as_str());
2242        let dsfb = capture
2243            .policies
2244            .iter()
2245            .find(|policy| policy.policy_id == AllocationPolicyId::ImportedTrust.as_str());
2246        let combined = capture
2247            .policies
2248            .iter()
2249            .find(|policy| policy.policy_id == AllocationPolicyId::CombinedHeuristic.as_str());
2250        if let (Some(uniform), Some(dsfb), Some(combined)) = (uniform, dsfb, combined) {
2251            let _ = writeln!(markdown);
2252            let _ = writeln!(markdown, "Aliasing vs variance discussion:");
2253            if capture.regime == "aliasing_limited" {
2254                let _ = writeln!(
2255                    markdown,
2256                    "- This imported capture is primarily aliasing-limited. DSFB imported trust changed ROI proxy error from {:.5} for uniform to {:.5}, while combined heuristic reached {:.5}.",
2257                    uniform.roi_mae, dsfb.roi_mae, combined.roi_mae
2258                );
2259            } else if capture.regime == "variance_limited" {
2260                let _ = writeln!(
2261                    markdown,
2262                    "- This imported capture is primarily variance-limited. Variance-like heuristics should remain competitive, and the imported trust result changed ROI proxy error from {:.5} for uniform to {:.5}, versus {:.5} for the combined heuristic.",
2263                    uniform.roi_mae, dsfb.roi_mae, combined.roi_mae
2264                );
2265            } else {
2266                let _ = writeln!(
2267                    markdown,
2268                    "- This imported capture is mixed-regime. The DSFB imported-trust allocator reached {:.5} ROI proxy error, compared with {:.5} for uniform and {:.5} for the combined heuristic.",
2269                    dsfb.roi_mae, uniform.roi_mae, combined.roi_mae
2270                );
2271            }
2272            if (dsfb.roi_mae - combined.roi_mae).abs() <= 1e-4 {
2273                let _ = writeln!(markdown, "- This capture is effectively a tie between DSFB imported trust and the strongest heuristic proxy.");
2274            } else if dsfb.roi_mae < combined.roi_mae {
2275                let _ = writeln!(
2276                    markdown,
2277                    "- DSFB imported trust wins on this imported capture under equal total budget."
2278                );
2279            } else {
2280                let _ = writeln!(markdown, "- The strongest heuristic proxy wins on this imported capture; that result is surfaced rather than hidden.");
2281            }
2282        }
2283        let _ = writeln!(markdown);
2284    }
2285    let _ = writeln!(markdown, "## What Is Proven");
2286    let _ = writeln!(markdown);
2287    let _ = writeln!(
2288        markdown,
2289        "- Imported captures can drive fixed-budget allocation policies, including DSFB imported trust and stronger heuristic competitors."
2290    );
2291    let _ = writeln!(
2292        markdown,
2293        "- Budget equality is enforced across all compared policies."
2294    );
2295    let _ = writeln!(markdown);
2296    let _ = writeln!(markdown, "## What Is Not Proven");
2297    let _ = writeln!(markdown);
2298    let _ = writeln!(
2299        markdown,
2300        "- This is still an allocation proxy because imported captures do not provide a live renderer or per-sample ground truth."
2301    );
2302    let _ = writeln!(
2303        markdown,
2304        "- It does not replace real engine-side fixed-budget sampling experiments."
2305    );
2306    let _ = writeln!(markdown);
2307    let _ = writeln!(markdown, "## Remaining Blockers");
2308    let _ = writeln!(markdown);
2309    let _ = writeln!(
2310        markdown,
2311        "- A renderer-integrated fixed-budget replay still needs to confirm the allocation story on real per-sample shading."
2312    );
2313    fs::write(path, markdown)?;
2314    Ok(())
2315}
2316
2317fn write_external_validation_report(
2318    path: &Path,
2319    bundle: &ExternalCaptureBundle,
2320    handoff: &ExternalHandoffMetrics,
2321    gpu: &ExternalGpuMetrics,
2322    demo_a: &ExternalDemoAMetrics,
2323    demo_b: &ExternalDemoBMetrics,
2324    scaling: &ExternalScalingMetrics,
2325) -> Result<()> {
2326    if let Some(parent) = path.parent() {
2327        fs::create_dir_all(parent)?;
2328    }
2329    let mut markdown = String::new();
2330    let _ = writeln!(markdown, "# External Validation Report");
2331    let _ = writeln!(markdown);
2332    let _ = writeln!(markdown, "{EXPERIMENT_SENTENCE}");
2333    let _ = writeln!(markdown);
2334    let _ = writeln!(markdown, "## Data Description");
2335    let _ = writeln!(markdown);
2336    let _ = writeln!(markdown, "- source_kind: `{}`", handoff.source_kind);
2337    let _ = writeln!(markdown, "- captures: `{}`", handoff.capture_count);
2338    let _ = writeln!(
2339        markdown,
2340        "- real_external_data_provided: `{}`",
2341        bundle.real_external_data_provided
2342    );
2343    let _ = writeln!(
2344        markdown,
2345        "- synthetic vs real: `{}`",
2346        if bundle.real_external_data_provided {
2347            "real external data"
2348        } else {
2349            "synthetic compatibility export"
2350        }
2351    );
2352    if bundle.no_real_external_data_provided {
2353        let _ = writeln!(markdown);
2354        let _ = writeln!(markdown, "{NO_REAL_EXTERNAL_DATA_PROVIDED}");
2355    }
2356    let _ = writeln!(markdown);
2357    let _ = writeln!(markdown, "## Pipeline Description");
2358    let _ = writeln!(markdown);
2359    let _ = writeln!(
2360        markdown,
2361        "- External replay uses the same DSFB host-minimum supervisory logic and the same minimum GPU kernel as the internal suite."
2362    );
2363    let _ = writeln!(
2364        markdown,
2365        "- Differences: imported buffers replace synthetic scene generation, and Demo B uses an allocation proxy because no live renderer samples are present."
2366    );
2367    let _ = writeln!(markdown, "- ROI contract: {ROI_CONTRACT_STATEMENT}");
2368    let _ = writeln!(markdown);
2369    let _ = writeln!(markdown, "## GPU Execution Summary");
2370    let _ = writeln!(markdown);
2371    let _ = writeln!(markdown, "- measured_gpu: `{}`", gpu.measured_gpu);
2372    let _ = writeln!(markdown, "- kernel: `{}`", gpu.kernel);
2373    for capture in &gpu.captures {
2374        let _ = writeln!(
2375            markdown,
2376            "- capture `{}`: adapter = `{}`, total_ms = {}, dispatch_ms = {}, readback_ms = {}",
2377            capture.capture_label,
2378            capture.adapter.as_deref().unwrap_or("unavailable"),
2379            format_f64(capture.total_ms),
2380            format_f64(capture.dispatch_ms),
2381            format_f64(capture.readback_ms),
2382        );
2383    }
2384    let _ = writeln!(markdown);
2385    let _ = writeln!(markdown, "## Demo A Results");
2386    let _ = writeln!(markdown);
2387    for capture in &demo_a.captures {
2388        let _ = writeln!(
2389            markdown,
2390            "- `{}`: ROI source = `{}`, ROI pixels = {}, ROI coverage = {:.2}%, metric_source = `{}`",
2391            capture.capture_label,
2392            capture.roi_source,
2393            capture.roi_pixels,
2394            capture.roi_coverage * 100.0,
2395            capture.metric_source
2396        );
2397        for method in &capture.methods {
2398            let _ = writeln!(
2399                markdown,
2400                "  - {}: full-frame MAE = {:.5}, ROI MAE = {:.5}, non-ROI MAE = {:.5}, max error = {:.5}, temporal accumulation = {:.5}, intervention rate = {:.5}",
2401                method.label,
2402                method.overall_mae,
2403                method.roi_mae,
2404                method.non_roi_mae,
2405                method.max_error,
2406                method.temporal_error_accumulation,
2407                method.intervention_rate
2408            );
2409        }
2410    }
2411    let _ = writeln!(markdown);
2412    let _ = writeln!(markdown, "## Demo B Results");
2413    let _ = writeln!(markdown);
2414    for capture in &demo_b.captures {
2415        let _ = writeln!(
2416            markdown,
2417            "- `{}`: regime = `{}`, fixed_budget_equal = `{}`",
2418            capture.capture_label, capture.regime, capture.fixed_budget_equal
2419        );
2420        for policy in &capture.policies {
2421            let _ = writeln!(
2422                markdown,
2423                "  - {}: ROI error = {:.5}, global error = {:.5}, ROI mean spp = {:.3}",
2424                policy.label, policy.roi_mae, policy.overall_mae, policy.roi_mean_spp
2425            );
2426        }
2427    }
2428    let _ = writeln!(markdown);
2429    let _ = writeln!(markdown, "## Scaling / Coverage Summary");
2430    let _ = writeln!(markdown);
2431    let _ = writeln!(markdown, "- attempted_1080p: `{}`", scaling.attempted_1080p);
2432    let _ = writeln!(markdown, "- attempted_4k: `{}`", scaling.attempted_4k);
2433    let _ = writeln!(
2434        markdown,
2435        "- realism_stress_case: `{}`",
2436        scaling.coverage.realism_stress_case
2437    );
2438    let _ = writeln!(
2439        markdown,
2440        "- larger_roi_case: `{}`",
2441        scaling.coverage.larger_roi_case
2442    );
2443    let _ = writeln!(
2444        markdown,
2445        "- mixed_regime_case: `{}`",
2446        scaling.coverage.mixed_regime_case
2447    );
2448    let _ = writeln!(
2449        markdown,
2450        "- coverage_status: `{}`",
2451        scaling.coverage.coverage_status
2452    );
2453    if !scaling.coverage.missing.is_empty() {
2454        let _ = writeln!(
2455            markdown,
2456            "- missing coverage labels: {}",
2457            scaling.coverage.missing.join(", ")
2458        );
2459    }
2460    let _ = writeln!(markdown);
2461    let _ = writeln!(markdown, "## What Is Proven");
2462    let _ = writeln!(markdown);
2463    let _ = writeln!(
2464        markdown,
2465        "- The crate can ingest external buffers through a strict manifest and run the DSFB host-minimum supervisory layer on them."
2466    );
2467    let _ = writeln!(
2468        markdown,
2469        "- The same GPU kernel can execute on imported buffers, with explicit measured-vs-unmeasured disclosure and isolated scaled-resolution probes when the standalone binary is available."
2470    );
2471    let _ = writeln!(
2472        markdown,
2473        "- ROI vs non-ROI reporting survives the external path, Demo B keeps equal budgets across stronger heuristic baselines, and Demo A now includes an explicit DSFB + strong heuristic hybrid."
2474    );
2475    let _ = writeln!(markdown);
2476    let _ = writeln!(markdown, "## What Is Not Proven");
2477    let _ = writeln!(markdown);
2478    let _ = writeln!(
2479        markdown,
2480        "- This report does not prove production-scene generalization."
2481    );
2482    let _ = writeln!(
2483        markdown,
2484        "- It does not prove engine integration unless real exported buffers are supplied."
2485    );
2486    let _ = writeln!(
2487        markdown,
2488        "- Demo B on imported captures remains an allocation proxy, not a renderer-integrated sampling benchmark."
2489    );
2490    if demo_a.captures.len() >= 2 {
2491        let _ = writeln!(
2492            markdown,
2493            "- The trust trajectory is now measured across an ordered five-frame real Unreal-native sequence, but that short sequence is still not enough to claim broad temporal calibration."
2494        );
2495    } else {
2496        let _ = writeln!(
2497            markdown,
2498            "- For the single checked-in Unreal-native sample, trust calibration artifacts are per-frame diagnostics only; no temporal trust trajectory claim is made."
2499        );
2500    }
2501    let _ = writeln!(markdown);
2502    let _ = writeln!(markdown, "## Remaining Blockers");
2503    let _ = writeln!(markdown);
2504    let _ = writeln!(markdown, "- engine-side GPU profiling on imported buffers");
2505    let _ = writeln!(
2506        markdown,
2507        "- renderer-integrated Demo B replay with per-sample budgets"
2508    );
2509    let _ = writeln!(markdown);
2510    let _ = writeln!(markdown, "## Next Required Experiment");
2511    let _ = writeln!(markdown);
2512    let _ = writeln!(
2513        markdown,
2514        "Move from the current five-frame exported Unreal-native sequence to a longer production-representative engine capture, preserve the same fixed ROI contract and baseline ladder, and confirm the trust trajectory plus scaled GPU timings on the target evaluation hardware."
2515    );
2516    fs::write(path, markdown)?;
2517    Ok(())
2518}
2519
2520fn copy_representative_figure_aliases(output_dir: &Path, figures_dir: &Path) -> Result<()> {
2521    for (source_name, target_name) in [
2522        ("current_color.png", "before_current_color.png"),
2523        ("reprojected_history.png", "before_history_color.png"),
2524        ("demo_a_dsfb.png", "after_dsfb_resolved.png"),
2525        ("trust_map.png", "trust_map.png"),
2526        ("intervention_map.png", "intervention_map.png"),
2527        ("roi_overlay.png", "roi_overlay.png"),
2528    ] {
2529        let source = figures_dir.join(source_name);
2530        if source.exists() {
2531            fs::copy(source, output_dir.join(target_name))?;
2532        }
2533    }
2534    Ok(())
2535}
2536
2537fn build_demo_a_method_metrics(
2538    method_id: &str,
2539    label: &str,
2540    resolved: &ImageFrame,
2541    reference: &ImageFrame,
2542    metric_source: &str,
2543    roi_mask: &[bool],
2544    intervention: &ScalarField,
2545) -> ExternalDemoAMethodMetrics {
2546    let error_field = absolute_error_field(resolved, reference);
2547    let non_roi_mask = invert_mask(roi_mask);
2548    ExternalDemoAMethodMetrics {
2549        method_id: method_id.to_string(),
2550        label: label.to_string(),
2551        metric_source: metric_source.to_string(),
2552        overall_mae: mean_abs_error(resolved, reference),
2553        roi_mae: mean_abs_error_over_mask(resolved, reference, roi_mask),
2554        non_roi_mae: mean_abs_error_over_mask(resolved, reference, &non_roi_mask),
2555        max_error: scalar_field_max(&error_field),
2556        temporal_error_accumulation: mean_abs_error(resolved, reference),
2557        intervention_rate: intervention.mean(),
2558    }
2559}
2560
2561pub(crate) fn capture_reference_frame_and_metric_source<'a>(
2562    capture: &'a ExternalLoadedCapture,
2563) -> (&'a ImageFrame, &'static str, &'static str) {
2564    if let Some(reference) = &capture.reference {
2565        (reference, "reference_color", "real_reference")
2566    } else {
2567        (
2568            &capture.inputs.current_color,
2569            "current_color_proxy",
2570            "current_color_proxy",
2571        )
2572    }
2573}
2574
2575pub(crate) fn absolute_error_field(frame_a: &ImageFrame, frame_b: &ImageFrame) -> ScalarField {
2576    let mut field = ScalarField::new(frame_a.width(), frame_a.height());
2577    for y in 0..frame_a.height() {
2578        for x in 0..frame_a.width() {
2579            field.set(x, y, frame_a.get(x, y).abs_diff(frame_b.get(x, y)));
2580        }
2581    }
2582    field
2583}
2584
2585pub(crate) fn scalar_field_max(field: &ScalarField) -> f32 {
2586    field.values().iter().copied().fold(0.0, f32::max)
2587}
2588
2589fn resolve_with_alpha(
2590    history: &ImageFrame,
2591    current: &ImageFrame,
2592    alpha: &ScalarField,
2593) -> ImageFrame {
2594    let mut frame = ImageFrame::new(current.width(), current.height());
2595    for y in 0..current.height() {
2596        for x in 0..current.width() {
2597            let blended = history.get(x, y).lerp(current.get(x, y), alpha.get(x, y));
2598            frame.set(x, y, blended);
2599        }
2600    }
2601    frame
2602}
2603
2604fn run_external_strong_heuristic(
2605    config: &DemoConfig,
2606    capture: &ExternalLoadedCapture,
2607) -> (ImageFrame, ScalarField, ScalarField) {
2608    let width = capture.inputs.width();
2609    let height = capture.inputs.height();
2610    let mut resolved = ImageFrame::new(width, height);
2611    let mut alpha = ScalarField::new(width, height);
2612    let mut response = ScalarField::new(width, height);
2613
2614    for y in 0..height {
2615        for x in 0..width {
2616            let index = y * width + x;
2617            let current = capture.inputs.current_color.get(x, y);
2618            let history = capture.inputs.reprojected_history.get(x, y);
2619            let clamped =
2620                clamp_to_current_neighborhood(&capture.inputs.current_color, history, x, y);
2621            let clamp_distance = clamped.abs_diff(history);
2622            let residual_gate = smoothstep(
2623                config.baseline.residual_threshold,
2624                current.abs_diff(clamped),
2625            );
2626            let depth_gate = smoothstep(
2627                config.baseline.depth_disagreement,
2628                (capture.inputs.current_depth[index] - capture.inputs.reprojected_depth[index])
2629                    .abs(),
2630            );
2631            let normal_gate = smoothstep(
2632                config.baseline.normal_disagreement,
2633                1.0 - capture.inputs.current_normals[index]
2634                    .dot(capture.inputs.reprojected_normals[index])
2635                    .clamp(-1.0, 1.0),
2636            );
2637            let neighborhood_gate =
2638                smoothstep(config.baseline.neighborhood_distance, clamp_distance);
2639            let trigger = residual_gate
2640                .max(depth_gate)
2641                .max(normal_gate)
2642                .max(neighborhood_gate);
2643            let pixel_alpha = config.baseline.residual_alpha_range.min
2644                + (config.baseline.residual_alpha_range.max
2645                    - config.baseline.residual_alpha_range.min)
2646                    * trigger;
2647            alpha.set(x, y, pixel_alpha);
2648            response.set(x, y, trigger);
2649            resolved.set(x, y, clamped.lerp(current, pixel_alpha));
2650        }
2651    }
2652
2653    (resolved, alpha, response)
2654}
2655
2656fn run_external_dsfb_plus_strong_heuristic(
2657    capture: &ExternalLoadedCapture,
2658    dsfb_alpha: &ScalarField,
2659    dsfb_intervention: &ScalarField,
2660    strong_alpha: &ScalarField,
2661    strong_response: &ScalarField,
2662) -> (ImageFrame, ScalarField, ScalarField) {
2663    let width = capture.inputs.width();
2664    let height = capture.inputs.height();
2665    let mut resolved = ImageFrame::new(width, height);
2666    let mut alpha = ScalarField::new(width, height);
2667    let mut response = ScalarField::new(width, height);
2668
2669    for y in 0..height {
2670        for x in 0..width {
2671            let current = capture.inputs.current_color.get(x, y);
2672            let history = capture.inputs.reprojected_history.get(x, y);
2673            let clamped =
2674                clamp_to_current_neighborhood(&capture.inputs.current_color, history, x, y);
2675            let hybrid_alpha = dsfb_alpha.get(x, y).max(strong_alpha.get(x, y));
2676            let hybrid_response = dsfb_intervention.get(x, y).max(strong_response.get(x, y));
2677            alpha.set(x, y, hybrid_alpha);
2678            response.set(x, y, hybrid_response);
2679            resolved.set(x, y, clamped.lerp(current, hybrid_alpha));
2680        }
2681    }
2682
2683    (resolved, alpha, response)
2684}
2685
2686fn clamp_to_current_neighborhood(
2687    current: &ImageFrame,
2688    history: Color,
2689    x: usize,
2690    y: usize,
2691) -> Color {
2692    let mut min_r = f32::INFINITY;
2693    let mut min_g = f32::INFINITY;
2694    let mut min_b = f32::INFINITY;
2695    let mut max_r = f32::NEG_INFINITY;
2696    let mut max_g = f32::NEG_INFINITY;
2697    let mut max_b = f32::NEG_INFINITY;
2698    for dy in -1i32..=1 {
2699        for dx in -1i32..=1 {
2700            let sample = current.sample_clamped(x as i32 + dx, y as i32 + dy);
2701            min_r = min_r.min(sample.r);
2702            min_g = min_g.min(sample.g);
2703            min_b = min_b.min(sample.b);
2704            max_r = max_r.max(sample.r);
2705            max_g = max_g.max(sample.g);
2706            max_b = max_b.max(sample.b);
2707        }
2708    }
2709    Color::rgb(
2710        history.r.clamp(min_r, max_r),
2711        history.g.clamp(min_g, max_g),
2712        history.b.clamp(min_b, max_b),
2713    )
2714}
2715
2716fn smoothstep(threshold: SmoothstepThreshold, value: f32) -> f32 {
2717    let span = (threshold.high - threshold.low).max(1e-6);
2718    let t = ((value - threshold.low) / span).clamp(0.0, 1.0);
2719    t * t * (3.0 - 2.0 * t)
2720}
2721
2722pub(crate) fn roi_mask_for_capture(
2723    capture: &ExternalLoadedCapture,
2724    baseline: &ImageFrame,
2725    reference: &ImageFrame,
2726) -> (Vec<bool>, String, f32) {
2727    let contrast = local_contrast_field(reference);
2728    let mut mask = vec![false; capture.inputs.width() * capture.inputs.height()];
2729    for y in 0..capture.inputs.height() {
2730        for x in 0..capture.inputs.width() {
2731            let index = y * capture.inputs.width() + x;
2732            let baseline_error = baseline.get(x, y).abs_diff(reference.get(x, y));
2733            let threshold = ROI_CONTRACT_ALPHA * contrast.get(x, y);
2734            mask[index] = baseline_error > threshold;
2735        }
2736    }
2737    let coverage = mask.iter().filter(|value| **value).count() as f32 / mask.len().max(1) as f32;
2738    (mask, ROI_CONTRACT_SOURCE.to_string(), coverage)
2739}
2740
2741fn overlay_roi_mask(frame: &ImageFrame, mask: &[bool]) -> ImageFrame {
2742    let mut output = frame.clone();
2743    for y in 0..frame.height() {
2744        for x in 0..frame.width() {
2745            let index = y * frame.width() + x;
2746            if mask[index] {
2747                let current = frame.get(x, y);
2748                output.set(x, y, current.lerp(Color::rgb(0.12, 1.0, 0.24), 0.45));
2749            }
2750        }
2751    }
2752    output
2753}
2754
2755fn write_trust_histogram_figure(trust: &ScalarField, path: &Path) -> Result<()> {
2756    if let Some(parent) = path.parent() {
2757        fs::create_dir_all(parent)?;
2758    }
2759
2760    let bin_count = 10usize;
2761    let mut counts = vec![0usize; bin_count];
2762    for value in trust.values() {
2763        let bin = ((*value).clamp(0.0, 0.999_999) * bin_count as f32) as usize;
2764        counts[bin.min(bin_count - 1)] += 1;
2765    }
2766    let max_count = counts.iter().copied().max().unwrap_or(1).max(1) as f32;
2767    let mut bars = String::new();
2768    for (index, count) in counts.iter().enumerate() {
2769        let height = 260.0 * (*count as f32 / max_count);
2770        let x = 86.0 + index as f32 * 62.0;
2771        let y = 368.0 - height;
2772        let _ = writeln!(
2773            bars,
2774            r##"<rect x="{x:.1}" y="{y:.1}" width="42" height="{height:.1}" fill="#4cc9f0"/>"##
2775        );
2776        let _ = writeln!(
2777            bars,
2778            r##"<text x="{:.1}" y="392" font-size="13" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">{:.1}</text>"##,
2779            x,
2780            index as f32 / bin_count as f32
2781        );
2782    }
2783
2784    let svg = format!(
2785        r##"<svg xmlns="http://www.w3.org/2000/svg" width="900" height="460" viewBox="0 0 900 460">
2786<rect width="900" height="460" fill="#0b1320"/>
2787<text x="36" y="42" font-size="28" font-family="Arial, Helvetica, sans-serif" fill="#f4f7fb">Trust Histogram</text>
2788<text x="36" y="68" font-size="16" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">Per-pixel DSFB trust distribution for the canonical imported capture.</text>
2789<line x1="70" y1="96" x2="70" y2="368" stroke="#f4f7fb" stroke-width="2"/>
2790<line x1="70" y1="368" x2="760" y2="368" stroke="#f4f7fb" stroke-width="2"/>
2791{bars}
2792<text x="36" y="106" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">count</text>
2793<text x="730" y="420" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">trust</text>
2794</svg>"##
2795    );
2796    fs::write(path, svg)?;
2797    Ok(())
2798}
2799
2800fn write_trust_vs_error_figure(
2801    trust: &ScalarField,
2802    error: &ScalarField,
2803    metric_source: &str,
2804    path: &Path,
2805) -> Result<()> {
2806    if let Some(parent) = path.parent() {
2807        fs::create_dir_all(parent)?;
2808    }
2809
2810    let bin_count = 10usize;
2811    let mut counts = vec![0usize; bin_count];
2812    let mut means = vec![0.0f32; bin_count];
2813    for (trust_value, error_value) in trust.values().iter().zip(error.values()) {
2814        let bin = ((*trust_value).clamp(0.0, 0.999_999) * bin_count as f32) as usize;
2815        let index = bin.min(bin_count - 1);
2816        counts[index] += 1;
2817        means[index] += *error_value;
2818    }
2819    for (count, mean) in counts.iter().zip(means.iter_mut()) {
2820        if *count > 0 {
2821            *mean /= *count as f32;
2822        }
2823    }
2824    let max_error = means.iter().copied().fold(0.05, f32::max);
2825    let left = 82.0f32;
2826    let right = 780.0f32;
2827    let top = 92.0f32;
2828    let bottom = 366.0f32;
2829    let x_scale = (right - left) / (bin_count.saturating_sub(1)) as f32;
2830    let mut points = String::new();
2831    for (index, mean) in means.iter().enumerate() {
2832        let x = left + index as f32 * x_scale;
2833        let y = bottom - (mean / max_error.max(1e-6)) * (bottom - top);
2834        let _ = writeln!(points, "{x:.1},{y:.1} ");
2835    }
2836
2837    let svg = format!(
2838        r##"<svg xmlns="http://www.w3.org/2000/svg" width="920" height="460" viewBox="0 0 920 460">
2839<rect width="920" height="460" fill="#0b1320"/>
2840<text x="36" y="42" font-size="28" font-family="Arial, Helvetica, sans-serif" fill="#f4f7fb">Trust vs Error Curve</text>
2841<text x="36" y="68" font-size="16" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">Mean per-pixel error by trust bin. Error source: {metric_source}.</text>
2842<line x1="{left}" y1="{top}" x2="{left}" y2="{bottom}" stroke="#f4f7fb" stroke-width="2"/>
2843<line x1="{left}" y1="{bottom}" x2="{right}" y2="{bottom}" stroke="#f4f7fb" stroke-width="2"/>
2844<polyline fill="none" stroke="#8bd450" stroke-width="3.5" points="{points}"/>
2845<text x="36" y="102" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">mean error</text>
2846<text x="744" y="420" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">trust bin</text>
2847</svg>"##
2848    );
2849    fs::write(path, svg)?;
2850    Ok(())
2851}
2852
2853fn save_trust_conditioned_error_map(
2854    trust: &ScalarField,
2855    error: &ScalarField,
2856    path: &Path,
2857) -> Result<()> {
2858    let mut conditioned = ScalarField::new(trust.width(), trust.height());
2859    for y in 0..trust.height() {
2860        for x in 0..trust.width() {
2861            conditioned.set(x, y, error.get(x, y) * (1.0 - trust.get(x, y)));
2862        }
2863    }
2864    save_scalar_field_png(&conditioned, path, heatmap_red)
2865}
2866
2867fn write_temporal_trust_trajectory_outputs(
2868    points: &[TemporalTrustTrajectoryPoint],
2869    figures_dir: &Path,
2870) -> Result<()> {
2871    let mut ordered = points.to_vec();
2872    ordered.sort_by_key(|point| point.frame_index);
2873    let peak_index = ordered
2874        .iter()
2875        .enumerate()
2876        .max_by(|(_, left), (_, right)| left.roi_coverage.total_cmp(&right.roi_coverage))
2877        .map(|(index, _)| index)
2878        .unwrap_or(0);
2879    let report = TemporalTrustTrajectoryReport {
2880        onset_capture_label: ordered
2881            .first()
2882            .map(|point| point.capture_label.clone())
2883            .unwrap_or_default(),
2884        peak_roi_capture_label: ordered[peak_index].capture_label.clone(),
2885        recovery_capture_label: ordered
2886            .last()
2887            .map(|point| point.capture_label.clone())
2888            .unwrap_or_default(),
2889        points: ordered.clone(),
2890    };
2891    fs::write(
2892        figures_dir.join("trust_temporal_trajectory.json"),
2893        serde_json::to_string_pretty(&report)?,
2894    )?;
2895    write_temporal_trust_trajectory_figure(
2896        &report,
2897        &figures_dir.join("trust_temporal_trajectory.svg"),
2898    )
2899}
2900
2901fn write_temporal_trust_trajectory_figure(
2902    report: &TemporalTrustTrajectoryReport,
2903    path: &Path,
2904) -> Result<()> {
2905    if let Some(parent) = path.parent() {
2906        fs::create_dir_all(parent)?;
2907    }
2908    let point_count = report.points.len().max(2);
2909    let width = 980.0f32;
2910    let height = 520.0f32;
2911    let left = 86.0f32;
2912    let right = 860.0f32;
2913    let top = 88.0f32;
2914    let bottom = 418.0f32;
2915    let inner_width = right - left;
2916    let inner_height = bottom - top;
2917    let x_scale = inner_width / (point_count.saturating_sub(1)) as f32;
2918    let max_roi_error = report
2919        .points
2920        .iter()
2921        .map(|point| point.dsfb_roi_mae.max(point.hybrid_roi_mae))
2922        .fold(1e-6f32, f32::max);
2923
2924    let trust_path = polyline_points(
2925        &report
2926            .points
2927            .iter()
2928            .enumerate()
2929            .map(|(index, point)| {
2930                (
2931                    left + index as f32 * x_scale,
2932                    bottom - point.mean_trust.clamp(0.0, 1.0) * inner_height,
2933                )
2934            })
2935            .collect::<Vec<_>>(),
2936    );
2937    let intervention_path = polyline_points(
2938        &report
2939            .points
2940            .iter()
2941            .enumerate()
2942            .map(|(index, point)| {
2943                (
2944                    left + index as f32 * x_scale,
2945                    bottom - point.intervention_rate.clamp(0.0, 1.0) * inner_height,
2946                )
2947            })
2948            .collect::<Vec<_>>(),
2949    );
2950    let roi_coverage_path = polyline_points(
2951        &report
2952            .points
2953            .iter()
2954            .enumerate()
2955            .map(|(index, point)| {
2956                (
2957                    left + index as f32 * x_scale,
2958                    bottom - point.roi_coverage.clamp(0.0, 1.0) * inner_height,
2959                )
2960            })
2961            .collect::<Vec<_>>(),
2962    );
2963    let dsfb_error_path = polyline_points(
2964        &report
2965            .points
2966            .iter()
2967            .enumerate()
2968            .map(|(index, point)| {
2969                (
2970                    left + index as f32 * x_scale,
2971                    bottom - (point.dsfb_roi_mae / max_roi_error) * inner_height,
2972                )
2973            })
2974            .collect::<Vec<_>>(),
2975    );
2976    let hybrid_error_path = polyline_points(
2977        &report
2978            .points
2979            .iter()
2980            .enumerate()
2981            .map(|(index, point)| {
2982                (
2983                    left + index as f32 * x_scale,
2984                    bottom - (point.hybrid_roi_mae / max_roi_error) * inner_height,
2985                )
2986            })
2987            .collect::<Vec<_>>(),
2988    );
2989    let peak_index = report
2990        .points
2991        .iter()
2992        .enumerate()
2993        .max_by(|(_, left), (_, right)| left.roi_coverage.total_cmp(&right.roi_coverage))
2994        .map(|(index, _)| index)
2995        .unwrap_or(0);
2996    let peak_x = left + peak_index as f32 * x_scale;
2997
2998    let mut labels = String::new();
2999    for (index, point) in report.points.iter().enumerate() {
3000        let x = left + index as f32 * x_scale;
3001        let _ = writeln!(
3002            labels,
3003            r##"<text x="{x:.1}" y="446" text-anchor="middle" font-size="13" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">{}</text>"##,
3004            point.capture_label
3005        );
3006    }
3007
3008    let svg = format!(
3009        r##"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">
3010<rect width="{width}" height="{height}" fill="#0b1320"/>
3011<text x="34" y="42" font-size="28" font-family="Arial, Helvetica, sans-serif" fill="#f4f7fb">Temporal Trust Trajectory</text>
3012<text x="34" y="68" font-size="16" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">Ordered real Unreal-native sequence from onset-side frame {onset} through peak ROI frame {peak} to recovery-side frame {recovery}.</text>
3013<line x1="{left}" y1="{top}" x2="{left}" y2="{bottom}" stroke="#f4f7fb" stroke-width="2"/>
3014<line x1="{left}" y1="{bottom}" x2="{right}" y2="{bottom}" stroke="#f4f7fb" stroke-width="2"/>
3015<path d="{trust_path}" fill="none" stroke="#4cc9f0" stroke-width="3.5"/>
3016<path d="{intervention_path}" fill="none" stroke="#ef476f" stroke-width="3.5" stroke-dasharray="10 8"/>
3017<path d="{roi_coverage_path}" fill="none" stroke="#8bd450" stroke-width="3.5"/>
3018<path d="{dsfb_error_path}" fill="none" stroke="#ffd166" stroke-width="3.5"/>
3019<path d="{hybrid_error_path}" fill="none" stroke="#f4978e" stroke-width="3.5" stroke-dasharray="6 6"/>
3020<line x1="{peak_x:.1}" y1="{top}" x2="{peak_x:.1}" y2="{bottom}" stroke="#f4f7fb" stroke-width="1.5" stroke-dasharray="8 6"/>
3021<text x="628" y="110" font-size="15" font-family="Arial, Helvetica, sans-serif" fill="#f4f7fb">Mean trust</text>
3022<line x1="566" y1="104" x2="616" y2="104" stroke="#4cc9f0" stroke-width="3.5"/>
3023<text x="628" y="136" font-size="15" font-family="Arial, Helvetica, sans-serif" fill="#f4f7fb">Intervention rate</text>
3024<line x1="566" y1="130" x2="616" y2="130" stroke="#ef476f" stroke-width="3.5" stroke-dasharray="10 8"/>
3025<text x="628" y="162" font-size="15" font-family="Arial, Helvetica, sans-serif" fill="#f4f7fb">ROI coverage</text>
3026<line x1="566" y1="156" x2="616" y2="156" stroke="#8bd450" stroke-width="3.5"/>
3027<text x="628" y="188" font-size="15" font-family="Arial, Helvetica, sans-serif" fill="#f4f7fb">DSFB ROI MAE / max</text>
3028<line x1="566" y1="182" x2="616" y2="182" stroke="#ffd166" stroke-width="3.5"/>
3029<text x="628" y="214" font-size="15" font-family="Arial, Helvetica, sans-serif" fill="#f4f7fb">Hybrid ROI MAE / max</text>
3030<line x1="566" y1="208" x2="616" y2="208" stroke="#f4978e" stroke-width="3.5" stroke-dasharray="6 6"/>
3031<text x="34" y="102" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">normalized value</text>
3032<text x="788" y="470" font-size="14" font-family="Arial, Helvetica, sans-serif" fill="#c6d2dd">ordered capture label</text>
3033{labels}
3034</svg>"##,
3035        onset = report.onset_capture_label,
3036        peak = report.peak_roi_capture_label,
3037        recovery = report.recovery_capture_label,
3038    );
3039    fs::write(path, svg)?;
3040    Ok(())
3041}
3042
3043fn polyline_points(points: &[(f32, f32)]) -> String {
3044    let mut path = String::new();
3045    for (index, (x, y)) in points.iter().enumerate() {
3046        let command = if index == 0 { "M" } else { "L" };
3047        let _ = write!(path, "{command}{x:.1},{y:.1} ");
3048    }
3049    path
3050}
3051
3052fn constant_field(width: usize, height: usize, value: f32) -> ScalarField {
3053    ScalarField::from_values(width, height, vec![value; width * height])
3054}
3055
3056fn invert_mask(mask: &[bool]) -> Vec<bool> {
3057    mask.iter().map(|value| !*value).collect()
3058}
3059
3060fn normalize_field(field: ScalarField) -> ScalarField {
3061    let max_value = field
3062        .values()
3063        .iter()
3064        .copied()
3065        .fold(0.0f32, f32::max)
3066        .max(1e-6);
3067    let values = field
3068        .values()
3069        .iter()
3070        .map(|value| (*value / max_value).clamp(0.0, 1.0))
3071        .collect();
3072    ScalarField::from_values(field.width(), field.height(), values)
3073}
3074
3075fn temporal_variance_proxy(current: &ImageFrame, history: &ImageFrame) -> ScalarField {
3076    let mut field = ScalarField::new(current.width(), current.height());
3077    for y in 0..current.height() {
3078        for x in 0..current.width() {
3079            let diff = current.get(x, y).abs_diff(history.get(x, y));
3080            field.set(x, y, (diff / 0.20).clamp(0.0, 1.0));
3081        }
3082    }
3083    field
3084}
3085
3086fn predicted_error_over_mask(
3087    base_error: &ScalarField,
3088    counts: &[usize],
3089    roi_mask: Option<&[bool]>,
3090    squared: bool,
3091) -> f32 {
3092    let width = base_error.width();
3093    let height = base_error.height();
3094    let mut sum = 0.0;
3095    let mut count = 0usize;
3096    for y in 0..height {
3097        for x in 0..width {
3098            let index = y * width + x;
3099            let include = roi_mask.map(|mask| mask[index]).unwrap_or(true);
3100            if !include {
3101                continue;
3102            }
3103            let predicted = base_error.get(x, y) / (counts[index].max(1) as f32).sqrt();
3104            sum += if squared {
3105                predicted * predicted
3106            } else {
3107                predicted
3108            };
3109            count += 1;
3110        }
3111    }
3112    if count == 0 {
3113        0.0
3114    } else if squared {
3115        (sum / count as f32).sqrt()
3116    } else {
3117        sum / count as f32
3118    }
3119}
3120
3121fn classify_external_demo_b_regime(
3122    gradient: &ScalarField,
3123    variance: &ScalarField,
3124    contrast: &ScalarField,
3125) -> String {
3126    let gradient_mean = gradient.mean();
3127    let variance_mean = variance.mean();
3128    let contrast_mean = contrast.mean();
3129    if gradient_mean + contrast_mean > variance_mean * 1.45 {
3130        "aliasing_limited".to_string()
3131    } else if variance_mean > gradient_mean * 1.20 {
3132        "variance_limited".to_string()
3133    } else {
3134        "mixed_regime".to_string()
3135    }
3136}
3137
3138fn mean_abs_delta(left: &[f32], right: &[f32]) -> f32 {
3139    let count = left.len().min(right.len()).max(1);
3140    left.iter()
3141        .zip(right.iter())
3142        .map(|(lhs, rhs)| (lhs - rhs).abs())
3143        .sum::<f32>()
3144        / count as f32
3145}
3146
3147
3148fn format_f64(value: Option<f64>) -> String {
3149    value
3150        .map(|current| format!("{current:.4}"))
3151        .unwrap_or_else(|| "n/a".to_string())
3152}
3153
3154fn format_f32(value: Option<f32>) -> String {
3155    value
3156        .map(|current| format!("{current:.6}"))
3157        .unwrap_or_else(|| "n/a".to_string())
3158}
3159
3160fn heatmap_blue(value: f32) -> [u8; 4] {
3161    let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
3162    [v / 4, v / 2, 255, 255]
3163}
3164
3165fn heatmap_orange(value: f32) -> [u8; 4] {
3166    let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
3167    [255, v, 32, 255]
3168}
3169
3170fn heatmap_red(value: f32) -> [u8; 4] {
3171    let v = (value.clamp(0.0, 1.0) * 255.0).round() as u8;
3172    [255, 24, v / 2, 255]
3173}