Lightningbeam/lightningbeam-ui/lightningbeam-editor/src/debug_overlay.rs

281 lines
11 KiB
Rust

//! 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<String>,
pub audio_input_devices: Vec<String>,
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<Duration>,
last_frame_time: Option<Instant>,
cached_audio_devices: Vec<String>,
last_device_refresh: Option<Instant>,
cached_memory_physical_mb: usize,
cached_memory_virtual_mb: usize,
last_memory_refresh: Option<Instant>,
}
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<wgpu::AdapterInfo>,
audio_controller: Option<&std::sync::Arc<std::sync::Mutex<daw_backend::EngineController>>>,
) -> 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<f32> = 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<String> {
use cpal::traits::{HostTrait, DeviceTrait};
let host = cpal::default_host();
host.input_devices()
.ok()
.map(|devices| {
devices
.filter_map(|d| d.name().ok())
.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");
});
});
}