#include "vf/ChunkPipeline.h" #include #include #include #include namespace vf { namespace { bool coordLess(const Vec3i& a, const Vec3i& b) { if (a.x != b.x) return a.x < b.x; if (a.y != b.y) return a.y < b.y; return a.z < b.z; } std::uint64_t nanosSince(const std::chrono::steady_clock::time_point& start) { auto elapsed = std::chrono::steady_clock::now() - start; return static_cast( std::chrono::duration_cast(elapsed).count()); } std::string coordLabel(const Vec3i& coord) { return "chunk(" + std::to_string(coord.x) + "," + std::to_string(coord.y) + "," + std::to_string(coord.z) + ")"; } } // namespace const char* chunkStageName(ChunkStage stage) { switch (stage) { case ChunkStage::Generate: return "generate"; case ChunkStage::Light: return "light"; case ChunkStage::Mesh: return "mesh"; case ChunkStage::Ready: return "ready"; } return "unknown"; } double PipelineStats::averageStageMs(ChunkStage stage) const { std::size_t index = static_cast(stage); if (stageCompleted[index] == 0) return 0.0; double total = static_cast(stageNanos[index]); return total / static_cast(stageCompleted[index]) / 1000000.0; } ChunkPipeline::ChunkPipeline(const PipelineConfig& config) : config(config), generator(config.gen), chunkStore(config.cacheBudgetBytes, config.cachePolicy), jobs(config.threadCount) {} void ChunkPipeline::setObserver(const Vec3i& chunkCoord) { { std::lock_guard lock(controlMutex); observerChunk = chunkCoord; } chunkStore.setObserver(chunkCoord); } Vec3i ChunkPipeline::observer() const { std::lock_guard lock(controlMutex); return observerChunk; } long long ChunkPipeline::distanceSqLocked(const Vec3i& chunkCoord) const { long long dx = chunkCoord.x - observerChunk.x; long long dy = chunkCoord.y - observerChunk.y; long long dz = chunkCoord.z - observerChunk.z; return dx * dx + dy * dy + dz * dz; } bool ChunkPipeline::inViewVolumeLocked(const Vec3i& chunkCoord) const { if (chunkCoord.y < 0 || chunkCoord.y >= config.verticalChunks) return false; long long dx = chunkCoord.x - observerChunk.x; long long dz = chunkCoord.z - observerChunk.z; long long radius = config.viewRadius; return dx * dx + dz * dz <= radius * radius; } bool ChunkPipeline::inViewVolume(const Vec3i& chunkCoord) const { std::lock_guard lock(controlMutex); return inViewVolumeLocked(chunkCoord); } JobPriority ChunkPipeline::priorityFor(long long distanceSq) const { long long radius = config.viewRadius; long long radiusSq = radius * radius; if (distanceSq * 9 <= radiusSq) return JobPriority::High; if (distanceSq * 9 <= radiusSq * 4) return JobPriority::Normal; return JobPriority::Low; } void ChunkPipeline::enqueue(const Vec3i& chunkCoord) { std::lock_guard lock(controlMutex); if (!pendingSet.insert(chunkCoord).second) return; pending.push_back(chunkCoord); } std::size_t ChunkPipeline::enqueueViewVolume() { std::vector volume; { std::lock_guard lock(controlMutex); int radius = config.viewRadius; for (int dx = -radius; dx <= radius; ++dx) { for (int dz = -radius; dz <= radius; ++dz) { for (int y = 0; y < config.verticalChunks; ++y) { Vec3i coord{observerChunk.x + dx, y, observerChunk.z + dz}; if (inViewVolumeLocked(coord)) volume.push_back(coord); } } } } std::sort(volume.begin(), volume.end(), coordLess); std::size_t added = 0; for (const Vec3i& coord : volume) { std::lock_guard lock(controlMutex); if (!pendingSet.insert(coord).second) continue; pending.push_back(coord); ++added; } return added; } std::size_t ChunkPipeline::pendingCount() const { std::lock_guard lock(controlMutex); return pending.size(); } void ChunkPipeline::cancel(const Vec3i& chunkCoord) { std::lock_guard lock(controlMutex); cancelledCoords.insert(chunkCoord); auto active = activeIndex.find(chunkCoord); if (active != activeIndex.end()) cancelFlags[active->second]->store(true, std::memory_order_release); } bool ChunkPipeline::isCancelled(const Vec3i& chunkCoord) const { std::lock_guard lock(controlMutex); return cancelledCoords.count(chunkCoord) != 0; } std::vector ChunkPipeline::pendingTasks() const { std::lock_guard lock(controlMutex); std::vector tasks; tasks.reserve(pending.size()); for (const Vec3i& coord : pending) { ChunkTask task; task.coord = coord; task.stage = ChunkStage::Generate; task.distanceSq = distanceSqLocked(coord); task.priority = priorityFor(task.distanceSq); task.inRange = inViewVolumeLocked(coord); tasks.push_back(task); } std::sort(tasks.begin(), tasks.end(), [](const ChunkTask& a, const ChunkTask& b) { if (a.distanceSq != b.distanceSq) return a.distanceSq < b.distanceSq; return coordLess(a.coord, b.coord); }); return tasks; } void ChunkPipeline::stageGenerate(ChunkTaskResult& slot, Chunk* target, std::atomic& cancelled) { if (cancelled.load(std::memory_order_acquire)) { slot.cancelled = true; return; } auto start = std::chrono::steady_clock::now(); generator.generateChunk(*target, slot.coord); slot.stage = ChunkStage::Generate; slot.stageNanos[static_cast(ChunkStage::Generate)] = nanosSince(start); } void ChunkPipeline::stageLight(ChunkTaskResult& slot, std::atomic& cancelled) { if (cancelled.load(std::memory_order_acquire) || slot.cancelled) { slot.cancelled = true; return; } auto start = std::chrono::steady_clock::now(); Vec3i origin{slot.coord.x * chunkSize - 1, slot.coord.y * chunkSize - 1, slot.coord.z * chunkSize - 1}; Vec3i dims{chunkSize + 2, chunkSize + 2, chunkSize + 2}; slot.light = computeLight(world, origin, dims); slot.stage = ChunkStage::Light; slot.stageNanos[static_cast(ChunkStage::Light)] = nanosSince(start); } void ChunkPipeline::stageMesh(ChunkTaskResult& slot, std::atomic& cancelled) { if (cancelled.load(std::memory_order_acquire) || slot.cancelled) { slot.cancelled = true; return; } auto start = std::chrono::steady_clock::now(); slot.mesh = meshChunkLit(world, slot.coord, slot.light); slot.stage = ChunkStage::Mesh; slot.stageNanos[static_cast(ChunkStage::Mesh)] = nanosSince(start); } void ChunkPipeline::stageReady(ChunkTaskResult& slot, const Chunk* source, std::atomic& cancelled) { if (cancelled.load(std::memory_order_acquire) || slot.cancelled) { slot.cancelled = true; return; } auto start = std::chrono::steady_clock::now(); slot.chunk = *source; slot.entryBytes = ChunkCache::entrySizeBytes(slot.chunk, slot.mesh); slot.stage = ChunkStage::Ready; slot.stageNanos[static_cast(ChunkStage::Ready)] = nanosSince(start); } void ChunkPipeline::run() { std::vector tasks = pendingTasks(); if (tasks.empty()) return; std::size_t count = tasks.size(); std::vector*> flags(count, nullptr); { std::lock_guard lock(controlMutex); activeIndex.clear(); cancelFlags.clear(); cancelFlags.reserve(count); for (std::size_t i = 0; i < count; ++i) { activeIndex[tasks[i].coord] = i; bool cancelled = !tasks[i].inRange || cancelledCoords.count(tasks[i].coord) != 0; cancelFlags.push_back(std::unique_ptr>( new std::atomic(cancelled))); flags[i] = cancelFlags.back().get(); } } std::vector slots(count); std::vector targets(count, nullptr); std::unordered_map indexOf; indexOf.reserve(count * 2); for (std::size_t i = 0; i < count; ++i) { slots[i].coord = tasks[i].coord; indexOf[tasks[i].coord] = i; if (!flags[i]->load(std::memory_order_acquire)) targets[i] = &world.ensureChunk(tasks[i].coord); } JobGraph graph; std::vector> nodes(count); for (std::size_t i = 0; i < count; ++i) { JobPriority priority = tasks[i].priority; std::string label = coordLabel(tasks[i].coord); nodes[i][0] = graph.addNode(label + " generate", [this, i, &slots, &targets, &flags] { stageGenerate(slots[i], targets[i], *flags[i]); }, priority); nodes[i][1] = graph.addNode(label + " light", [this, i, &slots, &flags] { stageLight(slots[i], *flags[i]); }, priority); nodes[i][2] = graph.addNode(label + " mesh", [this, i, &slots, &flags] { stageMesh(slots[i], *flags[i]); }, priority); nodes[i][3] = graph.addNode(label + " ready", [this, i, &slots, &targets, &flags] { stageReady(slots[i], targets[i], *flags[i]); }, priority); } for (std::size_t i = 0; i < count; ++i) { graph.addDependency(nodes[i][1], nodes[i][0]); graph.addDependency(nodes[i][2], nodes[i][1]); graph.addDependency(nodes[i][3], nodes[i][2]); for (int dx = -1; dx <= 1; ++dx) { for (int dy = -1; dy <= 1; ++dy) { for (int dz = -1; dz <= 1; ++dz) { if (dx == 0 && dy == 0 && dz == 0) continue; Vec3i neighbour = tasks[i].coord + Vec3i{dx, dy, dz}; auto found = indexOf.find(neighbour); if (found == indexOf.end()) continue; graph.addDependency(nodes[i][1], nodes[found->second][0]); graph.addDependency(nodes[i][2], nodes[found->second][0]); } } } } JobBatch batch = jobs.runGraph(graph); batch.wait(); jobs.waitAll(); std::vector mergeOrder(count); std::iota(mergeOrder.begin(), mergeOrder.end(), std::size_t{0}); std::sort(mergeOrder.begin(), mergeOrder.end(), [&tasks](std::size_t a, std::size_t b) { return coordLess(tasks[a].coord, tasks[b].coord); }); PipelineStats delta; completedResults.clear(); resultIndex.clear(); completedResults.reserve(count); for (std::size_t index : mergeOrder) { ChunkTaskResult& slot = slots[index]; for (int stage = 0; stage < chunkStageCount; ++stage) delta.stageNanos[static_cast(stage)] += slot.stageNanos[stage]; if (slot.cancelled) { ++delta.cancelled; continue; } for (int stage = 0; stage <= static_cast(slot.stage); ++stage) ++delta.stageCompleted[static_cast(stage)]; ++delta.chunksReady; chunkStore.store(slot.coord, slot.chunk, slot.mesh); resultIndex[slot.coord] = completedResults.size(); completedResults.push_back(std::move(slot)); } for (const Vec3i& coord : chunkStore.residentCoords()) { if (inViewVolume(coord)) chunkStore.pin(coord); else chunkStore.unpin(coord); } std::lock_guard lock(controlMutex); for (int stage = 0; stage < chunkStageCount; ++stage) { std::size_t at = static_cast(stage); counters.stageCompleted[at] += delta.stageCompleted[at]; counters.stageNanos[at] += delta.stageNanos[at]; } counters.cancelled += delta.cancelled; counters.chunksReady += delta.chunksReady; counters.cachedBytes = chunkStore.bytesUsed(); pending.clear(); pendingSet.clear(); activeIndex.clear(); cancelFlags.clear(); } const ChunkTaskResult* ChunkPipeline::resultFor(const Vec3i& chunkCoord) const { auto found = resultIndex.find(chunkCoord); if (found == resultIndex.end()) return nullptr; return &completedResults[found->second]; } PipelineStats ChunkPipeline::stats() const { std::lock_guard lock(controlMutex); return counters; } void ChunkPipeline::resetStats() { std::lock_guard lock(controlMutex); counters = PipelineStats{}; } std::vector ChunkPipeline::digest() const { std::vector bytes; auto appendRaw = [&bytes](const void* source, std::size_t size) { const std::uint8_t* first = static_cast(source); bytes.insert(bytes.end(), first, first + size); }; auto appendInt = [&appendRaw](int value) { std::int32_t narrowed = value; appendRaw(&narrowed, sizeof(narrowed)); }; auto appendCount = [&appendRaw](std::uint64_t value) { appendRaw(&value, sizeof(value)); }; auto appendMesh = [&](const Mesh& mesh) { appendCount(mesh.vertices.size()); if (!mesh.vertices.empty()) appendRaw(mesh.vertices.data(), mesh.vertices.size() * sizeof(Vertex)); appendCount(mesh.indices.size()); if (!mesh.indices.empty()) appendRaw(mesh.indices.data(), mesh.indices.size() * sizeof(std::uint32_t)); }; std::vector worldCoords; worldCoords.reserve(world.chunkCount()); for (const auto& slot : world.allChunks()) worldCoords.push_back(slot.first); std::sort(worldCoords.begin(), worldCoords.end(), coordLess); appendCount(worldCoords.size()); for (const Vec3i& coord : worldCoords) { appendInt(coord.x); appendInt(coord.y); appendInt(coord.z); const Chunk* chunk = world.chunkAt(coord); appendRaw(chunk->raw().data(), chunk->raw().size() * sizeof(Block)); } appendCount(completedResults.size()); for (const ChunkTaskResult& produced : completedResults) { appendInt(produced.coord.x); appendInt(produced.coord.y); appendInt(produced.coord.z); bytes.push_back(static_cast(produced.stage)); bytes.push_back(produced.cancelled ? 1u : 0u); appendRaw(produced.chunk.raw().data(), produced.chunk.raw().size() * sizeof(Block)); appendCount(produced.light.sun.size()); if (!produced.light.sun.empty()) appendRaw(produced.light.sun.data(), produced.light.sun.size()); if (!produced.light.block.empty()) appendRaw(produced.light.block.data(), produced.light.block.size()); appendMesh(produced.mesh.opaque); appendMesh(produced.mesh.transparent); appendCount(produced.entryBytes); } std::vector cached = chunkStore.residentCoords(); appendCount(cached.size()); appendCount(chunkStore.bytesUsed()); for (const Vec3i& coord : cached) { appendInt(coord.x); appendInt(coord.y); appendInt(coord.z); bytes.push_back(chunkStore.hasMesh(coord) ? 1u : 0u); bytes.push_back(chunkStore.isPinned(coord) ? 1u : 0u); } return bytes; } } // namespace vf