OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain / src / dc_worker_cpu.cpp

413 lines · cpp ref:
#include "dc_worker_cpu.h"
#include <atomic>
#include <cstdio>
#include "dc_octree.h"
#include "dc_pool.h"
#include "dc_simplify.h"
#include "dc_contour.h"
#include "dc_edit.h"

#include "dc_gpu_thread.h"

#include <godot_cpp/variant/utility_functions.hpp>
#include <chrono>
#include <cstring>
#include <cmath>
#include <functional>

using godot::UtilityFunctions;

// -------------------------------------------------------------------------
// Lifecycle
// -------------------------------------------------------------------------

DCWorkerCPU::~DCWorkerCPU() {
	stop();
}

void DCWorkerCPU::start(int worker_id) {
	worker_id_ = worker_id;
	stop_requested_.store(false, std::memory_order_release);
	running_.store(true, std::memory_order_release);
	thread_ = std::thread(&DCWorkerCPU::_thread_func, this);
}

void DCWorkerCPU::stop() {
	if (!running_.load(std::memory_order_acquire)) return;
	stop_requested_.store(true, std::memory_order_release);
	if (thread_.joinable()) thread_.join();
	running_.store(false, std::memory_order_release);
	// Thread lifecycle logged at debug level only (noisy with 4+ workers)
}

bool DCWorkerCPU::submit(const CellRequest &request) {
	return request_queue_.try_push(request);
}

bool DCWorkerCPU::submit_priority(const CellRequest &request) {
	return priority_queue_.try_push(request);
}

bool DCWorkerCPU::poll_result(CellResult &out) {
	return result_queue_.try_pop(out);
}

bool DCWorkerCPU::is_running() const {
	return running_.load(std::memory_order_acquire);
}

// -------------------------------------------------------------------------
// Thread loop — single request at a time, no batching
// -------------------------------------------------------------------------

void DCWorkerCPU::_thread_func() {
	// Thread lifecycle logged at debug level only

	auto now_us = []() {
		return std::chrono::duration_cast<std::chrono::microseconds>(
			std::chrono::steady_clock::now().time_since_epoch()).count();
	};

	while (!stop_requested_.load(std::memory_order_acquire)) {
		CellRequest request;
		// Priority lane (user edits) drains first, then the normal LOD/pre-mesh
		// queue — an edit never waits behind streaming work (1b).
		if (priority_queue_.try_pop(request) || request_queue_.try_pop(request)) {
			int64_t picked_up_us = now_us();
			if (request.generation == current_generation_.load(std::memory_order_acquire)) {
				busy_.store(true, std::memory_order_release);
				_process_cell(request, picked_up_us);
				busy_.store(false, std::memory_order_release);
			} else {
				// Stale request — push empty result (timings still useful for tracking)
				CellResult empty;
				empty.node = request.node;
				empty.generation = request.generation;
				empty.node_generation = request.node_generation;
				empty.pre_mesh_morton = request.pre_mesh_morton;
				empty.is_pre_mesh = request.is_pre_mesh;
				empty.success = false;
				empty.submit_time_us = request.submit_time_us;
				empty.queue_wait_us = picked_up_us - request.submit_time_us;
				empty.compute_us = 0;
				empty.result_enqueue_us = now_us();
				result_queue_.try_push(std::move(empty));
			}
		} else {
			std::this_thread::sleep_for(std::chrono::microseconds(500));
		}
	}
}

// -------------------------------------------------------------------------
// Process one cell: density → octree → mesh → clip → smooth
// -------------------------------------------------------------------------

void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us) {
	auto now_us = []() {
		return std::chrono::duration_cast<std::chrono::microseconds>(
			std::chrono::steady_clock::now().time_since_epoch()).count();
	};

	CellResult result;
	result.node = request.node;
	result.generation = request.generation;

	const std::vector<TerrainEdit>* edit_ptr =
		(request.edits && !request.edits->empty()) ? request.edits.get() : nullptr;
	result.node_generation = request.node_generation;
	result.pre_mesh_morton = request.pre_mesh_morton;
	result.is_pre_mesh = request.is_pre_mesh;
	// Benchmarking: carry submit time and queue wait; stamp compute_us at each push site.
	result.submit_time_us = request.submit_time_us;
	result.queue_wait_us = picked_up_us - request.submit_time_us;

	DCGenerator *gen_ptr = request.generator;
	if (!gen_ptr || !request.density) {
		result.success = true;
		result.compute_us = now_us() - picked_up_us;
		result.result_enqueue_us = now_us();
		while (!result_queue_.try_push(std::move(result))) {
			if (stop_requested_.load(std::memory_order_acquire)) return;
			std::this_thread::sleep_for(std::chrono::microseconds(100));
		}
		return;
	}

	// --- Fast early-out: sample 15 points to detect fully empty/solid chunks ---
	// Only valid on CPU path where evaluate() produces the real density.
	// GPU path: CPU evaluate() may be a stub (e.g., proc-nodes returns -y).
	//
	// Edit awareness: only ADDITIVE edits (strength > 0) can create geometry
	// in empty space. Subtractive edits can only remove material. So:
	// Edit-aware early-out:
	// - No edits: normal early-out (evaluate only)
	// - Any edits present: skip early-out entirely. The 15 sparse sample points
	//   (8 corners + center + 6 face centers) don't fall inside small edit
	//   volumes, so a chunk could be flagged "all solid"/"all empty" while
	//   actually containing an edit-carved pocket. Observed: subtract edits
	//   inside flat_center_radius were invisible because corner samples stayed
	//   uniform. Skip early-out whenever this chunk has any culled edits.
	if (request.gpu_eligible) {
		// GPU path — can't early-out with CPU samples
	} else if (edit_ptr) {
		// Any edit present — skip early-out (sparse samples may miss the edit volume)
	} else {
		float ox = request.cell_origin[0];
		float oy = request.cell_origin[1];
		float oz = request.cell_origin[2];
		float span = request.voxel_size * request.voxels_per_cell;
		float half = span * 0.5f;
		float lvs = request.voxel_size;

		// Sample with edits applied (evaluate() no longer includes edits)
		auto sample = [&](float x, float y, float z) -> float {
			float d = gen_ptr->evaluate(x, y, z, lvs);
			if (edit_ptr) d = DCEditList::apply(*edit_ptr, x, y, z, d);
			return d;
		};

		bool all_positive = true, all_negative = true;
		// 8 corners
		for (int cz = 0; cz <= 1; cz++) {
			for (int cy = 0; cy <= 1; cy++) {
				for (int cx = 0; cx <= 1; cx++) {
					float d = sample(
						ox + cx * span, oy + cy * span, oz + cz * span);
					if (d >= 0.0f) all_negative = false;
					else all_positive = false;
				}
			}
		}
		// Center + 6 face centers
		if (all_positive || all_negative) {
			float d = sample(ox + half, oy + half, oz + half);
			if (d >= 0.0f) all_negative = false;
			else all_positive = false;
		}
		if (all_positive || all_negative) {
			float samples[6] = {
				sample(ox + half, oy + half, oz),
				sample(ox + half, oy + half, oz + span),
				sample(ox + half, oy, oz + half),
				sample(ox + half, oy + span, oz + half),
				sample(ox, oy + half, oz + half),
				sample(ox + span, oy + half, oz + half),
			};
			for (int i = 0; i < 6; i++) {
				if (samples[i] >= 0.0f) all_negative = false;
				else all_positive = false;
			}
		}
		if (all_positive || all_negative) {
			result.success = true;
			result.compute_us = now_us() - picked_up_us;
			result.result_enqueue_us = now_us();
			while (!result_queue_.try_push(std::move(result))) {
				if (stop_requested_.load(std::memory_order_acquire)) return;
				std::this_thread::sleep_for(std::chrono::microseconds(100));
			}
			return;
		}
	}

	// --- Density grid fill + smooth ---
	int cells = request.voxels_per_cell;
	float origin[3] = { request.cell_origin[0], request.cell_origin[1], request.cell_origin[2] };

	// Base density function (noise only — no edits).
	// Edits are applied separately after grid fill for caching.
	struct DensityUD {
		DCGenerator* generator;
		float lod_voxel_size;
	};
	DensityUD density_ctx { gen_ptr, request.voxel_size };
	auto base_density_fn = [](float x, float y, float z, void* ud) -> float {
		auto* ctx = static_cast<const DensityUD*>(ud);
		return ctx->generator->evaluate(x, y, z, ctx->lod_voxel_size);
	};

	DensityGrid grid;
	grid.allocate(cells, request.voxel_size, request.cell_origin);

	// Try GPU density fill + optional GPU mesh generation
	bool gpu_filled = false;
	bool gpu_meshed = false;
	DCMeshData gpu_mesh;
	if (gpu_thread_ && request.density && request.gpu_eligible) {
		const char* custom_glsl = nullptr;
		const float* custom_data = nullptr;
		int custom_data_n = 0;
		uint32_t custom_data_version = 0;
		const void* const* noise_volumes = nullptr;
		int noise_dim = 0;
		uint32_t noise_version = 0;
		if (request.generator && request.generator->has_gpu_shader()) {
			custom_glsl = request.generator->gpu_shader_source();
			custom_data = request.generator->gpu_custom_data();
			custom_data_n = request.generator->gpu_custom_data_count();
			custom_data_version = request.generator->gpu_custom_data_version();
			// Volumes for a body that SAMPLES noise instead of computing it.
			noise_volumes = request.generator->gpu_noise_volumes();
			noise_dim = request.generator->gpu_noise_dim();
			noise_version = request.generator->gpu_noise_version();
		}
		const std::vector<TerrainEdit>* gpu_edit_ptr =
			(request.edits && !request.edits->empty()) ? request.edits.get() : nullptr;
		// GPU path handles edits internally via compute shader
		gpu_filled = gpu_thread_->submit(grid, *request.density, custom_glsl, gpu_edit_ptr,
			&gpu_mesh, request.cell_origin, request.voxel_size,
			(request.use_adaptive_qef ? 2u : 0u) | (request.use_shortest_diagonal ? 1u : 0u),
			custom_data, custom_data_n, custom_data_version,
			noise_volumes, noise_dim, noise_version);
		if (!gpu_mesh.indices.empty()) {
			gpu_meshed = true;
		}
	}
	if (!gpu_filled) {
		// CPU density fill with per-worker LRU cache.
		// Cache stores base density (noise only). Edits applied on top.
		DensityCacheKey cache_key { request.node_position.x, request.node_position.y,
		                            request.node_position.z, request.lod_level };
		auto cache_it = cache_map_.find(cache_key);
		if (cache_it != cache_map_.end()) {
			// Cache hit: copy base density, skip expensive noise evaluation
			grid.data = cache_it->second->second;
			// Move to front (most recently used)
			cache_lru_.splice(cache_lru_.begin(), cache_lru_, cache_it->second);
		} else {
			// Cache miss: fill from the generator, a Z-slice at a time.
			// Identical values to grid.fill(base_density_fn, ...) -- the
			// default evaluate_batch IS that loop -- but a generator that
			// resolves per-grid state gets to do it once per slice instead of
			// once per corner.
			grid.fill_generator(gen_ptr, request.voxel_size);
			// Cache the base density (before edits)
			cache_lru_.push_front({cache_key, grid.data});
			cache_map_[cache_key] = cache_lru_.begin();
			// Evict oldest if over capacity
			while ((int)cache_lru_.size() > DENSITY_CACHE_MAX) {
				cache_map_.erase(cache_lru_.back().first);
				cache_lru_.pop_back();
			}
		}
		// Apply culled edits on top of base density
		if (edit_ptr && !edit_ptr->empty()) {
			grid.apply_edits(*edit_ptr);
		}
	}

	// If GPU generated the mesh, skip CPU octree + DC pipeline
	if (gpu_meshed) {
		result.mesh_data = std::move(gpu_mesh);
	} else {
		// CPU mesh path: octree → dual contour → smooth
		grid.compute_normals();

		int grid_min[3] = { 0, 0, 0 };
		Pool<OctreeNode> node_pool;
		node_pool.reserve(cells * cells * cells / 4);
		OctreeNode *root = construct_octree_grid_pooled(grid_min, cells, grid, node_pool);

		if (root) {
			// Edits are baked into the density grid (correct inside/outside signs).
			// Also pass them to the mesher for geometric refinement: snap_to_box_face
			// aligns edge crossings to exact box planes with axis-aligned normals,
			// and mark_box_corners enables mesh SDF precision. Not a double-apply —
			// the density grid and edit list serve complementary purposes.
			dc_generate_mesh_grid(root, request.voxel_size, origin, grid, request.overlap, result.mesh_data,
				edit_ptr);
		}
	}

	int tris_raw = static_cast<int>(result.mesh_data.indices.size()) / 3;

	// ⛔ WHICH MESHER PRODUCED THIS CHUNK? Counted HERE, after the mesh exists,
	// because that is the only point where the three cases are distinguishable.
	//
	// A first cut of this counter sat before the mesh block and reported
	// "gpu_mesh 15%", which CANNOT be read: gpu_meshed is set from
	// `!gpu_mesh.indices.empty()`, so a chunk the GPU meshed correctly to an
	// EMPTY result (no surface in it, and there are many) counts identically to
	// one the CPU had to mesh. Those have opposite meanings.
	//
	// cpu_mesh_solid is the number that matters: chunks with real geometry that
	// the CPU dual-contoured. Every one of those sits in the same world as
	// GPU-meshed neighbours, and the two are SEPARATE implementations —
	// qef_solve.glsl vs libdc/src/dc_contour.cpp. test_erosion_parity.cpp exists
	// because the GPU and CPU DENSITY paths silently drifted 4 km apart; there is
	// no equivalent gate for the two MESHERS, so a disagreement between them
	// shows up only as chunks that do not line up.
	{
		static std::atomic<uint64_t> s_total{0}, s_dens{0}, s_gmesh{0}, s_cmesh{0}, s_empty{0};
		const uint64_t n = ++s_total;
		if (gpu_filled) s_dens++;
		if (tris_raw == 0)      s_empty++;
		else if (gpu_meshed)    s_gmesh++;
		else                    s_cmesh++;
		if ((n % 64) == 0) {
			const uint64_t solid = s_gmesh.load() + s_cmesh.load();
			fprintf(stderr, "[GPUPATH] chunks=%llu  gpu_density=%llu (%.0f%%)  "
			                "| SOLID %llu: gpu_mesh=%llu (%.0f%%) cpu_mesh=%llu (%.0f%%)"
			                "  | empty=%llu  cpu_density_fallback=%llu\n",
			        (unsigned long long)n,
			        (unsigned long long)s_dens.load(), 100.0 * s_dens.load() / n,
			        (unsigned long long)solid,
			        (unsigned long long)s_gmesh.load(), solid ? 100.0 * s_gmesh.load() / solid : 0.0,
			        (unsigned long long)s_cmesh.load(), solid ? 100.0 * s_cmesh.load() / solid : 0.0,
			        (unsigned long long)s_empty.load(),
			        (unsigned long long)(n - s_dens.load()));
		}
	}

	// Free grid early
	grid.data.clear(); grid.data.shrink_to_fit();
	grid.normal_x.clear(); grid.normal_x.shrink_to_fit();
	grid.normal_y.clear(); grid.normal_y.shrink_to_fit();
	grid.normal_z.clear(); grid.normal_z.shrink_to_fit();

	// Cell-ownership filtering happens inside contour_leaves_flat now —
	// no post-process clip needed.
	int tris_after_clip = tris_raw;

	// --- Geometric error ---
	{
		const auto& pos = result.mesh_data.positions;
		size_t vert_count = pos.size() / 3;
		float max_err = 0.0f;
		for (size_t i = 0; i < vert_count; i++) {
			float d = gen_ptr->evaluate(pos[i*3], pos[i*3+1], pos[i*3+2], request.voxel_size);
			float err = std::fabs(d);
			if (err > max_err) max_err = err;
		}
		result.geometric_error = max_err;
	}

	// --- Auto-smooth normals ---
	if (!result.mesh_data.indices.empty()) {
		dc_auto_smooth(result.mesh_data, 40.0f);
	}

	int tris_after_smooth = static_cast<int>(result.mesh_data.indices.size()) / 3;

	// --- Optimize vertex cache ---
	if (!result.mesh_data.indices.empty()) {
		dc_optimize_vertex_cache(result.mesh_data);
	}

	int tris_final = static_cast<int>(result.mesh_data.indices.size()) / 3;

	// DIAGNOSTIC disabled — enable via debug_logging property if needed

	result.active_voxel_count = static_cast<uint32_t>(result.mesh_data.positions.size() / 3);
	result.triangle_count = static_cast<uint32_t>(result.mesh_data.indices.size() / 3);
	result.success = true;
	result.compute_us = now_us() - picked_up_us;
	result.result_enqueue_us = now_us();

	while (!result_queue_.try_push(std::move(result))) {
		if (stop_requested_.load(std::memory_order_acquire)) return;
		std::this_thread::sleep_for(std::chrono::microseconds(100));
	}
}