kitgit

tirbofish/dropbear · diff

cc363a7 · Thribhu K

fix: terrible performance within the GameEditor fix: saving didnt work properly and hiked up CPU usage to 100%. fix: attempted to make ResourceReferenceType::Bytes be Arc<[u8]> for cloning (memory opt).

Unverified

diff --git a/crates/dropbear-engine/src/utils.rs b/crates/dropbear-engine/src/utils.rs
index f8012d9..fe94353 100644
--- a/crates/dropbear-engine/src/utils.rs
+++ b/crates/dropbear-engine/src/utils.rs
@@ -4,6 +4,7 @@ use crate::procedural::ProcedurallyGeneratedObject;
 use serde::{Deserialize, Serialize};
 use std::fmt::{Display, Formatter};
 use std::path::Path;
+use std::sync::Arc;
 
 pub const EUCA_SCHEME: &str = "euca://";
 
@@ -97,7 +98,7 @@ pub enum ResourceReferenceType {
 
     /// The content in bytes. Sometimes, there is a model that is loaded into memory through the
     /// [`include_bytes!`] macro, this type stores it.
-    Bytes(Vec<u8>),
+    Bytes(Arc<[u8]>),
 
     /// An object that can be generated at runtime with the usage of vertices and indices, as well
     /// as a solid grey mesh.
@@ -145,10 +146,15 @@ impl ResourceReference {
     /// Creates a new `ResourceReference` from bytes
     pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self {
         Self {
-            ref_type: ResourceReferenceType::Bytes(bytes.as_ref().to_vec()),
+            ref_type: ResourceReferenceType::Bytes(Arc::<[u8]>::from(bytes.as_ref())),
         }
     }
 
+    /// Returns true when this reference points to a file path rather than embedded bytes.
+    pub fn is_file_backed(&self) -> bool {
+        matches!(self.ref_type, ResourceReferenceType::File(_))
+    }
+
     pub fn from_reference(ref_type: ResourceReferenceType) -> Self {
         match ref_type {
             ResourceReferenceType::File(reference) => {
@@ -240,7 +246,7 @@ impl ResourceReference {
 
     pub fn as_bytes(&self) -> Option<&[u8]> {
         match &self.ref_type {
-            ResourceReferenceType::Bytes(bytes) => Some(bytes),
+            ResourceReferenceType::Bytes(bytes) => Some(bytes.as_ref()),
             _ => None,
         }
     }
diff --git a/crates/eucalyptus-core/src/component.rs b/crates/eucalyptus-core/src/component.rs
index 45aeaa2..1752a6e 100644
--- a/crates/eucalyptus-core/src/component.rs
+++ b/crates/eucalyptus-core/src/component.rs
@@ -24,7 +24,33 @@ use std::time::{SystemTime, UNIX_EPOCH};
 
 pub use typetag::*;
 
+// todo: fix up this ---------------
 pub const DRAGGED_ASSET_ID: &str = "dragged_asset_reference";
+fn sanitize_resource_reference_for_scene_save(
+    reference: ResourceReference,
+    context: &str,
+) -> ResourceReference {
+    match reference.ref_type {
+        ResourceReferenceType::Bytes(bytes) => {
+            log::warn!(
+                "Dropping in-memory byte resource for {} during scene save ({} bytes); use a file-backed asset for persistence",
+                context,
+                bytes.len()
+            );
+            ResourceReference::default()
+        }
+        _ => reference,
+    }
+}
+
+fn sanitize_optional_resource_reference_for_scene_save(
+    reference: Option<ResourceReference>,
+    context: &str,
+) -> Option<ResourceReference> {
+    reference.map(|resource| sanitize_resource_reference_for_scene_save(resource, context))
+}
+
+// ------------------------
 
 pub struct ComponentRegistry {
     /// Maps TypeId to ComponentDescriptor for quick lookups
@@ -589,10 +615,11 @@ impl Component for MeshRenderer {
                                 ResourceReferenceType::File(_) => {
                                     let path = dif.resolve().ok()?;
                                     let bytes = std::fs::read(&path).ok()?;
-                                    let texture = TextureBuilder::new(&graphics.device)
+                                    let mut texture = TextureBuilder::new(&graphics.device)
                                         .from_bytes(graphics.clone(), bytes.as_slice())
                                         .label(label.as_str())
                                         .build();
+                                    texture.reference = Some(dif.clone());
                                     let mut registry = ASSET_REGISTRY.write();
                                     Some(Ok(registry.add_texture_with_label(label.clone(), texture)))
                                 }
@@ -662,7 +689,13 @@ impl Component for MeshRenderer {
         let asset = ASSET_REGISTRY.read();
         let model = asset.get_model(self.model());
         let (label, handle) = if let Some(model) = model.as_ref() {
-            (model.label.clone(), model.path.clone())
+            (
+                model.label.clone(),
+                sanitize_resource_reference_for_scene_save(
+                    model.path.clone(),
+                    "mesh renderer model handle",
+                ),
+            )
         } else {
             if !self.model().is_null() {
                 log::warn!(
@@ -689,6 +722,10 @@ impl Component for MeshRenderer {
                     .get_texture(mat.diffuse_texture)
                     .and_then(|t| t.reference.clone())
             };
+            let diffuse_texture = sanitize_optional_resource_reference_for_scene_save(
+                diffuse_texture,
+                "mesh renderer diffuse texture",
+            );
 
             let normal_texture = if default_material.map(|default| default.normal_texture)
                 == Some(mat.normal_texture)
@@ -698,6 +735,10 @@ impl Component for MeshRenderer {
                 mat.normal_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
+            let normal_texture = sanitize_optional_resource_reference_for_scene_save(
+                normal_texture,
+                "mesh renderer normal texture",
+            );
 
             let emissive_texture = if default_material.map(|default| default.emissive_texture)
                 == Some(mat.emissive_texture)
@@ -707,6 +748,10 @@ impl Component for MeshRenderer {
                 mat.emissive_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
+            let emissive_texture = sanitize_optional_resource_reference_for_scene_save(
+                emissive_texture,
+                "mesh renderer emissive texture",
+            );
 
             let occlusion_texture = if default_material.map(|default| default.occlusion_texture)
                 == Some(mat.occlusion_texture)
@@ -716,6 +761,10 @@ impl Component for MeshRenderer {
                 mat.occlusion_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
+            let occlusion_texture = sanitize_optional_resource_reference_for_scene_save(
+                occlusion_texture,
+                "mesh renderer occlusion texture",
+            );
 
             let metallic_roughness_texture = if default_material
                 .map(|default| default.metallic_roughness_texture)
@@ -726,6 +775,10 @@ impl Component for MeshRenderer {
                 mat.metallic_roughness_texture
                     .and_then(|h| asset.get_texture(h).and_then(|t| t.reference.clone()))
             };
+            let metallic_roughness_texture = sanitize_optional_resource_reference_for_scene_save(
+                metallic_roughness_texture,
+                "mesh renderer metallic-roughness texture",
+            );
 
             texture_override.insert(
                 label.to_string(),
diff --git a/crates/eucalyptus-core/src/config.rs b/crates/eucalyptus-core/src/config.rs
index 50f2ee5..cfa0996 100644
--- a/crates/eucalyptus-core/src/config.rs
+++ b/crates/eucalyptus-core/src/config.rs
@@ -80,6 +80,7 @@ impl ProjectConfig {
     /// reload or write scene/resource/source configs. This is intended for small editor-facing
     /// settings updates (like per-model import scales) where reloading configs would be disruptive.
     pub fn write_project_only(&mut self) -> anyhow::Result<()> {
+        log::debug!("Writing the project config");
         self.date_last_accessed = format!("{}", Utc::now().format("%Y-%m-%d %H:%M:%S"));
 
         let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default())
@@ -90,6 +91,7 @@ impl ProjectConfig {
             .join(format!("{}.eucp", self.project_name.clone().to_lowercase()));
 
         fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?;
+        log::debug!("Written to {}", config_path.display());
         Ok(())
     }
 
diff --git a/crates/eucalyptus-core/src/scene.rs b/crates/eucalyptus-core/src/scene.rs
index 994ceb7..ce90a3e 100644
--- a/crates/eucalyptus-core/src/scene.rs
+++ b/crates/eucalyptus-core/src/scene.rs
@@ -141,14 +141,18 @@ impl SceneConfig {
 
     /// Write the scene config to a .eucs file
     pub fn write_to(&self, project_path: impl AsRef<Path>) -> anyhow::Result<()> {
+        log::debug!("Writing scene config");
         let ron_str = ron::ser::to_string_pretty(&self, PrettyConfig::default())
             .map_err(|e| anyhow::anyhow!("RON serialization error: {}", e))?;
 
         let scenes_dir = project_path.as_ref().join("scenes");
+        log::debug!("Creating scene dir at {}", scenes_dir.display());
         fs::create_dir_all(&scenes_dir)?;
 
         let config_path = scenes_dir.join(format!("{}.eucs", self.scene_name));
-        fs::write(&config_path, ron_str).map_err(|e| anyhow::anyhow!(e.to_string()))?;
+        log::debug!("Writing scene info to {}", config_path.display());
+        fs::write(&config_path, ron_str)?;
+        log::debug!("Wrote scene config to {}", config_path.display());
         Ok(())
     }
 
diff --git a/crates/eucalyptus-editor/Cargo.toml b/crates/eucalyptus-editor/Cargo.toml
index 92a90a1..8857b34 100644
--- a/crates/eucalyptus-editor/Cargo.toml
+++ b/crates/eucalyptus-editor/Cargo.toml
@@ -11,7 +11,7 @@ readme = "README.md"
 eucalyptus-core = { path = "../eucalyptus-core", features = ["editor"] }
 magna-carta = { path = "../magna-carta" }
 redback-runtime = { path = "../redback-runtime", features = ["debug"]}
-kino-ui = { path = "../kino-ui" }
+kino-ui = { path = "../kino-ui", features = ["trace"]}
 
 anyhow.workspace = true
 app_dirs2.workspace = true
diff --git a/crates/eucalyptus-editor/src/editor/entity_list.rs b/crates/eucalyptus-editor/src/editor/entity_list.rs
index 70bcfe5..0a2106d 100644
--- a/crates/eucalyptus-editor/src/editor/entity_list.rs
+++ b/crates/eucalyptus-editor/src/editor/entity_list.rs
@@ -6,16 +6,19 @@ use eucalyptus_core::{
     states::{Label, PROJECT},
 };
 use hecs::{Entity, World};
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, HashMap};
 
 use crate::editor::{Editor, EditorTabDock, EditorTabDockDescriptor, EditorTabViewer, Signal, StaticallyKept, TABS_GLOBAL};
 use crate::editor::page::EditorTabVisibility;
 
 impl<'a> EditorTabViewer<'a> {
     pub(crate) fn entity_list(&mut self, ui: &mut egui::Ui) {
+        puffin::profile_function!();
         let mut cfg = TABS_GLOBAL.lock();
 
-        let (_response, action) = egui_ltreeview::TreeView::new(egui::Id::new(
+        let (_response, action) = {
+            puffin::profile_scope!("entity_list.tree_build");
+            egui_ltreeview::TreeView::new(egui::Id::new(
             "model_entity_list",
         ))
             .show(ui, |builder| {
@@ -44,9 +47,12 @@ impl<'a> EditorTabViewer<'a> {
                     entity: Entity,
                     world: &mut World,
                     registry: &ComponentRegistry,
+                    component_ids_by_entity: &HashMap<Entity, Vec<u64>>,
+                    rigidbody_component_id: Option<u64>,
                     cfg: &mut StaticallyKept,
                     signal: &mut Signal,
                 ) -> anyhow::Result<()> {
+                    puffin::profile_scope!("entity_list.add_entity_to_tree");
                     let entity_id = entity.to_bits().get();
                     let label = if let Ok(label) = world.query_one::<&Label>(entity).get()
                     {
@@ -112,51 +118,57 @@ impl<'a> EditorTabViewer<'a> {
                             }),
                     );
 
-                    let components = registry.extract_all_components(world, entity);
-
-                    for component in components.iter() {
-                        let Some(component_type_id) =
-                            registry.id_for_component(component.as_ref())
-                        else {
-                            log_once::warn_once!(
-                                    "Component missing registry id, skipping tree entry"
-                                );
-                            continue;
-                        };
+                    if let Some(component_ids) = component_ids_by_entity.get(&entity) {
                         let display_id = crate::features::is_enabled(crate::features::ShowComponentTypeIDInEditor);
-
-                        let component_node_id =
-                            cfg.component_node_id(entity, component_type_id as u64);
-                        let display = registry
-                            .get_descriptor_by_numeric_id(component_type_id)
-                            .map(|desc| if display_id { format!("{} (id #{component_type_id})", desc.type_name) } else { desc.type_name.clone() })
-                            .unwrap_or_else(|| if display_id { format!("Unknown (id #{component_type_id})") } else { String::from("Unknown")});
-
                         let has_rigidbody = world.get::<&RigidBody>(entity).is_ok();
                         let has_collider = world.get::<&ColliderGroup>(entity).is_ok();
 
-                        let node = NodeBuilder::leaf(component_node_id)
-                            .label_ui(|ui| {
-                                ui.label(display.clone());
+                        for component_type_id in component_ids {
+                            let component_node_id =
+                                cfg.component_node_id(entity, *component_type_id);
+                            let display = registry
+                                .get_descriptor_by_numeric_id(*component_type_id)
+                                .map(|desc| {
+                                    if display_id {
+                                        format!("{} (id #{component_type_id})", desc.type_name)
+                                    } else {
+                                        desc.type_name.clone()
+                                    }
+                                })
+                                .unwrap_or_else(|| {
+                                    if display_id {
+                                        format!("Unknown (id #{component_type_id})")
+                                    } else {
+                                        String::from("Unknown")
+                                    }
+                                });
 
-                                if has_rigidbody && !has_collider && component.typetag_name().contains("RigidBody") {
-                                    ui.add_space(4.0);
-                                    ui.small_button("⚠")
-                                        .on_hover_text("RigidBody has no colliders! Add the ColliderGroup component");
-                                }
-                            })
-                            .context_menu(|ui| {
-                                if ui.button("Remove Component").clicked() {
-                                    registry.remove_component_by_id(
-                                        world,
-                                        entity,
-                                        component_type_id,
-                                    );
-                                    ui.close();
-                                }
-                            });
-
-                        builder.node(node);
+                            let node = NodeBuilder::leaf(component_node_id)
+                                .label_ui(|ui| {
+                                    ui.label(display.clone());
+
+                                    if has_rigidbody
+                                        && !has_collider
+                                        && Some(*component_type_id) == rigidbody_component_id
+                                    {
+                                        ui.add_space(4.0);
+                                        ui.small_button("⚠")
+                                            .on_hover_text("RigidBody has no colliders! Add the ColliderGroup component");
+                                    }
+                                })
+                                .context_menu(|ui| {
+                                    if ui.button("Remove Component").clicked() {
+                                        registry.remove_component_by_id(
+                                            world,
+                                            entity,
+                                            *component_type_id,
+                                        );
+                                        ui.close();
+                                    }
+                                });
+
+                            builder.node(node);
+                        }
                     }
 
                     let children_entities = if let Ok(children) = world.get::<&Children>(entity) {
@@ -167,7 +179,16 @@ impl<'a> EditorTabViewer<'a> {
 
                     for child in children_entities {
                         if let Err(e) =
-                            add_entity_to_tree(builder, child, world, registry, cfg, signal)
+                            add_entity_to_tree(
+                                builder,
+                                child,
+                                world,
+                                registry,
+                                component_ids_by_entity,
+                                rigidbody_component_id,
+                                cfg,
+                                signal,
+                            )
                         {
                             log_once::error_once!(
                                     "Failed to add child entity to tree, skipping: {}",
@@ -181,6 +202,32 @@ impl<'a> EditorTabViewer<'a> {
                     Ok(())
                 }
 
+                let mut component_ids_by_entity: HashMap<Entity, Vec<u64>> = HashMap::new();
+                let rigidbody_component_id = self
+                    .component_registry
+                    .iter_available_components()
+                    .find_map(|(id, desc)| {
+                        if desc.fqtn == "eucalyptus_core::physics::rigidbody::RigidBody" {
+                            Some(id)
+                        } else {
+                            None
+                        }
+                    });
+                {
+                    puffin::profile_scope!("entity_list.index_components");
+                    for (component_id, _) in self.component_registry.iter_available_components() {
+                        for entity in self
+                            .component_registry
+                            .find_entities_by_numeric_id(self.world, component_id)
+                        {
+                            component_ids_by_entity
+                                .entry(entity)
+                                .or_default()
+                                .push(component_id);
+                        }
+                    }
+                }
+
                 let root_entities: Vec<Entity> = self
                     .world
                     .query::<Entity>()
@@ -190,11 +237,14 @@ impl<'a> EditorTabViewer<'a> {
                     .collect();
 
                 for entity in root_entities {
+                    puffin::profile_scope!("entity_list.root_entity");
                     if let Err(e) = add_entity_to_tree(
                         builder,
                         entity,
                         &mut self.world,
                         &self.component_registry,
+                        &component_ids_by_entity,
+                        rigidbody_component_id,
                         &mut cfg,
                         self.signal,
                     ) {
@@ -206,8 +256,10 @@ impl<'a> EditorTabViewer<'a> {
                 }
 
                 builder.close_dir();
-            });
+            })
+        };
 
+        puffin::profile_scope!("entity_list.actions");
         for i in action {
             match i {
                 egui_ltreeview::Action::SetSelected(items) => {
diff --git a/crates/eucalyptus-editor/src/editor/input.rs b/crates/eucalyptus-editor/src/editor/input.rs
index f1f9df2..b9a232b 100644
--- a/crates/eucalyptus-editor/src/editor/input.rs
+++ b/crates/eucalyptus-editor/src/editor/input.rs
@@ -32,6 +32,7 @@ impl Keyboard for Editor {
             || self.input_state.pressed_keys.contains(&KeyCode::ShiftRight);
 
         let is_double_press = self.double_key_pressed(key);
+        let is_key_repeat = self.input_state.pressed_keys.contains(&key);
 
         let is_playing = matches!(self.editor_state, EditorState::Playing);
 
@@ -112,7 +113,7 @@ impl Keyboard for Editor {
                 }
             }
             KeyCode::KeyQ => {
-                if ctrl_pressed && !is_playing {
+                if ctrl_pressed && !is_playing && !is_key_repeat {
                     match self.save_project_config() {
                         Ok(_) => {}
                         Err(e) => {
@@ -212,7 +213,7 @@ impl Keyboard for Editor {
                 }
             }
             KeyCode::KeyS => {
-                if ctrl_pressed {
+                if ctrl_pressed && !is_key_repeat {
                     if !is_playing {
                         match self.save_project_config() {
                             Ok(_) => {
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index 226078a..a6c7bcf 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -406,6 +406,7 @@ impl Editor {
 
     /// Save the current world state to the active scene
     pub fn save_current_scene(&mut self) -> anyhow::Result<()> {
+        log::debug!("Saving current scene");
         let mut scenes = SCENES.write();
 
         if scenes.is_empty() {
@@ -430,15 +431,14 @@ impl Editor {
             scene.scene_name
         );
 
-        let labels = self
-            .world
-            .query::<(Entity, &Label)>()
-            .iter()
-            .map(|(e, l)| (e, l.clone()))
-            .collect::<Vec<_>>();
+        let entity_ids = self.world.query::<Entity>().iter().collect::<Vec<_>>();
 
-        for (id, label) in labels {
-            let entity_label = label.clone();
+        for id in entity_ids {
+            let Ok(label) = self.world.get::<&Label>(id) else {
+                log::warn!("Skipping entity {:?} without Label during save", id);
+                continue;
+            };
+            let entity_label = (*label).clone();
 
             let components = self
                 .component_registry
@@ -461,13 +461,13 @@ impl Editor {
             }
 
             let scene_entity = SceneEntity {
-                label: entity_label.clone(),
+                label: entity_label,
                 components,
                 entity_id: Some(id),
             };
 
+            log::debug!("Saved entity: {}", scene_entity.label);
             scene.entities.push(scene_entity);
-            log::debug!("Saved entity: {}", entity_label);
         }
 
         log::info!(
@@ -476,49 +476,24 @@ impl Editor {
             scene.scene_name
         );
 
-        Ok(())
-    }
-
-    fn persist_active_scene_to_disk(&self) -> anyhow::Result<()> {
-        let target_scene_name = self.current_scene_name.clone().or_else(|| {
-            let scenes = SCENES.read();
-            scenes.first().map(|scene| scene.scene_name.clone())
-        });
-
-        let Some(scene_name) = target_scene_name else {
-            return Ok(());
-        };
-
-        let scene_clone = {
-            let scenes = SCENES.read();
-            scenes
-                .iter()
-                .find(|scene| scene.scene_name == scene_name)
-                .cloned()
-        };
-
-        let Some(scene_clone) = scene_clone else {
-            log::warn!(
-                "Attempted to persist scene '{}' but it is not loaded",
-                scene_name
-            );
-            return Ok(());
-        };
-
         let project_path = {
             let project = PROJECT.read();
             project.project_path.clone()
         };
 
-        scene_clone.write_to(&project_path)?;
+        log::debug!("Writing scene to {}", project_path.display());
+        scene.write_to(&project_path)?;
+        log::debug!("Saved active scene '{}' to disk", scene.scene_name);
+
         Ok(())
     }
 
     pub fn save_project_config(&mut self) -> anyhow::Result<()> {
+        log::debug!("starting save of project config");
         self.save_current_scene()?;
-        self.persist_active_scene_to_disk()?;
-
+        
         {
+            log::debug!("Writing to editor settings");
             let mut config = EDITOR_SETTINGS.write();
             config.game_editor_dock_state = Some(self.game_editor_dock_state.clone());
             config.ui_editor_dock_state = Some(self.ui_editor_dock_state.clone());
@@ -526,8 +501,9 @@ impl Editor {
         }
 
         {
+            log::debug!("Writing to project");
             let mut config = PROJECT.write();
-            config.write_to_all()?;
+            config.write_project_only()?;
         }
 
         Ok(())
@@ -723,7 +699,6 @@ impl Editor {
 
         if should_persist_current {
             self.save_current_scene()?;
-            self.persist_active_scene_to_disk()?;
         }
 
         if let Some(current) = self.current_scene_name.as_deref() {
@@ -913,15 +888,21 @@ impl Editor {
     }
 
     pub fn show_ui(&mut self, ctx: &Context, graphics: Arc<SharedGraphicsContext>) {
-        if let Some(scene_name) = self.pending_scene_creation.take() {
-            let result = self.create_new_scene(scene_name.as_str());
-            self.new_scene_name.clear();
-            if let Err(e) = result {
-                fatal!("Failed to create scene '{}': {}", scene_name, e);
+        puffin::profile_function!();
+
+        {
+            puffin::profile_scope!("show_ui.pending_scene_creation");
+            if let Some(scene_name) = self.pending_scene_creation.take() {
+                let result = self.create_new_scene(scene_name.as_str());
+                self.new_scene_name.clear();
+                if let Err(e) = result {
+                    fatal!("Failed to create scene '{}': {}", scene_name, e);
+                }
             }
         }
 
         egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
+            puffin::profile_scope!("show_ui.top_bar");
             let top_bar_rect = ui.max_rect();
 
             ui.horizontal(|ui| {
@@ -1226,6 +1207,7 @@ impl Editor {
             );
 
             ui.scope_builder(egui::UiBuilder::new().max_rect(center_rect), |ui| {
+                puffin::profile_scope!("show_ui.page_switcher");
                 ui.with_layout(
                     egui::Layout::centered_and_justified(egui::Direction::LeftToRight),
                     |ui| {
@@ -1254,6 +1236,7 @@ impl Editor {
             );
 
             ui.scope_builder(egui::UiBuilder::new().max_rect(right_rect), |ui| {
+                puffin::profile_scope!("show_ui.play_controls");
                 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                     let can_play = matches!(self.editor_state, EditorState::Playing);
                     ui.group(|ui| {
@@ -1293,6 +1276,7 @@ impl Editor {
         egui::TopBottomPanel::bottom("status bar")
             .resizable(false)
             .show(ctx, |ui| {
+            puffin::profile_scope!("show_ui.status_bar");
                 let active_camera = *self.active_camera.lock();
                 let (label, is_debug, active_entity) = if let Some(entity) = active_camera {
                     let camera_label = self
@@ -1363,6 +1347,7 @@ impl Editor {
 
         let Some(view) = self.texture_id else {
             egui::CentralPanel::default().show(ctx, |ui| {
+                puffin::profile_scope!("show_ui.viewport_initialising");
                 ui.centered_and_justified(|ui| {
                     ui.label("Viewport is still initialising...");
                 });
@@ -1371,6 +1356,7 @@ impl Editor {
         };
 
         egui::CentralPanel::default().show(ctx, |ui| {
+            puffin::profile_scope!("show_ui.dock_area");
             DockArea::new(if self.current_page.contains(EditorTabVisibility::GameEditor) {
                 &mut self.game_editor_dock_state
             } else if self.current_page.contains(EditorTabVisibility::UIEditor) {
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 8a0c280..adcd122 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -492,6 +492,7 @@ impl Scene for Editor {
                 .query::<(Entity, &MeshRenderer, Option<&mut AnimationComponent>)>();
 
             for (entity, renderer, animation) in query.iter() {
+                puffin::profile_scope!(format!("locating {:?}", entity));
                 let handle = renderer.model();
                 if handle.is_null() {
                     continue;
@@ -610,6 +611,7 @@ impl Scene for Editor {
 
                     render_pass.set_pipeline(light_pipeline.pipeline());
                     for light in &lights {
+                        puffin::profile_scope!("rendering light", &light.label);
                         render_pass.set_vertex_buffer(1, light.instance_buffer.buffer().slice(..));
                         if !light.component.visible {
                             continue;
@@ -763,6 +765,7 @@ impl Scene for Editor {
                 morph_weight_count,
             ) in animated_instances
             {
+                puffin::profile_scope!("rendering animated model", format!("{:?}", entity));
                 let Some(model) = registry.get_model(Handle::new(handle)) else {
                     log_once::error_once!("Missing model handle {} in registry", handle);
                     continue;
@@ -1021,6 +1024,7 @@ impl Scene for Editor {
 
         // kino billboard renderer (late stage, runtime parity)
         {
+            puffin::profile_scope!("rendering billboard targets");
             if let Some(kino) = &mut self.kino {
                 let mut kino_encoder = CommandEncoder::new(graphics.clone(), Some("kino billboard encoder"));
                 kino.render_billboard_targets(
@@ -1055,6 +1059,7 @@ impl Scene for Editor {
                     .query::<(Entity, &BillboardComponent, Option<&EntityTransform>)>();
 
                 for (entity, billboard, entity_transform) in query.iter() {
+                    puffin::profile_scope!("rendering billboard", format!("{:?}", entity));
                     if !billboard.enabled {
                         continue;
                     }
@@ -1100,6 +1105,7 @@ impl Scene for Editor {
                 }
 
                 if !billboards.is_empty() {
+                    puffin::profile_scope!("billboard render pass");
                     let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                         label: Some("editor billboard render pass"),
                         color_attachments: &[Some(wgpu::RenderPassColorAttachment {
diff --git a/crates/eucalyptus-editor/src/editor/settings/editor.rs b/crates/eucalyptus-editor/src/editor/settings/editor.rs
index 23c053d..aef08b7 100644
--- a/crates/eucalyptus-editor/src/editor/settings/editor.rs
+++ b/crates/eucalyptus-editor/src/editor/settings/editor.rs
@@ -67,6 +67,7 @@ impl EditorSettings {
 
     /// Saves the current EditorSettings configuration (as shown in [EDITOR_SETTINGS]) into `{app_dir}/editor.eucc`.
     pub fn save(&self) -> anyhow::Result<()> {
+        log::debug!("Saving the current EditorSettings configuration");
         let app_data = app_dirs2::app_root(AppDataType::UserData, &APP_INFO)?;
         let serialized = ron::ser::to_string_pretty(&self, ron::ser::PrettyConfig::default())?;
         std::fs::write(app_data.join("editor.eucc"), serialized)?;
diff --git a/crates/kino-ui/Cargo.toml b/crates/kino-ui/Cargo.toml
index 6c4f824..5ee7279 100644
--- a/crates/kino-ui/Cargo.toml
+++ b/crates/kino-ui/Cargo.toml
@@ -21,6 +21,8 @@ winit.workspace = true
 glyphon.workspace = true
 serde = { workspace = true, optional = true}
 typetag = { workspace = true, optional = true}
+puffin = { workspace = true, optional = true}
 
 [features]
 ser = ["serde", "typetag"]
+trace = ["puffin"]
diff --git a/crates/kino-ui/src/lib.rs b/crates/kino-ui/src/lib.rs
index 8a652ea..c61347d 100644
--- a/crates/kino-ui/src/lib.rs
+++ b/crates/kino-ui/src/lib.rs
@@ -256,6 +256,7 @@ impl KinoState {
         queue: &wgpu::Queue,
         encoder: &mut wgpu::CommandEncoder,
     ) {
+        puffin::profile_function!();
         self.render_target_cache.tick();
 
         let targets = self
@@ -269,6 +270,7 @@ impl KinoState {
             .collect::<Vec<_>>();
 
         for target in targets {
+            puffin::profile_scope!("rendering target", format!("{:?}", target));
             let mut geometry = self
                 .batches
                 .remove(&target)