#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));
}
}