use std::io::Write;
pub struct BenchmarkStats {
frame_times_ms: Vec<f32>,
frame_phases: Vec<String>,
pub adapter_name: String,
pub adapter_backend: String,
pub resolution: [u32; 2],
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,
pub quality_preset: String,
pub gi_cost_ms: f32,
pub screen_trace_hit_rate: f32,
pub rt_shadow_cost_ms: f32,
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,
}
}
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;
}
pub fn record_frame(&mut self, dt: f32) {
self.record_frame_with_phase(dt, "");
}
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
}
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
}
pub fn p1_low_ms(&self) -> f32 { Self::percentile_of(&self.sorted_times(), 99.0) }
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)
}
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!();
}
pub fn export_csv(&self, path: &str) -> Result<(), std::io::Error> {
let mut file = std::fs::File::create(path)?;
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)?;
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)?;
}
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);
}
}