From 0a9e5ebcdb1bb62c98e56cbe06a7a4fed8977213 Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Mon, 1 Apr 2024 10:50:17 -0400 Subject: [PATCH] render: improve fix for rendering shared 3d modules --- lyra-game/src/render/render_job.rs | 6 +- lyra-game/src/render/renderer.rs | 18 +- .../src/render/transform_buffer_storage.rs | 246 +++++++++++++++--- 3 files changed, 227 insertions(+), 43 deletions(-) diff --git a/lyra-game/src/render/render_job.rs b/lyra-game/src/render/render_job.rs index 8a6d72d..e6b0b56 100755 --- a/lyra-game/src/render/render_job.rs +++ b/lyra-game/src/render/render_job.rs @@ -1,14 +1,16 @@ use lyra_ecs::Entity; +use super::transform_buffer_storage::TransformIndex; + pub struct RenderJob { pub entity: Entity, pub shader_id: u64, pub mesh_uuid: uuid::Uuid, - pub transform_id: u32, + pub transform_id: TransformIndex, } impl RenderJob { - pub fn new(entity: Entity, shader_id: u64, mesh_buffer_id: uuid::Uuid, transform_id: u32) -> Self { + pub fn new(entity: Entity, shader_id: u64, mesh_buffer_id: uuid::Uuid, transform_id: TransformIndex) -> Self { Self { entity, shader_id, diff --git a/lyra-game/src/render/renderer.rs b/lyra-game/src/render/renderer.rs index 5015e9c..9369873 100755 --- a/lyra-game/src/render/renderer.rs +++ b/lyra-game/src/render/renderer.rs @@ -29,7 +29,7 @@ use super::light_cull_compute::LightCullCompute; use super::material::Material; use super::render_buffer::BufferWrapper; use super::texture::RenderTexture; -use super::transform_buffer_storage::TransformBuffers; +use super::transform_buffer_storage::{TransformBuffers, TransformGroup}; use super::vertex::Vertex; use super::{render_pipeline::FullRenderPipeline, render_buffer::BufferStorage, render_job::RenderJob}; @@ -452,8 +452,10 @@ impl Renderer for BasicRenderer { self.check_mesh_buffers(entity, &mesh_han); } - let transform_id = self.transform_buffers.push_transform(&self.queue, &self.render_limits, interop_pos.calculate_mat4(), glam::Mat3::from_quat(interop_pos.rotation)); - + let group = TransformGroup::EntityRes(entity, mesh_han.uuid()); + let transform_id = self.transform_buffers.update_or_push(&self.queue, &self.render_limits, + group, || ( interop_pos.calculate_mat4(), glam::Mat3::from_quat(interop_pos.rotation) )); + let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); let shader = material.shader_uuid.unwrap_or(0); @@ -482,7 +484,10 @@ impl Renderer for BasicRenderer { self.check_mesh_buffers(entity, &mesh_han); } - let transform_id = self.transform_buffers.push_transform(&self.queue, &self.render_limits, mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation)); + let scene_mesh_group = TransformGroup::Res(scene_han.uuid(), mesh_han.uuid()); + let group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); + let transform_id = self.transform_buffers.update_or_push(&self.queue, &self.render_limits, + group, || ( mesh_interpo.calculate_mat4(), glam::Mat3::from_quat(mesh_interpo.rotation) )); let material = mesh.material.as_ref().unwrap() .data_ref().unwrap(); @@ -581,11 +586,8 @@ impl Renderer for BasicRenderer { } // Get the bindgroup for job's transform and bind to it using an offset. - /* let transform_indices = *self.transform_buffers.transform_indices(job.entity).unwrap(); - let bindgroup = self.transform_buffers.bind_group(transform_indices).unwrap(); */ - let bindgroup = self.transform_buffers.bind_group(job.transform_id); - let offset = self.transform_buffers.buffer_offset(job.transform_id);//TransformBuffers::index_offset(&self.render_limits, transform_indices) as u32; + let offset = self.transform_buffers.buffer_offset(job.transform_id); render_pass.set_bind_group(1, bindgroup, &[ offset, offset, ]); render_pass.set_bind_group(2, &self.camera_buffer.bindgroup(), &[]); diff --git a/lyra-game/src/render/transform_buffer_storage.rs b/lyra-game/src/render/transform_buffer_storage.rs index 0db9851..9044715 100644 --- a/lyra-game/src/render/transform_buffer_storage.rs +++ b/lyra-game/src/render/transform_buffer_storage.rs @@ -1,37 +1,167 @@ -use std::num::NonZeroU64; +use std::{collections::{HashMap, VecDeque}, hash::{BuildHasher, DefaultHasher, Hash, Hasher, RandomState}, num::NonZeroU64}; +use lyra_ecs::Entity; +use uuid::Uuid; use wgpu::Limits; use std::mem; +/// A group id created from a [`TransformGroup`]. +/// +/// This is mainly created so that [`TransformGroup::OwnedGroup`] can use another group inside of it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub struct TransformGroupId(u64); + +impl From for TransformGroupId { + fn from(value: TransformGroup) -> Self { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + let hash = hasher.finish(); + + TransformGroupId(hash) + } +} + +/// Used as a key into the [`TransformBuffers`]. +/// +/// This enum is used as a key to identify a transform for a RenderJob. The renderer uses this +/// to differentiate a transform between two entities that share a resource handle to the same +/// scene: +/// ```nobuild +/// // The group of the mesh in the scene. +/// let scene_mesh_group = TransformGroup::Res(scene_handle.uuid(), mesh_handle.uuid()); +/// // The group of the owned entity that has mesh in a scene. +/// let finished_group = TransformGroup::OwnedGroup(entity, scene_mesh_group.into()); +/// ``` +/// +/// A simpler example of the use of a transform group is when processing lone mesh handles +/// owned by entities: +/// ```nobuild +/// let group = TransformGroup::EntityRes(entity, mesh_handle.uuid()); +/// ``` +/// +/// These were made to fix [#6](https://git.seanomik.net/SeanOMik/lyra-engine/issues/6). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum TransformGroup { + /// Just an entity. + Entity(Entity), + /// An entity that owns another group. + OwnedGroup(Entity, TransformGroupId), + /// A resource uuid grouped with an owning Entity. + EntityRes(Entity, Uuid), + /// A resource uuid grouped with another resource uuid. + Res(Uuid, Uuid), +} + +/// The index of a specific Transform inside of the buffers. #[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] -pub(crate) struct TransformBufferIndices { - pub buffer_index: usize, - pub transform_index: usize, +pub struct TransformIndex { + /// The index of the entry in the buffer chain. + entry_index: usize, + /// The index of the transform in the entry. + transform_index: usize, } /// A struct representing a single transform buffer. There can be multiple of these -pub(crate) struct TransformBufferEntry { +struct BufferEntry { pub len: usize, pub bindgroup: wgpu::BindGroup, pub transform_buffer: wgpu::Buffer, pub normal_buffer: wgpu::Buffer, } +/// A HashMap that caches values for reuse. +/// +/// The map detects dead values by tracking which entries were not updated since the last time +/// [`CachedValMap::update`] was ran. When dead values are collected, they can be reused on an +/// [`insert`](CachedValMap::insert) into the map. +struct CachedValMap { + latest: HashMap, + old: HashMap, + dead: VecDeque, +} + +impl Default for CachedValMap { + fn default() -> Self { + Self { + latest: Default::default(), + old: Default::default(), + dead: Default::default() + } + } +} + +#[allow(dead_code)] +impl CachedValMap { + /// Insert a key, possibly reusing a value in the map. + /// + /// Returns the reused value, if one was reused. If its `None`, then the value was retrieved + /// from running `val_fn`. + pub fn insert(&mut self, key: K, mut val_fn: F) -> Option + where + F: FnMut() -> V + { + if self.latest.contains_key(&key) { + self.latest.insert(key, val_fn()); + None + } else { + let val = self.dead.pop_front() + .unwrap_or_else(val_fn); + self.latest.insert(key, val.clone()); + Some(val) + } + } + + /// Returns a reference to the value corresponding to the key. + pub fn get(&mut self, key: K) -> Option<&V> { + if let Some(v) = self.old.remove(&key) { + self.latest.insert(key.clone(), v); + } + + self.latest.get(&key) + } + + /// Keep a key alive without updating its value. + pub fn keep_alive(&mut self, key: K) { + if let Some(v) = self.old.remove(&key) { + self.latest.insert(key, v); + } + } + + /// Returns `true` if the map contains a value for the specified key. + pub fn contains(&self, key: K) -> bool { + self.old.contains_key(&key) || self.latest.contains_key(&key) + } + + /// Collects the now dead values for reuse. + /// + /// This detects dead values by tracking which entries were not updated since the last time + /// update was ran. + pub fn update(&mut self) { + // drain the dead values into the dead queue + self.dead.extend(self.old.drain().map(|(_, v)| v)); + + // now drain the latest entries into the old entries + self.old.extend(self.latest.drain()); + } +} + /// A helper struct for managing the Transform buffers for meshes. /// -/// This struct manages a "chain" of uniform buffers that store Transform for meshes. When -/// first created it only has a single "chain-link" with a buffer that is the maximum length +/// This struct manages a "chain" of uniform buffers that store Transform for [`TransformGroup`]s. +/// When first created it only has a single "chain-link" with a buffer that is the maximum length /// the GPU supports. When the first buffer fills up, a new one should be created which will also /// be the maximum length the GPU supports. When the new buffer fills up, a new one will be /// created once again, and so on. /// -/// `Uuid`'s are used to represent entries (usually Meshes) in the buffer. The Uuid's can be used -/// to insert, update, and retrieve the transforms. -pub(crate) struct TransformBuffers { +/// [`TransformGroup`]s are used to represent entries in the buffer. They are used to insert, +/// update, and retrieve the transforms. +pub struct TransformBuffers { pub bindgroup_layout: wgpu::BindGroupLayout, - pub entries: Vec, + groups: CachedValMap, + entries: Vec, limits: wgpu::Limits, + max_transform_count: usize, } impl TransformBuffers { @@ -66,7 +196,9 @@ impl TransformBuffers { let mut s = Self { bindgroup_layout, - entries: vec![], + groups: Default::default(), + entries: Default::default(), + max_transform_count: (limits.max_uniform_buffer_binding_size / 2) as usize / (mem::size_of::()), limits, }; @@ -76,26 +208,70 @@ impl TransformBuffers { s } - pub fn push_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, transform: glam::Mat4, normal_matrix: glam::Mat3) -> u32 { - let entry = self.entries.iter_mut().next().unwrap(); // TODO: use other buffers than just the first + /// Update an existing transform in the buffers. + /// + /// # Panics + /// Panics if the `entity_group` is not already inside of the buffers. + pub fn update_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, entity_group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { + let index = *self.groups.get(entity_group.into()) + .expect("Use 'push_transform' for new entities"); + let entry = self.entries.get_mut(index.entry_index).unwrap(); let normal_matrix = glam::Mat4::from_mat3(normal_matrix); // write the transform and normal to the end of the transform - //let offset = (mem::size_of::() * entry.len) as u64; - let offset = Self::get_buffer_offset(limits, entry.len as u32) as _; + let offset = Self::get_buffer_offset(limits, index) as _; queue.write_buffer(&entry.transform_buffer, offset, bytemuck::bytes_of(&transform)); queue.write_buffer(&entry.normal_buffer, offset, bytemuck::bytes_of(&normal_matrix)); - let index = entry.len; - entry.len += 1; - index as _ + index + } + + /// Push a new transform into the buffers. + pub fn push_transform(&mut self, queue: &wgpu::Queue, limits: &Limits, entity_group: TransformGroup, transform: glam::Mat4, normal_matrix: glam::Mat3) -> TransformIndex { + self.groups.insert(entity_group.into(), || { + // this closure is only called when there are no values that can be reused, + // so we get a brand new index at the end of the last entry in the chain. + let last = self.entries.last_mut().unwrap(); + + // ensure the gpu buffer is not overflown + debug_assert!(last.len < self.max_transform_count, + "Transform buffer is filled and 'next_indices' was not incremented! \ + Was a new buffer created?"); + + let tidx = last.len; + last.len += 1; + + TransformIndex { + entry_index: self.entries.len() - 1, + transform_index: tidx + } + }); + + self.update_transform(queue, limits, entity_group, transform, normal_matrix) } /// Collect the dead transforms and prepare self to check next time. pub fn tick(&mut self) { - for entry in self.entries.iter_mut() { - entry.len = 0; + self.groups.update(); + } + + /// Returns a boolean indicating if the buffer contains this group. + pub fn contains(&self, group: TransformGroup) -> bool { + self.groups.contains(group.into()) + } + + /// Update an existing transform group or if its not existing yet, pushes it to the buffer. + /// + /// Returns: the index that the transform is at in the buffers. + pub fn update_or_push(&mut self, queue: &wgpu::Queue, limits: &Limits, group: TransformGroup, transform_fn: F) -> TransformIndex + where F: Fn() -> (glam::Mat4, glam::Mat3) + { + let (transform, normal_matrix) = transform_fn(); + if self.contains(group) { + self.update_transform(queue, limits, group, transform, normal_matrix) + } else { + self.push_transform(queue, limits, group, transform, normal_matrix) } } @@ -105,7 +281,7 @@ impl TransformBuffers { /// "chain-link" is created. pub fn expand_buffers(&mut self, device: &wgpu::Device) { let limits = device.limits(); - let max_buffer_sizes = (limits.max_uniform_buffer_binding_size as u64) / 2; + let max_buffer_sizes = self.max_transform_count as u64 * limits.min_uniform_buffer_offset_alignment as u64; let transform_buffer = device.create_buffer( &wgpu::BufferDescriptor { @@ -157,7 +333,7 @@ impl TransformBuffers { label: Some("BG_Transforms"), }); - let entry = TransformBufferEntry { + let entry = BufferEntry { bindgroup: transform_bind_group, transform_buffer, normal_buffer: normal_mat_buffer, @@ -166,24 +342,28 @@ impl TransformBuffers { self.entries.push(entry); } - pub fn bind_group(&self, transform_id: u32) -> &wgpu::BindGroup { - let _ = transform_id; - - let entry = self.entries.iter().next().unwrap(); // TODO: use other buffers than just the first + /// Returns the bind group for the transform index. + pub fn bind_group(&self, transform_id: TransformIndex) -> &wgpu::BindGroup { + let entry = self.entries.get(transform_id.entry_index).unwrap(); &entry.bindgroup } /// Get the buffer offset for a transform using wgpu limits. /// /// If its possible to borrow immutably, use [`TransformBuffers::buffer_offset`]. - fn get_buffer_offset(limits: &wgpu::Limits, transform_id: u32) -> u32 { - //let entry = self.entries.iter().next().unwrap(); // TODO: use other buffers than just the first - - transform_id * limits.min_uniform_buffer_offset_alignment as u32//mem::size_of::() as u32 + fn get_buffer_offset(limits: &wgpu::Limits, transform_index: TransformIndex) -> u32 { + transform_index.transform_index as u32 * limits.min_uniform_buffer_offset_alignment as u32 } - pub fn buffer_offset(&self, transform_id: u32) -> u32 { - Self::get_buffer_offset(&self.limits, transform_id) + /// Returns the offset of the transform inside the bind group buffer. + /// + /// ```nobuild + /// let bindgroup = transform_buffers.bind_group(job.transform_id); + /// let offset = transform_buffers.buffer_offset(job.transform_id); + /// render_pass.set_bind_group(1, bindgroup, &[ offset, offset, ]); + /// ``` + pub fn buffer_offset(&self, transform_index: TransformIndex) -> u32 { + Self::get_buffer_offset(&self.limits, transform_index) } }