kitgit

tirbofish/dropbear · commit

51c240503156f18f30153d895701785c51117a23

perf: optimised editor/scene.rs fix: reapplied editor's rendering system to the runtime to get the texture fix

Unverified

Thribhu K <4tkbytes@pm.me> · 2026-03-14 06:34

view full diff

diff --git a/crates/dropbear-engine/src/animation.rs b/crates/dropbear-engine/src/animation.rs
index ec651e8..c1b1841 100644
--- a/crates/dropbear-engine/src/animation.rs
+++ b/crates/dropbear-engine/src/animation.rs
@@ -4,6 +4,7 @@ use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform};
 use glam::Mat4;
 use std::collections::HashMap;
 use std::sync::Arc;
+use dropbear_utils::Dirty;
 
 #[repr(C)]
 #[derive(Copy, Clone, Default, Debug, bytemuck::Pod, bytemuck::Zeroable)]
@@ -35,7 +36,7 @@ pub struct AnimationComponent {
     #[serde(skip)]
     pub local_pose: HashMap<usize, NodeTransform>,
     #[serde(skip)]
-    pub skinning_matrices: Vec<Mat4>,
+    pub skinning_matrices: Dirty<Vec<Mat4>>,
 
     #[serde(skip)]
     pub skinning_buffer: Option<ResizableBuffer<Mat4>>,
@@ -53,7 +54,7 @@ pub struct AnimationComponent {
     pub last_animation_index: Option<usize>,
 
     #[serde(skip)]
-    pub morph_weights: HashMap<usize, Vec<f32>>,
+    pub morph_weights: Dirty<HashMap<usize, Vec<f32>>>,
 
     #[serde(skip)]
     pub morph_weight_count: u32,
@@ -69,14 +70,14 @@ impl Clone for AnimationComponent {
             is_playing: self.is_playing,
             animation_settings: self.animation_settings.clone(),
             local_pose: HashMap::new(),
-            skinning_matrices: Vec::new(),
+            skinning_matrices: Dirty::new(Vec::new()),
             skinning_buffer: None,
             morph_deltas_buffer: None,
             morph_weights_buffer: None,
             morph_info_buffer: None,
             available_animations: Vec::new(),
             last_animation_index: None,
-            morph_weights: HashMap::new(),
+            morph_weights: Dirty::new(HashMap::new()),
             morph_weight_count: 0,
         }
     }
@@ -115,10 +116,10 @@ impl Default for AnimationComponent {
             is_playing: true,
             animation_settings: HashMap::new(),
             local_pose: HashMap::new(),
-            skinning_matrices: Vec::new(),
+            skinning_matrices: Dirty::new(Vec::new()),
             available_animations: vec![],
             last_animation_index: None,
-            morph_weights: HashMap::new(),
+            morph_weights: Dirty::new(HashMap::new()),
             morph_weight_count: 0,
             skinning_buffer: None,
             morph_deltas_buffer: None,
@@ -495,7 +496,10 @@ impl AnimationComponent {
                 )
             });
 
-            buffer.write(&graphics.device, &graphics.queue, &self.skinning_matrices);
+            if self.skinning_matrices.is_dirty() {
+                buffer.write(&graphics.device, &graphics.queue, &self.skinning_matrices);
+                self.skinning_matrices.mark_clean();
+            }
         }
 
         if has_skinning || has_morph_weights {
@@ -534,6 +538,7 @@ impl AnimationComponent {
             weights_buffer.write(&graphics.device, &graphics.queue, &flat);
             self.morph_weight_count = num_targets as u32;
 
+            // todo: this is extremely inefficient
             let info = MorphTargetInfo {
                 num_vertices: 0,
                 num_targets: num_targets as u32,
diff --git a/crates/dropbear-engine/src/lighting.rs b/crates/dropbear-engine/src/lighting.rs
index 6e84d62..030059a 100644
--- a/crates/dropbear-engine/src/lighting.rs
+++ b/crates/dropbear-engine/src/lighting.rs
@@ -8,6 +8,7 @@ use crate::{entity::Transform, model::Model};
 use glam::{DMat4, DQuat, DVec3};
 use std::fmt::{Display, Formatter};
 use std::sync::Arc;
+use dropbear_utils::Dirty;
 
 pub const MAX_LIGHTS: usize = 10;
 
@@ -267,7 +268,7 @@ impl LightComponent {
 
 #[derive(Clone)]
 pub struct Light {
-    pub uniform: LightUniform,
+    pub uniform: Dirty<LightUniform>,
     pub cube_model: Handle<Model>,
     pub label: String,
     pub buffer: UniformBuffer<LightUniform>,
@@ -284,7 +285,7 @@ impl Light {
     ) -> Self {
         puffin::profile_function!();
 
-        let uniform = LightUniform {
+        let uniform = Dirty::new(LightUniform {
             position: dvec3_to_uniform_array(light.position),
             direction: dvec3_direction_to_uniform_array(light.direction, light.outer_cutoff_angle),
             colour: dvec3_colour_to_uniform_array(
@@ -295,7 +296,7 @@ impl Light {
             linear: light.attenuation.linear,
             quadratic: light.attenuation.quadratic,
             cutoff: f32::cos(light.cutoff_angle.to_radians()),
-        };
+        });
 
         log::trace!("Created new light uniform");
 
@@ -366,13 +367,19 @@ impl Light {
 
         self.uniform.cutoff = f32::cos(light.cutoff_angle.to_radians());
 
-        self.buffer.write(&graphics.queue, &self.uniform);
+        if self.uniform.is_dirty() {
+            self.buffer.write(&graphics.queue, &self.uniform);
+        }
     }
 
-    pub fn uniform(&self) -> &LightUniform {
+    pub fn uniform(&self) -> &Dirty<LightUniform> {
         &self.uniform
     }
 
+    pub fn mark_clean(&mut self) {
+        self.uniform.mark_clean();
+    }
+
     pub fn model(&self) -> Handle<Model> {
         self.cube_model
     }
diff --git a/crates/dropbear-engine/src/pipelines/globals.rs b/crates/dropbear-engine/src/pipelines/globals.rs
index 4120217..9167ba0 100644
--- a/crates/dropbear-engine/src/pipelines/globals.rs
+++ b/crates/dropbear-engine/src/pipelines/globals.rs
@@ -1,5 +1,5 @@
 use std::sync::Arc;
-
+use dropbear_utils::Dirty;
 use crate::buffer::UniformBuffer;
 use crate::graphics::SharedGraphicsContext;
 
@@ -23,7 +23,7 @@ impl Default for Globals {
 
 #[derive(Debug, Clone)]
 pub struct GlobalsUniform {
-    pub data: Globals,
+    pub data: Dirty<Globals>,
     pub buffer: UniformBuffer<Globals>,
 }
 
@@ -31,9 +31,9 @@ impl GlobalsUniform {
     pub fn new(graphics: Arc<SharedGraphicsContext>, label: Option<&str>) -> Self {
         let label = label.unwrap_or("shader globals");
 
-        let buffer = UniformBuffer::new(&graphics.device, label);
+        let buffer: UniformBuffer<Globals> = UniformBuffer::new(&graphics.device, label);
 
-        let data = Globals::default();
+        let data = Dirty::new(Globals::default());
         buffer.write(&graphics.queue, &data);
 
         Self {
@@ -43,7 +43,10 @@ impl GlobalsUniform {
     }
 
     pub fn write(&mut self, queue: &wgpu::Queue) {
-        self.buffer.write(queue, &self.data);
+        if self.data.is_dirty() {
+            self.buffer.write(queue, &self.data);
+        }
+
     }
 
     pub fn set_num_lights(&mut self, num_lights: u32) {
diff --git a/crates/dropbear-engine/src/pipelines/light_cube.rs b/crates/dropbear-engine/src/pipelines/light_cube.rs
index 4133516..0b44d75 100644
--- a/crates/dropbear-engine/src/pipelines/light_cube.rs
+++ b/crates/dropbear-engine/src/pipelines/light_cube.rs
@@ -151,11 +151,13 @@ impl LightCubePipeline {
                 .write(&graphics.device, &graphics.queue, &[instance]);
 
             if light.component.enabled && light_index < MAX_LIGHTS {
-                let uniform = *light.uniform();
+                let uniform = light.uniform();
+                
+                if uniform.is_dirty() {
+                    light.buffer.write(&graphics.queue, &uniform);
+                }
 
-                light.buffer.write(&graphics.queue, &uniform);
-
-                light_array.lights[light_index] = uniform;
+                light_array.lights[light_index] = *uniform.get();
                 light_index += 1;
             }
         }
diff --git a/crates/dropbear-utils/Cargo.toml b/crates/dropbear-utils/Cargo.toml
index dac27af..c85c031 100644
--- a/crates/dropbear-utils/Cargo.toml
+++ b/crates/dropbear-utils/Cargo.toml
@@ -8,3 +8,4 @@ readme = "README.md"
 authors.workspace = true
 
 [dependencies]
+serde.workspace = true
\ No newline at end of file
diff --git a/crates/dropbear-utils/src/either.rs b/crates/dropbear-utils/src/either.rs
new file mode 100644
index 0000000..f37a38f
--- /dev/null
+++ b/crates/dropbear-utils/src/either.rs
@@ -0,0 +1,82 @@
+#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default)]
+pub struct Dirty<T> {
+    value: T,
+    dirty: bool,
+}
+
+impl<T> Dirty<T> {
+    /// Creates a new [`Dirty`] of type [`T`]. Marks clean on initial creation.
+    pub fn new(value: T) -> Self {
+        Self { value, dirty: false }
+    }
+
+    /// Creates a new [`Dirty`] of type [`T`]. Marks dirty on initial creation. 
+    pub fn new_dirty(value: T) -> Self {
+        Self { value, dirty: true }
+    }
+
+    /// Fetches a reference to the value.
+    /// 
+    /// Does not change the state of the cleanliness
+    pub fn get(&self) -> &T {
+        &self.value
+    }
+
+    /// Sets this to a new value, marking dirty in the process. 
+    pub fn set(&mut self, value: T) {
+        self.value = value;
+        self.dirty = true;
+    }
+
+    /// Mutates the inner value and marks dirty. 
+    pub fn mutate(&mut self, f: impl FnOnce(&mut T)) {
+        f(&mut self.value);
+        self.dirty = true;
+    }
+
+    /// Returns the dirtiness of the value.
+    pub fn is_dirty(&self) -> bool {
+        self.dirty
+    }
+
+    /// Marks the value as clean.
+    pub fn mark_clean(&mut self) {
+        self.dirty = false;
+    }
+
+    /// Marks the value as dirty. 
+    pub fn mark_dirty(&mut self) {
+        self.dirty = true;
+    }
+
+    /// Takes the value and clears the dirty flag if dirty, returning None if clean.
+    pub fn take_if_dirty(&mut self) -> Option<&T> {
+        if self.dirty {
+            self.dirty = false;
+            Some(&self.value)
+        } else {
+            None
+        }
+    }
+}
+
+impl<T: Clone> Dirty<T> {
+    pub fn get_clean(&mut self) -> T {
+        self.dirty = false;
+        self.value.clone()
+    }
+}
+
+impl<T> std::ops::Deref for Dirty<T> {
+    type Target = T;
+    fn deref(&self) -> &T {
+        &self.value
+    }
+}
+
+impl<T> std::ops::DerefMut for Dirty<T> {
+    fn deref_mut(&mut self) -> &mut T {
+        self.dirty = true;
+        &mut self.value
+    }
+}
\ No newline at end of file
diff --git a/crates/dropbear-utils/src/lib.rs b/crates/dropbear-utils/src/lib.rs
index 16eeb44..a9a451c 100644
--- a/crates/dropbear-utils/src/lib.rs
+++ b/crates/dropbear-utils/src/lib.rs
@@ -1,3 +1,5 @@
 mod hashmap;
+pub mod either;
 
-pub use hashmap::*;
\ No newline at end of file
+pub use hashmap::*;
+pub use either::*;
\ No newline at end of file
diff --git a/crates/eucalyptus-core/src/scene/scripting.rs b/crates/eucalyptus-core/src/scene/scripting.rs
index 0cd31a3..1bd9b44 100644
--- a/crates/eucalyptus-core/src/scene/scripting.rs
+++ b/crates/eucalyptus-core/src/scene/scripting.rs
@@ -29,7 +29,6 @@ pub mod shared {
             scene_name: scene_name.clone(),
         };
 
-        // Send load command
         command_buffer
             .try_send(CommandBuffer::LoadSceneAsync(handle))
             .map_err(|_| DropbearNativeError::SendError)?;
diff --git a/crates/eucalyptus-editor/src/editor/mod.rs b/crates/eucalyptus-editor/src/editor/mod.rs
index a6c7bcf..1db224c 100644
--- a/crates/eucalyptus-editor/src/editor/mod.rs
+++ b/crates/eucalyptus-editor/src/editor/mod.rs
@@ -78,7 +78,7 @@ use winit::dpi::PhysicalSize;
 use dropbear_engine::multisampling::AntiAliasingMode;
 use winit::window::{CursorGrabMode, WindowAttributes};
 use winit::{keyboard::KeyCode, window::Window};
-use dropbear_engine::animation::MAX_MORPH_WEIGHTS;
+use dropbear_engine::animation::{MorphTargetInfo, MAX_MORPH_WEIGHTS};
 use crate::editor::page::EditorTabVisibility;
 use crate::editor::ui::UiEditor;
 
@@ -116,6 +116,10 @@ pub struct Editor {
     pub(crate) default_morph_weights_buffer: Option<wgpu::Buffer>,
     pub(crate) default_morph_info_buffer: Option<wgpu::Buffer>,
     pub(crate) default_animation_bind_group: Option<wgpu::BindGroup>,
+    pub(crate) static_batches: HashMap<u64, Vec<(Entity, InstanceRaw)>>,
+    pub(crate) animated_instances: Vec<(Entity, u64, InstanceRaw, wgpu::Buffer, wgpu::Buffer, wgpu::Buffer, u32)>,
+    pub(crate) animated_bind_group_cache: HashMap<Entity, (u64, wgpu::BindGroup)>,
+    pub(crate) last_morph_info_per_mesh: HashMap<u32, MorphTargetInfo>,  // key = morph_deltas_offset
 
     pub active_camera: Arc<Mutex<Option<Entity>>>,
 
@@ -332,10 +336,14 @@ impl Editor {
             default_morph_weights_buffer: None,
             default_morph_info_buffer: None,
             default_animation_bind_group: None,
+            static_batches: Default::default(),
+            animated_instances: vec![],
+            animated_bind_group_cache: Default::default(),
             dt: 60.0,
             ui_editor_dock_state: DockState::new(vec![]),
             current_page: EditorTabVisibility::GameEditor,
             ui_editor: UiEditor::new(),
+            last_morph_info_per_mesh: Default::default(),
         })
     }
 
diff --git a/crates/eucalyptus-editor/src/editor/scene.rs b/crates/eucalyptus-editor/src/editor/scene.rs
index 5938e24..030d79a 100644
--- a/crates/eucalyptus-editor/src/editor/scene.rs
+++ b/crates/eucalyptus-editor/src/editor/scene.rs
@@ -28,6 +28,7 @@ use std::{
     fs,
     path::{Path, PathBuf},
 };
+use std::hash::{DefaultHasher, Hash, Hasher};
 use winit::{event::WindowEvent, event_loop::ActiveEventLoop, keyboard::KeyCode};
 use winit::event::{MouseScrollDelta, TouchPhase};
 use kino_ui::rendering::KinoRenderTargetId;
@@ -425,6 +426,7 @@ impl Scene for Editor {
             return;
         };
 
+        // clear viewport render pass
         {
             puffin::profile_scope!("Clearing viewport");
             let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
@@ -460,30 +462,27 @@ impl Scene for Editor {
             light_pipeline.update(graphics.clone(), &self.world);
         }
 
-        let lights = {
+        let (lights, enabled_light_count) = {
             puffin::profile_scope!("Locating lights");
             let mut lights = Vec::new();
-            let mut query = self.world.query::<&Light>();
-            for light in query.iter() {
+            let mut enabled = 0u32;
+            for light in self.world.query::<&Light>().iter() {
+                if light.component.enabled {
+                    enabled += 1;
+                }
                 lights.push(light.clone());
             }
-            lights
+            (lights, enabled)
         };
 
         if let Some(globals) = &mut self.shader_globals {
             puffin::profile_scope!("Fetching globals");
-            let enabled_count = lights
-                .iter()
-                .filter(|light| light.component.enabled)
-                .count() as u32;
-            globals.set_num_lights(enabled_count);
+            globals.set_num_lights(enabled_light_count);
             globals.write(&graphics.queue);
         }
 
-        let mut static_batches: HashMap<u64, Vec<(Entity, InstanceRaw)>> = HashMap::new();
-        let mut animated_instances: Vec<
-            (Entity, u64, InstanceRaw, wgpu::Buffer, wgpu::Buffer, wgpu::Buffer, u32),
-        > = Vec::new();
+        self.static_batches.clear();
+        self.animated_instances.clear();
 
         {
             puffin::profile_scope!("finding all renderers and animation components");
@@ -499,11 +498,13 @@ impl Scene for Editor {
                 }
 
                 let instance = renderer.instance.to_raw();
+
                 if let Some(animation) = animation {
-                    let has_skinning = !animation.skinning_matrices.is_empty();
-                    let has_morph_weights = !animation.morph_weights.is_empty();
-                    if !has_skinning && !has_morph_weights {
-                        static_batches
+                    let has_skinning   = !animation.skinning_matrices.is_empty();
+                    let has_morph      = !animation.morph_weights.is_empty();
+
+                    if !has_skinning && !has_morph {
+                        self.static_batches
                             .entry(handle.id)
                             .or_default()
                             .push((entity, instance));
@@ -512,53 +513,56 @@ impl Scene for Editor {
 
                     animation.prepare_gpu_resources(graphics.clone());
 
-                    let skinning_buffer = if let Some(buffer) = animation
+                    let skinning_buffer = match animation
                         .skinning_buffer
                         .as_ref()
-                        .map(|buffer| buffer.buffer().clone())
+                        .map(|b| b.buffer().clone())
                     {
-                        buffer
-                    } else if !has_skinning {
-                        let Some(default_skinning_buffer) = self.default_skinning_buffer.as_ref()
-                        else {
-                            static_batches
+                        Some(buf) => buf,
+                        None if !has_skinning => {
+                            let Some(default) = self.default_skinning_buffer.as_ref() else {
+                                self.static_batches
+                                    .entry(handle.id)
+                                    .or_default()
+                                    .push((entity, instance));
+                                continue;
+                            };
+                            default.clone()
+                        }
+                        None => {
+                            self.static_batches
                                 .entry(handle.id)
                                 .or_default()
                                 .push((entity, instance));
                             continue;
-                        };
-                        default_skinning_buffer.clone()
-                    } else {
-                        static_batches
-                            .entry(handle.id)
-                            .or_default()
-                            .push((entity, instance));
-                        continue;
+                        }
                     };
+
                     let Some(morph_weights_buffer) = animation
                         .morph_weights_buffer
                         .as_ref()
-                        .map(|buffer| buffer.buffer().clone())
+                        .map(|b| b.buffer().clone())
                     else {
-                        static_batches
+                        self.static_batches
                             .entry(handle.id)
                             .or_default()
                             .push((entity, instance));
                         continue;
                     };
+
                     let Some(morph_info_buffer) = animation
                         .morph_info_buffer
                         .as_ref()
-                        .map(|buffer| buffer.buffer().clone())
+                        .map(|b| b.buffer().clone())
                     else {
-                        static_batches
+                        self.static_batches
                             .entry(handle.id)
                             .or_default()
                             .push((entity, instance));
                         continue;
                     };
 
-                    animated_instances.push((
+                    self.animated_instances.push((
                         entity,
                         handle.id,
                         instance,
@@ -568,7 +572,7 @@ impl Scene for Editor {
                         animation.morph_weight_count,
                     ));
                 } else {
-                    static_batches
+                    self.static_batches
                         .entry(handle.id)
                         .or_default()
                         .push((entity, instance));
@@ -577,21 +581,23 @@ impl Scene for Editor {
         }
 
         let registry = ASSET_REGISTRY.read();
+
+        let mut model_cache: HashMap<u64, _> = HashMap::new();
         let mut prepared_models = Vec::new();
-        for (handle, batched_instances) in static_batches {
+        for (handle, batched_instances) in &self.static_batches {
             puffin::profile_scope!("preparing models");
-            let Some(model) = registry.get_model(Handle::new(handle)) else {
+            let Some(model) = registry.get_model(Handle::new(*handle)) else {
                 log_once::error_once!("Missing model handle {} in registry", handle);
                 continue;
             };
 
-            let entity = batched_instances.first().map(|(entity, _)| *entity);
+            let entity = batched_instances.first().map(|(e, _)| *e);
             let instances: Vec<InstanceRaw> = batched_instances
-                .into_iter()
-                .map(|(_, instance)| instance)
+                .iter()
+                .map(|(_, inst)| *inst)
                 .collect();
 
-            let instance_buffer = self.instance_buffer_cache.entry(handle).or_insert_with(|| {
+            let instance_buffer = self.instance_buffer_cache.entry(*handle).or_insert_with(|| {
                 ResizableBuffer::new(
                     &graphics.device,
                     instances.len().max(1),
@@ -601,9 +607,19 @@ impl Scene for Editor {
             });
             instance_buffer.write(&graphics.device, &graphics.queue, &instances);
 
-            prepared_models.push((model, handle, instances.len() as u32, entity));
+            model_cache.insert(*handle, model.clone());
+            prepared_models.push((model, *handle, instances.len() as u32, entity));
         }
 
+        for (_, handle, ..) in &self.animated_instances {
+            if !model_cache.contains_key(handle) {
+                if let Some(model) = registry.get_model(Handle::new(*handle)) {
+                    model_cache.insert(*handle, model);
+                }
+            }
+        }
+
+        // light cube rendering
         if let Some(light_pipeline) = &self.light_cube_pipeline {
             if let Some(l) = lights.first()
                 && let Some(model) = registry.get_model(l.cube_model)
@@ -648,7 +664,6 @@ impl Scene for Editor {
             }
         }
 
-        // model rendering
         let sky = self
             .sky_pipeline
             .as_ref()
@@ -664,30 +679,28 @@ impl Scene for Editor {
         // static models
         if let Some(_) = &self.light_cube_pipeline {
             puffin::profile_scope!("model render pass");
+
+            let default_skinning_buffer = self
+                .default_skinning_buffer
+                .as_ref()
+                .expect("Default skinning buffer not initialised");
+            let default_morph_weights_buffer = self
+                .default_morph_weights_buffer
+                .as_ref()
+                .expect("Default morph weights buffer not initialised");
+            let default_morph_info_buffer = self
+                .default_morph_info_buffer
+                .as_ref()
+                .expect("Default morph info buffer not initialised");
+            let per_frame_bind_group = pipeline
+                .per_frame
+                .as_ref()
+                .expect("Per-frame bind group not initialised")
+                .clone();
+
             for (model, handle, instance_count, entity) in prepared_models {
-                let Some(entity) = entity else {
-                    continue;
-                };
-                let Ok(renderer) = self.world.get::<&MeshRenderer>(entity) else {
-                    continue;
-                };
-                let default_skinning_buffer = self
-                    .default_skinning_buffer
-                    .as_ref()
-                    .expect("Default skinning buffer not initialised");
-                let default_morph_weights_buffer = self
-                    .default_morph_weights_buffer
-                    .as_ref()
-                    .expect("Default morph weights buffer not initialised");
-                let default_morph_info_buffer = self
-                    .default_morph_info_buffer
-                    .as_ref()
-                    .expect("Default morph info buffer not initialised");
-                let per_frame_bind_group = pipeline
-                    .per_frame
-                    .as_ref()
-                    .expect("Per-frame bind group not initialised")
-                    .clone();
+                let Some(entity) = entity else { continue };
+                let Ok(renderer) = self.world.get::<&MeshRenderer>(entity) else { continue };
 
                 let morph_deltas_buffer = model
                     .morph_deltas_buffer
@@ -728,13 +741,10 @@ impl Scene for Editor {
                     occlusion_query_set: None,
                     timestamp_writes: None,
                 });
+
                 render_pass.set_pipeline(pipeline.pipeline());
-                if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) {
-                    render_pass
-                        .set_vertex_buffer(1, instance_buffer.slice(instance_count as usize));
-                } else {
-                    continue;
-                }
+                let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) else { continue };
+                render_pass.set_vertex_buffer(1, instance_buffer.slice(instance_count as usize));
 
                 for mesh in &model.meshes {
                     let mut weights = mesh.morph_default_weights.clone();
@@ -757,25 +767,40 @@ impl Scene for Editor {
                         num_targets: mesh.morph_target_count,
                         base_offset: mesh.morph_deltas_offset,
                         weight_offset: 0,
-                        uses_morph: if mesh.morph_target_count > 0 && !weights.is_empty() {
-                            1
-                        } else {
-                            0
-                        },
+                        uses_morph: if mesh.morph_target_count > 0 && !weights.is_empty() { 1 } else { 0 },
                         _padding: Default::default(),
                     };
 
-                    graphics
-                        .queue
-                        .write_buffer(default_morph_info_buffer, 0, bytemuck::bytes_of(&info));
+                    let cache_key = mesh.morph_deltas_offset;
+                    let needs_write = self
+                        .last_morph_info_per_mesh
+                        .get(&cache_key)
+                        .map_or(true, |prev| {
+                            prev.num_vertices != info.num_vertices
+                                || prev.num_targets != info.num_targets
+                                || prev.base_offset != info.base_offset
+                                || prev.uses_morph != info.uses_morph
+                        });
+
+                    if needs_write {
+                        graphics.queue.write_buffer(
+                            default_morph_info_buffer,
+                            0,
+                            bytemuck::bytes_of(&info),
+                        );
+                        self.last_morph_info_per_mesh.insert(cache_key, info);
+                    }
 
                     let material = &model.materials[mesh.material];
                     let material = if let Some(mat) = renderer.material_snapshot.get(&material.name) {
                         mat
                     } else {
-                        log_once::warn_once!("Unable to locate MeshRenderer's material_snapshot for that specific material");
+                        log_once::warn_once!(
+                        "Unable to locate MeshRenderer's material_snapshot for that specific material"
+                    );
                         material
                     };
+
                     render_pass.draw_mesh_instanced(
                         mesh,
                         material,
@@ -791,50 +816,19 @@ impl Scene for Editor {
         // animated models
         if let Some(_) = &self.light_cube_pipeline {
             puffin::profile_scope!("animated model render pass");
-            for (
-                entity,
-                handle,
-                instance,
-                skinning_buffer,
-                morph_weights_buffer,
-                morph_info_buffer,
-                morph_weight_count,
-            ) in animated_instances
-            {
-                let Ok(renderer) = self.world.get::<&MeshRenderer>(entity) else {
-                    continue;
-                };
-                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;
-                };
-
-                let morph_deltas_buffer = model
-                    .morph_deltas_buffer
-                    .as_ref()
-                    .or(self.default_morph_deltas_buffer.as_ref());
-                let Some(morph_deltas_buffer) = morph_deltas_buffer else {
-                    log_once::error_once!("Missing morph deltas buffer for model {}", handle);
-                    continue;
-                };
 
-                let animation_bind_group = pipeline.animation_bind_group(
-                    graphics.clone(),
-                    &skinning_buffer,
-                    &morph_deltas_buffer,
-                    &morph_weights_buffer,
-                    &morph_info_buffer,
-                );
-                let per_frame_bind_group = pipeline
-                    .per_frame
-                    .as_ref()
-                    .expect("Per-frame bind group not initialised")
-                    .clone();
+            let per_frame_bind_group = pipeline
+                .per_frame
+                .as_ref()
+                .expect("Per-frame bind group not initialised")
+                .clone();
 
+            for (entity, _, instance, _, _, _, _)
+            in &self.animated_instances
+            {
                 let instance_buffer = self
                     .animated_instance_buffers
-                    .entry(entity)
+                    .entry(*entity)
                     .or_insert_with(|| {
                         ResizableBuffer::new(
                             &graphics.device,
@@ -843,64 +837,111 @@ impl Scene for Editor {
                             "animated instance buffer",
                         )
                     });
-                instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
+                instance_buffer.write(&graphics.device, &graphics.queue, &[*instance]);
+            }
 
-                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("model render pass (animated)"),
-                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                        view: hdr.render_view(),
-                        depth_slice: None,
-                        resolve_target: hdr.resolve_target(),
-                        ops: wgpu::Operations {
-                            load: wgpu::LoadOp::Load,
-                            store: wgpu::StoreOp::Store,
-                        },
-                    })],
-                    depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
-                        view: &graphics.depth_texture.view,
-                        depth_ops: Some(wgpu::Operations {
-                            load: wgpu::LoadOp::Load,
-                            store: wgpu::StoreOp::Store,
+            for (entity, handle, _, skinning_buffer, morph_weights_buffer, morph_info_buffer, morph_weight_count)
+            in &self.animated_instances
+            {
+                let Ok(renderer) = self.world.get::<&MeshRenderer>(*entity) else { continue };
+                puffin::profile_scope!("rendering animated model", format!("{:?}", entity));
+                {
+                    let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                        label: Some("animated model render pass"),
+                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                            view: hdr.render_view(),
+                            depth_slice: None,
+                            resolve_target: hdr.resolve_target(),
+                            ops: wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            },
+                        })],
+                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                            view: &graphics.depth_texture.view,
+                            depth_ops: Some(wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            }),
+                            stencil_ops: None,
                         }),
-                        stencil_ops: None,
-                    }),
-                    occlusion_query_set: None,
-                    timestamp_writes: None,
-                });
+                        occlusion_query_set: None,
+                        timestamp_writes: None,
+                    });
 
-                render_pass.set_pipeline(pipeline.pipeline());
-                render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
+                    render_pass.set_pipeline(pipeline.pipeline());
 
-                for mesh in &model.meshes {
-                    let mesh_target_count = mesh.morph_target_count.min(morph_weight_count);
-                    let info = MorphTargetInfo {
-                        num_vertices: mesh.morph_vertex_count,
-                        num_targets: mesh_target_count,
-                        base_offset: mesh.morph_deltas_offset,
-                        weight_offset: 0,
-                        uses_morph: if mesh_target_count > 0 { 1 } else { 0 },
-                        _padding: Default::default(),
+                    let Some(model) = model_cache.get(handle) else {
+                        log_once::error_once!("Missing model handle {} in registry", handle);
+                        continue;
                     };
 
-                    graphics
-                        .queue
-                        .write_buffer(&morph_info_buffer, 0, bytemuck::bytes_of(&info));
+                    let morph_deltas_buffer = model
+                        .morph_deltas_buffer
+                        .as_ref()
+                        .or(self.default_morph_deltas_buffer.as_ref());
+                    let Some(morph_deltas_buffer) = morph_deltas_buffer else {
+                        log_once::error_once!("Missing morph deltas buffer for model {}", handle);
+                        continue;
+                    };
 
-                    let material = &model.materials[mesh.material];
-                    let material = if let Some(mat) = renderer.material_snapshot.get(&material.name) {
-                        mat
-                    } else {
-                        log_once::warn_once!("Unable to locate MeshRenderer's material_snapshot for that specific material");
-                        material
+                    let mut hasher = DefaultHasher::new();
+                    skinning_buffer.hash(&mut hasher);
+                    let bind_group_stamp = hasher.finish();
+                    let animation_bind_group = {
+                        let cached = self.animated_bind_group_cache.get(entity);
+                        if cached.map_or(true, |(stamp, _)| *stamp != bind_group_stamp) {
+                            let bg = pipeline.animation_bind_group(
+                                graphics.clone(),
+                                skinning_buffer,
+                                morph_deltas_buffer,
+                                morph_weights_buffer,
+                                morph_info_buffer,
+                            );
+                            self.animated_bind_group_cache.insert(*entity, (bind_group_stamp, bg));
+                        }
+                        &self.animated_bind_group_cache[entity].1
                     };
-                    render_pass.draw_mesh_instanced(
-                        mesh,
-                        material,
-                        0..1,
-                        &per_frame_bind_group,
-                        &animation_bind_group,
-                        environment_bind_group,
-                    );
+
+                    let Some(instance_buffer) = self.animated_instance_buffers.get(entity) else { continue };
+                    render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
+
+                    for mesh in &model.meshes {
+                        let mesh_target_count = mesh.morph_target_count.min(*morph_weight_count);
+
+                        let info = MorphTargetInfo {
+                            num_vertices: mesh.morph_vertex_count,
+                            num_targets: mesh_target_count,
+                            base_offset: mesh.morph_deltas_offset,
+                            weight_offset: 0,
+                            uses_morph: if mesh_target_count > 0 { 1 } else { 0 },
+                            _padding: Default::default(),
+                        };
+
+                        graphics
+                            .queue
+                            .write_buffer(morph_info_buffer, 0, bytemuck::bytes_of(&info));
+
+                        let material = &model.materials[mesh.material];
+                        let material =
+                            if let Some(mat) = renderer.material_snapshot.get(&material.name) {
+                                mat
+                            } else {
+                                log_once::warn_once!(
+                                    "Unable to locate MeshRenderer's material_snapshot for that specific material"
+                                );
+                                material
+                            };
+
+                        render_pass.draw_mesh_instanced(
+                            mesh,
+                            material,
+                            0..1,
+                            &per_frame_bind_group,
+                            animation_bind_group,
+                            environment_bind_group,
+                        );
+                    }
                 }
             }
         }
@@ -1067,7 +1108,7 @@ impl Scene for Editor {
             }
         }
 
-        // kino billboard renderer (late stage, runtime parity)
+        // kino billboard renderer
         {
             puffin::profile_scope!("rendering billboard targets");
             if let Some(kino) = &mut self.kino {
@@ -1193,6 +1234,7 @@ impl Scene for Editor {
             log_once::error_once!("{}", e);
         }
 
+        // kino hud renderer
         if let Some(kino) = &mut self.kino {
             let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder"));
             kino.render(&graphics.device, &graphics.queue, &mut encoder, hdr.view());
diff --git a/crates/redback-runtime/Cargo.toml b/crates/redback-runtime/Cargo.toml
index 6e4bd89..0737371 100644
--- a/crates/redback-runtime/Cargo.toml
+++ b/crates/redback-runtime/Cargo.toml
@@ -33,6 +33,7 @@ egui.workspace = true
 egui_extras.workspace = true
 glam.workspace = true
 wgpu.workspace = true
+puffin = "0.19.1"
 #yakui-winit.workspace = true
 #yakui.workspace = true
 #yakui-wgpu.workspace = true
diff --git a/crates/redback-runtime/src/lib.rs b/crates/redback-runtime/src/lib.rs
index 96de6d9..f3e4a1c 100644
--- a/crates/redback-runtime/src/lib.rs
+++ b/crates/redback-runtime/src/lib.rs
@@ -41,6 +41,7 @@ use std::sync::Arc;
 use wgpu::SurfaceConfiguration;
 use wgpu::util::DeviceExt;
 use winit::window::Fullscreen;
+use dropbear_engine::animation::MorphTargetInfo;
 
 mod command;
 mod input;
@@ -147,6 +148,10 @@ pub struct PlayMode {
     default_morph_info_buffer: Option<wgpu::Buffer>,
     default_animation_bind_group: Option<wgpu::BindGroup>,
     billboard_pipeline: Option<BillboardPipeline>,
+    pub(crate) static_batches: HashMap<u64, Vec<(Entity, InstanceRaw)>>,
+    pub(crate) animated_instances: Vec<(Entity, u64, InstanceRaw, wgpu::Buffer, wgpu::Buffer, wgpu::Buffer, u32)>,
+    pub(crate) animated_bind_group_cache: HashMap<Entity, (u64, wgpu::BindGroup)>,
+    pub(crate) last_morph_info_per_mesh: HashMap<u32, MorphTargetInfo>,
 
     initial_scene: Option<String>,
     current_scene: Option<String>,
@@ -223,7 +228,6 @@ impl PlayMode {
             collision_event_receiver: Some(ce_r),
             collision_force_event_receiver: Some(cfe_r),
             event_collector,
-            // yakui_winit: None,
             display_settings: DisplaySettings {
                 window_mode: WindowMode::Windowed,
                 maintain_aspect_ratio: true,
@@ -241,6 +245,10 @@ impl PlayMode {
             default_morph_info_buffer: None,
             default_animation_bind_group: None,
             billboard_pipeline: None,
+            static_batches: Default::default(),
+            animated_instances: vec![],
+            animated_bind_group_cache: Default::default(),
+            last_morph_info_per_mesh: Default::default(),
         };
 
         log::debug!("Created new play mode instance");
diff --git a/crates/redback-runtime/src/scene.rs b/crates/redback-runtime/src/scene.rs
index a296c81..8dd7e17 100644
--- a/crates/redback-runtime/src/scene.rs
+++ b/crates/redback-runtime/src/scene.rs
@@ -27,11 +27,12 @@ use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResu
 use eucalyptus_core::states::SCENES;
 use eucalyptus_core::states::{Label, PROJECT};
 use eucalyptus_core::ui::HUDComponent;
-use glam::{DVec3, Mat3, Mat4, Quat, Vec2, Vec3};
+use glam::{DVec3, Mat4, Quat, Vec2, Vec3};
 use hecs::Entity;
 use kino_ui::WidgetTree;
 use kino_ui::rendering::KinoRenderTargetId;
 use std::collections::HashMap;
+use std::hash::{DefaultHasher, Hash, Hasher};
 use winit::event::WindowEvent;
 use winit::event_loop::ActiveEventLoop;
 
@@ -596,7 +597,8 @@ impl Scene for PlayMode {
 
         let mut encoder = CommandEncoder::new(graphics.clone(), Some("runtime viewport encoder"));
 
-        let Some(active_camera) = self.active_camera else {
+        let active_camera = { self.active_camera.as_ref().cloned() };
+        let Some(active_camera) = active_camera else {
             return;
         };
         log_once::debug_once!("Active camera found: {:?}", active_camera);
@@ -619,7 +621,9 @@ impl Scene for PlayMode {
             return;
         };
 
+        // clear viewport render pass
         {
+            puffin::profile_scope!("Clearing viewport");
             let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                 label: Some("viewport clear pass"),
                 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -653,92 +657,107 @@ impl Scene for PlayMode {
             light_pipeline.update(graphics.clone(), &self.world);
         }
 
-        let lights = {
+        let (lights, enabled_light_count) = {
+            puffin::profile_scope!("Locating lights");
             let mut lights = Vec::new();
-            let mut query = self.world.query::<&Light>();
-            for light in query.iter() {
+            let mut enabled = 0u32;
+            for light in self.world.query::<&Light>().iter() {
+                if light.component.enabled {
+                    enabled += 1;
+                }
                 lights.push(light.clone());
             }
-            lights
+            (lights, enabled)
         };
 
         if let Some(globals) = &mut self.shader_globals {
-            let enabled_count = lights
-                .iter()
-                .filter(|light| light.component.enabled)
-                .count() as u32;
-            globals.set_num_lights(enabled_count);
+            puffin::profile_scope!("Fetching globals");
+            globals.set_num_lights(enabled_light_count);
             globals.write(&graphics.queue);
         }
 
-        let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new();
-        let mut animated_instances: Vec<(
-            Entity,
-            u64,
-            InstanceRaw,
-            wgpu::Buffer,
-            wgpu::Buffer,
-            wgpu::Buffer,
-            u32,
-        )> = Vec::new();
+        self.static_batches.clear();
+        self.animated_instances.clear();
 
         {
+            puffin::profile_scope!("finding all renderers and animation components");
             let mut query = self
                 .world
                 .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;
                 }
 
                 let instance = renderer.instance.to_raw();
+
                 if let Some(animation) = animation {
-                    let has_skinning = !animation.skinning_matrices.is_empty();
-                    let has_morph_weights = !animation.morph_weights.is_empty();
-                    if !has_skinning && !has_morph_weights {
-                        static_batches.entry(handle.id).or_default().push(instance);
+                    let has_skinning   = !animation.skinning_matrices.is_empty();
+                    let has_morph      = !animation.morph_weights.is_empty();
+
+                    if !has_skinning && !has_morph {
+                        self.static_batches
+                            .entry(handle.id)
+                            .or_default()
+                            .push((entity, instance));
                         continue;
                     }
 
                     animation.prepare_gpu_resources(graphics.clone());
 
-                    let skinning_buffer = if let Some(buffer) = animation
+                    let skinning_buffer = match animation
                         .skinning_buffer
                         .as_ref()
-                        .map(|buffer| buffer.buffer().clone())
+                        .map(|b| b.buffer().clone())
                     {
-                        buffer
-                    } else if !has_skinning {
-                        let Some(default_skinning_buffer) = self.default_skinning_buffer.as_ref()
-                        else {
-                            static_batches.entry(handle.id).or_default().push(instance);
+                        Some(buf) => buf,
+                        None if !has_skinning => {
+                            let Some(default) = self.default_skinning_buffer.as_ref() else {
+                                self.static_batches
+                                    .entry(handle.id)
+                                    .or_default()
+                                    .push((entity, instance));
+                                continue;
+                            };
+                            default.clone()
+                        }
+                        None => {
+                            self.static_batches
+                                .entry(handle.id)
+                                .or_default()
+                                .push((entity, instance));
                             continue;
-                        };
-                        default_skinning_buffer.clone()
-                    } else {
-                        static_batches.entry(handle.id).or_default().push(instance);
-                        continue;
+                        }
                     };
+
                     let Some(morph_weights_buffer) = animation
                         .morph_weights_buffer
                         .as_ref()
-                        .map(|buffer| buffer.buffer().clone())
+                        .map(|b| b.buffer().clone())
                     else {
-                        static_batches.entry(handle.id).or_default().push(instance);
+                        self.static_batches
+                            .entry(handle.id)
+                            .or_default()
+                            .push((entity, instance));
                         continue;
                     };
+
                     let Some(morph_info_buffer) = animation
                         .morph_info_buffer
                         .as_ref()
-                        .map(|buffer| buffer.buffer().clone())
+                        .map(|b| b.buffer().clone())
                     else {
-                        static_batches.entry(handle.id).or_default().push(instance);
+                        self.static_batches
+                            .entry(handle.id)
+                            .or_default()
+                            .push((entity, instance));
                         continue;
                     };
 
-                    animated_instances.push((
+                    self.animated_instances.push((
                         entity,
                         handle.id,
                         instance,
@@ -748,20 +767,32 @@ impl Scene for PlayMode {
                         animation.morph_weight_count,
                     ));
                 } else {
-                    static_batches.entry(handle.id).or_default().push(instance);
+                    self.static_batches
+                        .entry(handle.id)
+                        .or_default()
+                        .push((entity, instance));
                 }
             }
         }
 
         let registry = ASSET_REGISTRY.read();
+
+        let mut model_cache: HashMap<u64, _> = HashMap::new();
         let mut prepared_models = Vec::new();
-        for (handle, instances) in static_batches {
-            let Some(model) = registry.get_model(Handle::new(handle)) else {
+        for (handle, batched_instances) in &self.static_batches {
+            puffin::profile_scope!("preparing models");
+            let Some(model) = registry.get_model(Handle::new(*handle)) else {
                 log_once::error_once!("Missing model handle {} in registry", handle);
                 continue;
             };
 
-            let instance_buffer = self.instance_buffer_cache.entry(handle).or_insert_with(|| {
+            let entity = batched_instances.first().map(|(e, _)| *e);
+            let instances: Vec<InstanceRaw> = batched_instances
+                .iter()
+                .map(|(_, inst)| *inst)
+                .collect();
+
+            let instance_buffer = self.instance_buffer_cache.entry(*handle).or_insert_with(|| {
                 ResizableBuffer::new(
                     &graphics.device,
                     instances.len().max(1),
@@ -771,15 +802,25 @@ impl Scene for PlayMode {
             });
             instance_buffer.write(&graphics.device, &graphics.queue, &instances);
 
-            prepared_models.push((model, handle, instances.len() as u32));
+            model_cache.insert(*handle, model.clone());
+            prepared_models.push((model, *handle, instances.len() as u32, entity));
         }
 
-        let registry = ASSET_REGISTRY.read();
+        for (_, handle, ..) in &self.animated_instances {
+            if !model_cache.contains_key(handle) {
+                if let Some(model) = registry.get_model(Handle::new(*handle)) {
+                    model_cache.insert(*handle, model);
+                }
+            }
+        }
+
+        // light cube rendering
         if let Some(light_pipeline) = &self.light_cube_pipeline {
             if let Some(l) = lights.first()
                 && let Some(model) = registry.get_model(l.cube_model)
             {
                 {
+                    puffin::profile_scope!("light cube pass");
                     let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                         label: Some("light cube render pass"),
                         color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -805,6 +846,7 @@ impl Scene for PlayMode {
 
                     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;
@@ -816,20 +858,7 @@ impl Scene for PlayMode {
                 log_once::error_once!("Missing light cube model handle in registry",);
             }
         }
-        let default_skinning_buffer = self
-            .default_skinning_buffer
-            .as_ref()
-            .expect("Default skinning buffer not initialised");
-        let default_morph_weights_buffer = self
-            .default_morph_weights_buffer
-            .as_ref()
-            .expect("Default morph weights buffer not initialised");
-        let default_morph_info_buffer = self
-            .default_morph_info_buffer
-            .as_ref()
-            .expect("Default morph info buffer not initialised");
 
-        // model rendering
         let sky = self
             .sky_pipeline
             .as_ref()
@@ -842,120 +871,31 @@ impl Scene for PlayMode {
         };
         log_once::debug_once!("Pipeline ready");
 
-        for (model, handle, instance_count) in prepared_models {
-            let morph_deltas_buffer = model
-                .morph_deltas_buffer
-                .as_ref()
-                .or(self.default_morph_deltas_buffer.as_ref());
-            let Some(morph_deltas_buffer) = morph_deltas_buffer else {
-                log_once::error_once!("Missing morph deltas buffer for model {}", handle);
-                continue;
-            };
-
-            let animation_bind_group = pipeline.animation_bind_group(
-                graphics.clone(),
-                default_skinning_buffer,
-                morph_deltas_buffer,
-                default_morph_weights_buffer,
-                default_morph_info_buffer,
-            );
+        // static models
+        if let Some(_) = &self.light_cube_pipeline {
+            puffin::profile_scope!("model render pass");
 
+            let default_skinning_buffer = self
+                .default_skinning_buffer
+                .as_ref()
+                .expect("Default skinning buffer not initialised");
+            let default_morph_weights_buffer = self
+                .default_morph_weights_buffer
+                .as_ref()
+                .expect("Default morph weights buffer not initialised");
+            let default_morph_info_buffer = self
+                .default_morph_info_buffer
+                .as_ref()
+                .expect("Default morph info buffer not initialised");
             let per_frame_bind_group = pipeline
                 .per_frame
                 .as_ref()
                 .expect("Per-frame bind group not initialised")
                 .clone();
-            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                label: Some("model render pass"),
-                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
-                    view: hdr.render_view(),
-                    depth_slice: None,
-                    resolve_target: hdr.resolve_target(),
-                    ops: wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    },
-                })],
-                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
-                    view: &graphics.depth_texture.view,
-                    depth_ops: Some(wgpu::Operations {
-                        load: wgpu::LoadOp::Load,
-                        store: wgpu::StoreOp::Store,
-                    }),
-                    stencil_ops: None,
-                }),
-                occlusion_query_set: None,
-                timestamp_writes: None,
-            });
-            render_pass.set_pipeline(pipeline.pipeline());
-            if let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) {
-                render_pass.set_vertex_buffer(1, instance_buffer.slice(instance_count as usize));
-            } else {
-                continue;
-            }
 
-            for mesh in &model.meshes {
-                let mut weights = mesh.morph_default_weights.clone();
-                let target_count = mesh.morph_target_count as usize;
-                if weights.len() < target_count {
-                    weights.resize(target_count, 0.0);
-                }
-                if weights.is_empty() {
-                    weights.push(0.0);
-                }
-
-                graphics.queue.write_buffer(
-                    default_morph_weights_buffer,
-                    0,
-                    bytemuck::cast_slice(&weights),
-                );
-
-                let info = MorphTargetInfo {
-                    num_vertices: mesh.morph_vertex_count,
-                    num_targets: mesh.morph_target_count,
-                    base_offset: mesh.morph_deltas_offset,
-                    weight_offset: 0,
-                    uses_morph: if mesh.morph_target_count > 0 && !weights.is_empty() {
-                        1
-                    } else {
-                        0
-                    },
-                    _padding: Default::default(),
-                };
-
-                graphics.queue.write_buffer(
-                    default_morph_info_buffer,
-                    0,
-                    bytemuck::bytes_of(&info),
-                );
-
-                let material = &model.materials[mesh.material];
-                render_pass.draw_mesh_instanced(
-                    mesh,
-                    material,
-                    0..instance_count,
-                    &per_frame_bind_group,
-                    &animation_bind_group,
-                    environment_bind_group,
-                );
-            }
-        }
-
-        if let Some(_lcp) = &self.light_cube_pipeline {
-            for (
-                entity,
-                handle,
-                instance,
-                skinning_buffer,
-                morph_weights_buffer,
-                morph_info_buffer,
-                morph_weight_count,
-            ) in animated_instances
-            {
-                let Some(model) = registry.get_model(Handle::new(handle)) else {
-                    log_once::error_once!("Missing model handle {} in registry", handle);
-                    continue;
-                };
+            for (model, handle, instance_count, entity) in prepared_models {
+                let Some(entity) = entity else { continue };
+                let Ok(renderer) = self.world.get::<&MeshRenderer>(entity) else { continue };
 
                 let morph_deltas_buffer = model
                     .morph_deltas_buffer
@@ -968,32 +908,14 @@ impl Scene for PlayMode {
 
                 let animation_bind_group = pipeline.animation_bind_group(
                     graphics.clone(),
-                    &skinning_buffer,
-                    &morph_deltas_buffer,
-                    &morph_weights_buffer,
-                    &morph_info_buffer,
+                    default_skinning_buffer,
+                    morph_deltas_buffer,
+                    default_morph_weights_buffer,
+                    default_morph_info_buffer,
                 );
-                let per_frame_bind_group = pipeline
-                    .per_frame
-                    .as_ref()
-                    .expect("Per-frame bind group not initialised")
-                    .clone();
-
-                let instance_buffer =
-                    self.animated_instance_buffers
-                        .entry(entity)
-                        .or_insert_with(|| {
-                            ResizableBuffer::new(
-                                &graphics.device,
-                                1,
-                                wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
-                                "Runtime Animated Instance Buffer",
-                            )
-                        });
-                instance_buffer.write(&graphics.device, &graphics.queue, &[instance]);
 
                 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                    label: Some("model render pass (animated)"),
+                    label: Some("model render pass"),
                     color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                         view: hdr.render_view(),
                         depth_slice: None,
@@ -1016,28 +938,68 @@ impl Scene for PlayMode {
                 });
 
                 render_pass.set_pipeline(pipeline.pipeline());
-                render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
+                let Some(instance_buffer) = self.instance_buffer_cache.get(&handle) else { continue };
+                render_pass.set_vertex_buffer(1, instance_buffer.slice(instance_count as usize));
 
                 for mesh in &model.meshes {
-                    let mesh_target_count = mesh.morph_target_count.min(morph_weight_count);
+                    let mut weights = mesh.morph_default_weights.clone();
+                    let target_count = mesh.morph_target_count as usize;
+                    if weights.len() < target_count {
+                        weights.resize(target_count, 0.0);
+                    }
+                    if weights.is_empty() {
+                        weights.push(0.0);
+                    }
+
+                    graphics.queue.write_buffer(
+                        default_morph_weights_buffer,
+                        0,
+                        bytemuck::cast_slice(&weights),
+                    );
+
                     let info = MorphTargetInfo {
                         num_vertices: mesh.morph_vertex_count,
-                        num_targets: mesh_target_count,
+                        num_targets: mesh.morph_target_count,
                         base_offset: mesh.morph_deltas_offset,
                         weight_offset: 0,
-                        uses_morph: if mesh_target_count > 0 { 1 } else { 0 },
+                        uses_morph: if mesh.morph_target_count > 0 && !weights.is_empty() { 1 } else { 0 },
                         _padding: Default::default(),
                     };
 
-                    graphics
-                        .queue
-                        .write_buffer(&morph_info_buffer, 0, bytemuck::bytes_of(&info));
+                    let cache_key = mesh.morph_deltas_offset;
+                    let needs_write = self
+                        .last_morph_info_per_mesh
+                        .get(&cache_key)
+                        .map_or(true, |prev| {
+                            prev.num_vertices != info.num_vertices
+                                || prev.num_targets != info.num_targets
+                                || prev.base_offset != info.base_offset
+                                || prev.uses_morph != info.uses_morph
+                        });
+
+                    if needs_write {
+                        graphics.queue.write_buffer(
+                            default_morph_info_buffer,
+                            0,
+                            bytemuck::bytes_of(&info),
+                        );
+                        self.last_morph_info_per_mesh.insert(cache_key, info);
+                    }
 
                     let material = &model.materials[mesh.material];
+                    let material = if let Some(mat) = renderer.material_snapshot.get(&material.name) {
+                        mat
+                    } else {
+                        log_once::warn_once!(
+                        "Unable to locate MeshRenderer's material_snapshot for that specific material"
+                    );
+                        material
+                    };
+
                     render_pass.draw_mesh_instanced(
                         mesh,
                         material,
-                        0..1,
+                        0..instance_count,
                         &per_frame_bind_group,
                         &animation_bind_group,
                         environment_bind_group,
@@ -1046,7 +1008,142 @@ impl Scene for PlayMode {
             }
         }
 
+        // animated models
+        if let Some(_) = &self.light_cube_pipeline {
+            puffin::profile_scope!("animated model render pass");
+
+            let per_frame_bind_group = pipeline
+                .per_frame
+                .as_ref()
+                .expect("Per-frame bind group not initialised")
+                .clone();
+
+            for (entity, _, instance, _, _, _, _)
+            in &self.animated_instances
+            {
+                let instance_buffer = self
+                    .animated_instance_buffers
+                    .entry(*entity)
+                    .or_insert_with(|| {
+                        ResizableBuffer::new(
+                            &graphics.device,
+                            1,
+                            wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+                            "animated instance buffer",
+                        )
+                    });
+                instance_buffer.write(&graphics.device, &graphics.queue, &[*instance]);
+            }
+
+            for (entity, handle, _, skinning_buffer, morph_weights_buffer, morph_info_buffer, morph_weight_count)
+            in &self.animated_instances
+            {
+                let Ok(renderer) = self.world.get::<&MeshRenderer>(*entity) else { continue };
+                puffin::profile_scope!("rendering animated model", format!("{:?}", entity));
+                {
+                    let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
+                        label: Some("animated model render pass"),
+                        color_attachments: &[Some(wgpu::RenderPassColorAttachment {
+                            view: hdr.render_view(),
+                            depth_slice: None,
+                            resolve_target: hdr.resolve_target(),
+                            ops: wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            },
+                        })],
+                        depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
+                            view: &graphics.depth_texture.view,
+                            depth_ops: Some(wgpu::Operations {
+                                load: wgpu::LoadOp::Load,
+                                store: wgpu::StoreOp::Store,
+                            }),
+                            stencil_ops: None,
+                        }),
+                        occlusion_query_set: None,
+                        timestamp_writes: None,
+                    });
+
+                    render_pass.set_pipeline(pipeline.pipeline());
+
+                    let Some(model) = model_cache.get(handle) else {
+                        log_once::error_once!("Missing model handle {} in registry", handle);
+                        continue;
+                    };
+
+                    let morph_deltas_buffer = model
+                        .morph_deltas_buffer
+                        .as_ref()
+                        .or(self.default_morph_deltas_buffer.as_ref());
+                    let Some(morph_deltas_buffer) = morph_deltas_buffer else {
+                        log_once::error_once!("Missing morph deltas buffer for model {}", handle);
+                        continue;
+                    };
+
+                    let mut hasher = DefaultHasher::new();
+                    skinning_buffer.hash(&mut hasher);
+                    let bind_group_stamp = hasher.finish();
+                    let animation_bind_group = {
+                        let cached = self.animated_bind_group_cache.get(entity);
+                        if cached.map_or(true, |(stamp, _)| *stamp != bind_group_stamp) {
+                            let bg = pipeline.animation_bind_group(
+                                graphics.clone(),
+                                skinning_buffer,
+                                morph_deltas_buffer,
+                                morph_weights_buffer,
+                                morph_info_buffer,
+                            );
+                            self.animated_bind_group_cache.insert(*entity, (bind_group_stamp, bg));
+                        }
+                        &self.animated_bind_group_cache[entity].1
+                    };
+
+                    let Some(instance_buffer) = self.animated_instance_buffers.get(entity) else { continue };
+                    render_pass.set_vertex_buffer(1, instance_buffer.slice(1));
+
+                    for mesh in &model.meshes {
+                        let mesh_target_count = mesh.morph_target_count.min(*morph_weight_count);
+
+                        let info = MorphTargetInfo {
+                            num_vertices: mesh.morph_vertex_count,
+                            num_targets: mesh_target_count,
+                            base_offset: mesh.morph_deltas_offset,
+                            weight_offset: 0,
+                            uses_morph: if mesh_target_count > 0 { 1 } else { 0 },
+                            _padding: Default::default(),
+                        };
+
+                        graphics
+                            .queue
+                            .write_buffer(morph_info_buffer, 0, bytemuck::bytes_of(&info));
+
+                        let material = &model.materials[mesh.material];
+                        let material =
+                            if let Some(mat) = renderer.material_snapshot.get(&material.name) {
+                                mat
+                            } else {
+                                log_once::warn_once!(
+                                    "Unable to locate MeshRenderer's material_snapshot for that specific material"
+                                );
+                                material
+                            };
+
+                        render_pass.draw_mesh_instanced(
+                            mesh,
+                            material,
+                            0..1,
+                            &per_frame_bind_group,
+                            animation_bind_group,
+                            environment_bind_group,
+                        );
+                    }
+                }
+            }
+        }
+
+        // skybox rendering
         if let Some(sky) = &self.sky_pipeline {
+            puffin::profile_scope!("sky render pass");
             let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                 label: Some("sky render pass"),
                 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -1076,7 +1173,7 @@ impl Scene for PlayMode {
             render_pass.draw(0..3, 0..1);
         }
 
-        // collider wireframe hitbox renderer
+        // collider pipeline
         {
             let show_hitboxes = self
                 .current_scene
@@ -1091,7 +1188,9 @@ impl Scene for PlayMode {
                 .unwrap_or(false);
 
             if show_hitboxes {
+                puffin::profile_scope!("collider wireframe pipeline");
                 if let Some(collider_pipeline) = &self.collider_wireframe_pipeline {
+                    log_once::debug_once!("Found collider wireframe pipeline");
                     let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
                         label: Some("collider wireframe render pass"),
                         color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@@ -1198,24 +1297,28 @@ impl Scene for PlayMode {
                             render_pass.draw_indexed(0..geometry.index_count, 0, 0..count as u32);
                         }
                     }
+                } else {
+                    log_once::warn_once!("No collider pipeline found");
                 }
             }
         }
 
         // kino billboard renderer
         {
-            // 1. render billboards and prepare views
+            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(&graphics.device, &graphics.queue, &mut kino_encoder);
+                let mut kino_encoder = CommandEncoder::new(graphics.clone(), Some("kino billboard encoder"));
+                kino.render_billboard_targets(
+                    &graphics.device,
+                    &graphics.queue,
+                    &mut kino_encoder,
+                );
 
                 if let Err(e) = kino_encoder.submit() {
                     log_once::error_once!("Unable to submit billboard kino pass: {}", e);
                 }
             }
 
-            // 2. prepare billboards for application to viewport
             if let Some(billboard_pipeline) = &self.billboard_pipeline {
                 let camera_position = camera.position().as_vec3();
                 let camera_projection = Mat4::from_cols_array_2d(&camera.uniform.view_proj);
@@ -1234,8 +1337,10 @@ impl Scene for PlayMode {
                 let mut billboards: Vec<(Mat4, wgpu::TextureView)> = Vec::new();
                 let mut query = self
                     .world
-                    .query::<(Entity, &BillboardComponent, &EntityTransform)>();
+                    .query::<(Entity, &BillboardComponent, Option<&EntityTransform>)>();
+
                 for (entity, billboard, entity_transform) in query.iter() {
+                    puffin::profile_scope!("rendering billboard", format!("{:?}", entity));
                     if !billboard.enabled {
                         continue;
                     }
@@ -1250,38 +1355,40 @@ impl Scene for PlayMode {
                         continue;
                     };
 
-                    let world_transform = entity_transform.sync();
-                    let position = world_transform.position.as_vec3() + billboard.offset;
+                    let position = entity_transform
+                        .map(|transform| transform.sync().position.as_vec3())
+                        .unwrap_or(glam::Vec3::ZERO)
+                        + billboard.offset;
                     let world_size = billboard.world_size;
-                    let scale = Vec3::new(world_size.x, world_size.y, 1.0);
+                    let scale = glam::Vec3::new(world_size.x, world_size.y, 1.0);
 
                     let rotation = if let Some(rotation) = billboard.rotation {
                         rotation
                     } else {
                         let to_camera = (camera_position - position).normalize_or_zero();
                         if to_camera.length_squared() > 0.0 {
-                            let mut world_up = Vec3::Y;
+                            let mut world_up = glam::Vec3::Y;
                             if to_camera.dot(world_up).abs() > 0.999 {
-                                world_up = Vec3::X;
+                                world_up = glam::Vec3::X;
                             }
 
                             let right = world_up.cross(to_camera).normalize_or_zero();
                             let up = to_camera.cross(right).normalize_or_zero();
-                            let basis = Mat3::from_cols(right, up, to_camera);
-                            Quat::from_mat3(&basis)
+                            let basis = glam::Mat3::from_cols(right, up, to_camera);
+                            glam::Quat::from_mat3(&basis)
                         } else {
-                            Quat::IDENTITY
+                            glam::Quat::IDENTITY
                         }
                     };
 
-                    let transform =
-                        Mat4::from_scale_rotation_translation(scale, rotation, position);
+                    let transform = Mat4::from_scale_rotation_translation(scale, rotation, position);
                     billboards.push((transform, texture_view));
                 }
 
                 if !billboards.is_empty() {
+                    puffin::profile_scope!("billboard render pass");
                     let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
-                        label: Some("runtime billboard render pass"),
+                        label: Some("editor billboard render pass"),
                         color_attachments: &[Some(wgpu::RenderPassColorAttachment {
                             view: hdr.render_view(),
                             depth_slice: None,
@@ -1304,7 +1411,6 @@ impl Scene for PlayMode {
                     });
 
                     for (transform, texture_view) in billboards {
-                        // i mean technically it *is* versatile in the sense that you can use other views 🤷
                         billboard_pipeline.draw(
                             graphics.clone(),
                             &mut render_pass,
@@ -1317,10 +1423,13 @@ impl Scene for PlayMode {
             }
         }
 
+        hdr.process(&mut encoder, &graphics.viewport_texture.view);
+
         if let Err(e) = encoder.submit() {
             log_once::error_once!("{}", e);
         }
 
+        // kino hud renderer
         if let Some(kino) = &mut self.kino {
             let mut encoder = CommandEncoder::new(graphics.clone(), Some("kino encoder"));
             kino.render(&graphics.device, &graphics.queue, &mut encoder, hdr.view());