Chunk
A 10 × 10 × 10 block cube stored as a local palette plus bit-packed palette indices, with optional sparse per-block metadata.
Location
| Header | include/GalaxyEggbert/Worlds/Chunk.hpp (also declares ChunkBlockMetadataRecord) |
|---|---|
| Source | src/GalaxyEggbert/Worlds/Chunk.cpp |
| Bit packing | include/GalaxyEggbert/Worlds/BitPacking.hpp, src/GalaxyEggbert/Worlds/BitPacking.cpp |
| Namespace | GalaxyEggbert::Worlds |
Purpose
A World is 1000 chunks. A chunk does not store 1000 raw 16-bit blocks; it stores the distinct block values it contains (its palette, up to 256 entries) and, for each cell, the index of its value in that palette, packed with as few bits as the palette size needs. Handcrafted worlds have few distinct blocks per 10³ region, so most chunks pack into 1-3 bits per cell. A chunk holds no mesh; meshes are runtime caches built by the TerrainRenderer.
The chunk also owns the sparse extra metadata of its cells: a list of
ChunkBlockMetadataRecords, each a local cell index, a 16-bit type and a byte payload.
This is where objects, BigDecor, tile turns, hint zones and player starts live in a saved world.
Palette and adaptive bit packing
- A new chunk is all air: palette
{Air}, 1 bit per cell, 16 packed 64-bit words. setBlock()looks the value up in the palette and appends it if missing. When the palette grows past a power of two (2 → 3, 4 → 5, ... 128 → 129) every cell is re-packed at the wider width.- The width is
bitsNeededForPalette(n): 1 bit for 1-2 entries, 2 for 3-4, 3 for 5-8, ... 8 for 129-256. - Cell
ioccupies bits[i * bits, (i + 1) * bits)of a little-endian stream ofuint64_twords; a value may straddle two words. The word count isceil(1000 * bits / 64). - The palette never shrinks during editing: a value that is no longer used stays in the palette (and is saved) until the chunk is rebuilt with
fromBlocks(). - A 257th distinct value throws
std::runtime_error("Chunk palette is full"). The header comments saystd::out_of_range; the code throwsruntime_error.
Local cell index, shared by packing and metadata:
linearIndex(x, y, z) = x + y * 10 + z * 100 // 0..999
Public API
Verified in Chunk.hpp as of 2026-11-10.
| Member | Description |
|---|---|
Size = 10, Volume = 1000 | From VoxelConfig. |
Chunk() | All-air, uniform, clean. |
static createUniform(Block) | Every cell the same value; one-entry palette. |
static fromBlocks(const std::array<Block, Volume>&) | Builds a minimal palette from dense data. |
getBlock(lx, ly, lz) / setBlock(lx, ly, lz, Block) | Local coordinates 0-9; std::out_of_range outside. setBlock marks the chunk dirty. |
unpackBlocks() | All 1000 values in linearIndex order. |
isEmpty() | All air and no extra metadata. World::saveToFile() skips empty chunks, so an object anchored in an otherwise empty chunk is still saved (a bug fixed on 2026-07-09). |
isUniform() | Palette has one entry. |
isDirty() / clearDirty() | Runtime-only flag set by every mutation; not saved. |
palette(), packedIndices(), bitsPerBlock() | Read-only views of the storage. |
extraMetadata() | The sparse records. |
setExtraMetadata(localIndex, type, payload) | Add or replace (one record per cell and type). Payload over 65 535 bytes throws. |
removeExtraMetadata(localIndex, type) | Returns true if removed. |
clearExtraMetadata() | Drops all records. |
static linearIndex(lx, ly, lz) | Public so World can address a cell's metadata. |
write(std::ostream&) / static read(std::istream&) | The VCH1 chunk payload (always written as chunk version 2; version 1 is still readable). |
ChunkBlockMetadataRecord has localBlockIndex (uint32_t), metadataType (uint16_t) and payload (std::vector<std::uint8_t>), and compares with ==.
Serialized form
A chunk payload is a 24-byte header (VCH1, version 2, chunk size, bits per block,
flags, palette count, block count, packed data size, metadata size), the palette as raw
uint16_t values, the packed words, and, when flag bit 0 is set, a BMD1
metadata section. read() validates every field (bits must match the palette size,
word count must match, no trailing bytes in the metadata section). Byte tables are on
The .vwr Format.
Usage
Game code normally goes through World, which splits world coordinates into chunk and local ones:
// src/GalaxyEggbert/Worlds/World.cpp, World::setBlock()
auto& target = chunk(chunkX, chunkY, chunkZ);
const auto oldRaw = target.getBlock(localX, localY, localZ).rawValue();
target.setBlock(localX, localY, localZ, blockValue);
Direct use appears in the tests and in round-trips through a stream:
Chunk chunk;
chunk.setBlock(1, 2, 3, Block::make(10));
std::ostringstream out(std::ios::binary);
chunk.write(out);
std::istringstream in(out.str(), std::ios::binary);
Chunk copy = Chunk::read(in);
Tests
tests/GalaxyEggbert/Worlds/ChunkTests.cpp: default state, uniform chunks, boundary cells, the dirty flag, coordinate validation,PaletteGrowthUpdatesBitWidth,FromBlocksRoundTripPreservesDenseData,AllAirChunkWithExtraMetadataIsNotEmpty, metadata set/replace/remove, and serialization (version 2 header, metadata round-trip, reading a legacy version 1 payload, rejecting bad magic, palette count, bit width, metadata flag/size mismatch, badBMD1magic, out-of-range metadata index).tests/GalaxyEggbert/Worlds/BitPackingTests.cpp: width boundaries, round-trips across word boundaries, argument validation.- Indirectly: every world test and
VerifyShippedWorlds.