#pragma once #include #include #include #include #include #include "vf/Chunk.h" #include "vf/Math.h" #include "vf/Mesh.h" #include "vf/World.h" namespace vf { // Lru drops the entry untouched for the longest time. DistanceWeighted mixes // staleness with the squared chunk distance to the observer, so a chunk far // behind the player loses to a stale one still under his feet. enum class CachePolicy : std::uint8_t { Lru = 0, DistanceWeighted = 1, }; struct CacheEntry { Vec3i coord; Chunk chunk; ChunkMesh mesh; std::uint64_t bytes = 0; std::uint64_t lastUse = 0; long long distanceSq = 0; bool hasMesh = false; bool pinned = false; }; struct CacheStats { std::uint64_t hits = 0; std::uint64_t misses = 0; std::uint64_t evictions = 0; std::uint64_t invalidations = 0; std::uint64_t meshInvalidations = 0; std::uint64_t bytesUsed = 0; std::size_t entries = 0; double hitRate() const { std::uint64_t lookups = hits + misses; if (lookups == 0) return 0.0; return static_cast(hits) / static_cast(lookups); } }; // Resident chunks and their baked meshes under a hard byte budget. Entries are // evicted by policy once a store pushes the cache past the budget; pinned // entries are never candidates, which is how the pipeline keeps everything // inside the view radius alive. Every method is safe to call concurrently: // peek reads share the lock, everything that touches recency or the store takes // it exclusively. class ChunkCache { public: explicit ChunkCache(std::uint64_t byteBudget = 64ull * 1024ull * 1024ull, CachePolicy policy = CachePolicy::Lru); ChunkCache(const ChunkCache&) = delete; ChunkCache& operator=(const ChunkCache&) = delete; void setByteBudget(std::uint64_t bytes); void setPolicy(CachePolicy policy); void setDistanceWeight(std::uint64_t weight); void setObserver(const Vec3i& chunkCoord); std::uint64_t byteBudget() const; CachePolicy policy() const; Vec3i observer() const; void store(const Vec3i& coord, const Chunk& chunk); void store(const Vec3i& coord, const Chunk& chunk, const ChunkMesh& mesh); // Counting lookups: they record a hit or a miss and refresh recency. bool fetchChunk(const Vec3i& coord, Chunk& out); bool fetchMesh(const Vec3i& coord, ChunkMesh& out); // Silent lookups for the pipeline workers: no recency, no statistics, and // only a shared lock, so many stages can read at once. bool peekChunk(const Vec3i& coord, Chunk& out) const; bool peekMesh(const Vec3i& coord, ChunkMesh& out) const; bool contains(const Vec3i& coord) const; bool hasMesh(const Vec3i& coord) const; bool entryFor(const Vec3i& coord, CacheEntry& out) const; void pin(const Vec3i& coord); void unpin(const Vec3i& coord); bool isPinned(const Vec3i& coord) const; // Drops the owning chunk entirely and the meshes of every neighbour whose // ambient occlusion or light sampling can read the edited voxel across the // chunk border. Returns the number of neighbour meshes dropped. std::size_t invalidateBlock(int gx, int gy, int gz); void invalidateChunk(const Vec3i& coord); void invalidateMesh(const Vec3i& coord); void clear(); std::size_t size() const; std::uint64_t bytesUsed() const; CacheStats stats() const; void resetStats(); // Victims first. Pinned entries are excluded because they cannot be chosen. std::vector evictionOrder() const; std::vector residentCoords() const; static std::uint64_t entrySizeBytes(const Chunk& chunk, const ChunkMesh& mesh); private: long long distanceSqTo(const Vec3i& coord) const; std::uint64_t evictionScoreLocked(const CacheEntry& entry) const; void enforceBudgetLocked(); void touchLocked(CacheEntry& entry); void dropMeshLocked(CacheEntry& entry); void eraseLocked(const Vec3i& coord); mutable std::shared_mutex mutex; std::unordered_map entries; CacheStats counters; Vec3i observerChunk; std::uint64_t budget; std::uint64_t used = 0; std::uint64_t clock = 0; std::uint64_t distanceWeight = 64; CachePolicy replacement; }; } // namespace vf