Add debug overlay

This commit is contained in:
Skyler Lehmkuhl 2025-12-12 12:19:12 -05:00
parent c2f092b5eb
commit dda1319c42
3 changed files with 313 additions and 0 deletions

View File

@ -51,3 +51,6 @@ directories = "5.0"
# Desktop notifications
notify-rust = { workspace = true }
# Debug overlay - memory tracking
memory-stats = "1.1"

View File

@ -0,0 +1,280 @@
//! 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");
});
});
}

View File

@ -33,6 +33,8 @@ mod notifications;
mod effect_thumbnails;
use effect_thumbnails::EffectThumbnailGenerator;
mod debug_overlay;
/// Lightningbeam Editor - Animation and video editing software
#[derive(Parser, Debug)]
#[command(name = "Lightningbeam Editor")]
@ -593,6 +595,11 @@ struct EditorApp {
export_orchestrator: Option<export::ExportOrchestrator>,
/// GPU-rendered effect thumbnail generator
effect_thumbnail_generator: Option<EffectThumbnailGenerator>,
/// Debug overlay (F3) state
debug_overlay_visible: bool,
debug_stats_collector: debug_overlay::DebugStatsCollector,
gpu_info: Option<wgpu::AdapterInfo>,
}
/// Import filter types for the file dialog
@ -680,6 +687,9 @@ impl EditorApp {
// Create audio extraction channel for background thread communication
let (audio_extraction_tx, audio_extraction_rx) = std::sync::mpsc::channel();
// Extract GPU info for debug overlay
let gpu_info = cc.wgpu_render_state.as_ref().map(|rs| rs.adapter.get_info());
Self {
layouts,
current_layout_index: 0,
@ -743,6 +753,11 @@ impl EditorApp {
export_progress_dialog: export::dialog::ExportProgressDialog::default(),
export_orchestrator: None,
effect_thumbnail_generator: None, // Initialized when GPU available
// Debug overlay (F3)
debug_overlay_visible: false,
debug_stats_collector: debug_overlay::DebugStatsCollector::new(),
gpu_info,
}
}
@ -3235,12 +3250,27 @@ impl eframe::App for EditorApp {
}
});
// F3 debug overlay toggle (works even when text input is active)
if ctx.input(|i| i.key_pressed(egui::Key::F3)) {
self.debug_overlay_visible = !self.debug_overlay_visible;
}
// Clear the set of audio pools with new waveforms at the end of the frame
// (Thumbnails have been invalidated above, so this can be cleared for next frame)
if !self.audio_pools_with_new_waveforms.is_empty() {
println!("🧹 [UPDATE] Clearing waveform update set: {:?}", self.audio_pools_with_new_waveforms);
}
self.audio_pools_with_new_waveforms.clear();
// Render F3 debug overlay on top of everything
if self.debug_overlay_visible {
let stats = self.debug_stats_collector.collect(
ctx,
&self.gpu_info,
self.audio_controller.as_ref(),
);
debug_overlay::render_debug_overlay(ctx, &stats);
}
}
}