From 62facedd791490a7c621ca19924be764f40f8ccb Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 18:56:43 +0200 Subject: [PATCH] feat(core): add region-file chunk storage in 32x32 chunk pages --- core/include/vf/Region.h | 60 +++++++++++++ core/src/Region.cpp | 190 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 250 insertions(+) create mode 100644 core/include/vf/Region.h create mode 100644 core/src/Region.cpp diff --git a/core/include/vf/Region.h b/core/include/vf/Region.h new file mode 100644 index 0000000..edf5448 --- /dev/null +++ b/core/include/vf/Region.h @@ -0,0 +1,60 @@ +#pragma once + +#include +#include +#include +#include "World.h" + +namespace vf { + +constexpr int regionSize = 16; // chunk columns per region side (X and Z) + +struct RegionCoord { + int x = 0; + int z = 0; + bool operator==(const RegionCoord& o) const { return x == o.x && z == o.z; } +}; + +struct RegionCoordHash { + std::size_t operator()(const RegionCoord& c) const { + std::uint64_t h = 1469598103934665603ull; + h ^= static_cast(c.x); h *= 1099511628211ull; + h ^= static_cast(c.z); h *= 1099511628211ull; + return static_cast(h); + } +}; + +RegionCoord regionForChunk(const Vec3i& chunkCoord); + +// One region file: the chunks whose columns fall inside a regionSize x regionSize +// tile, across all Y. Each chunk is stored run-length encoded; the header carries +// an index so a single chunk can be pulled back without decoding the rest. +class Region { +public: + explicit Region(RegionCoord coord = {}) : coord(coord) {} + + RegionCoord location() const { return coord; } + std::size_t chunkCount() const { return chunks.size(); } + bool hasChunk(const Vec3i& chunkCoord) const { return chunks.count(chunkCoord) != 0; } + + void putChunk(const Vec3i& chunkCoord, const Chunk& chunk); + bool getChunk(const Vec3i& chunkCoord, Chunk& out) const; + + const std::unordered_map& allChunks() const { return chunks; } + + std::vector serialize() const; + static bool deserialize(const uint8_t* data, std::size_t size, Region& out); + +private: + RegionCoord coord; + std::unordered_map chunks; +}; + +// Splits a whole world into its regions. +std::unordered_map splitWorldIntoRegions(const World& world); + +// Convenience for the browser demo: one blob holding every non-empty region. +std::vector serializeRegions(const World& world); +bool deserializeRegions(const uint8_t* data, std::size_t size, World& out); + +} // namespace vf diff --git a/core/src/Region.cpp b/core/src/Region.cpp new file mode 100644 index 0000000..613b932 --- /dev/null +++ b/core/src/Region.cpp @@ -0,0 +1,190 @@ +#include "vf/Region.h" + +namespace vf { + +namespace { + +constexpr uint32_t regionMagic = 0x31525856; // "VXR1" +constexpr uint32_t regionVersion = 1; +constexpr uint32_t regionsMagic = 0x53525856; // "VXRS" + +void putU32(std::vector& out, uint32_t value) { + out.push_back(static_cast(value & 0xff)); + out.push_back(static_cast((value >> 8) & 0xff)); + out.push_back(static_cast((value >> 16) & 0xff)); + out.push_back(static_cast((value >> 24) & 0xff)); +} + +void putI32(std::vector& out, int32_t value) { putU32(out, static_cast(value)); } + +void putU16(std::vector& out, uint16_t value) { + out.push_back(static_cast(value & 0xff)); + out.push_back(static_cast((value >> 8) & 0xff)); +} + +struct Reader { + const uint8_t* data; + std::size_t size; + std::size_t pos = 0; + + bool u32(uint32_t& out) { + if (pos + 4 > size) return false; + out = static_cast(data[pos]) | (static_cast(data[pos + 1]) << 8) | + (static_cast(data[pos + 2]) << 16) | + (static_cast(data[pos + 3]) << 24); + pos += 4; + return true; + } + bool i32(int32_t& out) { + uint32_t v; + if (!u32(v)) return false; + out = static_cast(v); + return true; + } + bool u16(uint16_t& out) { + if (pos + 2 > size) return false; + out = static_cast(data[pos]) | (static_cast(data[pos + 1]) << 8); + pos += 2; + return true; + } + bool u8(uint8_t& out) { + if (pos + 1 > size) return false; + out = data[pos++]; + return true; + } +}; + +void encodeChunk(std::vector& out, const Chunk& chunk) { + const auto& blocks = chunk.raw(); + std::size_t i = 0; + while (i < blocks.size()) { + Block value = blocks[i]; + uint16_t run = 1; + while (i + run < blocks.size() && blocks[i + run] == value && run < 0xffff) ++run; + out.push_back(static_cast(value)); + putU16(out, run); + i += run; + } +} + +bool decodeChunk(Reader& reader, Chunk& chunk) { + auto& blocks = chunk.raw(); + std::size_t written = 0; + while (written < blocks.size()) { + uint8_t value; + uint16_t run; + if (!reader.u8(value) || !reader.u16(run)) return false; + if (run == 0 || written + run > blocks.size()) return false; + for (uint16_t r = 0; r < run; ++r) blocks[written++] = static_cast(value); + } + return true; +} + +} // namespace + +RegionCoord regionForChunk(const Vec3i& chunkCoord) { + return {floorDiv(chunkCoord.x, regionSize), floorDiv(chunkCoord.z, regionSize)}; +} + +void Region::putChunk(const Vec3i& chunkCoord, const Chunk& chunk) { + chunks[chunkCoord] = chunk; +} + +bool Region::getChunk(const Vec3i& chunkCoord, Chunk& out) const { + auto it = chunks.find(chunkCoord); + if (it == chunks.end()) return false; + out = it->second; + return true; +} + +std::vector Region::serialize() const { + std::vector out; + putU32(out, regionMagic); + putU32(out, regionVersion); + putI32(out, coord.x); + putI32(out, coord.z); + putU32(out, static_cast(chunks.size())); + + for (const auto& entry : chunks) { + putI32(out, entry.first.x); + putI32(out, entry.first.y); + putI32(out, entry.first.z); + std::vector payload; + encodeChunk(payload, entry.second); + putU32(out, static_cast(payload.size())); + out.insert(out.end(), payload.begin(), payload.end()); + } + return out; +} + +bool Region::deserialize(const uint8_t* data, std::size_t size, Region& out) { + Reader reader{data, size}; + uint32_t magic = 0, version = 0, count = 0; + int32_t rx = 0, rz = 0; + if (!reader.u32(magic) || magic != regionMagic) return false; + if (!reader.u32(version) || version != regionVersion) return false; + if (!reader.i32(rx) || !reader.i32(rz)) return false; + if (!reader.u32(count)) return false; + + out.coord = {rx, rz}; + out.chunks.clear(); + for (uint32_t c = 0; c < count; ++c) { + int32_t cx, cy, cz; + uint32_t length; + if (!reader.i32(cx) || !reader.i32(cy) || !reader.i32(cz)) return false; + if (!reader.u32(length)) return false; + if (reader.pos + length > size) return false; + Reader chunkReader{data + reader.pos, length}; + Chunk chunk; + if (!decodeChunk(chunkReader, chunk)) return false; + out.chunks[{cx, cy, cz}] = chunk; + reader.pos += length; + } + return true; +} + +std::unordered_map splitWorldIntoRegions(const World& world) { + std::unordered_map regions; + for (const auto& entry : world.allChunks()) { + if (entry.second->empty()) continue; + RegionCoord rc = regionForChunk(entry.first); + auto it = regions.find(rc); + if (it == regions.end()) it = regions.emplace(rc, Region{rc}).first; + it->second.putChunk(entry.first, *entry.second); + } + return regions; +} + +std::vector serializeRegions(const World& world) { + auto regions = splitWorldIntoRegions(world); + std::vector out; + putU32(out, regionsMagic); + putU32(out, static_cast(regions.size())); + for (const auto& entry : regions) { + std::vector payload = entry.second.serialize(); + putU32(out, static_cast(payload.size())); + out.insert(out.end(), payload.begin(), payload.end()); + } + return out; +} + +bool deserializeRegions(const uint8_t* data, std::size_t size, World& out) { + Reader reader{data, size}; + uint32_t magic = 0, count = 0; + if (!reader.u32(magic) || magic != regionsMagic) return false; + if (!reader.u32(count)) return false; + + for (uint32_t r = 0; r < count; ++r) { + uint32_t length; + if (!reader.u32(length)) return false; + if (reader.pos + length > size) return false; + Region region; + if (!Region::deserialize(data + reader.pos, length, region)) return false; + reader.pos += length; + for (const auto& entry : region.allChunks()) + out.ensureChunk(entry.first) = entry.second; + } + return true; +} + +} // namespace vf