| … | … | @@ -0,0 +1,117 @@ |
| 1 | # Post-mortem — dc-terrain LOD won't converge near edits (2026-07-04) | |
| 2 | ||
| 3 | A multi-day, ~10-deploy debugging saga (#116). The terrain LOD would not converge | |
| 4 | around the play area of an edited room: coarse chunks (blue/yellow/orange over green | |
| 5 | LOD0) persisted near the player for 1–2 minutes or indefinitely, "randomly stuck in | |
| 6 | places," fast far from origin. It took a full 4-part audit + a one-line isolation test | |
| 7 | to find the real cause, after a long string of wrong guesses. This documents the real | |
| 8 | root cause, the four fixes, the dead-ends, and the process failures — so we never spend | |
| 9 | three 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 | |
| 14 | in 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 | ||
| 32 | 1. **`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()`. | |
| 40 | 2. **`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." | |
| 46 | 3. **`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. | |
| 52 | 4. **`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 | ||
| 82 | 1. **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. | |
| 84 | 2. **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. | |
| 87 | 3. **Instrument, don't guess** (`[STALEDIAG]`, `[LODDIAG]`) killed two wrong theories | |
| 88 | with data instead of argument. | |
| 89 | 4. **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 | ||
| 117 | Related: `postmortem-2026-07-03-edit-perf.md`, `reversible-baking-design.md`. |
| … | … | @@ -43,6 +43,23 @@ public: |
| 43 | 43 | // Optional: GPU compute shader source (GLSL). Return nullptr for CPU-only. |
| 44 | 44 | virtual bool has_gpu_shader() const { return false; } |
| 45 | 45 | 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; } | |
| 46 | 63 | |
| 47 | 64 | // Optional: bounding box hint for grid allocation. |
| 48 | 65 | // Writes axis-aligned min/max into min[3]/max[3]. Returns false if unbounded. |
| … | … | @@ -177,6 +177,17 @@ layout(set = 0, binding = 12, std430) readonly buffer ConvexPlaneBuf { |
| 177 | 177 | vec4 convex_planes[]; |
| 178 | 178 | }; |
| 179 | 179 | |
| 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. | |
| 187 | layout(set = 0, binding = 13, std430) readonly buffer CustomDataBuf { | |
| 188 | float custom_data[]; | |
| 189 | }; | |
| 190 | ||
| 180 | 191 | // --- Mesh SDF helper functions --- |
| 181 | 192 | |
| 182 | 193 | float aabb_point_dist_sq(vec3 p, vec3 bmin, vec3 bmax) { |
| … | … | @@ -135,6 +135,26 @@ layout(set = 0, binding = 9, std430) readonly buffer CountBuf { uint counts[]; } |
| 135 | 135 | // endpoints. A zero buffer is an exact no-op (d += 0). Binding 10 was free. |
| 136 | 136 | layout(set = 0, binding = 10, std430) readonly buffer BrickBuf { float brick_delta[]; }; |
| 137 | 137 | |
| 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. | |
| 155 | layout(set = 0, binding = 13, std430) readonly buffer CustomDataBuf { float custom_data[]; }; | |
| 156 | ||
| 157 | ||
| 138 | 158 | layout(push_constant) uniform PushConstants { |
| 139 | 159 | vec3 cell_origin; |
| 140 | 160 | float voxel_size; |
| … | … | @@ -345,7 +365,51 @@ void main() { |
| 345 | 365 | vec3 pos = mix(p0, p1, t); |
| 346 | 366 | |
| 347 | 367 | // ---- 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; | |
| 349 | 413 | vec3 n; |
| 350 | 414 | n.x = dc_sample_field(pos.x + eps, pos.y, pos.z) - dc_sample_field(pos.x - eps, pos.y, pos.z); |
| 351 | 415 | n.y = dc_sample_field(pos.x, pos.y + eps, pos.z) - dc_sample_field(pos.x, pos.y - eps, pos.z); |
| … | … | @@ -139,7 +139,22 @@ void main() { |
| 139 | 139 | float wz = edge_data[base + 2u]; |
| 140 | 140 | |
| 141 | 141 | // 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; | |
| 143 | 158 | vec3 n; |
| 144 | 159 | n.x = evaluate_density(wx + eps, wy, wz) - evaluate_density(wx - eps, wy, wz); |
| 145 | 160 | n.y = evaluate_density(wx, wy + eps, wz) - evaluate_density(wx, wy - eps, wz); |
| … | … | @@ -176,6 +176,25 @@ public: |
| 176 | 176 | RID shape = ps->concave_polygon_shape_create(); |
| 177 | 177 | Dictionary shape_data; |
| 178 | 178 | 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; | |
| 179 | 198 | ps->shape_set_data(shape, shape_data); |
| 180 | 199 | ps->body_add_shape(body, shape); |
| 181 | 200 | cnode->collision_shape = shape; |
| … | … | @@ -151,9 +151,13 @@ bool DCCompute::set_custom_shader(const char* glsl) { |
| 151 | 151 | return false; |
| 152 | 152 | } |
| 153 | 153 | |
| 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. | |
| 155 | 158 | if (pipeline_.is_valid()) rd_->free_rid(pipeline_); |
| 156 | 159 | if (shader_.is_valid()) rd_->free_rid(shader_); |
| 160 | uniform_set_ = RID(); | |
| 157 | 161 | |
| 158 | 162 | shader_ = rd_->shader_create_from_spirv(spirv); |
| 159 | 163 | if (!shader_.is_valid()) { |
| … | … | @@ -179,6 +183,9 @@ void DCCompute::cleanup() { |
| 179 | 183 | if (!rd_) return; |
| 180 | 184 | |
| 181 | 185 | _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; } | |
| 182 | 189 | |
| 183 | 190 | if (pipeline_.is_valid()) { |
| 184 | 191 | rd_->free_rid(pipeline_); |
| … | … | @@ -250,6 +257,15 @@ void DCCompute::_create_buffers(int stride) { |
| 250 | 257 | // (edge_intersect). Alloc-once per stride like the other buffers. |
| 251 | 258 | brick_buf_ = rd_->storage_buffer_create(buf_size, zero_data); |
| 252 | 259 | |
| 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 | ||
| 253 | 269 | // Mesh BVH buffers: start with small empties (resized on first mesh upload) |
| 254 | 270 | // Binding 8: BVH nodes (16-byte header + nodes) |
| 255 | 271 | PackedByteArray bvh_zero; |
| … | … | @@ -298,6 +314,13 @@ void DCCompute::_destroy_buffers() { |
| 298 | 314 | rd_->free_rid(uniform_set_); |
| 299 | 315 | uniform_set_ = RID(); |
| 300 | 316 | } |
| 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. | |
| 301 | 324 | if (density_buf_.is_valid()) { rd_->free_rid(density_buf_); density_buf_ = RID(); } |
| 302 | 325 | if (nx_buf_.is_valid()) { rd_->free_rid(nx_buf_); nx_buf_ = RID(); } |
| 303 | 326 | if (ny_buf_.is_valid()) { rd_->free_rid(ny_buf_); ny_buf_ = RID(); } |
| … | … | @@ -320,6 +343,36 @@ void DCCompute::_destroy_buffers() { |
| 320 | 343 | uniform_set_dirty_ = true; |
| 321 | 344 | } |
| 322 | 345 | |
| 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. | |
| 349 | void 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 | ||
| 323 | 376 | void DCCompute::_rebuild_uniform_set() { |
| 324 | 377 | // Skip rebuild if buffers haven't changed (optimization 5) |
| 325 | 378 | if (!uniform_set_dirty_ && uniform_set_.is_valid()) return; |
| … | … | @@ -413,6 +466,13 @@ void DCCompute::_rebuild_uniform_set() { |
| 413 | 466 | u11->set_binding(11); |
| 414 | 467 | u11->add_id(brick_buf_); |
| 415 | 468 | |
| 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 | ||
| 416 | 476 | // binding 12: convex-brush planes SSBO |
| 417 | 477 | Ref<RDUniform> u12; |
| 418 | 478 | u12.instantiate(); |
| … | … | @@ -434,8 +494,16 @@ void DCCompute::_rebuild_uniform_set() { |
| 434 | 494 | uniforms.push_back(u10); |
| 435 | 495 | uniforms.push_back(u11); |
| 436 | 496 | uniforms.push_back(u12); |
| 497 | uniforms.push_back(u13); | |
| 437 | 498 | |
| 438 | 499 | 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 | } | |
| 439 | 507 | uniform_set_dirty_ = false; |
| 440 | 508 | } |
| 441 | 509 |
| … | … | @@ -96,6 +96,10 @@ public: |
| 96 | 96 | RID perm_buf() const { return perm_buf_; } |
| 97 | 97 | RID edit_buf() const { return edit_buf_; } |
| 98 | 98 | 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); | |
| 99 | 103 | RID bvh_node_buf() const { return bvh_node_buf_; } |
| 100 | 104 | RID bvh_tri_buf() const { return bvh_tri_buf_; } |
| 101 | 105 | RID bvh_idx_buf() const { return bvh_idx_buf_; } |
| … | … | @@ -120,6 +124,8 @@ private: |
| 120 | 124 | RID perm_buf_; |
| 121 | 125 | RID edit_buf_; |
| 122 | 126 | RID brick_buf_; |
| 127 | RID custom_data_buf_; // baked diffusion custom data; binding 13 in BOTH shaders | |
| 128 | int custom_data_n_ = 0; | |
| 123 | 129 | RID bvh_node_buf_; |
| 124 | 130 | RID bvh_tri_buf_; |
| 125 | 131 | // Which mesh SDF the BVH buffers currently hold. upload_edits() runs PER |
| … | … | @@ -132,6 +132,7 @@ void DCGpuMeshPipeline::cleanup() { |
| 132 | 132 | _free_shader_pipeline(sp_voxel_classify_); |
| 133 | 133 | _free_shader_pipeline(sp_bake_planeset_); |
| 134 | 134 | _free_shader_pipeline(sp_qef_solve_); |
| 135 | _free_shader_pipeline(sp_vertex_polish_); | |
| 135 | 136 | _free_shader_pipeline(sp_quad_emit_); |
| 136 | 137 | |
| 137 | 138 | rd_ = nullptr; |
| … | … | @@ -201,6 +202,7 @@ bool DCGpuMeshPipeline::_compile_all_shaders() { |
| 201 | 202 | { "voxel_classify.glsl", &sp_voxel_classify_ }, |
| 202 | 203 | { "bake_planeset.glsl", &sp_bake_planeset_ }, |
| 203 | 204 | { "qef_solve.glsl", &sp_qef_solve_ }, |
| 205 | { "vertex_polish.glsl", &sp_vertex_polish_ }, | |
| 204 | 206 | { "quad_emit.glsl", &sp_quad_emit_ }, |
| 205 | 207 | }; |
| 206 | 208 | |
| … | … | @@ -458,7 +460,7 @@ void DCGpuMeshPipeline::_free_uniform_sets() { |
| 458 | 460 | fr(us_edge_classify_); fr(us_compact_edge_); fr(us_prepare_edge_); |
| 459 | 461 | fr(us_edge_intersect_); |
| 460 | 462 | 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_); | |
| 462 | 464 | for (int i = 0; i < 3; i++) { fr(us_scan_edge_[i]); fr(us_scan_voxel_[i]); } |
| 463 | 465 | uniform_sets_valid_ = false; // 2a: any teardown forces a rebuild next prepare |
| 464 | 466 | } |
| … | … | @@ -493,7 +495,7 @@ void DCGpuMeshPipeline::_build_uniform_sets(const DensityBuffers& bufs) { |
| 493 | 495 | {3, edge_data_buf_}, |
| 494 | 496 | {4, bufs.params}, {5, bufs.voronoi}, {6, bufs.perm}, {7, bufs.edit}, |
| 495 | 497 | {8, active_edges_buf_}, {9, counts_buf_}, {10, bufs.brick}, |
| 496 | {11, bufs.convex_planes} | |
| 498 | {11, bufs.convex_planes}, {13, bufs.custom_data} | |
| 497 | 499 | }, true); |
| 498 | 500 | |
| 499 | 501 | // Stage 7: voxel_classify |
| … | … | @@ -524,6 +526,19 @@ void DCGpuMeshPipeline::_build_uniform_sets(const DensityBuffers& bufs) { |
| 524 | 526 | {9, bufs.edit}, {10, bufs.convex_planes}, {11, planeset_buf_} |
| 525 | 527 | }); |
| 526 | 528 | |
| 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 | ||
| 527 | 542 | // Plane-set BAKE (runs per cell before qef_solve): edits + convex planes in, |
| 528 | 543 | // the per-cell plane-set brick out. |
| 529 | 544 | us_bake_planeset_ = _make_uniform_set(sp_bake_planeset_.shader, { |
| … | … | @@ -745,6 +760,13 @@ bool DCGpuMeshPipeline::dispatch_mesh_stages(int64_t cl, |
| 745 | 760 | rd_->compute_list_dispatch_indirect(cl, dispatch_args_buf_, 12); |
| 746 | 761 | rd_->compute_list_add_barrier(cl); |
| 747 | 762 | |
| 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 | ||
| 748 | 770 | // Stage 11: Quad Emit |
| 749 | 771 | rd_->compute_list_bind_compute_pipeline(cl, sp_quad_emit_.pipeline); |
| 750 | 772 | rd_->compute_list_bind_uniform_set(cl, us_quad_emit_, 0); |
| … | … | @@ -34,6 +34,7 @@ struct DensityBuffers { |
| 34 | 34 | RID brick; // edge_intersect binding 10: settled-edit SDF delta brick |
| 35 | 35 | RID convex_planes; // edge_intersect binding 11: convex-brush half-space planes |
| 36 | 36 | 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 | |
| 37 | 38 | }; |
| 38 | 39 | |
| 39 | 40 | class DCGpuMeshPipeline { |
| … | … | @@ -92,6 +93,7 @@ private: |
| 92 | 93 | ShaderPipeline sp_compact_index_; |
| 93 | 94 | ShaderPipeline sp_prepare_dispatch_; |
| 94 | 95 | ShaderPipeline sp_edge_intersect_; // recompiled when SDF changes |
| 96 | ShaderPipeline sp_vertex_polish_; // stage 9: clamped gradient descent | |
| 95 | 97 | ShaderPipeline sp_voxel_classify_; |
| 96 | 98 | ShaderPipeline sp_qef_solve_; |
| 97 | 99 | ShaderPipeline sp_quad_emit_; |
| … | … | @@ -120,7 +122,7 @@ private: |
| 120 | 122 | RID us_edge_classify_, us_compact_edge_, us_prepare_edge_; |
| 121 | 123 | RID us_edge_intersect_; |
| 122 | 124 | 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_; | |
| 124 | 126 | RID us_bake_planeset_; |
| 125 | 127 | // Scan uniform sets (edge scan: 3, voxel scan: 3) |
| 126 | 128 | RID us_scan_edge_[3], us_scan_voxel_[3]; |
| … | … | @@ -38,6 +38,8 @@ struct GpuFillRequest { |
| 38 | 38 | DensityGrid* grid; |
| 39 | 39 | const DCDensity* density; |
| 40 | 40 | 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; | |
| 41 | 43 | const std::vector<TerrainEdit>* edits = nullptr; // terrain edits for GPU evaluation |
| 42 | 44 | bool wants_gpu_mesh = false; // request GPU mesh generation (skip CPU DC) |
| 43 | 45 | DCMeshData* mesh_output = nullptr; // where to write GPU-generated mesh |
| … | … | @@ -98,7 +100,8 @@ public: |
| 98 | 100 | const std::vector<TerrainEdit>* edits = nullptr, |
| 99 | 101 | DCMeshData* mesh_output = nullptr, |
| 100 | 102 | 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) { | |
| 102 | 105 | if (!running_.load() || !gpu_available_.load()) { |
| 103 | 106 | return false; |
| 104 | 107 | } |
| … | … | @@ -112,6 +115,8 @@ public: |
| 112 | 115 | req.grid = &grid; |
| 113 | 116 | req.density = &density; |
| 114 | 117 | req.custom_glsl = custom_glsl; |
| 118 | req.custom_data = custom_data; | |
| 119 | req.custom_data_n = custom_data_n; | |
| 115 | 120 | req.edits = edits; |
| 116 | 121 | if (mesh_output && cell_origin) { |
| 117 | 122 | req.wants_gpu_mesh = true; |
| … | … | @@ -149,6 +154,7 @@ private: |
| 149 | 154 | DCGpuMeshPipeline mesh_pipeline_; |
| 150 | 155 | bool mesh_pipeline_available_ = false; |
| 151 | 156 | 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 | |
| 152 | 158 | |
| 153 | 159 | void _run() { |
| 154 | 160 | // Create the RenderingDevice on THIS thread (thread affinity). |
| … | … | @@ -261,6 +267,18 @@ private: |
| 261 | 267 | return; |
| 262 | 268 | } |
| 263 | 269 | 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; | |
| 264 | 282 | } |
| 265 | 283 | |
| 266 | 284 | // Process each request INDIVIDUALLY with ITS OWN culled edit list. |
| … | … | @@ -361,6 +379,7 @@ private: |
| 361 | 379 | bufs.bvh_nodes = compute_.bvh_node_buf(); |
| 362 | 380 | bufs.bvh_tris = compute_.bvh_tri_buf(); |
| 363 | 381 | bufs.bvh_indices = compute_.bvh_idx_buf(); |
| 382 | bufs.custom_data = compute_.custom_data_buf(); | |
| 364 | 383 | mesh_pipeline_.prepare_mesh(grid_dims, bufs); |
| 365 | 384 | |
| 366 | 385 | // Single compute list: density + mesh pipeline |
| … | … | @@ -86,6 +86,12 @@ struct OctreeClipmapNode { |
| 86 | 86 | State state = EMPTY; |
| 87 | 87 | |
| 88 | 88 | 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 }; | |
| 89 | 95 | RID collision_body; |
| 90 | 96 | RID collision_shape; |
| 91 | 97 | RID normal_map_texture; |
| … | … | @@ -721,10 +721,44 @@ void DCTerrain::_process(double delta) { |
| 721 | 721 | std::vector<OctreeClipmapNode*> stk; |
| 722 | 722 | stk.push_back(octree_clipmap_.get_root()); |
| 723 | 723 | int leaked = 0; |
| 724 | int col_leaked = 0; | |
| 724 | 725 | while (!stk.empty()) { |
| 725 | 726 | OctreeClipmapNode* n = stk.back(); stk.pop_back(); |
| 726 | 727 | if (!n) continue; |
| 727 | 728 | 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 | } | |
| 728 | 762 | if (n->mesh_instance.is_valid() || n->seam_mesh_instance.is_valid()) { |
| 729 | 763 | int bad = -1, bstate = -1; float bfade = -1.0f; |
| 730 | 764 | for (int i = 0; i < 8; i++) { |
| … | … | @@ -749,6 +783,10 @@ void DCTerrain::_process(double delta) { |
| 749 | 783 | UtilityFunctions::print("[STALEDIAG] ", leaked, |
| 750 | 784 | " internal nodes still drawing a coarse mesh/seam over their children"); |
| 751 | 785 | } |
| 786 | if (col_leaked > 0) { | |
| 787 | UtilityFunctions::print("[STALECOL] ", col_leaked, | |
| 788 | " internal nodes still COLLIDING with a coarse surface over their children"); | |
| 789 | } | |
| 752 | 790 | } |
| 753 | 791 | |
| 754 | 792 | // NOTE: Stale parent shadow disable was removed — disabling shadows on |
| … | … | @@ -2076,6 +2114,9 @@ void DCTerrain::_upload_cell(CellResult& result) { |
| 2076 | 2114 | Transform3D chunk_xform; |
| 2077 | 2115 | chunk_xform.origin = Vector3(chunk_origin_x, chunk_origin_y, chunk_origin_z); |
| 2078 | 2116 | 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; | |
| 2079 | 2120 | |
| 2080 | 2121 | // AABB in local space (relative to chunk origin) |
| 2081 | 2122 | 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 { |
| 2841 | 2882 | // --------------------------------------------------------------------------- |
| 2842 | 2883 | // Density sampling |
| 2843 | 2884 | // --------------------------------------------------------------------------- |
| 2885 | PackedVector3Array DCTerrain::debug_collision_faces(const Vector3 ¢er, 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. | |
| 2949 | PackedVector3Array DCTerrain::debug_visual_faces(const Vector3 ¢er, 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 | ||
| 2990 | String DCTerrain::build_stamp() const { | |
| 2991 | return String("dc-terrain built ") + __DATE__ + " " + __TIME__; | |
| 2992 | } | |
| 2993 | ||
| 2994 | Dictionary 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 | ||
| 3019 | PackedFloat32Array 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 | ||
| 2844 | 3042 | float DCTerrain::sample_density(const Vector3 &position) const { |
| 2845 | 3043 | float d = generator_ptr_->evaluate(position.x, position.y, position.z, 0.0f); |
| 2846 | 3044 | if (config_.edits && !config_.edits->empty()) { |
| … | … | @@ -4051,6 +4249,11 @@ void DCTerrain::_bind_methods() { |
| 4051 | 4249 | ClassDB::bind_method(D_METHOD("rebuild"), &DCTerrain::rebuild); |
| 4052 | 4250 | ClassDB::bind_method(D_METHOD("warmup_shaders"), &DCTerrain::warmup_shaders); |
| 4053 | 4251 | 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); | |
| 4054 | 4257 | |
| 4055 | 4258 | // SDF collision API |
| 4056 | 4259 | ClassDB::bind_method(D_METHOD("get_distance", "position"), &DCTerrain::get_distance); |
| … | … | @@ -16,6 +16,7 @@ |
| 16 | 16 | #include <godot_cpp/classes/physics_server3d.hpp> |
| 17 | 17 | #include <godot_cpp/variant/color.hpp> |
| 18 | 18 | #include <godot_cpp/variant/rid.hpp> |
| 19 | #include <godot_cpp/variant/dictionary.hpp> | |
| 19 | 20 | |
| 20 | 21 | #include "dc_density.h" |
| 21 | 22 | #include "dc_density_grid.h" |
| … | … | @@ -180,6 +181,50 @@ public: |
| 180 | 181 | // -- Density sampling (for spawn placement, etc.) -- |
| 181 | 182 | float sample_density(const Vector3 &position) const; |
| 182 | 183 | |
| 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 ¢er, float radius) const; | |
| 211 | ||
| 212 | // The DRAWN triangles in world space, for comparison against the above. | |
| 213 | PackedVector3Array debug_visual_faces(const Vector3 ¢er, 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 | ||
| 183 | 228 | // -- SDF collision API (direct density field queries) -- |
| 184 | 229 | float _eval_with_edits(float x, float y, float z) const; |
| 185 | 230 | // Returns signed distance to terrain surface (positive = inside solid, negative = in air). |
| … | … | @@ -15,6 +15,8 @@ |
| 15 | 15 | // dependency on proc-nodes' classes -- the ClassDB reflection in the header |
| 16 | 16 | // comment still stands. It is a shared math header and nothing more. |
| 17 | 17 | #include "erosion_cpp_shim.h" |
| 18 | #include "erosion_wrapper_glsl.h" | |
| 19 | #include <godot_cpp/classes/file_access.hpp> | |
| 18 | 20 | |
| 19 | 21 | namespace godot { |
| 20 | 22 | |
| … | … | @@ -41,70 +43,26 @@ void DCTerrainGeneratorProcNodes::_bind_methods() { |
| 41 | 43 | ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "ceiling_y", PROPERTY_HINT_RANGE, "-1000.0,1000.0,0.5"), |
| 42 | 44 | "set_ceiling_y", "get_ceiling_y"); |
| 43 | 45 | |
| 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 | ||
| 108 | 66 | |
| 109 | 67 | ClassDB::bind_method(D_METHOD("set_height_space_sqrt", "value"), &DCTerrainGeneratorProcNodes::set_height_space_sqrt); |
| 110 | 68 | 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) { |
| 133 | 91 | emit_changed(); |
| 134 | 92 | } |
| 135 | 93 | |
| 136 | void 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 | } | |
| 142 | void 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 | } | |
| 148 | void 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 | } | |
| 154 | 94 | void DCTerrainGeneratorProcNodes::set_height_space_sqrt(bool p_val) { |
| 155 | 95 | if (height_space_sqrt_ == p_val) return; |
| 156 | 96 | height_space_sqrt_ = p_val; |
| 157 | 97 | _recompile_glsl(); |
| 158 | 98 | emit_changed(); |
| 159 | 99 | } |
| 160 | void 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 | } | |
| 166 | void 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 | } | |
| 172 | void 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 | } | |
| 178 | void 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 | } | |
| 184 | void 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 | } | |
| 190 | void 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 | } | |
| 196 | void 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 | } | |
| 202 | void 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. | |
| 104 | bool 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; | |
| 207 | 140 | } |
| 208 | void DCTerrainGeneratorProcNodes::set_mountain_height(float p_val) { | |
| 209 | if (mountain_height_ == p_val) return; | |
| 210 | mountain_height_ = p_val; | |
| 141 | ||
| 142 | ||
| 143 | void DCTerrainGeneratorProcNodes::set_heightfield_only(bool p_val) { | |
| 144 | if (heightfield_only_ == p_val) return; | |
| 145 | heightfield_only_ = p_val; | |
| 211 | 146 | _recompile_glsl(); |
| 212 | 147 | emit_changed(); |
| 213 | 148 | } |
| 214 | void DCTerrainGeneratorProcNodes::set_mountain_octaves(int p_val) { | |
| 215 | if (mountain_octaves_ == p_val) return; | |
| 216 | mountain_octaves_ = p_val; | |
| 149 | ||
| 150 | void 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); | |
| 217 | 154 | _recompile_glsl(); |
| 218 | 155 | emit_changed(); |
| 219 | 156 | } |
| 220 | 157 | |
| 158 | ||
| 221 | 159 | void DCTerrainGeneratorProcNodes::set_graph(const Ref<Resource> &p_graph) { |
| 222 | 160 | if (p_graph.is_valid()) { |
| 223 | 161 | // Validate that the resource is a ProcNodeGraph (from proc-nodes GDExtension) |
| … | … | @@ -231,6 +169,7 @@ void DCTerrainGeneratorProcNodes::set_graph(const Ref<Resource> &p_graph) { |
| 231 | 169 | } |
| 232 | 170 | graph_ = p_graph; |
| 233 | 171 | _discover_params(); |
| 172 | _collect_node_data(); | |
| 234 | 173 | _recompile_glsl(); // Eager — must happen on main thread before worker sees this generator |
| 235 | 174 | notify_property_list_changed(); |
| 236 | 175 | emit_changed(); |
| … | … | @@ -238,6 +177,59 @@ void DCTerrainGeneratorProcNodes::set_graph(const Ref<Resource> &p_graph) { |
| 238 | 177 | |
| 239 | 178 | // ── Parameter Discovery ─────────────────────────────────────────────────────── |
| 240 | 179 | |
| 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. | |
| 189 | void 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 | ||
| 241 | 233 | void DCTerrainGeneratorProcNodes::_discover_params() { |
| 242 | 234 | discovered_params_.clear(); |
| 243 | 235 | if (graph_.is_null()) return; |
| … | … | @@ -383,27 +375,172 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() { |
| 383 | 375 | // pn_max_octaves must be declared BEFORE proc_density (which reads it). |
| 384 | 376 | String th_str = String::num(terrain_height_, 4); |
| 385 | 377 | 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 | ||
| 386 | 417 | String ceil_str = String::num(ceiling_y_, 4); |
| 387 | 418 | // y_scale maps [floor_y, ceiling_y] to proc_density's native [0, HeightScale] |
| 388 | 419 | float y_range = ceiling_y_ - floor_y_; |
| 389 | 420 | 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. | |
| 407 | 544 | |
| 408 | 545 | String adapted = |
| 409 | 546 | "// Octave limit — fixed high value for LOD-consistent surface position.\n" |
| … | … | @@ -424,61 +561,56 @@ void DCTerrainGeneratorProcNodes::_recompile_glsl() { |
| 424 | 561 | // right up to the shader compile. pn_max_octaves is different: nothing |
| 425 | 562 | // else declares it, so it stays here. |
| 426 | 563 | + proc_glsl + "\n" |
| 564 | + hblk_glsl + | |
| 427 | 565 | "float evaluate_density(float x, float y, float z) {\n" |
| 428 | 566 | " float inv_scale = 1.0 / " + th_str + ";\n" |
| 429 | 567 | " 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. | |
| 476 | 606 | + (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. | |
| 477 | 611 | ? 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") | |
| 482 | 614 | + "" |
| 483 | 615 | " float d = proc_density(vec3(x * inv_scale, y_mapped, z * inv_scale));\n" |
| 484 | 616 | " d *= " + inv_yscale_str + ";\n" |
| … | … | @@ -512,14 +644,41 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate( |
| 512 | 644 | float terrain_height = owner_->terrain_height_; |
| 513 | 645 | float floor_y = owner_->floor_y_; |
| 514 | 646 | 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_; | |
| 518 | 647 | |
| 519 | 648 | // 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; | |
| 521 | 655 | float en_height_scale = 5.0f, en_erosion_str = 0.16f, en_slope_power = 0.6f; |
| 522 | 656 | 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; | |
| 523 | 682 | for (int i = 0; i < owner_->discovered_params_.size(); i++) { |
| 524 | 683 | const auto &gp = owner_->discovered_params_[i]; |
| 525 | 684 | if (gp.node_title == "Gradient Noise" && gp.socket_name == "Scale") |
| … | … | @@ -540,6 +699,69 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate( |
| 540 | 699 | en_octaves = (int)gp.default_value; |
| 541 | 700 | if (gp.node_title == "Erosion Noise" && gp.socket_name == "Gain") |
| 542 | 701 | 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; | |
| 543 | 765 | } |
| 544 | 766 | |
| 545 | 767 | float inv_scale = 1.0f / terrain_height; |
| … | … | @@ -557,49 +779,192 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate( |
| 557 | 779 | // remaining piece. |
| 558 | 780 | using dcerosion::vec2; |
| 559 | 781 | 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); | |
| 579 | 879 | } |
| 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. | |
| 586 | 951 | 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; | |
| 592 | 957 | } |
| 593 | 958 | |
| 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. | |
| 596 | 961 | float y_mapped; |
| 597 | 962 | if (owner_->height_space_sqrt_) { |
| 598 | 963 | float un = (y - floor_y) / y_range; |
| 599 | 964 | 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; | |
| 601 | 966 | } else { |
| 602 | y_mapped = (y - floor_y) * y_scale - det_offset * en_height_scale; | |
| 967 | y_mapped = (y - floor_y) * y_scale; | |
| 603 | 968 | } |
| 604 | 969 | |
| 605 | 970 | // GradientNoiseNode. Lacunarity is hardcoded 2.0 in the node |
| … | … | @@ -615,13 +980,43 @@ float DCTerrainGeneratorProcNodes::ProcNodesBridge::evaluate( |
| 615 | 980 | // wrapper, shared with the GLSL, so there is nothing left to get wrong. |
| 616 | 981 | // It returns vec2(height * height_scale, erosion); .y is the ridge/crease |
| 617 | 982 | // 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( | |
| 619 | 1007 | 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; | |
| 625 | 1020 | d *= inv_y_scale; |
| 626 | 1021 | |
| 627 | 1022 | // Clip: solid below floor_y, air above ceiling_y |
| … | … | @@ -642,4 +1037,24 @@ const char *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_shader_source() co |
| 642 | 1037 | return owner_->cached_glsl_.empty() ? nullptr : owner_->cached_glsl_.c_str(); |
| 643 | 1038 | } |
| 644 | 1039 | |
| 1040 | const float *DCTerrainGeneratorProcNodes::ProcNodesBridge::gpu_custom_data() const { | |
| 1041 | return owner_->hblk_.empty() ? nullptr : owner_->hblk_.data(); | |
| 1042 | } | |
| 1043 | ||
| 1044 | int 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 | ||
| 645 | 1060 | } // namespace godot |
| … | … | @@ -33,9 +33,6 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator { |
| 33 | 33 | |
| 34 | 34 | // Mountain layer: low-frequency noise that creates large-scale terrain shape. |
| 35 | 35 | // 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 | |
| 39 | 36 | |
| 40 | 37 | // RELIEF FIELD — makes mountain amplitude a FUNCTION OF PLACE. |
| 41 | 38 | // |
| … | … | @@ -47,8 +44,6 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator { |
| 47 | 44 | // |
| 48 | 45 | // relief_scale_ = 0 DISABLES it (amplitude is uniform, exactly as before), so |
| 49 | 46 | // 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 | |
| 52 | 47 | |
| 53 | 48 | // PERSISTENCE, which was hardcoded 0.5 and is why the terrain is smooth |
| 54 | 49 | // underfoot. Octave k has amplitude gain^k of an amplitude set by |
| … | … | @@ -57,7 +52,80 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator { |
| 57 | 52 | // raising mountain_octaves 4 -> 12 moved local roughness 0.72 -> 0.98 m per |
| 58 | 53 | // 2 m stride; it cannot fix this, because it only adds ever-tinier ripples. |
| 59 | 54 | // 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. | |
| 61 | 129 | |
| 62 | 130 | // DETAIL LAYER — high frequency with its OWN amplitude, in METRES. |
| 63 | 131 | // |
| … | … | @@ -71,30 +139,23 @@ class DCTerrainGeneratorProcNodes : public DCTerrainGenerator { |
| 71 | 139 | // mountains so an Everest region and a Vermont region can share a map — but |
| 72 | 140 | // a Vermont hillside still has rock underfoot. Scaling detail by relief too |
| 73 | 141 | // 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; | |
| 78 | 142 | |
| 79 | 143 | // LACUNARITY, hardcoded 2.0 until now — and 2.0 is exactly why the terrain |
| 80 | 144 | // reads as self-similar. With frequency doubling and gain constant, every |
| 81 | 145 | // octave is a scaled copy of the last and they all land on harmonics of each |
| 82 | 146 | // other. A non-integer ratio (~1.87) stops them lining up. Applied to BOTH |
| 83 | 147 | // the mountain and detail fBm, since the repetition shows in both. |
| 84 | float noise_lacunarity_ = 2.0f; | |
| 85 | 148 | // Gully band position: coarsest gully wavelength is |
| 86 | 149 | // terrain_height * erosion_scale * cell_scale, in world metres. Was baked at |
| 87 | 150 | // 0.15 inside erosion_node_eval, which locked the gully band to a fixed |
| 88 | 151 | // fraction of the terrain scale -- so raising terrain_height for view |
| 89 | 152 | // distance dragged the gullies up with it. |
| 90 | float erosion_scale_ = 0.15f; | |
| 91 | 153 | |
| 92 | 154 | // Multifractal blend for the MOUNTAIN layer. 0 = plain fBm (bit-identical to |
| 93 | 155 | // fractal_noise_2d, verified), 1 = each octave's amplitude weighted by the |
| 94 | 156 | // elevation accumulated so far, giving rugged peaks above smooth valleys. |
| 95 | 157 | // Measured at 1.0: roughness on high ground is 5.8x that on low ground, |
| 96 | 158 | // against 0.98 -- i.e. homogeneous -- for fBm. |
| 97 | float mountain_variance_ = 0.0f; | |
| 98 | 159 | |
| 99 | 160 | // SIGNED-SQRT HEIGHT SPACE — opt in, default off. |
| 100 | 161 | // |
| … | … | @@ -141,6 +202,7 @@ private: |
| 141 | 202 | }; |
| 142 | 203 | Vector<GraphParam> discovered_params_; |
| 143 | 204 | void _discover_params(); |
| 205 | void _collect_node_data(); | |
| 144 | 206 | |
| 145 | 207 | // Bridge: implements DCGenerator interface |
| 146 | 208 | class ProcNodesBridge : public DCGenerator { |
| … | … | @@ -152,6 +214,8 @@ private: |
| 152 | 214 | float evaluate(float x, float y, float z, float lod_voxel_size) override; |
| 153 | 215 | bool has_gpu_shader() const override; |
| 154 | 216 | const char *gpu_shader_source() const override; |
| 217 | const float *gpu_custom_data() const override; | |
| 218 | int gpu_custom_data_count() const override; | |
| 155 | 219 | }; |
| 156 | 220 | ProcNodesBridge bridge_{ this }; |
| 157 | 221 | |
| … | … | @@ -179,32 +243,10 @@ public: |
| 179 | 243 | void set_ceiling_y(float p_val); |
| 180 | 244 | float get_ceiling_y() const { return ceiling_y_; } |
| 181 | 245 | |
| 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_; } | |
| 208 | 250 | void set_height_space_sqrt(bool p_val); |
| 209 | 251 | bool get_height_space_sqrt() const { return height_space_sqrt_; } |
| 210 | 252 |
| … | … | @@ -234,15 +234,20 @@ void DCWorkerCPU::_process_cell(const CellRequest &request, int64_t picked_up_us |
| 234 | 234 | DCMeshData gpu_mesh; |
| 235 | 235 | if (gpu_thread_ && request.density && request.gpu_eligible) { |
| 236 | 236 | const char* custom_glsl = nullptr; |
| 237 | const float* custom_data = nullptr; | |
| 238 | int custom_data_n = 0; | |
| 237 | 239 | if (request.generator && request.generator->has_gpu_shader()) { |
| 238 | 240 | 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(); | |
| 239 | 243 | } |
| 240 | 244 | const std::vector<TerrainEdit>* gpu_edit_ptr = |
| 241 | 245 | (request.edits && !request.edits->empty()) ? request.edits.get() : nullptr; |
| 242 | 246 | // GPU path handles edits internally via compute shader |
| 243 | 247 | gpu_filled = gpu_thread_->submit(grid, *request.density, custom_glsl, gpu_edit_ptr, |
| 244 | 248 | &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); | |
| 246 | 251 | if (!gpu_mesh.indices.empty()) { |
| 247 | 252 | gpu_meshed = true; |
| 248 | 253 | } |
| … | … | @@ -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" | |
| 6 | int 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 | } |
| … | … | @@ -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 | |
| 14 | set -e | |
| 15 | BODY="${1:-/tmp/hblk_body.glsl}" | |
| 16 | DIR=$(cd "$(dirname "$0")/.." && pwd) | |
| 17 | ||
| 18 | [ -f "$BODY" ] || { echo "missing $BODY -- run the godot emission gate first"; exit 2; } | |
| 19 | ||
| 20 | g++ -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 | ||
| 24 | cat /tmp/density_header.glsl > /tmp/hblk_full.comp | |
| 25 | printf '\n' >> /tmp/hblk_full.comp | |
| 26 | cat "$BODY" >> /tmp/hblk_full.comp | |
| 27 | printf '\n' >> /tmp/hblk_full.comp | |
| 28 | cat /tmp/density_main.glsl >> /tmp/hblk_full.comp | |
| 29 | ||
| 30 | echo "assembled $(wc -c < /tmp/hblk_full.comp) bytes" | |
| 31 | if 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)" | |
| 33 | else | |
| 34 | echo " COMPILE FAIL" | |
| 35 | head -20 /tmp/hblk_err.txt | |
| 36 | exit 1 | |
| 37 | fi | |
| 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. | |
| 51 | sed '0,/float evaluate_density(/s//float evaluate_density_NOT_CALLED(/' \ | |
| 52 | "$BODY" > /tmp/hblk_broken.glsl | |
| 53 | if ! 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)" | |
| 64 | else | |
| 65 | echo " CONTROL SKIPPED -- substitution not present to break" | |
| 66 | exit 2 | |
| 67 | fi | |
| 68 | echo "" | |
| 69 | echo "VERDICT: the assembled heightfield density shader compiles." |
| … | … | @@ -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 | ||
| 32 | using dcerosion::vec2; | |
| 33 | using dcerosion::vec3; | |
| 34 | using dcerosion::vec4; | |
| 35 | ||
| 36 | // The map's own erosion settings (map_dc_diffusion.gd). | |
| 37 | static const float HS = 5.0f; | |
| 38 | static const float STR = 0.22f; | |
| 39 | static const float CELL = 0.7f; | |
| 40 | static const int OCT = 7; | |
| 41 | static const float GAIN = 0.5f; | |
| 42 | static const float FDIV = 4.8f; | |
| 43 | static 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. | |
| 47 | static 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 | ||
| 54 | static 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 | ||
| 62 | int 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 | } |
| … | … | @@ -235,8 +235,12 @@ static float gpu_wrapper(float x, float y, float z) { |
| 235 | 235 | // inlined here, and the inlined version's numbers are what the "hoist is |
| 236 | 236 | // faithful" check compared against. |
| 237 | 237 | 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); | |
| 240 | 244 | |
| 241 | 245 | float d = en.x - y_mapped; |
| 242 | 246 | d *= inv_yscale; |
| … | … | @@ -269,8 +273,12 @@ static float cpu_wrapper(float x, float y, float z) { |
| 269 | 273 | // erosion_filter's `scale` and skipped the remap, water gate and offset. |
| 270 | 274 | // It now calls the same hoisted node wrapper the GPU path calls. |
| 271 | 275 | 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); | |
| 274 | 282 | |
| 275 | 283 | float d = en.x - y_mapped; |
| 276 | 284 | d *= inv_yscale; |
| … | … | @@ -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 | ||
| 47 | typedef struct | |
| 48 | { | |
| 49 | unsigned char _x[4] | |
| 50 | __attribute__((__aligned__(4))); | |
| 51 | } omp_lock_t; | |
| 52 | ||
| 53 | typedef 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 | ||
| 71 | typedef 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 | ||
| 80 | typedef 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 | ||
| 91 | typedef 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 | ||
| 106 | typedef __GOMP_DEPRECATED_5_0 omp_sync_hint_t omp_lock_hint_t; | |
| 107 | ||
| 108 | typedef struct __attribute__((__aligned__ (sizeof (void *)))) omp_depend_t | |
| 109 | { | |
| 110 | char __omp_depend_t__[2 * sizeof (void *)]; | |
| 111 | } omp_depend_t; | |
| 112 | ||
| 113 | typedef enum omp_pause_resource_t | |
| 114 | { | |
| 115 | omp_pause_soft = 1, | |
| 116 | omp_pause_hard = 2 | |
| 117 | } omp_pause_resource_t; | |
| 118 | ||
| 119 | typedef __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 | ||
| 127 | typedef 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 | ||
| 137 | typedef 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 | ||
| 151 | typedef 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 | ||
| 163 | typedef 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 | ||
| 187 | typedef struct omp_alloctrait_t | |
| 188 | { | |
| 189 | omp_alloctrait_key_t key; | |
| 190 | omp_uintptr_t value; | |
| 191 | } omp_alloctrait_t; | |
| 192 | ||
| 193 | typedef enum omp_event_handle_t __GOMP_UINTPTR_T_ENUM | |
| 194 | { | |
| 195 | __omp_event_handle_t_max__ = __UINTPTR_MAX__ | |
| 196 | } omp_event_handle_t; | |
| 197 | ||
| 198 | enum | |
| 199 | { | |
| 200 | omp_initial_device = -1, | |
| 201 | omp_invalid_device = -4 | |
| 202 | }; | |
| 203 | ||
| 204 | #ifdef __cplusplus | |
| 205 | extern "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 | ||
| 213 | extern void omp_set_num_threads (int) __GOMP_NOTHROW; | |
| 214 | extern int omp_get_num_threads (void) __GOMP_NOTHROW; | |
| 215 | extern int omp_get_max_threads (void) __GOMP_NOTHROW; | |
| 216 | extern int omp_get_thread_num (void) __GOMP_NOTHROW; | |
| 217 | extern int omp_get_num_procs (void) __GOMP_NOTHROW; | |
| 218 | ||
| 219 | extern int omp_in_parallel (void) __GOMP_NOTHROW; | |
| 220 | ||
| 221 | extern void omp_set_dynamic (int) __GOMP_NOTHROW; | |
| 222 | extern int omp_get_dynamic (void) __GOMP_NOTHROW; | |
| 223 | ||
| 224 | extern void omp_set_nested (int) __GOMP_NOTHROW __GOMP_DEPRECATED_5_0; | |
| 225 | extern int omp_get_nested (void) __GOMP_NOTHROW __GOMP_DEPRECATED_5_0; | |
| 226 | ||
| 227 | extern void omp_init_lock (omp_lock_t *) __GOMP_NOTHROW; | |
| 228 | extern void omp_init_lock_with_hint (omp_lock_t *, omp_sync_hint_t) | |
| 229 | __GOMP_NOTHROW; | |
| 230 | extern void omp_destroy_lock (omp_lock_t *) __GOMP_NOTHROW; | |
| 231 | extern void omp_set_lock (omp_lock_t *) __GOMP_NOTHROW; | |
| 232 | extern void omp_unset_lock (omp_lock_t *) __GOMP_NOTHROW; | |
| 233 | extern int omp_test_lock (omp_lock_t *) __GOMP_NOTHROW; | |
| 234 | ||
| 235 | extern void omp_init_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW; | |
| 236 | extern void omp_init_nest_lock_with_hint (omp_nest_lock_t *, omp_sync_hint_t) | |
| 237 | __GOMP_NOTHROW; | |
| 238 | extern void omp_destroy_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW; | |
| 239 | extern void omp_set_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW; | |
| 240 | extern void omp_unset_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW; | |
| 241 | extern int omp_test_nest_lock (omp_nest_lock_t *) __GOMP_NOTHROW; | |
| 242 | ||
| 243 | extern double omp_get_wtime (void) __GOMP_NOTHROW; | |
| 244 | extern double omp_get_wtick (void) __GOMP_NOTHROW; | |
| 245 | ||
| 246 | extern void omp_set_schedule (omp_sched_t, int) __GOMP_NOTHROW; | |
| 247 | extern void omp_get_schedule (omp_sched_t *, int *) __GOMP_NOTHROW; | |
| 248 | extern int omp_get_thread_limit (void) __GOMP_NOTHROW; | |
| 249 | extern void omp_set_max_active_levels (int) __GOMP_NOTHROW; | |
| 250 | extern int omp_get_max_active_levels (void) __GOMP_NOTHROW; | |
| 251 | extern int omp_get_supported_active_levels (void) __GOMP_NOTHROW; | |
| 252 | extern int omp_get_level (void) __GOMP_NOTHROW; | |
| 253 | extern int omp_get_ancestor_thread_num (int) __GOMP_NOTHROW; | |
| 254 | extern int omp_get_team_size (int) __GOMP_NOTHROW; | |
| 255 | extern int omp_get_active_level (void) __GOMP_NOTHROW; | |
| 256 | ||
| 257 | extern int omp_in_final (void) __GOMP_NOTHROW; | |
| 258 | extern int omp_in_explicit_task (void) __GOMP_NOTHROW; | |
| 259 | ||
| 260 | extern int omp_get_cancellation (void) __GOMP_NOTHROW; | |
| 261 | extern omp_proc_bind_t omp_get_proc_bind (void) __GOMP_NOTHROW; | |
| 262 | extern int omp_get_num_places (void) __GOMP_NOTHROW; | |
| 263 | extern int omp_get_place_num_procs (int) __GOMP_NOTHROW; | |
| 264 | extern void omp_get_place_proc_ids (int, int *) __GOMP_NOTHROW; | |
| 265 | extern int omp_get_place_num (void) __GOMP_NOTHROW; | |
| 266 | extern int omp_get_partition_num_places (void) __GOMP_NOTHROW; | |
| 267 | extern void omp_get_partition_place_nums (int *) __GOMP_NOTHROW; | |
| 268 | ||
| 269 | extern void omp_set_default_device (int) __GOMP_NOTHROW; | |
| 270 | extern int omp_get_default_device (void) __GOMP_NOTHROW; | |
| 271 | extern int omp_get_num_devices (void) __GOMP_NOTHROW; | |
| 272 | extern int omp_get_device_num (void) __GOMP_NOTHROW; | |
| 273 | extern int omp_get_num_teams (void) __GOMP_NOTHROW; | |
| 274 | extern int omp_get_team_num (void) __GOMP_NOTHROW; | |
| 275 | ||
| 276 | extern int omp_is_initial_device (void) __GOMP_NOTHROW; | |
| 277 | extern int omp_get_initial_device (void) __GOMP_NOTHROW; | |
| 278 | extern int omp_get_max_task_priority (void) __GOMP_NOTHROW; | |
| 279 | ||
| 280 | extern void omp_fulfill_event (omp_event_handle_t) __GOMP_NOTHROW; | |
| 281 | ||
| 282 | extern void omp_set_num_teams (int) __GOMP_NOTHROW; | |
| 283 | extern int omp_get_max_teams (void) __GOMP_NOTHROW; | |
| 284 | extern void omp_set_teams_thread_limit (int) __GOMP_NOTHROW; | |
| 285 | extern int omp_get_teams_thread_limit (void) __GOMP_NOTHROW; | |
| 286 | ||
| 287 | extern void *omp_target_alloc (__SIZE_TYPE__, int) __GOMP_NOTHROW; | |
| 288 | extern void omp_target_free (void *, int) __GOMP_NOTHROW; | |
| 289 | extern int omp_target_is_present (const void *, int) __GOMP_NOTHROW; | |
| 290 | extern int omp_target_memcpy (void *, const void *, __SIZE_TYPE__, | |
| 291 | __SIZE_TYPE__, __SIZE_TYPE__, int, int) | |
| 292 | __GOMP_NOTHROW; | |
| 293 | extern 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; | |
| 297 | extern 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; | |
| 304 | extern 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; | |
| 312 | extern int omp_target_associate_ptr (const void *, const void *, __SIZE_TYPE__, | |
| 313 | __SIZE_TYPE__, int) __GOMP_NOTHROW; | |
| 314 | extern int omp_target_disassociate_ptr (const void *, int) __GOMP_NOTHROW; | |
| 315 | extern void *omp_get_mapped_ptr (const void *, int) __GOMP_NOTHROW; | |
| 316 | extern int omp_target_is_accessible (const void *, __SIZE_TYPE__, int) | |
| 317 | __GOMP_NOTHROW; | |
| 318 | ||
| 319 | extern void omp_set_affinity_format (const char *) __GOMP_NOTHROW; | |
| 320 | extern __SIZE_TYPE__ omp_get_affinity_format (char *, __SIZE_TYPE__) | |
| 321 | __GOMP_NOTHROW; | |
| 322 | extern void omp_display_affinity (const char *) __GOMP_NOTHROW; | |
| 323 | extern __SIZE_TYPE__ omp_capture_affinity (char *, __SIZE_TYPE__, const char *) | |
| 324 | __GOMP_NOTHROW; | |
| 325 | ||
| 326 | extern int omp_pause_resource (omp_pause_resource_t, int) __GOMP_NOTHROW; | |
| 327 | extern int omp_pause_resource_all (omp_pause_resource_t) __GOMP_NOTHROW; | |
| 328 | ||
| 329 | extern omp_allocator_handle_t omp_init_allocator (omp_memspace_handle_t, | |
| 330 | int, | |
| 331 | const omp_alloctrait_t []) | |
| 332 | __GOMP_NOTHROW; | |
| 333 | extern void omp_destroy_allocator (omp_allocator_handle_t) __GOMP_NOTHROW; | |
| 334 | extern void omp_set_default_allocator (omp_allocator_handle_t) __GOMP_NOTHROW; | |
| 335 | extern omp_allocator_handle_t omp_get_default_allocator (void) __GOMP_NOTHROW; | |
| 336 | extern void omp_free (void *, | |
| 337 | omp_allocator_handle_t __GOMP_DEFAULT_NULL_ALLOCATOR) | |
| 338 | __GOMP_NOTHROW; | |
| 339 | extern void *omp_alloc (__SIZE_TYPE__, | |
| 340 | omp_allocator_handle_t __GOMP_DEFAULT_NULL_ALLOCATOR) | |
| 341 | __GOMP_NOTHROW __attribute__((__malloc__, __malloc__, | |
| 342 | __alloc_size__ (1))); | |
| 343 | extern 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))); | |
| 348 | extern 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))); | |
| 352 | extern 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))); | |
| 357 | extern 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 | ||
| 362 | extern void omp_display_env (int) __GOMP_NOTHROW; | |
| 363 | ||
| 364 | #ifdef __cplusplus | |
| 365 | } | |
| 366 | #endif | |
| 367 | ||
| 368 | #endif /* _OMP_H */ |
| … | … | @@ -0,0 +1,136 @@ |
| 1 | #!/usr/bin/env python3 | |
| 2 | """Does the SHIPPING density expression have enough float32 precision to give | |
| 3 | dual contouring a usable surface normal? | |
| 4 | ||
| 5 | edge_intersect.glsl computes the normal that feeds the QEF by central | |
| 6 | differences on evaluate_density, with eps = voxel_size * 0.01. The shipping | |
| 7 | heightfield 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 | ||
| 13 | A and B are both about 2.63 near the ground, and their difference is what | |
| 14 | carries the geometry. This script does that arithmetic in float32, exactly as | |
| 15 | the shader does, and asks how much of the horizontal gradient survives. | |
| 16 | ||
| 17 | It reports the angular error of the resulting normal. Run it against a | |
| 18 | synthetic ramp (a known answer: the normal of a plane of slope s) so the check | |
| 19 | can FAIL -- an oracle whose input has no known answer proves nothing. | |
| 20 | """ | |
| 21 | import numpy as np | |
| 22 | ||
| 23 | F = np.float32 | |
| 24 | YS_NODE = F(0.000238095) # emitted by HeightfieldNode from 5.0/21000 | |
| 25 | YS_WRAP = F(0.000238) # emitted by String::num(height_scale/y_range, 6) | |
| 26 | FLOOR = F(-10900.0) | |
| 27 | SCALE = F(4200.0) | |
| 28 | ||
| 29 | ||
| 30 | def 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 | ||
| 38 | def 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 | ||
| 45 | def 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 | ||
| 60 | def 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 | ||
| 75 | def 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 | ||
| 135 | if __name__ == "__main__": | |
| 136 | main() |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | #!/bin/sh | |
| 2 | export 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. | |
| 7 | args=""; openmp=0; compile=0 | |
| 8 | for 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 | |
| 15 | done | |
| 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). | |
| 18 | omp="" | |
| 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. | |
| 21 | exec 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 |
| … | … | @@ -0,0 +1,21 @@ |
| 1 | #!/bin/sh | |
| 2 | export 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. | |
| 7 | args=""; openmp=0; compile=0 | |
| 8 | for 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 | |
| 15 | done | |
| 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). | |
| 18 | omp="" | |
| 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. | |
| 21 | exec 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 |