OverviewFilesCommitsIssuesDocsReleasesPeers

dc-terrain commits / 457b89c

procnodes: the graph decomposition, parked for review
Steps 1-6 of docs/spec-graph-composition.md: the GPU compiler honours the wiring (pn.in/pn.out tokens, per-socket names, scope blocks, Kahn order shared with the CPU executor), all 24 GPU nodes migrated, ErosionNoiseNode owns its 20 parameters as sockets, MountainNode and DetailNode extracted, the wrapper reduced to inv_scale/y_mapped/rescale/clip, 30 dead generator properties and the dormant lookdev-wrapper path deleted. PARKED because the field was never measured. Verification was compile- correctness plus one CPU column, which is not a measurement of the terrain, and the maps regressed in ways that could not be attributed with what was checked. Also carries the previous session's uncommitted work, which was NOT mine and is the state master returns to. Findings worth keeping regardless of this branch's fate: - gpu_custom_data_count() returns the tile SIDE while set_custom_data() SQUARES it, so a mip pyramid uploads only level 0. HBLK_OFF[1] = 1052676 is exactly one float past the uploaded buffer, so every LOD coarser than the block's 30 m pitch reads zeros and comes out flat. - the heightfield_only diagnostic still emits hblk_data, which no longer exists now that dc-terrain transports opaque custom_data. - nine math nodes emitted bare expressions and could never compile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
457b89cf581c728b9b60c0e6fb2d1b60f7ce4fd7
by plankatron <fleeky@gmail.com> · 2026-09-02T01:17:08.000Z

Changed files (26) +2138 -297

docs/postmortem-2026-07-04-lod-convergence.md +117 -0
……@@ -0,0 +1,117 @@
1# Post-mortem — dc-terrain LOD won't converge near edits (2026-07-04)
2
3A multi-day, ~10-deploy debugging saga (#116). The terrain LOD would not converge
4around the play area of an edited room: coarse chunks (blue/yellow/orange over green
5LOD0) persisted near the player for 1–2 minutes or indefinitely, "randomly stuck in
6places," fast far from origin. It took a full 4-part audit + a one-line isolation test
7to find the real cause, after a long string of wrong guesses. This documents the real
8root cause, the four fixes, the dead-ends, and the process failures — so we never spend
9three days on this class of bug again.
10
11## The real root cause (verified)
12
13**A global "kill-switch" suppressed *all* LOD pre-mesh generation whenever any edit was
14in flight, and a perpetual edit backlog held that switch on.**
15
16- `dc_terrain.cpp` had `if (BLOCKING_SWAP && !edits_active) _dispatch_pre_mesh_requests(...)`.
17 In `LOD_BLOCKING_SWAP`, a coarse chunk can only split once **all 8 of its children's
18 pre-meshes are cached** (`dc_octree_clipmap.cpp:360-395`). Those child meshes come
19 ONLY from `_dispatch_pre_mesh_requests`. So while `edits_active`, pre-mesh = 0 →
20 no child meshes → no splits → **coarse LOD frozen**.
21- The backlog that held `edits_active` true: **476 durable edits replayed at spawn**,
22 each calling `edit_terrain`/`edit_terrain_box` which set `node->edit_dirty=true` →
23 routed the chunk to the worker **priority lane**; PLUS a **3-second `state.list` poll**
24 (`keet_game_state.gd`) that re-emitted edits (whose optimistic cache differed from the
25 daemon `var_to_str` round-trip) and re-fed the priority lane forever. Near the edit
26 cluster the lane never drained, so pre-mesh stayed at zero.
27- **Fresh room (no edits) → empty lane → converged instantly.** That one test (below)
28 was the proof.
29
30## The four fixes (all shipped)
31
321. **`f87d8f5` — disable `content_aware_lod` + gate the seam on `is_leaf()`.** A separate
33 pre-existing thrash: `content_aware_lod=true` (forced on over the header default)
34 inverted the split/merge hysteresis — the empty-cell merge collapse (`merge_factor =
35 lod_factor*0.5`) dropped the merge threshold *below* the split threshold, so mostly-
36 empty volume oscillated split↔merge forever. Disabling it (matching the header default
37 and the already-disabled `use_screen_space_error`, whose comment documents this exact
38 thrash) removed the churn. Also: a coarse **seam** mesh leaked onto internal nodes
39 because the seam path gated on `state==ACTIVE` but not `is_leaf()`.
402. **`a67089b` — fair-share: delete the pre-mesh kill-switch.** Edits already hold the
41 worker PRIORITY lane (workers drain it before the NORMAL lane pre-mesh uses), and
42 `_dispatch_pre_mesh_requests` self-caps at `TARGET_QUEUE_DEPTH` while guaranteeing
43 `MIN_PREMESH_PER_FRAME`. So pre-mesh can always run on spare capacity while edits keep
44 priority. The `!edits_active` suppression was redundant for responsiveness and was the
45 thing starving LOD. → near-edit LOD went from "stuck 1–2 min" to "converges."
463. **`d43abf5` — scope edit priority to IN-THE-MOMENT edits only.** Threaded an
47 `interactive` flag (default true) through `edit_terrain`/`edit_terrain_box`/
48 `stamp_mesh_*`. Replayed + poll-synced edits are **landscape** — already baked into
49 `config_.edits` — so they pass `interactive=false`: no `edit_dirty` (normal lane, no
50 priority flood) and, for mesh stamps, **no `edit_focus_points` push**. Only a live
51 carve gets priority + focus. → the room converges *fast*, like the fresh room.
524. **`d0a5528` — sub-LOD (−2) at stamps: distance-driven + LOD0-gated.** Negative LODs
53 come ONLY from `edit_focus_points` → `min_lod=-2` (`dc_octree_clipmap.cpp:353`), added
54 ONLY by mesh stamps. They were non-convergent because the focus point *also* collapsed
55 `effective_dist` to ~0 (split regardless of camera distance), which fought the
56 real-distance merge → thrash. Fix: split by **actual** distance (drop the collapse) so
57 −2 refines near / merges far / converges everywhere; and gate `min_lod=-2` behind a new
58 `lod0_settled` (world reaches LOD0 first, THEN stamps push to −2).
59
60## Dead-ends (what was chased and shipped-then-reverted)
61
62- **`157f2af` / `bfec2ff` / `bbab9df` — "keep traverse awake" / `wants_finer`
63 (reverted twice).** Diagnosis: the `settled` latch under-counts async work → tree stops
64 mid-refine. Real but SECONDARY, and forcing the tree awake exposed/worsened the
65 `content_aware` thrash and later regressed spawn (never settling, churn). The settle
66 latch was a symptom-mask, not the disease.
67- **"Coarse mesh not freed" / stale-parent / seam-leak theories.** A one-shot ungated
68 `[STALEDIAG]` scan (`bb88196`) that walked the tree for internal nodes still owning a
69 mesh returned **zero** — definitively ruling out "unfreed parent mesh." The coarse-over-
70 fine was **under-refinement** (leaves that never split), not leftover meshes.
71- **morton / negative-coordinate / "origin bug" theory.** An audit proved it false with
72 arithmetic: octree extent 896 ≪ 2^20, so negative-coord residues `[1048128,1048575]`
73 are disjoint from positive `[0,447]` — no morton collision possible. The "origin" was a
74 red herring: the origin is just where the dense edit load sits.
75- **Sub-agent contradiction.** Audit-1 assumed the edits were mesh-stamps (→ its
76 focus-point root cause). Audit-3 read the actual stored data: they're **box/sphere
77 carves** that push **no** focus points. Audit-1's mechanism didn't apply. Never act on
78 one agent's *assumption*; reconcile against source.
79
80## What actually broke the loop
81
821. **The fresh-room isolation test** (user joined a new, unedited room → "it converges").
83 Minimal reproduction: strip the edits → the cause is the edits. Decisive in one try.
842. **A 4-part parallel deep audit** (LOD core / GPU+edit-cost / edit-persistence /
85 coordinate) — run only after the user demanded "a complete 100% audit of your
86 understanding." Solo reasoning had failed for days; the audit found the kill-switch.
873. **Instrument, don't guess** (`[STALEDIAG]`, `[LODDIAG]`) killed two wrong theories
88 with data instead of argument.
894. **The user's own design instincts** were sharper than mine: "only edits *in the moment*
90 matter," and "after everything reaches 0, continue to −2 for the special stamps."
91
92## Process failures (the honest part)
93
94- **Guess-deployed ~10 times over days**, each fix built on an unverified assumption,
95 reverting repeatedly. Should have STOPPED after the 2nd wrong deploy and done the audit.
96- **Anchored on the wrong subsystem** (the octree settle-latch) because the first plan
97 (#119) pointed there — and kept patching it instead of re-characterizing.
98- **Trusted a sub-agent's assumption** ("mesh stamps") without checking the data.
99- The lesson (again): **when a fix doesn't work, do not ship another guess. Isolate the
100 variable, instrument, and audit the whole path BEFORE the next change.** The user had to
101 force this by stopping the tangents.
102
103## Load-bearing facts for future LOD work
104
105- LOD mode is `BLOCKING_SWAP`: a split needs all 8 children pre-meshed first; pre-mesh is
106 the bottleneck and shares one GPU thread with edit remeshing.
107- Negative LODs (−1,−2) exist ONLY via `edit_focus_points` → `min_lod=-2`; focus points
108 are pushed ONLY by `stamp_mesh_obj`/`stamp_mesh_arrays` (mesh stamps), never carves.
109- `content_aware_lod` and `use_screen_space_error` both invert the hysteresis → thrash;
110 keep them off.
111- `GameState.erase_state` is **local-mirror-only** — it never issues a durable `state.del`
112 (`game_state.gd`), so `clear_edits`/`undo` don't actually delete durable edits; the 3s
113 poll resurrects them. (Separate bug, still open.)
114- The mesh-SDF BVH is rebuilt on EVERY dispatch of a mesh-stamp chunk (`dc_compute.cpp:1344`,
115 uncached) — a real perf bug for mesh-stamp rooms, not fixed here.
116
117Related: `postmortem-2026-07-03-edit-perf.md`, `reversible-baking-design.md`.
libdc/include/dc_generator.h +17 -0
……@@ -43,6 +43,23 @@ public:
4343 // Optional: GPU compute shader source (GLSL). Return nullptr for CPU-only.
4444 virtual bool has_gpu_shader() const { return false; }
4545 virtual const char* gpu_shader_source() const { return nullptr; }
46 // OPAQUE DATA for the custom shader. dc-terrain does not know or care
47 // what these floats mean -- a heightfield, a mask, a lookup table. It
48 // transports them to a storage buffer the density body may read, and
49 // the generator that supplied them owns their meaning.
50 //
51 // The pointer must stay valid and STABLE: the GPU thread re-uploads only
52 // when it changes, exactly as it recompiles only when the shader source
53 // changes.
54 virtual const float* gpu_custom_data() const { return nullptr; }
55 // ELEMENTS, not a side length. It was read as a side once -- DCCompute
56 // uploaded count*count floats -- which is correct only while the data is a
57 // plain square. A generator whose data is a MIP PYRAMID (1,403,226 floats
58 // for a 1026 tile) then had everything above level 0 left off the GPU, and
59 // the sampler read past the end of the buffer: zeros, a constant height,
60 // and terrain that is flat everywhere the LOD is coarse enough to reach a
61 // mip level. Return the number of floats behind gpu_custom_data().
62 virtual int gpu_custom_data_count() const { return 0; }
4663
4764 // Optional: bounding box hint for grid allocation.
4865 // Writes axis-aligned min/max into min[3]/max[3]. Returns false if unbounded.
libdc/include/dc_shader_source.h +11 -0
……@@ -177,6 +177,17 @@ layout(set = 0, binding = 12, std430) readonly buffer ConvexPlaneBuf {
177177 vec4 convex_planes[];
178178};
179179
180// --- Baked custom data (hyper-games #50 section 5.3) ---
181// A terrain-diffusion block, elevation in METRES, row-major, row = world Z.
182// The generator emits custom_data_height_m() around this with the block's geometry
183// baked in as literals, so no Params field is needed and the std140 layout --
184// which is declared here, in edge_intersect.glsl and in C++, and desynchronises
185// SILENTLY -- is untouched. Unbound, this is a one-float placeholder that
186// nothing references.
187layout(set = 0, binding = 13, std430) readonly buffer CustomDataBuf {
188 float custom_data[];
189};
190
180191// --- Mesh SDF helper functions ---
181192
182193float aabb_point_dist_sq(vec3 p, vec3 bmin, vec3 bmax) {
shaders/edge_intersect.glsl +65 -1
……@@ -135,6 +135,26 @@ layout(set = 0, binding = 9, std430) readonly buffer CountBuf { uint counts[]; }
135135// endpoints. A zero buffer is an exact no-op (d += 0). Binding 10 was free.
136136layout(set = 0, binding = 10, std430) readonly buffer BrickBuf { float brick_delta[]; };
137137
138// ---------------------------------------------------------------------------
139// EXTERNAL HEIGHTFIELD (hyper-games #50 section 5.3)
140//
141// A baked terrain-diffusion block, elevation in METRES, row-major with
142// row = world Z and column = world X. Bound in BOTH shaders the proc-nodes
143// density body is injected into, because that body references custom_data_height_m
144// and a shader missing it does not compile at all.
145//
146// The block's GEOMETRY (size, origin, pitch) is NOT here. It is baked into
147// the generator's emitted GLSL as literals, so this needs no Params field and
148// therefore cannot desynchronise the std140 layout between the two shaders and
149// the C++ struct -- which is the one failure in this area that is silent.
150// The generator recompiles whenever a property changes, so a new block is a
151// recompile, not a uniform update.
152//
153// Unbound, this is a one-float placeholder and the generator emits no
154// reference to it.
155layout(set = 0, binding = 13, std430) readonly buffer CustomDataBuf { float custom_data[]; };
156
157
138158layout(push_constant) uniform PushConstants {
139159 vec3 cell_origin;
140160 float voxel_size;
……@@ -345,7 +365,51 @@ void main() {
345365 vec3 pos = mix(p0, p1, t);
346366
347367 // ---- Surface normal via central differences (fused from Stage 5) ----
348 float eps = voxel_size * 0.01;
368 //
369 // HALF A VOXEL, NOT A HUNDREDTH, AND THIS IS THE FILE THAT MATTERS.
370 //
371 // The same fix was written into surface_normals.glsl, which _compile_all
372 // does not load: the two stages were fused into this one (see the header),
373 // so that file is retired and editing it changed nothing that runs. Naming
374 // the artifact a check examined is hyper-games #44's rule; this is the same
375 // failure applied to a fix instead of a check.
376 //
377 // WHY A HUNDREDTH OF A VOXEL CANNOT WORK. evaluate_density reaches the
378 // surface by subtracting two numbers that are both about 2.63 near the
379 // ground -- the field's height and y_mapped, each (metres + 10900) * 2.4e-4
380 // -- and only then multiplies the difference by 4200. Float32 has about
381 // 2.4e-7 of resolution at 2.63, so a 1 cm horizontal probe on gentle ground
382 // asks for a difference far below one ULP.
383 //
384 // MEASURED, tools/oracles/density_precision.py, float32, the shipping
385 // expression, voxel 1 m, against a float64 control:
386 //
387 // slope eps=0.01 voxel eps=0.5 voxel
388 // 0.003 n.x EXACTLY 0.0 4.005e-03 (0.06 deg err)
389 // 0.010 n.x EXACTLY 0.0 1.001e-02 (0.00)
390 // 0.030 n.x EXACTLY 0.0 2.904e-02 (0.05)
391 // 0.100 2.003e-03 (10x the 1.001e-01 (0.01)
392 // true 2.0e-04)
393 // 0.300 4.16 deg error 3.004e-01 (0.03)
394 //
395 // A zero n.x hands the QEF a plane that says the ground is level when it is
396 // not. Above the collapse the survivor is QUANTISED: n.x moves in steps of
397 // ~2e-3 against an n.y of 0.02, so the normal can only point in directions
398 // about 5.7 degrees apart, and a smooth hillside gets a dithered normal
399 // field -- the scaled, dimpled look, with isolated wrong-facing triangles
400 // where rounding tips one over.
401 //
402 // It is WORSE ON SMOOTH GROUND, which is why it shows on the baked
403 // heightfield map and not on the erosion map: erosion puts more than 5.7
404 // degrees of variation between neighbouring cells everywhere, so the
405 // quantisation is masked. A Catmull-Rom heightfield over 30 m texels does
406 // not.
407 //
408 // Half a voxel costs nothing DC could have represented -- it resolves at
409 // one voxel by construction -- and brush faces are unaffected: the exact
410 // analytic plane normal replaces this one below for convex hits, where it
411 // survives only as a sign reference.
412 float eps = voxel_size * 0.5;
349413 vec3 n;
350414 n.x = dc_sample_field(pos.x + eps, pos.y, pos.z) - dc_sample_field(pos.x - eps, pos.y, pos.z);
351415 n.y = dc_sample_field(pos.x, pos.y + eps, pos.z) - dc_sample_field(pos.x, pos.y - eps, pos.z);
shaders/surface_normals.glsl +16 -1
……@@ -139,7 +139,22 @@ void main() {
139139 float wz = edge_data[base + 2u];
140140
141141 // Central differences on the full SDF
142 float eps = voxel_size * 0.01;
142 // HALF A VOXEL, not a hundredth.
143 //
144 // evaluate_density reaches the surface through a large offset and a small
145 // scale -- (y - floor_y) * (height_scale / y_range), about 2.63 for a
146 // terrain 165 m up in a 21 km band. A 1 cm probe asks for a difference of
147 // ~1e-7 out of 2.63, which is below one float32 ULP: measured, the
148 // horizontal components came back EXACTLY 0.0 for every slope tested from
149 // 0.003 to 0.03. The normal then collapses to straight up and the mesh
150 // shows isolated wrong-facing triangles wherever rounding tips it -- worst
151 // on SMOOTH ground, where the true gradient is smallest.
152 //
153 // Half a voxel restores it (2.9e-2 against an exact 3.0e-2). DC already
154 // resolves features at one voxel, so probing at half of one costs nothing
155 // it could have represented; a hundredth of a voxel was asking float32 for
156 // precision it does not have.
157 float eps = voxel_size * 0.5;
143158 vec3 n;
144159 n.x = evaluate_density(wx + eps, wy, wz) - evaluate_density(wx - eps, wy, wz);
145160 n.y = evaluate_density(wx, wy + eps, wz) - evaluate_density(wx, wy - eps, wz);
src/dc_collision_trimesh.h +19 -0
……@@ -176,6 +176,25 @@ public:
176176 RID shape = ps->concave_polygon_shape_create();
177177 Dictionary shape_data;
178178 shape_data["faces"] = creq.faces;
179 // backface_collision IS REQUIRED, not optional.
180 //
181 // GodotPhysics3D defaults it when the key is absent, so
182 // omitting it worked and looked fine. Jolt does not:
183 // JoltConcavePolygonShape3D::set_data ERR_FAIL_CONDs unless
184 // the dictionary carries a BOOL under this key, and on that
185 // failure the shape keeps NO faces at all -- so every
186 // terrain chunk became an empty collision body and the world
187 // had no collision whatsoever, with the only symptom being
188 // 102 identical errors naming a file most people never open.
189 //
190 // false is the right value and it is measured, not assumed:
191 // hyper-tps scenes/test/trimesh_winding_gate.gd drops the
192 // shipping capsule on a floor of each winding and reports
193 // which one collides. A standable floor has (b-a)x(c-a)
194 // pointing DOWN, which is what the mesher emits, so the
195 // front face is the one you walk on and back faces should
196 // not collide.
197 shape_data["backface_collision"] = false;
179198 ps->shape_set_data(shape, shape_data);
180199 ps->body_add_shape(body, shape);
181200 cnode->collision_shape = shape;
src/dc_compute.cpp +69 -1
……@@ -151,9 +151,13 @@ bool DCCompute::set_custom_shader(const char* glsl) {
151151 return false;
152152 }
153153
154 // Free old pipeline and shader
154 // Free old pipeline and shader. Freeing a shader IMPLICITLY invalidates
155 // every uniform set built against it, so drop our handle without freeing
156 // it -- _rebuild_uniform_set would otherwise free it a second time, which
157 // is the "DOUBLE FREE (was uniform_set)" the log reports on every recompile.
155158 if (pipeline_.is_valid()) rd_->free_rid(pipeline_);
156159 if (shader_.is_valid()) rd_->free_rid(shader_);
160 uniform_set_ = RID();
157161
158162 shader_ = rd_->shader_create_from_spirv(spirv);
159163 if (!shader_.is_valid()) {
……@@ -179,6 +183,9 @@ void DCCompute::cleanup() {
179183 if (!rd_) return;
180184
181185 _destroy_buffers();
186 // The custom data outlives stride changes by design (see _destroy_buffers),
187 // so this is the only place it is released.
188 if (custom_data_buf_.is_valid()) { rd_->free_rid(custom_data_buf_); custom_data_buf_ = RID(); custom_data_n_ = 0; }
182189
183190 if (pipeline_.is_valid()) {
184191 rd_->free_rid(pipeline_);
……@@ -250,6 +257,15 @@ void DCCompute::_create_buffers(int stride) {
250257 // (edge_intersect). Alloc-once per stride like the other buffers.
251258 brick_buf_ = rd_->storage_buffer_create(buf_size, zero_data);
252259
260 // Baked custom data (binding 13): a one-float placeholder until
261 // set_custom_data() uploads a real block. A descriptor in the set MUST
262 // point at a live buffer even when the shader never reads it, so this
263 // cannot simply be left invalid.
264 if (!custom_data_buf_.is_valid()) {
265 PackedByteArray hz; hz.resize(4); memset(hz.ptrw(), 0, 4);
266 custom_data_buf_ = rd_->storage_buffer_create(4, hz);
267 }
268
253269 // Mesh BVH buffers: start with small empties (resized on first mesh upload)
254270 // Binding 8: BVH nodes (16-byte header + nodes)
255271 PackedByteArray bvh_zero;
……@@ -298,6 +314,13 @@ void DCCompute::_destroy_buffers() {
298314 rd_->free_rid(uniform_set_);
299315 uniform_set_ = RID();
300316 }
317 // NOT the custom data. _destroy_buffers runs whenever the grid STRIDE
318 // changes, and the custom data has nothing to do with the grid — it is the
319 // world, not the chunk. Freeing it here recreated it as the 4-byte
320 // placeholder in _create_buffers, while DCGpuThread's current_custom_data_ still
321 // held the same pointer and therefore never re-uploaded. The shader then
322 // read past a 4-float buffer, heightmap_gn returned a constant, and the
323 // terrain came out perfectly flat. It is released in cleanup() instead.
301324 if (density_buf_.is_valid()) { rd_->free_rid(density_buf_); density_buf_ = RID(); }
302325 if (nx_buf_.is_valid()) { rd_->free_rid(nx_buf_); nx_buf_ = RID(); }
303326 if (ny_buf_.is_valid()) { rd_->free_rid(ny_buf_); ny_buf_ = RID(); }
……@@ -320,6 +343,36 @@ void DCCompute::_destroy_buffers() {
320343 uniform_set_dirty_ = true;
321344}
322345
346// Upload a baked custom data block. Reallocates, so the uniform set is
347// invalidated -- a storage buffer cannot be resized in place and the old RID
348// stays referenced by the set until it is rebuilt.
349void DCCompute::set_custom_data(const float* data, int n) {
350 if (!rd_) return;
351 if (custom_data_buf_.is_valid()) { rd_->free_rid(custom_data_buf_); custom_data_buf_ = RID(); }
352 if (data == nullptr || n <= 0) {
353 PackedByteArray hz; hz.resize(4); memset(hz.ptrw(), 0, 4);
354 custom_data_buf_ = rd_->storage_buffer_create(4, hz);
355 custom_data_n_ = 0;
356 } else {
357 // n IS AN ELEMENT COUNT. This used to be n*n, which silently truncated
358 // any generator whose data is not a plain square -- see
359 // DCGenerator::gpu_custom_data_count.
360 size_t bytes = (size_t)n * sizeof(float);
361 PackedByteArray pb; pb.resize((int64_t)bytes);
362 memcpy(pb.ptrw(), data, bytes);
363 custom_data_buf_ = rd_->storage_buffer_create((uint32_t)bytes, pb);
364 custom_data_n_ = n;
365 float lo = data[0], hi = data[0];
366 for (size_t i = 1; i < (size_t)n; i++) {
367 if (data[i] < lo) lo = data[i];
368 if (data[i] > hi) hi = data[i];
369 }
370 fprintf(stderr, "DCCompute: custom data uploaded %dx%d (%zu bytes), range %.0f..%.0f\n",
371 n, n, bytes, (double)lo, (double)hi);
372 }
373 uniform_set_dirty_ = true;
374}
375
323376void DCCompute::_rebuild_uniform_set() {
324377 // Skip rebuild if buffers haven't changed (optimization 5)
325378 if (!uniform_set_dirty_ && uniform_set_.is_valid()) return;
……@@ -413,6 +466,13 @@ void DCCompute::_rebuild_uniform_set() {
413466 u11->set_binding(11);
414467 u11->add_id(brick_buf_);
415468
469 // binding 13: baked custom data SSBO
470 Ref<RDUniform> u13;
471 u13.instantiate();
472 u13->set_uniform_type(RenderingDevice::UNIFORM_TYPE_STORAGE_BUFFER);
473 u13->set_binding(13);
474 u13->add_id(custom_data_buf_);
475
416476 // binding 12: convex-brush planes SSBO
417477 Ref<RDUniform> u12;
418478 u12.instantiate();
……@@ -434,8 +494,16 @@ void DCCompute::_rebuild_uniform_set() {
434494 uniforms.push_back(u10);
435495 uniforms.push_back(u11);
436496 uniforms.push_back(u12);
497 uniforms.push_back(u13);
437498
438499 uniform_set_ = rd_->uniform_set_create(uniforms, shader_, 0);
500 if (!uniform_set_.is_valid()) {
501 // A set that fails to create means every dispatch after it reads
502 // nothing. Silent until now, and indistinguishable from a wrong field.
503 fprintf(stderr, "DCCompute: uniform_set_create FAILED (%d uniforms) — "
504 "density dispatches will not see their buffers\n",
505 (int)uniforms.size());
506 }
439507 uniform_set_dirty_ = false;
440508}
441509
src/dc_compute.h +6 -0
……@@ -96,6 +96,10 @@ public:
9696 RID perm_buf() const { return perm_buf_; }
9797 RID edit_buf() const { return edit_buf_; }
9898 RID brick_buf() const { return brick_buf_; }
99 RID custom_data_buf() const { return custom_data_buf_; }
100 // Upload a baked custom data block (metres, n*n row-major, row = Z).
101 // Pass n = 0 to release it back to the one-float placeholder.
102 void set_custom_data(const float* data, int n);
99103 RID bvh_node_buf() const { return bvh_node_buf_; }
100104 RID bvh_tri_buf() const { return bvh_tri_buf_; }
101105 RID bvh_idx_buf() const { return bvh_idx_buf_; }
……@@ -120,6 +124,8 @@ private:
120124 RID perm_buf_;
121125 RID edit_buf_;
122126 RID brick_buf_;
127 RID custom_data_buf_; // baked diffusion custom data; binding 13 in BOTH shaders
128 int custom_data_n_ = 0;
123129 RID bvh_node_buf_;
124130 RID bvh_tri_buf_;
125131 // Which mesh SDF the BVH buffers currently hold. upload_edits() runs PER
src/dc_gpu_mesh_pipeline.cpp +24 -2
……@@ -132,6 +132,7 @@ void DCGpuMeshPipeline::cleanup() {
132132 _free_shader_pipeline(sp_voxel_classify_);
133133 _free_shader_pipeline(sp_bake_planeset_);
134134 _free_shader_pipeline(sp_qef_solve_);
135 _free_shader_pipeline(sp_vertex_polish_);
135136 _free_shader_pipeline(sp_quad_emit_);
136137
137138 rd_ = nullptr;
……@@ -201,6 +202,7 @@ bool DCGpuMeshPipeline::_compile_all_shaders() {
201202 { "voxel_classify.glsl", &sp_voxel_classify_ },
202203 { "bake_planeset.glsl", &sp_bake_planeset_ },
203204 { "qef_solve.glsl", &sp_qef_solve_ },
205 { "vertex_polish.glsl", &sp_vertex_polish_ },
204206 { "quad_emit.glsl", &sp_quad_emit_ },
205207 };
206208
……@@ -458,7 +460,7 @@ void DCGpuMeshPipeline::_free_uniform_sets() {
458460 fr(us_edge_classify_); fr(us_compact_edge_); fr(us_prepare_edge_);
459461 fr(us_edge_intersect_);
460462 fr(us_voxel_classify_); fr(us_compact_voxel_); fr(us_prepare_voxel_);
461 fr(us_qef_solve_); fr(us_quad_emit_); fr(us_bake_planeset_);
463 fr(us_qef_solve_); fr(us_vertex_polish_); fr(us_quad_emit_); fr(us_bake_planeset_);
462464 for (int i = 0; i < 3; i++) { fr(us_scan_edge_[i]); fr(us_scan_voxel_[i]); }
463465 uniform_sets_valid_ = false; // 2a: any teardown forces a rebuild next prepare
464466}
……@@ -493,7 +495,7 @@ void DCGpuMeshPipeline::_build_uniform_sets(const DensityBuffers& bufs) {
493495 {3, edge_data_buf_},
494496 {4, bufs.params}, {5, bufs.voronoi}, {6, bufs.perm}, {7, bufs.edit},
495497 {8, active_edges_buf_}, {9, counts_buf_}, {10, bufs.brick},
496 {11, bufs.convex_planes}
498 {11, bufs.convex_planes}, {13, bufs.custom_data}
497499 }, true);
498500
499501 // Stage 7: voxel_classify
……@@ -524,6 +526,19 @@ void DCGpuMeshPipeline::_build_uniform_sets(const DensityBuffers& bufs) {
524526 {9, bufs.edit}, {10, bufs.convex_planes}, {11, planeset_buf_}
525527 });
526528
529 // Stage 9: VERTEX POLISH. Clamped gradient descent, pulling each QEF vertex
530 // onto the true isosurface. The shader has existed and never been
531 // dispatched. QEF on a near-planar surface is underdetermined in two axes --
532 // one vertical edge crosses and the horizontal ones do not -- so the solver
533 // falls back on centroid regularisation and the vertex lands wherever the
534 // cell centre is. That reads as scattered misplaced facets, and it survives
535 // dc_auto_smooth because a badly placed vertex makes faces beyond its 40
536 // degree threshold. Normal precision cannot touch it: the GEOMETRY is wrong.
537 us_vertex_polish_ = _make_uniform_set(sp_vertex_polish_.shader, {
538 {0, bufs.density}, {4, voxel_flags_buf_}, {5, voxel_scan_buf_},
539 {6, vertex_buf_}, {7, active_voxels_buf_}, {8, counts_buf_}
540 });
541
527542 // Plane-set BAKE (runs per cell before qef_solve): edits + convex planes in,
528543 // the per-cell plane-set brick out.
529544 us_bake_planeset_ = _make_uniform_set(sp_bake_planeset_.shader, {
……@@ -745,6 +760,13 @@ bool DCGpuMeshPipeline::dispatch_mesh_stages(int64_t cl,
745760 rd_->compute_list_dispatch_indirect(cl, dispatch_args_buf_, 12);
746761 rd_->compute_list_add_barrier(cl);
747762
763 // Stage 9b: Vertex Polish — refine the QEF positions onto the isosurface.
764 rd_->compute_list_bind_compute_pipeline(cl, sp_vertex_polish_.pipeline);
765 rd_->compute_list_bind_uniform_set(cl, us_vertex_polish_, 0);
766 rd_->compute_list_set_push_constant(cl, pc_bytes, sizeof(MeshPushConstants));
767 rd_->compute_list_dispatch_indirect(cl, dispatch_args_buf_, 12);
768 rd_->compute_list_add_barrier(cl);
769
748770 // Stage 11: Quad Emit
749771 rd_->compute_list_bind_compute_pipeline(cl, sp_quad_emit_.pipeline);
750772 rd_->compute_list_bind_uniform_set(cl, us_quad_emit_, 0);
src/dc_gpu_mesh_pipeline.h +3 -1
……@@ -34,6 +34,7 @@ struct DensityBuffers {
3434 RID brick; // edge_intersect binding 10: settled-edit SDF delta brick
3535 RID convex_planes; // edge_intersect binding 11: convex-brush half-space planes
3636 RID convex_faceproj; // bake_planeset binding 12: per-face projection index (parallel to convex_planes)
37 RID custom_data; // binding 13 in BOTH density_fill and edge_intersect: baked custom data
3738};
3839
3940class DCGpuMeshPipeline {
……@@ -92,6 +93,7 @@ private:
9293 ShaderPipeline sp_compact_index_;
9394 ShaderPipeline sp_prepare_dispatch_;
9495 ShaderPipeline sp_edge_intersect_; // recompiled when SDF changes
96 ShaderPipeline sp_vertex_polish_; // stage 9: clamped gradient descent
9597 ShaderPipeline sp_voxel_classify_;
9698 ShaderPipeline sp_qef_solve_;
9799 ShaderPipeline sp_quad_emit_;
……@@ -120,7 +122,7 @@ private:
120122 RID us_edge_classify_, us_compact_edge_, us_prepare_edge_;
121123 RID us_edge_intersect_;
122124 RID us_voxel_classify_, us_compact_voxel_, us_prepare_voxel_;
123 RID us_qef_solve_, us_quad_emit_;
125 RID us_qef_solve_, us_vertex_polish_, us_quad_emit_;
124126 RID us_bake_planeset_;
125127 // Scan uniform sets (edge scan: 3, voxel scan: 3)
126128 RID us_scan_edge_[3], us_scan_voxel_[3];
src/dc_gpu_thread.h +20 -1
……@@ -38,6 +38,8 @@ struct GpuFillRequest {
3838 DensityGrid* grid;
3939 const DCDensity* density;
4040 const char* custom_glsl = nullptr; // optional: proc-nodes compiled shader
41 const float* custom_data = nullptr; // optional: baked custom data (binding 13)
42 int custom_data_n = 0;
4143 const std::vector<TerrainEdit>* edits = nullptr; // terrain edits for GPU evaluation
4244 bool wants_gpu_mesh = false; // request GPU mesh generation (skip CPU DC)
4345 DCMeshData* mesh_output = nullptr; // where to write GPU-generated mesh
……@@ -98,7 +100,8 @@ public:
98100 const std::vector<TerrainEdit>* edits = nullptr,
99101 DCMeshData* mesh_output = nullptr,
100102 const float* cell_origin = nullptr, float voxel_size = 0.0f,
101 uint32_t mesh_quality_flags = 0) {
103 uint32_t mesh_quality_flags = 0,
104 const float* custom_data = nullptr, int custom_data_n = 0) {
102105 if (!running_.load() || !gpu_available_.load()) {
103106 return false;
104107 }
……@@ -112,6 +115,8 @@ public:
112115 req.grid = &grid;
113116 req.density = &density;
114117 req.custom_glsl = custom_glsl;
118 req.custom_data = custom_data;
119 req.custom_data_n = custom_data_n;
115120 req.edits = edits;
116121 if (mesh_output && cell_origin) {
117122 req.wants_gpu_mesh = true;
……@@ -149,6 +154,7 @@ private:
149154 DCGpuMeshPipeline mesh_pipeline_;
150155 bool mesh_pipeline_available_ = false;
151156 const char* current_custom_glsl_ = nullptr; // track to avoid recompiling same shader
157 const float* current_custom_data_ = nullptr; // same trick: re-upload only on change
152158
153159 void _run() {
154160 // Create the RenderingDevice on THIS thread (thread affinity).
……@@ -261,6 +267,18 @@ private:
261267 return;
262268 }
263269 current_custom_glsl_ = glsl;
270 // A new shader means a new uniform set, so force the custom data to
271 // be re-uploaded even if the generator handed back the same pointer.
272 current_custom_data_ = nullptr;
273 }
274
275 // Baked custom data, same change-detection contract as the shader above.
276 // Uploading reallocates the buffer and dirties the uniform set, so doing
277 // it per chunk would rebuild the set thousands of times a second.
278 const float* hb = batch[0].custom_data;
279 if (hb != current_custom_data_) {
280 compute_.set_custom_data(hb, batch[0].custom_data_n);
281 current_custom_data_ = hb;
264282 }
265283
266284 // Process each request INDIVIDUALLY with ITS OWN culled edit list.
……@@ -361,6 +379,7 @@ private:
361379 bufs.bvh_nodes = compute_.bvh_node_buf();
362380 bufs.bvh_tris = compute_.bvh_tri_buf();
363381 bufs.bvh_indices = compute_.bvh_idx_buf();
382 bufs.custom_data = compute_.custom_data_buf();
364383 mesh_pipeline_.prepare_mesh(grid_dims, bufs);
365384
366385 // Single compute list: density + mesh pipeline
src/dc_octree_clipmap.h +6 -0
……@@ -86,6 +86,12 @@ struct OctreeClipmapNode {
8686 State state = EMPTY;
8787
8888 RID mesh_instance;
89 // The origin HANDED TO instance_set_transform, recorded rather than
90 // recomputed. Comparing the visual surface against the collision surface is
91 // only evidence if both numbers come from what was actually used: the
92 // collision side is read back from PhysicsServer3D, so this side must be
93 // the value that was actually set, not the formula re-evaluated.
94 float mesh_origin[3] = { 0.0f, 0.0f, 0.0f };
8995 RID collision_body;
9096 RID collision_shape;
9197 RID normal_map_texture;
src/dc_terrain.cpp +203 -0
……@@ -721,10 +721,44 @@ void DCTerrain::_process(double delta) {
721721 std::vector<OctreeClipmapNode*> stk;
722722 stk.push_back(octree_clipmap_.get_root());
723723 int leaked = 0;
724 int col_leaked = 0;
724725 while (!stk.empty()) {
725726 OctreeClipmapNode* n = stk.back(); stk.pop_back();
726727 if (!n) continue;
727728 if (!n->is_leaf()) {
729 // COLLISION, WHICH THIS DIAGNOSTIC NEVER CHECKED.
730 //
731 // It reported internal nodes still DRAWING a coarse mesh and
732 // said nothing about internal nodes still COLLIDING with one --
733 // and the collision case is the worse of the two, because it is
734 // invisible. _on_mesh_free deliberately keeps a parent's
735 // collision alive while its children mesh, so a parent that
736 // never passes the Stage 2 gate leaves a coarse surface, 2
737 // voxels of overlap wide, sitting above the fine one you can
738 // see. A capsule between them collects contacts from both,
739 // barely penetrates either, and gets ejected: measured 5.64
740 // contacts per frame on ground whose steepest triangle is 11.8
741 // degrees, with a contact normal at 168.8 degrees -- a ceiling,
742 // on ground with no overhang anywhere in the visible mesh.
743 //
744 // debug_collision_faces cannot see it either: it walks
745 // get_leaves(), and a node that has split is not a leaf.
746 if (n->collision_body.is_valid()) {
747 int cbad = -1, cbstate = -1; float cbfade = -1.0f;
748 for (int i = 0; i < 8; i++) {
749 OctreeClipmapNode* c = n->children[i];
750 bool ok = c && (c->state == OctreeClipmapNode::ACTIVE ||
751 c->state == OctreeClipmapNode::EMPTY) && c->fade_t >= 0.99f;
752 if (!ok) { cbad = i; cbstate = c ? (int)c->state : -1; cbfade = c ? c->fade_t : -1.0f; break; }
753 }
754 if (col_leaked < 8) {
755 UtilityFunctions::print("[STALECOL] INTERNAL lod=", n->lod_level,
756 " still COLLIDING pos=(", n->position.x, ",", n->position.y, ",", n->position.z,
757 ") parent_collision_stale=", n->parent_collision_stale,
758 " blockChild=", cbad, " childState=", cbstate, " childFade=", cbfade);
759 }
760 col_leaked++;
761 }
728762 if (n->mesh_instance.is_valid() || n->seam_mesh_instance.is_valid()) {
729763 int bad = -1, bstate = -1; float bfade = -1.0f;
730764 for (int i = 0; i < 8; i++) {
……@@ -749,6 +783,10 @@ void DCTerrain::_process(double delta) {
749783 UtilityFunctions::print("[STALEDIAG] ", leaked,
750784 " internal nodes still drawing a coarse mesh/seam over their children");
751785 }
786 if (col_leaked > 0) {
787 UtilityFunctions::print("[STALECOL] ", col_leaked,
788 " internal nodes still COLLIDING with a coarse surface over their children");
789 }
752790 }
753791
754792 // NOTE: Stale parent shadow disable was removed — disabling shadows on
……@@ -2076,6 +2114,9 @@ void DCTerrain::_upload_cell(CellResult& result) {
20762114 Transform3D chunk_xform;
20772115 chunk_xform.origin = Vector3(chunk_origin_x, chunk_origin_y, chunk_origin_z);
20782116 rs->instance_set_transform(node->mesh_instance, chunk_xform);
2117 node->mesh_origin[0] = chunk_xform.origin.x;
2118 node->mesh_origin[1] = chunk_xform.origin.y;
2119 node->mesh_origin[2] = chunk_xform.origin.z;
20792120
20802121 // AABB in local space (relative to chunk origin)
20812122 float cs = 32.0f * lod_voxel; // full grid extent (includes overlap for conservative culling)
……@@ -2841,6 +2882,163 @@ int DCTerrain::get_splits_this_frame() const {
28412882// ---------------------------------------------------------------------------
28422883// Density sampling
28432884// ---------------------------------------------------------------------------
2885PackedVector3Array DCTerrain::debug_collision_faces(const Vector3 &center, float radius) const {
2886 PackedVector3Array out;
2887 PhysicsServer3D *ps = PhysicsServer3D::get_singleton();
2888 if (!ps) return out;
2889 const float r2 = radius * radius;
2890 std::vector<OctreeClipmapNode *> leaves =
2891 const_cast<OctreeClipmap &>(octree_clipmap_).get_leaves();
2892 for (OctreeClipmapNode *n : leaves) {
2893 if (!n || !n->collision_shape.is_valid() || !n->collision_body.is_valid()) continue;
2894 Transform3D xf = ps->body_get_state(n->collision_body,
2895 PhysicsServer3D::BODY_STATE_TRANSFORM);
2896
2897 // CULL BY THE CHUNK'S BOX, NOT BY ITS CORNER. The body origin is the
2898 // chunk's minimum corner and a chunk spans 32 voxels -- 32 m at LOD 0,
2899 // 1024 m at LOD 5. Testing the corner against the probe radius rejects
2900 // the very chunk the player is standing in whenever the probe is
2901 // smaller than the chunk, and an empty return reads as "there is no
2902 // collision here" rather than "the cull threw it away".
2903 const float cs = 32.0f * lod_voxel_size(voxel_size_, n->lod_level);
2904 const Vector3 lo = xf.origin;
2905 const Vector3 hi = xf.origin + Vector3(cs, cs, cs);
2906 const Vector3 q(
2907 std::min(std::max(center.x, lo.x), hi.x),
2908 std::min(std::max(center.y, lo.y), hi.y),
2909 std::min(std::max(center.z, lo.z), hi.z));
2910 if (q.distance_squared_to(center) > r2) continue;
2911
2912 Variant data = ps->shape_get_data(n->collision_shape);
2913 if (data.get_type() != Variant::DICTIONARY) continue;
2914 PackedVector3Array faces = ((Dictionary)data).get("faces", PackedVector3Array());
2915
2916 // WHOLE TRIANGLES, ONE DECISION PER TRIANGLE. The previous form walked
2917 // vertices: an in-range vertex was pushed on its own AND its triangle
2918 // was pushed again by the i%3 branch, so the array was no longer a
2919 // triangle list at all. Every inclination or step computed from it read
2920 // a triangle made of three vertices that were never one.
2921 for (int t = 0; t + 2 < faces.size(); t += 3) {
2922 const Vector3 a = xf.xform(faces[t]);
2923 const Vector3 b = xf.xform(faces[t + 1]);
2924 const Vector3 c = xf.xform(faces[t + 2]);
2925 if (a.distance_squared_to(center) <= r2 ||
2926 b.distance_squared_to(center) <= r2 ||
2927 c.distance_squared_to(center) <= r2) {
2928 out.push_back(a); out.push_back(b); out.push_back(c);
2929 }
2930 }
2931 }
2932 return out;
2933}
2934
2935// THE CHUNK UNDER A POINT, as the streamer sees it. Answers the question every
2936// terrain-collision report needs first and no existing API could: what LOD is
2937// the ground here, how wide is one of its voxels, and does it have a collision
2938// body at all. A miss is reported as an EMPTY dictionary rather than as zeros,
2939// because "there is no leaf here" and "LOD 0" are different findings.
2940// THE VISUAL MESH, as triangles, in world space -- the counterpart to
2941// debug_collision_faces, so "is the collision where the terrain looks like it
2942// is" becomes a measurement instead of an argument from source.
2943//
2944// The two are supposed to be the same triangles: _upload_cell builds the
2945// collision faces from result.mesh_data, the same array it builds the ArrayMesh
2946// from, and positions both with the same origin expression. Supposed to is not
2947// measured, and this session has twice caught a difference between what the
2948// source says and what runs.
2949PackedVector3Array DCTerrain::debug_visual_faces(const Vector3 &center, float radius) const {
2950 PackedVector3Array out;
2951 const float r2 = radius * radius;
2952 std::vector<OctreeClipmapNode *> leaves =
2953 const_cast<OctreeClipmap &>(octree_clipmap_).get_leaves();
2954 for (OctreeClipmapNode *n : leaves) {
2955 if (!n || !n->mesh_instance.is_valid()) continue;
2956 auto it = node_meshes_.find(n);
2957 if (it == node_meshes_.end() || it->second.is_null()) continue;
2958
2959 const float cs = 32.0f * lod_voxel_size(voxel_size_, n->lod_level);
2960 const Vector3 lo(n->mesh_origin[0], n->mesh_origin[1], n->mesh_origin[2]);
2961 const Vector3 hi = lo + Vector3(cs, cs, cs);
2962 const Vector3 q(
2963 std::min(std::max(center.x, lo.x), hi.x),
2964 std::min(std::max(center.y, lo.y), hi.y),
2965 std::min(std::max(center.z, lo.z), hi.z));
2966 if (q.distance_squared_to(center) > r2) continue;
2967
2968 Ref<ArrayMesh> mesh = it->second;
2969 for (int si = 0; si < mesh->get_surface_count(); si++) {
2970 Array arrays = mesh->surface_get_arrays(si);
2971 if (arrays.size() <= Mesh::ARRAY_INDEX) continue;
2972 PackedVector3Array verts = arrays[Mesh::ARRAY_VERTEX];
2973 PackedInt32Array idx = arrays[Mesh::ARRAY_INDEX];
2974 if (verts.is_empty() || idx.is_empty()) continue;
2975 for (int t = 0; t + 2 < idx.size(); t += 3) {
2976 const Vector3 a = verts[idx[t]] + lo;
2977 const Vector3 b = verts[idx[t + 1]] + lo;
2978 const Vector3 c = verts[idx[t + 2]] + lo;
2979 if (a.distance_squared_to(center) <= r2 ||
2980 b.distance_squared_to(center) <= r2 ||
2981 c.distance_squared_to(center) <= r2) {
2982 out.push_back(a); out.push_back(b); out.push_back(c);
2983 }
2984 }
2985 }
2986 }
2987 return out;
2988}
2989
2990String DCTerrain::build_stamp() const {
2991 return String("dc-terrain built ") + __DATE__ + " " + __TIME__;
2992}
2993
2994Dictionary DCTerrain::debug_chunk_info(const Vector3 &position) const {
2995 Dictionary d;
2996 if (voxel_size_ <= 0.0f) return d;
2997 const int vx = (int)Math::floor(position.x / voxel_size_);
2998 const int vy = (int)Math::floor(position.y / voxel_size_);
2999 const int vz = (int)Math::floor(position.z / voxel_size_);
3000 OctreeClipmapNode *n = octree_clipmap_.find_leaf_containing(vx, vy, vz);
3001 if (!n) return d;
3002 const float lv = lod_voxel_size(voxel_size_, n->lod_level);
3003 d["lod"] = n->lod_level;
3004 d["voxel_size"] = lv;
3005 d["chunk_size"] = 32.0f * lv;
3006 d["state"] = (int)n->state;
3007 // MESH_NEED_UPDATE (4) is the answer to "did my carve reach this chunk":
3008 // an edit marks the chunk dirty, and a chunk still UP_TO_DATE after a carve
3009 // never heard about it.
3010 d["mesh_state"] = (int)n->mesh_state;
3011 d["triangles"] = (int)n->triangle_count;
3012 d["has_collision"] = n->collision_body.is_valid();
3013 d["collision_eligible"] = collision_trimesh_.should_enqueue(n->lod_level, n->triangle_count > 0);
3014 d["origin"] = Vector3(n->position.x, n->position.y, n->position.z) * voxel_size_;
3015 d["parent_collision_stale"] = n->parent_collision_stale;
3016 return d;
3017}
3018
3019PackedFloat32Array DCTerrain::debug_gpu_density(const Vector3 &origin, float voxel_size, int cells) {
3020 PackedFloat32Array out;
3021 if (!gpu_thread_ || !generator_ptr_) return out;
3022 if (cells < 1) cells = 1;
3023
3024 DensityGrid grid;
3025 float org[3] = { (float)origin.x, (float)origin.y, (float)origin.z };
3026 grid.allocate(cells, voxel_size, org);
3027
3028 const char* glsl = generator_ptr_->has_gpu_shader()
3029 ? generator_ptr_->gpu_shader_source() : nullptr;
3030 const float* hb = generator_ptr_->gpu_custom_data();
3031 int hbn = generator_ptr_->gpu_custom_data_count();
3032
3033 if (!gpu_thread_->submit(grid, density_, glsl, nullptr, nullptr,
3034 nullptr, 0.0f, 0u, hb, hbn)) {
3035 return out; // empty: the GPU declined, and that is the finding
3036 }
3037 out.resize((int64_t)grid.data.size());
3038 memcpy(out.ptrw(), grid.data.data(), grid.data.size() * sizeof(float));
3039 return out;
3040}
3041
28443042float DCTerrain::sample_density(const Vector3 &position) const {
28453043 float d = generator_ptr_->evaluate(position.x, position.y, position.z, 0.0f);
28463044 if (config_.edits && !config_.edits->empty()) {
……@@ -4051,6 +4249,11 @@ void DCTerrain::_bind_methods() {
40514249 ClassDB::bind_method(D_METHOD("rebuild"), &DCTerrain::rebuild);
40524250 ClassDB::bind_method(D_METHOD("warmup_shaders"), &DCTerrain::warmup_shaders);
40534251 ClassDB::bind_method(D_METHOD("sample_density", "position"), &DCTerrain::sample_density);
4252 ClassDB::bind_method(D_METHOD("debug_gpu_density", "origin", "voxel_size", "cells"), &DCTerrain::debug_gpu_density);
4253 ClassDB::bind_method(D_METHOD("debug_collision_faces", "center", "radius"), &DCTerrain::debug_collision_faces);
4254 ClassDB::bind_method(D_METHOD("debug_chunk_info", "position"), &DCTerrain::debug_chunk_info);
4255 ClassDB::bind_method(D_METHOD("debug_visual_faces", "center", "radius"), &DCTerrain::debug_visual_faces);
4256 ClassDB::bind_method(D_METHOD("build_stamp"), &DCTerrain::build_stamp);
40544257
40554258 // SDF collision API
40564259 ClassDB::bind_method(D_METHOD("get_distance", "position"), &DCTerrain::get_distance);
src/dc_terrain.h +45 -0
……@@ -16,6 +16,7 @@
1616#include <godot_cpp/classes/physics_server3d.hpp>
1717#include <godot_cpp/variant/color.hpp>
1818#include <godot_cpp/variant/rid.hpp>
19#include <godot_cpp/variant/dictionary.hpp>
1920
2021#include "dc_density.h"
2122#include "dc_density_grid.h"
……@@ -180,6 +181,50 @@ public:
180181 // -- Density sampling (for spawn placement, etc.) --
181182 float sample_density(const Vector3 &position) const;
182183
184 // THE GPU ORACLE. Runs the real density pipeline -- the same shader, the
185 // same uniform set, the same buffers the mesher uses -- over a small grid
186 // and hands the values back. Returns EMPTY if the GPU refused, which is
187 // itself the answer.
188 //
189 // It exists because every gate written before it either ran outside Godot
190 // or inspected GLSL text, and none could tell a correct shader from one
191 // that never ran. sample_density() is the CPU twin; comparing the two is
192 // the parity check test_erosion_parity.cpp does for the wrapper, applied
193 // to the engine.
194 //
195 // A local RenderingDevice is null under --headless, so this only answers
196 // in a real render session.
197 PackedFloat32Array debug_gpu_density(const Vector3 &origin, float voxel_size, int cells);
198
199 // THE COLLISION MESH, as triangles, so it can be drawn.
200 //
201 // Collision is built through PhysicsServer3D as bare RIDs hung off the
202 // clipmap nodes, not as scene-tree CollisionShape3Ds -- so nothing in
203 // GDScript could see it, and "is the collision where the terrain is" had no
204 // answer but walking into things. This reads the concave shapes back and
205 // returns their faces in WORLD space, ready for a debug overlay.
206 //
207 // Gaps in what comes back are the finding: collision is only built for
208 // lod_level <= collision_lod_max (5 by default), so beyond that there is
209 // nothing to stand on however solid the terrain looks.
210 PackedVector3Array debug_collision_faces(const Vector3 &center, float radius) const;
211
212 // The DRAWN triangles in world space, for comparison against the above.
213 PackedVector3Array debug_visual_faces(const Vector3 &center, float radius) const;
214
215 // WHICH BINARY IS THIS. Baked at compile time, so a report can name the
216 // artifact it examined instead of assuming the .so matches the source --
217 // the rule hyper-games #44 wrote and #45 broke within hours. The .so under
218 // addons/ is a symlink into dc-terrain/bin and is not rebuilt by anything
219 // automatic, so "I changed the C++" and "the game runs the change" are
220 // independent facts.
221 String build_stamp() const;
222
223 // The streaming state of the leaf under a point: lod, voxel_size,
224 // chunk_size, state, triangles, has_collision, collision_eligible, origin.
225 // EMPTY dictionary when no leaf contains the point -- distinct from LOD 0.
226 Dictionary debug_chunk_info(const Vector3 &position) const;
227
183228 // -- SDF collision API (direct density field queries) --
184229 float _eval_with_edits(float x, float y, float z) const;
185230 // Returns signed distance to terrain surface (positive = inside solid, negative = in air).
src/dc_terrain_generator_procnodes.cpp +661 -246
……@@ -15,6 +15,8 @@
1515// dependency on proc-nodes' classes -- the ClassDB reflection in the header
1616// comment still stands. It is a shared math header and nothing more.
1717#include "erosion_cpp_shim.h"
18#include "erosion_wrapper_glsl.h"
19#include <godot_cpp/classes/file_access.hpp>
1820
1921namespace godot {
2022
……@@ -41,70 +43,26 @@ void DCTerrainGeneratorProcNodes::_bind_methods() {
4143 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ceiling_y", PROPERTY_HINT_RANGE, "-1000.0,1000.0,0.5"),
4244 "set_ceiling_y", "get_ceiling_y");
4345
44 ClassDB::bind_method(D_METHOD("set_mountain_scale", "value"), &DCTerrainGeneratorProcNodes::set_mountain_scale);
45 ClassDB::bind_method(D_METHOD("get_mountain_scale"), &DCTerrainGeneratorProcNodes::get_mountain_scale);
46 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mountain_scale", PROPERTY_HINT_RANGE, "0.1,10.0,0.1"),
47 "set_mountain_scale", "get_mountain_scale");
48
49 ClassDB::bind_method(D_METHOD("set_mountain_height", "value"), &DCTerrainGeneratorProcNodes::set_mountain_height);
50 ClassDB::bind_method(D_METHOD("get_mountain_height"), &DCTerrainGeneratorProcNodes::get_mountain_height);
51 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mountain_height", PROPERTY_HINT_RANGE, "0.0,2.0,0.01"),
52 "set_mountain_height", "get_mountain_height");
53
54 ClassDB::bind_method(D_METHOD("set_mountain_octaves", "value"), &DCTerrainGeneratorProcNodes::set_mountain_octaves);
55 ClassDB::bind_method(D_METHOD("get_mountain_octaves"), &DCTerrainGeneratorProcNodes::get_mountain_octaves);
56 ADD_PROPERTY(PropertyInfo(Variant::INT, "mountain_octaves", PROPERTY_HINT_RANGE, "1,6,1"),
57 "set_mountain_octaves", "get_mountain_octaves");
58
59 ClassDB::bind_method(D_METHOD("set_relief_scale", "value"), &DCTerrainGeneratorProcNodes::set_relief_scale);
60 ClassDB::bind_method(D_METHOD("get_relief_scale"), &DCTerrainGeneratorProcNodes::get_relief_scale);
61 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "relief_scale", PROPERTY_HINT_RANGE, "0.0,8.0,0.01"),
62 "set_relief_scale", "get_relief_scale");
63
64 ClassDB::bind_method(D_METHOD("set_relief_floor", "value"), &DCTerrainGeneratorProcNodes::set_relief_floor);
65 ClassDB::bind_method(D_METHOD("get_relief_floor"), &DCTerrainGeneratorProcNodes::get_relief_floor);
66 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "relief_floor", PROPERTY_HINT_RANGE, "0.0,1.0,0.01"),
67 "set_relief_floor", "get_relief_floor");
68
69 ClassDB::bind_method(D_METHOD("set_mountain_gain", "value"), &DCTerrainGeneratorProcNodes::set_mountain_gain);
70 ClassDB::bind_method(D_METHOD("get_mountain_gain"), &DCTerrainGeneratorProcNodes::get_mountain_gain);
71 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mountain_gain", PROPERTY_HINT_RANGE, "0.2,0.9,0.01"),
72 "set_mountain_gain", "get_mountain_gain");
73
74 ClassDB::bind_method(D_METHOD("set_detail_scale", "value"), &DCTerrainGeneratorProcNodes::set_detail_scale);
75 ClassDB::bind_method(D_METHOD("get_detail_scale"), &DCTerrainGeneratorProcNodes::get_detail_scale);
76 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_scale", PROPERTY_HINT_RANGE, "0.0,2000.0,1.0"),
77 "set_detail_scale", "get_detail_scale");
78
79 ClassDB::bind_method(D_METHOD("set_detail_height", "value"), &DCTerrainGeneratorProcNodes::set_detail_height);
80 ClassDB::bind_method(D_METHOD("get_detail_height"), &DCTerrainGeneratorProcNodes::get_detail_height);
81 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_height", PROPERTY_HINT_RANGE, "0.0,500.0,0.5"),
82 "set_detail_height", "get_detail_height");
83
84 ClassDB::bind_method(D_METHOD("set_detail_octaves", "value"), &DCTerrainGeneratorProcNodes::set_detail_octaves);
85 ClassDB::bind_method(D_METHOD("get_detail_octaves"), &DCTerrainGeneratorProcNodes::get_detail_octaves);
86 ADD_PROPERTY(PropertyInfo(Variant::INT, "detail_octaves", PROPERTY_HINT_RANGE, "1,10,1"),
87 "set_detail_octaves", "get_detail_octaves");
88
89 ClassDB::bind_method(D_METHOD("set_detail_gain", "value"), &DCTerrainGeneratorProcNodes::set_detail_gain);
90 ClassDB::bind_method(D_METHOD("get_detail_gain"), &DCTerrainGeneratorProcNodes::get_detail_gain);
91 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "detail_gain", PROPERTY_HINT_RANGE, "0.2,0.9,0.01"),
92 "set_detail_gain", "get_detail_gain");
93
94 ClassDB::bind_method(D_METHOD("set_mountain_variance", "value"), &DCTerrainGeneratorProcNodes::set_mountain_variance);
95 ClassDB::bind_method(D_METHOD("get_mountain_variance"), &DCTerrainGeneratorProcNodes::get_mountain_variance);
96 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "mountain_variance", PROPERTY_HINT_RANGE, "0.0,1.0,0.01"),
97 "set_mountain_variance", "get_mountain_variance");
98
99 ClassDB::bind_method(D_METHOD("set_erosion_scale", "value"), &DCTerrainGeneratorProcNodes::set_erosion_scale);
100 ClassDB::bind_method(D_METHOD("get_erosion_scale"), &DCTerrainGeneratorProcNodes::get_erosion_scale);
101 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "erosion_scale", PROPERTY_HINT_RANGE, "0.002,0.5,0.001"),
102 "set_erosion_scale", "get_erosion_scale");
103
104 ClassDB::bind_method(D_METHOD("set_noise_lacunarity", "value"), &DCTerrainGeneratorProcNodes::set_noise_lacunarity);
105 ClassDB::bind_method(D_METHOD("get_noise_lacunarity"), &DCTerrainGeneratorProcNodes::get_noise_lacunarity);
106 ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "noise_lacunarity", PROPERTY_HINT_RANGE, "1.5,3.0,0.01"),
107 "set_noise_lacunarity", "get_noise_lacunarity");
46
47
48
49
50
51 ClassDB::bind_method(D_METHOD("set_heightfield_path", "path"), &DCTerrainGeneratorProcNodes::set_heightfield_path);
52 ClassDB::bind_method(D_METHOD("get_heightfield_path"), &DCTerrainGeneratorProcNodes::get_heightfield_path);
53 ADD_PROPERTY(PropertyInfo(Variant::STRING, "heightfield_path", PROPERTY_HINT_FILE, "*.hblk"),
54 "set_heightfield_path", "get_heightfield_path");
55 ClassDB::bind_method(D_METHOD("set_heightfield_only", "value"), &DCTerrainGeneratorProcNodes::set_heightfield_only);
56 ClassDB::bind_method(D_METHOD("get_heightfield_only"), &DCTerrainGeneratorProcNodes::get_heightfield_only);
57 ADD_PROPERTY(PropertyInfo(Variant::BOOL, "heightfield_only"),
58 "set_heightfield_only", "get_heightfield_only");
59
60
61
62
63
64
65
10866
10967 ClassDB::bind_method(D_METHOD("set_height_space_sqrt", "value"), &DCTerrainGeneratorProcNodes::set_height_space_sqrt);
11068 ClassDB::bind_method(D_METHOD("get_height_space_sqrt"), &DCTerrainGeneratorProcNodes::get_height_space_sqrt);
……@@ -133,91 +91,71 @@ void DCTerrainGeneratorProcNodes::set_ceiling_y(float p_val) {
13391 emit_changed();
13492}
13593
136void DCTerrainGeneratorProcNodes::set_mountain_variance(float p_val) {
137 if (mountain_variance_ == p_val) return;
138 mountain_variance_ = p_val;
139 _recompile_glsl();
140 emit_changed();
141}
142void DCTerrainGeneratorProcNodes::set_erosion_scale(float p_val) {
143 if (erosion_scale_ == p_val) return;
144 erosion_scale_ = p_val;
145 _recompile_glsl();
146 emit_changed();
147}
148void DCTerrainGeneratorProcNodes::set_noise_lacunarity(float p_val) {
149 if (noise_lacunarity_ == p_val) return;
150 noise_lacunarity_ = p_val;
151 _recompile_glsl();
152 emit_changed();
153}
15494void DCTerrainGeneratorProcNodes::set_height_space_sqrt(bool p_val) {
15595 if (height_space_sqrt_ == p_val) return;
15696 height_space_sqrt_ = p_val;
15797 _recompile_glsl();
15898 emit_changed();
15999}
160void DCTerrainGeneratorProcNodes::set_mountain_gain(float p_val) {
161 if (mountain_gain_ == p_val) return;
162 mountain_gain_ = p_val;
163 _recompile_glsl();
164 emit_changed();
165}
166void DCTerrainGeneratorProcNodes::set_detail_scale(float p_val) {
167 if (detail_scale_ == p_val) return;
168 detail_scale_ = p_val;
169 _recompile_glsl();
170 emit_changed();
171}
172void DCTerrainGeneratorProcNodes::set_detail_height(float p_val) {
173 if (detail_height_ == p_val) return;
174 detail_height_ = p_val;
175 _recompile_glsl();
176 emit_changed();
177}
178void DCTerrainGeneratorProcNodes::set_detail_octaves(int p_val) {
179 if (detail_octaves_ == p_val) return;
180 detail_octaves_ = p_val;
181 _recompile_glsl();
182 emit_changed();
183}
184void DCTerrainGeneratorProcNodes::set_detail_gain(float p_val) {
185 if (detail_gain_ == p_val) return;
186 detail_gain_ = p_val;
187 _recompile_glsl();
188 emit_changed();
189}
190void DCTerrainGeneratorProcNodes::set_relief_scale(float p_val) {
191 if (relief_scale_ == p_val) return;
192 relief_scale_ = p_val;
193 _recompile_glsl();
194 emit_changed();
195}
196void DCTerrainGeneratorProcNodes::set_relief_floor(float p_val) {
197 if (relief_floor_ == p_val) return;
198 relief_floor_ = p_val;
199 _recompile_glsl();
200 emit_changed();
201}
202void DCTerrainGeneratorProcNodes::set_mountain_scale(float p_val) {
203 if (mountain_scale_ == p_val) return;
204 mountain_scale_ = p_val;
205 _recompile_glsl();
206 emit_changed();
100// Load a .hblk written by terrain-lookdev/stitch_block.py. The format is
101// deliberately trivial -- a 32-byte header and int16 metres -- because the
102// alternative was a Godot Image import path, and the field has to be readable
103// identically by the standalone harness that gates it, which has no engine.
104bool DCTerrainGeneratorProcNodes::_load_heightfield(const String &p_path) {
105 hblk_.clear(); hblk_n_ = 0;
106 if (p_path.is_empty()) return true;
107 Ref<FileAccess> f = FileAccess::open(p_path, FileAccess::READ);
108 if (f.is_null()) {
109 UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: cannot open heightfield ", p_path);
110 return false;
111 }
112 PackedByteArray hdr = f->get_buffer(32);
113 if (hdr.size() != 32 || hdr[0] != 'H' || hdr[1] != 'B' || hdr[2] != 'L' || hdr[3] != 'K') {
114 UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: not an .hblk: ", p_path);
115 return false;
116 }
117 const uint8_t *p = hdr.ptr();
118 uint32_t ver, n; float mpp, ox, oz;
119 memcpy(&ver, p + 4, 4); memcpy(&n, p + 8, 4); memcpy(&mpp, p + 12, 4);
120 memcpy(&ox, p + 16, 4); memcpy(&oz, p + 20, 4);
121 if (ver != 1u || n == 0u) {
122 UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: unsupported .hblk version/size");
123 return false;
124 }
125 PackedByteArray body = f->get_buffer((int64_t)n * (int64_t)n * 2);
126 if (body.size() != (int64_t)n * (int64_t)n * 2) {
127 UtilityFunctions::push_warning("DCTerrainGeneratorProcNodes: short .hblk data");
128 return false;
129 }
130 hblk_.resize((size_t)n * (size_t)n);
131 const uint8_t *b = body.ptr();
132 for (size_t i = 0; i < hblk_.size(); i++) {
133 int16_t v; memcpy(&v, b + i * 2, 2);
134 hblk_[i] = (float)v;
135 }
136 hblk_n_ = (int)n; hblk_mpp_ = mpp; hblk_ox_ = ox; hblk_oz_ = oz;
137 UtilityFunctions::print("[DCTerrainGeneratorProcNodes] heightfield ", p_path,
138 ": ", (int)n, "x", (int)n, " @ ", mpp, " m/px, origin x ", ox, " z ", oz);
139 return true;
207140}
208void DCTerrainGeneratorProcNodes::set_mountain_height(float p_val) {
209 if (mountain_height_ == p_val) return;
210 mountain_height_ = p_val;
141
142
143void DCTerrainGeneratorProcNodes::set_heightfield_only(bool p_val) {
144 if (heightfield_only_ == p_val) return;
145 heightfield_only_ = p_val;
211146 _recompile_glsl();
212147 emit_changed();
213148}
214void DCTerrainGeneratorProcNodes::set_mountain_octaves(int p_val) {
215 if (mountain_octaves_ == p_val) return;
216 mountain_octaves_ = p_val;
149
150void DCTerrainGeneratorProcNodes::set_heightfield_path(const String &p_path) {
151 if (heightfield_path_ == p_path) return;
152 heightfield_path_ = p_path;
153 _load_heightfield(p_path);
217154 _recompile_glsl();
218155 emit_changed();
219156}
220157
158
221159void DCTerrainGeneratorProcNodes::set_graph(const Ref<Resource> &p_graph) {
222160 if (p_graph.is_valid()) {
223161 // Validate that the resource is a ProcNodeGraph (from proc-nodes GDExtension)
……@@ -231,6 +169,7 @@ void DCTerrainGeneratorProcNodes::set_graph(const Ref<Resource> &p_graph) {
231169 }
232170 graph_ = p_graph;
233171 _discover_params();
172 _collect_node_data();
234173 _recompile_glsl(); // Eager — must happen on main thread before worker sees this generator
235174 notify_property_list_changed();
236175 emit_changed();
……@@ -238,6 +177,59 @@ void DCTerrainGeneratorProcNodes::set_graph(const Ref<Resource> &p_graph) {
238177
239178// ── Parameter Discovery ───────────────────────────────────────────────────────
240179
180// Collect opaque GPU data from the graph's nodes.
181//
182// dc-terrain transports floats and knows nothing about them; a NODE decides what
183// they mean. So the generator asks each node "do you have data?" and hands the
184// first answer up as custom data. This is what lets a heightfield live in the
185// graph rather than as another property on this class.
186//
187// One producer for now, which matches one bound buffer. More than one wants an
188// offset table emitted alongside, which is the same change a grid of tiles needs.
189void DCTerrainGeneratorProcNodes::_collect_node_data() {
190 if (graph_.is_null()) return;
191 Variant nc_var = graph_->call("get_node_count");
192 if (nc_var.get_type() != Variant::INT) return;
193 int node_count = nc_var;
194 for (int ni = 0; ni < node_count; ni++) {
195 Variant node_var = graph_->call("get_node", ni);
196 if (node_var.get_type() != Variant::OBJECT) continue;
197 Object *node_obj = node_var.operator Object *();
198 if (!node_obj || !node_obj->has_method("get_gpu_data")) continue;
199 PackedFloat32Array d = node_obj->call("get_gpu_data");
200 if (d.is_empty()) continue;
201 hblk_.assign(d.ptr(), d.ptr() + d.size());
202 // ASK THE NODE FOR THE SIDE LENGTH; DO NOT DERIVE IT FROM THE ARRAY.
203 //
204 // get_gpu_data() returns the MIP PYRAMID, not one square: 1,403,226
205 // floats for a 1026 tile. sqrt of that is 1184.6, so the old derivation
206 // produced 1185 and every consumer of this copy -- the lookdev
207 // wrapper's sampler and the CPU twin -- would have read the block with
208 // a stride 159 texels too wide, which shears the terrain rather than
209 // failing. It has been latent because nothing used hblk_n_ on the
210 // node-supplied path yet.
211 if (node_obj->has_method("get_tile_size")) {
212 hblk_n_ = (int)(int64_t)node_obj->call("get_tile_size");
213 } else {
214 hblk_n_ = (int)Math::round(Math::sqrt((double)d.size()));
215 }
216 // AND ITS GEOMETRY. Copying the floats without the origin and pitch left
217 // the CPU sampling at world/30 from (0,0) -- off the end of the tile, so
218 // every query clamped to a corner and reported the block's minimum. The
219 // data means nothing without where it sits.
220 if (node_obj->has_method("get_tile_mpp"))
221 hblk_mpp_ = (float)(double)node_obj->call("get_tile_mpp");
222 if (node_obj->has_method("get_tile_origin")) {
223 Vector2 o = node_obj->call("get_tile_origin");
224 hblk_ox_ = o.x;
225 hblk_oz_ = o.y;
226 }
227 UtilityFunctions::print("[DCTerrainGeneratorProcNodes] node ", ni,
228 " supplied ", (int64_t)d.size(), " floats of custom data");
229 return;
230 }
231}
232
241233void DCTerrainGeneratorProcNodes::_discover_params() {
242234 discovered_params_.clear();
243235 if (graph_.is_null()) return;
……@@ -383,27 +375,172 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
383375 // pn_max_octaves must be declared BEFORE proc_density (which reads it).
384376 String th_str = String::num(terrain_height_, 4);
385377 String floor_str = String::num(floor_y_, 4);
378 String ceil_str_early = String::num(ceiling_y_, 4);
379
380 // ── HEIGHTFIELD ONLY ───────────────────────────────────────────────────
381 // Everything below this point -- the graph, the noise library, the mountain
382 // and relief and detail layers, the filter-space mapping -- is skipped. The
383 // density IS the field. If this does not show the block, nothing downstream
384 // of it can, and the fault is in getting the bytes to the GPU rather than in
385 // anything about terrain.
386 if (heightfield_only_ && hblk_n_ > 0) {
387 String hb =
388 String("const int HBLK_N = ") + itos(hblk_n_) + ";\n"
389 "const float HBLK_OX = " + String::num(hblk_ox_, 4) + ";\n"
390 "const float HBLK_OZ = " + String::num(hblk_oz_, 4) + ";\n"
391 "const float HBLK_MPP = " + String::num(hblk_mpp_, 6) + ";\n"
392 "float hblk_height_m(float x, float z) {\n"
393 " float u = (x - HBLK_OX) / HBLK_MPP;\n"
394 " float v = (z - HBLK_OZ) / HBLK_MPP;\n"
395 " u = clamp(u, 0.0, float(HBLK_N - 1));\n"
396 " v = clamp(v, 0.0, float(HBLK_N - 1));\n"
397 " int i0 = int(floor(u)); int j0 = int(floor(v));\n"
398 " int i1 = min(i0 + 1, HBLK_N - 1); int j1 = min(j0 + 1, HBLK_N - 1);\n"
399 " float fu = u - float(i0); float fv = v - float(j0);\n"
400 " float h00 = hblk_data[j0*HBLK_N + i0]; float h10 = hblk_data[j0*HBLK_N + i1];\n"
401 " float h01 = hblk_data[j1*HBLK_N + i0]; float h11 = hblk_data[j1*HBLK_N + i1];\n"
402 " return mix(mix(h00, h10, fu), mix(h01, h11, fu), fv);\n"
403 "}\n"
404 "float evaluate_density(float x, float y, float z) {\n"
405 " float d = hblk_height_m(x, z) - y;\n"
406 " d = max(d, (" + floor_str + " - y) * 2.0);\n"
407 " d = min(d, (" + ceil_str_early + " - y) * 2.0);\n"
408 " return d;\n"
409 "}\n";
410 CharString u8 = hb.utf8();
411 cached_glsl_ = std::string(u8.get_data(), u8.length());
412 UtilityFunctions::print("[DCTerrainGeneratorProcNodes] HEIGHTFIELD ONLY: ",
413 String::num_int64(cached_glsl_.size()), " bytes of GLSL, no graph, no erosion");
414 return;
415 }
416
386417 String ceil_str = String::num(ceiling_y_, 4);
387418 // y_scale maps [floor_y, ceiling_y] to proc_density's native [0, HeightScale]
388419 float y_range = ceiling_y_ - floor_y_;
389420 if (y_range < 0.01f) y_range = 0.01f;
390 String yscale_str = String::num(height_scale / y_range, 6);
391 String inv_yscale_str = String::num(y_range / height_scale, 6);
392 // detail_height_ is in METRES; mtn_offset is in units where 1.0 == y_range
393 // metres (y_mapped divides by y_scale = height_scale / y_range). Convert once
394 // here so the shader carries a plain fraction.
395 float detail_frac = (y_range > 0.01f) ? (detail_height_ / y_range) : 0.0f;
396 String mgain_str = String::num(mountain_gain_, 4);
397 String lac_str = String::num(noise_lacunarity_, 4);
398 String dscale_str = String::num(detail_scale_, 4);
399 String dfrac_str = String::num(detail_frac, 8);
400 String doct_str = itos(detail_octaves_);
401 String dgain_str = String::num(detail_gain_, 4);
402 String rscale_str = String::num(relief_scale_, 4);
403 String rfloor_str = String::num(relief_floor_, 4);
404 String mscale_str = String::num(mountain_scale_, 4);
405 String mheight_str = String::num(mountain_height_, 4);
406 String moct_str = itos(mountain_octaves_);
421 // NINE DECIMALS, NOT SIX -- the same bug proc-nodes' gpu_compiler.cpp fixed
422 // in its own literal emission, still live here.
423 //
424 // String::num's second argument is DECIMAL PLACES, so for a value of about
425 // 2.4e-4 six of them leave THREE significant figures: 5/21000 =
426 // 0.000238095238 was emitted as 0.000238. The HeightfieldNode's matching
427 // constant is emitted by gpu_compiler at nine, so the two halves of the same
428 // mapping disagreed by 4.0e-4 relative -- and since the density is their
429 // difference, the surface came out where they balance rather than where the
430 // data says:
431 //
432 // height -5300 m -> density zero at -5306.466 m (-6.466 m)
433 // height 0 m -> -4.351 m (-4.351 m)
434 // height +165 m -> +160.715 m (-4.285 m)
435 // height +5300 m -> +5297.765 m (-2.235 m)
436 //
437 // (tools/oracles/density_precision.py). A smooth error, so it does not make
438 // the ground rough -- it just puts the world several metres from its own
439 // heightfield, which is why this map's spawn height had to be measured by
440 // hand instead of read from the block.
441 //
442 // Nine also makes the two literals IDENTICAL when the map derives the node's
443 // YScale from the same height_scale/y_range, so the difference is exact
444 // rather than merely closer.
445 // Cell size from the graph's own Erosion Noise node -- the same place the
446 // CPU path and the lookdev read it, so the three cannot drift.
447 float emit_cell_scale = 1.0f;
448 for (int i = 0; i < discovered_params_.size(); i++) {
449 const GraphParam &gp = discovered_params_[i];
450 if (gp.node_title == "Erosion Noise" && gp.socket_name == "CellScale")
451 emit_cell_scale = (float)(double)gp.default_value;
452 }
453
454 String yscale_str = String::num(height_scale / y_range, 9);
455 String inv_yscale_str = String::num(y_range / height_scale, 9);
456 // The mountain, relief and detail literals that used to be prepared here are
457 // gone with the blocks that emitted them. Every one of those layers is a
458 // node, and a node bakes its own sockets.
459
460 // ── Baked heightfield, if one is loaded ────────────────────────────────
461 // The SAMPLER is ours (storage is per-consumer); the metres -> filter-space
462 // CONVERSION comes from erosion_wrapper_glsl.h, the same text the standalone
463 // harness and its identity gate compile. That is the whole point of the
464 // split: a unit error here would be invisible in-engine and is exactly the
465 // class of bug that cost #45 a session.
466 String hblk_glsl_sampler;
467 String hblk_glsl;
468 // ONLY for the generator's OWN heightfield_path. When the data came from a
469 // NODE, that node emits its own sampler in its prelude -- emitting a second
470 // one here is a redefinition, and this older copy still names the buffer
471 // hblk_data, which no longer exists now that dc-terrain transports opaque
472 // custom_data. Left in place for the pre-node path rather than deleted.
473 if (hblk_n_ > 0 && !heightfield_path_.is_empty()) {
474 // The sampler alone — HBLK_* consts plus hblk_height_m. Split out because
475 // the lookdev path needs ONLY this: erosion_wrapper_glsl emits its own
476 // M_* consts and its own heightmap_gn, and emitting ours as well would be
477 // a redefinition, which is precisely how this file once shipped a shader
478 // that never compiled and a world with no terrain.
479 hblk_glsl_sampler =
480 String("const int HBLK_N = ") + itos(hblk_n_) + ";\n"
481 "const float HBLK_OX = " + String::num(hblk_ox_, 4) + ";\n"
482 "const float HBLK_OZ = " + String::num(hblk_oz_, 4) + ";\n"
483 "const float HBLK_MPP = " + String::num(hblk_mpp_, 6) + ";\n"
484 "float hblk_height_m(float x, float z) {\n"
485 " float u = (x - HBLK_OX) / HBLK_MPP;\n"
486 " float v = (z - HBLK_OZ) / HBLK_MPP;\n"
487 " u = clamp(u, 0.0, float(HBLK_N - 1));\n"
488 " v = clamp(v, 0.0, float(HBLK_N - 1));\n"
489 " int i0 = int(floor(u)); int j0 = int(floor(v));\n"
490 " int i1 = min(i0 + 1, HBLK_N - 1); int j1 = min(j0 + 1, HBLK_N - 1);\n"
491 " float fu = u - float(i0); float fv = v - float(j0);\n"
492 " float h00 = hblk_data[j0*HBLK_N + i0]; float h10 = hblk_data[j0*HBLK_N + i1];\n"
493 " float h01 = hblk_data[j1*HBLK_N + i0]; float h11 = hblk_data[j1*HBLK_N + i1];\n"
494 " return mix(mix(h00, h10, fu), mix(h01, h11, fu), fv);\n"
495 "}\n"
496 // The five names erosion_heightmap_gn_glsl() reads. EN_HAMP is the
497 // filter's height_amp, which erosion_node_eval BAKES at 0.25 on the
498 // engine path -- the same 0.125 = HAMP*0.5 the mountain layer's
499 // "* 8.0" below is the inverse of.
500 ;
501 hblk_glsl = hblk_glsl_sampler
502 + "const float M_FLOOR_Y = " + floor_str + ";\n"
503 "const float M_CEIL_Y = " + ceil_str + ";\n"
504 "const float M_TH = " + th_str + ";\n"
505 "const float EN_HAMP = 0.25;\n"
506 "const float HBLK_STEP = " + String::num(hblk_mpp_, 6) + ";\n"
507 + String(erosion_heightmap_gn_glsl().c_str());
508 }
509
510 // THE LOOKDEV-WRAPPER EMISSION MODE IS GONE.
511 //
512 // It emitted erosion_wrapper_glsl.h's gpu_wrapper() in place of this file's
513 // assembly, so the engine ran the lookdev harness's own shader rather than a
514 // transcription of it. That was worth having while this file assembled 47 KB
515 // of terrain by hand; the wrapper is now four lines and the layers are nodes,
516 // so there is no transcription left to check against.
517 //
518 // It was also switched OFF everywhere -- its only reference in the tree was
519 // map_dc_diffusion passing false -- and it read six generator properties that
520 // no longer exist, since the layers own their parameters now.
521 //
522 // test_erosion_parity.cpp still compiles erosion_wrapper_glsl.h directly and
523 // does not go through this class, so the parity oracle is unaffected.
524
525 // DOES THE GRAPH BRING THE NOISE LIBRARY?
526 //
527 // compile_to_function emits it only when a node mentions noise, and this
528 // wrapper calls fractal_* and assigns pn_terrain_offset / pn_fade_div /
529 // pn_erosion_scale -- all of which the LIBRARY declares. A graph that needs
530 // no noise (a heightfield node on its own) leaves every one of those
531 // undeclared and the shader does not compile, which the map experiences as
532 // a silent fallback. So the wrapper's noise layers are emitted only when
533 // there is a library to support them.
534 const bool has_noise_lib = proc_glsl.find("fractal_noise_2d_lod") != -1;
535 // want_mountain is GONE with the block it gated. It was
536 // `has_noise_lib && mountain_height_ != 0.0f`, and because the erosion
537 // filter's parameters were emitted inside the same branch, setting
538 // MOUNTAIN_HEIGHT to 0 -- which every heightfield map does by definition,
539 // the baked field REPLACING the mountain layer -- silently unconfigured the
540 // filter as well. That is hyper-games #53 section 2, and it cannot recur:
541 // the mountain is a node and the filter's knobs are its own sockets.
542 // want_detail is GONE with the block it gated, as want_mountain was. The
543 // detail layer is a node; a node is in the graph or it is not.
407544
408545 String adapted =
409546 "// Octave limit — fixed high value for LOD-consistent surface position.\n"
……@@ -424,61 +561,56 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() {
424561 // right up to the shader compile. pn_max_octaves is different: nothing
425562 // else declares it, so it stays here.
426563 + proc_glsl + "\n"
564 + hblk_glsl +
427565 "float evaluate_density(float x, float y, float z) {\n"
428566 " float inv_scale = 1.0 / " + th_str + ";\n"
429567 " pn_max_octaves = 99;\n"
430 " // MOUNTAIN LAYER, height AND gradient. The gradient used to be computed and\n"
431 " // thrown away: mtn_offset was added to y_mapped AFTER the erosion, so the\n"
432 " // filter never saw the range it was carving and aligned its gullies to gn\n"
433 " // alone -- three octaves at gain 0.1, i.e. one smooth 6.7 km blob. It read\n"
434 " // as noise laid over a mountain rather than erosion of one.\n"
435 " // MULTIFRACTAL. variance 0 is bit-identical to fractal_noise_2d, so the\n"
436 " // default costs nothing; above 0 each octave is weighted by the elevation\n"
437 " // accumulated so far, putting the roughness on high ground. Plain fBm is\n"
438 " // statistically homogeneous -- equally rugged in the basins and on the\n"
439 " // peaks -- which is what reads as every range looking the same.\n"
440 " // NYQUIST LIMITS. voxel_scale is the LOD's voxel size in metres and is\n"
441 " // declared by BOTH shaders this body is injected into (density_fill.glsl\n"
442 " // and edge_intersect.glsl), above the injection point. One cycle of a\n"
443 " // layer spans terrain_height/scale world metres, so that is its base\n"
444 " // wavelength. Fractional, so an octave fades in as a chunk refines\n"
445 " // rather than snapping on and moving the surface underfoot.\n"
446 " float mtn_lim = nyquist_octaves(" + th_str + " / max(" + mscale_str + ", 1e-6), voxel_scale, " + lac_str + ");\n"
447 " float rel_lim = nyquist_octaves(" + th_str + " / max(" + rscale_str + ", 1e-6), voxel_scale, 2.0);\n"
448 " float det_lim = nyquist_octaves(" + th_str + " / max(" + dscale_str + ", 1e-6), voxel_scale, " + lac_str + ");\n"
449 " vec3 mtn = fractal_multifractal_2d_lod(vec2(x, z) * inv_scale * " + mscale_str + ", 1.0, " + moct_str + ", " + lac_str + ", " + mgain_str + ", " + String::num(mountain_variance_, 4) + ", mtn_lim);\n"
450 " float relief_amp = 1.0;\n"
451 " if (" + rscale_str + " > 0.0) {\n"
452 " vec3 rel = fractal_noise_2d_lod(vec2(x, z) * inv_scale * " + rscale_str + ", 1.0, 2, 2.0, 0.5, rel_lim);\n"
453 " float t = clamp(rel.x * 0.5 + 0.5, 0.0, 1.0);\n"
454 " relief_amp = " + rfloor_str + " + (1.0 - " + rfloor_str + ") * t;\n"
455 " }\n"
456 " float mtn_amp = " + mheight_str + " * relief_amp;\n"
457 " // mtn is evaluated at p * mountain_scale, so d/dp = mtn.yz * mountain_scale.\n"
458 " // d(relief_amp)/dp is dropped: relief is a 40 km envelope, orders coarser\n"
459 " // than anything gully direction responds to. The 8.0 is exact, not fitted --\n"
460 " // gn reaches the surface as gn.x * HEIGHT_AMP * 0.5 = gn.x * 0.125, so\n"
461 " // dividing the offset by 0.125 reproduces the identical base surface at\n"
462 " // zero erosion strength.\n"
463 " pn_terrain_offset = vec3(mtn.x, mtn.y * " + mscale_str + ", mtn.z * " + mscale_str + ") * mtn_amp * 8.0;\n"
464 " pn_fade_div = 4.8;\n"
465 " pn_erosion_scale = " + String::num(erosion_scale_, 6) + ";\n"
466 " // DETAIL rides ON TOP of the eroded surface and is deliberately NOT folded\n"
467 " // into pn_terrain_offset: its gradient carries a factor of detail_scale, so\n"
468 " // inside one gully cell the direction flips several times and the stripes go\n"
469 " // chaotic -- measured 77.6 m of relief across a 2 m step, i.e. vertical\n"
470 " // noise. Detail is what water runs over, not what steers it.\n"
471 " float det_offset = 0.0;\n"
472 " if (" + dscale_str + " > 0.0) {\n"
473 " vec3 det = fractal_noise_2d_lod(vec2(x, z) * inv_scale * " + dscale_str + ", 1.0, " + doct_str + ", " + lac_str + ", " + dgain_str + ", det_lim);\n"
474 " det_offset = det.x * " + dfrac_str + ";\n"
475 " }\n"
568 // THE MOUNTAIN LAYER IS A NODE (proc-nodes MountainNode).
569 //
570 // This block emitted the multifractal, the relief envelope and the
571 // conversion into filter space, assigning them to the global
572 // pn_terrain_offset -- with a second branch that replaced the whole
573 // thing with heightmap_gn() when a baked field was loaded. Both are
574 // graph shapes now: a Mountain node, or a Heightfield node, or both
575 // summed through Math(Add) + VectorMath(Add).
576 //
577 // The `want_mountain` flag it was gated on is what made the erosion
578 // filter's own parameters unreachable on every heightfield map
579 // (hyper-games #53 section 2), because they were emitted in the same
580 // branch. There is no flag now: a layer is in the graph or it is not.
581 // THE FILTER'S PARAMETERS BELONG TO THE FILTER, AND NOW LIVE THERE.
582 //
583 // This block used to assign twenty globals -- pn_fade_div,
584 // pn_erosion_scale, seventeen pn_ero_*, and a computed
585 // pn_ero_max_octaves -- that the erosion node then read. It was emitted
586 // INSIDE the want_mountain branch, and want_mountain is
587 // `has_noise_lib && mountain_height_ != 0.0`, which is false for every
588 // heightfield map because the baked field REPLACES the mountain layer.
589 // So on exactly the maps the filter exists for, none of it was set and
590 // the filter ran on the noise library's declaration defaults.
591 //
592 // They are input sockets on ErosionNoiseNode now
593 // (docs/spec-graph-composition.md section 5.3), baked into the emitted
594 // GLSL as literals by GpuCompiler. A socket has no branch to sit inside.
595 // THE DETAIL LAYER IS A NODE (proc-nodes DetailNode), AND SO IS THE
596 // SUBTRACTION IT USED TO MAKE HERE.
597 //
598 // This emitted a fBm into det_offset and then subtracted it from
599 // y_mapped. Density is (eroded - y_mapped), so subtracting from
600 // y_mapped and adding to the eroded height are one operation seen from
601 // two ends -- and only the second can be a node, because only the second
602 // happens after the filter has run.
603 //
604 // What is left of y_mapped is the vertical mapping and nothing else,
605 // which is all a wrapper should ever have been.
476606 + (height_space_sqrt_
607 // SIGNED-SQRT HEIGHT SPACE. Endpoints are unchanged (H=0 -> floor_y,
608 // H=height_scale -> ceiling_y), so no other parameter shifts meaning;
609 // the surface becomes y = floor_y + y_range * (H/height_scale)^2, so a
610 // constant noise amplitude produces elevation-proportional relief.
477611 ? String(" float un = (y - ") + floor_str + ") / " + String::num(y_range, 4) + ";\n"
478 " float y_mapped = sign(un) * sqrt(abs(un)) * " + String::num(height_scale, 4) +
479 " - det_offset * " + String::num(height_scale, 4) + ";\n"
480 : String(" float y_mapped = (y - ") + floor_str + ") * " + yscale_str +
481 " - det_offset * " + String::num(height_scale, 4) + ";\n")
612 " float y_mapped = sign(un) * sqrt(abs(un)) * " + String::num(height_scale, 4) + ";\n"
613 : String(" float y_mapped = (y - ") + floor_str + ") * " + yscale_str + ";\n")
482614 + ""
483615 " float d = proc_density(vec3(x * inv_scale, y_mapped, z * inv_scale));\n"
484616 " d *= " + inv_yscale_str + ";\n"
……@@ -512,14 +644,41 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate(
512644 float terrain_height = owner_->terrain_height_;
513645 float floor_y = owner_->floor_y_;
514646 float ceiling_y = owner_->ceiling_y_;
515 float mtn_scale = owner_->mountain_scale_;
516 float mtn_height = owner_->mountain_height_;
517 int mtn_octaves = owner_->mountain_octaves_;
518647
519648 // Extract noise params from discovered graph parameters
520 float base_freq = 3.0f, gn_detail = 3, gn_gain = 0.1f;
649 // gn_detail defaults to ZERO, not 3. These defaults stand in for a node that
650 // is not in the graph, and a graph with no Gradient Noise node has no base
651 // fBm -- defaulting to 3 invented three octaves the GPU never evaluates, and
652 // the CPU and GPU then disagree by exactly that. The loop below overwrites
653 // this when the node IS present.
654 float base_freq = 3.0f, gn_detail = 0, gn_gain = 0.1f;
521655 float en_height_scale = 5.0f, en_erosion_str = 0.16f, en_slope_power = 0.6f;
522656 float en_cell_scale = 1.0f; int en_octaves = 5; float en_gain = 0.5f;
657 // Defaults are ErosionNoiseNode's own socket defaults, so a graph whose
658 // erosion node predates these sockets evaluates exactly as before.
659 float ero_terrain_height = 60000.0f, ero_band_scale = 0.15f, ero_fade_div = 4.8f;
660 float ero_height_amp = 0.25f, ero_oct_lac = 2.0f, ero_gully_depth = 0.5f;
661 float ero_fine_restrict = 1.5f, ero_normalize = 0.5f;
662 float ero_off_a = -0.65f, ero_off_b = 0.0f;
663 float ero_round_x = 0.1f, ero_round_y = 0.0f, ero_round_z = 0.1f, ero_round_w = 2.0f;
664 float ero_onset_x = 1.25f, ero_onset_y = 1.25f, ero_onset_z = 2.8f, ero_onset_w = 1.5f;
665 float ero_slope_mag = 0.7f, ero_slope_mix = 1.0f;
666 // MountainNode's sockets. Height 0 means "no mountain node in this graph",
667 // which is the honest default: a layer that is not in the graph contributes
668 // nothing. The generator's own mountain_* properties are not read here any
669 // more -- they are what made the CPU compute a layer the shader had stopped
670 // emitting, and vice versa.
671 // DetailNode's sockets. Scale 0 -- the node's own default -- switches the
672 // layer off, so a graph without one evaluates as if it were never there.
673 bool has_detail = false;
674 float dt_th = 60000.0f, dt_scale = 0.0f, dt_amp = 0.0f;
675 int dt_octaves = 4;
676 float dt_gain = 0.5f, dt_lac = 1.87f;
677 bool has_mountain = false;
678 float mn_th = 60000.0f, mn_scale = 1.0f, mn_height = 0.0f;
679 int mn_octaves = 8, mn_mode = 0;
680 float mn_gain = 0.68f, mn_lac = 1.87f, mn_variance = 0.0f, mn_damping = 1.0f;
681 float mn_relief_scale = 0.0f, mn_relief_floor = 0.15f, mn_filter_amp = 0.25f;
523682 for (int i = 0; i < owner_->discovered_params_.size(); i++) {
524683 const auto &gp = owner_->discovered_params_[i];
525684 if (gp.node_title == "Gradient Noise" && gp.socket_name == "Scale")
……@@ -540,6 +699,69 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate(
540699 en_octaves = (int)gp.default_value;
541700 if (gp.node_title == "Erosion Noise" && gp.socket_name == "Gain")
542701 en_gain = (float)(double)gp.default_value;
702 if (gp.node_title == "Detail") {
703 has_detail = true;
704 const String &dn = gp.socket_name;
705 float dv = (gp.type == Variant::INT) ? (float)(int)gp.default_value
706 : (float)(double)gp.default_value;
707 if (dn == "TerrainHeight") dt_th = dv;
708 else if (dn == "Scale") dt_scale = dv;
709 else if (dn == "Amplitude") dt_amp = dv;
710 else if (dn == "Octaves") dt_octaves = (int)dv;
711 else if (dn == "Gain") dt_gain = dv;
712 else if (dn == "Lacunarity") dt_lac = dv;
713 continue;
714 }
715 if (gp.node_title == "Mountain") {
716 has_mountain = true;
717 const String &mn = gp.socket_name;
718 float mv = (gp.type == Variant::INT) ? (float)(int)gp.default_value
719 : (float)(double)gp.default_value;
720 if (mn == "TerrainHeight") mn_th = mv;
721 else if (mn == "Scale") mn_scale = mv;
722 else if (mn == "Height") mn_height = mv;
723 else if (mn == "Octaves") mn_octaves = (int)mv;
724 else if (mn == "Gain") mn_gain = mv;
725 else if (mn == "Lacunarity") mn_lac = mv;
726 else if (mn == "Variance") mn_variance = mv;
727 else if (mn == "Mode") mn_mode = (int)mv;
728 else if (mn == "Damping") mn_damping = mv;
729 else if (mn == "ReliefScale") mn_relief_scale = mv;
730 else if (mn == "ReliefFloor") mn_relief_floor = mv;
731 else if (mn == "FilterHeightAmplitude") mn_filter_amp = mv;
732 continue;
733 }
734 // THE FILTER'S OWN KNOBS, read from the same sockets the shader bakes.
735 //
736 // This path called erosion_node_eval -- TEN arguments, runevision's
737 // reference values baked inside -- while the GPU called
738 // erosion_node_eval_full with NINETEEN. So every one of the seventeen
739 // was already a CPU/GPU divergence: turn one and the surface you see
740 // moves while the surface you collide with does not. Collision reads
741 // this function; rendering reads the shader.
742 if (gp.node_title != "Erosion Noise") continue;
743 const String &sn = gp.socket_name;
744 float v = (float)(double)gp.default_value;
745 if (sn == "TerrainHeight") ero_terrain_height = v;
746 else if (sn == "GullyBandScale") ero_band_scale = v;
747 else if (sn == "PeakHeightDivisor") ero_fade_div = v;
748 else if (sn == "InputHeightAmplitude") ero_height_amp = v;
749 else if (sn == "OctaveLacunarity") ero_oct_lac = v;
750 else if (sn == "GullyDepth") ero_gully_depth = v;
751 else if (sn == "FineGullySlopeRestriction") ero_fine_restrict = v;
752 else if (sn == "GullyNormalizeThreshold") ero_normalize = v;
753 else if (sn == "HeightOffset") ero_off_a = v;
754 else if (sn == "HeightOffsetFollowsAltitude") ero_off_b = v;
755 else if (sn == "CreaseRounding") ero_round_x = v;
756 else if (sn == "RidgeRounding") ero_round_y = v;
757 else if (sn == "InputFeatureRounding") ero_round_z = v;
758 else if (sn == "RoundingPerOctave") ero_round_w = v;
759 else if (sn == "GullyOnsetSlope") ero_onset_x = v;
760 else if (sn == "OctaveOnsetSlope") ero_onset_y = v;
761 else if (sn == "RidgeOnsetSlope") ero_onset_z = v;
762 else if (sn == "RidgeOctaveOnset") ero_onset_w = v;
763 else if (sn == "AssumedSlope") ero_slope_mag = v;
764 else if (sn == "AssumedSlopeMix") ero_slope_mix = v;
543765 }
544766
545767 float inv_scale = 1.0f / terrain_height;
……@@ -557,49 +779,192 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate(
557779 // remaining piece.
558780 using dcerosion::vec2;
559781 using dcerosion::vec3;
560 // MUST match the GLSL above: same base wavelengths, same lacunarities.
561 const float lac = owner_->noise_lacunarity_;
562 float mtn_lim = dcerosion::nyquist_octaves(terrain_height / MAX(mtn_scale, 1e-6f),
563 lod_voxel_size, lac);
564 float rel_lim = dcerosion::nyquist_octaves(
565 terrain_height / MAX(owner_->relief_scale_, 1e-6f), lod_voxel_size, 2.0f);
566 float det_lim = dcerosion::nyquist_octaves(
567 terrain_height / MAX(owner_->detail_scale_, 1e-6f), lod_voxel_size, lac);
568
569 vec3 mtn = dcerosion::fractal_multifractal_2d_lod(vec2(x, z) * inv_scale * mtn_scale, 1.0f,
570 mtn_octaves, lac, owner_->mountain_gain_,
571 owner_->mountain_variance_, mtn_lim);
572 // Relief field — MUST stay identical to the GLSL above.
573 float relief_amp = 1.0f;
574 if (owner_->relief_scale_ > 0.0f) {
575 vec3 rel = dcerosion::fractal_noise_2d_lod(vec2(x, z) * inv_scale * owner_->relief_scale_,
576 1.0f, 2, 2.0f, 0.5f, rel_lim);
577 float t = dcerosion::clamp(rel.x * 0.5f + 0.5f, 0.0f, 1.0f);
578 relief_amp = owner_->relief_floor_ + (1.0f - owner_->relief_floor_) * t;
782
783 // DOES THE GRAPH ACTUALLY HAVE AN EROSION NODE?
784 //
785 // This function is a hardcoded pipeline -- gn, then terrain_offset, then
786 // erosion_node_eval -- written when the graph was always GradientNoise ->
787 // ErosionNoise. A graph of one Heightfield node runs none of that on the
788 // GPU, where the node sets pos.y itself, so the CPU answered from a pipeline
789 // the shader no longer runs and reported a surface 5.3 km out. sample_density
790 // drives the spawn, _lift_out_of_terrain and the fall-respawn, so that is a
791 // player floating kilometres off the ground.
792 //
793 // The honest fix is a CPU evaluator that walks the graph the way the GPU
794 // compiler does. Until then, match the two shapes that exist.
795 bool graph_has_erosion = false;
796 for (int i = 0; i < owner_->discovered_params_.size(); i++) {
797 if (owner_->discovered_params_[i].node_title == "Erosion Noise") {
798 graph_has_erosion = true;
799 break;
800 }
801 }
802 if (!graph_has_erosion && owner_->hblk_n_ > 0) {
803 // Heightfield alone: density IS height minus query, exactly what the
804 // node emits. No filter space, no offsets, nothing to get wrong.
805 const std::vector<float> &hb = owner_->hblk_;
806 const int hn = owner_->hblk_n_;
807 const float hox = owner_->hblk_ox_, hoz = owner_->hblk_oz_, hmpp = owner_->hblk_mpp_;
808 float u = dcerosion::clamp((x - hox) / hmpp, 0.0f, (float)(hn - 1));
809 float v = dcerosion::clamp((z - hoz) / hmpp, 0.0f, (float)(hn - 1));
810 int i0 = (int)floorf(u), j0 = (int)floorf(v);
811 int i1 = MIN(i0 + 1, hn - 1), j1 = MIN(j0 + 1, hn - 1);
812 float fu = u - (float)i0, fv = v - (float)j0;
813 float h00 = hb[(size_t)j0*hn + i0], h10 = hb[(size_t)j0*hn + i1];
814 float h01 = hb[(size_t)j1*hn + i0], h11 = hb[(size_t)j1*hn + i1];
815 float a0 = h00 + (h10 - h00) * fu, b0 = h01 + (h11 - h01) * fu;
816 float d = (a0 + (b0 - a0) * fv) - y;
817 d = MAX(d, (floor_y - y) * 2.0f);
818 d = MIN(d, (ceiling_y - y) * 2.0f);
819 return d;
820 }
821
822 // HEIGHTFIELD ONLY — the CPU twin, and deliberately trivial. There is no
823 // algebra to get wrong here, so if the world still looks like noise with
824 // this on, the field is not reaching the GPU and this is what you are
825 // standing on instead.
826 if (owner_->heightfield_only_ && owner_->hblk_n_ > 0) {
827 const std::vector<float> &hbv = owner_->hblk_;
828 const int hn = owner_->hblk_n_;
829 const float hox = owner_->hblk_ox_, hoz = owner_->hblk_oz_, hmpp = owner_->hblk_mpp_;
830 float u = dcerosion::clamp((x - hox) / hmpp, 0.0f, (float)(hn - 1));
831 float v = dcerosion::clamp((z - hoz) / hmpp, 0.0f, (float)(hn - 1));
832 int i0 = (int)floorf(u), j0 = (int)floorf(v);
833 int i1 = MIN(i0 + 1, hn - 1), j1 = MIN(j0 + 1, hn - 1);
834 float fu = u - (float)i0, fv = v - (float)j0;
835 float h00 = hbv[(size_t)j0*hn + i0], h10 = hbv[(size_t)j0*hn + i1];
836 float h01 = hbv[(size_t)j1*hn + i0], h11 = hbv[(size_t)j1*hn + i1];
837 float a0 = h00 + (h10 - h00) * fu, b0 = h01 + (h11 - h01) * fu;
838 float d = (a0 + (b0 - a0) * fv) - y;
839 d = MAX(d, (floor_y - y) * 2.0f);
840 d = MIN(d, (ceiling_y - y) * 2.0f);
841 return d;
842 }
843
844 // The three Nyquist limits that used to be computed here for three layers at
845 // once are computed inside each layer's own block now, from that layer's own
846 // sockets -- which is what stopped the detail layer depending on a local the
847 // mountain layer happened to declare.
848
849 // THE MOUNTAIN LAYER — MountainNode's CPU twin, term for term.
850 //
851 // Read from the NODE's sockets, not from generator properties. While these
852 // were two independent parameter sets the CPU could compute a layer the
853 // shader had stopped emitting, which is precisely how sample_density came
854 // to report a surface 5.3 km from the one being drawn.
855 vec3 mtn = vec3(0.0f, 0.0f, 0.0f);
856 float mtn_amp = 0.0f;
857 if (has_mountain) {
858 float mn_lim = dcerosion::nyquist_octaves(mn_th / MAX(mn_scale, 1e-6f),
859 lod_voxel_size, mn_lac);
860 vec2 mn_p = vec2(x, z) * inv_scale * mn_scale;
861 mtn = (mn_mode == 1)
862 ? dcerosion::fractal_gradient_2d_lod(mn_p, 1.0f, mn_octaves, mn_lac,
863 mn_gain, mn_damping, mn_lim)
864 : dcerosion::fractal_multifractal_2d_lod(mn_p, 1.0f, mn_octaves, mn_lac,
865 mn_gain, mn_variance, mn_lim);
866 float relief_amp = 1.0f;
867 if (mn_relief_scale > 0.0f) {
868 vec3 rel = dcerosion::fractal_noise_2d_lod(vec2(x, z) * inv_scale * mn_relief_scale,
869 1.0f, 2, 2.0f, 0.5f,
870 dcerosion::nyquist_octaves(mn_th / MAX(mn_relief_scale, 1e-6f),
871 lod_voxel_size, 2.0f));
872 float t = dcerosion::clamp(rel.x * 0.5f + 0.5f, 0.0f, 1.0f);
873 relief_amp = mn_relief_floor + (1.0f - mn_relief_floor) * t;
874 }
875 // 1/(FilterHeightAmplitude * 0.5) -- the wrapper's `* 8.0`, derived
876 // rather than baked, and it must match the erosion node's
877 // InputHeightAmplitude.
878 mtn_amp = (mn_height * relief_amp) / (mn_filter_amp * 0.5f);
579879 }
580 float mtn_amp = mtn_height * relief_amp;
581 // The mountain's HEIGHT AND GRADIENT, in gn units, to hand to the erosion —
582 // the pn_terrain_offset the GLSL sets. See the comment there for the 8.0 and
583 // for why the gradient stopped being thrown away.
584 vec3 terrain_offset = vec3(mtn.x, mtn.y * mtn_scale, mtn.z * mtn_scale) * (mtn_amp * 8.0f);
585 // Detail rides on TOP and is NOT folded in — MUST stay identical to the GLSL.
880 // The mountain's HEIGHT AND GRADIENT in gn units, which is what the Add
881 // nodes in the graph sum into the erosion filter's Height and Slope inputs.
882 vec3 terrain_offset = vec3(mtn.x, mtn.y * mn_scale, mtn.z * mn_scale) * mtn_amp;
883
884 // THE SAME SUBSTITUTION THE GLSL MAKES.
885 //
886 // Not for collision -- that is COLLISION_TRIMESH, built from the GPU mesh.
887 // For two other reasons, both of which cost real time when this was missing:
888 // 1. sample_density() is this path, so without it every headless gate and
889 // every map script asking "where is the ground" answers from the fBm
890 // world the heightfield replaced -- and a gate that cannot see the
891 // field cannot tell a correct shader from one that never ran.
892 // 2. The !gpu_filled fallback in dc_worker_cpu.cpp fills chunks from here.
893 // Left unwired it generates DIFFERENT TERRAIN from its neighbours.
894 //
895 // The algebra is erosion_wrapper_glsl.h's erosion_heightmap_gn_cpu, sitting
896 // beside the GLSL it mirrors, so the two cannot drift apart quietly.
897 if (owner_->hblk_n_ > 0) {
898 const std::vector<float> &hb = owner_->hblk_;
899 const int hn = owner_->hblk_n_;
900 const float hox = owner_->hblk_ox_, hoz = owner_->hblk_oz_, hmpp = owner_->hblk_mpp_;
901 auto sample = [&](float sx, float sz) -> float {
902 float u = dcerosion::clamp((sx - hox) / hmpp, 0.0f, (float)(hn - 1));
903 float v = dcerosion::clamp((sz - hoz) / hmpp, 0.0f, (float)(hn - 1));
904 int i0 = (int)floorf(u), j0 = (int)floorf(v);
905 int i1 = MIN(i0 + 1, hn - 1), j1 = MIN(j0 + 1, hn - 1);
906 float fu = u - (float)i0, fv = v - (float)j0;
907 float h00 = hb[(size_t)j0*hn + i0], h10 = hb[(size_t)j0*hn + i1];
908 float h01 = hb[(size_t)j1*hn + i0], h11 = hb[(size_t)j1*hn + i1];
909 float a0 = h00 + (h10 - h00) * fu;
910 float b0 = h01 + (h11 - h01) * fu;
911 return a0 + (b0 - a0) * fv;
912 };
913 float oh, odx, odz;
914 erosion_heightmap_gn_cpu(x, z, floor_y, ceiling_y, terrain_height,
915 /* height_amp */ 0.25f, hmpp, sample, oh, odx, odz);
916 terrain_offset = vec3(oh, odx, odz);
917 }
918 // THE SAME SUBSTITUTION THE GLSL MAKES, and it must stay the same one:
919 // collision reads this field while rendering reads the shader's, so a
920 // divergence here is a player falling through ground that is visibly there.
921 // The algebra comes from erosion_wrapper_glsl.h, beside the GLSL twin.
922 if (owner_->hblk_n_ > 0) {
923 const std::vector<float> &hb = owner_->hblk_;
924 const int hn = owner_->hblk_n_;
925 const float hox = owner_->hblk_ox_, hoz = owner_->hblk_oz_, hmpp = owner_->hblk_mpp_;
926 auto sample = [&](float sx, float sz) -> float {
927 float u = (sx - hox) / hmpp;
928 float v = (sz - hoz) / hmpp;
929 u = dcerosion::clamp(u, 0.0f, (float)(hn - 1));
930 v = dcerosion::clamp(v, 0.0f, (float)(hn - 1));
931 int i0 = (int)floorf(u), j0 = (int)floorf(v);
932 int i1 = MIN(i0 + 1, hn - 1), j1 = MIN(j0 + 1, hn - 1);
933 float fu = u - (float)i0, fv = v - (float)j0;
934 float h00 = hb[(size_t)j0 * hn + i0], h10 = hb[(size_t)j0 * hn + i1];
935 float h01 = hb[(size_t)j1 * hn + i0], h11 = hb[(size_t)j1 * hn + i1];
936 float a = h00 + (h10 - h00) * fu;
937 float b = h01 + (h11 - h01) * fu;
938 return a + (b - a) * fv;
939 };
940 float oh, odx, odz;
941 erosion_heightmap_gn_cpu(x, z, floor_y, ceiling_y, terrain_height,
942 /* height_amp */ 0.25f, hmpp, sample, oh, odx, odz);
943 terrain_offset = vec3(oh, odx, odz);
944 }
945 // DETAIL — DetailNode's CPU twin, read from its sockets.
946 //
947 // It is ADDED to the eroded height rather than subtracted from y_mapped.
948 // Those are the same algebra, since density is (eroded - y_mapped), but only
949 // the first can run after the filter, which is where a layer that rides on
950 // the eroded surface belongs.
586951 float det_offset = 0.0f;
587 if (owner_->detail_scale_ > 0.0f) {
588 float detail_frac = (y_range > 0.01f) ? (owner_->detail_height_ / y_range) : 0.0f;
589 vec3 det = dcerosion::fractal_noise_2d_lod(vec2(x, z) * inv_scale * owner_->detail_scale_,
590 1.0f, owner_->detail_octaves_, lac, owner_->detail_gain_, det_lim);
591 det_offset = det.x * detail_frac;
952 if (has_detail && dt_scale > 0.0f) {
953 vec3 det = dcerosion::fractal_noise_2d_lod(vec2(x, z) * inv_scale * dt_scale,
954 1.0f, dt_octaves, dt_lac, dt_gain,
955 dcerosion::nyquist_octaves(dt_th / MAX(dt_scale, 1e-6f), lod_voxel_size, dt_lac));
956 det_offset = det.x * dt_amp;
592957 }
593958
594 // Map world Y to proc_density space
595 // MUST mirror the GLSL above, both branches.
959 // THE VERTICAL MAPPING, and nothing else -- the wrapper's whole remaining
960 // job. MUST mirror the GLSL above, both branches.
596961 float y_mapped;
597962 if (owner_->height_space_sqrt_) {
598963 float un = (y - floor_y) / y_range;
599964 float su = (un < 0.0f) ? -sqrtf(-un) : sqrtf(un);
600 y_mapped = su * en_height_scale - det_offset * en_height_scale;
965 y_mapped = su * en_height_scale;
601966 } else {
602 y_mapped = (y - floor_y) * y_scale - det_offset * en_height_scale;
967 y_mapped = (y - floor_y) * y_scale;
603968 }
604969
605970 // GradientNoiseNode. Lacunarity is hardcoded 2.0 in the node
……@@ -615,13 +980,43 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate(
615980 // wrapper, shared with the GLSL, so there is nothing left to get wrong.
616981 // It returns vec2(height * height_scale, erosion); .y is the ridge/crease
617982 // value, unused on the density path.
618 vec2 en = dcerosion::erosion_node_eval(
983 // slope_power is gone from the signature -- it was never read. Parsed off
984 // the graph above and deliberately unused, so old graphs still load.
985 (void)en_slope_power;
986 // THE FRACTIONAL OCTAVE LIMIT, computed here exactly as the shader computes
987 // it. If these two expressions ever differ the surface you walk on and the
988 // surface you see part company by however much an octave is worth, which is
989 // the failure test_erosion_parity exists to catch -- it caught a 60x version
990 // of it once already.
991 //
992 // The coarsest gully spans terrain_height * gully_band_scale * cell_size in
993 // world metres and halves per octave, so that is the base wavelength.
994 // THE SAME EXPRESSION THE NODE EMITS, from the same sockets: the coarsest
995 // gully spans TerrainHeight * GullyBandScale * CellScale in world metres and
996 // halves per octave. If these two ever differ the surface you walk on and
997 // the surface you see part company by however much an octave is worth.
998 float ero_max_oct = 999.0f;
999 if (lod_voxel_size > 0.0f) {
1000 ero_max_oct = dcerosion::nyquist_octaves(
1001 ero_terrain_height * ero_band_scale * en_cell_scale,
1002 lod_voxel_size, 2.0f);
1003 }
1004 // NINETEEN ARGUMENTS, matching the shader. See the note above the socket
1005 // reads for what calling the ten-argument form cost.
1006 vec2 en = dcerosion::erosion_node_eval_full(
6191007 uv, gn + terrain_offset, en_height_scale, en_erosion_str,
620 en_slope_power, en_cell_scale, en_octaves, en_gain, 4.8f,
621 owner_->erosion_scale_);
622
623 // SDF convention: negative = inside, positive = outside.
624 float d = en.x - y_mapped;
1008 en_cell_scale, en_octaves, en_gain, ero_fade_div, ero_band_scale,
1009 ero_height_amp, ero_oct_lac, ero_gully_depth, ero_fine_restrict,
1010 ero_normalize, dcerosion::vec2(ero_off_a, ero_off_b),
1011 dcerosion::vec4(ero_round_x, ero_round_y, ero_round_z, ero_round_w),
1012 dcerosion::vec4(ero_onset_x, ero_onset_y, ero_onset_z, ero_onset_w),
1013 dcerosion::vec2(ero_slope_mag, ero_slope_mix),
1014 ero_max_oct);
1015
1016 // SDF convention: negative = inside, positive = outside. det_offset is added
1017 // to the eroded height, which is exactly what DetailNode's `pos.y += ...`
1018 // does on the GPU.
1019 float d = (en.x + det_offset) - y_mapped;
6251020 d *= inv_y_scale;
6261021
6271022 // Clip: solid below floor_y, air above ceiling_y
……@@ -642,4 +1037,24 @@ const char *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_shader_source() co
6421037 return owner_->cached_glsl_.empty() ? nullptr : owner_->cached_glsl_.c_str();
6431038}
6441039
1040const float *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data() const {
1041 return owner_->hblk_.empty() ? nullptr : owner_->hblk_.data();
1042}
1043
1044int DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data_count() const {
1045 // THE NUMBER OF FLOATS, not the tile's side.
1046 //
1047 // hblk_ has two producers and they store different shapes: _load_heightfield
1048 // stores a plain n*n square, while _collect_node_data stores the node's MIP
1049 // PYRAMID -- 1,403,226 floats for a 1026 tile, because hblk_sample needs the
1050 // coarser levels to band-limit itself per LOD. Returning the side made
1051 // DCCompute upload side*side, which is exactly level 0 and nothing else, so
1052 // HBLK_OFF[1] = 1052676 pointed one float past the end of the buffer.
1053 //
1054 // Every read at LOD >= 1 then came back zero and the terrain was flat
1055 // wherever the voxel exceeded the block's 30 m pitch -- which is most of a
1056 // 917 km world.
1057 return (int)owner_->hblk_.size();
1058}
1059
6451060} // namespace godot
src/dc_terrain_generator_procnodes.h +81 -39
……@@ -33,9 +33,6 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator {
3333
3434 // Mountain layer: low-frequency noise that creates large-scale terrain shape.
3535 // The erosion detail rides on top of this.
36 float mountain_scale_ = 1.0f; // Features per terrain_height (lower = bigger mountains)
37 float mountain_height_ = 0.5f; // Amplitude as fraction of [floor_y, ceiling_y] range
38 int mountain_octaves_ = 2; // Octaves of mountain noise
3936
4037 // RELIEF FIELD — makes mountain amplitude a FUNCTION OF PLACE.
4138 //
……@@ -47,8 +44,6 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator {
4744 //
4845 // relief_scale_ = 0 DISABLES it (amplitude is uniform, exactly as before), so
4946 // every existing generator keeps its current output.
50 float relief_scale_ = 0.0f; // Relief lobes per terrain_height; 0 = off
51 float relief_floor_ = 0.15f; // Amplitude in the flattest regions, 0..1
5247
5348 // PERSISTENCE, which was hardcoded 0.5 and is why the terrain is smooth
5449 // underfoot. Octave k has amplitude gain^k of an amplitude set by
……@@ -57,7 +52,80 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator {
5752 // raising mountain_octaves 4 -> 12 moved local roughness 0.72 -> 0.98 m per
5853 // 2 m stride; it cannot fix this, because it only adds ever-tinier ripples.
5954 // 0.5 is the previous behaviour exactly.
60 float mountain_gain_ = 0.5f;
55
56 // ── THE EROSION FILTER'S OWN KNOBS ────────────────────────────────────
57 // Baked literals inside erosion_node_eval until now, because GpuCompiler's
58 // push-constant block is param0..param6 -- seven floats, a hard cap, and
59 // rounding and onset alone need eight. That cap only ever applied to SOCKET
60 // parameters; pn_fade_div and pn_erosion_scale already escaped it by being
61 // globals the wrapper sets. These take the same door.
62 //
63 // Defaults are runevision's reference values, so an untouched generator
64 // produces exactly the previous surface.
65 // scales the incoming height field into the filter's space
66 // frequency step per gully octave
67 // scales the gully contribution down
68 // LOWER keeps high-frequency gullies to steeper slopes
69 // only gullies above this are normalised -- keeps peaks pointy
70 // flat offset, times the accumulated octave magnitude
71 // mixes that offset toward -fade_target
72 // rounding applied in creases
73 // rounding applied on ridges
74 // the INPUT height function's feature size -- must move when terrain_height does
75 // rounding multiplier compounded per octave
76 // slope at which gullies begin at all
77 // per-octave slope gate
78 // the same, for the ridge map
79 // per-octave ridge gate
80 // the slope magnitude to pretend the input has
81 // 0 = use the real slope, 1 = fully assumed
82
83 // ── Baked heightfield (hyper-games #50 section 5.3) ────────────────
84 // A terrain-diffusion block in METRES, from terrain-lookdev/stitch_block.py.
85 // When loaded it REPLACES the mountain layer: pn_terrain_offset stops being
86 // the multifractal and becomes the field, which is exactly where the model's
87 // large forms belong -- the erosion filter then carves the model's landscape
88 // instead of an fBm blob, and the fine gn base noise is untouched.
89 //
90 // The geometry lives here as literals baked into the emitted GLSL, so
91 // changing block invalidates the shader (a recompile), not a uniform.
92 String heightfield_path_;
93 std::vector<float> hblk_;
94 int hblk_n_ = 0;
95 float hblk_mpp_ = 30.0f, hblk_ox_ = 0.0f, hblk_oz_ = 0.0f;
96 bool _load_heightfield(const String &p_path);
97
98 // HEIGHTFIELD ONLY — the isolation switch.
99 //
100 // Density becomes height(x,z) - y and nothing else: no graph, no erosion, no
101 // noise library, no filter-space conversion. It exists because "the terrain
102 // is just perlin noise" has exactly one cheap explanation -- the GPU shader
103 // failed and DCCompute fell back to the CPU path, which evaluates the fBm
104 // mountain -- and the only way to tell that apart from a wrong field is to
105 // remove everything that could be wrong except the field itself.
106 //
107 // Both paths implement it, so it also answers WHICH path ran: they cannot
108 // disagree here, because there is nothing to disagree about.
109 bool heightfield_only_ = false;
110
111 // LOOKDEV WRAPPER — emit the SHARED wrapper instead of this file's.
112 //
113 // _recompile_glsl assembles ~47 KB of engine-specific GLSL: a multifractal
114 // mountain layer, a relief layer, a 4-octave detail fBm, a y-mapping and a
115 // clip. The lookdev is none of that -- it is the heightfield and the erosion
116 // filter, full stop. Swapping ONE term inside the engine wrapper and calling
117 // it "identical to lookdev" was wrong: the detail layer alone puts +/-30 m of
118 // fBm into the output that the lookdev never had.
119 //
120 // With this on the generator emits, instead:
121 // noise library (GpuCompiler::get_noise_library_glsl)
122 // hblk sampler (ours -- storage is per-consumer)
123 // erosion_wrapper_glsl(heightmap_source = true)
124 // evaluate_density(x,y,z) { return gpu_wrapper(x,y,z); }
125 //
126 // gpu_wrapper is the function erosion_render compiles and test_erosion_parity
127 // guards, so the engine runs the lookdev's shader rather than a transcription
128 // of it. That removes the entire hand-assembled wrapper as a source of error.
61129
62130 // DETAIL LAYER — high frequency with its OWN amplitude, in METRES.
63131 //
……@@ -71,30 +139,23 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator {
71139 // mountains so an Everest region and a Vermont region can share a map — but
72140 // a Vermont hillside still has rock underfoot. Scaling detail by relief too
73141 // made the low-relief lobes glassy, which is where the player spawns.
74 float detail_scale_ = 0.0f; // Detail features per terrain_height; 0 = off
75 float detail_height_ = 0.0f; // Amplitude in WORLD METRES
76 int detail_octaves_ = 4;
77 float detail_gain_ = 0.5f;
78142
79143 // LACUNARITY, hardcoded 2.0 until now — and 2.0 is exactly why the terrain
80144 // reads as self-similar. With frequency doubling and gain constant, every
81145 // octave is a scaled copy of the last and they all land on harmonics of each
82146 // other. A non-integer ratio (~1.87) stops them lining up. Applied to BOTH
83147 // the mountain and detail fBm, since the repetition shows in both.
84 float noise_lacunarity_ = 2.0f;
85148 // Gully band position: coarsest gully wavelength is
86149 // terrain_height * erosion_scale * cell_scale, in world metres. Was baked at
87150 // 0.15 inside erosion_node_eval, which locked the gully band to a fixed
88151 // fraction of the terrain scale -- so raising terrain_height for view
89152 // distance dragged the gullies up with it.
90 float erosion_scale_ = 0.15f;
91153
92154 // Multifractal blend for the MOUNTAIN layer. 0 = plain fBm (bit-identical to
93155 // fractal_noise_2d, verified), 1 = each octave's amplitude weighted by the
94156 // elevation accumulated so far, giving rugged peaks above smooth valleys.
95157 // Measured at 1.0: roughness on high ground is 5.8x that on low ground,
96158 // against 0.98 -- i.e. homogeneous -- for fBm.
97 float mountain_variance_ = 0.0f;
98159
99160 // SIGNED-SQRT HEIGHT SPACE — opt in, default off.
100161 //
……@@ -141,6 +202,7 @@ private:
141202 };
142203 Vector<GraphParam> discovered_params_;
143204 void _discover_params();
205 void _collect_node_data();
144206
145207 // Bridge: implements DCGenerator interface
146208 class ProcNodesBridge : public DCGenerator {
……@@ -152,6 +214,8 @@ private:
152214 float evaluate(float x, float y, float z, float lod_voxel_size) override;
153215 bool has_gpu_shader() const override;
154216 const char *gpu_shader_source() const override;
217 const float *gpu_custom_data() const override;
218 int gpu_custom_data_count() const override;
155219 };
156220 ProcNodesBridge bridge_{ this };
157221
……@@ -179,32 +243,10 @@ public:
179243 void set_ceiling_y(float p_val);
180244 float get_ceiling_y() const { return ceiling_y_; }
181245
182 void set_mountain_scale(float p_val);
183 float get_mountain_scale() const { return mountain_scale_; }
184 void set_mountain_height(float p_val);
185 float get_mountain_height() const { return mountain_height_; }
186 void set_mountain_octaves(int p_val);
187 int get_mountain_octaves() const { return mountain_octaves_; }
188 void set_relief_scale(float p_val);
189 float get_relief_scale() const { return relief_scale_; }
190 void set_relief_floor(float p_val);
191 float get_relief_floor() const { return relief_floor_; }
192 void set_mountain_gain(float p_val);
193 float get_mountain_gain() const { return mountain_gain_; }
194 void set_detail_scale(float p_val);
195 float get_detail_scale() const { return detail_scale_; }
196 void set_detail_height(float p_val);
197 float get_detail_height() const { return detail_height_; }
198 void set_detail_octaves(int p_val);
199 int get_detail_octaves() const { return detail_octaves_; }
200 void set_detail_gain(float p_val);
201 float get_detail_gain() const { return detail_gain_; }
202 void set_noise_lacunarity(float p_val);
203 void set_erosion_scale(float value);
204 void set_mountain_variance(float value);
205 float get_mountain_variance() const { return mountain_variance_; }
206 float get_erosion_scale() const { return erosion_scale_; }
207 float get_noise_lacunarity() const { return noise_lacunarity_; }
246 void set_heightfield_path(const String &p_path);
247 String get_heightfield_path() const { return heightfield_path_; }
248 void set_heightfield_only(bool p_val);
249 bool get_heightfield_only() const { return heightfield_only_; }
208250 void set_height_space_sqrt(bool p_val);
209251 bool get_height_space_sqrt() const { return height_space_sqrt_; }
210252
src/dc_worker_cpu.cpp +6 -1
……@@ -234,15 +234,20 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us
234234 DCMeshData gpu_mesh;
235235 if (gpu_thread_ && request.density && request.gpu_eligible) {
236236 const char* custom_glsl = nullptr;
237 const float* custom_data = nullptr;
238 int custom_data_n = 0;
237239 if (request.generator && request.generator->has_gpu_shader()) {
238240 custom_glsl = request.generator->gpu_shader_source();
241 custom_data = request.generator->gpu_custom_data();
242 custom_data_n = request.generator->gpu_custom_data_count();
239243 }
240244 const std::vector<TerrainEdit>* gpu_edit_ptr =
241245 (request.edits && !request.edits->empty()) ? request.edits.get() : nullptr;
242246 // GPU path handles edits internally via compute shader
243247 gpu_filled = gpu_thread_->submit(grid, *request.density, custom_glsl, gpu_edit_ptr,
244248 &gpu_mesh, request.cell_origin, request.voxel_size,
245 (request.use_adaptive_qef ? 2u : 0u) | (request.use_shortest_diagonal ? 1u : 0u));
249 (request.use_adaptive_qef ? 2u : 0u) | (request.use_shortest_diagonal ? 1u : 0u),
250 custom_data, custom_data_n);
246251 if (!gpu_mesh.indices.empty()) {
247252 gpu_meshed = true;
248253 }
test/benchmark_cpu_dc_throughput +0 -0
(no content changes — binary or rename)
tests/dump_density_parts.cpp +13 -0
……@@ -0,0 +1,13 @@
1// Print the EXACT header and main the density pipeline uses, so the gate
2// assembles the same string _build_custom_shader does rather than a copy.
3#include <cstdio>
4#include <string>
5#include "dc_shader_source.h"
6int main(int argc, char** argv) {
7 if (argc < 3) { fprintf(stderr, "usage: dump_density_parts <header.out> <main.out>\n"); return 2; }
8 FILE* h = fopen(argv[1], "wb"); fputs(DENSITY_GLSL_HEADER, h); fclose(h);
9 FILE* m = fopen(argv[2], "wb"); fputs(DENSITY_GLSL_MAIN, m); fclose(m);
10 printf("header %zu bytes, main %zu bytes\n",
11 std::string(DENSITY_GLSL_HEADER).size(), std::string(DENSITY_GLSL_MAIN).size());
12 return 0;
13}
tests/gate_hblk_compile.sh +69 -0
……@@ -0,0 +1,69 @@
1#!/bin/sh
2# Does the heightfield density shader COMPILE, exactly as assembled?
3#
4# _build_custom_shader() is HEADER + body + MAIN (dc_shader_source.h:721). This
5# reproduces that from the SAME constants -- dumped by a C++ probe rather than
6# transcribed -- around the body the generator actually emitted, and runs glslc.
7#
8# The failure it exists for is specific: a body that does not compile leaves the
9# world empty while every log line looks healthy. The CONTROL is the same
10# assembly with a deliberately broken body, which must be REJECTED; without it
11# a glslc that silently accepted anything would read as a pass.
12#
13# tests/gate_hblk_compile.sh /tmp/hblk_body.glsl
14set -e
15BODY="${1:-/tmp/hblk_body.glsl}"
16DIR=$(cd "$(dirname "$0")/.." && pwd)
17
18[ -f "$BODY" ] || { echo "missing $BODY -- run the godot emission gate first"; exit 2; }
19
20g++ -O0 -std=c++17 -I "$DIR/libdc/include" -o /tmp/dump_density_parts \
21 "$DIR/tests/dump_density_parts.cpp"
22/tmp/dump_density_parts /tmp/density_header.glsl /tmp/density_main.glsl
23
24cat /tmp/density_header.glsl > /tmp/hblk_full.comp
25printf '\n' >> /tmp/hblk_full.comp
26cat "$BODY" >> /tmp/hblk_full.comp
27printf '\n' >> /tmp/hblk_full.comp
28cat /tmp/density_main.glsl >> /tmp/hblk_full.comp
29
30echo "assembled $(wc -c < /tmp/hblk_full.comp) bytes"
31if glslc -fshader-stage=compute /tmp/hblk_full.comp -o /tmp/hblk_full.spv 2>/tmp/hblk_err.txt; then
32 echo " COMPILE PASS ($(wc -c < /tmp/hblk_full.spv) bytes of SPIR-V)"
33else
34 echo " COMPILE FAIL"
35 head -20 /tmp/hblk_err.txt
36 exit 1
37fi
38
39# CONTROL: break the body and require rejection.
40#
41# THE ANCHOR IS THE ENTRY POINT, because that is the one line every emission
42# mode contains. The previous anchor was `float y_range = M_CEIL_Y - M_FLOOR_Y;`,
43# which appears ONLY in the lookdev wrapper's text -- so on an engine-wrapper
44# dump this took the SKIPPED branch and exited 2, and the gate was blind on
45# exactly the path most changes touch. It had a comment saying anchoring on a
46# mode-specific line was the mistake, and then did it.
47#
48# Renaming the function rather than dropping a semicolon also makes the control
49# stronger: it proves the assembled MAIN actually calls the body. A body that
50# compiles but is never called would pass a syntax-error control.
51sed '0,/float evaluate_density(/s//float evaluate_density_NOT_CALLED(/' \
52 "$BODY" > /tmp/hblk_broken.glsl
53if ! cmp -s "$BODY" /tmp/hblk_broken.glsl; then
54 cat /tmp/density_header.glsl > /tmp/hblk_broken.comp
55 printf '\n' >> /tmp/hblk_broken.comp
56 cat /tmp/hblk_broken.glsl >> /tmp/hblk_broken.comp
57 printf '\n' >> /tmp/hblk_broken.comp
58 cat /tmp/density_main.glsl >> /tmp/hblk_broken.comp
59 if glslc -fshader-stage=compute /tmp/hblk_broken.comp -o /dev/null 2>/dev/null; then
60 echo " CONTROL broken body ACCEPTED -- this gate is blind"
61 exit 2
62 fi
63 echo " CONTROL broken body REJECTED (gate can fail)"
64else
65 echo " CONTROL SKIPPED -- substitution not present to break"
66 exit 2
67fi
68echo ""
69echo "VERDICT: the assembled heightfield density shader compiles."
tests/test_erosion_gradient.cpp +129 -0
……@@ -0,0 +1,129 @@
1// ============================================================================
2// IS THE FILTER'S OWN SLOPE THE GRADIENT OF ITS OWN HEIGHT?
3//
4// erosion_filter accumulates height and both slope components together and
5// returns all three; every caller used the height and discarded the slope.
6// edge_intersect.glsl then rebuilds that gradient by central differences --
7// six more full evaluations per edge, each seven octaves of phacelle noise.
8// Measured: the octave loop takes a 32^3 grid from 491 us to 3795 us, and
9// edge_intersect is 91% of the mesh pipeline.
10//
11// Before any of that is replaced, the accumulated slope has to BE the gradient.
12// This compares erosion_node_eval_grad's .z/.w against central differences of
13// erosion_node_eval_full's .x over a field of points, in float64 steps so the
14// reference is not itself limited by the thing being tested.
15//
16// It can fail: a deliberately wrong gradient is checked too, and must be
17// rejected. A gate that only ever sees the correct answer proves nothing --
18// this session has already produced three harnesses that returned the same
19// number for every input.
20//
21// Build (no engine, no Vulkan):
22// g++ -O2 -std=c++17 -I ../proc-nodes/src/shared \
23// -o /tmp/erosion_gradient tests/test_erosion_gradient.cpp
24// Run: /tmp/erosion_gradient
25// ============================================================================
26#include <cstdio>
27#include <cmath>
28#include <algorithm>
29
30#include "erosion_cpp_shim.h" // brings in erosion_filter.inc as C++
31
32using dcerosion::vec2;
33using dcerosion::vec3;
34using dcerosion::vec4;
35
36// The map's own erosion settings (map_dc_diffusion.gd).
37static const float HS = 5.0f;
38static const float STR = 0.22f;
39static const float CELL = 0.7f;
40static const int OCT = 7;
41static const float GAIN = 0.5f;
42static const float FDIV = 4.8f;
43static const float ESCL = 0.15f;
44
45// A stand-in for the heightfield: smooth, with real slope, and NOT axis
46// aligned, so a gradient that is right in x and wrong in z cannot pass.
47static vec3 input_gn(float px, float pz) {
48 double h = 0.6 * std::sin(px * 3.1) * std::cos(pz * 2.3) + 0.25 * std::sin(pz * 5.7);
49 double dx = 0.6 * 3.1 * std::cos(px * 3.1) * std::cos(pz * 2.3);
50 double dz = -0.6 * 2.3 * std::sin(px * 3.1) * std::sin(pz * 2.3) + 0.25 * 5.7 * std::cos(pz * 5.7);
51 return vec3((float)h, (float)dx, (float)dz);
52}
53
54static float height_at(float px, float pz) {
55 vec2 r = dcerosion::erosion_node_eval_full(vec2(px, pz), input_gn(px, pz),
56 HS, STR, CELL, OCT, GAIN, FDIV, ESCL,
57 0.25f, 2.0f, 0.5f, 1.5f, 0.5f, vec2(-0.65f, 0.0f),
58 vec4(0.1f, 0.0f, 0.1f, 2.0f), vec4(1.25f, 1.25f, 2.8f, 1.5f), vec2(0.7f, 1.0f), 999.0f);
59 return r.x;
60}
61
62int main() {
63 printf("analytic slope from erosion_node_eval_grad vs central differences of\n");
64 printf("erosion_node_eval_full, %d octaves, strength %.2f\n\n", OCT, STR);
65 printf(" %-22s %12s %12s %12s\n", "point", "analytic dx", "numeric dx", "rel err");
66
67 const float eps = 1e-3f; // in p units; p = world / terrain_height
68 double worst_rel = 0.0;
69 double sum_rel = 0.0;
70 int n = 0;
71 int reported = 0;
72 for (int i = 0; i < 12; i++) {
73 for (int j = 0; j < 12; j++) {
74 float px = -0.5f + 0.0834f * (float)i;
75 float pz = -0.5f + 0.0834f * (float)j;
76 vec4 g = dcerosion::erosion_node_eval_grad(vec2(px, pz), input_gn(px, pz),
77 HS, STR, CELL, OCT, GAIN, FDIV, ESCL,
78 0.25f, 2.0f, 0.5f, 1.5f, 0.5f, vec2(-0.65f, 0.0f),
79 vec4(0.1f, 0.0f, 0.1f, 2.0f), vec4(1.25f, 1.25f, 2.8f, 1.5f), vec2(0.7f, 1.0f), 999.0f);
80 // The height must be unchanged from the vec2 form, or the two
81 // functions have already diverged and the slope is moot.
82 float h_ref = height_at(px, pz);
83 if (std::fabs(g.x - h_ref) > 1e-4f) {
84 printf(" HEIGHT MISMATCH at (%.3f, %.3f): %.6f vs %.6f\n", px, pz, g.x, h_ref);
85 return 2;
86 }
87 double num_dx = (height_at(px + eps, pz) - height_at(px - eps, pz)) / (2.0 * eps);
88 double num_dz = (height_at(px, pz + eps) - height_at(px, pz - eps)) / (2.0 * eps);
89 double scale = std::max(1.0, std::max(std::fabs(num_dx), std::fabs(num_dz)));
90 double rx = std::fabs(g.z - num_dx) / scale;
91 double rz = std::fabs(g.w - num_dz) / scale;
92 double rel = std::max(rx, rz);
93 sum_rel += rel; n++;
94 if (rel > worst_rel) worst_rel = rel;
95 if (reported < 6) {
96 printf(" (%+.3f, %+.3f) %12.5f %12.5f %12.5f\n", px, pz, g.z, num_dx, rel);
97 reported++;
98 }
99 }
100 }
101 printf("\n points %d mean rel err %.6f worst %.6f\n", n, sum_rel / n, worst_rel);
102
103 // THE NEGATIVE CONTROL. A gradient scaled by 1.5 must be REJECTED, or this
104 // gate cannot tell a right answer from a wrong one.
105 {
106 float px = 0.13f, pz = -0.21f;
107 vec4 g = dcerosion::erosion_node_eval_grad(vec2(px, pz), input_gn(px, pz),
108 HS, STR, CELL, OCT, GAIN, FDIV, ESCL,
109 0.25f, 2.0f, 0.5f, 1.5f, 0.5f, vec2(-0.65f, 0.0f),
110 vec4(0.1f, 0.0f, 0.1f, 2.0f), vec4(1.25f, 1.25f, 2.8f, 1.5f), vec2(0.7f, 1.0f), 999.0f);
111 double num_dx = (height_at(px + eps, pz) - height_at(px - eps, pz)) / (2.0 * eps);
112 double bad = g.z * 1.5;
113 double scale = std::max(1.0, std::fabs(num_dx));
114 double rel_bad = std::fabs(bad - num_dx) / scale;
115 printf(" negative control: a 1.5x gradient reads rel err %.6f -> %s\n",
116 rel_bad, rel_bad > 0.02 ? "REJECTED (gate is alive)" : "ACCEPTED (GATE IS BLIND)");
117 if (rel_bad <= 0.02) return 3;
118 }
119
120 // The filter is piecewise-smooth: sign(ph.y) in the straight-gully term is a
121 // step, so a central difference straddling a gully edge is wrong, not the
122 // analytic value. Judge on the MEAN, and report the worst rather than
123 // failing on it.
124 bool ok = (sum_rel / n) < 0.02;
125 printf("\n VERDICT: %s\n", ok
126 ? "the accumulated slope IS the gradient of the accumulated height."
127 : "they DISAGREE -- do not replace the central differences.");
128 return ok ? 0 : 1;
129}
tests/test_erosion_parity.cpp +12 -4
……@@ -235,8 +235,12 @@ static float gpu_wrapper(float x, float y, float z) {
235235 // inlined here, and the inlined version's numbers are what the "hoist is
236236 // faithful" check compared against.
237237 vec2 en = dcerosion::erosion_node_eval(
238 p, gn + terrain_offset(x, z), EN_HEIGHT_SCALE, EN_STRENGTH, EN_SLOPE_POWER,
239 EN_CELL_SCALE, EN_OCTAVES, EN_GAIN, 4.8f, EN_EROSION_SCALE);
238 p, gn + terrain_offset(x, z), EN_HEIGHT_SCALE, EN_STRENGTH,
239 EN_CELL_SCALE, EN_OCTAVES, EN_GAIN, 4.8f, EN_EROSION_SCALE,
240 // 999: this oracle compares the two WRAPPERS at one fixed grid, so there
241 // is no LOD to taper against. Tapering here would compare the taper with
242 // itself and hide a wrapper divergence behind it.
243 999.0f);
240244
241245 float d = en.x - y_mapped;
242246 d *= inv_yscale;
……@@ -269,8 +273,12 @@ static float cpu_wrapper(float x, float y, float z) {
269273 // erosion_filter's `scale` and skipped the remap, water gate and offset.
270274 // It now calls the same hoisted node wrapper the GPU path calls.
271275 vec2 en = dcerosion::erosion_node_eval(
272 p, gn + terrain_offset(x, z), EN_HEIGHT_SCALE, EN_STRENGTH, EN_SLOPE_POWER,
273 EN_CELL_SCALE, EN_OCTAVES, EN_GAIN, 4.8f, EN_EROSION_SCALE);
276 p, gn + terrain_offset(x, z), EN_HEIGHT_SCALE, EN_STRENGTH,
277 EN_CELL_SCALE, EN_OCTAVES, EN_GAIN, 4.8f, EN_EROSION_SCALE,
278 // 999: this oracle compares the two WRAPPERS at one fixed grid, so there
279 // is no LOD to taper against. Tapering here would compare the taper with
280 // itself and hide a wrapper divergence behind it.
281 999.0f);
274282
275283 float d = en.x - y_mapped;
276284 d *= inv_yscale;
tools/include/omp.h +368 -0
……@@ -0,0 +1,368 @@
1/* Copyright (C) 2005-2023 Free Software Foundation, Inc.
2 Contributed by Richard Henderson <rth@redhat.com>.
3
4 This file is part of the GNU Offloading and Multi Processing Library
5 (libgomp).
6
7 Libgomp is free software; you can redistribute it and/or modify it
8 under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 3, or (at your option)
10 any later version.
11
12 Libgomp is distributed in the hope that it will be useful, but WITHOUT ANY
13 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 FOR A PARTICULAR PURPOSE. See the GNU General Public License for
15 more details.
16
17 Under Section 7 of GPL version 3, you are granted additional
18 permissions described in the GCC Runtime Library Exception, version
19 3.1, as published by the Free Software Foundation.
20
21 You should have received a copy of the GNU General Public License and
22 a copy of the GCC Runtime Library Exception along with this program;
23 see the files COPYING3 and COPYING.RUNTIME respectively. If not, see
24 <http://www.gnu.org/licenses/>. */
25
26#ifndef _OMP_H
27#define _OMP_H 1
28
29#if defined(__GNUC__) && _OPENMP >= 201811
30# define __GOMP_DEPRECATED_5_0 __attribute__((__deprecated__))
31#else
32# define __GOMP_DEPRECATED_5_0
33#endif
34
35#if defined(__GNUC__) && _OPENMP >= 202011
36# define __GOMP_DEPRECATED_5_1 __attribute__((__deprecated__))
37#else
38# define __GOMP_DEPRECATED_5_1
39#endif
40
41#ifndef _LIBGOMP_OMP_LOCK_DEFINED
42#define _LIBGOMP_OMP_LOCK_DEFINED 1
43/* These two structures get edited by the libgomp build process to
44 reflect the shape of the two types. Their internals are private
45 to the library. */
46
47typedef struct
48{
49 unsigned char _x[4]
50 __attribute__((__aligned__(4)));
51} omp_lock_t;
52
53typedef struct
54{
55 /*
56 Derive OMP_NEST_LOCK_SIZE and OMP_NEST_LOCK_ALIGN, don't hard
57 code the values because the header is used for all multilibs.
58 OMP_NEST_LOCK_SIZE = 16
59 OMP_NEST_LOCK_ALIGN = 8
60 */
61#if defined(__linux__) && !(defined(__hppa__) || defined(__alpha__))
62 unsigned char _x[8 + sizeof (void *)]
63 __attribute__((__aligned__(sizeof (void *))));
64#else
65 unsigned char _x[16]
66 __attribute__((__aligned__(8)));
67#endif
68} omp_nest_lock_t;
69#endif
70
71typedef enum omp_sched_t
72{
73 omp_sched_static = 1,
74 omp_sched_dynamic = 2,
75 omp_sched_guided = 3,
76 omp_sched_auto = 4,
77 omp_sched_monotonic = 0x80000000U
78} omp_sched_t;
79
80typedef enum omp_proc_bind_t
81{
82 omp_proc_bind_false = 0,
83 omp_proc_bind_true = 1,
84 omp_proc_bind_primary = 2,
85 omp_proc_bind_master __GOMP_DEPRECATED_5_1
86 = omp_proc_bind_primary,
87 omp_proc_bind_close = 3,
88 omp_proc_bind_spread = 4
89} omp_proc_bind_t;
90
91typedef enum omp_sync_hint_t
92{
93 omp_sync_hint_none = 0,
94 omp_lock_hint_none __GOMP_DEPRECATED_5_0 = omp_sync_hint_none,
95 omp_sync_hint_uncontended = 1,
96 omp_lock_hint_uncontended __GOMP_DEPRECATED_5_0 = omp_sync_hint_uncontended,
97 omp_sync_hint_contended = 2,
98 omp_lock_hint_contended __GOMP_DEPRECATED_5_0 = omp_sync_hint_contended,
99 omp_sync_hint_nonspeculative = 4,
100 omp_lock_hint_nonspeculative __GOMP_DEPRECATED_5_0
101 = omp_sync_hint_nonspeculative,
102 omp_sync_hint_speculative = 8,
103 omp_lock_hint_speculative __GOMP_DEPRECATED_5_0 = omp_sync_hint_speculative
104} omp_sync_hint_t;
105
106typedef __GOMP_DEPRECATED_5_0 omp_sync_hint_t omp_lock_hint_t;
107
108typedef struct __attribute__((__aligned__ (sizeof (void *)))) omp_depend_t
109{
110 char __omp_depend_t__[2 * sizeof (void *)];
111} omp_depend_t;
112
113typedef enum omp_pause_resource_t
114{
115 omp_pause_soft = 1,
116 omp_pause_hard = 2
117} omp_pause_resource_t;
118
119typedef __UINTPTR_TYPE__ omp_uintptr_t;
120
121#if __cplusplus >= 201103L
122# define __GOMP_UINTPTR_T_ENUM : omp_uintptr_t
123#else
124# define __GOMP_UINTPTR_T_ENUM
125#endif
126
127typedef enum omp_memspace_handle_t __GOMP_UINTPTR_T_ENUM
128{
129 omp_default_mem_space = 0,
130 omp_large_cap_mem_space = 1,
131 omp_const_mem_space = 2,
132 omp_high_bw_mem_space = 3,
133 omp_low_lat_mem_space = 4,
134 __omp_memspace_handle_t_max__ = __UINTPTR_MAX__
135} omp_memspace_handle_t;
136
137typedef enum omp_allocator_handle_t __GOMP_UINTPTR_T_ENUM
138{
139 omp_null_allocator = 0,
140 omp_default_mem_alloc = 1,
141 omp_large_cap_mem_alloc = 2,
142 omp_const_mem_alloc = 3,
143 omp_high_bw_mem_alloc = 4,
144 omp_low_lat_mem_alloc = 5,
145 omp_cgroup_mem_alloc = 6,
146 omp_pteam_mem_alloc = 7,
147 omp_thread_mem_alloc = 8,
148 __omp_allocator_handle_t_max__ = __UINTPTR_MAX__
149} omp_allocator_handle_t;
150
151typedef enum omp_alloctrait_key_t
152{
153 omp_atk_sync_hint = 1,
154 omp_atk_alignment = 2,
155 omp_atk_access = 3,
156 omp_atk_pool_size = 4,
157 omp_atk_fallback = 5,
158 omp_atk_fb_data = 6,
159 omp_atk_pinned = 7,
160 omp_atk_partition = 8
161} omp_alloctrait_key_t;
162
163typedef enum omp_alloctrait_value_t
164{
165 omp_atv_default = (__UINTPTR_TYPE__) -1,
166 omp_atv_false = 0,
167 omp_atv_true = 1,
168 omp_atv_contended = 3,
169 omp_atv_uncontended = 4,
170 omp_atv_serialized = 5,
171 omp_atv_sequential __GOMP_DEPRECATED_5_1 = omp_atv_serialized,
172 omp_atv_private = 6,
173 omp_atv_all = 7,
174 omp_atv_thread = 8,
175 omp_atv_pteam = 9,
176 omp_atv_cgroup = 10,
177 omp_atv_default_mem_fb = 11,
178 omp_atv_null_fb = 12,
179 omp_atv_abort_fb = 13,
180 omp_atv_allocator_fb = 14,
181 omp_atv_environment = 15,
182 omp_atv_nearest = 16,
183 omp_atv_blocked = 17,
184 omp_atv_interleaved = 18
185} omp_alloctrait_value_t;
186
187typedef struct omp_alloctrait_t
188{
189 omp_alloctrait_key_t key;
190 omp_uintptr_t value;
191} omp_alloctrait_t;
192
193typedef enum omp_event_handle_t __GOMP_UINTPTR_T_ENUM
194{
195 __omp_event_handle_t_max__ = __UINTPTR_MAX__
196} omp_event_handle_t;
197
198enum
199{
200 omp_initial_device = -1,
201 omp_invalid_device = -4
202};
203
204#ifdef __cplusplus
205extern "C" {
206# define __GOMP_NOTHROW throw ()
207# define __GOMP_DEFAULT_NULL_ALLOCATOR = omp_null_allocator
208#else
209# define __GOMP_NOTHROW __attribute__((__nothrow__))
210# define __GOMP_DEFAULT_NULL_ALLOCATOR
211#endif
212
213extern void omp_set_num_threads (int) __GOMP_NOTHROW;
214extern int omp_get_num_threads (void) __GOMP_NOTHROW;
215extern int omp_get_max_threads (void) __GOMP_NOTHROW;
216extern int omp_get_thread_num (void) __GOMP_NOTHROW;
217extern int omp_get_num_procs (void) __GOMP_NOTHROW;
218
219extern int omp_in_parallel (void) __GOMP_NOTHROW;
220
221extern void omp_set_dynamic (int) __GOMP_NOTHROW;
222extern int omp_get_dynamic (void) __GOMP_NOTHROW;
223
224extern void omp_set_nested (int) __GOMP_NOTHROW __GOMP_DEPRECATED_5_0;
225extern int omp_get_nested (void) __GOMP_NOTHROW __GOMP_DEPRECATED_5_0;
226
227extern void omp_init_lock (omp_lock_t *) __GOMP_NOTHROW;
228extern void omp_init_lock_with_hint (omp_lock_t *, omp_sync_hint_t)
229 __GOMP_NOTHROW;
230extern void omp_destroy_lock (omp_lock_t *) __GOMP_NOTHROW;
231extern void omp_set_lock (omp_lock_t *) __GOMP_NOTHROW;
232extern void omp_unset_lock (omp_lock_t *) __GOMP_NOTHROW;
233extern int omp_test_lock (omp_lock_t *) __GOMP_NOTHROW;
234
235extern void omp_init_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW;
236extern void omp_init_nest_lock_with_hint (omp_nest_lock_t *, omp_sync_hint_t)
237 __GOMP_NOTHROW;
238extern void omp_destroy_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW;
239extern void omp_set_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW;
240extern void omp_unset_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW;
241extern int omp_test_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW;
242
243extern double omp_get_wtime (void) __GOMP_NOTHROW;
244extern double omp_get_wtick (void) __GOMP_NOTHROW;
245
246extern void omp_set_schedule (omp_sched_t, int) __GOMP_NOTHROW;
247extern void omp_get_schedule (omp_sched_t *, int *) __GOMP_NOTHROW;
248extern int omp_get_thread_limit (void) __GOMP_NOTHROW;
249extern void omp_set_max_active_levels (int) __GOMP_NOTHROW;
250extern int omp_get_max_active_levels (void) __GOMP_NOTHROW;
251extern int omp_get_supported_active_levels (void) __GOMP_NOTHROW;
252extern int omp_get_level (void) __GOMP_NOTHROW;
253extern int omp_get_ancestor_thread_num (int) __GOMP_NOTHROW;
254extern int omp_get_team_size (int) __GOMP_NOTHROW;
255extern int omp_get_active_level (void) __GOMP_NOTHROW;
256
257extern int omp_in_final (void) __GOMP_NOTHROW;
258extern int omp_in_explicit_task (void) __GOMP_NOTHROW;
259
260extern int omp_get_cancellation (void) __GOMP_NOTHROW;
261extern omp_proc_bind_t omp_get_proc_bind (void) __GOMP_NOTHROW;
262extern int omp_get_num_places (void) __GOMP_NOTHROW;
263extern int omp_get_place_num_procs (int) __GOMP_NOTHROW;
264extern void omp_get_place_proc_ids (int, int *) __GOMP_NOTHROW;
265extern int omp_get_place_num (void) __GOMP_NOTHROW;
266extern int omp_get_partition_num_places (void) __GOMP_NOTHROW;
267extern void omp_get_partition_place_nums (int *) __GOMP_NOTHROW;
268
269extern void omp_set_default_device (int) __GOMP_NOTHROW;
270extern int omp_get_default_device (void) __GOMP_NOTHROW;
271extern int omp_get_num_devices (void) __GOMP_NOTHROW;
272extern int omp_get_device_num (void) __GOMP_NOTHROW;
273extern int omp_get_num_teams (void) __GOMP_NOTHROW;
274extern int omp_get_team_num (void) __GOMP_NOTHROW;
275
276extern int omp_is_initial_device (void) __GOMP_NOTHROW;
277extern int omp_get_initial_device (void) __GOMP_NOTHROW;
278extern int omp_get_max_task_priority (void) __GOMP_NOTHROW;
279
280extern void omp_fulfill_event (omp_event_handle_t) __GOMP_NOTHROW;
281
282extern void omp_set_num_teams (int) __GOMP_NOTHROW;
283extern int omp_get_max_teams (void) __GOMP_NOTHROW;
284extern void omp_set_teams_thread_limit (int) __GOMP_NOTHROW;
285extern int omp_get_teams_thread_limit (void) __GOMP_NOTHROW;
286
287extern void *omp_target_alloc (__SIZE_TYPE__, int) __GOMP_NOTHROW;
288extern void omp_target_free (void *, int) __GOMP_NOTHROW;
289extern int omp_target_is_present (const void *, int) __GOMP_NOTHROW;
290extern int omp_target_memcpy (void *, const void *, __SIZE_TYPE__,
291 __SIZE_TYPE__, __SIZE_TYPE__, int, int)
292 __GOMP_NOTHROW;
293extern int omp_target_memcpy_async (void *, const void *, __SIZE_TYPE__,
294 __SIZE_TYPE__, __SIZE_TYPE__, int, int,
295 int, omp_depend_t *)
296 __GOMP_NOTHROW;
297extern int omp_target_memcpy_rect (void *, const void *, __SIZE_TYPE__, int,
298 const __SIZE_TYPE__ *,
299 const __SIZE_TYPE__ *,
300 const __SIZE_TYPE__ *,
301 const __SIZE_TYPE__ *,
302 const __SIZE_TYPE__ *, int, int)
303 __GOMP_NOTHROW;
304extern int omp_target_memcpy_rect_async (void *, const void *, __SIZE_TYPE__,
305 int, const __SIZE_TYPE__ *,
306 const __SIZE_TYPE__ *,
307 const __SIZE_TYPE__ *,
308 const __SIZE_TYPE__ *,
309 const __SIZE_TYPE__ *, int, int, int,
310 omp_depend_t *)
311 __GOMP_NOTHROW;
312extern int omp_target_associate_ptr (const void *, const void *, __SIZE_TYPE__,
313 __SIZE_TYPE__, int) __GOMP_NOTHROW;
314extern int omp_target_disassociate_ptr (const void *, int) __GOMP_NOTHROW;
315extern void *omp_get_mapped_ptr (const void *, int) __GOMP_NOTHROW;
316extern int omp_target_is_accessible (const void *, __SIZE_TYPE__, int)
317 __GOMP_NOTHROW;
318
319extern void omp_set_affinity_format (const char *) __GOMP_NOTHROW;
320extern __SIZE_TYPE__ omp_get_affinity_format (char *, __SIZE_TYPE__)
321 __GOMP_NOTHROW;
322extern void omp_display_affinity (const char *) __GOMP_NOTHROW;
323extern __SIZE_TYPE__ omp_capture_affinity (char *, __SIZE_TYPE__, const char *)
324 __GOMP_NOTHROW;
325
326extern int omp_pause_resource (omp_pause_resource_t, int) __GOMP_NOTHROW;
327extern int omp_pause_resource_all (omp_pause_resource_t) __GOMP_NOTHROW;
328
329extern omp_allocator_handle_t omp_init_allocator (omp_memspace_handle_t,
330 int,
331 const omp_alloctrait_t [])
332 __GOMP_NOTHROW;
333extern void omp_destroy_allocator (omp_allocator_handle_t) __GOMP_NOTHROW;
334extern void omp_set_default_allocator (omp_allocator_handle_t) __GOMP_NOTHROW;
335extern omp_allocator_handle_t omp_get_default_allocator (void) __GOMP_NOTHROW;
336extern void omp_free (void *,
337 omp_allocator_handle_t __GOMP_DEFAULT_NULL_ALLOCATOR)
338 __GOMP_NOTHROW;
339extern void *omp_alloc (__SIZE_TYPE__,
340 omp_allocator_handle_t __GOMP_DEFAULT_NULL_ALLOCATOR)
341 __GOMP_NOTHROW __attribute__((__malloc__, __malloc__,
342 __alloc_size__ (1)));
343extern void *omp_aligned_alloc (__SIZE_TYPE__, __SIZE_TYPE__,
344 omp_allocator_handle_t
345 __GOMP_DEFAULT_NULL_ALLOCATOR)
346 __GOMP_NOTHROW __attribute__((__malloc__, __malloc__,
347 __alloc_size__ (2), __alloc_align__ (1)));
348extern void *omp_calloc (__SIZE_TYPE__, __SIZE_TYPE__,
349 omp_allocator_handle_t __GOMP_DEFAULT_NULL_ALLOCATOR)
350 __GOMP_NOTHROW __attribute__((__malloc__, __malloc__,
351 __alloc_size__ (1, 2)));
352extern void *omp_aligned_calloc (__SIZE_TYPE__, __SIZE_TYPE__, __SIZE_TYPE__,
353 omp_allocator_handle_t
354 __GOMP_DEFAULT_NULL_ALLOCATOR)
355 __GOMP_NOTHROW __attribute__((__malloc__, __malloc__,
356 __alloc_size__ (2, 3), __alloc_align__ (1)));
357extern void *omp_realloc (void *, __SIZE_TYPE__,
358 omp_allocator_handle_t __GOMP_DEFAULT_NULL_ALLOCATOR,
359 omp_allocator_handle_t __GOMP_DEFAULT_NULL_ALLOCATOR)
360 __GOMP_NOTHROW __attribute__((__malloc__, __alloc_size__ (2)));
361
362extern void omp_display_env (int) __GOMP_NOTHROW;
363
364#ifdef __cplusplus
365}
366#endif
367
368#endif /* _OMP_H */
tools/oracles/density_precision.py +136 -0
……@@ -0,0 +1,136 @@
1#!/usr/bin/env python3
2"""Does the SHIPPING density expression have enough float32 precision to give
3dual contouring a usable surface normal?
4
5edge_intersect.glsl computes the normal that feeds the QEF by central
6differences on evaluate_density, with eps = voxel_size * 0.01. The shipping
7heightfield map's evaluate_density is
8
9 A = (H(x,z) + 10900) * 0.000238095 # the node's metres -> band mapping
10 B = (y + 10900) * 0.000238 # the wrapper's y_mapped
11 d = (A - B) * 4200 # back into metres
12
13A and B are both about 2.63 near the ground, and their difference is what
14carries the geometry. This script does that arithmetic in float32, exactly as
15the shader does, and asks how much of the horizontal gradient survives.
16
17It reports the angular error of the resulting normal. Run it against a
18synthetic ramp (a known answer: the normal of a plane of slope s) so the check
19can FAIL -- an oracle whose input has no known answer proves nothing.
20"""
21import numpy as np
22
23F = np.float32
24YS_NODE = F(0.000238095) # emitted by HeightfieldNode from 5.0/21000
25YS_WRAP = F(0.000238) # emitted by String::num(height_scale/y_range, 6)
26FLOOR = F(-10900.0)
27SCALE = F(4200.0)
28
29
30def density(H, y):
31 """evaluate_density, in float32, term for term as the shader writes it."""
32 H = F(H); y = F(y)
33 A = F(F(H - FLOOR) * YS_NODE)
34 B = F(F(y - FLOOR) * YS_WRAP)
35 return F(F(A - B) * SCALE)
36
37
38def normal_from_central_diff(h_of_x, x, y, z, eps):
39 """n as edge_intersect.glsl builds it: three central differences."""
40 nx = F(density(h_of_x(x + eps), y) - density(h_of_x(x - eps), y))
41 ny = F(density(h_of_x(x), F(y + eps)) - density(h_of_x(x), F(y - eps)))
42 return nx, ny
43
44
45def angle_err_deg(nx, ny, slope):
46 """Angle between the measured normal and the true one for height slope s.
47 True surface y = s*x, so the density gradient is (-s, 1) up to scale."""
48 if nx == 0.0 and ny == 0.0:
49 return float("nan")
50 # d is POSITIVE BELOW GROUND (solid), so d ~ (H(x) - y) and its gradient is
51 # (+s, -1). Comparing against (-s, +1) -- the outward surface normal -- put
52 # every row 180 degrees out and the float64 control caught it, which is the
53 # only reason this function is written down rather than assumed.
54 m = np.arctan2(float(nx), float(ny))
55 t = np.arctan2(slope, -1.0)
56 e = abs(np.degrees(m - t)) % 360.0
57 return min(e, 360.0 - e)
58
59
60def run(base_h, y_off, voxel, slopes, epsf):
61 rows = []
62 for s in slopes:
63 h = lambda xx, s=s: F(base_h + F(s) * F(xx - F(53760.0)))
64 x = F(53760.0)
65 # Stand on the surface the DENSITY defines, not on base_h: the two scale
66 # constants differ, so the zero of d is not at y = H.
67 y = F(base_h * (YS_NODE / YS_WRAP)
68 + FLOOR * (YS_NODE / YS_WRAP - F(1.0)) + F(y_off))
69 eps = F(voxel * epsf)
70 nx, ny = normal_from_central_diff(h, x, y, F(53760.0), eps)
71 rows.append((s, float(nx), float(ny), angle_err_deg(nx, ny, s)))
72 return rows
73
74
75def main():
76 slopes = [0.003, 0.01, 0.03, 0.1, 0.3, 1.0]
77 print("Shipping expression, base height 165 m, voxel 1 m")
78 print(" true surface slope | eps=0.01 voxel (SHIPPING) | eps=0.5 voxel")
79 print(" %-8s | %-14s %-9s | %-14s %-9s" %
80 ("slope", "n.x", "err deg", "n.x", "err deg"))
81 a = run(165.0, 0.0, 1.0, slopes, 0.01)
82 b = run(165.0, 0.0, 1.0, slopes, 0.5)
83 zero_ship = 0
84 for (s, nx1, ny1, e1), (_, nx2, ny2, e2) in zip(a, b):
85 if nx1 == 0.0:
86 zero_ship += 1
87 print(" %-8.3f | %-14.3e %-9.2f | %-14.3e %-9.2f" % (s, nx1, e1, nx2, e2))
88
89 print()
90 print("A NORMAL OF EXACTLY ZERO IN X IS A COLLAPSED NORMAL: the QEF is handed")
91 print("a plane that says the ground is level when it is not.")
92 print(" shipping eps: %d of %d slopes collapsed to exactly 0" % (zero_ship, len(slopes)))
93
94 # THE ORACLE MUST BE ABLE TO FAIL. Same arithmetic in float64, where no
95 # cancellation occurs: every slope must come back correct. If this row ever
96 # shows an error, the harness is wrong, not the shader.
97 print()
98 print("CONTROL -- the same central difference in float64 (no cancellation):")
99 ok = True
100 for s in slopes:
101 h = lambda xx, s=s: 165.0 + s * (xx - 53760.0)
102 y = 165.0 * (0.000238095 / 0.000238) + (-10900.0) * (0.000238095 / 0.000238 - 1.0)
103 eps = 0.01
104 d = lambda H, yy: ((H + 10900.0) * 0.000238095 - (yy + 10900.0) * 0.000238) * 4200.0
105 nx = d(h(53760.0 + eps), y) - d(h(53760.0 - eps), y)
106 ny = d(h(53760.0), y + eps) - d(h(53760.0), y - eps)
107 e = angle_err_deg(nx, ny, s)
108 # 0.05 deg, not 0.01. The residual at slope 1.0 is 0.0114 deg and it is
109 # REAL, not harness error: this control simulates the two MISMATCHED
110 # constants, whose 4.0e-4 ratio tilts the true surface by that much. A
111 # tolerance tight enough to flag it would be flagging the defect the
112 # rows above exist to show.
113 flag = "ok" if e < 0.05 else "FAIL"
114 if e >= 0.05:
115 ok = False
116 print(" slope %-6.3f n.x %-12.4e err %.4f deg %s" % (s, nx, e, flag))
117 print("CONTROL: %s" % ("PASS -- the arithmetic is right, the precision is not"
118 if ok else "FAIL -- fix the harness before believing the rows above"))
119
120 # And the second, independent defect the same expression carries.
121 print()
122 print("WHERE THE DENSITY PUTS THE GROUND, against the heightfield's own value:")
123 for label, ys_wrap in (("BEFORE String::num(.., 6) -> 0.000238", 0.000238),
124 ("AFTER String::num(.., 9) -> 0.000238095", 0.000238095)):
125 r = 0.000238095 / ys_wrap
126 errs = []
127 for h in (-5300.0, 0.0, 165.0, 5300.0):
128 y0 = h * r + (-10900.0) * (r - 1.0)
129 errs.append(y0 - h)
130 print(" %-45s %s" % (label,
131 " ".join("%+.3f" % e for e in errs)))
132 print(" %-45s %s" % ("", "at h = -5300 / 0 / 165 / 5300 m"))
133
134
135if __name__ == "__main__":
136 main()
tools/zig-cc +21 -0
……@@ -0,0 +1,21 @@
1#!/bin/sh
2export HOME="${HOME:-/home/fleeky}"
3# zig cc wrapper: cross-compile to an OLD glibc (2.28) so the .so loads on older
4# hosts (e.g. ashatron, glibc 2.35), with OpenMP via LLVM's libomp (clang's ABI;
5# modern clang dropped libgomp). The target needs libomp.so.5 at runtime
6# (apt install libomp5); the build records only the SONAME, not host glibc.
7args=""; openmp=0; compile=0
8for arg in "$@"; do
9 case "$arg" in
10 -march=x86-64|-m64|-fno-gnu-unique) ;; # filtered: zig rejects these
11 -fopenmp) args="$args -fopenmp"; openmp=1 ;; # enable OpenMP (libomp/__kmpc ABI)
12 -c) args="$args -c"; compile=1 ;;
13 *) args="$args $arg" ;;
14 esac
15done
16# On LINK steps with OpenMP, link libomp by FULL PATH so lld records DT_NEEDED
17# libomp.so.5 (clang's auto -lomp doesn't fire under zig's lld for -shared).
18omp=""
19[ "$openmp" = 1 ] && [ "$compile" = 0 ] && omp="/usr/lib/llvm-18/lib/libomp.so.5"
20# -isystem: host headers + clang's omp.h. -L: host libs + llvm-18 lib dir.
21exec zig cc -target x86_64-linux-gnu.2.28 -isystem /usr/include -isystem /usr/lib/llvm-18/lib/clang/18/include -L/usr/lib/x86_64-linux-gnu -L/usr/lib/llvm-18/lib $args $omp
tools/zig-cxx +21 -0
……@@ -0,0 +1,21 @@
1#!/bin/sh
2export HOME="${HOME:-/home/fleeky}"
3# zig c++ wrapper: cross-compile to an OLD glibc (2.28) so the .so loads on older
4# hosts (e.g. ashatron, glibc 2.35), with OpenMP via LLVM's libomp (clang's ABI;
5# modern clang dropped libgomp). The target needs libomp.so.5 at runtime
6# (apt install libomp5); the build records only the SONAME, not host glibc.
7args=""; openmp=0; compile=0
8for arg in "$@"; do
9 case "$arg" in
10 -march=x86-64|-m64|-fno-gnu-unique) ;; # filtered: zig rejects these
11 -fopenmp) args="$args -fopenmp"; openmp=1 ;; # enable OpenMP (libomp/__kmpc ABI)
12 -c) args="$args -c"; compile=1 ;;
13 *) args="$args $arg" ;;
14 esac
15done
16# On LINK steps with OpenMP, link libomp by FULL PATH so lld records DT_NEEDED
17# libomp.so.5 (clang's auto -lomp doesn't fire under zig's lld for -shared).
18omp=""
19[ "$openmp" = 1 ] && [ "$compile" = 0 ] && omp="/usr/lib/llvm-18/lib/libomp.so.5"
20# -isystem: host headers + clang's omp.h. -L: host libs + llvm-18 lib dir.
21exec zig c++ -target x86_64-linux-gnu.2.28 -isystem /usr/include -isystem /usr/lib/llvm-18/lib/clang/18/include -L/usr/lib/x86_64-linux-gnu -L/usr/lib/llvm-18/lib $args $omp