Skip to content

[Batch 2] Chunk serialization/deserialization #377

@MichaelFisher1997

Description

@MichaelFisher1997

Summary

Implement serialization and deserialization of chunk data (blocks, light, biomes) to a compact binary format. This is the data layer that sits between Chunk structs and the region file API from #372.

Depends on: #372 (region file format API)

Current State

Chunk data lives purely in memory:

  • Chunk.blocks: [CHUNK_SIZE_X][CHUNK_SIZE_Y][CHUNK_SIZE_Z]BlockType — 16×256×16 × 1 byte = 64KB
  • Chunk.sky_light / Chunk.block_light: packed light arrays
  • BiomeId per column (or per-block in some paths)
  • No serialization format exists

Serialization Format

[ChunkHeader: 8 bytes]
  magic:      u32  = 0x5A434B00 ("ZCK\0")
  version:    u8   = 1
  flags:      u8   = bitfield (has_block_light, has_biome_data, has_heightmap)

[BlockData: 4096 bytes]
  16×16×16 block data per subchunk, NUM_SUBCHUNKS sections
  BlockType stored as u8 (enum value)
  Optional: RLE encoding for sparse subchunks (flag in header)

[LightData: variable]
  If has_block_light:
    Sky light: 16×16×16 packed nibbles per subchunk (2048 bytes total)
    Block light: same format (RGB packed into 2 nibbles per block, or separate R/G/B)

[BiomeData: 256 bytes]
  16×16 BiomeId (u8) per column

[HeightMap: 256 bytes] (optional)
  16×16 u8 height values for quick surface lookup

Total uncompressed: ~64-70KB per chunk. With RLE on block data (many chunks are all-stone or all-air below/above surface), typical size: 2-20KB.

Implementation Plan

Step 1: Define wire format

  • Create src/world/persistence/chunk_serializer.zig
  • Define ChunkHeader, ChunkDataVersion constants
  • Version field allows future format changes without breaking old saves

Step 2: Serialize

pub fn serializeChunk(chunk: *const Chunk, allocator: Allocator) ![]u8
  • Write header with flags based on what data is present
  • Write block data (subchunk by subchunk)
  • Write light data if non-zero
  • Write biome data
  • Return owned slice (caller frees)

Step 3: Deserialize

pub fn deserializeChunk(data: []const u8, chunk: *Chunk) !void
  • Validate magic and version
  • Read block data into chunk.blocks array
  • Read light data if present (else default to full skylight)
  • Read biome data if present
  • Return error on corrupt/unknown version

Step 4: RLE optimization (optional, can be follow-up)

  • For subchunks that are >80% single block type, use RLE: [count, blockType] pairs
  • Flag in header indicates which subchunks use RLE
  • Significant space savings for underground chunks

Files to Create

  • src/world/persistence/chunk_serializer.zig

Testing

  • Round-trip: serialize chunk → deserialize → verify every block matches
  • Round-trip: light data preserved correctly
  • Round-trip: biome data preserved correctly
  • Empty chunk (all air) serializes to minimal size
  • Full chunk (all stone) serializes efficiently
  • Corrupt data returns error, doesn't crash
  • Unknown version returns error
  • Integration: serialize → write to region file → read → deserialize → verify

Roadmap: docs/PERFORMANCE_ROADMAP.md — Batch 2, Issue 1B-2

Metadata

Metadata

Assignees

No one assigned

    Labels

    batch-2Batch 2: GPU Culling + CompressionbugSomething isn't workingdocumentationImprovements or additions to documentationenhancementNew feature or requesthotfixworld/persistenceWorld save/load system

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions