OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / 931d9f1

dc: instrument the GPU path, because "is the GPU running" had no answer
Three questions kept being argued from inference instead of measurement. [GPUPATH] in dc_worker_cpu counts the two decisions separately. They are NOT the same decision and conflating them hid the real defect: a chunk can have its density filled on the GPU and then be MESHED on the CPU, because submit() is consulted twice. Split into gpu_density / gpu_mesh / cpu_mesh / empty, it immediately showed 47 solid chunks meshed by dc_contour.cpp sitting in a world whose neighbours came from qef_solve.glsl -- two different dual-contouring implementations, adjacent, which is what "close LODs don't line up" was. custom_data_version in dc_gpu_thread: the upload was keyed on the BUFFER POINTER, so mutating the tile grid in place re-used a stale upload. DC_NOVER=1 restores the old comparison for A/B. DC_ATLAS re-enables the atlas upload. Two global changes landed together and guessing which one regressed a map that does not use the tile grid had already cost several runs; a switch answers it in one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
931d9f1a4070b82b34d25a2ac28a500ae67c72ee
by plankatron <fleeky@gmail.com> · 2026-09-03T21:35:43.000Z

Changed files (3) +59 -2

src/dc_compute.cpp +5 -1
……@@ -1,4 +1,5 @@
11#include "dc_compute.h"
2#include <cstdlib>
23#include "dc_gpu_thread.h" // for GpuFillRequest
34#include "dc_gpu_params.h" // shared GpuParams struct
45#include "dc_generator_mesh_sdf.h" // for BVH flattening
……@@ -391,7 +392,10 @@ void DCCompute::set_custom_data(const float* data, int n) {
391392 // one samples. Off until something reads it; one line to re-enable, and the
392393 // per-tile texture_copy blits it exists for are still proven in
393394 // gate_texture_atlas.gd.
394 static const bool kUploadAtlas = false;
395 // A/B SWITCH, because two global changes landed together and guessing which
396 // one regressed a map that does not even use the tile grid has already cost
397 // several runs. DC_ATLAS=1 restores the pre-change behaviour.
398 static const bool kUploadAtlas = (getenv("DC_ATLAS") != nullptr);
395399 if (kUploadAtlas) _upload_atlas(data, n);
396400 uniform_set_dirty_ = true;
397401}
src/dc_gpu_thread.h +14 -1
……@@ -20,6 +20,7 @@
2020// GPU thread and wait (or fall back to CPU if the GPU thread isn't available).
2121
2222#include "dc_compute.h"
23#include <cstdlib>
2324#include "dc_gpu_mesh_pipeline.h"
2425#include "dc_density.h"
2526#include "dc_density_grid.h"
……@@ -287,9 +288,21 @@ private:
287288 // existing capacity keeps the same data() address and the upload is
288289 // silently skipped -- new terrain on disk, old terrain on screen, no
289290 // error anywhere. A tile arriving from a peer mid-session is that case.
291 // A/B SWITCH: DC_NOVER=1 ignores the content version and restores the
292 // pointer-only test this had before 2026-09-03. It also COUNTS the
293 // uploads, because "set_custom_data ran every batch" and "it ran twice"
294 // look identical from a frame graph and only one of them is a
295 // regression.
296 static const bool kNoVersion = (getenv("DC_NOVER") != nullptr);
297 static uint64_t s_uploads = 0;
290298 const float* hb = batch[0].custom_data;
291 const uint32_t hv = batch[0].custom_data_version;
299 const uint32_t hv = kNoVersion ? current_custom_data_version_
300 : batch[0].custom_data_version;
292301 if (hb != current_custom_data_ || hv != current_custom_data_version_) {
302 if ((++s_uploads % 16) == 1 || s_uploads < 4) {
303 fprintf(stderr, "DCGpuThread: custom-data upload #%llu (%d floats)\n",
304 (unsigned long long)s_uploads, batch[0].custom_data_n);
305 }
293306 compute_.set_custom_data(hb, batch[0].custom_data_n);
294307 current_custom_data_ = hb;
295308 current_custom_data_version_ = hv;
src/dc_worker_cpu.cpp +40 -0
……@@ -1,4 +1,6 @@
11#include "dc_worker_cpu.h"
2#include <atomic>
3#include <cstdio>
24#include "dc_octree.h"
35#include "dc_pool.h"
46#include "dc_simplify.h"
……@@ -308,6 +310,44 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us
308310
309311 int tris_raw = static_cast<int>(result.mesh_data.indices.size()) / 3;
310312
313 // ⛔ WHICH MESHER PRODUCED THIS CHUNK? Counted HERE, after the mesh exists,
314 // because that is the only point where the three cases are distinguishable.
315 //
316 // A first cut of this counter sat before the mesh block and reported
317 // "gpu_mesh 15%", which CANNOT be read: gpu_meshed is set from
318 // `!gpu_mesh.indices.empty()`, so a chunk the GPU meshed correctly to an
319 // EMPTY result (no surface in it, and there are many) counts identically to
320 // one the CPU had to mesh. Those have opposite meanings.
321 //
322 // cpu_mesh_solid is the number that matters: chunks with real geometry that
323 // the CPU dual-contoured. Every one of those sits in the same world as
324 // GPU-meshed neighbours, and the two are SEPARATE implementations —
325 // qef_solve.glsl vs libdc/src/dc_contour.cpp. test_erosion_parity.cpp exists
326 // because the GPU and CPU DENSITY paths silently drifted 4 km apart; there is
327 // no equivalent gate for the two MESHERS, so a disagreement between them
328 // shows up only as chunks that do not line up.
329 {
330 static std::atomic<uint64_t> s_total{0}, s_dens{0}, s_gmesh{0}, s_cmesh{0}, s_empty{0};
331 const uint64_t n = ++s_total;
332 if (gpu_filled) s_dens++;
333 if (tris_raw == 0) s_empty++;
334 else if (gpu_meshed) s_gmesh++;
335 else s_cmesh++;
336 if ((n % 64) == 0) {
337 const uint64_t solid = s_gmesh.load() + s_cmesh.load();
338 fprintf(stderr, "[GPUPATH] chunks=%llu gpu_density=%llu (%.0f%%) "
339 "| SOLID %llu: gpu_mesh=%llu (%.0f%%) cpu_mesh=%llu (%.0f%%)"
340 " | empty=%llu cpu_density_fallback=%llu\n",
341 (unsigned long long)n,
342 (unsigned long long)s_dens.load(), 100.0 * s_dens.load() / n,
343 (unsigned long long)solid,
344 (unsigned long long)s_gmesh.load(), solid ? 100.0 * s_gmesh.load() / solid : 0.0,
345 (unsigned long long)s_cmesh.load(), solid ? 100.0 * s_cmesh.load() / solid : 0.0,
346 (unsigned long long)s_empty.load(),
347 (unsigned long long)(n - s_dens.load()));
348 }
349 }
350
311351 // Free grid early
312352 grid.data.clear(); grid.data.shrink_to_fit();
313353 grid.normal_x.clear(); grid.normal_x.shrink_to_fit();