tirbofish/dropbear · diff
perf: optimised editor/scene.rs
fix: reapplied editor's rendering system to the runtime to get the texture fix
Signature present but could not be verified.
Unverified
@@ -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, @@ -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 } @@ -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) { @@ -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; } } @@ -8,3 +8,4 @@ readme = "README.md" authors.workspace = true [dependencies] +serde.workspace = true @@ -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 + } +} @@ -1,3 +1,5 @@ mod hashmap; +pub mod either; -pub use hashmap::*; +pub use hashmap::*; +pub use either::*; @@ -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)?; @@ -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(), }) } @@ -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()); @@ -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 @@ -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"); @@ -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());