use std::sync::atomic::Ordering;
use bevy::{
app::{Plugin, Startup, Update},
ecs::{
archetype::Archetypes,
component::Components,
entity::Entity,
error::Result,
query::Without,
resource::Resource,
schedule::IntoScheduleConfigs,
system::{Query, Res, ResMut},
world::World,
},
input::{common_conditions::input_just_pressed, keyboard::KeyCode},
log::{
Level,
tracing_subscriber::{Layer, Registry},
},
};
use bevy_egui::{
EguiContext, EguiContexts, EguiPrimaryContextPass,
egui::{self, Grid, LayerId, ScrollArea, Ui, UiBuilder, Widget},
};
use tracing::{
field::{Field, Visit},
info,
};
use crate::{EXTERNAL_SYSTEMS_CMDS, global_allocator::ALLOCATION_SIZE_ORDER, spawn_egui_camera};
pub struct DebugPlugin;
#[derive(Clone, Debug)]
pub struct LogEvent {
file: Option<String>,
line: Option<u32>,
module_path: Option<String>,
message: Option<String>,
other_fields: Vec<(String, String)>,
level: Level,
}
impl Visit for LogEvent {
fn record_str(&mut self, field: &Field, value: &str) {
match field.name() {
"message" => {
self.message = Some(value.to_owned());
}
_ => self.record_debug(field, &value),
}
}
fn record_debug(&mut self, field: &Field, value: &dyn core::fmt::Debug) {
match field.name() {
"message" => {
self.message = Some(format!("{:?}", value));
}
_ => {
self.other_fields
.push((field.name().to_owned(), format!("{:?}", value)));
}
}
}
}
#[derive(Resource, Default)]
pub struct DebugLogList(Vec<LogEvent>);
pub struct DebugLogLayer;
impl Layer<Registry> for DebugLogLayer {
fn on_event(
&self,
event: &bevy::log::tracing::Event<'_>,
_ctx: bevy::log::tracing_subscriber::layer::Context<'_, Registry>,
) {
if !event.metadata().is_event() {
return;
}
let Some(cmds) = EXTERNAL_SYSTEMS_CMDS.get() else {
return;
};
let mut log_event = LogEvent {
file: event.metadata().file().map(|s| s.to_owned()),
line: event.metadata().line(),
other_fields: vec![],
message: None,
module_path: event.metadata().module_path().map(|s| s.to_owned()),
level: *event.metadata().level(),
};
event.record(&mut log_event);
cmds.send(Box::new(move |mut c| {
c.queue(move |world: &mut World| {
let mut list = world.resource_mut::<DebugLogList>();
list.0.push(log_event);
});
Ok(())
}))
.ok();
}
}
#[derive(Resource, Default)]
struct DebugUiState {
is_open: bool,
log_window_open: bool,
world_tree_view_open: bool,
memory_debugger: bool,
}
impl Plugin for DebugPlugin {
fn build(&self, app: &mut bevy::app::App) {
app.add_systems(Startup, spawn_egui_camera)
.insert_resource(DebugUiState::default())
.insert_resource(DebugLogList::default())
.add_systems(
Update,
toggle_debug_system.run_if(input_just_pressed(KeyCode::Backslash)),
)
.add_systems(
EguiPrimaryContextPass,
(
debug_ui_log_window,
debug_ui_system_top_panel,
debug_ui_world_tree_view,
memory_debugger,
)
.run_if(|resource: Res<DebugUiState>| resource.into_inner().is_open),
);
}
}
fn toggle_debug_system(mut resource: ResMut<DebugUiState>) {
info!("toggeling dev tools");
resource.is_open ^= true;
}
fn debug_ui_system_top_panel(
mut contexts: EguiContexts,
mut ui_state: ResMut<DebugUiState>,
) -> Result {
let ctx = contexts.ctx_mut()?;
let mut viewport_ui = Ui::new(
ctx.clone(),
"viewport".into(),
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(ctx.viewport_rect()),
);
egui::Panel::top("top_panel").show(&mut viewport_ui, |ui| {
ui.menu_button("Widgets", |ui| {
ui.toggle_value(&mut ui_state.log_window_open, "Log window");
ui.toggle_value(&mut ui_state.world_tree_view_open, "World view");
ui.toggle_value(&mut ui_state.memory_debugger, "Mem debugger");
})
});
Ok(())
}
fn debug_ui_log_window(
mut contexts: EguiContexts,
mut ui_state: ResMut<DebugUiState>,
log_list: Res<DebugLogList>,
) -> Result {
egui::Window::new("Debug")
.open(&mut ui_state.log_window_open)
.show(contexts.ctx_mut()?, |ui| {
ScrollArea::vertical().show(ui, |ui| {
Grid::new("Log Grid")
.striped(true)
.num_columns(2)
.show(ui, |ui| {
for log in &log_list.0 {
log.ui(ui);
ui.end_row();
}
})
});
});
Ok(())
}
fn size_to_category(size: usize) -> (&'static str, usize) {
match size.ilog2() {
10..20 => ("KiB", 2usize.pow(10)),
20..30 => ("MiB", 2usize.pow(20)),
30..40 => ("GiB", 2usize.pow(30)),
40..50 => ("TiB", 2usize.pow(40)),
50..60 => ("PiB", 2usize.pow(50)),
60..70 => ("EiB", 2usize.pow(60)),
_ => ("B", 1),
}
}
fn memory_debugger(mut contexts: EguiContexts, mut ui_state: ResMut<DebugUiState>) -> Result {
egui::Window::new("Mem Debug")
.open(&mut ui_state.memory_debugger)
.show(contexts.ctx_mut()?, |ui| {
use tikv_jemalloc_ctl::{epoch, stats};
epoch::advance().unwrap();
let allocated = stats::allocated::read().unwrap();
let (alloc_postfix, alloc_constant) = size_to_category(allocated);
let resident = stats::resident::read().unwrap();
let (res_postfix, res_constant) = size_to_category(allocated);
ui.label(format!(
"allocated: {}{}",
allocated / alloc_constant,
alloc_postfix
));
ui.label(format!(
"resident: {}{}",
resident / res_constant,
res_postfix
));
ScrollArea::vertical().show(ui, |ui| {
for (i, order) in ALLOCATION_SIZE_ORDER.iter().enumerate() {
let order_val = 2usize.pow(i as u32);
let (postfix, constant) = size_to_category(order_val);
ui.label(format!(
"~{}{}: {}",
order_val / constant,
postfix,
order.load(Ordering::Relaxed)
));
}
});
});
Ok(())
}
fn debug_ui_world_tree_view(
mut contexts: EguiContexts,
entities: Query<Entity, Without<EguiContext>>,
archetypes: &Archetypes,
components: &Components,
mut ui_state: ResMut<DebugUiState>,
) -> Result {
egui::Window::new("World View")
.open(&mut ui_state.world_tree_view_open)
.show(contexts.ctx_mut()?, |ui| {
ScrollArea::vertical().show(ui, |ui| {
Grid::new("World Grid")
.striped(true)
.num_columns(1)
.show(ui, |ui| {
for entity in entities {
let Some(archetype) = archetypes
.iter()
.find(|a| a.entities().iter().any(|v| v.id() == entity))
else {
continue;
};
let ent_ci = archetype.components();
for comp in ent_ci {
let Some(component_info) = components.get_info(*comp) else {
continue;
};
ui.label(&component_info.name().to_string());
}
ui.end_row();
}
})
});
});
Ok(())
}
impl Widget for &LogEvent {
fn ui(self, ui: &mut egui::Ui) -> egui::Response {
ui.label(self.level.as_str());
let response = ui.label(
self.message
.as_ref()
.map(|v| v.as_str())
.unwrap_or("NO MESSAGE"),
);
response
}
}