dreamwell-matter 1.0.0

DreamMatter benchmark — GPU physics materialization demo and profiler
Documentation
// Benchmark statistics — per-frame recording, phase-stratified metrics,
// percentile computation, GPU timing, scene complexity, and CSV export.

use std::io::Write;

/// Per-benchmark-run statistics with phase-level granularity.
pub struct BenchmarkStats {
    // Per-frame raw data
    frame_times_ms: Vec<f32>,
    frame_phases: Vec<String>,
    // Hardware context
    pub adapter_name: String,
    pub adapter_backend: String,
    pub resolution: [u32; 2],
    // Scene metrics
    pub scene_object_count: u32,
    pub dreamlet_capacity: u32,
    pub light_count: u32,
    pub pbr_pipeline_count: u32,
    pub ibl_initialized: bool,
    pub post_process_enabled: bool,
    // Dream Lighting metrics
    pub quality_preset: String,
    pub gi_cost_ms: f32,
    pub screen_trace_hit_rate: f32,
    pub rt_shadow_cost_ms: f32,
    // Aggregate counters
    total_frames: u32,
    total_time_s: f32,
    finalized: bool,
}

impl BenchmarkStats {
    pub fn new() -> Self {
        Self {
            frame_times_ms: Vec::with_capacity(2000),
            frame_phases: Vec::with_capacity(2000),
            adapter_name: String::new(),
            adapter_backend: String::new(),
            resolution: [1920, 1080],
            scene_object_count: 0,
            dreamlet_capacity: 0,
            light_count: 0,
            pbr_pipeline_count: 0,
            ibl_initialized: false,
            post_process_enabled: false,
            quality_preset: String::new(),
            gi_cost_ms: 0.0,
            screen_trace_hit_rate: 0.0,
            rt_shadow_cost_ms: 0.0,
            total_frames: 0,
            total_time_s: 0.0,
            finalized: false,
        }
    }

    /// Record one frame's delta time and current phase label.
    pub fn record_frame_with_phase(&mut self, dt: f32, phase: &str) {
        let ms = dt * 1000.0;
        self.frame_times_ms.push(ms);
        self.frame_phases.push(phase.to_string());
        self.total_frames += 1;
        self.total_time_s += dt;
    }

    /// Record one frame (backwards compat — no phase tracking).
    pub fn record_frame(&mut self, dt: f32) {
        self.record_frame_with_phase(dt, "");
    }

    /// Sort frame times for percentile computation.
    pub fn finalize(&mut self) {
        self.finalized = true;
    }

    pub fn frame_count(&self) -> usize {
        self.frame_times_ms.len()
    }

    pub fn avg_fps(&self) -> f32 {
        if self.total_time_s > 0.0 { self.total_frames as f32 / self.total_time_s } else { 0.0 }
    }

    pub fn avg_ms(&self) -> f32 {
        if self.frame_times_ms.is_empty() { return 0.0; }
        let sum: f32 = self.frame_times_ms.iter().sum();
        sum / self.frame_times_ms.len() as f32
    }

    fn sorted_times(&self) -> Vec<f32> {
        let mut sorted = self.frame_times_ms.clone();
        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
        sorted
    }

    /// Compute percentile using linear interpolation (inclusive method).
    /// Matches NumPy's default `percentile(method='linear')` for cross-tool consistency.
    fn percentile_of(sorted: &[f32], p: f32) -> f32 {
        if sorted.is_empty() { return 0.0; }
        if sorted.len() == 1 { return sorted[0]; }
        let rank = (p / 100.0) * (sorted.len() - 1) as f32;
        let lower = rank.floor() as usize;
        let upper = rank.ceil().min((sorted.len() - 1) as f32) as usize;
        let frac = rank - lower as f32;
        sorted[lower] * (1.0 - frac) + sorted[upper] * frac
    }

    /// 1% low — worst-case frame time for the slowest 1% of frames.
    pub fn p1_low_ms(&self) -> f32 { Self::percentile_of(&self.sorted_times(), 99.0) }
    /// 0.1% low — extreme worst-case frame time (1 in 1000 frames).
    pub fn p01_low_ms(&self) -> f32 { Self::percentile_of(&self.sorted_times(), 99.9) }
    pub fn p50_ms(&self) -> f32 { Self::percentile_of(&self.sorted_times(), 50.0) }
    pub fn p90_ms(&self) -> f32 { Self::percentile_of(&self.sorted_times(), 90.0) }
    pub fn p95_ms(&self) -> f32 { Self::percentile_of(&self.sorted_times(), 95.0) }
    pub fn p99_ms(&self) -> f32 { Self::percentile_of(&self.sorted_times(), 99.0) }
    pub fn max_ms(&self) -> f32 { self.frame_times_ms.iter().copied().fold(0.0f32, f32::max) }
    pub fn min_ms(&self) -> f32 {
        if self.frame_times_ms.is_empty() { return 0.0; }
        self.frame_times_ms.iter().copied().fold(f32::MAX, f32::min)
    }

    /// Print results to console.
    pub fn print_results(&self) {
        println!();
        println!("=== DreamMatter Benchmark Results ===");
        println!("GPU:            {}", self.adapter_name);
        println!("Backend:        {}", self.adapter_backend);
        println!("Resolution:     {}x{}", self.resolution[0], self.resolution[1]);
        println!("Duration:       {:.1}s", self.total_time_s);
        println!("Total frames:   {}", self.total_frames);
        println!();
        println!("--- Frame Timing ---");
        println!("Avg FPS:        {:.1}", self.avg_fps());
        println!("Avg frame:      {:.2}ms", self.avg_ms());
        println!("p50:            {:.2}ms", self.p50_ms());
        println!("p90:            {:.2}ms", self.p90_ms());
        println!("p95:            {:.2}ms", self.p95_ms());
        println!("p99:            {:.2}ms", self.p99_ms());
        println!("Max:            {:.2}ms", self.max_ms());
        println!("Min:            {:.2}ms", self.min_ms());
        println!();
        println!("--- Scene Complexity ---");
        println!("Objects:        {}", self.scene_object_count);
        println!("Dreamlets:      {} capacity", self.dreamlet_capacity);
        println!("Lights:         {}", self.light_count);
        println!("PBR pipelines:  {}/4", self.pbr_pipeline_count);
        println!("IBL:            {}", if self.ibl_initialized { "active" } else { "inactive" });
        println!("Post-process:   {}", if self.post_process_enabled { "HDR+bloom+ACES" } else { "off" });
        if !self.quality_preset.is_empty() {
            println!();
            println!("--- Dream Lighting ---");
            println!("Quality:        {}", self.quality_preset);
            println!("GI cost:        {:.2}ms", self.gi_cost_ms);
            println!("Screen traces:  {:.1}% hit rate", self.screen_trace_hit_rate * 100.0);
            println!("RT shadows:     {:.2}ms", self.rt_shadow_cost_ms);
        }
        println!("=====================================");
        println!();
    }

    /// Export results to CSV with per-frame data and phase annotations.
    pub fn export_csv(&self, path: &str) -> Result<(), std::io::Error> {
        let mut file = std::fs::File::create(path)?;

        // Summary section
        writeln!(file, "# DreamMatter Benchmark Results")?;
        writeln!(file, "metric,value")?;
        writeln!(file, "gpu,\"{}\"", self.adapter_name)?;
        writeln!(file, "backend,\"{}\"", self.adapter_backend)?;
        writeln!(file, "resolution,\"{}x{}\"", self.resolution[0], self.resolution[1])?;
        writeln!(file, "duration_s,{:.3}", self.total_time_s)?;
        writeln!(file, "total_frames,{}", self.total_frames)?;
        writeln!(file, "avg_fps,{:.2}", self.avg_fps())?;
        writeln!(file, "avg_frame_ms,{:.3}", self.avg_ms())?;
        writeln!(file, "p50_ms,{:.3}", self.p50_ms())?;
        writeln!(file, "p90_ms,{:.3}", self.p90_ms())?;
        writeln!(file, "p95_ms,{:.3}", self.p95_ms())?;
        writeln!(file, "p99_ms,{:.3}", self.p99_ms())?;
        writeln!(file, "max_ms,{:.3}", self.max_ms())?;
        writeln!(file, "min_ms,{:.3}", self.min_ms())?;
        writeln!(file, "scene_objects,{}", self.scene_object_count)?;
        writeln!(file, "dreamlet_capacity,{}", self.dreamlet_capacity)?;
        writeln!(file, "light_count,{}", self.light_count)?;
        writeln!(file, "pbr_pipelines,{}", self.pbr_pipeline_count)?;
        writeln!(file, "ibl_active,{}", self.ibl_initialized)?;
        writeln!(file, "post_process,{}", self.post_process_enabled)?;
        // Dream Lighting metrics
        if !self.quality_preset.is_empty() {
            writeln!(file, "quality_preset,\"{}\"", self.quality_preset)?;
            writeln!(file, "gi_cost_ms,{:.3}", self.gi_cost_ms)?;
            writeln!(file, "screen_trace_hit_rate,{:.3}", self.screen_trace_hit_rate)?;
            writeln!(file, "rt_shadow_cost_ms,{:.3}", self.rt_shadow_cost_ms)?;
        }

        // Per-frame data with phase annotations
        writeln!(file)?;
        writeln!(file, "frame,frame_time_ms,phase")?;
        for (i, ms) in self.frame_times_ms.iter().enumerate() {
            let phase = self.frame_phases.get(i).map(|s| s.as_str()).unwrap_or("");
            writeln!(file, "{},{:.3},{}", i, ms, phase)?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn empty_stats() {
        let stats = BenchmarkStats::new();
        assert_eq!(stats.avg_fps(), 0.0);
        assert_eq!(stats.avg_ms(), 0.0);
        assert_eq!(stats.min_ms(), 0.0);
        assert_eq!(stats.max_ms(), 0.0);
        assert_eq!(stats.p50_ms(), 0.0);
        assert_eq!(stats.total_frames, 0);
    }

    #[test]
    fn single_frame() {
        let mut stats = BenchmarkStats::new();
        stats.record_frame(0.016);
        stats.finalize();
        assert_eq!(stats.total_frames, 1);
        assert!(stats.avg_ms() > 15.0 && stats.avg_ms() < 17.0);
        assert_eq!(stats.min_ms(), stats.max_ms());
    }

    #[test]
    fn record_and_finalize() {
        let mut stats = BenchmarkStats::new();
        for _ in 0..100 {
            stats.record_frame(1.0 / 60.0);
        }
        stats.finalize();
        assert!((stats.avg_fps() - 60.0).abs() < 1.0);
        assert!(stats.p50_ms() > 0.0);
        assert!(stats.p95_ms() >= stats.p50_ms());
    }

    #[test]
    fn percentile_ordering() {
        let mut stats = BenchmarkStats::new();
        for i in 1..=100 {
            stats.record_frame(i as f32 / 1000.0);
        }
        stats.finalize();
        assert!(stats.p50_ms() <= stats.p90_ms());
        assert!(stats.p90_ms() <= stats.p95_ms());
        assert!(stats.p95_ms() <= stats.p99_ms());
        assert!(stats.p99_ms() <= stats.max_ms());
    }

    #[test]
    fn phase_tracking() {
        let mut stats = BenchmarkStats::new();
        stats.record_frame_with_phase(0.016, "Warmup");
        stats.record_frame_with_phase(0.016, "Particle");
        assert_eq!(stats.frame_phases.len(), 2);
        assert_eq!(stats.frame_phases[0], "Warmup");
        assert_eq!(stats.frame_phases[1], "Particle");
    }

    #[test]
    fn scene_complexity_defaults() {
        let stats = BenchmarkStats::new();
        assert_eq!(stats.scene_object_count, 0);
        assert_eq!(stats.dreamlet_capacity, 0);
        assert!(!stats.ibl_initialized);
    }

    #[test]
    fn csv_export_roundtrip() {
        let mut stats = BenchmarkStats::new();
        stats.adapter_name = "TestGPU".into();
        stats.record_frame_with_phase(0.016, "Warmup");
        stats.record_frame_with_phase(0.017, "Particle");
        stats.finalize();

        let path = std::env::temp_dir().join("dreamwell_test_benchmark.csv");
        let path_str = path.to_str().unwrap();
        stats.export_csv(path_str).unwrap();

        let content = std::fs::read_to_string(&path).unwrap();
        assert!(content.contains("TestGPU"));
        assert!(content.contains("Warmup"));
        assert!(content.contains("Particle"));
        assert!(content.contains("frame,frame_time_ms,phase"));

        let _ = std::fs::remove_file(&path);
    }
}