OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / src / dc_worker_cpu.h

142 lines · c ref:
#ifndef DC_WORKER_CPU_H
#define DC_WORKER_CPU_H

// DCWorkerCPU: Simple CPU-only terrain worker.
// Replaces DCWorker's GPU pipeline with pure CPU dual contouring.
// Single code path: density grid → octree → mesh → clip → smooth.

#include "dc_spsc_queue.h"
#include "dc_octree_clipmap.h"
#include "dc_contour.h"
#include "dc_density_grid.h"
#include "dc_density.h"
#include "dc_generator.h"
#include "dc_seam.h"
#include "dc_edit.h"

#include <atomic>
#include <thread>
#include <vector>
#include <unordered_map>
#include <list>

// Request: main thread -> worker
struct CellRequest {
	OctreeClipmapNode *node = nullptr;
	float cell_origin[3] = {};
	float voxel_size = 0.25f;
	int grid_dims = 33;
	int voxels_per_cell = 32;
	int overlap = 0;
	float nominal_origin[3] = {};
	float nominal_size = 0.0f;

	Vector3i node_position;
	int lod_level = 0;

	const DCDensity *density = nullptr;
	DCGenerator *generator = nullptr;
	bool gpu_eligible = true;  // ignored in CPU worker

	bool use_adaptive_qef = true;
	bool use_shortest_diagonal = true;
	bool use_mesh_simplify = true;

	std::shared_ptr<const std::vector<TerrainEdit>> edits;  // box/sphere edits for sharp features

	uint32_t generation = 0;
	uint32_t node_generation = 0;
	uint64_t pre_mesh_morton = 0;
	bool is_pre_mesh = false;

	// Benchmarking: timestamp set at _submit_to_worker (steady_clock.now() in microseconds).
	// Worker stamps queue_wait_us and compute_us in the result, main thread computes total.
	int64_t submit_time_us = 0;
};

// Result: worker -> main thread
struct CellResult {
	OctreeClipmapNode *node = nullptr;
	DCMeshData mesh_data;
	std::vector<SeamNodeInfo> seam_nodes;
	uint32_t active_voxel_count = 0;
	uint32_t triangle_count = 0;
	float geometric_error = 0.0f;
	bool success = false;
	uint32_t generation = 0;
	uint32_t node_generation = 0;
	uint64_t pre_mesh_morton = 0;
	bool is_pre_mesh = false;

	// Benchmarking deltas (microseconds), filled by worker thread.
	int64_t submit_time_us = 0;     // copied from request
	int64_t queue_wait_us = 0;      // time spent in request_queue before worker picked it up
	int64_t compute_us = 0;         // time spent in _process_cell on worker thread
	int64_t result_enqueue_us = 0;  // steady_clock at result_queue push (for poll-wait calc)
};

class DCGpuThread;  // forward declaration

class DCWorkerCPU {
public:
	DCWorkerCPU() = default;
	~DCWorkerCPU();

	void start(int worker_id);
	void stop();
	void set_gpu_thread(DCGpuThread* gpu) { gpu_thread_ = gpu; }
	bool submit(const CellRequest &request);
	// High-priority lane for user-initiated edits (1b). The worker drains this
	// queue before request_queue_, so an edit skips ahead of any LOD/pre-mesh
	// work already queued. Same single-producer (main thread) / single-consumer
	// (worker) invariant as request_queue_.
	bool submit_priority(const CellRequest &request);
	size_t priority_queue_size() const { return priority_queue_.approx_size(); }
	bool poll_result(CellResult &out);
	bool is_running() const;
	void set_generation(uint32_t gen) { current_generation_.store(gen, std::memory_order_release); }
	bool is_busy() const { return busy_.load(std::memory_order_acquire); }
	size_t request_queue_size() const { return request_queue_.approx_size(); }

private:
	void _thread_func();
	void _process_cell(const CellRequest &request, int64_t picked_up_us);

	int worker_id_ = 0;
	std::thread thread_;
	std::atomic<bool> running_{ false };
	std::atomic<bool> stop_requested_{ false };
	std::atomic<bool> busy_{ false };
	std::atomic<uint32_t> current_generation_{ 0 };
	DCGpuThread* gpu_thread_ = nullptr;  // optional GPU density fill

	SPSCQueue<CellRequest, 64> request_queue_;
	SPSCQueue<CellRequest, 64> priority_queue_;  // edit lane, drained before request_queue_ (1b)
	SPSCQueue<CellResult, 64> result_queue_;

	// Per-worker LRU cache for base density grids (noise only, no edits).
	// Avoids re-evaluating expensive noise when only edits changed.
	// Key: packed (position_x, position_y, position_z, lod_level).
	struct DensityCacheKey {
		int px, py, pz, lod;
		bool operator==(const DensityCacheKey& o) const {
			return px == o.px && py == o.py && pz == o.pz && lod == o.lod;
		}
	};
	struct DensityCacheKeyHash {
		size_t operator()(const DensityCacheKey& k) const {
			size_t h = std::hash<int>()(k.px);
			h ^= std::hash<int>()(k.py) + 0x9e3779b9 + (h << 6) + (h >> 2);
			h ^= std::hash<int>()(k.pz) + 0x9e3779b9 + (h << 6) + (h >> 2);
			h ^= std::hash<int>()(k.lod) + 0x9e3779b9 + (h << 6) + (h >> 2);
			return h;
		}
	};
	static constexpr int DENSITY_CACHE_MAX = 64;  // ~9 MB per worker (64 × 140 KB)
	using CacheList = std::list<std::pair<DensityCacheKey, std::vector<float>>>;
	CacheList cache_lru_;
	std::unordered_map<DensityCacheKey, CacheList::iterator, DensityCacheKeyHash> cache_map_;
};

#endif // DC_WORKER_CPU_H