From b45cf13386014271394c33ec3eac4158d8e4b30a Mon Sep 17 00:00:00 2001 From: mars Date: Fri, 15 Apr 2022 21:13:27 -0600 Subject: [PATCH] Enhance mesh pooling --- src/mesh.rs | 243 -------------------------------------------- src/mesh/attr.rs | 172 +++++++++++++++++++++++++++++++ src/mesh/group.rs | 86 ++++++++++++++++ src/mesh/mod.rs | 129 +++++++++++++++++++++++ src/mesh/staging.rs | 51 ++++++++++ 5 files changed, 438 insertions(+), 243 deletions(-) delete mode 100644 src/mesh.rs create mode 100644 src/mesh/attr.rs create mode 100644 src/mesh/group.rs create mode 100644 src/mesh/mod.rs create mode 100644 src/mesh/staging.rs diff --git a/src/mesh.rs b/src/mesh.rs deleted file mode 100644 index 5655799..0000000 --- a/src/mesh.rs +++ /dev/null @@ -1,243 +0,0 @@ -//! Dynamic mesh data storage. -//! -//! Meshes are based on ECS-like archetypes. Each pool contains a set of mesh -//! "attributes," which can be either vertex attributes, indices of different -//! formats (U8, U16, U32), or in the future, fixed-size mesh chunklets too. -//! The mesh pool itself is agnostic to specific rendering implementation. It -//! has no implicit knowledge of what a vertex position, normal, or texture -//! coordinate is, or even what an index is. -//! -//! Multiple attributes can have the same layout. For example, a rudimentary -//! mesh format might use three 32-bit floating point values (`[f32; 3]`) for -//! both vertex position and vertex normals. In this case, positions and normals -//! would have different [AttrId]s to distuingish them, and must each be -//! registered to the pool. Once an attribute is registered in a pool instance, -//! it cannot be unregistered, although the mesh pool may free GPU buffers for -//! unused attribute pools. -//! -//! TODO: mesh coherency - -use slab::Slab; -use smallvec::SmallVec; -use std::collections::HashMap; - -/// An externally-defined identifier for a mesh attribute. -#[repr(transparent)] -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -pub struct AttrId(pub usize); - -/// A description of a mesh attribute. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct AttrLayout {} - -/// The data and layout of a single mesh attribute. -pub struct AttrBuffer { - pub id: AttrId, - pub layout: AttrLayout, - pub count: usize, - pub data: Vec, -} - -/// A mesh and all of its attributes. -/// -/// An attribute ID can be used multiple times in a mesh, corresponding to -/// multiple allocations within an [AttrPool]. -pub struct MeshBuffer { - pub attributes: SmallVec<[AttrBuffer; MAX_MESH_INLINE_ATTRIBUTES]>, -} - -/// The number of attributes a mesh can have before they're moved to the heap. -pub const MAX_MESH_INLINE_ATTRIBUTES: usize = 16; - -/// A mesh that has been allocated in a [MeshPool]. -pub struct MeshAlloc { - pub attributes: SmallVec<[AttrAlloc; MAX_MESH_INLINE_ATTRIBUTES]>, -} - -/// An error that can be returned when allocating a mesh. -pub enum PoolError { - TooBig, - NoMoreRoom, - InvalidFree, - AttrTaken, - AttrUnregistered, - MismatchedId, - MismatchedLayout, -} - -/// An attribute buffer that has been allocated in an [AttrPool]. -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -pub struct AttrAlloc { - id: AttrId, - offset: usize, - count: usize, -} - -/// An unused space range in an [AttrPool]. -pub struct FreeSpace { - offset: usize, - count: usize, -} - -/// A single GPU buffer containing linear arrays of individual attributes. -pub struct AttrPool { - id: AttrId, - layout: AttrLayout, - count: usize, - allocs: Vec, - free_space: Vec, -} - -impl AttrPool { - pub fn new(id: AttrId, layout: AttrLayout, count: usize) -> Result { - Ok(Self { - id, - layout, - count, - free_space: vec![FreeSpace { offset: 0, count }], - allocs: vec![], - }) - } - - /// Tests if an [AttrBuffer] can be allocated without taking ownership. - /// - /// Returns the result of [Self::best_fit]. - pub fn can_alloc(&self, buf: &AttrBuffer) -> Result { - if buf.id != self.id { - Err(PoolError::MismatchedId) - } else if buf.layout != self.layout { - Err(PoolError::MismatchedLayout) - } else if buf.count > self.count { - Err(PoolError::TooBig) - } else if let Some(best_index) = self.best_fit(buf.count) { - Ok(best_index) - } else { - Err(PoolError::NoMoreRoom) - } - } - - /// Finds the index of the best-fit free space for an array of attributes. - /// - /// TODO: use a binary tree to find best-fit free space in logarithmic time - pub fn best_fit(&self, count: usize) -> Option { - let mut best_index = None; - let mut best_count = usize::MAX; - for (index, space) in self.free_space.iter().enumerate() { - if space.count >= count && space.count < best_count { - best_index = Some(index); - best_count = space.count; - } - } - - best_index - } - - /// Allocates an [AttrBuffer]. - /// - /// If you need to check if an [AttrBuffer] can be successfully - /// allocated without moving it into this function, try using - /// [Self::can_alloc] instead. - pub fn alloc(&mut self, buf: AttrBuffer) -> Result { - self.can_alloc(&buf)?; - - // can_alloc() should catch potential panics - let best_index = self.best_fit(buf.count).unwrap(); - let free_space = self.free_space.get_mut(best_index).unwrap(); - - let alloc = AttrAlloc { - id: buf.id, - offset: free_space.offset, - count: buf.count, - }; - - self.allocs.push(alloc); - - if free_space.count > buf.count { - free_space.count -= buf.count; - free_space.offset += buf.count; - } else { - self.free_space.remove(best_index); - } - - Ok(alloc) - } - - /// Frees an [AttrAlloc] from the pool. - pub fn free(&mut self, alloc: AttrAlloc) -> Result<(), PoolError> { - todo!() - } -} - -/// A set of GPU-side vertex attribute pools and index pools. -pub struct MeshPool { - pools: HashMap, - meshes: Slab, -} - -impl MeshPool { - pub fn new() -> Self { - Self { - pools: Default::default(), - meshes: Default::default(), - } - } - - /// Registers an [AttrId], and creates the pool for it. - /// - /// Fails if the [AttrId] has already been registered. - /// - /// `pool_size` defines the size of the new pool. Once an attribute pool - /// has been created, it cannot be resized, so if it runs out of room for - /// new attributes, a new [MeshPool] must be created. - pub fn add_attribute( - &mut self, - id: AttrId, - layout: AttrLayout, - pool_size: usize, - ) -> Result<(), PoolError> { - if self.pools.contains_key(&id) { - return Err(PoolError::AttrTaken); - } - - let pool = AttrPool::new(id, layout, pool_size)?; - self.pools.insert(id, pool); - - Ok(()) - } - - /// Checks to see if a mesh can be allocated within this pool. - /// - /// Because [Self::alloc] takes ownership of the [MeshBuffer], this function - /// can be called with a reference, to determine if a different pool needs - /// to be used instead. - pub fn can_alloc(&self, buf: &MeshBuffer) -> Result<(), PoolError> { - for attr in buf.attributes.iter() { - match self.pools.get(&attr.id) { - None => return Err(PoolError::AttrUnregistered), - Some(pool) => pool.can_alloc(attr)?, - }; - } - - Ok(()) - } - - /// Allocates a [MeshBuffer] in this pool. Returns a mesh key. - /// - /// If you need to still have ownership of the mesh in the occasion that - /// allocation fails, [Self::can_alloc] can be used instead without - /// consuming it. - pub fn alloc(&mut self, buf: MeshBuffer) -> Result { - self.can_alloc(&buf)?; - - let mut allocs = SmallVec::with_capacity(buf.attributes.len()); - for attr in buf.attributes.into_iter() { - match self.pools.get_mut(&attr.id) { - None => unreachable!(), - Some(pool) => allocs.push(pool.alloc(attr)?), - } - } - - let mesh = MeshAlloc { attributes: allocs }; - Ok(self.meshes.insert(mesh)) - } -} diff --git a/src/mesh/attr.rs b/src/mesh/attr.rs new file mode 100644 index 0000000..52d39be --- /dev/null +++ b/src/mesh/attr.rs @@ -0,0 +1,172 @@ +//! Mesh storage pooling for a single attribute. +//! +//! Attribute pools have a fixed size, and once created cannot be expanded to +//! fit more data. + +use super::*; + +/// An externally-defined identifier for a mesh attribute. +#[repr(transparent)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub struct AttrId(pub usize); + +/// A description of a mesh attribute. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct AttrLayout { + /// The size (in bytes) of this attribute. + pub size: usize, +} + +/// An attribute buffer that has been allocated in an [AttrPool]. +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +pub struct AttrAlloc { + offset: usize, + count: usize, +} + +/// An unused space range in an [AttrPool]. +pub struct FreeSpace { + offset: usize, + count: usize, +} + +/// A single GPU buffer containing linear arrays of attributes. +pub struct AttrPool { + group: usize, + id: AttrId, + layout: AttrLayout, + size: usize, + allocs: Slab, + free_space: Vec, +} + +impl AttrPool { + pub fn new( + group: usize, + id: AttrId, + layout: AttrLayout, + size: usize, + ) -> Result { + Ok(Self { + group, + id, + layout, + size, + free_space: vec![FreeSpace { + offset: 0, + count: size, + }], + allocs: Default::default(), + }) + } + + /// Tests if attributes can be allocated. + /// + /// Returns the result of [Self::best_fit]. + pub fn can_alloc(&self, count: usize) -> Result { + if count > self.size { + Err(PoolError::TooBig) + } else if let Some(best_index) = self.best_fit(count) { + Ok(best_index) + } else { + Err(PoolError::NoMoreRoom) + } + } + + /// Tests if an [AttrBuffer] can be loaded. + /// + /// Returns the result of [Self::best_fit]. + pub fn can_load(&self, buf: &AttrBuffer) -> Result { + if buf.id != self.id { + Err(PoolError::MismatchedId) + } else if buf.layout != self.layout { + Err(PoolError::MismatchedLayout) + } else { + self.can_alloc(buf.count) + } + } + + /// Finds the index of the best-fit free space for an array of attributes. + /// + /// TODO: use a binary tree to find best-fit free space in logarithmic time + pub fn best_fit(&self, count: usize) -> Option { + let mut best_index = None; + let mut best_count = usize::MAX; + for (index, space) in self.free_space.iter().enumerate() { + if space.count >= count && space.count < best_count { + best_index = Some(index); + best_count = space.count; + } + } + + best_index + } + + /// Allocates room for attributes at a specific free space index. + /// + /// Returns the new [AttrAlloc] and its key. + pub fn alloc_at( + &mut self, + index: usize, + count: usize, + ) -> Result<(AttrAlloc, usize), PoolError> { + let free_space = match self.free_space.get_mut(index) { + Some(index) => index, + None => return Err(PoolError::InvalidIndex), + }; + + let alloc = AttrAlloc { + offset: free_space.offset, + count, + }; + + let key = self.allocs.insert(alloc); + + use std::cmp::Ordering; + match free_space.count.cmp(&count) { + Ordering::Less => { + return Err(PoolError::TooBig); + } + Ordering::Equal => { + self.free_space.remove(index); + } + Ordering::Greater => { + free_space.count -= count; + free_space.offset += count; + } + } + + Ok((alloc, key)) + } + + /// Allocates room for attributes. + /// + /// Returns the new [AttrAlloc] and its key. + pub fn alloc(&mut self, count: usize) -> Result<(AttrAlloc, usize), PoolError> { + let best_index = self.can_alloc(count)?; + self.alloc_at(best_index, count) + } + + /// Loads an [AttrBuffer]. + /// + /// Returns the key for the allocation, as well as [CopyInfo] that can be + /// queued into a [StagingPool]. + pub fn load(&mut self, buf: &AttrBuffer) -> Result<(usize, CopyInfo), PoolError> { + let best_index = self.can_load(buf)?; + let (alloc, key) = self.alloc_at(best_index, buf.count)?; + + let copy = CopyInfo { + group: self.group, + target: self.id, + offset: alloc.offset * self.layout.size, + size: alloc.count * self.layout.size, + }; + + Ok((key, copy)) + } + + /// Frees an allocation (by key) from the pool. + pub fn free(&mut self, alloc: usize) -> Result<(), PoolError> { + todo!() + } +} diff --git a/src/mesh/group.rs b/src/mesh/group.rs new file mode 100644 index 0000000..837258b --- /dev/null +++ b/src/mesh/group.rs @@ -0,0 +1,86 @@ +//! Fixed-room pooling of mesh data. + +use super::*; + +/// A mesh that has been allocated in a [MeshGroup]. +pub struct MeshAlloc { + pub attributes: SmallVec<[(usize, AttrId); MAX_MESH_INLINE_ATTRIBUTES]>, +} + +/// A set of GPU-side vertex attribute pools and index pools. +pub struct MeshGroup { + id: usize, + pools: HashMap, + meshes: Slab, +} + +impl MeshGroup { + pub fn new(id: usize) -> Self { + Self { + id, + pools: Default::default(), + meshes: Default::default(), + } + } + + /// Registers an [AttrId], and creates the [AttrPool] for it. + /// + /// Fails if the [AttrId] has already been registered. + /// + /// `pool_size` defines the size of the new pool. Once an attribute pool + /// has been created, it cannot be resized, so if it runs out of room for + /// new attributes, a new [MeshGroup] must be created. + pub fn add_attribute( + &mut self, + id: AttrId, + layout: AttrLayout, + pool_size: usize, + ) -> Result<(), PoolError> { + if self.pools.contains_key(&id) { + return Err(PoolError::AttrTaken); + } + + let pool = AttrPool::new(self.id, id, layout, pool_size)?; + self.pools.insert(id, pool); + + Ok(()) + } + + /// Checks to see if a mesh can be loaded within this group. + pub fn can_load(&self, buf: &MeshBuffer) -> Result<(), PoolError> { + for attr in buf.attributes.iter() { + match self.pools.get(&attr.id) { + None => return Err(PoolError::AttrUnregistered), + Some(pool) => pool.can_load(attr)?, + }; + } + + Ok(()) + } + + /// Tries to load a [MeshBuffer] into this pool. Returns a [MeshHandle]. + pub fn load(&mut self, buf: &MeshBuffer) -> Result<(MeshHandle, Vec), PoolError> { + self.can_load(&buf)?; + + let mut allocs = SmallVec::with_capacity(buf.attributes.len()); + let mut copies = Vec::new(); + + for attr in buf.attributes.iter() { + match self.pools.get_mut(&attr.id) { + None => unreachable!(), + Some(pool) => { + let (alloc, copy) = pool.load(attr)?; + allocs.push((alloc, attr.id)); + copies.push(copy); + } + } + } + + let mesh = MeshAlloc { attributes: allocs }; + let sub = self.meshes.insert(mesh); + let group = self.id; + let handle = MeshHandle { group, sub }; + + Ok((handle, copies)) + } +} diff --git a/src/mesh/mod.rs b/src/mesh/mod.rs new file mode 100644 index 0000000..b21a85f --- /dev/null +++ b/src/mesh/mod.rs @@ -0,0 +1,129 @@ +//! Dynamic mesh data storage. +//! +//! Meshes are based on ECS-like archetypes. Each pool contains a set of mesh +//! "attributes," which can be either vertex attributes, indices of different +//! formats (u8, u16, u32), or in the future, fixed-size mesh chunklets too. +//! The mesh pool itself is agnostic to specific rendering implementation. It +//! has no implicit knowledge of what a vertex position, normal, or texture +//! coordinate is, or even what an index is. +//! +//! Multiple attributes can have the same layout. For example, a rudimentary +//! mesh format might use three 32-bit floating point values (`[f32; 3]`) for +//! both vertex position and vertex normals. In this case, positions and normals +//! would have different [AttrIds][AttrId] to distuingish them, and must each be +//! registered to the pool. Once an attribute is registered in a pool instance, +//! it cannot be unregistered, although the mesh pool may free GPU buffers for +//! unused attribute pools. +//! +//! Meshes are pooled by [groups][MeshGroup], so all mesh data in a group +//! shares the same memory. This allows the rendering pipeline to operate on as +//! much mesh data simultaneously as possible without rebinding buffers, +//! enabling some highly-efficient rendering techniques like bindless forward +//! rendering, bindless vertex skinning, and mesh shading. +//! +//! However, because a mesh groups' underlying buffers are so large, they cannot +//! be resized without copying all of the mesh data within to a new allocation, +//! putting a lot of pressure on the GPU's memory bus and causing massive lag +//! spikes. Instead, an entirely new group must be created to store more mesh +//! data. In practice, new groups will not be created often, again due to the +//! large size of their underlying buffers. +//! +//! When a mesh is loaded, the pool is searched for a group that has spare room +//! for all of the mesh's attributes. If one is found, the pool copies the mesh's +//! attribute data into the pool's internal staging buffer, which is later +//! copied by the GPU into the corresponding attribute pools in the selected +//! group. If no group has enough free space to store all of the attributes, a +//! new group is created. +//! +//! Staging buffers are fixed-size, so when a large amount of mesh data is loaded +//! at once and the pool can't fit it all into an available staging buffer, the +//! memory is instead copied to a CPU-side spillover buffer, and GPU transfer is +//! deferred to a future staging pass. Because of this, meshes are not guaranteed +//! to be available for drawing on the frame that they are loaded. +//! +//! TODO: mesh coherency +//! TODO: make spillover buffers GPU-transferrable on iGPUs + +use slab::Slab; +use smallvec::SmallVec; +use std::collections::HashMap; + +pub mod attr; +pub mod group; +pub mod staging; + +use attr::*; +use group::*; +use staging::*; + +/// An error that can be returned when allocating a mesh. +pub enum PoolError { + TooBig, + NoMoreRoom, + InvalidIndex, + AttrTaken, + AttrUnregistered, + MismatchedId, + MismatchedLayout, +} + +/// The number of attributes a mesh can have before they're moved to the heap. +pub const MAX_MESH_INLINE_ATTRIBUTES: usize = 16; + +/// The data and layout of a single mesh attribute. +pub struct AttrBuffer { + pub id: AttrId, + pub layout: AttrLayout, + pub count: usize, + pub data: Vec, +} + +/// A mesh and all of its attributes. +/// +/// An attribute ID can be used multiple times in a mesh, corresponding to +/// multiple allocations within an [AttrPool]. +pub struct MeshBuffer { + pub attributes: SmallVec<[AttrBuffer; MAX_MESH_INLINE_ATTRIBUTES]>, +} + +/// A handle to an allocated mesh. +pub struct MeshHandle { + pub(crate) group: usize, + pub(crate) sub: usize, +} + +/// The top-level mesh data pool. +pub struct MeshPool { + pub staging: StagingPool, + pub groups: Vec, +} + +impl MeshPool { + pub fn new() -> Self { + Self { + staging: StagingPool::new(1_000_000), + groups: Default::default(), + } + } + + pub fn load(&mut self, buf: &MeshBuffer) -> Result { + for group in self.groups.iter_mut() { + match group.load(buf) { + Ok((handle, copies)) => { + self.staging.queue_copies(copies); + return Ok(handle); + } + Err(PoolError::NoMoreRoom) => {} + Err(e) => return Err(e), + } + } + + let group_index = self.groups.len(); + self.groups.push(MeshGroup::new(group_index)); + let group = self.groups.get_mut(group_index).unwrap(); + + let (handle, copies) = group.load(buf)?; + self.staging.queue_copies(copies); + Ok(handle) + } +} diff --git a/src/mesh/staging.rs b/src/mesh/staging.rs new file mode 100644 index 0000000..2679aec --- /dev/null +++ b/src/mesh/staging.rs @@ -0,0 +1,51 @@ +//! Intermediate CPU-mappable, GPU-visible storage for transferral to an attribute pool. +//! +//! TODO: double-buffered staging + +use super::*; +use std::collections::VecDeque; + +pub struct StagingPool { + stage_size: usize, + current_budget: usize, + copies: Vec, + spillover: VecDeque, +} + +impl StagingPool { + pub fn new(stage_size: usize) -> Self { + Self { + stage_size, + current_budget: 0, + copies: Default::default(), + spillover: Default::default(), + } + } + + pub fn flush(&mut self) { + todo!() + } + + pub fn queue_copies(&mut self, copies: Vec) { + todo!() + } +} + +pub struct CopyInfo { + /// The index of the target attribute pool's group. + pub group: usize, + + /// The target attribute pool within the group. + pub target: AttrId, + + /// The destination offset *in bytes.* + pub offset: usize, + + /// The copy size *in bytes.* + pub size: usize, +} + +pub struct SpilloverBuffer { + pub info: CopyInfo, + pub data: Vec, +}