OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / c3fdfe5

gpu: a content VERSION, because a stable pointer cannot express mutation
dc_generator.h's own contract says it: "The pointer must stay valid and STABLE: the GPU thread re-uploads only when it changes." That is correct for a buffer only ever replaced wholesale, and WRONG the moment a generator can grow its data in place. std::vector::assign into spare capacity KEEPS data() unchanged. So a tile fetched from a peer mid-session would land in hblk_, the pointer would not move, _process_batch's `if (hb != current_custom_data_)` would skip the upload, and the GPU would keep rendering the old world -- new terrain on disk, old terrain on screen, and not one log line out of place. That is the failure this exists to make impossible. DCGenerator::gpu_custom_data_version() new virtual, defaults to 0 GpuFillRequest::custom_data_version carried per request submit(..., custom_data_version = 0) both call sites pass it _process_batch re-uploads on POINTER *OR* VERSION DCTerrainGeneratorProcNodes bumps hblk_version_ at all three sites that touch hblk_ -- the clear, the .hblk load, and _collect_node_data's assign. Defaulting the virtual to 0 keeps every other generator (noise, mesh SDF) byte-identical in behaviour: 0 == 0 forever, so only the pointer test can fire for them, exactly as before. ⚠ WIRING VERIFIED BY INSPECTION, NOT BY RUNNING. The new parameter is defaulted, so a call site that failed to pass it would still COMPILE -- the diff is the evidence, not the build. It becomes observable at runtime only once the generator actually grows its buffer, which is the next commit. Built template_release + template_debug (a half-built pair is how a stale extension segfaults, and headless loads a different variant than the game). Nothing was running that maps the extension: the only live process is `--daemon-only --no-game`, which loads no Godot at all (checked /proc/*/maps). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c3fdfe53bd5675c234777028ec51238b47dcfd8d
by plankatron <fleeky@gmail.com> · 2026-09-03T18:38:50.000Z

Changed files (6) +49 -5

libdc/include/dc_generator.h +14 -0
……@@ -1,6 +1,8 @@
11#ifndef DC_GENERATOR_H
22#define DC_GENERATOR_H
33
4#include <cstdint>
5
46#include <cmath>
57
68// Abstract base class for density/SDF evaluation.
……@@ -57,6 +59,18 @@ public:
5759 // moving one alone gives either a truncated buffer or an attempt to
5860 // allocate count² floats, which is 7.9 TB for a 1026 tile and a segfault.
5961 virtual int gpu_custom_data_count() const { return 0; }
62 // A CONTENT VERSION, because the STABLE-pointer contract above cannot
63 // express mutation.
64 //
65 // The GPU thread re-uploads when the POINTER changes. That is correct for a
66 // buffer that is only ever replaced wholesale, and WRONG the moment a
67 // generator can grow its data in place: a std::vector assigned to within
68 // its existing capacity keeps the same data() address, so the new contents
69 // never reach the GPU and every log line still reads healthy. A tile
70 // fetched from a peer mid-session is exactly that case.
71 //
72 // Bump this whenever the bytes change, whatever the pointer does.
73 virtual uint32_t gpu_custom_data_version() const { return 0; }
6074
6175 // Optional: bounding box hint for grid allocation.
6276 // Writes axis-aligned min/max into min[3]/max[3]. Returns false if unbounded.
src/dc_gpu_thread.h +16 -2
……@@ -40,6 +40,7 @@ struct GpuFillRequest {
4040 const char* custom_glsl = nullptr; // optional: proc-nodes compiled shader
4141 const float* custom_data = nullptr; // optional: baked custom data (binding 13)
4242 int custom_data_n = 0;
43 uint32_t custom_data_version = 0; // content version; see DCGenerator
4344 const std::vector<TerrainEdit>* edits = nullptr; // terrain edits for GPU evaluation
4445 bool wants_gpu_mesh = false; // request GPU mesh generation (skip CPU DC)
4546 DCMeshData* mesh_output = nullptr; // where to write GPU-generated mesh
……@@ -101,7 +102,8 @@ public:
101102 DCMeshData* mesh_output = nullptr,
102103 const float* cell_origin = nullptr, float voxel_size = 0.0f,
103104 uint32_t mesh_quality_flags = 0,
104 const float* custom_data = nullptr, int custom_data_n = 0) {
105 const float* custom_data = nullptr, int custom_data_n = 0,
106 uint32_t custom_data_version = 0) {
105107 if (!running_.load() || !gpu_available_.load()) {
106108 return false;
107109 }
……@@ -117,6 +119,7 @@ public:
117119 req.custom_glsl = custom_glsl;
118120 req.custom_data = custom_data;
119121 req.custom_data_n = custom_data_n;
122 req.custom_data_version = custom_data_version;
120123 req.edits = edits;
121124 if (mesh_output && cell_origin) {
122125 req.wants_gpu_mesh = true;
……@@ -155,6 +158,7 @@ private:
155158 bool mesh_pipeline_available_ = false;
156159 const char* current_custom_glsl_ = nullptr; // track to avoid recompiling same shader
157160 const float* current_custom_data_ = nullptr; // same trick: re-upload only on change
161 uint32_t current_custom_data_version_ = 0; // ...and on content version, for growth
158162
159163 void _run() {
160164 // Create the RenderingDevice on THIS thread (thread affinity).
……@@ -270,15 +274,25 @@ private:
270274 // A new shader means a new uniform set, so force the custom data to
271275 // be re-uploaded even if the generator handed back the same pointer.
272276 current_custom_data_ = nullptr;
277 current_custom_data_version_ = 0u;
273278 }
274279
275280 // Baked custom data, same change-detection contract as the shader above.
276281 // Uploading reallocates the buffer and dirties the uniform set, so doing
277282 // it per chunk would rebuild the set thousands of times a second.
283 //
284 // POINTER *OR* VERSION. The pointer alone was right while custom data
285 // was only ever replaced wholesale; it is wrong for a generator that
286 // GROWS its buffer, because a std::vector written to within its
287 // existing capacity keeps the same data() address and the upload is
288 // silently skipped -- new terrain on disk, old terrain on screen, no
289 // error anywhere. A tile arriving from a peer mid-session is that case.
278290 const float* hb = batch[0].custom_data;
279 if (hb != current_custom_data_) {
291 const uint32_t hv = batch[0].custom_data_version;
292 if (hb != current_custom_data_ || hv != current_custom_data_version_) {
280293 compute_.set_custom_data(hb, batch[0].custom_data_n);
281294 current_custom_data_ = hb;
295 current_custom_data_version_ = hv;
282296 }
283297
284298 // Process each request INDIVIDUALLY with ITS OWN culled edit list.
src/dc_terrain.cpp +2 -1
……@@ -3043,9 +3043,10 @@ PackedFloat32Array DCTerrain::debug_gpu_density(const Vector3 &origin, float vox
30433043 ? generator_ptr_->gpu_shader_source() : nullptr;
30443044 const float* hb = generator_ptr_->gpu_custom_data();
30453045 int hbn = generator_ptr_->gpu_custom_data_count();
3046 uint32_t hbv = generator_ptr_->gpu_custom_data_version();
30463047
30473048 if (!gpu_thread_->submit(grid, density_, glsl, nullptr, nullptr,
3048 nullptr, 0.0f, 0u, hb, hbn)) {
3049 nullptr, 0.0f, 0u, hb, hbn, hbv)) {
30493050 return out; // empty: the GPU declined, and that is the finding
30503051 }
30513052 out.resize((int64_t)grid.data.size());
src/dc_terrain_generator_procnodes.cpp +11 -1
……@@ -442,7 +442,7 @@ void DCTerrainGeneratorProcNodes::set_height_space_sqrt(bool p_val) {
442442// alternative was a Godot Image import path, and the field has to be readable
443443// identically by the standalone harness that gates it, which has no engine.
444444bool DCTerrainGeneratorProcNodes::_load_heightfield(const String &p_path) {
445 hblk_.clear(); hblk_n_ = 0;
445 hblk_.clear(); hblk_n_ = 0; hblk_version_++;
446446 if (p_path.is_empty()) return true;
447447 Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
448448 if (f.is_null()) {
……@@ -474,6 +474,7 @@ bool DCTerrainGeneratorProcNodes::_load_heightfield(const String &p_path) {
474474 hblk_[i] = (float)v;
475475 }
476476 hblk_n_ = (int)n; hblk_mpp_ = mpp; hblk_ox_ = ox; hblk_oz_ = oz;
477 hblk_version_++;
477478 UtilityFunctions::print("[DCTerrainGeneratorProcNodes] heightfield ", p_path,
478479 ": ", (int)n, "x", (int)n, " @ ", mpp, " m/px, origin x ", ox, " z ", oz);
479480 return true;
……@@ -539,6 +540,7 @@ void DCTerrainGeneratorProcNodes::_collect_node_data() {
539540 PackedFloat32Array d = node_obj->call("get_gpu_data");
540541 if (d.is_empty()) continue;
541542 hblk_.assign(d.ptr(), d.ptr() + d.size());
543 hblk_version_++;
542544 // ASK THE NODE FOR THE SIDE LENGTH; DO NOT DERIVE IT FROM THE ARRAY.
543545 //
544546 // get_gpu_data() returns the MIP PYRAMID, not one square: 1,403,226
……@@ -1437,6 +1439,14 @@ const char *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_shader_source() co
14371439 return owner_->cached_glsl_.empty() ? nullptr : owner_->cached_glsl_.c_str();
14381440}
14391441
1442// The CONTENT version. hblk_ is written by two producers and, once tiles can
1443// arrive at runtime, by growth in place -- and std::vector::assign into spare
1444// capacity keeps data() unchanged, so the pointer cannot be the signal. Every
1445// site that touches hblk_ bumps this.
1446uint32_t DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data_version() const {
1447 return owner_->hblk_version_;
1448}
1449
14401450const float *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data() const {
14411451 return owner_->hblk_.empty() ? nullptr : owner_->hblk_.data();
14421452}
src/dc_terrain_generator_procnodes.h +3 -0
……@@ -243,6 +243,8 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator {
243243 // Cached GLSL shader source (adapter-wrapped for dc-terrain).
244244 // Compiled eagerly on main thread — worker only reads cache.
245245 std::string cached_glsl_;
246 // Bumped on every hblk_ mutation. See ProcNodesBridge::gpu_custom_data_version.
247 uint32_t hblk_version_ = 0;
246248
247249public:
248250 // The ASSEMBLED shader string, exactly as handed to the compute pipeline.
……@@ -281,6 +283,7 @@ private:
281283 const char *gpu_shader_source() const override;
282284 const float *gpu_custom_data() const override;
283285 int gpu_custom_data_count() const override;
286 uint32_t gpu_custom_data_version() const override;
284287 };
285288 ProcNodesBridge bridge_{ this };
286289
src/dc_worker_cpu.cpp +3 -1
……@@ -236,10 +236,12 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us
236236 const char* custom_glsl = nullptr;
237237 const float* custom_data = nullptr;
238238 int custom_data_n = 0;
239 uint32_t custom_data_version = 0;
239240 if (request.generator && request.generator->has_gpu_shader()) {
240241 custom_glsl = request.generator->gpu_shader_source();
241242 custom_data = request.generator->gpu_custom_data();
242243 custom_data_n = request.generator->gpu_custom_data_count();
244 custom_data_version = request.generator->gpu_custom_data_version();
243245 }
244246 const std::vector<TerrainEdit>* gpu_edit_ptr =
245247 (request.edits && !request.edits->empty()) ? request.edits.get() : nullptr;
……@@ -247,7 +249,7 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us
247249 gpu_filled = gpu_thread_->submit(grid, *request.density, custom_glsl, gpu_edit_ptr,
248250 &gpu_mesh, request.cell_origin, request.voxel_size,
249251 (request.use_adaptive_qef ? 2u : 0u) | (request.use_shortest_diagonal ? 1u : 0u),
250 custom_data, custom_data_n);
252 custom_data, custom_data_n, custom_data_version);
251253 if (!gpu_mesh.indices.empty()) {
252254 gpu_meshed = true;
253255 }