| … | … | @@ -33,16 +33,24 @@ func _init() -> void: |
| 33 | 33 | _ck("good graph: no error", g1.get_last_compile_error() == "", |
| 34 | 34 | '"%s"' % g1.get_last_compile_error()) |
| 35 | 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. | |
| 36 | # C. a graph proc-nodes REFUSES to emit. | |
| 37 | # | |
| 38 | # ⛔ THIS USED VolumeToMesh AND WENT RED WHEN THAT NODE BECAME LEGITIMATE. | |
| 39 | # proc-nodes made it gpu_transparent() -- it NAMES a density rather than | |
| 40 | # failing to compute one -- so a graph containing it now compiles, and this | |
| 41 | # case silently stopped testing refusal. proc-nodes' own glsl_golden_gate.gd | |
| 42 | # had the identical dependency and was updated in the same commit; THIS gate | |
| 43 | # was not, and nothing re-ran it, so it sat at 7/9. | |
| 44 | # | |
| 45 | # Join Geometry instead: a density function is a float of a position, there | |
| 46 | # is no geometry in it to join, so this subject cannot be finished into | |
| 47 | # legitimacy by anybody. | |
| 40 | 48 | var bad := ProcNodeGraph.new() |
| 41 | 49 | var n2 := bad.add_node(GradientNoiseNode.new()) |
| 42 | 50 | var e2 := bad.add_node(ErosionNoiseNode.new()) |
| 43 | 51 | bad.connect_sockets(n2, 0, e2, 7) |
| 44 | 52 | bad.connect_sockets(n2, 1, e2, 8) |
| 45 | bad.add_node(VolumeToMeshNode.new()) | |
| 53 | bad.add_node(JoinGeometryNode.new()) | |
| 46 | 54 | var g2 := DCTerrainGeneratorProcNodes.new() |
| 47 | 55 | g2.set_graph(bad) |
| 48 | 56 | _ck("refused graph: not compiled", not g2.has_compiled_shader(), |
| … | … | @@ -0,0 +1,90 @@ |
| 1 | ## A proc-nodes ProcVolume, meshed ON THE GPU by dc-terrain's own pipeline. | |
| 2 | ## | |
| 3 | ## ⚠ NOT HEADLESS. The dummy rendering driver has no RenderingDevice, so there | |
| 4 | ## is no GPU thread and this would report failure for the wrong reason. Run with | |
| 5 | ## a real driver. | |
| 6 | ## | |
| 7 | ## ⚠ IT NEEDS demo/bin/linux/shaders. DCGpuMeshPipeline loads its .glsl files | |
| 8 | ## relative to the library, and dc-terrain/bin/linux has a `shaders` symlink | |
| 9 | ## while the demo project's did not -- so the mesh pipeline failed to open | |
| 10 | ## edge_classify.glsl, fell back to the CPU, and is_available() stayed false. | |
| 11 | ## demo/bin is gitignored, so that symlink is NOT in the repo: | |
| 12 | ## | |
| 13 | ## ln -s ../../../shaders demo/bin/linux/shaders | |
| 14 | ## | |
| 15 | ## ⚠ AND IT WAITS. GPU bring-up is not instant -- hyper-games #58 measured 30.2s | |
| 16 | ## for a starved second device, which is exactly the mistake this entry point | |
| 17 | ## avoids by borrowing the terrain's one device. So poll rather than assume. | |
| 18 | extends SceneTree | |
| 19 | ||
| 20 | var _t: DCTerrain = null | |
| 21 | var _glsl := "" | |
| 22 | var _frames := 0 | |
| 23 | var _done := false | |
| 24 | # ⚠ A TIME BUDGET, NOT A FRAME COUNT. The first version waited 601 FRAMES, and | |
| 25 | # an uncapped SceneTree burns those in a fraction of a second -- the GPU became | |
| 26 | # ready on the last line of the log, after every one of them had already failed. | |
| 27 | # Bring-up is seconds of wall clock; frames are not a unit of time here. | |
| 28 | var _t0 := 0.0 | |
| 29 | const TIMEOUT_S := 60.0 | |
| 30 | ||
| 31 | func _build_volume_glsl() -> String: | |
| 32 | var g := ProcNodeGraph.new() | |
| 33 | var s1 := g.add_node(SphereSdfNode.new()) | |
| 34 | var vtm := g.add_node(VolumeToMeshNode.new()) | |
| 35 | g.connect_sockets(s1, 0, vtm, 0) | |
| 36 | var geo = g.evaluate_socket(vtm, 0) | |
| 37 | if geo == null: | |
| 38 | return "" | |
| 39 | var vol = geo.get_volume() | |
| 40 | return "" if vol == null else vol.get_glsl_source() | |
| 41 | ||
| 42 | func _process(_d: float) -> bool: | |
| 43 | if _t == null: | |
| 44 | _glsl = _build_volume_glsl() | |
| 45 | print(" volume GLSL: %d chars" % _glsl.length()) | |
| 46 | _t = DCTerrain.new() | |
| 47 | _t.use_gpu = true | |
| 48 | root.add_child(_t) | |
| 49 | _t0 = Time.get_ticks_msec() | |
| 50 | return false | |
| 51 | _frames += 1 | |
| 52 | if _done: | |
| 53 | return true | |
| 54 | # 3 units across, centred on the origin: the unit sphere sits inside. | |
| 55 | var m: ArrayMesh = _t.bake_volume_mesh(_glsl, Vector3(-1.5, -1.5, -1.5), 3.0 / 16.0, 16) | |
| 56 | if m != null and m.get_surface_count() > 0: | |
| 57 | var arrays := m.surface_get_arrays(0) | |
| 58 | var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX] | |
| 59 | var idx: PackedInt32Array = arrays[Mesh.ARRAY_INDEX] | |
| 60 | var worst := 0.0 | |
| 61 | var lo := Vector3(1e9, 1e9, 1e9) | |
| 62 | var hi := Vector3(-1e9, -1e9, -1e9) | |
| 63 | for v in verts: | |
| 64 | worst = max(worst, abs(v.length() - 1.0)) | |
| 65 | lo = Vector3(min(lo.x, v.x), min(lo.y, v.y), min(lo.z, v.z)) | |
| 66 | hi = Vector3(max(hi.x, v.x), max(hi.y, v.y), max(hi.z, v.z)) | |
| 67 | print(" AABB: %v .. %v" % [lo, hi]) | |
| 68 | print(" first 3: %v %v %v" % [verts[0], verts[1], verts[2]]) | |
| 69 | # If the mesh comes back GRID-LOCAL, re-centring on the grid middle is | |
| 70 | # what makes it a unit sphere again. Measure both readings rather than | |
| 71 | # assume which one the pipeline returns. | |
| 72 | var worst_local := 0.0 | |
| 73 | for v in verts: | |
| 74 | worst_local = max(worst_local, abs((v - Vector3(1.5, 1.5, 1.5)).length() - 1.0)) | |
| 75 | print(" worst |r-1| world-reading=%.4f grid-local-reading=%.4f" % [worst, worst_local]) | |
| 76 | worst = min(worst, worst_local) | |
| 77 | print(" BAKED: %d verts, %d tris, worst |r-1| = %.4f (frame %d)" | |
| 78 | % [verts.size(), idx.size() / 3, worst, _frames]) | |
| 79 | print(" after %.1fs of GPU bring-up" % ((Time.get_ticks_msec() - _t0) / 1000.0)) | |
| 80 | print(" RESULT: %s" % ("PASS" if verts.size() > 0 and idx.size() > 0 and worst < 0.3 else "FAIL")) | |
| 81 | _done = true | |
| 82 | quit(0) | |
| 83 | return true | |
| 84 | var elapsed := (Time.get_ticks_msec() - _t0) / 1000.0 | |
| 85 | if elapsed > TIMEOUT_S: | |
| 86 | print(" RESULT: FAIL -- no mesh after %.1fs / %d frames (GPU never became available?)" | |
| 87 | % [elapsed, _frames]) | |
| 88 | quit(1) | |
| 89 | return true | |
| 90 | return false |
| … | … | @@ -3191,6 +3191,122 @@ PackedFloat32Array DCTerrain::debug_gpu_density(const Vector3 &origin, float vox |
| 3191 | 3191 | return out; |
| 3192 | 3192 | } |
| 3193 | 3193 | |
| 3194 | Ref<ArrayMesh> DCTerrain::bake_volume_mesh(const String &p_glsl, const Vector3 &p_origin, | |
| 3195 | float p_voxel_size, int p_cells) { | |
| 3196 | Ref<ArrayMesh> out; | |
| 3197 | if (p_glsl.is_empty()) { | |
| 3198 | UtilityFunctions::push_error( | |
| 3199 | "DCTerrain.bake_volume_mesh: no GLSL. A ProcVolume carries one only " | |
| 3200 | "after its graph compiles -- check ProcVolume.get_glsl_source()."); | |
| 3201 | return out; | |
| 3202 | } | |
| 3203 | // BRING THE THREAD UP IF IT IS NOT, into the SAME SLOT the terrain uses. | |
| 3204 | // | |
| 3205 | // _notification(ENTER_TREE) only creates it on the game-mode path: | |
| 3206 | // force_shared = is_editor_hint() && use_gpu, and an editor or | |
| 3207 | // script-launched instance takes the shared-worker branch and creates no | |
| 3208 | // GPU thread at all. A bake should not require the terrain to be streaming. | |
| 3209 | // | |
| 3210 | // Creating it HERE is safe precisely because it is the same member: if the | |
| 3211 | // terrain later enters game mode it finds gpu_thread_ non-null and reuses | |
| 3212 | // it. One device, ever. A second VkDevice is the 30.2 s starvation that | |
| 3213 | // left two meshers in one world (hyper-games #58). | |
| 3214 | if (!gpu_thread_) { | |
| 3215 | gpu_thread_ = std::make_unique<DCGpuThread>(); | |
| 3216 | gpu_thread_->start(); | |
| 3217 | } | |
| 3218 | if (!gpu_thread_->is_available()) { | |
| 3219 | // NON-BLOCKING ON PURPOSE. Bring-up takes seconds, and blocking a bound | |
| 3220 | // call for that freezes the frame -- the same "right total, wrong shape" | |
| 3221 | // mistake the worker deferral records. Return null and let the caller | |
| 3222 | // ask again. | |
| 3223 | UtilityFunctions::push_warning( | |
| 3224 | "DCTerrain.bake_volume_mesh: the GPU is still coming up. This " | |
| 3225 | "returns null rather than blocking the frame -- call again next " | |
| 3226 | "frame."); | |
| 3227 | return out; | |
| 3228 | } | |
| 3229 | if (p_cells < 1) p_cells = 1; | |
| 3230 | if (p_voxel_size <= 0.0f) p_voxel_size = 1.0f; | |
| 3231 | ||
| 3232 | // ⛔ THE ENTRY POINT IS evaluate_density(float,float,float), NOT | |
| 3233 | // proc_density(vec3). DCCompute::set_custom_shader's own comment says | |
| 3234 | // "Custom GLSL provides evaluate_density() + helpers", and the template | |
| 3235 | // calls that name with three floats. | |
| 3236 | // | |
| 3237 | // Handing it the graph's text unadapted does NOT fail loudly: the shader | |
| 3238 | // compiles (proc_density is simply an unused function) and the template's | |
| 3239 | // own default density runs instead. Measured 2026-09-06 -- a unit-sphere | |
| 3240 | // volume baked to 225 verts and 392 triangles with EVERY VERTEX AT y = 0. | |
| 3241 | // A flat plane, produced without one error line. | |
| 3242 | // | |
| 3243 | // DCTerrainGeneratorProcNodes writes this same adapter at | |
| 3244 | // dc_terrain_generator_procnodes.cpp:860; a volume needs only the trivial | |
| 3245 | // form, with no terrain height mapping, because a ProcVolume is already in | |
| 3246 | // world space. | |
| 3247 | baked_volume_glsl_ = std::string(p_glsl.utf8().get_data()); | |
| 3248 | baked_volume_glsl_ += | |
| 3249 | "\n\nfloat evaluate_density(float x, float y, float z) {\n" | |
| 3250 | "\treturn proc_density(vec3(x, y, z));\n" | |
| 3251 | "}\n"; | |
| 3252 | ||
| 3253 | DensityGrid grid; | |
| 3254 | float org[3] = { (float)p_origin.x, (float)p_origin.y, (float)p_origin.z }; | |
| 3255 | grid.allocate(p_cells, p_voxel_size, org); | |
| 3256 | ||
| 3257 | DCMeshData md; | |
| 3258 | const uint32_t quality = (get_use_adaptive_qef() ? 2u : 0u) | |
| 3259 | | (get_use_shortest_diagonal() ? 1u : 0u); | |
| 3260 | if (!gpu_thread_->submit(grid, density_, baked_volume_glsl_.c_str(), nullptr, &md, | |
| 3261 | org, p_voxel_size, quality, nullptr, 0, 0u)) { | |
| 3262 | UtilityFunctions::push_error( | |
| 3263 | "DCTerrain.bake_volume_mesh: the GPU declined the submit. If the " | |
| 3264 | "custom shader failed to compile, DCGpuThread prints that above."); | |
| 3265 | return out; | |
| 3266 | } | |
| 3267 | ||
| 3268 | const int vcount = (int)md.positions.size() / 3; | |
| 3269 | if (vcount == 0 || md.indices.empty()) { | |
| 3270 | // An empty result is a real answer -- a density with no zero crossing | |
| 3271 | // in these bounds -- and it is worth distinguishing from a failure. | |
| 3272 | UtilityFunctions::push_warning( | |
| 3273 | "DCTerrain.bake_volume_mesh: the GPU meshed no surface. The density " | |
| 3274 | "has no zero crossing inside the bounds given, so either the volume " | |
| 3275 | "is empty there or the bounds miss it."); | |
| 3276 | return out; | |
| 3277 | } | |
| 3278 | ||
| 3279 | PackedVector3Array pos; | |
| 3280 | pos.resize(vcount); | |
| 3281 | Vector3 *pw = pos.ptrw(); | |
| 3282 | for (int i = 0; i < vcount; i++) { | |
| 3283 | pw[i] = Vector3(md.positions[i * 3], md.positions[i * 3 + 1], md.positions[i * 3 + 2]); | |
| 3284 | } | |
| 3285 | PackedInt32Array idx; | |
| 3286 | idx.resize((int)md.indices.size()); | |
| 3287 | int32_t *iw = idx.ptrw(); | |
| 3288 | for (size_t i = 0; i < md.indices.size(); i++) { | |
| 3289 | iw[i] = md.indices[i]; | |
| 3290 | } | |
| 3291 | ||
| 3292 | Array arrays; | |
| 3293 | arrays.resize(Mesh::ARRAY_MAX); | |
| 3294 | arrays[Mesh::ARRAY_VERTEX] = pos; | |
| 3295 | arrays[Mesh::ARRAY_INDEX] = idx; | |
| 3296 | if ((int)md.normals.size() == vcount * 3) { | |
| 3297 | PackedVector3Array nrm; | |
| 3298 | nrm.resize(vcount); | |
| 3299 | Vector3 *nw = nrm.ptrw(); | |
| 3300 | for (int i = 0; i < vcount; i++) { | |
| 3301 | nw[i] = Vector3(md.normals[i * 3], md.normals[i * 3 + 1], md.normals[i * 3 + 2]); | |
| 3302 | } | |
| 3303 | arrays[Mesh::ARRAY_NORMAL] = nrm; | |
| 3304 | } | |
| 3305 | out.instantiate(); | |
| 3306 | out->add_surface_from_arrays(Mesh::PRIMITIVE_TRIANGLES, arrays); | |
| 3307 | return out; | |
| 3308 | } | |
| 3309 | ||
| 3194 | 3310 | float DCTerrain::sample_density(const Vector3 &position) const { |
| 3195 | 3311 | n_sample_density_.fetch_add(1, std::memory_order_relaxed); |
| 3196 | 3312 | float d = generator_ptr_->evaluate(position.x, position.y, position.z, 0.0f); |
| … | … | @@ -4409,6 +4525,7 @@ void DCTerrain::_bind_methods() { |
| 4409 | 4525 | // there is no way for a test, or a map script, to ask whether the terrain |
| 4410 | 4526 | // on screen is the one the assigned generator describes. |
| 4411 | 4527 | ClassDB::bind_method(D_METHOD("is_using_builtin_generator"), &DCTerrain::is_using_builtin_generator); |
| 4528 | ClassDB::bind_method(D_METHOD("bake_volume_mesh", "glsl", "origin", "voxel_size", "cells"), &DCTerrain::bake_volume_mesh); | |
| 4412 | 4529 | ClassDB::bind_method(D_METHOD("get_scalar_query_counts"), &DCTerrain::get_scalar_query_counts); |
| 4413 | 4530 | ClassDB::bind_method(D_METHOD("reset_scalar_query_counts"), &DCTerrain::reset_scalar_query_counts); |
| 4414 | 4531 | ClassDB::bind_method(D_METHOD("sample_density", "position"), &DCTerrain::sample_density); |
| … | … | @@ -208,6 +208,26 @@ public: |
| 208 | 208 | // in a real render session. |
| 209 | 209 | PackedFloat32Array debug_gpu_density(const Vector3 &origin, float voxel_size, int cells); |
| 210 | 210 | |
| 211 | // ── BAKE A VOLUME TO A MESH, ON THE GPU ───────────────────────────────── | |
| 212 | // | |
| 213 | // Takes the GLSL a proc-nodes ProcVolume carries -- `float proc_density(vec3 | |
| 214 | // pos)` -- fills a density grid with it and meshes it, both through the | |
| 215 | // SAME DCGpuThread the terrain already runs. Returns an ArrayMesh, or null | |
| 216 | // if the GPU declined. | |
| 217 | // | |
| 218 | // WHY HERE AND NOT IN proc-nodes. This needs DCGpuThread and a | |
| 219 | // RenderingDevice, which live in dc-terrain/src and cannot move to libdc | |
| 220 | // without making libdc godot-dependent. And a SECOND RenderingDevice is not | |
| 221 | // an option: a second VkDevice bring-up starves and left two meshers | |
| 222 | // running in one world (hyper-games #58). One device, one GPU thread, and | |
| 223 | // this borrows it. | |
| 224 | // | |
| 225 | // The GLSL takes a String rather than a ProcVolume because dc-terrain has | |
| 226 | // no compile-time dependency on proc-nodes' classes -- the same reason | |
| 227 | // DCTerrainGeneratorProcNodes reaches the graph through reflection. | |
| 228 | Ref<ArrayMesh> bake_volume_mesh(const String &p_glsl, const Vector3 &p_origin, | |
| 229 | float p_voxel_size, int p_cells); | |
| 230 | ||
| 211 | 231 | // THE COLLISION MESH, as triangles, so it can be drawn. |
| 212 | 232 | // |
| 213 | 233 | // Collision is built through PhysicsServer3D as bare RIDs hung off the |
| … | … | @@ -393,6 +413,12 @@ private: |
| 393 | 413 | // Warn-once latch for the built-in-generator substitution below. |
| 394 | 414 | bool warned_generator_fallback_ = false; |
| 395 | 415 | bool using_builtin_generator_ = true; |
| 416 | // ⚠ HOLDS THE BAKED GLSL ALIVE. DCGpuThread caches the custom_glsl POINTER | |
| 417 | // and compares by pointer, not by string -- so handing it a temporary's | |
| 418 | // c_str() leaves it holding a dangling address, and a later allocation | |
| 419 | // landing there would read as "same shader" and reuse the wrong compiled | |
| 420 | // one. Silently. This member outlives every submit. | |
| 421 | std::string baked_volume_glsl_; | |
| 396 | 422 | // mutable + atomic: the query entry points are const and DCCollisionSDF is |
| 397 | 423 | // reachable from more than one thread. |
| 398 | 424 | mutable std::atomic<uint64_t> n_sample_density_{0}; |