OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / 99a8659

dc: a compile failure was silent, and the built-in generator looks like terrain
hyper-games #64 NEXT 4: make a first-compile failure visible rather than silently substituting the built-in generator. TWO SILENT PATHS, and they compound. _recompile_glsl clears cached_glsl_ and then has three early returns. Any of them leaves has_gpu_shader() answering false, dc_worker_cpu on the CPU path, and the map rendering a plausible landscape that is not the graph's -- with nothing in the log. The sharpest is the REFUSAL: proc-nodes' compile_to_glsl_function deliberately returns "" when a node claims GPU support and emits no GLSL, and pushes its own error saying which node. That refusal arrived here and vanished. It now says what the refusal COSTS, which is the half proc-nodes cannot know: the terrain will mesh from a hardcoded gradient-noise -> erosion pipeline that is not your graph. DCTerrain has the other one. A DCTerrainGenerator subclass that does not override get_native_generator() returns null and DCTerrain quietly points generator_ptr_ at its own noise generator, in two places. That generator renders a perfectly good landscape, so the symptom is "the terrain is wrong somehow" and never points at the resource. It now pushes an error naming the resource's class and what to override -- once per transition, because these run on every config propagation and a message per frame is a message nobody reads. VISIBLE HAS TO MEAN ANSWERABLE. A gate cannot see push_error, so three things are bound: DCTerrainGeneratorProcNodes.has_compiled_shader() DCTerrainGeneratorProcNodes.get_last_compile_error() DCTerrain.is_using_builtin_generator() ⚠ AND THE EMPTY STRING WAS AMBIGUOUS. _recompile_glsl only runs from set_graph()/_set(), so a generator nobody has configured has never ATTEMPTED a compile -- and "" made that indistinguishable from "compiled cleanly". The initial value is now "never compiled (no graph has been assigned)". Four states, four answers: never compiled, no graph assigned, a specific failure, or empty for success. My first version of the gate asserted the wrong one of those and went 5/6; the code was right and the expectation was not. demo/compile_failure_gate.gd, 9/9. Mutation-tested: renaming the REFUSED text and flipping using_builtin_generator_ takes it to 7/9, exactly those two cases. ⛔ AND THE FIRST MUTATION RUN WAS A FALSE GREEN. The mutation did not compile, scons said "building terminated because of errors", the .so kept the old code and the gate passed 9/9 against it. A mutation sweep has to check that the BUILD LANDED, not just that the file changed -- the second run confirms the mutant string is in the .so before running the gate. Same species as the stale-library trap in the commit before this one. ⚠ A headless SceneTree script that errors in _init never reaches quit() and hangs rather than exiting. Two runs were lost to that before it was recognised; gate runs are wrapped in `timeout` now. Worth knowing before the next probe. All three targets carry the change (checked by string, not by mtime). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
99a8659254956153c4e0270bd1dc10a64e4abf33
by plankatron <fleeky@gmail.com> · 2026-09-06T14:56:55.000Z

Changed files (5) +166 -3

demo/compile_failure_gate.gd +70 -0
……@@ -0,0 +1,70 @@
1## A compile failure must be VISIBLE and ANSWERABLE, not silent.
2##
3## hyper-games #64 NEXT 4. Every early return in _recompile_glsl leaves
4## cached_glsl_ empty; the terrain then meshes from the CPU wrapper, or from
5## dc-terrain's BUILT-IN noise generator, and renders a plausible landscape that
6## is not the graph's. This asserts each state reports itself.
7extends SceneTree
8var _fail := 0
9func _ck(name: String, ok: bool, detail: String) -> void:
10 print(" %-34s %s %s" % [name, "PASS" if ok else "FAIL", detail])
11 if not ok: _fail += 1
12
13func _init() -> void:
14 # A. no graph -- a legitimate state, recorded, not screamed about
15 var g0 := DCTerrainGeneratorProcNodes.new()
16 _ck("no graph: not compiled", not g0.has_compiled_shader(), "has_compiled_shader() false")
17 # "" here would be indistinguishable from "compiled cleanly" -- a fresh
18 # generator has never ATTEMPTED a compile, because _recompile_glsl only runs
19 # from set_graph()/_set(). That ambiguity is what this asserts is gone.
20 _ck("fresh: says never compiled",
21 g0.get_last_compile_error().begins_with("never compiled"),
22 '"%s"' % g0.get_last_compile_error())
23
24 # B. a graph that compiles
25 var good := ProcNodeGraph.new()
26 var n1 := good.add_node(GradientNoiseNode.new())
27 var e1 := good.add_node(ErosionNoiseNode.new())
28 good.connect_sockets(n1, 0, e1, 7)
29 good.connect_sockets(n1, 1, e1, 8)
30 var g1 := DCTerrainGeneratorProcNodes.new()
31 g1.set_graph(good)
32 _ck("good graph: compiled", g1.has_compiled_shader(), "has_compiled_shader() true")
33 _ck("good graph: no error", g1.get_last_compile_error() == "",
34 '"%s"' % g1.get_last_compile_error())
35
36 # C. a graph proc-nodes REFUSES to emit. VolumeToMesh outputs Geometry,
37 # which has no GLSL spelling, so it can never acquire one by someone
38 # doing the next item on a list -- the same reason glsl_golden_gate.gd
39 # uses it for its refusal case.
40 var bad := ProcNodeGraph.new()
41 var n2 := bad.add_node(GradientNoiseNode.new())
42 var e2 := bad.add_node(ErosionNoiseNode.new())
43 bad.connect_sockets(n2, 0, e2, 7)
44 bad.connect_sockets(n2, 1, e2, 8)
45 bad.add_node(VolumeToMeshNode.new())
46 var g2 := DCTerrainGeneratorProcNodes.new()
47 g2.set_graph(bad)
48 _ck("refused graph: not compiled", not g2.has_compiled_shader(),
49 "has_compiled_shader() false")
50 _ck("refused graph: says REFUSED",
51 g2.get_last_compile_error().contains("REFUSED"),
52 '"%s"' % g2.get_last_compile_error())
53
54 # D. THE BUILT-IN SUBSTITUTION. A DCTerrainGenerator that does not override
55 # get_native_generator() returns null, and DCTerrain quietly swaps in its
56 # own noise generator -- which renders a plausible landscape, so the
57 # symptom is "the terrain is wrong somehow" and never points here.
58 var t := DCTerrain.new()
59 _ck("no generator: builtin, honestly", t.is_using_builtin_generator(),
60 "is_using_builtin_generator() true")
61 t.set_generator(g1)
62 _ck("real generator: not builtin", not t.is_using_builtin_generator(),
63 "is_using_builtin_generator() false")
64 var plain := DCTerrainGenerator.new()
65 t.set_generator(plain)
66 _ck("generator with no native: builtin", t.is_using_builtin_generator(),
67 "is_using_builtin_generator() true, and an error was pushed")
68
69 print("\n%d/%d passed" % [9 - _fail, 9])
70 quit(1 if _fail > 0 else 0)
src/dc_terrain.cpp +34 -2
……@@ -1969,14 +1969,39 @@ void DCTerrain::_propagate_config() {
19691969 DCGenerator* native = generator_resource_->get_native_generator();
19701970 if (native) {
19711971 generator_ptr_ = native;
1972 warned_generator_fallback_ = false;
1973 using_builtin_generator_ = false;
19721974 } else {
1973 generator_ptr_ = &noise_generator_;
1975 _fallback_to_builtin("_propagate_config");
19741976 }
19751977 } else if (!generator_ptr_) {
19761978 generator_ptr_ = &noise_generator_;
19771979 }
19781980}
19791981
1982// A GENERATOR RESOURCE THAT CANNOT PROVIDE AN EVALUATOR IS A CONFIGURATION
1983// ERROR, and it used to be a silent one.
1984//
1985// The built-in noise generator produces a perfectly plausible landscape, so the
1986// substitution shows up as "the terrain is wrong somehow" rather than as
1987// anything pointing at the generator. Say it once per transition -- these paths
1988// run on every config propagation, and a message per frame is a message nobody
1989// reads. hyper-games #64 NEXT 4.
1990void DCTerrain::_fallback_to_builtin(const char* where) {
1991 generator_ptr_ = &noise_generator_;
1992 using_builtin_generator_ = true;
1993 if (warned_generator_fallback_) {
1994 return;
1995 }
1996 warned_generator_fallback_ = true;
1997 UtilityFunctions::push_error(
1998 "DCTerrain (", where, "): the assigned generator resource (",
1999 generator_resource_.is_valid() ? generator_resource_->get_class() : String("<null>"),
2000 ") returned no native generator, so the BUILT-IN NOISE generator is in "
2001 "use. The terrain you see is not the one this resource describes. A "
2002 "DCTerrainGenerator subclass must override get_native_generator().");
2003}
2004
19802005void DCTerrain::_drain_workers() {
19812006 // Wait for any workers that are mid-_process_cell() to finish.
19822007 // This is critical: workers hold a pointer to density_ and read
……@@ -4145,8 +4170,10 @@ void DCTerrain::set_generator(const Ref<DCTerrainGenerator> &p_generator) {
41454170 DCGenerator* native = generator_resource_->get_native_generator();
41464171 if (native) {
41474172 generator_ptr_ = native;
4173 warned_generator_fallback_ = false;
4174 using_builtin_generator_ = false;
41484175 } else {
4149 generator_ptr_ = &noise_generator_;
4176 _fallback_to_builtin("set_generator");
41504177 }
41514178 // Connect to generator's changed signal so parameter edits trigger rebuild
41524179 generator_resource_->connect("changed", callable_mp(this, &DCTerrain::_on_generator_changed));
……@@ -4345,6 +4372,11 @@ void DCTerrain::_bind_methods() {
43454372 // rebuild()
43464373 ClassDB::bind_method(D_METHOD("rebuild"), &DCTerrain::rebuild);
43474374 ClassDB::bind_method(D_METHOD("warmup_shaders"), &DCTerrain::warmup_shaders);
4375 // ANSWERABLE, not just printed. A gate cannot see push_error, and the
4376 // built-in noise generator renders a plausible landscape -- so without this
4377 // there is no way for a test, or a map script, to ask whether the terrain
4378 // on screen is the one the assigned generator describes.
4379 ClassDB::bind_method(D_METHOD("is_using_builtin_generator"), &DCTerrain::is_using_builtin_generator);
43484380 ClassDB::bind_method(D_METHOD("sample_density", "position"), &DCTerrain::sample_density);
43494381 ClassDB::bind_method(D_METHOD("debug_gpu_density", "origin", "voxel_size", "cells"), &DCTerrain::debug_gpu_density);
43504382 ClassDB::bind_method(D_METHOD("debug_collision_faces", "center", "radius"), &DCTerrain::debug_collision_faces);
src/dc_terrain.h +5 -0
……@@ -180,6 +180,7 @@ public:
180180 int get_splits_this_frame() const;
181181
182182 // -- Density sampling (for spawn placement, etc.) --
183 bool is_using_builtin_generator() const { return using_builtin_generator_; }
183184 float sample_density(const Vector3 &position) const;
184185
185186 // THE GPU ORACLE. Runs the real density pipeline -- the same shader, the
……@@ -379,6 +380,10 @@ private:
379380 // Generator abstraction — routes evaluate() through DCGenerator interface
380381 DCGeneratorNoise noise_generator_; // default noise generator (wraps density_)
381382 DCGenerator* generator_ptr_ = nullptr; // active generator pointer
383 // Warn-once latch for the built-in-generator substitution below.
384 bool warned_generator_fallback_ = false;
385 bool using_builtin_generator_ = true;
386 void _fallback_to_builtin(const char *where);
382387 Ref<DCTerrainGenerator> generator_resource_; // user-assigned Resource (inspector/script)
383388
384389 // Octree clipmap (replaces DCChunkManager)
src/dc_terrain_generator_procnodes.cpp +43 -1
……@@ -21,6 +21,10 @@
2121namespace godot {
2222
2323void DCTerrainGeneratorProcNodes::_bind_methods() {
24 // Bound so a gate can ASK whether the graph reached the GPU, instead of a
25 // human having to spot an error line. hyper-games #64 NEXT 4.
26 ClassDB::bind_method(D_METHOD("get_last_compile_error"), &DCTerrainGeneratorProcNodes::get_last_compile_error);
27 ClassDB::bind_method(D_METHOD("has_compiled_shader"), &DCTerrainGeneratorProcNodes::has_compiled_shader);
2428 ClassDB::bind_method(D_METHOD("set_erosion_minimal", "value"), &DCTerrainGeneratorProcNodes::set_erosion_minimal);
2529 ClassDB::bind_method(D_METHOD("get_erosion_minimal"), &DCTerrainGeneratorProcNodes::get_erosion_minimal);
2630 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "erosion_minimal"), "set_erosion_minimal", "get_erosion_minimal");
……@@ -730,10 +734,32 @@ void DCTerrainGeneratorProcNodes::_get_property_list(List<PropertyInfo> *p_list)
730734
731735// ── GLSL Compilation ──────────────────────────────────────────────────────────
732736
737// ============================================================================
738// A COMPILE FAILURE HERE IS SILENT, AND SILENCE IS THE BUG.
739//
740// Every early return below leaves cached_glsl_ empty. has_gpu_shader() then
741// answers false, dc_worker_cpu takes the CPU path, and DCTerrain may swap in
742// the BUILT-IN noise generator (dc_terrain.cpp, _propagate_config and
743// set_generator) -- so the map renders a plausible landscape that is not the
744// one the graph describes, with nothing in the log.
745//
746// proc-nodes' compile_to_glsl_function REFUSES on purpose: a node that claims
747// GPU support and emits nothing would leave a variable nothing declares, so it
748// returns "" and pushes its own error. That refusal used to arrive here and
749// vanish. It does not any more.
750//
751// last_compile_error_ is bound so a headless gate can ASK, rather than a human
752// having to notice a line scroll past. See hyper-games #64 NEXT 4.
753// ============================================================================
733754void DCTerrainGeneratorProcNodes::_recompile_glsl() {
734755 cached_glsl_.clear();
756 last_compile_error_ = String();
735757
736758 if (graph_.is_null()) {
759 last_compile_error_ = "no graph assigned";
760 // Not an error: an unconfigured generator is a legitimate state, and a
761 // scene that has not been wired up yet would scream on every load. It
762 // is recorded so a gate can still tell this apart from a real failure.
737763 return;
738764 }
739765
……@@ -761,12 +787,28 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
761787 }
762788 Variant glsl_var = graph_->call("compile_to_glsl_function", "proc_density", true, skip);
763789 if (glsl_var.get_type() != Variant::STRING) {
764 UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: compile_to_glsl_function returned non-string");
790 last_compile_error_ = "compile_to_glsl_function returned non-string "
791 "(is the proc-nodes extension loaded?)";
792 UtilityFunctions::push_error(
793 "DCTerrainGeneratorProcNodes: ", last_compile_error_,
794 " -- no GPU shader. The terrain will not be the graph's.");
765795 return;
766796 }
767797
768798 String proc_glsl = glsl_var;
769799 if (proc_glsl.is_empty()) {
800 // THE REFUSAL. proc-nodes returns "" when a node claims GPU support and
801 // emits no GLSL, and it has already said which node. What it cannot know
802 // is that the caller was a terrain generator, so say what the refusal
803 // COSTS -- that is the half that was missing.
804 last_compile_error_ = "compile_to_glsl_function REFUSED (a node claims "
805 "GPU support and emitted no GLSL -- see the proc-nodes error "
806 "above it for which)";
807 UtilityFunctions::push_error(
808 "DCTerrainGeneratorProcNodes: ", last_compile_error_,
809 ". No GPU shader: this terrain will mesh from the CPU wrapper, "
810 "which is a hardcoded gradient-noise -> erosion pipeline and NOT "
811 "your graph.");
770812 return;
771813 }
772814
src/dc_terrain_generator_procnodes.h +14 -0
……@@ -324,6 +324,17 @@ private:
324324
325325 void _recompile_glsl();
326326
327 // WHY THE LAST COMPILE PRODUCED NOTHING, empty when it produced something.
328 // Bound so a headless gate can assert on it: "visible" has to mean
329 // answerable, not just printed. See _recompile_glsl in the .cpp.
330 //
331 // ⚠ THE INITIAL VALUE IS NOT EMPTY, and that is the point. _recompile_glsl
332 // only runs from set_graph()/_set(), so a generator nobody has configured
333 // has never attempted a compile -- and "" would make that indistinguishable
334 // from "compiled cleanly". Four states, four answers: "never compiled",
335 // "no graph assigned", a specific failure, or empty for success.
336 String last_compile_error_ = "never compiled (no graph has been assigned)";
337
327338protected:
328339 static void _bind_methods();
329340
……@@ -335,6 +346,9 @@ protected:
335346public:
336347 DCGenerator *get_native_generator() override { return &bridge_; }
337348
349 String get_last_compile_error() const { return last_compile_error_; }
350 bool has_compiled_shader() const { return !cached_glsl_.empty(); }
351
338352 void set_graph(const Ref<Resource> &p_graph);
339353 Ref<Resource> get_graph() const { return graph_; }
340354