pub mod entity_highlight;
pub mod colliders;
pub mod transforms;
pub mod system_flow;
pub mod performance_metrics;
pub mod custom_markers;
use crate::server::brp_messages::DebugOverlayType;
#[cfg(feature = "visual_overlays")]
use bevy::prelude::*;
#[cfg(feature = "visual_overlays")]
use bevy::camera::Viewport;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
pub trait VisualOverlay: Send + Sync + std::fmt::Debug {
fn initialize(&mut self, app: &mut App);
fn update_config(&mut self, config: &serde_json::Value) -> Result<(), String>;
fn set_enabled(&mut self, enabled: bool);
fn is_enabled(&self) -> bool;
fn get_metrics(&self) -> OverlayMetrics;
fn overlay_type(&self) -> DebugOverlayType;
fn cleanup(&mut self);
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct OverlayMetrics {
pub render_time_us: u64,
pub element_count: usize,
pub memory_usage_bytes: usize,
pub frame_updates: usize,
pub active_this_frame: bool,
pub viewport_stats: HashMap<String, ViewportRenderStats>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ViewportRenderStats {
pub elements_rendered: usize,
pub render_time_us: u64,
pub active: bool,
pub viewport_size: Option<(u32, u32)>,
}
#[derive(Resource, Debug, Clone)]
pub struct ViewportConfig {
pub viewport_overlays: HashMap<String, ViewportOverlaySettings>,
pub default_settings: ViewportOverlaySettings,
pub auto_detect_viewports: bool,
pub max_viewports: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ViewportOverlaySettings {
pub enabled: bool,
pub render_budget_us: u64,
pub max_elements: usize,
pub lod_settings: LodSettings,
pub overlay_visibility: HashMap<String, bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LodSettings {
pub distance_thresholds: Vec<f32>, pub element_limits: Vec<usize>,
pub detail_levels: Vec<f32>,
}
impl Default for ViewportConfig {
fn default() -> Self {
Self {
viewport_overlays: HashMap::new(),
default_settings: ViewportOverlaySettings::default(),
auto_detect_viewports: true,
max_viewports: 8, }
}
}
impl Default for ViewportOverlaySettings {
fn default() -> Self {
Self {
enabled: true,
render_budget_us: 800, max_elements: 50, lod_settings: LodSettings::default(),
overlay_visibility: HashMap::new(),
}
}
}
impl Default for LodSettings {
fn default() -> Self {
Self {
distance_thresholds: vec![10.0, 50.0, 200.0],
element_limits: vec![100, 50, 20],
detail_levels: vec![1.0, 0.7, 0.3],
}
}
}
#[cfg_attr(feature = "visual_overlays", derive(Resource))]
#[derive(Debug)]
pub struct VisualOverlayManager {
overlays: HashMap<String, Box<dyn VisualOverlay>>,
global_enabled: bool,
performance_budget_us: u64,
total_metrics: OverlayMetrics,
}
impl VisualOverlayManager {
pub fn new() -> Self {
Self {
overlays: HashMap::new(),
global_enabled: true,
performance_budget_us: 2000, total_metrics: OverlayMetrics::default(),
}
}
pub fn initialize(&mut self, app: &mut App) {
self.register_overlay("entity_highlight", Box::new(entity_highlight::EntityHighlightOverlay::new()));
self.register_overlay("colliders", Box::new(colliders::CollidersOverlay::new()));
self.register_overlay("transforms", Box::new(transforms::TransformsOverlay::new()));
self.register_overlay("system_flow", Box::new(system_flow::SystemFlowOverlay::new()));
self.register_overlay("performance_metrics", Box::new(performance_metrics::PerformanceMetricsOverlay::new()));
for overlay in self.overlays.values_mut() {
overlay.initialize(app);
}
app.insert_resource(ViewportConfig::default());
app.add_systems(PostUpdate, (
Self::update_performance_metrics,
Self::check_performance_budget.after(Self::update_performance_metrics),
Self::manage_viewport_config,
));
}
fn register_overlay(&mut self, key: &str, overlay: Box<dyn VisualOverlay>) {
self.overlays.insert(key.to_string(), overlay);
}
pub fn set_overlay_enabled(
&mut self,
overlay_type: &DebugOverlayType,
enabled: bool,
config: Option<&serde_json::Value>,
) -> Result<(), String> {
let key = self.overlay_type_to_key(overlay_type);
if let Some(overlay) = self.overlays.get_mut(&key) {
overlay.set_enabled(enabled);
if let Some(config) = config {
overlay.update_config(config)?;
}
info!(
"Visual overlay '{}' {} with config: {:?}",
key,
if enabled { "enabled" } else { "disabled" },
config
);
Ok(())
} else {
Err(format!("Unknown overlay type: {:?}", overlay_type))
}
}
pub fn get_overlay_status(&self, overlay_type: &DebugOverlayType) -> Option<(bool, OverlayMetrics)> {
let key = self.overlay_type_to_key(overlay_type);
self.overlays.get(&key).map(|overlay| {
(overlay.is_enabled(), overlay.get_metrics())
})
}
pub fn get_all_statuses(&self) -> HashMap<String, (bool, OverlayMetrics)> {
self.overlays
.iter()
.map(|(key, overlay)| {
(key.clone(), (overlay.is_enabled(), overlay.get_metrics()))
})
.collect()
}
pub fn get_total_metrics(&self) -> &OverlayMetrics {
&self.total_metrics
}
pub fn is_performance_budget_exceeded(&self) -> bool {
self.total_metrics.render_time_us > self.performance_budget_us
}
fn overlay_type_to_key(&self, overlay_type: &DebugOverlayType) -> String {
match overlay_type {
DebugOverlayType::EntityHighlight => "entity_highlight".to_string(),
DebugOverlayType::Colliders => "colliders".to_string(),
DebugOverlayType::Transforms => "transforms".to_string(),
DebugOverlayType::SystemFlow => "system_flow".to_string(),
DebugOverlayType::PerformanceMetrics => "performance_metrics".to_string(),
DebugOverlayType::ColliderVisualization => "collider_visualization".to_string(),
DebugOverlayType::TransformGizmos => "transform_gizmos".to_string(),
DebugOverlayType::DebugMarkers => "debug_markers".to_string(),
DebugOverlayType::Custom(name) => format!("custom_{}", name),
}
}
fn update_performance_metrics(
time: Res<Time>,
mut overlay_manager: ResMut<VisualOverlayManager>,
) {
let start_time = std::time::Instant::now();
let mut total_render_time = 0;
let mut total_element_count = 0;
let mut total_memory_usage = 0;
let mut total_frame_updates = 0;
let mut any_active = false;
for overlay in overlay_manager.overlays.values() {
let metrics = overlay.get_metrics();
total_render_time += metrics.render_time_us;
total_element_count += metrics.element_count;
total_memory_usage += metrics.memory_usage_bytes;
total_frame_updates += metrics.frame_updates;
any_active |= metrics.active_this_frame;
}
overlay_manager.total_metrics = OverlayMetrics {
render_time_us: total_render_time,
element_count: total_element_count,
memory_usage_bytes: total_memory_usage,
frame_updates: total_frame_updates,
active_this_frame: any_active,
viewport_stats: HashMap::new(),
};
let execution_time = start_time.elapsed().as_micros() as u64;
overlay_manager.total_metrics.render_time_us += execution_time;
}
fn check_performance_budget(
overlay_manager: Res<VisualOverlayManager>,
) {
if overlay_manager.is_performance_budget_exceeded() {
warn!(
"Visual debug overlay performance budget exceeded: {}μs > {}μs",
overlay_manager.total_metrics.render_time_us,
overlay_manager.performance_budget_us
);
for (name, overlay) in &overlay_manager.overlays {
let metrics = overlay.get_metrics();
if metrics.render_time_us > 100 { warn!(
"Overlay '{}' performance: {}μs, {} elements",
name,
metrics.render_time_us,
metrics.element_count
);
}
}
}
}
fn manage_viewport_config(
mut viewport_config: ResMut<ViewportConfig>,
cameras: Query<(Entity, &Camera, &GlobalTransform), With<Camera>>,
) {
if !viewport_config.auto_detect_viewports {
return;
}
let mut active_viewports = std::collections::HashSet::new();
for (entity, camera, _transform) in &cameras {
if !camera.is_active {
continue;
}
let viewport_id = format!("camera_{}", entity.index());
active_viewports.insert(viewport_id.clone());
if !viewport_config.viewport_overlays.contains_key(&viewport_id) {
if viewport_config.viewport_overlays.len() < viewport_config.max_viewports {
let default = viewport_config.default_settings.clone();
viewport_config.viewport_overlays.insert(
viewport_id.clone(),
default,
);
info!("Auto-detected new viewport: {}", viewport_id);
} else {
warn!("Maximum viewport limit reached: {}", viewport_config.max_viewports);
}
}
}
let inactive_viewports: Vec<String> = viewport_config
.viewport_overlays
.keys()
.filter(|k| !active_viewports.contains(*k))
.cloned()
.collect();
for viewport_id in inactive_viewports {
viewport_config.viewport_overlays.remove(&viewport_id);
debug!("Removed inactive viewport: {}", viewport_id);
}
}
}
impl Default for VisualOverlayManager {
fn default() -> Self {
Self::new()
}
}
pub struct VisualDebugOverlayPlugin {
pub auto_initialize: bool,
}
impl Default for VisualDebugOverlayPlugin {
fn default() -> Self {
Self {
auto_initialize: true,
}
}
}
impl Plugin for VisualDebugOverlayPlugin {
fn build(&self, app: &mut App) {
if self.auto_initialize {
let mut manager = VisualOverlayManager::new();
manager.initialize(app);
app.insert_resource(manager);
} else {
app.insert_resource(VisualOverlayManager::new());
}
}
}
pub use entity_highlight::{EntityHighlightOverlay, HighlightedEntity, HighlightMode, HighlightConfig};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_overlay_type_to_key() {
let manager = VisualOverlayManager::new();
assert_eq!(
manager.overlay_type_to_key(&DebugOverlayType::EntityHighlight),
"entity_highlight"
);
assert_eq!(
manager.overlay_type_to_key(&DebugOverlayType::Colliders),
"colliders"
);
assert_eq!(
manager.overlay_type_to_key(&DebugOverlayType::Custom("test".to_string())),
"custom_test"
);
}
#[test]
fn test_performance_budget_tracking() {
let manager = VisualOverlayManager::new();
assert_eq!(manager.performance_budget_us, 2000);
assert!(!manager.is_performance_budget_exceeded()); }
}