From 153cde1a04d640e8929505eab13bb96da27fb35e Mon Sep 17 00:00:00 2001 From: Ryan Johnson Date: Fri, 7 Oct 2022 15:52:55 -0700 Subject: [PATCH] Chunk Rewrite With Paletted Containers (#91) The current approach to managing chunk data is misconceived. This new approach uses genuine paletted containers and does not suffer from complexities caused by caching. As a result, memory usage (according to htop) in the terrain example with render distance = 32 has gone from 785 megs to 137 megs. That's 17.4% of the memory it used to use. Terrain generation speed was not affected. --- build/block.rs | 5 - examples/combat.rs | 5 +- examples/conway.rs | 2 +- examples/terrain.rs | 6 +- src/chunk.rs | 743 ++++++++++++++++++-------------- src/chunk/paletted_container.rs | 310 +++++++++++++ src/client.rs | 1 - src/config.rs | 33 +- src/entity.rs | 18 +- src/server.rs | 6 - src/util.rs | 11 + src/world.rs | 12 +- 12 files changed, 793 insertions(+), 359 deletions(-) create mode 100644 src/chunk/paletted_container.rs diff --git a/build/block.rs b/build/block.rs index 54f3152..a1d4336 100644 --- a/build/block.rs +++ b/build/block.rs @@ -453,11 +453,6 @@ pub fn build() -> anyhow::Result { } } - pub(crate) const fn from_raw_unchecked(id: u16) -> Self { - debug_assert!(Self::from_raw(id).is_some()); - Self(id) - } - /// Returns the [`BlockKind`] of this block state. pub const fn to_kind(self) -> BlockKind { match self.0 { diff --git a/examples/combat.rs b/examples/combat.rs index 9b6127e..df1a7ec 100644 --- a/examples/combat.rs +++ b/examples/combat.rs @@ -88,9 +88,8 @@ impl Config for Game { let (_, world) = server.worlds.insert(DimensionId::default(), ()); server.state = Some(server.player_lists.insert(()).0); - let dim = server.shared.dimension(DimensionId::default()); - let min_y = dim.min_y; - let height = dim.height as usize; + let min_y = world.chunks.min_y(); + let height = world.chunks.height(); // Create circular arena. let size = 2; diff --git a/examples/conway.rs b/examples/conway.rs index 2202977..3673978 100644 --- a/examples/conway.rs +++ b/examples/conway.rs @@ -279,7 +279,7 @@ impl Config for Game { mem::swap(&mut server.state.board, &mut server.state.board_buf); } - let min_y = server.shared.dimensions().next().unwrap().1.min_y; + let min_y = world.chunks.min_y(); for chunk_x in 0..Integer::div_ceil(&SIZE_X, &16) { for chunk_z in 0..Integer::div_ceil(&SIZE_Z, &16) { diff --git a/examples/terrain.rs b/examples/terrain.rs index 993d5d7..caa1a17 100644 --- a/examples/terrain.rs +++ b/examples/terrain.rs @@ -203,8 +203,8 @@ impl Config for Game { // Add grass for y in (0..chunk.height()).rev() { - if chunk.get_block_state(x, y, z).is_air() - && chunk.get_block_state(x, y - 1, z) == BlockState::GRASS_BLOCK + if chunk.block_state(x, y, z).is_air() + && chunk.block_state(x, y - 1, z) == BlockState::GRASS_BLOCK { let density = fbm( &self.grass_noise, @@ -215,7 +215,7 @@ impl Config for Game { ); if density > 0.55 { - if density > 0.7 && chunk.get_block_state(x, y + 1, z).is_air() { + if density > 0.7 && chunk.block_state(x, y + 1, z).is_air() { let upper = BlockState::TALL_GRASS .set(PropName::Half, PropValue::Upper); let lower = BlockState::TALL_GRASS diff --git a/src/chunk.rs b/src/chunk.rs index 1ad2ec8..8f722b4 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -6,15 +6,13 @@ //! In addition to blocks, chunks also contain [biomes](crate::biome::Biome). //! Every 4x4x4 segment of blocks in a chunk corresponds to a biome. -// TODO: https://github.com/rust-lang/rust/issues/88581 for div_ceil - use std::collections::hash_map::Entry; use std::collections::HashMap; use std::io::Write; use std::iter::FusedIterator; use bitvec::vec::BitVec; -use num::Integer; +use paletted_container::{PalettedContainer, PalettedContainerElement}; use rayon::iter::{IntoParallelRefIterator, IntoParallelRefMutIterator, ParallelIterator}; use valence_nbt::compound; @@ -23,26 +21,27 @@ use crate::block::BlockState; use crate::block_pos::BlockPos; pub use crate::chunk_pos::ChunkPos; use crate::config::Config; -use crate::dimension::DimensionId; use crate::protocol::packets::s2c::play::{ BlockUpdate, ChunkDataAndUpdateLight, S2cPlayPacket, UpdateSectionBlocks, }; use crate::protocol::{Encode, VarInt, VarLong}; -use crate::server::SharedServer; +use crate::util::log2_ceil; + +mod paletted_container; /// A container for all [`LoadedChunk`]s in a [`World`](crate::world::World). pub struct Chunks { chunks: HashMap>, - shared: SharedServer, - dimension: DimensionId, + dimension_height: i32, + dimension_min_y: i32, } impl Chunks { - pub(crate) fn new(shared: SharedServer, dimension: DimensionId) -> Self { + pub(crate) fn new(dimension_height: i32, dimension_min_y: i32) -> Self { Self { chunks: HashMap::new(), - shared, - dimension, + dimension_height, + dimension_min_y, } } @@ -64,9 +63,8 @@ impl Chunks { chunk: UnloadedChunk, state: C::ChunkState, ) -> &mut LoadedChunk { - let dimension_section_count = (self.shared.dimension(self.dimension).height / 16) as usize; - let biome_registry_len = self.shared.biomes().len(); - let loaded = LoadedChunk::new(chunk, dimension_section_count, biome_registry_len, state); + let dimension_section_count = (self.dimension_height / 16) as usize; + let loaded = LoadedChunk::new(chunk, dimension_section_count, state); match self.chunks.entry(pos.into()) { Entry::Occupied(mut oe) => { @@ -84,13 +82,31 @@ impl Chunks { pub fn remove(&mut self, pos: impl Into) -> Option<(UnloadedChunk, C::ChunkState)> { let loaded = self.chunks.remove(&pos.into())?; - let unloaded = UnloadedChunk { + let mut unloaded = UnloadedChunk { sections: loaded.sections.into(), }; + for sect in &mut unloaded.sections { + sect.modified_blocks.fill(0); + sect.modified_blocks_count = 0; + } + Some((unloaded, loaded.state)) } + /// Returns the height of all loaded chunks in the world. This returns the + /// same value as [`Chunk::height`] for all loaded chunks. + pub fn height(&self) -> usize { + self.dimension_height as usize + } + + /// The minimum Y coordinate in world space that chunks in this world can + /// occupy. This is relevant for [`Chunks::block_state`] and + /// [`Chunks::set_block_state`]. + pub fn min_y(&self) -> i32 { + self.dimension_min_y + } + /// Returns the number of loaded chunks. pub fn len(&self) -> usize { self.chunks.len() @@ -166,19 +182,17 @@ impl Chunks { /// /// **Note**: if you need to get a large number of blocks, it is more /// efficient to read from the chunks directly with - /// [`Chunk::get_block_state`]. - pub fn get_block_state(&self, pos: impl Into) -> Option { + /// [`Chunk::block_state`]. + pub fn block_state(&self, pos: impl Into) -> Option { let pos = pos.into(); let chunk_pos = ChunkPos::from(pos); let chunk = self.get(chunk_pos)?; - let min_y = self.shared.dimension(self.dimension).min_y; - - let y = pos.y.checked_sub(min_y)?.try_into().ok()?; + let y = pos.y.checked_sub(self.dimension_min_y)?.try_into().ok()?; if y < chunk.height() { - Some(chunk.get_block_state( + Some(chunk.block_state( pos.x.rem_euclid(16) as usize, y, pos.z.rem_euclid(16) as usize, @@ -202,9 +216,11 @@ impl Chunks { let chunk_pos = ChunkPos::from(pos); if let Some(chunk) = self.chunks.get_mut(&chunk_pos) { - let min_y = self.shared.dimension(self.dimension).min_y; - - if let Some(y) = pos.y.checked_sub(min_y).and_then(|y| y.try_into().ok()) { + if let Some(y) = pos + .y + .checked_sub(self.dimension_min_y) + .and_then(|y| y.try_into().ok()) + { if y < chunk.height() { chunk.set_block_state( pos.x.rem_euclid(16) as usize, @@ -220,25 +236,10 @@ impl Chunks { false } - /// Apply chunk modifications to only the chunks that were created this - /// tick. - pub(crate) fn update_created_this_tick(&mut self) { - let biome_registry_len = self.shared.biomes().len(); - self.chunks.par_iter_mut().for_each(|(_, chunk)| { - if chunk.created_this_tick() { - chunk.apply_modifications(biome_registry_len); - } - }); - } - - /// Apply chunk modifications to all chunks and clear the created_this_tick - /// flag. pub(crate) fn update(&mut self) { - let biome_registry_len = self.shared.biomes().len(); - self.chunks.par_iter_mut().for_each(|(_, chunk)| { - chunk.apply_modifications(biome_registry_len); - chunk.created_this_tick = false; - }); + for (_, chunk) in self.chunks.iter_mut() { + chunk.update(); + } } } @@ -253,14 +254,15 @@ pub trait Chunk { /// /// **Note**: The arguments to this function are offsets from the minimum /// corner of the chunk in _chunk space_ rather than _world space_. You - /// might be looking for [`Chunks::get_block_state`] instead. + /// might be looking for [`Chunks::block_state`] instead. /// /// # Panics /// /// Panics if the offsets are outside the bounds of the chunk. - fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState; + fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState; - /// Sets the block state at the provided offsets in the chunk. + /// Sets the block state at the provided offsets in the chunk. The previous + /// block state at the position is returned. /// /// **Note**: The arguments to this function are offsets from the minimum /// corner of the chunk in _chunk space_ rather than _world space_. You @@ -269,7 +271,17 @@ pub trait Chunk { /// # Panics /// /// Panics if the offsets are outside the bounds of the chunk. - fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState); + fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState; + + /// Sets every block state in this chunk to the given block state. + /// + /// This is semantically equivalent to calling [`set_block_state`] on every + /// block in the chunk followed by a call to [`optimize`] at the end. + /// However, this function may be implemented more efficiently. + /// + /// [`set_block_state`]: Self::set_block_state + /// [`optimize`]: Self::optimize + fn fill_block_states(&mut self, block: BlockState); /// Gets the biome at the provided biome offsets in the chunk. /// @@ -279,9 +291,10 @@ pub trait Chunk { /// # Panics /// /// Panics if the offsets are outside the bounds of the chunk. - fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId; + fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId; - /// Sets the biome at the provided biome offsets in the chunk. + /// Sets the biome at the provided offsets in the chunk. The previous + /// biome at the position is returned. /// /// **Note**: the arguments are **not** block positions. Biomes are 4x4x4 /// segments of a chunk, so `x` and `z` are in `0..4`. @@ -289,18 +302,35 @@ pub trait Chunk { /// # Panics /// /// Panics if the offsets are outside the bounds of the chunk. - fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId); + fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId; + + /// Sets every biome in this chunk to the given biome. + /// + /// This is semantically equivalent to calling [`set_biome`] on every + /// biome in the chunk followed by a call to [`optimize`] at the end. + /// However, this function may be implemented more efficiently. + /// + /// [`set_biome`]: Self::set_biome + /// [`optimize`]: Self::optimize + fn fill_biomes(&mut self, biome: BiomeId); + + /// Optimizes this chunk to use the minimum amount of memory possible. It + /// should have no observable effect on the contents of the chunk. + /// + /// This is a potentially expensive operation. The function is most + /// effective when a large number of blocks and biomes have changed states. + fn optimize(&mut self); } /// A chunk that is not loaded in any world. pub struct UnloadedChunk { sections: Vec, - // TODO: block_entities: HashMap, + // TODO: block_entities: BTreeMap, } impl UnloadedChunk { - /// Constructs a new unloaded chunk containing only [`BlockState::AIR`] with - /// the given height in blocks. + /// Constructs a new unloaded chunk containing only [`BlockState::AIR`] and + /// [`BiomeId::default()`] with the given height in blocks. /// /// # Panics /// @@ -319,11 +349,11 @@ impl UnloadedChunk { /// expensive operation that may involve copying. /// /// The chunk is extended and truncated from the top. New blocks are always - /// [`BlockState::AIR`]. + /// [`BlockState::AIR`] and biomes are [`BiomeId::default()`]. /// /// # Panics /// - /// The constraints on `new_height` are the same as [`Self::new`]. + /// The constraints on `new_height` are the same as [`UnloadedChunk::new`]. pub fn resize(&mut self, new_height: usize) { assert!( new_height % 16 == 0 && new_height <= 4064, @@ -333,16 +363,10 @@ impl UnloadedChunk { let old_height = self.sections.len() * 16; if new_height > old_height { - let sect = ChunkSection { - blocks: [BlockState::AIR.to_raw(); 4096], - modified_count: 0, - biomes: [BiomeId::default(); 64], - compact_data: Vec::new(), - }; - let additional = (new_height - old_height) / 16; self.sections.reserve_exact(additional); - self.sections.resize_with(new_height / 16, || sect.clone()); + self.sections + .resize_with(new_height / 16, ChunkSection::default); debug_assert_eq!(self.sections.capacity(), self.sections.len()); } else if new_height < old_height { self.sections.truncate(new_height / 16); @@ -362,43 +386,81 @@ impl Chunk for UnloadedChunk { self.sections.len() * 16 } - fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState { + fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState { assert!( x < 16 && y < self.height() && z < 16, - "chunk block offsets must be within bounds" + "chunk block offsets of ({x}, {y}, {z}) are out of bounds" ); - BlockState::from_raw_unchecked( - self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] & BLOCK_STATE_MASK, - ) + self.sections[y / 16] + .block_states + .get(x + z * 16 + y % 16 * 16 * 16) } - fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) { + fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState { assert!( x < 16 && y < self.height() && z < 16, - "chunk block offsets must be within bounds" + "chunk block offsets of ({x}, {y}, {z}) are out of bounds" ); - self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] = block.to_raw(); - // TODO: handle block entity here? + let mut sect = &mut self.sections[y / 16]; + + let old_block = sect.block_states.set(x + z * 16 + y % 16 * 16 * 16, block); + + match (block.is_air(), old_block.is_air()) { + (true, false) => sect.non_air_count -= 1, + (false, true) => sect.non_air_count += 1, + _ => {} + } + + old_block } - fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId { - assert!( - x < 4 && y < self.height() / 4 && z < 4, - "chunk biome offsets must be within bounds" - ); + fn fill_block_states(&mut self, block: BlockState) { + for sect in self.sections.iter_mut() { + // TODO: adjust motion blocking here. - self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] + if block.is_air() { + sect.non_air_count = 0; + } else { + sect.non_air_count = SECTION_BLOCK_COUNT as u16; + } + + sect.block_states.fill(block); + } } - fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) { + fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId { assert!( x < 4 && y < self.height() / 4 && z < 4, - "chunk biome offsets must be within bounds" + "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" ); - self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = biome; + self.sections[y / 4].biomes.get(x + z * 4 + y % 4 * 4 * 4) + } + + fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId { + assert!( + x < 4 && y < self.height() / 4 && z < 4, + "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" + ); + + self.sections[y / 4] + .biomes + .set(x + z * 4 + y % 4 * 4 * 4, biome) + } + + fn fill_biomes(&mut self, biome: BiomeId) { + for sect in self.sections.iter_mut() { + sect.biomes.fill(biome); + } + } + + fn optimize(&mut self) { + for sect in self.sections.iter_mut() { + sect.block_states.optimize(); + sect.biomes.optimize(); + } } } @@ -407,59 +469,89 @@ pub struct LoadedChunk { /// Custom state. pub state: C::ChunkState, sections: Box<[ChunkSection]>, - // TODO block_entities: HashMap, - /// The MOTION_BLOCKING heightmap - heightmap: Vec, + // TODO block_entities: BTreeMap, + // TODO: motion_blocking_heightmap: Box<[u16; 256]>, created_this_tick: bool, } -/// A 16x16x16 section of blocks, biomes, and light in a chunk. +/// A 16x16x16 meter volume of blocks, biomes, and light in a chunk. #[derive(Clone)] struct ChunkSection { - /// The block states in this section stored in x, z, y order. - /// The most significant bit is used to indicate if this block has been - /// modified. - blocks: [u16; 4096], - /// The number of modified blocks - modified_count: u16, - biomes: [BiomeId; 64], - compact_data: Vec, + block_states: PalettedContainer, + /// Contains a set bit for every block that has been modified in this + /// section this tick. Ignored in unloaded chunks. + modified_blocks: [usize; SECTION_BLOCK_COUNT / USIZE_BITS], + /// The number of blocks that have been modified in this section this tick. + /// Ignored in unloaded chunks. + modified_blocks_count: u16, + /// Number of non-air blocks in this section. + non_air_count: u16, + biomes: PalettedContainer, } -const BLOCK_STATE_MASK: u16 = 0x7fff; +// [T; 64] Doesn't implement Default so we can't derive :( +impl Default for ChunkSection { + fn default() -> Self { + Self { + block_states: Default::default(), + modified_blocks: [Default::default(); SECTION_BLOCK_COUNT / USIZE_BITS], + modified_blocks_count: Default::default(), + non_air_count: Default::default(), + biomes: Default::default(), + } + } +} -const _: () = assert!( - BlockState::max_raw() <= BLOCK_STATE_MASK, - "There is not enough space in the block state type to store the modified bit. A bit array \ - separate from the block state array should be created to keep track of modified blocks in \ - the chunk section." -); +const SECTION_BLOCK_COUNT: usize = 4096; +const USIZE_BITS: usize = usize::BITS as _; + +impl PalettedContainerElement for BlockState { + const DIRECT_BITS: usize = log2_ceil(BlockState::max_raw() as _); + const MAX_INDIRECT_BITS: usize = 8; + const MIN_INDIRECT_BITS: usize = 4; + + fn to_bits(self) -> u64 { + self.to_raw() as _ + } +} + +impl PalettedContainerElement for BiomeId { + const DIRECT_BITS: usize = 6; + const MAX_INDIRECT_BITS: usize = 4; + const MIN_INDIRECT_BITS: usize = 0; + + fn to_bits(self) -> u64 { + self.0 as _ + } +} + +impl ChunkSection { + fn mark_block_as_modified(&mut self, idx: usize) { + if !self.is_block_modified(idx) { + self.modified_blocks[idx / USIZE_BITS] |= 1 << (idx % USIZE_BITS); + self.modified_blocks_count += 1; + } + } + + fn mark_all_blocks_as_modified(&mut self) { + self.modified_blocks.fill(usize::MAX); + self.modified_blocks_count = SECTION_BLOCK_COUNT as u16; + } + + fn is_block_modified(&self, idx: usize) -> bool { + self.modified_blocks[idx / USIZE_BITS] >> (idx % USIZE_BITS) & 1 == 1 + } +} impl LoadedChunk { - fn new( - mut chunk: UnloadedChunk, - dimension_section_count: usize, - biome_registry_len: usize, - state: C::ChunkState, - ) -> Self { + fn new(mut chunk: UnloadedChunk, dimension_section_count: usize, state: C::ChunkState) -> Self { chunk.resize(dimension_section_count * 16); - let mut sections = chunk.sections.into_boxed_slice(); - - // Mark all sections as modified so the chunk is properly initialized. - for sect in sections.iter_mut() { - sect.modified_count = 1; - } - - let mut loaded = Self { + Self { state, - sections, - heightmap: Vec::new(), + sections: chunk.sections.into_boxed_slice(), created_this_tick: true, - }; - - loaded.apply_modifications(biome_registry_len); - loaded + } } /// Returns `true` if this chunk was created during the current tick. @@ -467,23 +559,25 @@ impl LoadedChunk { self.created_this_tick } - /// Gets the chunk data packet for this chunk with the given position. This - /// does not include unapplied changes. + /// Gets the chunk data packet for this chunk with the given position. pub(crate) fn chunk_data_packet(&self, pos: ChunkPos) -> ChunkDataAndUpdateLight { let mut blocks_and_biomes = Vec::new(); for sect in self.sections.iter() { - blocks_and_biomes.extend_from_slice(§.compact_data); + sect.non_air_count.encode(&mut blocks_and_biomes).unwrap(); + sect.block_states.encode(&mut blocks_and_biomes).unwrap(); + sect.biomes.encode(&mut blocks_and_biomes).unwrap(); } ChunkDataAndUpdateLight { chunk_x: pos.x, chunk_z: pos.z, heightmaps: compound! { - "MOTION_BLOCKING" => self.heightmap.clone(), + // TODO: placeholder heightmap. + "MOTION_BLOCKING" => vec![0_i64; 37], }, blocks_and_biomes, - block_entities: Vec::new(), // TODO + block_entities: vec![], // TODO trust_edges: true, // sky_light_mask: bitvec![u64, _; 1; section_count + 2], sky_light_mask: BitVec::new(), @@ -491,49 +585,58 @@ impl LoadedChunk { empty_sky_light_mask: BitVec::new(), empty_block_light_mask: BitVec::new(), // sky_light_arrays: vec![[0xff; 2048]; section_count + 2], - sky_light_arrays: Vec::new(), - block_light_arrays: Vec::new(), + sky_light_arrays: vec![], + block_light_arrays: vec![], } } - /// Returns unapplied changes to this chunk as block change packets through - /// the provided closure. + /// Returns changes to this chunk as block change packets through the + /// provided closure. pub(crate) fn block_change_packets( &self, pos: ChunkPos, min_y: i32, - mut push_packet: impl FnMut(BlockChangePacket), + mut push_packet: impl FnMut(S2cPlayPacket), ) { for (sect_y, sect) in self.sections.iter().enumerate() { - if sect.modified_count == 1 { - let (idx, &block) = sect - .blocks + if sect.modified_blocks_count == 1 { + let (i, bits) = sect + .modified_blocks .iter() + .cloned() .enumerate() - .find(|&(_, &b)| b & !BLOCK_STATE_MASK != 0) + .find(|(_, n)| *n > 0) .expect("invalid modified count"); + debug_assert_eq!(bits.count_ones(), 1); + + let idx = i * USIZE_BITS + log2_ceil(bits); + let block = sect.block_states.get(idx); + let global_x = pos.x * 16 + (idx % 16) as i32; let global_y = sect_y as i32 * 16 + (idx / (16 * 16)) as i32 + min_y; let global_z = pos.z * 16 + (idx / 16 % 16) as i32; - push_packet(BlockChangePacket::Single(BlockUpdate { - location: BlockPos::new(global_x, global_y, global_z), - block_id: VarInt((block & BLOCK_STATE_MASK).into()), - })); - } else if sect.modified_count > 1 { - let mut blocks = Vec::new(); + push_packet( + BlockUpdate { + location: BlockPos::new(global_x, global_y, global_z), + block_id: VarInt(block.to_raw() as _), + } + .into(), + ); + } else if sect.modified_blocks_count > 1 { + let mut blocks = Vec::with_capacity(sect.modified_blocks_count.into()); + for y in 0..16 { for z in 0..16 { for x in 0..16 { - let block = - sect.blocks[x as usize + z as usize * 16 + y as usize * 16 * 16]; + let idx = x as usize + z as usize * 16 + y as usize * 16 * 16; - if block & !BLOCK_STATE_MASK != 0 { - blocks.push(VarLong( - ((block & BLOCK_STATE_MASK) as i64) << 12 - | (x << 8 | z << 4 | y), - )) + if sect.is_block_modified(idx) { + let block_id = sect.block_states.get(idx).to_raw(); + let compact = (block_id as i64) << 12 | (x << 8 | z << 4 | y); + + blocks.push(VarLong(compact)); } } } @@ -543,59 +646,26 @@ impl LoadedChunk { | (pos.z as i64 & 0x3fffff) << 20 | (sect_y as i64 + min_y.div_euclid(16) as i64) & 0xfffff; - push_packet(BlockChangePacket::Multi(UpdateSectionBlocks { - chunk_section_position, - invert_trust_edges: false, - blocks, - })); + push_packet( + UpdateSectionBlocks { + chunk_section_position, + invert_trust_edges: false, + blocks, + } + .into(), + ); } } } - fn apply_modifications(&mut self, biome_registry_len: usize) { - let mut any_modified = false; - + fn update(&mut self) { for sect in self.sections.iter_mut() { - if sect.modified_count > 0 { - sect.modified_count = 0; - any_modified = true; - - sect.compact_data.clear(); - - let mut non_air_block_count: i16 = 0; - - for b in &mut sect.blocks { - *b &= BLOCK_STATE_MASK; - if !BlockState::from_raw_unchecked(*b).is_air() { - non_air_block_count += 1; - } - } - - non_air_block_count.encode(&mut sect.compact_data).unwrap(); - - encode_paletted_container( - sect.blocks.iter().cloned(), - 4, - 9, - log2_ceil(BlockState::max_raw() as usize), - &mut sect.compact_data, - ) - .unwrap(); - - encode_paletted_container( - sect.biomes.iter().map(|b| b.0), - 0, - 4, - log2_ceil(biome_registry_len), - &mut sect.compact_data, - ) - .unwrap(); + if sect.modified_blocks_count > 0 { + sect.modified_blocks_count = 0; + sect.modified_blocks.fill(0); } } - - if any_modified { - build_heightmap(&self.sections, &mut self.heightmap); - } + self.created_this_tick = false; } } @@ -604,178 +674,215 @@ impl Chunk for LoadedChunk { self.sections.len() * 16 } - fn get_block_state(&self, x: usize, y: usize, z: usize) -> BlockState { + fn block_state(&self, x: usize, y: usize, z: usize) -> BlockState { assert!( x < 16 && y < self.height() && z < 16, - "chunk block offsets must be within bounds" + "chunk block offsets of ({x}, {y}, {z}) are out of bounds" ); - BlockState::from_raw_unchecked( - self.sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] & BLOCK_STATE_MASK, - ) + self.sections[y / 16] + .block_states + .get(x + z * 16 + y % 16 * 16 * 16) } - fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) { + fn set_block_state(&mut self, x: usize, y: usize, z: usize, block: BlockState) -> BlockState { assert!( x < 16 && y < self.height() && z < 16, - "chunk block offsets must be within bounds" + "chunk block offsets of ({x}, {y}, {z}) are out of bounds" ); let sect = &mut self.sections[y / 16]; let idx = x + z * 16 + y % 16 * 16 * 16; - if block.to_raw() != sect.blocks[idx] & BLOCK_STATE_MASK { - if sect.blocks[idx] & !BLOCK_STATE_MASK == 0 { - sect.modified_count += 1; + let old_block = sect.block_states.set(idx, block); + + if block != old_block { + match (block.is_air(), old_block.is_air()) { + (true, false) => sect.non_air_count -= 1, + (false, true) => sect.non_air_count += 1, + _ => {} } - sect.blocks[idx] = block.to_raw() | !BLOCK_STATE_MASK; - // TODO: handle block entity here? + // TODO: adjust MOTION_BLOCKING here. + + sect.mark_block_as_modified(idx); + } + + old_block + } + + fn fill_block_states(&mut self, block: BlockState) { + for sect in self.sections.iter_mut() { + // Mark the appropriate blocks as modified. + // No need to iterate through all the blocks if we know they're all the same. + if let PalettedContainer::Single(single) = §.block_states { + if block != *single { + sect.mark_all_blocks_as_modified(); + } + } else { + for i in 0..SECTION_BLOCK_COUNT { + if block != sect.block_states.get(i) { + sect.mark_block_as_modified(i); + } + } + } + + // TODO: adjust motion blocking here. + + if block.is_air() { + sect.non_air_count = 0; + } else { + sect.non_air_count = SECTION_BLOCK_COUNT as u16; + } + + sect.block_states.fill(block); } } - fn get_biome(&self, x: usize, y: usize, z: usize) -> BiomeId { + fn biome(&self, x: usize, y: usize, z: usize) -> BiomeId { assert!( x < 4 && y < self.height() / 4 && z < 4, - "chunk biome offsets must be within bounds" + "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" ); - self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] + self.sections[y / 4].biomes.get(x + z * 4 + y % 4 * 4 * 4) } - fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) { + fn set_biome(&mut self, x: usize, y: usize, z: usize, biome: BiomeId) -> BiomeId { assert!( x < 4 && y < self.height() / 4 && z < 4, - "chunk biome offsets must be within bounds" + "chunk biome offsets of ({x}, {y}, {z}) are out of bounds" ); - self.sections[y / 4].biomes[x + z * 4 + y % 4 * 4 * 4] = biome; + self.sections[y / 4] + .biomes + .set(x + z * 4 + y % 4 * 4 * 4, biome) } -} -#[derive(Clone, Debug)] -pub(crate) enum BlockChangePacket { - Single(BlockUpdate), - Multi(UpdateSectionBlocks), -} + fn fill_biomes(&mut self, biome: BiomeId) { + for sect in self.sections.iter_mut() { + sect.biomes.fill(biome); + } + } -impl From for S2cPlayPacket { - fn from(p: BlockChangePacket) -> Self { - match p { - BlockChangePacket::Single(p) => p.into(), - BlockChangePacket::Multi(p) => p.into(), + fn optimize(&mut self) { + for sect in self.sections.iter_mut() { + sect.block_states.optimize(); + sect.biomes.optimize(); } } } -/// Builds the MOTION_BLOCKING heightmap. -fn build_heightmap(sections: &[ChunkSection], heightmap: &mut Vec) { - let height = sections.len() * 16; - let bits_per_val = log2_ceil(height); +/* +fn is_motion_blocking(b: BlockState) -> bool { + // TODO: use is_solid || is_fluid ? + !b.is_air() +} +*/ + +fn compact_u64s_len(vals_count: usize, bits_per_val: usize) -> usize { let vals_per_u64 = 64 / bits_per_val; - let u64_count = Integer::div_ceil(&256, &vals_per_u64); - - heightmap.clear(); - heightmap.resize(u64_count, 0); - - for x in 0..16 { - for z in 0..16 { - for y in (0..height).rev() { - let block = BlockState::from_raw_unchecked( - sections[y / 16].blocks[x + z * 16 + y % 16 * 16 * 16] & BLOCK_STATE_MASK, - ); - - // TODO: is_solid || is_fluid heuristic for motion blocking. - if !block.is_air() { - let column_height = y as u64; - - let i = x * 16 + z; // TODO: X or Z major? - heightmap[i / vals_per_u64] |= - (column_height << (i % vals_per_u64 * bits_per_val)) as i64; - - break; - } - } - } - } + num::Integer::div_ceil(&vals_count, &vals_per_u64) } -fn encode_paletted_container( - mut entries: impl ExactSizeIterator + Clone, - min_bits_per_idx: usize, - direct_threshold: usize, - direct_bits_per_idx: usize, +#[inline] +fn encode_compact_u64s( w: &mut impl Write, + mut vals: impl Iterator, + bits_per_val: usize, ) -> anyhow::Result<()> { - let mut palette = Vec::new(); + debug_assert!(bits_per_val <= 64); - for entry in entries.clone() { - if !palette.contains(&entry) { - palette.push(entry); + let vals_per_u64 = 64 / bits_per_val; + + loop { + let mut n = 0; + for i in 0..vals_per_u64 { + match vals.next() { + Some(val) => { + debug_assert!(val < 2_u128.pow(bits_per_val as _) as _); + n |= val << (i * bits_per_val); + } + None if i > 0 => return n.encode(w), + None => return Ok(()), + } + } + n.encode(w)?; + } +} + +#[cfg(test)] +mod tests { + use rand::prelude::*; + + use super::*; + use crate::config::MockConfig; + + fn check_invariants(sections: &[ChunkSection]) { + for sect in sections { + assert_eq!( + sect.modified_blocks + .iter() + .map(|bits| bits.count_ones() as u16) + .sum::(), + sect.modified_blocks_count, + "number of modified blocks does not match counter" + ); + + assert_eq!( + (0..SECTION_BLOCK_COUNT) + .filter(|&i| !sect.block_states.get(i).is_air()) + .count(), + sect.non_air_count as usize, + "number of non-air blocks does not match counter" + ); } } - let bits_per_idx = log2_ceil(palette.len()); - - (bits_per_idx as u8).encode(w)?; - - if bits_per_idx == 0 { - // Single value case - debug_assert_eq!(palette.len(), 1); - VarInt(palette[0] as i32).encode(w)?; - VarInt(0).encode(w)?; // data array length - } else if bits_per_idx >= direct_threshold { - // Direct case - // Skip the palette - let idxs_per_u64 = 64 / direct_bits_per_idx; - let u64_count = Integer::div_ceil(&entries.len(), &idxs_per_u64); - - VarInt(u64_count as i32).encode(w)?; - - for _ in 0..idxs_per_u64 { - let mut val = 0u64; - for i in 0..idxs_per_u64 { - if let Some(entry) = entries.next() { - val |= (entry as u64) << (i * direct_bits_per_idx); - } - } - val.encode(w)?; - } - } else { - // Indirect case - VarInt(palette.len() as i32).encode(w)?; - for &val in &palette { - VarInt(val as i32).encode(w)?; - } - - let bits_per_idx = bits_per_idx.max(min_bits_per_idx); - let idxs_per_u64 = 64 / bits_per_idx; - let u64_count = Integer::div_ceil(&entries.len(), &idxs_per_u64); - - VarInt(u64_count as i32).encode(w)?; - - for _ in 0..u64_count { - let mut val = 0u64; - for i in 0..idxs_per_u64 { - if let Some(entry) = entries.next() { - let palette_idx = palette - .iter() - .position(|&e| e == entry) - .expect("entry should be in the palette") - as u64; - - val |= palette_idx << (i * bits_per_idx); - } - } - val.encode(w)?; - } + fn rand_block_state(rng: &mut (impl Rng + ?Sized)) -> BlockState { + BlockState::from_raw(rng.gen_range(0..=BlockState::max_raw())).unwrap() } - Ok(()) -} + #[test] + fn random_block_assignments() { + let mut rng = thread_rng(); -/// Calculates the log base 2 rounded up. -fn log2_ceil(n: usize) -> usize { - debug_assert_ne!(n, 0); - n.next_power_of_two().trailing_zeros() as usize + let height = 512; + + let mut loaded = LoadedChunk::::new(UnloadedChunk::default(), height / 16, ()); + let mut unloaded = UnloadedChunk::new(height); + + for i in 0..10_000 { + let state = if i % 250 == 0 { + [BlockState::AIR, BlockState::CAVE_AIR, BlockState::VOID_AIR] + .into_iter() + .choose(&mut rng) + .unwrap() + } else { + rand_block_state(&mut rng) + }; + + let x = rng.gen_range(0..16); + let y = rng.gen_range(0..height); + let z = rng.gen_range(0..16); + + loaded.set_block_state(x, y, z, state); + unloaded.set_block_state(x, y, z, state); + } + + check_invariants(&loaded.sections); + check_invariants(&unloaded.sections); + + loaded.optimize(); + unloaded.optimize(); + + check_invariants(&loaded.sections); + check_invariants(&unloaded.sections); + + loaded.fill_block_states(rand_block_state(&mut rng)); + unloaded.fill_block_states(rand_block_state(&mut rng)); + + check_invariants(&loaded.sections); + check_invariants(&unloaded.sections); + } } diff --git a/src/chunk/paletted_container.rs b/src/chunk/paletted_container.rs new file mode 100644 index 0000000..ba51607 --- /dev/null +++ b/src/chunk/paletted_container.rs @@ -0,0 +1,310 @@ +use std::array; +use std::io::Write; + +use arrayvec::ArrayVec; + +use crate::chunk::{compact_u64s_len, encode_compact_u64s}; +use crate::protocol::{Encode, VarInt}; +use crate::util::log2_ceil; + +/// `HALF_LEN` must be equal to `ceil(LEN / 2)`. +#[derive(Clone, Debug)] +pub enum PalettedContainer { + Single(T), + Indirect(Box>), + Direct(Box<[T; LEN]>), +} + +pub trait PalettedContainerElement: Copy + Eq + Default { + /// The minimum number of bits required to represent all instances of the + /// element type. If `N` is the total number of possible values, then + /// `DIRECT_BITS` is `ceil(log2(N))`. + const DIRECT_BITS: usize; + /// The maximum number of bits per element allowed in the indirect + /// representation while encoding. Any higher than this will force + /// conversion to the direct representation while encoding. + const MAX_INDIRECT_BITS: usize; + /// The minimum number of bits used to represent the element type in the + /// indirect representation while encoding. If the bits per index is lower, + /// it will be rounded up to this. + const MIN_INDIRECT_BITS: usize; + /// Converts the element type to bits. The output must be less than two to + /// the power of `DIRECT_BITS`. + fn to_bits(self) -> u64; +} + +#[derive(Clone, Debug)] +pub struct Indirect { + /// Each element is a unique instance of `T`. + palette: ArrayVec, + /// Each half-byte is an index into `palette`. + indices: [u8; HALF_LEN], +} + +impl + PalettedContainer +{ + pub fn new() -> Self { + assert_eq!(num::Integer::div_ceil(&LEN, &2), HALF_LEN); + assert_ne!(LEN, 0); + + Self::Single(T::default()) + } + + pub fn fill(&mut self, val: T) { + *self = Self::Single(val) + } + + pub fn get(&self, idx: usize) -> T { + self.check_oob(idx); + + match self { + Self::Single(elem) => *elem, + Self::Indirect(ind) => ind.get(idx), + Self::Direct(elems) => elems[idx], + } + } + + pub fn set(&mut self, idx: usize, val: T) -> T { + self.check_oob(idx); + + match self { + Self::Single(old_val) => { + if *old_val == val { + *old_val + } else { + // Upgrade to indirect. + let old = *old_val; + let mut ind = Box::new(Indirect { + palette: ArrayVec::from_iter([old, val]), + // All indices are initialized to index 0 (the old element). + indices: [0; HALF_LEN], + }); + + ind.indices[idx / 2] = 1 << (idx % 2 * 4); + *self = Self::Indirect(ind); + old + } + } + Self::Indirect(ind) => { + if let Some(old) = ind.set(idx, val) { + old + } else { + // Upgrade to direct. + *self = Self::Direct(Box::new(array::from_fn(|i| ind.get(i)))); + self.set(idx, val) + } + } + Self::Direct(vals) => { + let old = vals[idx]; + vals[idx] = val; + old + } + } + } + + pub fn optimize(&mut self) { + match self { + Self::Single(_) => {} + Self::Indirect(ind) => { + let mut new_ind = Indirect { + palette: ArrayVec::new(), + indices: [0; HALF_LEN], + }; + + for i in 0..LEN { + new_ind.set(i, ind.get(i)); + } + + if new_ind.palette.len() == 1 { + *self = Self::Single(new_ind.palette[0]); + } else { + **ind = new_ind; + } + } + Self::Direct(dir) => { + let mut ind = Indirect { + palette: ArrayVec::new(), + indices: [0; HALF_LEN], + }; + + for (i, val) in dir.iter().cloned().enumerate() { + if ind.set(i, val).is_none() { + return; + } + } + + *self = if ind.palette.len() == 1 { + Self::Single(ind.palette[0]) + } else { + Self::Indirect(Box::new(ind)) + }; + } + } + } + + #[inline] + fn check_oob(&self, idx: usize) { + assert!( + idx < LEN, + "index {idx} is out of bounds in paletted container of length {LEN}" + ); + } +} + +impl Default + for PalettedContainer +{ + fn default() -> Self { + Self::new() + } +} + +impl + Indirect +{ + pub fn get(&self, idx: usize) -> T { + let palette_idx = self.indices[idx / 2] >> (idx % 2 * 4) & 0b1111; + self.palette[palette_idx as usize] + } + + pub fn set(&mut self, idx: usize, val: T) -> Option { + let palette_idx = if let Some(i) = self.palette.iter().position(|v| *v == val) { + i + } else { + self.palette.try_push(val).ok()?; + self.palette.len() - 1 + }; + + let old_val = self.get(idx); + let u8 = &mut self.indices[idx / 2]; + let shift = idx % 2 * 4; + *u8 = (*u8 & !(0b1111 << shift)) | ((palette_idx as u8) << shift); + Some(old_val) + } +} + +/// Encodes the paletted container in the format that Minecraft expects. +impl Encode + for PalettedContainer +{ + fn encode(&self, w: &mut impl Write) -> anyhow::Result<()> { + assert!(T::DIRECT_BITS <= 64); + assert!(T::MAX_INDIRECT_BITS <= 64); + assert!(T::MIN_INDIRECT_BITS <= T::MAX_INDIRECT_BITS); + assert!(T::MIN_INDIRECT_BITS <= 4); + + match self { + Self::Single(val) => { + // Bits per entry + 0_u8.encode(w)?; + + // Palette + VarInt(val.to_bits() as i32).encode(w)?; + + // Number of longs + VarInt(0).encode(w)?; + } + Self::Indirect(ind) => { + let bits_per_entry = T::MIN_INDIRECT_BITS.max(log2_ceil(ind.palette.len())); + + // TODO: if bits_per_entry > MAX_INDIRECT_BITS, encode as direct. + debug_assert!(bits_per_entry <= T::MAX_INDIRECT_BITS); + + // Bits per entry + (bits_per_entry as u8).encode(w)?; + + // Palette len + VarInt(ind.palette.len() as i32).encode(w)?; + // Palette + for val in &ind.palette { + VarInt(val.to_bits() as i32).encode(w)?; + } + + // Number of longs in data array. + VarInt(compact_u64s_len(LEN, bits_per_entry) as _).encode(w)?; + // Data array + encode_compact_u64s( + w, + ind.indices + .iter() + .cloned() + .flat_map(|byte| [byte & 0b1111, byte >> 4]) + .map(u64::from) + .take(LEN), + bits_per_entry, + )?; + } + Self::Direct(dir) => { + // Bits per entry + (T::DIRECT_BITS as u8).encode(w)?; + + // Number of longs in data array. + VarInt(compact_u64s_len(LEN, T::DIRECT_BITS) as _).encode(w)?; + // Data array + encode_compact_u64s(w, dir.iter().map(|v| v.to_bits()), T::DIRECT_BITS)?; + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use rand::Rng; + + use super::*; + + fn check( + p: &PalettedContainer, + s: &[T], + ) -> bool { + assert_eq!(s.len(), LEN); + (0..LEN).all(|i| p.get(i) == s[i]) + } + + impl PalettedContainerElement for u32 { + const DIRECT_BITS: usize = 0; + const MAX_INDIRECT_BITS: usize = 0; + const MIN_INDIRECT_BITS: usize = 0; + + fn to_bits(self) -> u64 { + self.into() + } + } + + #[test] + fn random_assignments() { + const LEN: usize = 100; + let range = 0..64; + + let mut rng = rand::thread_rng(); + + for _ in 0..20 { + let mut p = PalettedContainer::::new(); + + let init = rng.gen_range(range.clone()); + + p.fill(init); + let mut a = [init; LEN]; + + assert!(check(&p, &a)); + + let mut rng = rand::thread_rng(); + + for _ in 0..LEN * 10 { + let idx = rng.gen_range(0..LEN); + let val = rng.gen_range(range.clone()); + + assert_eq!(p.get(idx), p.set(idx, val)); + assert_eq!(val, p.get(idx)); + a[idx] = val; + + p.optimize(); + + assert!(check(&p, &a)); + } + } + } +} diff --git a/src/client.rs b/src/client.rs index 90aacdc..cbd4308 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1252,7 +1252,6 @@ impl Client { if let Some(chunk) = world.chunks.get(pos) { if self.loaded_chunks.insert(pos) { self.send_packet(chunk.chunk_data_packet(pos)); - chunk.block_change_packets(pos, dimension.min_y, |pkt| self.send_packet(pkt)); } } } diff --git a/src/config.rs b/src/config.rs index 15dffc1..44e20a0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,7 +2,6 @@ use std::borrow::Cow; use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; -use std::panic::{RefUnwindSafe, UnwindSafe}; use async_trait::async_trait; use serde::Serialize; @@ -24,7 +23,7 @@ use crate::{Ticks, STANDARD_TPS}; /// [async_trait]: https://docs.rs/async-trait/latest/async_trait/ #[async_trait] #[allow(unused_variables)] -pub trait Config: Sized + Send + Sync + UnwindSafe + RefUnwindSafe + 'static { +pub trait Config: Sized + Send + Sync + 'static { /// Custom state to store with the [`Server`]. type ServerState: Send + Sync; /// Custom state to store with every [`Client`](crate::client::Client). @@ -306,3 +305,33 @@ pub struct PlayerSampleEntry<'a> { /// The player UUID. pub id: Uuid, } + +/// A minimal `Config` implementation for testing purposes. +#[cfg(test)] +pub(crate) struct MockConfig { + _marker: std::marker::PhantomData<(S, Cl, E, W, Ch, P)>, +} + +#[cfg(test)] +impl Config for MockConfig +where + S: Send + Sync + 'static, + Cl: Default + Send + Sync + 'static, + E: Send + Sync + 'static, + W: Send + Sync + 'static, + Ch: Send + Sync + 'static, + P: Send + Sync + 'static, +{ + type ServerState = S; + type ClientState = Cl; + type EntityState = E; + type WorldState = W; + type ChunkState = Ch; + type PlayerListState = P; + + fn max_connections(&self) -> usize { + 64 + } + + fn update(&self, _server: &mut Server) {} +} diff --git a/src/entity.rs b/src/entity.rs index e45f72e..402753f 100644 --- a/src/entity.rs +++ b/src/entity.rs @@ -821,25 +821,9 @@ mod tests { use uuid::Uuid; use super::{Entities, EntityId, EntityKind}; - use crate::config::Config; - use crate::server::Server; use crate::slab_versioned::Key; - /// Created for the sole purpose of use during unit tests. - struct MockConfig; - impl Config for MockConfig { - type ServerState = (); - type ClientState = (); - type EntityState = u8; // Just for identification purposes - type WorldState = (); - type ChunkState = (); - type PlayerListState = (); - - fn max_connections(&self) -> usize { - 10 - } - fn update(&self, _server: &mut Server) {} - } + type MockConfig = crate::config::MockConfig<(), (), u8>; #[test] fn entities_has_valid_new_state() { diff --git a/src/server.rs b/src/server.rs index 7139db3..9967ddb 100644 --- a/src/server.rs +++ b/src/server.rs @@ -440,12 +440,6 @@ fn do_update_loop(server: &mut Server) -> ShutdownResult { shared.config().update(server); server.worlds.par_iter_mut().for_each(|(id, world)| { - // Chunks created this tick can have their changes applied immediately because - // they have not been observed by clients yet. Clients will not have to be sent - // the block change packet in this case, since the changes are applied before we - // update clients. - world.chunks.update_created_this_tick(); - world.spatial_index.update(&server.entities, id); }); diff --git a/src/util.rs b/src/util.rs index acc7922..cf89093 100644 --- a/src/util.rs +++ b/src/util.rs @@ -127,6 +127,17 @@ pub fn ray_box_intersect(ro: Vec3, rd: Vec3, bb: Aabb) -> Option< } } +/// Calculates the log base 2 rounded up. +pub(crate) const fn log2_ceil(n: usize) -> usize { + debug_assert!(n != 0); + + // TODO: replace with `n.wrapping_next_power_of_two().trailing_zeros()`. + match n.checked_next_power_of_two() { + Some(n) => n.trailing_zeros() as usize, + None => 0_u64.trailing_zeros() as usize, + } +} + #[cfg(test)] mod tests { use approx::assert_relative_eq; diff --git a/src/world.rs b/src/world.rs index 1f1f3bc..0a92ee8 100644 --- a/src/world.rs +++ b/src/world.rs @@ -44,12 +44,18 @@ impl Worlds { /// Creates a new world on the server with the provided dimension. A /// reference to the world along with its ID is returned. - pub fn insert(&mut self, dim: DimensionId, state: C::WorldState) -> (WorldId, &mut World) { + pub fn insert( + &mut self, + dimension: DimensionId, + state: C::WorldState, + ) -> (WorldId, &mut World) { + let dim = self.shared.dimension(dimension); + let (id, world) = self.slab.insert(World { state, spatial_index: SpatialIndex::new(), - chunks: Chunks::new(self.shared.clone(), dim), - meta: WorldMeta { dimension: dim }, + chunks: Chunks::new(dim.height, dim.min_y), + meta: WorldMeta { dimension }, }); (WorldId(id), world)