Newer
Older
vengine / engine-core / src / debug.rs
Maple Nebel Maple Nebel 9 days ago 8 KB things
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
    }
}