From 5a9a2bf96bdac7f93d2fa177a8e717ab338adc59 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Tue, 1 Sep 2026 20:54:56 +0200 Subject: [PATCH] feat(vf): add ChunkTask and ChunkTaskResult --- core/include/vf/ChunkPipeline.h | 159 ++++++++++++++++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 core/include/vf/ChunkPipeline.h diff --git a/core/include/vf/ChunkPipeline.h b/core/include/vf/ChunkPipeline.h new file mode 100644 index 0000000..d9ae770 --- /dev/null +++ b/core/include/vf/ChunkPipeline.h @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "vf/ChunkCache.h" +#include "vf/JobSystem.h" +#include "vf/Lighting.h" +#include "vf/LitMesh.h" +#include "vf/Math.h" +#include "vf/Mesh.h" +#include "vf/World.h" +#include "vf/WorldGen.h" + +namespace vf { + +enum class ChunkStage : std::uint8_t { + Generate = 0, + Light = 1, + Mesh = 2, + Ready = 3, +}; + +constexpr int chunkStageCount = 4; + +const char* chunkStageName(ChunkStage stage); + +struct ChunkTask { + Vec3i coord; + ChunkStage stage = ChunkStage::Generate; + JobPriority priority = JobPriority::Normal; + long long distanceSq = 0; + bool inRange = true; +}; + +struct ChunkTaskResult { + Vec3i coord; + ChunkStage stage = ChunkStage::Generate; + bool cancelled = false; + Chunk chunk; + LightField light; + ChunkMesh mesh; + std::uint64_t entryBytes = 0; + std::uint64_t stageNanos[chunkStageCount] = {0, 0, 0, 0}; +}; + +struct PipelineConfig { + unsigned threadCount = 0; + int viewRadius = 2; // chunk columns kept around the observer + int verticalChunks = 2; // chunk layers stacked upwards from y = 0 + std::uint64_t cacheBudgetBytes = 256ull * 1024ull * 1024ull; + CachePolicy cachePolicy = CachePolicy::DistanceWeighted; + WorldGenParams gen; +}; + +struct PipelineStats { + std::array stageCompleted{}; + std::array stageNanos{}; + std::uint64_t cancelled = 0; + std::uint64_t chunksReady = 0; + std::uint64_t cachedBytes = 0; + + double averageStageMs(ChunkStage stage) const; +}; + +// Chunk production line running on a JobSystem. +// +// Every queued chunk becomes four graph nodes: Generate fills its blocks, Light +// floods a box one voxel wider than the chunk on each side, Mesh bakes the lit +// and ambient-occluded geometry, Ready snapshots the entry for the cache. Light +// and Mesh both read across the chunk border, so their nodes depend on the +// Generate nodes of every one of the 26 neighbours that is part of the same run. +// +// The output is order independent by construction: workers only ever write into +// their own preallocated slot, and the slots are folded back in chunk +// coordinate order once the graph has drained. The same configuration with one +// worker and with eight therefore produces the same bytes, which digest() makes +// directly comparable. +class ChunkPipeline { +public: + explicit ChunkPipeline(const PipelineConfig& config = {}); + + ChunkPipeline(const ChunkPipeline&) = delete; + ChunkPipeline& operator=(const ChunkPipeline&) = delete; + + const PipelineConfig& settings() const { return config; } + JobSystem& jobSystem() { return jobs; } + ChunkCache& cache() { return chunkStore; } + const ChunkCache& cache() const { return chunkStore; } + const World& worldView() const { return world; } + const WorldGen& worldGenerator() const { return generator; } + + void setObserver(const Vec3i& chunkCoord); + Vec3i observer() const; + bool inViewVolume(const Vec3i& chunkCoord) const; + + void enqueue(const Vec3i& chunkCoord); + std::size_t enqueueViewVolume(); + std::size_t pendingCount() const; + + // Marks a chunk cancelled. Every stage tests the flag as it starts, so a + // chunk that leaves the view radius stops at the next stage boundary and + // never reaches the result list or the cache. + void cancel(const Vec3i& chunkCoord); + bool isCancelled(const Vec3i& chunkCoord) const; + + // Distance ordered, nearest first, each tagged with its service class. + std::vector pendingTasks() const; + + // Drains the pending queue through all four stages and folds the results in. + void run(); + + const std::vector& results() const { return completedResults; } + const ChunkTaskResult* resultFor(const Vec3i& chunkCoord) const; + + PipelineStats stats() const; + void resetStats(); + + // Deterministic byte image of the world, the merged results and the cache + // footprint. Two runs that agree here agree on every produced byte. + std::vector digest() const; + +private: + long long distanceSqLocked(const Vec3i& chunkCoord) const; + bool inViewVolumeLocked(const Vec3i& chunkCoord) const; + JobPriority priorityFor(long long distanceSq) const; + + void stageGenerate(ChunkTaskResult& slot, Chunk* target, std::atomic& cancelled); + void stageLight(ChunkTaskResult& slot, std::atomic& cancelled); + void stageMesh(ChunkTaskResult& slot, std::atomic& cancelled); + void stageReady(ChunkTaskResult& slot, const Chunk* source, std::atomic& cancelled); + + PipelineConfig config; + WorldGen generator; + World world; + ChunkCache chunkStore; + JobSystem jobs; + + mutable std::mutex controlMutex; + Vec3i observerChunk; + std::vector pending; + std::unordered_set pendingSet; + std::unordered_set cancelledCoords; + std::unordered_map activeIndex; + std::vector>> cancelFlags; + + std::vector completedResults; + std::unordered_map resultIndex; + PipelineStats counters; +}; + +} // namespace vf