pub struct BenchmarkConfig {
pub width: u32,
pub height: u32,
pub duration_secs: f32,
pub vram_budget_bytes: u64,
pub vsync: bool,
pub warmup_end: f32,
pub particle_end: f32,
pub convergence_end: f32,
pub settled_end: f32,
pub materialized_end: f32,
pub max_dt: f32,
pub csv_path: String,
}
impl Default for BenchmarkConfig {
fn default() -> Self {
Self {
width: 1920,
height: 1080,
duration_secs: 60.0,
vram_budget_bytes: 8 * 1024 * 1024 * 1024, vsync: false,
warmup_end: 5.0,
particle_end: 15.0,
convergence_end: 30.0,
settled_end: 40.0,
materialized_end: 52.0,
max_dt: 0.1,
csv_path: "dreamwell_matter_benchmark.csv".into(),
}
}
}
impl BenchmarkConfig {
pub fn aspect_ratio(&self) -> f32 {
if self.height > 0 {
self.width as f32 / self.height as f32
} else {
16.0 / 9.0
}
}
pub fn present_mode(&self) -> wgpu::PresentMode {
if self.vsync {
wgpu::PresentMode::AutoVsync
} else {
wgpu::PresentMode::Immediate
}
}
pub fn validate(&self) -> Vec<String> {
let mut warnings = Vec::new();
if self.width < 640 || self.height < 480 {
warnings.push(format!("Resolution {}x{} below minimum 640x480", self.width, self.height));
}
if self.duration_secs < 5.0 {
warnings.push(format!("Duration {:.1}s too short for meaningful measurement", self.duration_secs));
}
if self.warmup_end >= self.particle_end {
warnings.push("Warmup phase overlaps particle phase".into());
}
if self.materialized_end >= self.duration_secs {
warnings.push("Materialized phase extends beyond benchmark duration".into());
}
if self.max_dt <= 0.0 || self.max_dt > 1.0 {
warnings.push(format!("max_dt {:.3}s outside safe range (0, 1]", self.max_dt));
}
warnings
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config_is_valid() {
let config = BenchmarkConfig::default();
let warnings = config.validate();
assert!(warnings.is_empty(), "Default config should have no warnings: {:?}", warnings);
}
#[test]
fn aspect_ratio_correct() {
let config = BenchmarkConfig::default();
let aspect = config.aspect_ratio();
assert!((aspect - 16.0 / 9.0).abs() < 0.01);
}
#[test]
fn zero_height_fallback() {
let config = BenchmarkConfig { height: 0, ..Default::default() };
assert!((config.aspect_ratio() - 16.0 / 9.0).abs() < 0.01);
}
#[test]
fn validation_catches_bad_resolution() {
let config = BenchmarkConfig { width: 100, height: 100, ..Default::default() };
let warnings = config.validate();
assert!(!warnings.is_empty());
}
#[test]
fn validation_catches_short_duration() {
let config = BenchmarkConfig { duration_secs: 2.0, ..Default::default() };
let warnings = config.validate();
assert!(!warnings.is_empty());
}
#[test]
fn validation_catches_overlapping_phases() {
let config = BenchmarkConfig { warmup_end: 15.0, particle_end: 10.0, ..Default::default() };
let warnings = config.validate();
assert!(!warnings.is_empty());
}
#[test]
fn present_mode_selection() {
assert_eq!(BenchmarkConfig::default().present_mode(), wgpu::PresentMode::Immediate);
assert_eq!(BenchmarkConfig { vsync: true, ..Default::default() }.present_mode(), wgpu::PresentMode::AutoVsync);
}
#[test]
fn vram_budget_is_8gib() {
let config = BenchmarkConfig::default();
assert_eq!(config.vram_budget_bytes, 8 * 1024 * 1024 * 1024);
}
#[test]
fn max_dt_is_safe() {
let config = BenchmarkConfig::default();
assert!(config.max_dt > 0.0);
assert!(config.max_dt <= 0.5); }
#[test]
fn phase_boundaries_monotonic() {
let config = BenchmarkConfig::default();
assert!(config.warmup_end < config.particle_end);
assert!(config.particle_end < config.convergence_end);
assert!(config.convergence_end < config.settled_end);
assert!(config.settled_end < config.materialized_end);
assert!(config.materialized_end < config.duration_secs);
}
#[test]
fn validation_catches_bad_max_dt() {
let config = BenchmarkConfig { max_dt: -1.0, ..Default::default() };
assert!(!config.validate().is_empty());
}
#[test]
fn csv_path_is_nonempty() {
let config = BenchmarkConfig::default();
assert!(!config.csv_path.is_empty());
}
}