| … | … | @@ -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. | |
| 7 | extends SceneTree | |
| 8 | var _fail := 0 | |
| 9 | func _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 | ||
| 13 | func _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) |
| … | … | @@ -1969,14 +1969,39 @@ void DCTerrain::_propagate_config() { |
| 1969 | 1969 | DCGenerator* native = generator_resource_->get_native_generator(); |
| 1970 | 1970 | if (native) { |
| 1971 | 1971 | generator_ptr_ = native; |
| 1972 | warned_generator_fallback_ = false; | |
| 1973 | using_builtin_generator_ = false; | |
| 1972 | 1974 | } else { |
| 1973 | generator_ptr_ = &noise_generator_; | |
| 1975 | _fallback_to_builtin("_propagate_config"); | |
| 1974 | 1976 | } |
| 1975 | 1977 | } else if (!generator_ptr_) { |
| 1976 | 1978 | generator_ptr_ = &noise_generator_; |
| 1977 | 1979 | } |
| 1978 | 1980 | } |
| 1979 | 1981 | |
| 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. | |
| 1990 | void 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 | ||
| 1980 | 2005 | void DCTerrain::_drain_workers() { |
| 1981 | 2006 | // Wait for any workers that are mid-_process_cell() to finish. |
| 1982 | 2007 | // This is critical: workers hold a pointer to density_ and read |
| … | … | @@ -4145,8 +4170,10 @@ void DCTerrain::set_generator(const Ref<DCTerrainGenerator> &p_generator) { |
| 4145 | 4170 | DCGenerator* native = generator_resource_->get_native_generator(); |
| 4146 | 4171 | if (native) { |
| 4147 | 4172 | generator_ptr_ = native; |
| 4173 | warned_generator_fallback_ = false; | |
| 4174 | using_builtin_generator_ = false; | |
| 4148 | 4175 | } else { |
| 4149 | generator_ptr_ = &noise_generator_; | |
| 4176 | _fallback_to_builtin("set_generator"); | |
| 4150 | 4177 | } |
| 4151 | 4178 | // Connect to generator's changed signal so parameter edits trigger rebuild |
| 4152 | 4179 | generator_resource_->connect("changed", callable_mp(this, &DCTerrain::_on_generator_changed)); |
| … | … | @@ -4345,6 +4372,11 @@ void DCTerrain::_bind_methods() { |
| 4345 | 4372 | // rebuild() |
| 4346 | 4373 | ClassDB::bind_method(D_METHOD("rebuild"), &DCTerrain::rebuild); |
| 4347 | 4374 | 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); | |
| 4348 | 4380 | ClassDB::bind_method(D_METHOD("sample_density", "position"), &DCTerrain::sample_density); |
| 4349 | 4381 | ClassDB::bind_method(D_METHOD("debug_gpu_density", "origin", "voxel_size", "cells"), &DCTerrain::debug_gpu_density); |
| 4350 | 4382 | ClassDB::bind_method(D_METHOD("debug_collision_faces", "center", "radius"), &DCTerrain::debug_collision_faces); |
| … | … | @@ -180,6 +180,7 @@ public: |
| 180 | 180 | int get_splits_this_frame() const; |
| 181 | 181 | |
| 182 | 182 | // -- Density sampling (for spawn placement, etc.) -- |
| 183 | bool is_using_builtin_generator() const { return using_builtin_generator_; } | |
| 183 | 184 | float sample_density(const Vector3 &position) const; |
| 184 | 185 | |
| 185 | 186 | // THE GPU ORACLE. Runs the real density pipeline -- the same shader, the |
| … | … | @@ -379,6 +380,10 @@ private: |
| 379 | 380 | // Generator abstraction — routes evaluate() through DCGenerator interface |
| 380 | 381 | DCGeneratorNoise noise_generator_; // default noise generator (wraps density_) |
| 381 | 382 | 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); | |
| 382 | 387 | Ref<DCTerrainGenerator> generator_resource_; // user-assigned Resource (inspector/script) |
| 383 | 388 | |
| 384 | 389 | // Octree clipmap (replaces DCChunkManager) |
| … | … | @@ -21,6 +21,10 @@ |
| 21 | 21 | namespace godot { |
| 22 | 22 | |
| 23 | 23 | void 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); | |
| 24 | 28 | ClassDB::bind_method(D_METHOD("set_erosion_minimal", "value"), &DCTerrainGeneratorProcNodes::set_erosion_minimal); |
| 25 | 29 | ClassDB::bind_method(D_METHOD("get_erosion_minimal"), &DCTerrainGeneratorProcNodes::get_erosion_minimal); |
| 26 | 30 | 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) |
| 730 | 734 | |
| 731 | 735 | // ── GLSL Compilation ────────────────────────────────────────────────────────── |
| 732 | 736 | |
| 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 | // ============================================================================ | |
| 733 | 754 | void DCTerrainGeneratorProcNodes::_recompile_glsl() { |
| 734 | 755 | cached_glsl_.clear(); |
| 756 | last_compile_error_ = String(); | |
| 735 | 757 | |
| 736 | 758 | 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. | |
| 737 | 763 | return; |
| 738 | 764 | } |
| 739 | 765 | |
| … | … | @@ -761,12 +787,28 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() { |
| 761 | 787 | } |
| 762 | 788 | Variant glsl_var = graph_->call("compile_to_glsl_function", "proc_density", true, skip); |
| 763 | 789 | 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."); | |
| 765 | 795 | return; |
| 766 | 796 | } |
| 767 | 797 | |
| 768 | 798 | String proc_glsl = glsl_var; |
| 769 | 799 | 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."); | |
| 770 | 812 | return; |
| 771 | 813 | } |
| 772 | 814 |
| … | … | @@ -324,6 +324,17 @@ private: |
| 324 | 324 | |
| 325 | 325 | void _recompile_glsl(); |
| 326 | 326 | |
| 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 | ||
| 327 | 338 | protected: |
| 328 | 339 | static void _bind_methods(); |
| 329 | 340 | |
| … | … | @@ -335,6 +346,9 @@ protected: |
| 335 | 346 | public: |
| 336 | 347 | DCGenerator *get_native_generator() override { return &bridge_; } |
| 337 | 348 | |
| 349 | String get_last_compile_error() const { return last_compile_error_; } | |
| 350 | bool has_compiled_shader() const { return !cached_glsl_.empty(); } | |
| 351 | ||
| 338 | 352 | void set_graph(const Ref<Resource> &p_graph); |
| 339 | 353 | Ref<Resource> get_graph() const { return graph_; } |
| 340 | 354 |