synvael/crates/shared/src/world/chunk.rs

158 lines
6 KiB
Rust

// SPDX-License-Identifier: AGPL-3.0-only
//! Dense and palette-compressed chunk storage forms.
use super::{BlockId, CHUNK_SIZE, CHUNK_VOLUME};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, hash_map::Entry};
/// A spatial volume containing voxel data.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Chunk {
/// The array of blocks contained within the chunk.
pub blocks: Box<[BlockId]>,
}
impl Chunk {
/// Calculates the one-dimensional index for a three-dimensional block coordinate.
#[inline]
#[must_use]
pub const fn index(x: usize, y: usize, z: usize) -> usize {
(x * CHUNK_SIZE * CHUNK_SIZE) + (z * CHUNK_SIZE) + y
}
/// Retrieves the block identifier at the specified coordinates.
#[must_use]
pub fn get(&self, x: usize, y: usize, z: usize) -> BlockId {
self.blocks[Self::index(x, y, z)]
}
/// Sets the block identifier at the specified coordinates.
pub fn set(&mut self, x: usize, y: usize, z: usize, block: BlockId) {
self.blocks[Self::index(x, y, z)] = block;
}
}
impl Default for Chunk {
fn default() -> Self {
Self {
blocks: vec![BlockId::AIR; CHUNK_VOLUME].into_boxed_slice(),
}
}
}
/// The palette-compressed representation of a chunk's voxel data, used as the stored and transmitted form.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PalettedChunk {
/// The distinct materials present in the chunk. A stored voxel index refers to a position in this table.
palette: Vec<BlockId>,
/// The palette indices for all `CHUNK_VOLUME` voxels, packed `bits_per_index` bits each, low voxel first, into 64-bit words.
indices: Vec<u64>,
/// The width in bits of a single packed palette index, equal to `ceil(log2(palette.len()))` with a floor of one.
bits_per_index: u32,
}
impl PalettedChunk {
/// Encodes a dense [`Chunk`] into its palette-compressed form.
#[must_use]
pub fn from_chunk(chunk: &Chunk) -> Self {
// Assign a palette slot to each distinct material in first-encounter order.
let mut lookup: HashMap<BlockId, usize> = HashMap::new();
let mut palette: Vec<BlockId> = Vec::new();
for &block in &*chunk.blocks {
if let Entry::Vacant(entry) = lookup.entry(block) {
entry.insert(palette.len());
palette.push(block);
}
}
let bits_per_index = Self::bits_for_palette(palette.len());
let mut indices = vec![0u64; Self::packed_word_count(bits_per_index)];
for (voxel, &block) in chunk.blocks.iter().enumerate() {
// Every block was inserted into `lookup` above, so this cannot miss.
let index = lookup[&block] as u64;
Self::write_packed(&mut indices, voxel, bits_per_index, index);
}
Self {
palette,
indices,
bits_per_index,
}
}
/// Decodes the palette-compressed form back into a dense [`Chunk`].
#[must_use]
pub fn to_chunk(&self) -> Chunk {
let mut blocks = vec![BlockId::AIR; CHUNK_VOLUME].into_boxed_slice();
for (voxel, slot) in blocks.iter_mut().enumerate() {
// A stored index was produced from a palette position, so it is always in range for `palette`.
#[expect(
clippy::cast_possible_truncation,
reason = "a packed index originates from a valid palette position and fits usize"
)]
let index = Self::read_packed(&self.indices, voxel, self.bits_per_index) as usize;
*slot = self.palette[index];
}
Chunk { blocks }
}
/// Returns the number of bits required to store a palette index for a palette of `len` entries.
fn bits_for_palette(len: usize) -> u32 {
if len <= 1 {
1
} else {
// Bit width of the largest index `len - 1`.
usize::BITS - (len - 1).leading_zeros()
}
}
/// Returns the number of 64-bit words needed to pack every voxel index at `bits_per_index` bits.
fn packed_word_count(bits_per_index: u32) -> usize {
let total_bits = CHUNK_VOLUME * bits_per_index as usize;
total_bits.div_ceil(u64::BITS as usize)
}
/// Writes `value` as the `voxel`-th index of width `bits_per_index` into the packed `words` buffer.
fn write_packed(words: &mut [u64], voxel: usize, bits_per_index: u32, value: u64) {
let bit_offset = voxel * bits_per_index as usize;
let word = bit_offset / 64;
let bit_in_word = bit_offset % 64;
// Low part: the bits that fit in the current word at `bit_in_word` and above. Bits shifted past bit 63 are dropped and handled by the spill below.
words[word] |= value << bit_in_word;
// High part: only present when the index straddles the word boundary.
let bits_in_first = 64 - bit_in_word;
if bits_per_index as usize > bits_in_first {
words[word + 1] |= value >> bits_in_first;
}
}
/// Reads the `voxel`-th index of width `bits_per_index` from the packed `words` buffer, reassembling any value that straddles a 64-bit word boundary.
fn read_packed(words: &[u64], voxel: usize, bits_per_index: u32) -> u64 {
let bit_offset = voxel * bits_per_index as usize;
let word = bit_offset / 64;
let bit_in_word = bit_offset % 64;
// `bits_per_index` never reaches 64 (a chunk holds at most `CHUNK_VOLUME` distinct materials, so at most 15 bits), so this shift cannot overflow.
let mask = (1u64 << bits_per_index) - 1;
let bits_in_first = 64 - bit_in_word;
let low = words[word] >> bit_in_word;
if bits_per_index as usize <= bits_in_first {
low & mask
} else {
// Reassemble a straddling index: low bits from the current word, high bits from the next.
let high = words[word + 1] << bits_in_first;
(low | high) & mask
}
}
}
#[cfg(test)]
#[path = "../tests/chunk.rs"]
mod tests;