WIP: paletted containers and mesher changes

Work in progress before merging main.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ludovic Marie
2026-10-10 03:48:16 +02:00
parent 88efc73ac1
commit a0e9605390
15 changed files with 1473 additions and 178 deletions
@@ -0,0 +1,196 @@
//! Resident chunk storage benchmark: generates a block of vanilla terrain, then
//! reports the resident block/biome memory, the cost of lookups, meshing and
//! lighting on it.
//!
//! chunk_bench <client resource dir> [--seed S] [--size N]
#![forbid(unsafe_code)]
use horizon_client::{
assets::Assets,
biome::Biomes,
blocks::BlockFacts,
chunk::{Chunk, MIN_Y, SECTIONS},
light::{LightTables, light_chunk},
mesher::{MeshTables, Mesher},
models,
streaming::{ChunkGenerator, Proto},
vanillagen::VanillaTerrain,
};
use horizon_core::biome::BiomeZoom;
use std::{path::PathBuf, sync::Arc, time::Instant};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn main() -> Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let root = PathBuf::from(args.first().ok_or("usage: chunk_bench RESOURCES")?);
let flag = |name: &str, default: i64| -> i64 {
args.iter()
.position(|a| a == name)
.and_then(|i| args.get(i + 1))
.and_then(|v| v.parse().ok())
.unwrap_or(default)
};
let (seed, size) = (flag("--seed", 12345), flag("--size", 8) as i32);
let assets = Assets::load(&root)?;
let facts = BlockFacts::load(&root, assets.registry)?;
let biomes = Biomes::load(&root)?;
let tables = MeshTables::new(&assets, &facts, models::load_visibility(&root, assets.registry)?)?;
let light = LightTables::load(&root, assets.registry, &facts)?;
let terrain = VanillaTerrain::new(&root.join("worldgen"), assets.registry, &biomes, seed)?;
let mut worker = terrain.worker();
// Protos for size+4 columns, decorated for the inner size+2, finished as size+2.
let started = Instant::now();
let outer = size + 4;
let mut protos: Vec<Option<Proto>> = (0..outer * outer)
.map(|i| Some(terrain.proto(&mut worker, i % outer, i / outer)))
.collect();
eprintln!("proto: {:.2}s", started.elapsed().as_secs_f64());
let started = Instant::now();
let inner = size + 2;
for z in 1..=inner {
for x in 1..=inner {
let mut region: [Proto; 9] = std::array::from_fn(|k| {
let (dx, dz) = (k as i32 % 3 - 1, k as i32 / 3 - 1);
protos[((z + dz) * outer + x + dx) as usize].take().expect("proto")
});
terrain.decorate(&mut worker, &mut region);
for (k, p) in region.into_iter().enumerate() {
let (dx, dz) = (k as i32 % 3 - 1, k as i32 / 3 - 1);
protos[((z + dz) * outer + x + dx) as usize] = Some(p);
}
}
}
eprintln!("decorate: {:.2}s", started.elapsed().as_secs_f64());
let started = Instant::now();
let mut chunks: Vec<Chunk> = Vec::new();
for z in 1..=inner {
for x in 1..=inner {
let proto = protos[(z * outer + x) as usize].take().expect("proto");
let mut chunk = terrain.finish(proto);
chunk.x = x;
chunk.z = z;
chunks.push(chunk);
}
}
let finish = started.elapsed().as_secs_f64();
println!("finish (proto -> resident chunk): {:.1} ms per chunk", finish * 1e3 / chunks.len() as f64);
let chunks: Vec<Arc<Chunk>> = chunks.into_iter().map(Arc::new).collect();
let at = |x: i32, z: i32| chunks[((z - 1) * inner + x - 1) as usize].clone();
// Light all inner-inner columns.
let started = Instant::now();
let mut lit = Vec::new();
for z in 2..inner {
for x in 2..inner {
let region: [[Option<Arc<Chunk>>; 3]; 3] = std::array::from_fn(|dz| {
std::array::from_fn(|dx| Some(at(x + dx as i32 - 1, z + dz as i32 - 1)))
});
let borrowed = horizon_client::mesher::borrow(&region);
lit.push(((x, z), light_chunk(&borrowed, &light, true)));
}
}
println!("light: {:.2} ms per column", started.elapsed().as_secs_f64() * 1e3 / lit.len() as f64);
let mut chunks: Vec<Chunk> = chunks.iter().map(|c| (**c).clone()).collect();
for ((x, z), sections) in lit {
chunks[((z - 1) * inner + x - 1) as usize].sections = sections;
}
let chunks: Vec<Arc<Chunk>> = chunks.into_iter().map(Arc::new).collect();
let at = |x: i32, z: i32| chunks[((z - 1) * inner + x - 1) as usize].clone();
// Memory.
let (mut bytes, mut sections, mut uniform) = (0usize, 0usize, 0usize);
for chunk in &chunks {
bytes += chunk.heap_bytes();
for s in &chunk.sections {
sections += 1;
uniform += usize::from(s.is_uniform(s.get(0)));
}
}
println!(
"resident: {} chunks, {} sections ({} uniform), {:.1} KiB per chunk ({:.2} MiB total)",
chunks.len(),
sections,
uniform,
bytes as f64 / 1024.0 / chunks.len() as f64,
bytes as f64 / 1048576.0
);
// Meshing.
let mut mesher = Mesher {
nether_shading: false,
assets: &assets,
tables: &tables,
biomes: &biomes,
visibility: tables.visibility.clone(),
zoom: BiomeZoom::new(7),
blend_radius: 2,
smooth_lighting: true,
cutout_leaves: true,
};
let mut best = f64::MAX;
let mut quads = 0usize;
for _ in 0..3 {
let started = Instant::now();
let mut jobs = 0;
quads = 0;
for z in 2..inner {
for x in 2..inner {
let region: [[Option<Arc<Chunk>>; 3]; 3] = std::array::from_fn(|dz| {
std::array::from_fn(|dx| Some(at(x + dx as i32 - 1, z + dz as i32 - 1)))
});
let borrowed = horizon_client::mesher::borrow(&region);
for sy in 0..SECTIONS as i32 {
let mesh = mesher.mesh(&borrowed, sy);
quads += (mesh.solid.len() + mesh.translucent.len()) / 4;
jobs += 1;
}
}
}
best = best.min(started.elapsed().as_secs_f64() / f64::from(jobs));
}
println!("mesh: {:.1} us per section ({} quads total)", best * 1e6, quads);
// Random lookups, as collision and entity queries do.
let mut state = 0x9E37_79B9_7F4A_7C15u64;
let started = Instant::now();
let mut sum = 0u64;
let lookups = 20_000_000u64;
for _ in 0..lookups {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
let cx = 2 + (state % u64::from((inner - 2) as u32)) as i32;
let cz = 2 + ((state >> 8) % u64::from((inner - 2) as u32)) as i32;
let chunk = &chunks[((cz - 1) * inner + cx - 1) as usize];
let (x, y, z) = (
(state >> 16) as i32 & 15,
MIN_Y + ((state >> 24) % 384) as i32,
(state >> 40) as i32 & 15,
);
sum += u64::from(chunk.get(x, y, z));
}
println!(
"random get: {:.2} ns per lookup (checksum {sum})",
started.elapsed().as_secs_f64() * 1e9 / lookups as f64
);
// Coherent scan, as a column or area query does.
let started = Instant::now();
let mut sum = 0u64;
for _ in 0..4 {
for chunk in &chunks {
for y in MIN_Y..MIN_Y + 384 {
for z in 0..16 {
for x in 0..16 {
sum += u64::from(chunk.get(x, y, z));
}
}
}
}
}
println!(
"scan get: {:.2} ns per lookup (checksum {sum})",
started.elapsed().as_secs_f64() * 1e9 / (4.0 * chunks.len() as f64 * 98304.0)
);
Ok(())
}
@@ -104,14 +104,21 @@ fn scene(
blend_radius = int(&field["radius"])?;
let default = biomes.id(field["default"].as_str().ok_or("missing default biome")?);
for chunk in &mut chunks {
Arc::make_mut(&mut chunk.biomes).fill(default);
for qy in 0..96 {
for cell in 0..16 {
chunk.set_biome_cell(cell & 3, qy, cell >> 2, default);
}
}
}
for cell in field["cells"].as_array().ok_or("missing biome cells")? {
let (qx, qy, qz) = (int(&cell[0])?, int(&cell[1])?, int(&cell[2])?);
let i = chunk_index(qx * 4, qz * 4)?;
let slot = ((qy - MIN_Y / 4) * 16 + (qz & 3) * 4 + (qx & 3)) as usize;
Arc::make_mut(&mut chunks[i].biomes)[slot] =
biomes.id(cell[3].as_str().ok_or("biome name")?);
chunks[i].set_biome_cell(
qx,
qy - MIN_Y / 4,
qz,
biomes.id(cell[3].as_str().ok_or("biome name")?),
);
}
}
let region: [[Option<&Chunk>; 3]; 3] =
+56 -35
View File
@@ -2,17 +2,18 @@
//! threads can mesh and light immutable snapshots while the game thread edits
//! (copy-on-write through `Arc::make_mut`).
use horizon_world::registry::BlockStateId;
use horizon_core::paletted::{PalettedContainer, Strategy};
use std::sync::Arc;
pub const MIN_Y: i32 = -64;
pub const HEIGHT: i32 = 384;
pub const SECTIONS: usize = (HEIGHT / 16) as usize;
#[derive(Clone)]
pub enum Blocks {
Uniform(u16),
Full(Box<[u16; 4096]>),
}
/// Block states are `PalettedContainer`s over the pinned registry (35,723
/// states: a 16-bit global palette); biomes are 4x4x4 containers over the
/// biome table (67 entries: a 7-bit global palette).
pub const BLOCK_STRATEGY: Strategy = Strategy::blocks(16);
pub const BIOME_STRATEGY: Strategy = Strategy::biomes(7);
/// Packed light per block: sky light in the high nibble, block light in the low one.
#[derive(Clone)]
pub enum Light {
@@ -25,13 +26,16 @@ pub fn index(x: i32, y: i32, z: i32) -> usize {
}
#[derive(Clone)]
pub struct Section {
pub blocks: Blocks,
pub blocks: PalettedContainer,
/// Biome table index per 4x4x4 cell, `(y << 2 | z) << 2 | x`.
pub biomes: PalettedContainer,
pub light: Light,
}
impl Default for Section {
fn default() -> Self {
Self {
blocks: Blocks::Uniform(0),
blocks: PalettedContainer::new(BLOCK_STRATEGY, 0),
biomes: PalettedContainer::new(BIOME_STRATEGY, 0),
light: Light::Uniform(0xF0),
}
}
@@ -39,23 +43,19 @@ impl Default for Section {
impl Section {
#[inline]
pub fn get(&self, i: usize) -> u16 {
match &self.blocks {
Blocks::Uniform(v) => *v,
Blocks::Full(a) => a[i],
}
self.blocks.get(i) as u16
}
pub fn set(&mut self, i: usize, v: u16) -> u16 {
match &mut self.blocks {
Blocks::Uniform(u) if *u == v => v,
Blocks::Uniform(u) => {
let old = *u;
let mut a = Box::new([old; 4096]);
a[i] = v;
self.blocks = Blocks::Full(a);
old
}
Blocks::Full(a) => std::mem::replace(&mut a[i], v),
}
self.blocks.set(i, u32::from(v)) as u16
}
/// All 4,096 block states in index order.
pub fn unpack(&self, out: &mut [u16; 4096]) {
self.blocks.unpack_u16(out);
}
/// The block states of a whole section at once.
pub fn from_blocks(values: &[u16; 4096]) -> PalettedContainer {
let wide: Vec<u32> = values.iter().map(|&v| u32::from(v)).collect();
PalettedContainer::from_values(BLOCK_STRATEGY, &wide)
}
#[inline]
pub fn light(&self, i: usize) -> u8 {
@@ -65,17 +65,26 @@ impl Section {
}
}
pub fn is_uniform(&self, v: u16) -> bool {
match &self.blocks {
Blocks::Uniform(u) => *u == v,
Blocks::Full(a) => a.iter().all(|&b| b == v),
}
self.blocks.is_uniform(u32::from(v))
}
/// Collapse storage that became uniform.
/// Heap bytes held by the block, biome and light storage (benchmark accounting).
pub fn heap_bytes(&self) -> usize {
self.blocks.heap_bytes()
+ self.biomes.heap_bytes()
+ match &self.light {
Light::Uniform(_) => 0,
Light::Full(_) => 4096,
}
}
/// Collapse storage that became uniform, and drop palette entries no
/// block uses any more.
pub fn compact(&mut self) {
if let Blocks::Full(a) = &self.blocks
&& a.iter().all(|&b| b == a[0])
{
self.blocks = Blocks::Uniform(a[0]);
for container in [&mut self.blocks, &mut self.biomes] {
if container.bits() != 0 {
if let Some(v) = container.uniform() {
*container = PalettedContainer::new(container.strategy(), v);
}
}
}
if let Light::Full(a) = &self.light
&& a.iter().all(|&b| b == a[0])
@@ -89,8 +98,6 @@ pub struct Chunk {
pub x: i32,
pub z: i32,
pub sections: Vec<Arc<Section>>,
/// Biome index per 4x4x4 cell (x,z in 0..4, y in 0..96) into `Biomes`.
pub biomes: Arc<Vec<u16>>,
/// Highest non-air, light-blocking-or-fluid Y per column (MOTION_BLOCKING-like), for rain/sky.
pub heights: Arc<[i16; 256]>,
pub lit: bool,
@@ -119,7 +126,6 @@ impl Chunk {
sections: (0..SECTIONS)
.map(|_| Arc::new(Section::default()))
.collect(),
biomes: Arc::new(vec![0; 16 * (HEIGHT as usize / 4)]),
heights: Arc::new([MIN_Y as i16; 256]),
lit: false,
light_loaded: false,
@@ -161,9 +167,24 @@ impl Chunk {
}
self.sections[s as usize].light(index(x, y, z))
}
/// Heap bytes held by the sections (benchmark accounting).
pub fn heap_bytes(&self) -> usize {
self.sections.iter().map(|s| s.heap_bytes()).sum::<usize>()
}
/// Biome table index at a block position (`ChunkAccess.getNoiseBiome` with
/// the quart y clamped to the column).
#[inline]
pub fn biome(&self, x: i32, y: i32, z: i32) -> u16 {
let qy = ((y - MIN_Y) >> 2).clamp(0, HEIGHT / 4 - 1);
self.biomes[(qy * 16 + ((z & 15) >> 2) * 4 + ((x & 15) >> 2)) as usize]
self.sections[(qy >> 2) as usize]
.biomes
.get(((qy & 3) << 4 | ((z & 15) >> 2) << 2 | (x & 15) >> 2) as usize) as u16
}
/// Sets the biome of one quart cell (`qy` counts from the column's bottom).
pub fn set_biome_cell(&mut self, qx: i32, qy: i32, qz: i32, biome: u16) {
let section = Arc::make_mut(&mut self.sections[(qy >> 2) as usize]);
section
.biomes
.set(((qy & 3) << 4 | (qz & 3) << 2 | (qx & 3)) as usize, u32::from(biome));
}
}
+7 -2
View File
@@ -933,9 +933,14 @@ impl Game<'_> {
};
let mut found = Vec::new();
for (s, section) in chunk.sections.iter().enumerate() {
let horizon_client::chunk::Blocks::Full(blocks) = &section.blocks else {
if !section
.blocks
.maybe_has(|v| v == u32::from(portal) || v == u32::from(gateway))
{
continue;
};
}
let mut blocks = Box::new([0u16; 4096]);
section.unpack(&mut blocks);
for (i, &b) in blocks.iter().enumerate() {
if b == portal || b == gateway {
let y = horizon_client::chunk::MIN_Y + s as i32 * 16 + (i >> 8) as i32;
+36 -35
View File
@@ -9,7 +9,7 @@
use super::{Dim, Game, Mining, Screen};
use horizon_client::{
Result,
chunk::{Blocks, Chunk, Light, MIN_Y, SECTIONS, Section},
chunk::{BIOME_STRATEGY, BLOCK_STRATEGY, Chunk, Light, MIN_Y, SECTIONS, Section},
containers::{BlockEntity, Furnace, FurnaceKind},
entities::{Experience, ItemEntity},
hud::{HudBatch, rgba},
@@ -22,7 +22,7 @@ use horizon_client::{
streaming::{ChunkGenerator, Proto, Streaming},
world::World,
};
use horizon_core::{geometry::Vec3, random::RandomSource};
use horizon_core::{geometry::Vec3, paletted::PalettedContainer, random::RandomSource};
use horizon_protocol::{
entity_data::{ITEM_ENTITY_ITEM, Value},
item::{Hashed, Stack},
@@ -434,54 +434,55 @@ impl Game<'_> {
.sections(r.dimension.sections(), 16, r.biome_bits)
.map_err(|e| e.to_string())?;
let mut chunk = Chunk::empty(p.x, p.z);
let mut biomes = vec![0u16; chunk.biomes.len()];
let offset = (r.dimension.min_y - MIN_Y).div_euclid(16);
// Biome table index per quart cell of the dimension's own sections.
let mut biome_cells: Vec<[u32; 64]> = Vec::with_capacity(sections.len());
for (i, s) in sections.iter().enumerate() {
let target = offset + i as i32;
let mut cells = [0u32; 64];
for (j, cell) in cells.iter_mut().enumerate() {
*cell = u32::from(
r.biome_map
.get(s.biomes.get(j) as usize)
.copied()
.unwrap_or(0),
);
}
biome_cells.push(cells);
if !(0..SECTIONS as i32).contains(&target) {
continue;
}
let blocks = if s.states.bits == 0 {
Blocks::Uniform(s.states.get(0) as u16)
} else {
let mut a = Box::new([0u16; 4096]);
for (j, v) in a.iter_mut().enumerate() {
*v = s.states.get(j) as u16;
}
Blocks::Full(a)
};
let strategy = BLOCK_STRATEGY;
let palette = s.states.palette.iter().map(|&p| p as u32).collect();
let words = s.states.data.iter().map(|&w| w as u64).collect();
let blocks = PalettedContainer::from_wire(strategy, s.states.bits, palette, words)
.map_err(|e| e.to_string())?;
let mut section = Section {
blocks,
biomes: PalettedContainer::from_values(BIOME_STRATEGY, &cells),
light: Light::Uniform(0xF0),
};
section.compact();
chunk.sections[target as usize] = Arc::new(section);
for j in 0..64 {
let biome = r
.biome_map
.get(s.biomes.get(j) as usize)
.copied()
.unwrap_or(0);
if let Some(cell) = biomes.get_mut(target as usize * 64 + j) {
*cell = biome;
}
// Sections outside the dimension's range take its nearest biomes
// (the column's top or bottom cell layer repeated).
if !biome_cells.is_empty() {
let last = biome_cells.len() - 1;
for si in 0..SECTIONS as i32 {
let i = si - offset;
if (0..sections.len() as i32).contains(&i) {
continue;
}
let (edge, layer) = if i < 0 { (0, 0) } else { (last, 3) };
let mut cells = [0u32; 64];
for (j, cell) in cells.iter_mut().enumerate() {
*cell = biome_cells[edge][layer * 16 + j % 16];
}
Arc::make_mut(&mut chunk.sections[si as usize]).biomes =
PalettedContainer::from_values(BIOME_STRATEGY, &cells);
}
}
// Columns outside the dimension's range take its nearest biome.
let (lo, hi) = (
offset.max(0) as usize * 64,
((offset + sections.len() as i32).min(SECTIONS as i32)) as usize * 64,
);
if lo < hi {
for j in 0..biomes.len() {
if j < lo {
biomes[j] = biomes[lo + j % 16];
} else if j >= hi {
biomes[j] = biomes[hi - 16 + j % 16];
}
}
}
chunk.biomes = Arc::new(biomes);
Ok(chunk)
}
/// Keep the renderable block entities (chests, furnaces) in step with blocks.
+3 -2
View File
@@ -216,6 +216,7 @@ pub fn light_chunk(
let mut block = vec![0u8; W * W * h];
let mut queue = VecDeque::new();
let mut sky_queue = VecDeque::new();
let mut ids = Box::new([0u16; 4096]);
for (dz, row) in region.iter().enumerate() {
for (dx, chunk) in row.iter().enumerate() {
let Some(chunk) = chunk else { continue };
@@ -223,8 +224,8 @@ pub fn light_chunk(
if section.is_uniform(0) {
continue;
}
for i in 0..4096 {
let id = section.get(i);
section.unpack(&mut ids);
for (i, &id) in ids.iter().enumerate() {
let (lx, lz, ly) = (i & 15, (i >> 4) & 15, i >> 8);
let p = at(dx * 16 + lx, si * 16 + ly, dz * 16 + lz);
states[p] = id;
+15 -1
View File
@@ -383,6 +383,13 @@ impl Padded {
let mut blocks = vec![0u16; P * P * P];
let mut light = vec![0u8; P * P * P];
let base_y = MIN_Y + sy * 16;
// The section itself is unpacked in one pass; the border cells of the
// neighbouring sections are read one by one.
let mut own = Box::new([0u16; 4096]);
let center = region[1][1].map(|c| &c.sections[sy as usize]);
if let Some(section) = center {
section.unpack(&mut own);
}
for y in -B..16 + B {
let wy = base_y + y;
for z in -B..16 + B {
@@ -390,7 +397,14 @@ impl Padded {
let chunk = region[((z + 16) >> 4) as usize][((x + 16) >> 4) as usize];
let i = pi(x, y, z);
if let Some(c) = chunk {
blocks[i] = c.get(x & 15, wy, z & 15);
blocks[i] = if (0..16).contains(&x)
&& (0..16).contains(&y)
&& (0..16).contains(&z)
{
own[(y << 8 | z << 4 | x) as usize]
} else {
c.get(x & 15, wy, z & 15)
};
light[i] = c.light(x & 15, wy, z & 15);
} else {
light[i] = 0xF0;
+37 -79
View File
@@ -16,10 +16,11 @@
use crate::{
Result,
biome::Biomes,
chunk::{Blocks, Chunk, Light, MIN_Y, SECTIONS},
chunk::{BIOME_STRATEGY, BLOCK_STRATEGY, Chunk, Light, MIN_Y, SECTIONS},
streaming::ChunkGenerator,
};
use horizon_core::{
paletted::PalettedContainer,
position::{BlockPos, ChunkPos},
ticks::{SavedTick, TickPriority, TickTypeId},
};
@@ -42,29 +43,6 @@ use std::{
pub const VERSION_NAME: &str = "26.3";
fn pack(indices: &[u16], bits: u32) -> Vec<i64> {
let per_long = (64 / bits) as usize;
let mut longs = vec![0i64; indices.len().div_ceil(per_long)];
for (i, &v) in indices.iter().enumerate() {
longs[i / per_long] |=
(i64::from(v) & ((1i64 << bits) - 1)) << ((i % per_long) as u32 * bits);
}
longs
}
fn unpack(longs: &[i64], bits: u32, count: usize) -> Vec<u16> {
let per_long = (64 / bits) as usize;
let mask = (1u64 << bits) - 1;
(0..count)
.map(|i| {
longs.get(i / per_long).map_or(0, |&l| {
((l as u64 >> ((i % per_long) as u32 * bits)) & mask) as u16
})
})
.collect()
}
fn bits_for(len: usize, min: u32) -> u32 {
(usize::BITS - (len.max(1) - 1).leading_zeros()).max(min)
}
fn string(s: &str) -> Tag {
Tag::String(s.into())
}
@@ -584,33 +562,22 @@ pub fn encode_with(
});
for (si, section) in chunk.sections.iter().enumerate() {
let y = (MIN_Y >> 4) + si as i32;
let ids: Vec<u16> = match &section.blocks {
Blocks::Uniform(v) => vec![*v],
Blocks::Full(a) => a.to_vec(),
};
let mut palette: Vec<u16> = Vec::new();
let mut lookup: HashMap<u16, u16> = HashMap::new();
let indices: Vec<u16> = ids
.iter()
.map(|id| {
*lookup.entry(*id).or_insert_with(|| {
palette.push(*id);
(palette.len() - 1) as u16
})
})
.collect();
// PalettedContainer.pack: distinct states in index order, indices at
// the saved width.
let packed = section.blocks.pack();
let mut states = vec![(
"palette",
Tag::List(
palette
packed
.palette
.iter()
.map(|id| {
let state = registry.state(BlockStateId(u32::from(*id)));
.map(|&id| {
let state = registry.state(BlockStateId(id));
match state {
// BlockState.CODEC: the block name for default states.
Some(s)
if tables.is_some_and(|t| {
t.default_state.get(*id as usize) == Some(&true)
t.default_state.get(id as usize) == Some(&true)
}) =>
{
string(&s.name)
@@ -622,37 +589,27 @@ pub fn encode_with(
.collect(),
),
)];
if palette.len() > 1 {
if let Some(data) = &packed.data {
states.push((
"data",
Tag::LongArray(pack(&indices, bits_for(palette.len(), 4))),
Tag::LongArray(data.iter().map(|&w| w as i64).collect()),
));
}
let quart: Vec<u16> = (0..64).map(|i| chunk.biomes[si * 64 + i]).collect();
let mut biome_palette: Vec<u16> = Vec::new();
let biome_indices: Vec<u16> = quart
.iter()
.map(|b| match biome_palette.iter().position(|p| p == b) {
Some(i) => i as u16,
None => {
biome_palette.push(*b);
(biome_palette.len() - 1) as u16
}
})
.collect();
let biome_packed = section.biomes.pack();
let mut biome_tag = vec![(
"palette",
Tag::List(
biome_palette
biome_packed
.palette
.iter()
.map(|b| string(&biomes.get(*b).name))
.map(|&b| string(&biomes.get(b as u16).name))
.collect(),
),
)];
if biome_palette.len() > 1 {
if let Some(data) = &biome_packed.data {
biome_tag.push((
"data",
Tag::LongArray(pack(&biome_indices, bits_for(biome_palette.len(), 0))),
Tag::LongArray(data.iter().map(|&w| w as i64).collect()),
));
}
let mut entries = vec![
@@ -811,7 +768,6 @@ pub fn decode_with(
}
let air = registry.default_state("minecraft:air")?.0 as u16;
let mut cache: HashMap<String, u16> = HashMap::new();
let mut biome_data = (*chunk.biomes).clone();
let Some(Tag::List(sections)) = tag.get("sections") else {
return Some(chunk);
};
@@ -854,15 +810,14 @@ pub fn decode_with(
})
.collect();
let target = Arc::make_mut(&mut chunk.sections[si as usize]);
if ids.len() <= 1 {
target.blocks = Blocks::Uniform(ids.first().copied().unwrap_or(air));
} else if let Some(Tag::LongArray(data)) = states.get("data") {
let indices = unpack(data, bits_for(ids.len(), 4), 4096);
let mut full = Box::new([air; 4096]);
for (i, idx) in indices.iter().enumerate() {
full[i] = ids.get(*idx as usize).copied().unwrap_or(air);
}
target.blocks = Blocks::Full(full);
let data = match states.get("data") {
Some(Tag::LongArray(data)) => Some(data.iter().map(|&w| w as u64).collect::<Vec<_>>()),
_ => None,
};
let palette: Vec<u32> = ids.iter().map(|&id| u32::from(id)).collect();
if let Ok(blocks) = PalettedContainer::from_packed(BLOCK_STRATEGY, palette, data.as_deref())
{
target.blocks = blocks;
}
}
if let Some(b) = section.get("biomes")
@@ -875,18 +830,21 @@ pub fn decode_with(
_ => 0,
})
.collect();
let indices = match b.get("data") {
Some(Tag::LongArray(data)) if ids.len() > 1 => {
unpack(data, bits_for(ids.len(), 0), 64)
}
_ => vec![0; 64],
let data = match b.get("data") {
Some(Tag::LongArray(data)) => Some(data.iter().map(|&w| w as u64).collect::<Vec<_>>()),
_ => None,
};
for (i, idx) in indices.iter().enumerate() {
biome_data[si as usize * 64 + i] = ids.get(*idx as usize).copied().unwrap_or(0);
let palette: Vec<u32> = ids.iter().map(|&id| u32::from(id)).collect();
let target = Arc::make_mut(&mut chunk.sections[si as usize]);
if let Ok(container) =
PalettedContainer::from_packed(BIOME_STRATEGY, palette.clone(), data.as_deref())
{
target.biomes = container;
} else if let Some(&first) = palette.first() {
target.biomes = PalettedContainer::new(BIOME_STRATEGY, first);
}
}
}
chunk.biomes = Arc::new(biome_data);
let mut extras = ChunkExtras {
last_update: number(tag.get("LastUpdate")).unwrap_or(0),
..ChunkExtras::default()
+7 -1
View File
@@ -76,7 +76,13 @@ impl ChunkGenerator for TestTerrain {
impl TestTerrain {
fn generate(&self, _: &mut (), cx: i32, cz: i32) -> Chunk {
let mut chunk = Chunk::empty(cx, cz);
chunk.biomes = std::sync::Arc::new(vec![self.plains; chunk.biomes.len()]);
for s in &mut chunk.sections {
std::sync::Arc::make_mut(s).biomes =
horizon_core::paletted::PalettedContainer::new(
crate::chunk::BIOME_STRATEGY,
u32::from(self.plains),
);
}
for z in 0..16 {
for x in 0..16 {
let (wx, wz) = (cx * 16 + x, cz * 16 + z);
+28 -19
View File
@@ -2,10 +2,10 @@
//! aquifers, surface rules, carvers and placed features) to chunk streaming.
use crate::{
biome::Biomes,
chunk::{Chunk, HEIGHT, MIN_Y},
chunk::{BIOME_STRATEGY, Chunk, HEIGHT, MIN_Y, SECTIONS, Section},
streaming::{ChunkGenerator, Proto},
};
use horizon_core::position::ChunkPos;
use horizon_core::{paletted::PalettedContainer, position::ChunkPos};
use horizon_world::{
registry::BlockRegistry,
worldgen::generator::{GeneratorData, GeneratorWorker, OverworldGenerator, ProtoChunk},
@@ -177,15 +177,26 @@ impl ChunkGenerator for VanillaTerrain {
chunk.terrain.min_y().max(MIN_Y),
(chunk.terrain.min_y() + chunk.terrain.height()).min(MIN_Y + HEIGHT),
);
for y in lo..hi {
for z in 0..16 {
for x in 0..16 {
let id = chunk.block(bx + x, y, bz + z).0 as u16;
if id != 0 {
out.set(x, y, z, id);
let mut values = Box::new([0u16; 4096]);
for si in 0..SECTIONS as i32 {
let base = MIN_Y + si * 16;
if base + 16 <= lo || base >= hi {
continue;
}
values.fill(0);
let mut any = false;
for y in base.max(lo)..(base + 16).min(hi) {
for z in 0..16 {
for x in 0..16 {
let id = chunk.block(bx + x, y, bz + z).0 as u16;
values[((y & 15) << 8 | z << 4 | x) as usize] = id;
any |= id != 0;
}
}
}
if any {
Arc::make_mut(&mut out.sections[si as usize]).blocks = Section::from_blocks(&values);
}
}
let base_section = chunk.terrain.min_y() >> 4;
for (i, marks) in chunk.terrain.postprocessing().iter().enumerate() {
@@ -199,19 +210,17 @@ impl ChunkGenerator for VanillaTerrain {
}
}
}
let mut biomes = vec![0u16; 16 * (HEIGHT as usize / 4)];
for qy in 0..HEIGHT / 4 {
for qz in 0..4 {
for qx in 0..4 {
let y = (MIN_Y + qy * 4).clamp(lo, hi - 1);
let id = chunk.biome_id_at(bx + qx * 4, y, bz + qz * 4);
let index = id.0 as usize;
biomes[(qy * 16 + qz * 4 + qx) as usize] =
self.biome_map.get(index).copied().unwrap_or(0);
}
for si in 0..SECTIONS as i32 {
let mut cells = [0u32; 64];
for (j, cell) in cells.iter_mut().enumerate() {
let (qx, qz, qy) = (j as i32 & 3, (j as i32 >> 2) & 3, si * 4 + (j as i32 >> 4));
let y = (MIN_Y + qy * 4).clamp(lo, hi - 1);
let id = chunk.biome_id_at(bx + qx * 4, y, bz + qz * 4);
*cell = u32::from(self.biome_map.get(id.0 as usize).copied().unwrap_or(0));
}
Arc::make_mut(&mut out.sections[si as usize]).biomes =
PalettedContainer::from_values(BIOME_STRATEGY, &cells);
}
out.biomes = Arc::new(biomes);
out.block_entities = crate::structure_blocks::block_entities(&chunk, self.loot.as_deref());
out.structure_refs = chunk
.structures()
@@ -0,0 +1,135 @@
//! Text transport for the PalettedContainer comparison: replays scripted edits
//! and prints the same hashes as `VanillaPalettedContainerOracle`.
use horizon_core::paletted::{PalettedContainer, Strategy};
use std::{
fs,
io::{self, Write},
};
const BASIS: u64 = 0xcbf2_9ce4_8422_2325;
fn fnv(h: u64, b: u8) -> u64 {
(h ^ u64::from(b)).wrapping_mul(0x100_0000_01b3)
}
fn hash_bytes(bytes: &[u8]) -> u64 {
bytes.iter().fold(BASIS, |h, &b| fnv(h, b))
}
fn varint(out: &mut Vec<u8>, mut v: u32) {
while v >= 0x80 {
out.push((v & 0x7F) as u8 | 0x80);
v >>= 7;
}
out.push(v as u8);
}
/// `PalettedContainer.write`.
fn write(c: &PalettedContainer) -> Vec<u8> {
let (bits, palette, words) = c.to_wire();
let mut out = vec![bits];
if bits == 0 {
varint(&mut out, palette[0]);
} else if !palette.is_empty() {
varint(&mut out, palette.len() as u32);
for p in palette {
varint(&mut out, p);
}
}
for w in words {
out.extend(w.to_be_bytes());
}
out
}
/// `PalettedContainer.read` for the leading byte, palette and longs.
fn read(strategy: Strategy, bytes: &[u8]) -> PalettedContainer {
let mut at = 1;
let mut varint = || {
let (mut v, mut shift) = (0u32, 0);
loop {
let b = bytes[at];
at += 1;
v |= u32::from(b & 0x7F) << shift;
if b & 0x80 == 0 {
return v;
}
shift += 7;
}
};
let bits = bytes[0];
let palette = if bits == 0 {
vec![varint()]
} else if strategy.wire_has_palette(bits) {
let n = varint();
(0..n).map(|_| varint()).collect()
} else {
Vec::new()
};
let words = bytes[at..]
.chunks_exact(8)
.map(|c| u64::from_be_bytes(c.try_into().unwrap()))
.collect();
PalettedContainer::from_wire(strategy, bits, palette, words).unwrap()
}
fn wire(c: &PalettedContainer) -> String {
let bytes = write(c);
format!("{}:{}", bytes.len(), hash_bytes(&bytes) as i64)
}
fn values(c: &PalettedContainer) -> i64 {
let mut h = BASIS;
for i in 0..c.strategy().entries() {
for b in c.get(i).to_le_bytes() {
h = fnv(h, b);
}
}
h as i64
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = std::env::args().collect();
let mut out = io::BufWriter::new(fs::File::create(&args[2])?);
for line in fs::read_to_string(&args[1])?.lines() {
let t: Vec<&str> = line.split_whitespace().collect();
let strategy = if t[1] == "blocks" {
Strategy::blocks(16)
} else {
Strategy::biomes(7)
};
let mut c = PalettedContainer::new(strategy, t[2].parse()?);
let every: usize = t[3].parse()?;
let (mut olds, mut n) = (BASIS, 0usize);
let mut checkpoints = Vec::new();
for pair in t[4..].chunks(2) {
let old = c.set(pair[0].parse()?, pair[1].parse()?);
for b in old.to_le_bytes() {
olds = fnv(olds, b);
}
n += 1;
if n % every == 0 {
checkpoints.push(wire(&c));
}
}
let reread = read(strategy, &write(&c));
let packed = c.pack();
let storage = packed.data.as_ref().map_or(BASIS, |data| {
data.iter()
.flat_map(|w| w.to_le_bytes())
.fold(BASIS, |h, b| fnv(h, b))
});
let palette: Vec<String> = packed.palette.iter().map(u32::to_string).collect();
let loaded =
PalettedContainer::from_packed(strategy, packed.palette.clone(), packed.data.as_deref())?;
writeln!(
out,
"{} {} cp={} bits={} final={} values={} read={}/{} packed={}/{}/{} loaded={}/{}",
t[0],
olds as i64,
checkpoints.join(","),
c.bits(),
wire(&c),
values(&c),
values(&reread),
wire(&reread),
packed.bits,
storage as i64,
palette.join(","),
values(&loaded),
wire(&loaded),
)?;
}
Ok(())
}
+1
View File
@@ -9,6 +9,7 @@ pub mod carving;
pub mod density;
pub mod geometry;
pub mod noise;
pub mod paletted;
pub mod position;
pub mod random;
pub mod storage;
+739
View File
@@ -0,0 +1,739 @@
//! Resident `PalettedContainer` of Java Edition 26.3: single-value, linear and
//! hash-map palettes with vanilla's bit-width thresholds, and the global
//! palette beyond them, over `SimpleBitStorage` words (values never span
//! longs). One container type serves blocks (16^3 entries) and biomes (4^3).
//!
//! Growth follows `PalettedContainer.onResize`: a full palette asks for one
//! more bit, the new configuration is built by re-adding the distinct values
//! in index order, and the value that did not fit is added last. Palettes
//! never shrink while resident; `pack` writes the canonical saved form.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
/// `Strategy.createForBlockStates`: 4096 entries, linear palettes up to 4
/// bits and hash-map palettes from 5 to 8.
Blocks,
/// `Strategy.createForBiomes`: 64 entries, linear palettes up to 3 bits.
Biomes,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Strategy {
pub kind: Kind,
/// `ceillog2` of the registry size: the width of the global palette.
pub global_bits: u8,
}
/// `getConfigurationForBitCount`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Config {
Single,
/// Palette of up to `1 << bits` entries, stored in `bits` bits.
List(u8),
Global,
}
impl Strategy {
pub const fn blocks(global_bits: u8) -> Self {
Self {
kind: Kind::Blocks,
global_bits,
}
}
pub const fn biomes(global_bits: u8) -> Self {
Self {
kind: Kind::Biomes,
global_bits,
}
}
pub const fn entries(self) -> usize {
match self.kind {
Kind::Blocks => 4096,
Kind::Biomes => 64,
}
}
fn config(self, bits: u8) -> Config {
match (self.kind, bits) {
(_, 0) => Config::Single,
(Kind::Blocks, 1..=4) => Config::List(4),
(Kind::Blocks, 5..=8) => Config::List(bits),
(Kind::Biomes, 1..=3) => Config::List(bits),
_ => Config::Global,
}
}
/// Whether a container written with leading byte `bits` lists its palette
/// (all but the global one).
pub fn wire_has_palette(self, bits: u8) -> bool {
self.config(bits) != Config::Global
}
/// Bits of the storage a configuration keeps in memory.
fn storage_bits(self, config: Config) -> u8 {
match config {
Config::Single => 0,
Config::List(bits) => bits,
Config::Global => self.global_bits,
}
}
}
/// `Mth.ceillog2`.
pub fn ceil_log2(n: usize) -> u8 {
if n <= 1 {
0
} else {
(usize::BITS - (n - 1).leading_zeros()) as u8
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PaletteError {
/// A palette bigger than its configuration allows, or empty.
BadPalette { len: usize },
/// Storage with the wrong number of longs.
BadStorage { expected: usize, actual: usize },
/// A stored index past the palette.
BadIndex { index: u32, palette: usize },
/// The declared bit count does not match the palette.
BadBits { declared: u8, expected: u8 },
}
impl std::fmt::Display for PaletteError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl std::error::Error for PaletteError {}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Palette {
Single(u32),
/// Linear and hash-map palettes alike: ids in insertion order.
List(Vec<u32>),
Global,
}
/// `n / per_word` for `n < 4096` by multiplication: `MAGIC[bits] = ceil(2^20 /
/// (64 / bits))` is exact while `n * (64 / bits) < 2^20`.
const MAGIC: [u32; 33] = {
let mut table = [0; 33];
let mut bits = 1;
while bits <= 32 {
let per = 64 / bits as u32;
table[bits] = (1u32 << 20).div_ceil(per);
bits += 1;
}
table
};
/// Words of `bits`-wide values: `entries.div_ceil(64 / bits)`.
pub fn storage_len(bits: u8, entries: usize) -> usize {
if bits == 0 {
0
} else {
entries.div_ceil(64 / usize::from(bits))
}
}
#[inline]
fn locate(bits: u8, i: usize) -> (usize, u32) {
if bits.is_power_of_two() {
// 64 / bits is a power of two: shifts instead of the multiplication.
let log = bits.trailing_zeros();
let word = i >> (6 - log);
(word, ((i & ((64 >> log) - 1)) as u32) << log)
} else {
let per = 64 / u32::from(bits);
let word = (i as u32 * MAGIC[usize::from(bits)]) >> 20;
(word as usize, (i as u32 - word * per) * u32::from(bits))
}
}
#[inline]
fn mask(bits: u8) -> u64 {
(1u64 << bits) - 1
}
fn read(words: &[u64], bits: u8, i: usize) -> u32 {
let (word, shift) = locate(bits, i);
((words[word] >> shift) & mask(bits)) as u32
}
fn write(words: &mut [u64], bits: u8, i: usize, value: u32) {
let (word, shift) = locate(bits, i);
words[word] = (words[word] & !(mask(bits) << shift)) | (u64::from(value) << shift);
}
/// Words holding `ids` in `bits`-wide slots.
fn pack_words(bits: u8, entries: usize, ids: impl Iterator<Item = u32>) -> Vec<u64> {
let mut words = vec![0u64; storage_len(bits, entries)];
for (i, id) in ids.enumerate() {
write(&mut words, bits, i, id);
}
words
}
/// The saved form: distinct values in index order, and the packed indices
/// when there is more than one value.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Packed {
pub palette: Vec<u32>,
/// Bits per index in `data` (0 when `data` is absent).
pub bits: u8,
pub data: Option<Vec<u64>>,
}
#[derive(Clone, Debug)]
pub struct PalettedContainer {
strategy: Strategy,
bits: u8,
palette: Palette,
words: Vec<u64>,
}
impl PalettedContainer {
/// `new PalettedContainer(initialValue, strategy)`.
pub fn new(strategy: Strategy, value: u32) -> Self {
Self {
strategy,
bits: 0,
palette: Palette::Single(value),
words: Vec::new(),
}
}
pub fn strategy(&self) -> Strategy {
self.strategy
}
/// Bits per stored index (`BitStorage.getBits`).
pub fn bits(&self) -> u8 {
self.bits
}
/// Entries in the palette (the global palette reports its bit capacity).
pub fn palette_len(&self) -> usize {
match &self.palette {
Palette::Single(_) => 1,
Palette::List(list) => list.len(),
Palette::Global => 1 << self.bits.min(31),
}
}
pub fn is_global(&self) -> bool {
matches!(self.palette, Palette::Global)
}
#[inline]
fn raw(&self, i: usize) -> usize {
let (word, shift) = locate(self.bits, i);
((self.words[word] >> shift) & mask(self.bits)) as usize
}
#[inline]
pub fn get(&self, i: usize) -> u32 {
match &self.palette {
Palette::Single(value) => *value,
Palette::List(list) => list[self.raw(i)],
Palette::Global => self.raw(i) as u32,
}
}
/// The value when every entry is equal, without the palette having to be
/// minimal.
pub fn uniform(&self) -> Option<u32> {
match &self.palette {
Palette::Single(value) => Some(*value),
Palette::List(list) if list.len() == 1 => Some(list[0]),
Palette::Global => self.all_equal(self.get(0)).then(|| self.get(0)),
Palette::List(list) => {
let first = list[self.raw(0)];
self.all_equal(first).then_some(first)
}
}
}
/// Whether all entries equal `value`.
pub fn is_uniform(&self, value: u32) -> bool {
match &self.palette {
Palette::Single(v) => *v == value,
Palette::List(list) => match list.iter().position(|&v| v == value) {
None => false,
Some(_) if list.len() == 1 => true,
Some(_) => self.all_equal(value),
},
Palette::Global => self.all_equal(value),
}
}
fn all_equal(&self, value: u32) -> bool {
let id = match &self.palette {
Palette::Single(v) => return *v == value,
Palette::List(list) => match list.iter().position(|&v| v == value) {
Some(id) => id as u64,
None => return false,
},
Palette::Global => u64::from(value),
};
let bits = usize::from(self.bits);
let per = 64 / bits;
let pattern = (0..per).fold(0u64, |word, slot| word | id << (slot * bits));
let entries = self.strategy.entries();
let full = entries / per;
if self.words[..full].iter().any(|&w| w != pattern) {
return false;
}
let tail = entries - full * per;
tail == 0 || {
let used = (1u64 << (tail * bits)) - 1;
self.words[full] & used == pattern & used
}
}
/// `getAndSet`: stores `value` and returns the previous one.
pub fn set(&mut self, i: usize, value: u32) -> u32 {
let id = self.id_for(value);
match &self.palette {
Palette::Single(old) => *old,
Palette::List(list) => {
let old = read(&self.words, self.bits, i) as usize;
write(&mut self.words, self.bits, i, id as u32);
list[old]
}
Palette::Global => {
let old = read(&self.words, self.bits, i);
write(&mut self.words, self.bits, i, id as u32);
old
}
}
}
/// `Palette.idFor`, growing the container when the palette is full.
fn id_for(&mut self, value: u32) -> usize {
match &mut self.palette {
Palette::Single(v) if *v == value => 0,
Palette::Single(_) => self.resize(1, value),
Palette::List(list) => {
if let Some(id) = list.iter().position(|&v| v == value) {
id
} else if list.len() < 1 << self.bits {
list.push(value);
list.len() - 1
} else {
self.resize(self.bits + 1, value)
}
}
Palette::Global => value as usize,
}
}
/// `PalettedContainer.onResize`: rebuild with the configuration for
/// `bits`, then add `last` and return its id.
fn resize(&mut self, bits: u8, last: u32) -> usize {
let strategy = self.strategy;
let config = strategy.config(bits);
let storage_bits = strategy.storage_bits(config);
let entries = strategy.entries();
let mut palette = match config {
Config::Single => Palette::Single(last),
Config::List(_) => Palette::List(Vec::new()),
Config::Global => Palette::Global,
};
let mut words = vec![0u64; storage_len(storage_bits, entries)];
let id_in = |palette: &mut Palette, value: u32| -> u32 {
match palette {
Palette::Single(_) => 0,
Palette::List(list) => match list.iter().position(|&v| v == value) {
Some(id) => id as u32,
None => {
list.push(value);
list.len() as u32 - 1
}
},
Palette::Global => value,
}
};
if let Palette::Single(only) = self.palette {
let id = id_in(&mut palette, only);
if storage_bits > 0 && id != 0 {
for i in 0..entries {
write(&mut words, storage_bits, i, id);
}
}
} else {
let (mut last_old, mut new_id) = (u32::MAX, 0);
for i in 0..entries {
let old = read(&self.words, self.bits, i);
if old != last_old {
last_old = old;
let value = match &self.palette {
Palette::List(list) => list[old as usize],
_ => old,
};
new_id = id_in(&mut palette, value);
}
if storage_bits > 0 {
write(&mut words, storage_bits, i, new_id);
}
}
}
let id = id_in(&mut palette, last);
self.palette = palette;
self.bits = storage_bits;
self.words = words;
id as usize
}
/// All values in index order.
pub fn unpack(&self, out: &mut [u32]) {
assert_eq!(out.len(), self.strategy.entries());
match &self.palette {
Palette::Single(value) => out.fill(*value),
palette => {
let bits = usize::from(self.bits);
let per = 64 / bits;
let m = mask(self.bits);
for (chunk, &word) in out.chunks_mut(per).zip(&self.words) {
for (slot, cell) in chunk.iter_mut().enumerate() {
let id = ((word >> (slot * bits)) & m) as u32;
*cell = match palette {
Palette::List(list) => list[id as usize],
_ => id,
};
}
}
}
}
}
/// Block ids as 16-bit values (callers whose registry fits in 16 bits).
pub fn unpack_u16(&self, out: &mut [u16; 4096]) {
debug_assert_eq!(self.strategy.entries(), 4096);
match &self.palette {
Palette::Single(value) => out.fill(*value as u16),
palette => {
let bits = usize::from(self.bits);
let per = 64 / bits;
let m = mask(self.bits);
for (chunk, &word) in out.chunks_mut(per).zip(&self.words) {
for (slot, cell) in chunk.iter_mut().enumerate() {
let id = ((word >> (slot * bits)) & m) as usize;
*cell = match palette {
Palette::List(list) => list[id] as u16,
_ => id as u16,
};
}
}
}
}
}
/// A container holding `values` in the smallest configuration for their
/// distinct count, with palette ids in first-occurrence order, like a
/// container unpacked from disk.
pub fn from_values(strategy: Strategy, values: &[u32]) -> Self {
assert_eq!(values.len(), strategy.entries());
let mut palette: Vec<u32> = Vec::new();
let mut small = true;
let mut lookup: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
let mut ids = Vec::with_capacity(values.len());
for &value in values {
let id = if small {
match palette.iter().position(|&v| v == value) {
Some(id) => id as u32,
None => {
palette.push(value);
if palette.len() > 16 {
small = false;
lookup.extend(palette.iter().enumerate().map(|(i, &v)| (v, i as u32)));
}
palette.len() as u32 - 1
}
}
} else {
*lookup.entry(value).or_insert_with(|| {
palette.push(value);
palette.len() as u32 - 1
})
};
ids.push(id);
}
Self::build(strategy, palette, &ids)
}
/// A container over a distinct-value `palette` and an id per entry.
fn build(strategy: Strategy, palette: Vec<u32>, ids: &[u32]) -> Self {
let config = strategy.config(ceil_log2(palette.len()));
match config {
Config::Single => Self::new(strategy, palette[0]),
Config::List(bits) => Self {
strategy,
bits,
words: pack_words(bits, ids.len(), ids.iter().copied()),
palette: Palette::List(palette),
},
Config::Global => {
let bits = strategy.global_bits;
Self {
strategy,
bits,
words: pack_words(bits, ids.len(), ids.iter().map(|&id| palette[id as usize])),
palette: Palette::Global,
}
}
}
}
/// `PalettedContainer.read`: the container as sent on the wire. `bits` is
/// the leading byte, `palette` the VarInt entries (a single one for
/// `bits == 0`, none for the global palette) and `words` the longs.
pub fn from_wire(
strategy: Strategy,
bits: u8,
palette: Vec<u32>,
words: Vec<u64>,
) -> Result<Self, PaletteError> {
let config = strategy.config(bits);
let storage_bits = strategy.storage_bits(config);
let expected = storage_len(storage_bits, strategy.entries());
if words.len() != expected {
return Err(PaletteError::BadStorage {
expected,
actual: words.len(),
});
}
match config {
Config::Single => {
if palette.len() != 1 {
return Err(PaletteError::BadPalette { len: palette.len() });
}
Ok(Self::new(strategy, palette[0]))
}
Config::List(bits) => {
if palette.is_empty() || palette.len() > 1 << bits {
return Err(PaletteError::BadPalette { len: palette.len() });
}
let container = Self {
strategy,
bits,
palette: Palette::List(palette),
words,
};
container.check_indices()?;
Ok(container)
}
Config::Global => Ok(Self {
strategy,
bits: storage_bits,
palette: Palette::Global,
words,
}),
}
}
fn check_indices(&self) -> Result<(), PaletteError> {
let Palette::List(list) = &self.palette else {
return Ok(());
};
if list.len() == 1 << self.bits {
return Ok(());
}
for i in 0..self.strategy.entries() {
let index = read(&self.words, self.bits, i);
if index as usize >= list.len() {
return Err(PaletteError::BadIndex {
index,
palette: list.len(),
});
}
}
Ok(())
}
/// `PalettedContainer.write`: the leading bits byte, the palette entries
/// (none for the global palette) and the raw longs, exactly as resident.
pub fn to_wire(&self) -> (u8, Vec<u32>, &[u64]) {
let palette = match &self.palette {
Palette::Single(value) => vec![*value],
Palette::List(list) => list.clone(),
Palette::Global => Vec::new(),
};
(self.bits, palette, &self.words)
}
/// `PalettedContainer.pack`: distinct values in index order, indices
/// re-encoded for the saved bit width.
pub fn pack(&self) -> Packed {
let entries = self.strategy.entries();
let mut palette: Vec<u32> = Vec::new();
let mut ids = Vec::with_capacity(entries);
let mut values = vec![0u32; entries];
self.unpack(&mut values);
let mut lookup: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
for &value in &values {
let id = *lookup.entry(value).or_insert_with(|| {
palette.push(value);
palette.len() as u32 - 1
});
ids.push(id);
}
let bits = match self.strategy.config(ceil_log2(palette.len())) {
Config::Single => 0,
Config::List(bits) => bits,
Config::Global => ceil_log2(palette.len()),
};
let data = (bits != 0).then(|| pack_words(bits, entries, ids.iter().copied()));
Packed {
palette,
bits,
data,
}
}
/// `PalettedContainer.unpack`: a container from the saved form.
pub fn from_packed(
strategy: Strategy,
palette: Vec<u32>,
data: Option<&[u64]>,
) -> Result<Self, PaletteError> {
if palette.is_empty() {
return Err(PaletteError::BadPalette { len: 0 });
}
let config = strategy.config(ceil_log2(palette.len()));
let entries = strategy.entries();
let disc_bits = match config {
Config::Single => return Ok(Self::new(strategy, palette[0])),
Config::List(bits) => bits,
Config::Global => ceil_log2(palette.len()),
};
let data = data.ok_or(PaletteError::BadStorage {
expected: storage_len(disc_bits, entries),
actual: 0,
})?;
let expected = storage_len(disc_bits, entries);
if data.len() != expected {
return Err(PaletteError::BadStorage {
expected,
actual: data.len(),
});
}
let mut ids = Vec::with_capacity(entries);
for i in 0..entries {
let index = read(data, disc_bits, i);
if index as usize >= palette.len() {
return Err(PaletteError::BadIndex {
index,
palette: palette.len(),
});
}
ids.push(index);
}
Ok(match config {
Config::List(bits) => Self {
strategy,
bits,
words: data.to_vec(),
palette: Palette::List(palette),
},
_ => Self::build(strategy, palette, &ids),
})
}
/// Whether any palette entry satisfies `predicate` (`maybeHas`): entries
/// no longer used by any position still count.
pub fn maybe_has(&self, mut predicate: impl FnMut(u32) -> bool) -> bool {
match &self.palette {
Palette::Single(value) => predicate(*value),
Palette::List(list) => list.iter().any(|&v| predicate(v)),
Palette::Global => (0..1u32 << self.bits.min(31)).any(predicate),
}
}
/// Heap bytes held by the palette and the storage.
pub fn heap_bytes(&self) -> usize {
self.words.capacity() * 8
+ match &self.palette {
Palette::List(list) => list.capacity() * 4,
_ => 0,
}
}
}
impl PartialEq for PalettedContainer {
/// Equal when every entry is.
fn eq(&self, other: &Self) -> bool {
let entries = self.strategy.entries();
entries == other.strategy.entries() && (0..entries).all(|i| self.get(i) == other.get(i))
}
}
#[cfg(test)]
mod tests {
use super::*;
const BLOCKS: Strategy = Strategy::blocks(16);
const BIOMES: Strategy = Strategy::biomes(7);
fn rng(state: &mut u64) -> u64 {
*state ^= *state << 13;
*state ^= *state >> 7;
*state ^= *state << 17;
*state
}
#[test]
fn grows_through_vanilla_widths() {
let mut c = PalettedContainer::new(BLOCKS, 0);
assert_eq!((c.bits(), c.palette_len()), (0, 1));
let mut expected = Vec::new();
for n in 1..=300u32 {
c.set(n as usize, n);
expected.push(match n + 1 {
2..=16 => 4,
17..=32 => 5,
33..=64 => 6,
65..=128 => 7,
129..=256 => 8,
_ => 16,
});
assert_eq!(c.bits(), *expected.last().unwrap(), "after {n} values");
}
assert!(c.is_global());
for n in 1..=300u32 {
assert_eq!(c.get(n as usize), n);
}
let mut b = PalettedContainer::new(BIOMES, 3);
for n in 1..=9u32 {
b.set(n as usize, n + 3);
let want = match n + 1 {
2 => 1,
3..=4 => 2,
5..=8 => 3,
_ => 7,
};
assert_eq!(b.bits(), want, "after {n} biomes");
}
}
#[test]
fn matches_a_plain_array_under_random_edits() {
for (strategy, distinct) in [(BLOCKS, 700u64), (BLOCKS, 12), (BIOMES, 9), (BIOMES, 3)] {
let entries = strategy.entries();
let mut state = 0x1234_5678_9ABC_DEF1u64;
let mut c = PalettedContainer::new(strategy, 5);
let mut plain = vec![5u32; entries];
for _ in 0..20_000 {
let r = rng(&mut state);
let i = (r % entries as u64) as usize;
let v = ((r >> 20) % distinct) as u32 * 7;
assert_eq!(c.set(i, v), plain[i]);
plain[i] = v;
}
let mut out = vec![0; entries];
c.unpack(&mut out);
assert_eq!(out, plain);
for (i, &v) in plain.iter().enumerate() {
assert_eq!(c.get(i), v);
}
let packed = c.pack();
let loaded = PalettedContainer::from_packed(strategy, packed.palette.clone(), packed.data.as_deref())
.unwrap();
assert_eq!(loaded, c);
assert_eq!(loaded.pack(), packed);
let again = PalettedContainer::from_values(strategy, &plain);
assert_eq!(again, c);
assert_eq!(again.pack(), packed);
let (bits, palette, words) = c.to_wire();
let wire = PalettedContainer::from_wire(strategy, bits, palette, words.to_vec()).unwrap();
assert_eq!(wire, c);
}
}
#[test]
fn uniform_detection_ignores_stale_palette_entries() {
let mut c = PalettedContainer::new(BLOCKS, 0);
c.set(10, 3);
assert!(!c.is_uniform(0));
c.set(10, 0);
assert!(c.palette_len() == 2 && c.is_uniform(0) && c.uniform() == Some(0));
assert!(!c.is_uniform(3));
for i in 0..4096 {
c.set(i, 3);
}
assert_eq!(c.uniform(), Some(3));
}
#[test]
fn rejects_malformed_wire_data() {
assert!(PalettedContainer::from_wire(BLOCKS, 4, vec![1, 2], vec![0; 255]).is_err());
assert!(PalettedContainer::from_wire(BLOCKS, 4, vec![1, 2], vec![0x3333; 256]).is_err());
assert!(PalettedContainer::from_wire(BLOCKS, 4, vec![], vec![0; 256]).is_err());
assert!(PalettedContainer::from_wire(BLOCKS, 0, vec![1, 2], vec![]).is_err());
assert!(PalettedContainer::from_wire(BIOMES, 2, vec![1, 2, 3, 4, 5], vec![0; 4]).is_err());
assert!(PalettedContainer::from_wire(BLOCKS, 0, vec![9], vec![]).is_ok());
}
#[test]
fn division_magic_is_exact() {
for bits in 1..=32u8 {
let per = 64 / usize::from(bits);
for i in 0..4096usize {
assert_eq!(locate(bits, i).0, i / per, "bits {bits} index {i}");
}
}
}
}
@@ -0,0 +1,103 @@
package dev.horizonlab.parity;
import com.google.gson.*;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.nio.file.*;
import java.util.*;
import java.util.stream.LongStream;
import net.minecraft.core.IdMap;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.level.chunk.PalettedContainer;
import net.minecraft.world.level.chunk.PalettedContainerRO;
import net.minecraft.world.level.chunk.Strategy;
/** Runs the real PalettedContainer (single-value, linear, hash-map and global palettes, resize,
* network write/read, pack/unpack) over scripted edits and records hashes of everything
* observable: set results, the serialized container after every few edits, the saved form and
* the containers read back from both. */
public final class VanillaPalettedContainerOracle {
private static final Gson JSON = new GsonBuilder().serializeNulls().create();
/** The palettes compare entries by identity, as registry objects allow: intern the boxes. */
private static final Map<Integer, Integer> INTERNED = new HashMap<>();
private static Integer intern(int value) { return INTERNED.computeIfAbsent(value, v -> new Integer(v)); }
private static final class Ids implements IdMap<Integer> {
final int size;
Ids(int size) { this.size = size; }
public int getId(Integer value) { return value < size ? value : -1; }
public Integer byId(int id) { return id >= 0 && id < size ? intern(id) : null; }
public int size() { return size; }
public Iterator<Integer> iterator() { return java.util.stream.IntStream.range(0, size).boxed().iterator(); }
}
private static long fnv(long h, int b) { return (h ^ (b & 0xFF)) * 0x100000001b3L; }
private static final long BASIS = 0xcbf29ce484222325L;
private static long hashBytes(byte[] bytes) {
long h = BASIS;
for (byte b : bytes) h = fnv(h, b);
return h;
}
private static long hashValues(PalettedContainer<Integer> c, int bits) {
long h = BASIS;
int n = 1 << (3 * bits);
for (int i = 0; i < n; i++) {
int v = c.get(i & (n - 1) & ((1 << bits) - 1), i >> (2 * bits), (i >> bits) & ((1 << bits) - 1));
for (int k = 0; k < 4; k++) h = fnv(h, v >> (8 * k));
}
return h;
}
private static byte[] write(PalettedContainer<Integer> c) {
var buffer = new FriendlyByteBuf(Unpooled.buffer());
c.write(buffer);
byte[] bytes = new byte[buffer.readableBytes()];
buffer.readBytes(bytes);
return bytes;
}
private static String wire(PalettedContainer<Integer> c) {
byte[] bytes = write(c);
return bytes.length + ":" + hashBytes(bytes);
}
/** Input line: name kind initial every i v i v ...; output: one line of fields. */
private static String scenario(String line) {
var t = line.trim().split(" ");
boolean blocks = t[1].equals("blocks");
int bits = blocks ? 4 : 2;
Strategy<Integer> strategy = blocks ? Strategy.createForBlockStates(new Ids(35723)) : Strategy.createForBiomes(new Ids(67));
var c = new PalettedContainer<>(intern(Integer.parseInt(t[2])), strategy);
int every = Integer.parseInt(t[3]);
var checkpoints = new StringJoiner(",");
long oldsHash = BASIS;
int n = 0, mask = (1 << bits) - 1;
for (int k = 4; k + 1 < t.length; k += 2) {
int i = Integer.parseInt(t[k]);
int old = c.getAndSetUnchecked(i & mask, i >> (2 * bits), (i >> bits) & mask, intern(Integer.parseInt(t[k + 1])));
for (int j = 0; j < 4; j++) oldsHash = fnv(oldsHash, old >> (8 * j));
if (++n % every == 0) checkpoints.add(wire(c));
}
var out = new StringJoiner(" ");
out.add(t[0]).add(Long.toString(oldsHash)).add("cp=" + checkpoints).add("bits=" + c.bitsPerEntry());
out.add("final=" + wire(c)).add("values=" + hashValues(c, bits));
var read = new PalettedContainer<>(intern(0), strategy);
read.read(new FriendlyByteBuf(Unpooled.wrappedBuffer(write(c))));
out.add("read=" + hashValues(read, bits) + "/" + wire(read));
var packed = c.pack(strategy);
long[] storage = packed.storage().map(LongStream::toArray).orElse(null);
long storageHash = BASIS;
if (storage != null) for (long word : storage) for (int j = 0; j < 8; j++) storageHash = fnv(storageHash, (int) (word >> (8 * j)));
var palette = new StringJoiner(",");
for (int v : packed.paletteEntries()) palette.add(Integer.toString(v));
out.add("packed=" + packed.bitsPerEntry() + "/" + storageHash + "/" + palette);
var loaded = PalettedContainer.unpack(strategy, new PalettedContainerRO.PackedData<>(packed.paletteEntries(),
Optional.ofNullable(storage).map(LongStream::of), -1)).getOrThrow();
out.add("loaded=" + hashValues(loaded, bits) + "/" + wire(loaded));
return out.toString();
}
public static void main(String[] args) throws Exception {
net.minecraft.SharedConstants.tryDetectVersion();
try (var input = Files.newBufferedReader(Path.of(args[0])); var output = Files.newBufferedWriter(Path.of(args[1]))) {
for (String line; (line = input.readLine()) != null; ) {
output.write(scenario(line));
output.newLine();
}
}
}
}
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Compare Horizon's resident PalettedContainer with the client's own.
Scripted edits run through vanilla's PalettedContainer (block-state and biome
strategies: single-value, linear, hash-map and global palettes, resizing, network
write and read, pack and unpack) and through horizon_core::paletted. Both sides
print the set results, the serialized container after every few edits, the
container read back from the wire, the saved form (palette, bit width, storage)
and the container loaded from it, as hashes.
"""
import argparse
import json
import os
from pathlib import Path
import platform
import random
import subprocess
try:
from . import lab, test_rust_parity as base
except ImportError:
import lab
import test_rust_parity as base
BUILD = lab.ROOT / 'target/paletted-container-parity'
LIMITS = {'blocks': (4096, 35723), 'biomes': (64, 67)}
def corpus():
rng = random.Random(0x50414C45545445)
counts = [1, 2, 3, 4, 5, 8, 9, 15, 16, 17, 31, 32, 33, 64, 100, 128, 129, 200, 256, 257, 300, 700, 2000]
for kind, (entries, ids) in LIMITS.items():
for count in counts:
if kind == 'biomes' and count > 70:
continue
for trial in range(6):
pool = rng.sample(range(ids), min(count, ids))
initial = rng.choice(pool) if trial % 2 else rng.randrange(ids)
ops = []
for _ in range(rng.choice([entries // 4, entries, entries * 3])):
ops.append((rng.randrange(entries), rng.choice(pool)))
if trial == 5:
# Every position written once, in order, then half of them reset.
ops = [(i, pool[i % len(pool)]) for i in range(entries)] + [(i, initial) for i in range(0, entries, 2)]
yield f'{kind}_{count}_{trial}', kind, initial, rng.choice([1, 7, 37, 211]), ops
# A few edits at a time: the palette grows one value at a time from a single value.
for trial in range(20):
ops = [(rng.randrange(entries), rng.randrange(ids)) for _ in range(rng.randrange(1, 40))]
yield f'{kind}_sparse_{trial}', kind, rng.randrange(ids), 1, ops
# Overwriting with the value already there, and filling a section with one value.
yield f'{kind}_fill', kind, 3, 500, [(i, 9) for i in range(entries)] + [(i, 3) for i in range(entries)]
yield f'{kind}_same', kind, 3, 1, [(i, 3) for i in range(0, entries, 5)]
def main():
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.parse_args()
lock, jars, libraries = base.verify_artifacts()
BUILD.mkdir(parents=True, exist_ok=True)
oracle = lab.ROOT / 'mods/horizon/src/test/java/dev/horizonlab/parity/VanillaPalettedContainerOracle.java'
sources = [Path(__file__), oracle, lab.ROOT / 'tools/lab.py', lab.ROOT / 'tools/test_rust_parity.py', lab.ROOT / 'Cargo.toml', lab.ROOT / 'Cargo.lock',
lab.ROOT / 'crates/horizon-core/Cargo.toml', lab.ROOT / 'crates/horizon-core/examples/paletted_fixture.rs',
*sorted((lab.ROOT / 'crates/horizon-core/src').rglob('*.rs'))]
tested = base.source_hashes(sources)
scenarios = list(corpus())
inputs = BUILD / 'inputs.txt'
inputs.write_text(''.join(f'{name} {kind} {initial} {every} ' + ' '.join(f'{i} {v}' for i, v in ops) + '\n'
for name, kind, initial, every, ops in scenarios))
def run(command, name):
with (BUILD / (name + '.log')).open('w') as log:
base.run(list(map(str, command)), stdout=log, stderr=subprocess.STDOUT)
classes = BUILD / 'classes'
classes.mkdir(exist_ok=True)
dependencies = os.pathsep.join(map(str, [jars['client'], *libraries]))
run(['javac', '--release', lock['minecraft']['java_major'], '-proc:none', '-cp', dependencies, '-d', classes, oracle], 'compile-java')
run(['cargo', 'build', '--locked', '--profile', 'fast', '-j', os.environ.get('CARGO_BUILD_JOBS', '3'), '-p', 'horizon-core', '--example', 'paletted_fixture'], 'compile-rust')
metadata = json.loads(subprocess.check_output(['cargo', 'metadata', '--locked', '--no-deps', '--format-version', '1'], cwd=lab.ROOT))
binary = Path(metadata['target_directory']) / 'fast/examples/paletted_fixture'
run([binary, inputs, BUILD / 'rust.txt'], 'rust')
run(['java', '-Xmx1G', '-cp', str(classes) + os.pathsep + dependencies, 'dev.horizonlab.parity.VanillaPalettedContainerOracle', inputs, BUILD / 'client.txt'], 'client')
client = (BUILD / 'client.txt').read_text().splitlines()
rust = (BUILD / 'rust.txt').read_text().splitlines()
assert len(client) == len(rust) == len(scenarios), (len(client), len(rust), len(scenarios))
bad = [(a, b) for a, b in zip(client, rust) if a != b]
base.ensure_sources_unchanged(sources, tested)
for a, b in bad[:5]:
print('client', a[:600])
print('rust ', b[:600])
if bad:
raise SystemExit(f'{len(bad)} of {len(scenarios)} scenarios differ')
edits = sum(len(s[4]) for s in scenarios)
report = {'schema': 1, 'minecraft': lock['minecraft']['version'], 'platform': platform.platform(), 'scenarios': len(scenarios), 'edits': edits,
'sources_sha256': tested}
lab.write_json(BUILD / 'report.json', report)
print(f'PalettedContainer parity: {len(scenarios)} scenarios, {edits:,} edits, all hashes identical')
if __name__ == '__main__':
main()