//! F3 Debug Overlay //! //! Displays performance metrics and system info similar to Minecraft's F3 screen. //! Press F3 to toggle visibility. use eframe::egui; use std::collections::VecDeque; use std::time::{Duration, Instant}; const FRAME_HISTORY_SIZE: usize = 60; // Track last 60 frames for FPS stats const DEVICE_REFRESH_INTERVAL: Duration = Duration::from_secs(2); // Refresh devices every 2 seconds const MEMORY_REFRESH_INTERVAL: Duration = Duration::from_millis(500); // Refresh memory every 500ms /// Statistics displayed in debug overlay #[derive(Debug, Clone)] pub struct DebugStats { pub fps_current: f32, // Current frame FPS (unsmoothed) pub fps_min: f32, // Minimum FPS over last 60 frames pub fps_avg: f32, // Average FPS over last 60 frames pub fps_max: f32, // Maximum FPS over last 60 frames pub frame_time_ms: f32, // Current frame time in milliseconds pub memory_physical_mb: usize, pub memory_virtual_mb: usize, pub gpu_name: String, pub gpu_backend: String, pub gpu_driver: String, pub midi_devices: Vec, pub audio_input_devices: Vec, pub has_pointer: bool, // Performance metrics for each section pub timing_memory_us: u64, pub timing_gpu_us: u64, pub timing_midi_us: u64, pub timing_audio_us: u64, pub timing_pointer_us: u64, pub timing_total_us: u64, } /// Collects and aggregates debug statistics pub struct DebugStatsCollector { frame_times: VecDeque, last_frame_time: Option, cached_audio_devices: Vec, last_device_refresh: Option, cached_memory_physical_mb: usize, cached_memory_virtual_mb: usize, last_memory_refresh: Option, } impl DebugStatsCollector { pub fn new() -> Self { Self { frame_times: VecDeque::with_capacity(FRAME_HISTORY_SIZE), last_frame_time: None, cached_audio_devices: Vec::new(), last_device_refresh: None, cached_memory_physical_mb: 0, cached_memory_virtual_mb: 0, last_memory_refresh: None, } } /// Collect current debug statistics pub fn collect( &mut self, ctx: &egui::Context, gpu_info: &Option, audio_controller: Option<&std::sync::Arc>>, ) -> DebugStats { let collection_start = Instant::now(); // Calculate actual frame time based on real elapsed time let now = Instant::now(); let frame_duration = if let Some(last_time) = self.last_frame_time { now.duration_since(last_time) } else { Duration::from_secs_f32(1.0 / 60.0) // Default to 60 FPS for first frame }; self.last_frame_time = Some(now); // Store frame duration in history self.frame_times.push_back(frame_duration); if self.frame_times.len() > FRAME_HISTORY_SIZE { self.frame_times.pop_front(); } // Calculate FPS stats from actual frame durations let frame_time_ms = frame_duration.as_secs_f32() * 1000.0; let fps_current = 1.0 / frame_duration.as_secs_f32(); let (fps_min, fps_avg, fps_max) = if !self.frame_times.is_empty() { let fps_values: Vec = self.frame_times .iter() .map(|dt| 1.0 / dt.as_secs_f32()) .collect(); let min = fps_values.iter().copied().fold(f32::INFINITY, f32::min); let max = fps_values.iter().copied().fold(f32::NEG_INFINITY, f32::max); let sum: f32 = fps_values.iter().sum(); let avg = sum / fps_values.len() as f32; (min, avg, max) } else { (fps_current, fps_current, fps_current) }; // Collect memory stats with timing - cache and refresh every 500ms let t0 = Instant::now(); let should_refresh_memory = self.last_memory_refresh .map(|last| now.duration_since(last) >= MEMORY_REFRESH_INTERVAL) .unwrap_or(true); if should_refresh_memory { if let Some(usage) = memory_stats::memory_stats() { self.cached_memory_physical_mb = usage.physical_mem / 1024 / 1024; self.cached_memory_virtual_mb = usage.virtual_mem / 1024 / 1024; } self.last_memory_refresh = Some(now); } let memory_physical_mb = self.cached_memory_physical_mb; let memory_virtual_mb = self.cached_memory_virtual_mb; let timing_memory_us = t0.elapsed().as_micros() as u64; // Extract GPU info with timing let t1 = Instant::now(); let (gpu_name, gpu_backend, gpu_driver) = if let Some(info) = gpu_info { ( info.name.clone(), format!("{:?}", info.backend), format!("{} ({})", info.driver, info.driver_info), ) } else { ("Unknown".to_string(), "Unknown".to_string(), "Unknown".to_string()) }; let timing_gpu_us = t1.elapsed().as_micros() as u64; // Collect MIDI devices with timing let t2 = Instant::now(); let midi_devices = if let Some(_controller) = audio_controller { // TODO: Add method to audio controller to get MIDI device names // For now, return empty vec vec![] } else { vec![] }; let timing_midi_us = t2.elapsed().as_micros() as u64; // Refresh audio input devices only every 2 seconds to avoid performance issues let t3 = Instant::now(); let should_refresh_devices = self.last_device_refresh .map(|last| now.duration_since(last) >= DEVICE_REFRESH_INTERVAL) .unwrap_or(true); if should_refresh_devices { self.cached_audio_devices = enumerate_audio_input_devices(); self.last_device_refresh = Some(now); } let audio_input_devices = self.cached_audio_devices.clone(); let timing_audio_us = t3.elapsed().as_micros() as u64; // Detect pointer usage with timing let t4 = Instant::now(); let has_pointer = ctx.input(|i| { i.pointer.is_decidedly_dragging() || i.pointer.any_down() || i.pointer.any_pressed() }); let timing_pointer_us = t4.elapsed().as_micros() as u64; let timing_total_us = collection_start.elapsed().as_micros() as u64; DebugStats { fps_current, fps_min, fps_avg, fps_max, frame_time_ms, memory_physical_mb, memory_virtual_mb, gpu_name, gpu_backend, gpu_driver, midi_devices, audio_input_devices, has_pointer, timing_memory_us, timing_gpu_us, timing_midi_us, timing_audio_us, timing_pointer_us, timing_total_us, } } } /// Enumerate audio input devices using cpal fn enumerate_audio_input_devices() -> Vec { use cpal::traits::{HostTrait, DeviceTrait}; let host = cpal::default_host(); host.input_devices() .ok() .map(|devices| { devices .filter_map(|d| d.description().ok().map(|desc| desc.name().to_string())) .collect() }) .unwrap_or_default() } /// Render the debug overlay in-window using egui::Area pub fn render_debug_overlay(ctx: &egui::Context, stats: &DebugStats) { egui::Area::new(egui::Id::new("debug_overlay_area")) .fixed_pos(egui::pos2(10.0, 10.0)) .show(ctx, |ui| { egui::Frame::new() .fill(egui::Color32::from_black_alpha(200)) .inner_margin(8.0) .show(ui, |ui| { // Use monospace font for alignment ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); // Performance section ui.colored_label(egui::Color32::YELLOW, "Performance:"); ui.label(format!( "FPS: {:.1} (min: {:.1} / avg: {:.1} / max: {:.1})", stats.fps_current, stats.fps_min, stats.fps_avg, stats.fps_max )); ui.label(format!("Frame time: {:.2} ms", stats.frame_time_ms)); ui.add_space(8.0); // Memory section with timing ui.colored_label(egui::Color32::YELLOW, format!("Memory: ({}µs)", stats.timing_memory_us)); ui.label(format!("Physical: {} MB", stats.memory_physical_mb)); ui.label(format!("Virtual: {} MB", stats.memory_virtual_mb)); ui.add_space(8.0); // Graphics section with timing ui.colored_label(egui::Color32::YELLOW, format!("Graphics: ({}µs)", stats.timing_gpu_us)); ui.label(format!("GPU: {}", stats.gpu_name)); ui.label(format!("Backend: {}", stats.gpu_backend)); ui.label(format!("Driver: {}", stats.gpu_driver)); ui.add_space(8.0); // Input devices section with timing ui.colored_label(egui::Color32::YELLOW, format!("Input Devices: ({}µs)", stats.timing_midi_us + stats.timing_audio_us + stats.timing_pointer_us)); if stats.has_pointer { ui.label(format!("• Mouse/Trackpad ({}µs)", stats.timing_pointer_us)); } if !stats.audio_input_devices.is_empty() { ui.label(format!("• {} Audio Input(s) ({}µs)", stats.audio_input_devices.len(), stats.timing_audio_us)); for device in &stats.audio_input_devices { ui.label(format!(" - {}", device)); } } if !stats.midi_devices.is_empty() { ui.label(format!("• {} MIDI Device(s) ({}µs)", stats.midi_devices.len(), stats.timing_midi_us)); for device in &stats.midi_devices { ui.label(format!(" - {}", device)); } } ui.add_space(8.0); ui.separator(); ui.colored_label(egui::Color32::CYAN, format!("Collection time: {}µs ({:.2}ms)", stats.timing_total_us, stats.timing_total_us as f32 / 1000.0)); ui.colored_label(egui::Color32::GRAY, "Press F3 to close"); }); }); }