From 3b1bbbc02ce9797cc387137bb78dc5d63c8a86a9 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 25 Aug 2026 21:47:13 +0200 Subject: [PATCH] feat(core): add chunk load/unload radius streaming --- core/include/vf/Streaming.h | 59 +++++++++++++++++++++++++++++++++++++ core/src/Streaming.cpp | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 core/include/vf/Streaming.h create mode 100644 core/src/Streaming.cpp diff --git a/core/include/vf/Streaming.h b/core/include/vf/Streaming.h new file mode 100644 index 0000000..15aad4d --- /dev/null +++ b/core/include/vf/Streaming.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include + +namespace vf { + +// A chunk column address (x, z); a column owns the full vertical stack of chunks. +struct ColumnCoord { + int x = 0; + int z = 0; + bool operator==(const ColumnCoord& o) const { return x == o.x && z == o.z; } +}; + +struct ColumnCoordHash { + std::size_t operator()(const ColumnCoord& 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); + } +}; + +struct StreamingConfig { + int loadRadius = 8; // columns within this circle are kept resident + int unloadRadius = 10; // columns are only evicted past this larger circle +}; + +struct StreamDelta { + std::vector load; // to generate, nearest-to-player first + std::vector unload; // now beyond the unload ring +}; + +// Tracks which chunk columns are resident around a moving centre. The load and +// unload rings differ (hysteresis) so a player pacing across a boundary does not +// thrash the same columns in and out every step. +class ChunkStreamer { +public: + explicit ChunkStreamer(StreamingConfig config = {}) : config(config) {} + + // Recomputes residency around `centre`, mutating the resident set and + // returning the columns that entered (to load) and left (to unload). + StreamDelta update(ColumnCoord centre); + + bool isLoaded(ColumnCoord column) const { return resident.count(column) != 0; } + std::size_t loadedCount() const { return resident.size(); } + const StreamingConfig& settings() const { return config; } + +private: + StreamingConfig config; + std::unordered_set resident; +}; + +// Columns whose centre lies within `radius` of (0,0), sorted nearest first. +// Distance is Euclidean so the loaded area is a disc, not a square. +std::vector columnsInRadius(ColumnCoord centre, int radius); + +} // namespace vf diff --git a/core/src/Streaming.cpp b/core/src/Streaming.cpp new file mode 100644 index 0000000..4cfc766 --- /dev/null +++ b/core/src/Streaming.cpp @@ -0,0 +1,51 @@ +#include "vf/Streaming.h" + +#include + +namespace vf { + +namespace { + +long long distanceSquared(ColumnCoord a, ColumnCoord b) { + long long dx = a.x - b.x; + long long dz = a.z - b.z; + return dx * dx + dz * dz; +} + +} // namespace + +std::vector columnsInRadius(ColumnCoord centre, int radius) { + std::vector columns; + long long limit = static_cast(radius) * radius; + for (int dz = -radius; dz <= radius; ++dz) { + for (int dx = -radius; dx <= radius; ++dx) { + ColumnCoord column{centre.x + dx, centre.z + dz}; + if (distanceSquared(column, centre) <= limit) columns.push_back(column); + } + } + std::stable_sort(columns.begin(), columns.end(), + [&](ColumnCoord lhs, ColumnCoord rhs) { + return distanceSquared(lhs, centre) < distanceSquared(rhs, centre); + }); + return columns; +} + +StreamDelta ChunkStreamer::update(ColumnCoord centre) { + StreamDelta delta; + + for (ColumnCoord column : columnsInRadius(centre, config.loadRadius)) { + if (resident.count(column) == 0) delta.load.push_back(column); + } + + long long unloadLimit = static_cast(config.unloadRadius) * config.unloadRadius; + for (ColumnCoord column : resident) { + if (distanceSquared(column, centre) > unloadLimit) delta.unload.push_back(column); + } + + for (ColumnCoord column : delta.load) resident.insert(column); + for (ColumnCoord column : delta.unload) resident.erase(column); + + return delta; +} + +} // namespace vf