alex/beta-minecraft-rust
Minecraft Beta 1.7.3 Rewrite
BRANCH main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
root crates common src world chunk.rs
use std::cmp::Reverse;
use std::ops::Range;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::mpsc::{self, Receiver, TryRecvError};
use std::sync::{Arc, LazyLock, Mutex, PoisonError};
use palettevec::PaletteVec;
use palettevec::index_buffer::AlignedIndexBuffer;
use palettevec::palette::vec::VecPalette;
use rustc_hash::FxHashMap;
use super::{Block, TileEntity};
pub const WIDTH: usize = 16;
pub const HEIGHT: usize = 128;
pub const VOLUME: usize = WIDTH * WIDTH * HEIGHT;
/// Highest value of the height map, which never reaches [`HEIGHT`].
const MAX_HEIGHT: usize = HEIGHT - 1;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct ChunkPos {
pub x: i32,
pub z: i32,
}
impl ChunkPos {
pub const fn new(x: i32, z: i32) -> Self {
Self { x, z }
}
pub const fn containing(block_x: i32, block_z: i32) -> Self {
Self::new(block_x >> 4, block_z >> 4)
}
pub const fn offset(self, dx: i32, dz: i32) -> Self {
Self::new(self.x + dx, self.z + dz)
}
pub const fn min_block_x(self) -> i32 {
self.x << 4
}
pub const fn min_block_z(self) -> i32 {
self.z << 4
}
}
/// Index into chunk arrays, using the original `x << 11 | z << 7 | y` layout.
pub const fn index(x: usize, y: usize, z: usize) -> usize {
x << 11 | z << 7 | y
}
const SECTIONS: usize = HEIGHT / 16;
const SECTION_VOLUME: usize = VOLUME / SECTIONS;
const SECTION_BYTES: usize = SECTION_VOLUME / 2;
/// The 16³ section of an [`index`] and the position within it, `x << 8 | z << 4 | y & 15`.
const fn split(index: usize) -> (usize, usize) {
(index >> 4 & (SECTIONS - 1), (index >> 3 & !15) | index & 15)
}
type NibbleSection = Arc<[u8; SECTION_BYTES]>;
/// A shared section for each value, for the sections that hold nothing else.
static UNIFORM: LazyLock<[NibbleSection; 16]> =
LazyLock::new(|| std::array::from_fn(|value| Arc::new([value as u8 * 0x11; SECTION_BYTES])));
fn nibble_section(bytes: [u8; SECTION_BYTES]) -> NibbleSection {
match uniform(&bytes) {
Some(value) => UNIFORM[usize::from(value)].clone(),
None => Arc::new(bytes),
}
}
/// The value of every nibble, if they are all the same.
fn uniform(bytes: &[u8; SECTION_BYTES]) -> Option<u8> {
let value = bytes[0] & 0xF;
bytes.iter().all(|&b| b == value * 0x11).then_some(value)
}
/// 4-bit values like `NibbleArray`, packed per 16³ section, low nibble first. Sections that hold a
/// single value share one array, and clones share all sections until one of them is written.
#[derive(Clone)]
pub struct Nibbles([NibbleSection; SECTIONS]);
impl Nibbles {
#[inline(always)]
pub fn get(&self, index: usize) -> u8 {
(self.byte(index >> 1) >> ((index & 1) << 2)) & 0xF
}
pub fn set(&mut self, index: usize, value: u8) {
let value = value & 0xF;
if self.get(index) == value {
return;
}
set_nibble(
Arc::make_mut(&mut self.0[index >> 4 & (SECTIONS - 1)]),
index,
value,
);
}
/// Byte `index / 2` of the original array.
#[inline(always)]
fn byte(&self, byte: usize) -> u8 {
self.0[byte >> 3 & (SECTIONS - 1)][local_byte(byte)]
}
/// Overwrites the original array's bytes from `start`.
fn write_bytes(&mut self, mut start: usize, mut data: &[u8]) {
// Runs of 8 bytes, half a column, stay within one section
while !data.is_empty() {
let len = (8 - (start & 7)).min(data.len());
let local = local_byte(start);
Arc::make_mut(&mut self.0[start >> 3 & (SECTIONS - 1)])[local..local + len]
.copy_from_slice(&data[..len]);
(start, data) = (start + len, &data[len..]);
}
}
/// The original array.
pub fn to_bytes(&self) -> Vec<u8> {
(0..VOLUME / 2).map(|byte| self.byte(byte)).collect()
}
/// Whether both share all sections.
pub fn ptr_eq(&self, other: &Self) -> bool {
self.0.iter().zip(&other.0).all(|(a, b)| Arc::ptr_eq(a, b))
}
/// Shares the sections that hold a single value again, of those given as bits of their
/// section y.
pub fn compact(&mut self, sections: u8) {
for (y, section) in self.0.iter_mut().enumerate() {
if sections >> y & 1 != 0
&& !Arc::ptr_eq(section, &UNIFORM[usize::from(section[0] & 0xF)])
&& let Some(value) = uniform(section)
{
*section = UNIFORM[usize::from(value)].clone();
}
}
}
/// Reads the original array, or `None` if the length is wrong.
pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
let bytes: &[u8; VOLUME / 2] = bytes.try_into().ok()?;
Some(Self(std::array::from_fn(|section| {
nibble_section(std::array::from_fn(|local| {
bytes[(local & !7) << 3 | section << 3 | local & 7]
}))
})))
}
}
/// Sets the nibble of an [`index`] within its section.
fn set_nibble(bytes: &mut [u8; SECTION_BYTES], index: usize, value: u8) {
let (byte, shift) = (&mut bytes[local_byte(index >> 1)], (index & 1) << 2);
*byte = (*byte & !(0xF << shift)) | (value << shift);
}
/// The byte within its section of byte `index / 2` of the original array.
const fn local_byte(byte: usize) -> usize {
(byte >> 3 & (SECTION_BYTES - 8)) | byte & 7
}
impl Default for Nibbles {
/// All zeros.
fn default() -> Self {
Self(std::array::from_fn(|_| UNIFORM[0].clone()))
}
}
type BlockSection = PaletteVec<Block, VecPalette<Block>, AlignedIndexBuffer>;
#[derive(Clone)]
enum BlockStorage {
Dense(Arc<[Block; VOLUME]>),
Packed(Arc<[BlockSection; SECTIONS]>),
}
/// Block ids, either in the original array or palette compressed per 16³ section, which takes a
/// fraction of the memory but is slower to access. Clones share them until one of them is written.
#[derive(Clone)]
pub struct Blocks(BlockStorage);
impl Blocks {
#[inline(always)]
pub fn get(&self, index: usize) -> Block {
match &self.0 {
BlockStorage::Dense(blocks) => blocks[index],
BlockStorage::Packed(sections) => packed_block(sections, index),
}
}
fn replace(&mut self, index: usize, block: Block) -> Block {
match &mut self.0 {
BlockStorage::Dense(blocks) => {
std::mem::replace(&mut Arc::make_mut(blocks)[index], block)
}
BlockStorage::Packed(sections) => {
let (section, local) = split(index);
let section = &mut Arc::make_mut(sections)[section];
let old = section.get(local).copied().unwrap_or_default();
section.set(local, &block);
old
}
}
}
/// The ids in the order of the original array.
pub fn ids(&self) -> impl Iterator<Item = u8> + '_ {
(0..VOLUME).map(|index| self.get(index).0)
}
/// The original array, shared unless the ids are packed.
pub fn dense(&self) -> Arc<[Block; VOLUME]> {
match &self.0 {
BlockStorage::Dense(blocks) => blocks.clone(),
BlockStorage::Packed(_) => self.unpacked(),
}
}
#[cold]
#[inline(never)]
fn unpacked(&self) -> Arc<[Block; VOLUME]> {
let mut blocks = Arc::new([Block::AIR; VOLUME]);
let dense = Arc::get_mut(&mut blocks).unwrap_or_else(|| unreachable!());
dense
.iter_mut()
.zip(self.ids())
.for_each(|(block, id)| *block = Block(id));
blocks
}
pub fn is_packed(&self) -> bool {
matches!(self.0, BlockStorage::Packed(_))
}
/// Whether both share the same ids.
pub fn ptr_eq(&self, other: &Self) -> bool {
match (&self.0, &other.0) {
(BlockStorage::Dense(a), BlockStorage::Dense(b)) => Arc::ptr_eq(a, b),
(BlockStorage::Packed(a), BlockStorage::Packed(b)) => Arc::ptr_eq(a, b),
_ => false,
}
}
/// A copy in the other form: packed if these are not, and the other way round.
pub fn converted(&self) -> Self {
let BlockStorage::Dense(blocks) = &self.0 else {
return Self(BlockStorage::Dense(self.unpacked()));
};
Self(BlockStorage::Packed(Arc::new(std::array::from_fn(
|section| {
let block = |local: usize| blocks[(local & !15) << 3 | section << 4 | local & 15];
let first = block(0);
if (1..SECTION_VOLUME).all(|local| block(local) == first) {
return BlockSection::filled(first, SECTION_VOLUME);
}
let mut packed = BlockSection::new();
(0..SECTION_VOLUME).for_each(|local| packed.push(block(local)));
packed
},
))))
}
}
/// Kept out of line, so reading unpacked ids stays as cheap as indexing an array.
#[inline(never)]
fn packed_block(sections: &[BlockSection; SECTIONS], index: usize) -> Block {
let (section, local) = split(index);
sections[section].get(local).copied().unwrap_or_default()
}
impl From<Box<[Block; VOLUME]>> for Blocks {
fn from(blocks: Box<[Block; VOLUME]>) -> Self {
Self(BlockStorage::Dense(blocks.into()))
}
}
/// Highest share of chunks whose block ids stay unpacked for being accessed the most. Measured
/// CPU time keeps falling up to this cap (by about a quarter against packing every settled
/// chunk), while the extra memory stays a few MB.
pub const HOT_SHARE: f64 = 0.25;
/// Ticks between rankings by heat, which also halve it.
const REPACK_PERIOD: u32 = 20;
/// Heat of a block write, which costs packed ids about as much as four reads.
const WRITE_HEAT: u32 = 4;
/// Recent accesses of a chunk's block ids, weighted by what each would cost on packed ids, in
/// block reads. On its own cache line, so counting never slows readers of the chunk on other
/// threads.
#[derive(Default)]
#[repr(align(64))]
pub struct Heat(AtomicU32);
impl Heat {
#[inline(always)]
pub fn add(&self, reads: u32) {
// Not a read-modify-write: it is counted on the owner's thread, and a rare lost update
// from another one does not matter
let heat = self.0.load(Ordering::Relaxed);
self.0.store(heat.saturating_add(reads), Ordering::Relaxed);
}
pub fn get(&self) -> u32 {
self.0.load(Ordering::Relaxed)
}
/// Halves the heat, so it follows recent accesses.
fn cool(&self) {
self.0.store(self.get() / 2, Ordering::Relaxed);
}
}
impl Clone for Heat {
fn clone(&self) -> Self {
Self(AtomicU32::new(self.get()))
}
}
/// Chunks with the block ids they were given and the ids to take instead.
type Repacked = Vec<(ChunkPos, Blocks, Blocks)>;
/// Keeps the block ids of the hottest chunks unpacked and packs those of the other settled ones,
/// converting them on a worker.
#[derive(Default)]
pub struct Packer {
running: Option<Mutex<Receiver<Repacked>>>,
countdown: u32,
}
impl Packer {
/// Call once per tick with the loaded chunks, each with whether it is settled, that is no
/// longer written in bulk. Returns finished conversions for [`Chunk::adopt_blocks`]. Every
/// [`REPACK_PERIOD`] ticks it ranks the chunks by heat, keeps the hottest [`HOT_SHARE`]
/// unpacked, and starts converting the settled chunks that are in the other form.
pub fn tick<'a>(
&mut self,
chunks: impl FnOnce() -> Vec<(ChunkPos, &'a Chunk, bool)>,
) -> Repacked {
self.countdown = self.countdown.saturating_sub(1);
if let Some(running) = &mut self.running {
match running
.get_mut()
.unwrap_or_else(PoisonError::into_inner)
.try_recv()
{
Ok(repacked) => {
self.running = None;
return repacked;
}
Err(TryRecvError::Empty) => return Vec::new(),
Err(TryRecvError::Disconnected) => self.running = None,
}
}
if self.countdown > 0 {
return Vec::new();
}
self.countdown = REPACK_PERIOD;
let mut chunks = chunks();
let hot = (chunks.len() as f64 * HOT_SHARE) as usize;
if hot > 0 {
chunks.select_nth_unstable_by_key(hot - 1, |(_, chunk, _)| Reverse(chunk.heat.get()));
}
let due: Vec<_> = (chunks.into_iter().enumerate())
.filter_map(|(rank, (pos, chunk, settled))| {
chunk.heat.cool();
let unpacked = rank < hot;
(settled && chunk.blocks.is_packed() == unpacked)
.then(|| (pos, chunk.blocks.clone()))
})
.collect();
if !due.is_empty() {
let (sender, receiver) = mpsc::sync_channel(1);
rayon::spawn(move || {
let repacked = due.into_iter().map(|(pos, blocks)| {
let converted = blocks.converted();
(pos, blocks, converted)
});
drop(sender.send(repacked.collect()));
});
self.running = Some(Mutex::new(receiver));
}
Vec::new()
}
}
/// Cheap to clone: the block and nibble arrays are shared until written.
#[derive(Clone)]
pub struct Chunk {
pub pos: ChunkPos,
blocks: Blocks,
pub metadata: Nibbles,
pub sky_light: Nibbles,
pub block_light: Nibbles,
height_map: [u8; WIDTH * WIDTH],
/// Keyed by [`index`].
pub tile_entities: FxHashMap<u16, TileEntity>,
pub populated: bool,
/// False in the Nether (`WorldProvider.hasNoSky`): no sky light at all.
pub has_sky: bool,
pub heat: Heat,
}
impl std::fmt::Debug for Chunk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Chunk")
.field("pos", &self.pos)
.finish_non_exhaustive()
}
}
impl Chunk {
pub fn new(pos: ChunkPos, blocks: Box<[Block; VOLUME]>) -> Self {
Self {
pos,
blocks: Blocks::from(blocks),
metadata: Nibbles::default(),
sky_light: Nibbles::default(),
block_light: Nibbles::default(),
height_map: [0; WIDTH * WIDTH],
tile_entities: FxHashMap::default(),
populated: false,
has_sky: true,
heat: Heat::default(),
}
}
/// Drops the sky light, for dimensions without a sky.
pub fn without_sky(mut self) -> Self {
self.has_sky = false;
self.sky_light = Nibbles::default();
self
}
/// A chunk read from a save. Without a valid height map and sky light both are regenerated.
pub fn from_saved(
pos: ChunkPos,
blocks: Box<[Block; VOLUME]>,
metadata: Option<Nibbles>,
sky_light: Option<Nibbles>,
block_light: Option<Nibbles>,
height_map: Option<[u8; WIDTH * WIDTH]>,
) -> Self {
let mut chunk = Self::new(pos, blocks);
chunk.metadata = metadata.unwrap_or_default();
chunk.block_light = block_light.unwrap_or_default();
match (sky_light, height_map) {
(Some(sky_light), Some(height_map)) => {
chunk.sky_light = sky_light;
chunk.height_map = height_map;
}
_ => chunk.generate_sky_light(),
}
chunk
}
/// The raw height map bytes, indexed `z << 4 | x`.
pub fn height_map(&self) -> &[u8; WIDTH * WIDTH] {
&self.height_map
}
pub fn blocks(&self) -> &Blocks {
&self.blocks
}
/// Takes `converted` as the block ids if they are still those it was converted from.
pub fn adopt_blocks(&mut self, from: &Blocks, converted: Blocks) {
if self.blocks.ptr_eq(from) {
self.blocks = converted;
}
}
#[inline]
pub fn block(&self, x: usize, y: usize, z: usize) -> Block {
self.heat.add(1);
self.blocks.get(index(x, y, z))
}
/// Lowest y whose column above is fully transparent to light, capped at 127 like the original.
pub fn height(&self, x: usize, z: usize) -> usize {
usize::from(self.height_map[z << 4 | x])
}
/// `Chunk.canBlockSeeTheSky`.
pub fn sees_sky(&self, x: usize, y: usize, z: usize) -> bool {
y >= self.height(x, z)
}
fn scan_height(&self, x: usize, z: usize, from: usize) -> usize {
let column = index(x, 0, z);
let mut height = from;
while height > 0 && self.blocks.get(column + height - 1).light_opacity() == 0 {
height -= 1;
}
height
}
/// Port of `Chunk.generateSkylightMap` without the neighbor checks, which the
/// light engine performs instead.
pub fn generate_sky_light(&mut self) {
// `ChunkProviderHell` never generates it, so Nether height maps stay zero
if !self.has_sky {
return;
}
self.generate_height_map();
// Section by section from the top, each column carrying its light down
let mut lights = [15_i32; WIDTH * WIDTH];
for section in (0..SECTIONS).rev() {
if lights.iter().all(|&light| light <= 0) {
break;
}
let bytes = Arc::make_mut(&mut self.sky_light.0[section]);
for (column, light) in columns(0..WIDTH, 0, 0..WIDTH).zip(&mut lights) {
for y in (section * 16..section * 16 + 16).rev().filter(|&y| y > 0) {
if *light <= 0 {
break;
}
*light -= i32::from(self.blocks.get(column + y).light_opacity());
if *light > 0 {
set_nibble(bytes, column + y, *light as u8);
}
}
}
}
self.sky_light.compact(u8::MAX);
}
fn generate_height_map(&mut self) {
for x in 0..WIDTH {
for z in 0..WIDTH {
self.height_map[z << 4 | x] = self.scan_height(x, z, MAX_HEIGHT) as u8;
}
}
}
/// Writes a block id and returns the previous one, the first step of `Chunk.setBlockID`.
/// Follow with [`Self::finish_block_change`] once the old block has been notified.
pub fn swap_block(&mut self, x: usize, y: usize, z: usize, block: Block) -> Block {
self.heat.add(WRITE_HEAT);
self.blocks.replace(index(x, y, z), block)
}
/// Second step of `Chunk.setBlockID`: stores the metadata (`None` for `setBlockID`, which
/// clears it) and updates the height map and sky light of the column. Returns the y range
/// whose sky light was rewritten.
pub fn finish_block_change(
&mut self,
x: usize,
y: usize,
z: usize,
metadata: Option<u8>,
) -> Option<Range<usize>> {
self.metadata.set(index(x, y, z), metadata.unwrap_or(0));
// Only `setBlockIDWithMetadata` skips the column without a sky
if !self.has_sky && metadata.is_some() {
return None;
}
let height = self.height(x, z);
if self.blocks.get(index(x, y, z)).light_opacity() != 0 {
(y >= height).then(|| self.relight_column(x, y + 1, z))?
} else if y + 1 == height {
self.relight_column(x, y, z)
} else {
None
}
}
/// Port of `Chunk.relightBlock`.
fn relight_column(&mut self, x: usize, y: usize, z: usize) -> Option<Range<usize>> {
let old = self.height(x, z);
let mut height = self.scan_height(x, z, y.max(old));
if height == old {
return None;
}
self.height_map[z << 4 | x] = height as u8;
let column = index(x, 0, z);
let lit = if height < old { 15 } else { 0 };
for y in old.min(height)..old.max(height) {
self.sky_light.set(column + y, lit);
}
let top = height.max(old);
let mut light = 15_i32;
while height > 0 && light > 0 {
height -= 1;
light =
(light - i32::from(self.blocks.get(column + height).light_opacity().max(1))).max(0);
self.sky_light.set(column + height, light as u8);
}
// A raised column was darkened from its old height up, which needs settling too
Some(height.min(old)..top)
}
/// `Chunk.getChunkData`: appends the ids, metadata, block light and sky light of a box,
/// column by column. `y` must start and end on even heights.
pub fn write_region(
&self,
out: &mut Vec<u8>,
xs: Range<usize>,
ys: Range<usize>,
zs: Range<usize>,
) {
let columns = columns(xs, ys.start, zs);
for start in columns.clone() {
out.extend((start..start + ys.len()).map(|index| self.blocks.get(index).0));
}
for nibbles in [&self.metadata, &self.block_light, &self.sky_light] {
for start in columns.clone() {
out.extend((start / 2..(start + ys.len()) / 2).map(|byte| nibbles.byte(byte)));
}
}
}
/// `Chunk.setChunkData`: reads a box written by [`Self::write_region`] and rebuilds the
/// height map. Returns the number of bytes consumed.
pub fn read_region(
&mut self,
data: &[u8],
xs: Range<usize>,
ys: Range<usize>,
zs: Range<usize>,
) -> usize {
let whole = xs.len() == WIDTH && ys.len() == HEIGHT && zs.len() == WIDTH;
let columns = columns(xs, ys.start, zs);
let mut offset = 0;
// A whole chunk arrives in the order of the original array
if let Some(ids) = data.first_chunk::<VOLUME>().filter(|_| whole) {
self.blocks = Blocks::from(Box::new(ids.map(Block)));
offset = VOLUME;
} else {
self.heat
.add((columns.clone().count() * ys.len()) as u32 * WRITE_HEAT);
for start in columns.clone() {
for (index, &id) in (start..start + ys.len()).zip(&data[offset..]) {
self.blocks.replace(index, Block(id));
}
offset += ys.len();
}
}
for nibbles in [
&mut self.metadata,
&mut self.block_light,
&mut self.sky_light,
] {
for start in columns.clone() {
let len = ys.len() / 2;
nibbles.write_bytes(start / 2, &data[offset..offset + len]);
offset += len;
}
nibbles.compact(u8::MAX);
}
self.generate_height_map();
// Like the original renderer, tile entities only exist for their blocks
let blocks = &self.blocks;
self.tile_entities
.retain(|&index, _| blocks.get(usize::from(index)).is_container());
offset
}
pub fn tile_entity(&self, x: usize, y: usize, z: usize) -> Option<&TileEntity> {
self.tile_entities.get(&(index(x, y, z) as u16))
}
pub fn tile_entity_mut(&mut self, x: usize, y: usize, z: usize) -> Option<&mut TileEntity> {
self.tile_entities.get_mut(&(index(x, y, z) as u16))
}
}
/// The [`index`] of `y` in each column of a box, x-major.
fn columns(xs: Range<usize>, y: usize, zs: Range<usize>) -> impl Iterator<Item = usize> + Clone {
xs.flat_map(move |x| zs.clone().map(move |z| index(x, y, z)))
}
fn zeroed_box<T: Copy + Default, const N: usize>() -> Box<[T; N]> {
vec![T::default(); N]
.into_boxed_slice()
.try_into()
.unwrap_or_else(|_| unreachable!())
}
pub fn empty_blocks() -> Box<[Block; VOLUME]> {
zeroed_box()
}