From 69f68ddf294a8d0d892c967e817705a0fe6e22da Mon Sep 17 00:00:00 2001 From: Joshua Yanovski Date: Tue, 29 Sep 2020 18:51:02 +0200 Subject: [PATCH] Reduce chunks / chonk by trimming the ends. This improves the defragment operation for chonks by letting them remove chunks at the top that match above, and bottom that match below. This reduces the chunks / chonk from around 5.9 to around 3.4 at origin. From my investigations, adding something for water would probably get us a full 50% reduction, if we could collapse intermediate chunks; block types other than rock / air / water never appear to have full chunks of the same block, so any additional optimization will require changes to the subchunk compression format or changes to the actual chunks we generate. --- common/src/terrain/chonk.rs | 48 +++++++++++++++++++++++++++++++++++++ common/src/volumes/chunk.rs | 12 ++++++++++ 2 files changed, 60 insertions(+) diff --git a/common/src/terrain/chonk.rs b/common/src/terrain/chonk.rs index 5fe51fdb1d..9651d5ffde 100644 --- a/common/src/terrain/chonk.rs +++ b/common/src/terrain/chonk.rs @@ -90,7 +90,47 @@ impl Chonk { where V: Clone + Eq + Hash, { + // First, defragment all subchunks. self.sub_chunks.iter_mut().for_each(SubChunk::defragment); + // For each homogeneous subchunk (i.e. those where all blocks are the same), + // find those which match `below` at the bottom of the cunk, or `above` + // at the top, since these subchunks are redundant and can be removed. + // Note that we find (and drain) the above chunks first, so that when we + // remove the below chunks we have fewer remaining chunks to backshift. + // Note that we use `take_while` instead of `rposition` here because `rposition` + // goes one past the end, which we only want in the forward direction. + let above_count = self + .sub_chunks + .iter() + .rev() + .take_while(|subchunk| subchunk.homogeneous() == Some(&self.above)) + .count(); + // Unfortunately, `TakeWhile` doesn't implement `ExactSizeIterator` or + // `DoubleEndedIterator`, so we have to recreate the same state by calling + // `nth_back` (note that passing 0 to nth_back goes back 1 time, not 0 + // times!). + let mut subchunks = self.sub_chunks.iter(); + if above_count > 0 { + subchunks.nth_back(above_count - 1); + } + // `above_index` is now the number of remaining elements, since all the elements + // we drained were at the end. + let above_index = subchunks.len(); + // `below_len` now needs to be applied to the state after the `above` chunks are + // drained, to make sure we don't accidentally have overlap (this is + // possible if self.above == self.below). + let below_len = subchunks.position(|subchunk| subchunk.homogeneous() != Some(&self.below)); + let below_len = below_len + // NOTE: If `below_index` is `None`, then every *remaining* chunk after we drained + // `above` was full and matched `below`. + .unwrap_or(above_index); + // Now, actually remove the redundant chunks. + self.sub_chunks.truncate(above_index); + self.sub_chunks.drain(..below_len); + // Finally, bump the z_offset to account for the removed subchunks at the + // bottom. TODO: Add invariants to justify why `below_len` must fit in + // i32. + self.z_offset += below_len as i32 * SubChunkSize::::SIZE.z as i32; } } @@ -131,6 +171,10 @@ impl WriteVol for Chonk let mut sub_chunk_idx = self.sub_chunk_idx(pos.z); if pos.z < self.get_min_z() { + // Make sure we're not adding a redundant chunk. + if block == self.below { + return Ok(()); + } // Prepend exactly sufficiently many SubChunks via Vec::splice let c = Chunk::, M>::filled(self.below.clone(), self.meta.clone()); let n = (-sub_chunk_idx) as usize; @@ -138,6 +182,10 @@ impl WriteVol for Chonk self.z_offset += sub_chunk_idx * SubChunkSize::::SIZE.z as i32; sub_chunk_idx = 0; } else if pos.z >= self.get_max_z() { + // Make sure we're not adding a redundant chunk. + if block == self.above { + return Ok(()); + } // Append exactly sufficiently many SubChunks via Vec::extend let c = Chunk::, M>::filled(self.above.clone(), self.meta.clone()); let n = 1 + sub_chunk_idx as usize - self.sub_chunks.len(); diff --git a/common/src/volumes/chunk.rs b/common/src/volumes/chunk.rs index 832fd44c87..d680957b5a 100644 --- a/common/src/volumes/chunk.rs +++ b/common/src/volumes/chunk.rs @@ -204,6 +204,18 @@ impl Chunk { pub fn num_groups(&self) -> usize { self.vox.len() / Self::GROUP_VOLUME as usize } + /// Returns `Some(v)` if the block is homogeneous and contains nothing but + /// voxels of value `v`, and `None` otherwise. This method is + /// conservative (it may return None when the chunk is + /// actually homogeneous) unless called immediately after `defragment`. + pub fn homogeneous(&self) -> Option<&V> { + if self.num_groups() == 0 { + Some(&self.default) + } else { + None + } + } + #[inline(always)] fn grp_idx(pos: Vec3) -> u32 { let grp_pos = pos.map2(Self::GROUP_SIZE, |e, s| e as u32 / s);