| … | … | @@ -85,6 +85,11 @@ |
| 85 | 85 | #include <vector> |
| 86 | 86 | |
| 87 | 87 | #include "blender_noise_cpp_shim.h" |
| 88 | // NOT `using`d. dcerosion has its own vec2/vec3/vec4, floor, max, dot and |
| 89 | // mix, and so does bnoisegl -- two shims for two .inc families, deliberately |
| 90 | // separate. Every erosion call below is qualified, and the types are built |
| 91 | // explicitly, so nothing is resolved by luck. |
| 92 | #include "erosion_cpp_shim.h" |
| 88 | 93 | |
| 89 | 94 | using namespace godot::bnoisegl; |
| 90 | 95 | |
| … | … | @@ -96,10 +101,52 @@ static const char *SUBJECT_FILES[] = { |
| 96 | 101 | "src/gpu/blender_voronoi_glsl.gen.h", |
| 97 | 102 | "src/gpu/blender_ramp_blend_glsl.gen.h", |
| 98 | 103 | "src/gpu/proc_simplex_glsl.gen.h", |
| 104 | // THE SIXTH, added 2026-09-06. erosion_filter.inc was the one dual-compiled |
| 105 | // source not among the subjects -- the oldest of them, the one three other |
| 106 | // systems evaluate, and the only one written before the f-suffix rule. It |
| 107 | // carries 119 bare float literals and not one suffixed, so on the C++ side |
| 108 | // every expression containing one is evaluated in DOUBLE and narrowed at the |
| 109 | // end, where the shader computes the whole chain in float. `t*t*t*(t*(t*6.0 |
| 110 | // - 15.0) + 10.0)` is that hazard in one line, and it is the quintic fade at |
| 111 | // the bottom of every octave of every erosion evaluation. |
| 112 | // |
| 113 | // It is self-contained: pn_max_octaves appears only in its comments, and the |
| 114 | // pn_ero_* globals belong to erosion_wrapper_glsl.h, not to this text. |
| 115 | // |
| 116 | // ⚠ THE HAZARD'S EFFECT WAS MEASURED, AND IT DOES NOT JUSTIFY SUFFIXING. |
| 117 | // hyper-games #65 asked for the effect, not the hazard. Both texts were |
| 118 | // compiled at once -- the shipped .inc and an f-suffixed twin, in two |
| 119 | // namespaces -- and asked the same questions over a dense sweep: |
| 120 | // |
| 121 | // erosion_hash 29282 cases 0 differ |
| 122 | // grad_noised_2d 43923 cases 0 differ |
| 123 | // fractal_noise_2d 43923 cases 0 differ |
| 124 | // phacelle_noise 58564 cases 4276 differ (7.3%) worst 9.2e-07 |
| 125 | // erosion_filter 8405 cases 1713 differ (20.4%) worst 3.0e-07 |
| 126 | // erosion_node_eval_full 3362 cases 581 differ (17.3%) worst 2.4e-07 |
| 127 | // |
| 128 | // The three zeros are the finding. `vec2 u = f*f*f*(f*(f*6.0-15.0)+10.0)` -- |
| 129 | // the quintic fade, which I predicted would be the sharpest site -- is |
| 130 | // IMMUNE, because `f*6.0` resolves to the shim's `operator*(vec2, float)` |
| 131 | // and the literal narrows AT THE CALL. Only genuinely scalar chains |
| 132 | // (`t - 0.5 * smoothing`) promote, which is why the three that differ are |
| 133 | // the three with scalar arithmetic in them. |
| 134 | // |
| 135 | // And suffixing does not move the CPU toward the GPU. With the twin in |
| 136 | // place the whole gate went from 3862/5371 bit-identical to 3861 -- one |
| 137 | // case WORSE. At this magnitude the GPU's FMA contraction dominates and it |
| 138 | // is not systematically on either side of the double promotion. So the |
| 139 | // literals are left alone: rewriting 119 of them in a filter that |
| 140 | // dc-terrain and hyper-tps both evaluate, to move numbers by 3e-07 in a |
| 141 | // direction the measurement does not endorse, is churn with a surface |
| 142 | // change attached. The class is not removed by construction here; it is |
| 143 | // covered by this gate instead. |
| 144 | "src/gpu/erosion_filter_glsl.gen.h", |
| 99 | 145 | }; |
| 100 | 146 | static const char *SUBJECT_MARKS[] = { "BLENDER_HASH_GLSL", "BLENDER_PERLIN_GLSL", |
| 101 | | "BLENDER_VORONOI_GLSL", "BLENDER_RAMP_BLEND_GLSL", "PROC_SIMPLEX_GLSL" }; |
| 102 | | static const int SUBJECT_N = 5; |
| 147 | "BLENDER_VORONOI_GLSL", "BLENDER_RAMP_BLEND_GLSL", "PROC_SIMPLEX_GLSL", |
| 148 | "EROSION_FILTER_GLSL" }; |
| 149 | static const int SUBJECT_N = 6; |
| 103 | 150 | |
| 104 | 151 | static const double TOL_REL = 1e-5; |
| 105 | 152 | static const double TOL_ABS = 1e-6; |
| … | … | @@ -289,8 +336,13 @@ static void init_vulkan(Ctx &c) { |
| 289 | 336 | // ============================================================================ |
| 290 | 337 | |
| 291 | 338 | // 24 rather than 16 since Voronoi arrived: its parameter block is eleven |
| 292 | | // fields and every one of them changes the answer. |
| 293 | | static const uint32_t STRIDE = 24; |
| 339 | // fields and every one of them changes the answer. 56 since the erosion filter |
| 340 | // arrived: erosion_node_eval_full takes NINETEEN parameters, thirty floats once |
| 341 | // the vec2s and vec4s are counted out, and every one of them changes the |
| 342 | // surface. Widening the record costs 1300 x 32 more words -- 166 KB -- and the |
| 343 | // alternative is reusing slots that already mean something else, which is how a |
| 344 | // gate comes to compare two things neither side meant. |
| 345 | static const uint32_t STRIDE = 56; |
| 294 | 346 | |
| 295 | 347 | enum Op { |
| 296 | 348 | OP_HASH1 = 1, OP_HASH2, OP_HASH3, OP_HASH4, |
| … | … | @@ -317,6 +369,21 @@ enum Op { |
| 317 | 369 | OP_RAMP_BLEND, // type = blend mode, comp = channel |
| 318 | 370 | OP_SIMPLEX, // the raw Ashima lattice |
| 319 | 371 | OP_SIMPLEX_FBM, // type = octaves |
| 372 | // erosion_filter.inc, bottom-up. The layering is the one the Blender port |
| 373 | // was built in, and for the same reason: a divergence in erosion_hash is |
| 374 | // unreadable once it has been through eight octaves and a fade. |
| 375 | OP_ERO_HASH, // comp 0..1 -- integer lattice, EXACT |
| 376 | OP_ERO_GRAD_NOISED, // comp 0..2 -- height + ANALYTIC derivative |
| 377 | OP_ERO_FRACTAL, // comp 0..2 -- the octave loop |
| 378 | OP_ERO_CLAMP01, |
| 379 | OP_ERO_POW_INV, |
| 380 | OP_ERO_EASE_OUT, |
| 381 | OP_ERO_SMOOTH_START, |
| 382 | OP_ERO_NORMALIZE_SAFE, // comp 0..1 |
| 383 | OP_ERO_PHACELLE, // comp 0..3 -- the fade-approach primitive |
| 384 | OP_ERO_FILTER, // comp 0..4 -- delta.xyz, magnitude, ridge |
| 385 | OP_ERO_EVAL_FULL, // comp 0..1 -- the wrapper ErosionNoiseNode calls |
| 386 | OP_ERO_EVAL_GRAD, // comp 0..3 -- same, keeping the slope |
| 320 | 387 | OP__COUNT |
| 321 | 388 | }; |
| 322 | 389 | |
| … | … | @@ -341,6 +408,13 @@ static const OpName OP_NAMES[] = { |
| 341 | 408 | { OP_VORONOI_XFX, "OP_VORONOI_XFX" }, { OP_VORONOI_EDGE, "OP_VORONOI_EDGE" }, |
| 342 | 409 | { OP_VORONOI_RADIUS, "OP_VORONOI_RADIUS" }, { OP_RAMP_BLEND, "OP_RAMP_BLEND" }, |
| 343 | 410 | { OP_SIMPLEX, "OP_SIMPLEX" }, { OP_SIMPLEX_FBM, "OP_SIMPLEX_FBM" }, |
| 411 | { OP_ERO_HASH, "OP_ERO_HASH" }, { OP_ERO_GRAD_NOISED, "OP_ERO_GRAD_NOISED" }, |
| 412 | { OP_ERO_FRACTAL, "OP_ERO_FRACTAL" }, { OP_ERO_CLAMP01, "OP_ERO_CLAMP01" }, |
| 413 | { OP_ERO_POW_INV, "OP_ERO_POW_INV" }, { OP_ERO_EASE_OUT, "OP_ERO_EASE_OUT" }, |
| 414 | { OP_ERO_SMOOTH_START, "OP_ERO_SMOOTH_START" }, |
| 415 | { OP_ERO_NORMALIZE_SAFE, "OP_ERO_NORMALIZE_SAFE" }, |
| 416 | { OP_ERO_PHACELLE, "OP_ERO_PHACELLE" }, { OP_ERO_FILTER, "OP_ERO_FILTER" }, |
| 417 | { OP_ERO_EVAL_FULL, "OP_ERO_EVAL_FULL" }, { OP_ERO_EVAL_GRAD, "OP_ERO_EVAL_GRAD" }, |
| 344 | 418 | }; |
| 345 | 419 | |
| 346 | 420 | struct Rec { |
| … | … | @@ -360,7 +434,27 @@ enum { W_OP = 0, W_DIM = 1, W_TYPE = 2, W_FLAGS = 3, W_COMP = 4, |
| 360 | 434 | W_DETAIL = 9, W_ROUGH = 10, W_LAC = 11, W_OFF = 12, W_GAIN = 13, W_DIST = 14, |
| 361 | 435 | // Voronoi's parameter block |
| 362 | 436 | W_SCALE = 15, W_SMOOTH = 16, W_EXPONENT = 17, W_RANDOMNESS = 18, W_MAXDIST = 19, |
| 363 | | W_METRIC = 20 }; |
| 437 | W_METRIC = 20, |
| 438 | // The erosion block. Voronoi's slots are spelled `gF(b, 15u)` by hand in the |
| 439 | // GLSL dispatcher, which is the drift this file warns about everywhere else, |
| 440 | // so these are EMITTED into the shader from the enum (see SLOT_NAMES) and |
| 441 | // both sides name them. |
| 442 | W_E_PX = 24, W_E_PY = 25, |
| 443 | W_E_NX = 26, W_E_NY = 27, W_E_NZ = 28, |
| 444 | W_E_S0 = 29, // 16 scalars, W_E_S0 + k |
| 445 | W_E_R0 = 45, // rounding vec4 |
| 446 | W_E_O0 = 49, // onset vec4 |
| 447 | W_E_A0 = 53, // assumed slope vec2 |
| 448 | W_E_OCT = 55 }; |
| 449 | |
| 450 | // Emitted into the shader beside the opcodes, same reason. |
| 451 | struct SlotName { int slot; const char *name; }; |
| 452 | static const SlotName SLOT_NAMES[] = { |
| 453 | { W_E_PX, "W_E_PX" }, { W_E_PY, "W_E_PY" }, |
| 454 | { W_E_NX, "W_E_NX" }, { W_E_NY, "W_E_NY" }, { W_E_NZ, "W_E_NZ" }, |
| 455 | { W_E_S0, "W_E_S0" }, { W_E_R0, "W_E_R0" }, { W_E_O0, "W_E_O0" }, |
| 456 | { W_E_A0, "W_E_A0" }, { W_E_OCT, "W_E_OCT" }, |
| 457 | }; |
| 364 | 458 | |
| 365 | 459 | // Filled from the record, all eleven fields -- a GLSL struct has no defaults |
| 366 | 460 | // and a field left unset is garbage, not a sensible value. |
| … | … | @@ -380,6 +474,18 @@ static BvParams params_from(const uint32_t *w) { |
| 380 | 474 | return p; |
| 381 | 475 | } |
| 382 | 476 | |
| 477 | // The erosion block's accessors. es(w, k) is scalar k; e2/e4 build the vector |
| 478 | // bundles at a named base. Spelled once so the arms below read like the .inc's |
| 479 | // own signatures. |
| 480 | static float es(const uint32_t *w, int k) { return float_of(w[W_E_S0 + k]); } |
| 481 | static dcerosion::vec2 e2(const uint32_t *w, int base) { |
| 482 | return dcerosion::vec2(float_of(w[base]), float_of(w[base + 1])); |
| 483 | } |
| 484 | static dcerosion::vec4 e4(const uint32_t *w, int base) { |
| 485 | return dcerosion::vec4(float_of(w[base]), float_of(w[base + 1]), |
| 486 | float_of(w[base + 2]), float_of(w[base + 3])); |
| 487 | } |
| 488 | |
| 383 | 489 | // --- the CPU half of the dispatcher ----------------------------------------- |
| 384 | 490 | static uint32_t eval_cpu(const uint32_t *w) { |
| 385 | 491 | const uint32_t op = w[W_OP], dim = w[W_DIM], comp = w[W_COMP]; |
| … | … | @@ -393,6 +499,10 @@ static uint32_t eval_cpu(const uint32_t *w) { |
| 393 | 499 | const float gain = float_of(w[W_GAIN]), dist = float_of(w[W_DIST]); |
| 394 | 500 | const vec2 v2(x, y); const vec3 v3(x, y, z); const vec4 v4(x, y, z, wv); |
| 395 | 501 | const ivec2 i2(ia, ib); const ivec3 i3(ia, ib, ic); const ivec4 i4(ia, ib, ic, id); |
| 502 | // The erosion block, read by slot name on both sides. |
| 503 | const float ex = float_of(w[W_E_PX]), ey = float_of(w[W_E_PY]); |
| 504 | const float enx = float_of(w[W_E_NX]), eny = float_of(w[W_E_NY]), |
| 505 | enz = float_of(w[W_E_NZ]); |
| 396 | 506 | |
| 397 | 507 | switch (op) { |
| 398 | 508 | case OP_HASH1: return bn_hash(ua); |
| … | … | @@ -510,6 +620,62 @@ static uint32_t eval_cpu(const uint32_t *w) { |
| 510 | 620 | : (dim == 3) ? bv_voronoi_n_sphere_radius(p, v3) |
| 511 | 621 | : bv_voronoi_n_sphere_radius(p, v4)); |
| 512 | 622 | } |
| 623 | // --- erosion_filter.inc, through dcerosion --------------------------- |
| 624 | case OP_ERO_HASH: { |
| 625 | dcerosion::vec2 q = dcerosion::erosion_hash(dcerosion::vec2(ex, ey)); |
| 626 | return bits_of(comp == 0 ? q.x : q.y); |
| 627 | } |
| 628 | case OP_ERO_GRAD_NOISED: { |
| 629 | dcerosion::vec3 q = dcerosion::grad_noised_2d(dcerosion::vec2(ex, ey)); |
| 630 | return bits_of(comp == 0 ? q.x : (comp == 1 ? q.y : q.z)); |
| 631 | } |
| 632 | case OP_ERO_FRACTAL: { |
| 633 | dcerosion::vec3 q = dcerosion::fractal_noise_2d(dcerosion::vec2(ex, ey), |
| 634 | es(w, 0), (int)w[W_E_OCT], es(w, 1), es(w, 2)); |
| 635 | return bits_of(comp == 0 ? q.x : (comp == 1 ? q.y : q.z)); |
| 636 | } |
| 637 | case OP_ERO_CLAMP01: return bits_of(dcerosion::clamp01(es(w, 0))); |
| 638 | case OP_ERO_POW_INV: return bits_of(dcerosion::pow_inv(es(w, 0), es(w, 1))); |
| 639 | case OP_ERO_EASE_OUT: return bits_of(dcerosion::ease_out(es(w, 0))); |
| 640 | case OP_ERO_SMOOTH_START: |
| 641 | return bits_of(dcerosion::smooth_start(es(w, 0), es(w, 1))); |
| 642 | case OP_ERO_NORMALIZE_SAFE: { |
| 643 | dcerosion::vec2 q = dcerosion::normalize_safe(dcerosion::vec2(ex, ey)); |
| 644 | return bits_of(comp == 0 ? q.x : q.y); |
| 645 | } |
| 646 | case OP_ERO_PHACELLE: { |
| 647 | dcerosion::vec4 q = dcerosion::phacelle_noise(dcerosion::vec2(ex, ey), |
| 648 | dcerosion::vec2(enx, eny), es(w, 0), es(w, 1), es(w, 2)); |
| 649 | return bits_of(comp == 0 ? q.x : (comp == 1 ? q.y : (comp == 2 ? q.z : q.w))); |
| 650 | } |
| 651 | case OP_ERO_FILTER: { |
| 652 | dcerosion::ErosionResult r = dcerosion::erosion_filter( |
| 653 | dcerosion::vec2(ex, ey), dcerosion::vec3(enx, eny, enz), |
| 654 | es(w, 0), es(w, 1), es(w, 2), es(w, 3), |
| 655 | e4(w, W_E_R0), e4(w, W_E_O0), e2(w, W_E_A0), |
| 656 | es(w, 4), (int)w[W_E_OCT], es(w, 5), |
| 657 | es(w, 6), es(w, 7), es(w, 8), es(w, 9)); |
| 658 | return bits_of(comp == 0 ? r.delta.x : (comp == 1 ? r.delta.y |
| 659 | : (comp == 2 ? r.delta.z : (comp == 3 ? r.magnitude : r.ridge)))); |
| 660 | } |
| 661 | case OP_ERO_EVAL_FULL: { |
| 662 | dcerosion::vec2 q = dcerosion::erosion_node_eval_full( |
| 663 | dcerosion::vec2(ex, ey), dcerosion::vec3(enx, eny, enz), |
| 664 | es(w, 0), es(w, 1), es(w, 2), (int)w[W_E_OCT], es(w, 3), |
| 665 | es(w, 4), es(w, 5), es(w, 6), es(w, 7), es(w, 8), |
| 666 | es(w, 9), es(w, 10), e2(w, W_E_S0 + 11), |
| 667 | e4(w, W_E_R0), e4(w, W_E_O0), e2(w, W_E_A0), es(w, 13)); |
| 668 | return bits_of(comp == 0 ? q.x : q.y); |
| 669 | } |
| 670 | case OP_ERO_EVAL_GRAD: { |
| 671 | dcerosion::vec4 q = dcerosion::erosion_node_eval_grad( |
| 672 | dcerosion::vec2(ex, ey), dcerosion::vec3(enx, eny, enz), |
| 673 | es(w, 0), es(w, 1), es(w, 2), (int)w[W_E_OCT], es(w, 3), |
| 674 | es(w, 4), es(w, 5), es(w, 6), es(w, 7), es(w, 8), |
| 675 | es(w, 9), es(w, 10), e2(w, W_E_S0 + 11), |
| 676 | e4(w, W_E_R0), e4(w, W_E_O0), e2(w, W_E_A0), es(w, 13)); |
| 677 | return bits_of(comp == 0 ? q.x : (comp == 1 ? q.y : (comp == 2 ? q.z : q.w))); |
| 678 | } |
| 513 | 679 | default: |
| 514 | 680 | fprintf(stderr, "FATAL: cpu dispatcher has no arm for op %u\n", op); |
| 515 | 681 | exit(1); |
| … | … | @@ -524,6 +690,8 @@ layout(std430, binding = 1) readonly buffer In { uint v[]; } in_v; |
| 524 | 690 | |
| 525 | 691 | uint gW(uint b, uint k) { return in_v.v[b + k]; } |
| 526 | 692 | float gF(uint b, uint k) { return uintBitsToFloat(in_v.v[b + k]); } |
| 693 | // The erosion block's scalar k, so this side reads it the way es(w, k) does. |
| 694 | float eS(uint b, uint k) { return gF(b, W_E_S0 + k); } |
| 527 | 695 | |
| 528 | 696 | void main() { |
| 529 | 697 | uint idx = gl_GlobalInvocationID.x; |
| … | … | @@ -545,6 +713,14 @@ void main() { |
| 545 | 713 | vec2 v2 = vec2(x, y); vec3 v3 = vec3(x, y, z); vec4 v4 = vec4(x, y, z, w); |
| 546 | 714 | ivec2 i2 = ivec2(ia, ib); ivec3 i3 = ivec3(ia, ib, ic); ivec4 i4 = ivec4(ia, ib, ic, id); |
| 547 | 715 | |
| 716 | // The erosion block. Slot names come from the C++ enum via SLOT_NAMES. |
| 717 | vec2 ep = vec2(gF(b, W_E_PX), gF(b, W_E_PY)); |
| 718 | vec3 en = vec3(gF(b, W_E_NX), gF(b, W_E_NY), gF(b, W_E_NZ)); |
| 719 | vec4 e_round = vec4(gF(b, W_E_R0), gF(b, W_E_R0 + 1u), gF(b, W_E_R0 + 2u), gF(b, W_E_R0 + 3u)); |
| 720 | vec4 e_onset = vec4(gF(b, W_E_O0), gF(b, W_E_O0 + 1u), gF(b, W_E_O0 + 2u), gF(b, W_E_O0 + 3u)); |
| 721 | vec2 e_slope = vec2(gF(b, W_E_A0), gF(b, W_E_A0 + 1u)); |
| 722 | int e_oct = int(gW(b, W_E_OCT)); |
| 723 | |
| 548 | 724 | uint r = 0u; |
| 549 | 725 | if (op == OP_HASH1) { r = bn_hash(ua); } |
| 550 | 726 | else if (op == OP_HASH2) { r = bn_hash(ua, ub); } |
| … | … | @@ -704,6 +880,62 @@ void main() { |
| 704 | 880 | r = floatBitsToUint(f); |
| 705 | 881 | } |
| 706 | 882 | } |
| 883 | else if (op == OP_ERO_HASH) { |
| 884 | vec2 q = erosion_hash(ep); |
| 885 | r = floatBitsToUint(comp == 0u ? q.x : q.y); |
| 886 | } |
| 887 | else if (op == OP_ERO_GRAD_NOISED) { |
| 888 | vec3 q = grad_noised_2d(ep); |
| 889 | r = floatBitsToUint(comp == 0u ? q.x : (comp == 1u ? q.y : q.z)); |
| 890 | } |
| 891 | else if (op == OP_ERO_FRACTAL) { |
| 892 | vec3 q = fractal_noise_2d(ep, eS(b, 0u), e_oct, eS(b, 1u), eS(b, 2u)); |
| 893 | r = floatBitsToUint(comp == 0u ? q.x : (comp == 1u ? q.y : q.z)); |
| 894 | } |
| 895 | else if (op == OP_ERO_CLAMP01) { r = floatBitsToUint(clamp01(eS(b, 0u))); } |
| 896 | else if (op == OP_ERO_POW_INV) { r = floatBitsToUint(pow_inv(eS(b, 0u), eS(b, 1u))); } |
| 897 | else if (op == OP_ERO_EASE_OUT) { r = floatBitsToUint(ease_out(eS(b, 0u))); } |
| 898 | else if (op == OP_ERO_SMOOTH_START) { |
| 899 | r = floatBitsToUint(smooth_start(eS(b, 0u), eS(b, 1u))); |
| 900 | } |
| 901 | else if (op == OP_ERO_NORMALIZE_SAFE) { |
| 902 | vec2 q = normalize_safe(ep); |
| 903 | r = floatBitsToUint(comp == 0u ? q.x : q.y); |
| 904 | } |
| 905 | else if (op == OP_ERO_PHACELLE) { |
| 906 | vec4 q = phacelle_noise(ep, vec2(en.x, en.y), eS(b, 0u), eS(b, 1u), eS(b, 2u)); |
| 907 | r = floatBitsToUint(comp == 0u ? q.x : (comp == 1u ? q.y : (comp == 2u ? q.z : q.w))); |
| 908 | } |
| 909 | else if (op == OP_ERO_FILTER) { |
| 910 | ErosionResult q = erosion_filter(ep, en, |
| 911 | eS(b, 0u), eS(b, 1u), eS(b, 2u), eS(b, 3u), |
| 912 | e_round, e_onset, e_slope, |
| 913 | eS(b, 4u), e_oct, eS(b, 5u), |
| 914 | eS(b, 6u), eS(b, 7u), eS(b, 8u), eS(b, 9u)); |
| 915 | float f; |
| 916 | if (comp == 0u) { f = q.delta.x; } |
| 917 | else if (comp == 1u) { f = q.delta.y; } |
| 918 | else if (comp == 2u) { f = q.delta.z; } |
| 919 | else if (comp == 3u) { f = q.magnitude; } |
| 920 | else { f = q.ridge; } |
| 921 | r = floatBitsToUint(f); |
| 922 | } |
| 923 | else if (op == OP_ERO_EVAL_FULL) { |
| 924 | vec2 q = erosion_node_eval_full(ep, en, |
| 925 | eS(b, 0u), eS(b, 1u), eS(b, 2u), e_oct, eS(b, 3u), |
| 926 | eS(b, 4u), eS(b, 5u), eS(b, 6u), eS(b, 7u), eS(b, 8u), |
| 927 | eS(b, 9u), eS(b, 10u), vec2(eS(b, 11u), eS(b, 12u)), |
| 928 | e_round, e_onset, e_slope, eS(b, 13u)); |
| 929 | r = floatBitsToUint(comp == 0u ? q.x : q.y); |
| 930 | } |
| 931 | else if (op == OP_ERO_EVAL_GRAD) { |
| 932 | vec4 q = erosion_node_eval_grad(ep, en, |
| 933 | eS(b, 0u), eS(b, 1u), eS(b, 2u), e_oct, eS(b, 3u), |
| 934 | eS(b, 4u), eS(b, 5u), eS(b, 6u), eS(b, 7u), eS(b, 8u), |
| 935 | eS(b, 9u), eS(b, 10u), vec2(eS(b, 11u), eS(b, 12u)), |
| 936 | e_round, e_onset, e_slope, eS(b, 13u)); |
| 937 | r = floatBitsToUint(comp == 0u ? q.x : (comp == 1u ? q.y : (comp == 2u ? q.z : q.w))); |
| 938 | } |
| 707 | 939 | out_v.v[idx] = r; |
| 708 | 940 | } |
| 709 | 941 | )DISPATCH"; |
| … | … | @@ -743,6 +975,28 @@ struct B { |
| 743 | 975 | w[W_EXPONENT] = bits_of(expo); w[W_RANDOMNESS] = bits_of(rand01); |
| 744 | 976 | w[W_MAXDIST] = bits_of(maxd); w[W_METRIC] = (uint32_t)metric; return *this; |
| 745 | 977 | } |
| 978 | // --- the erosion block --------------------------------------------------- |
| 979 | B &ep(float px, float py) { |
| 980 | w[W_E_PX] = bits_of(px); w[W_E_PY] = bits_of(py); return *this; |
| 981 | } |
| 982 | B &en(float nx, float ny, float nz) { |
| 983 | w[W_E_NX] = bits_of(nx); w[W_E_NY] = bits_of(ny); w[W_E_NZ] = bits_of(nz); |
| 984 | return *this; |
| 985 | } |
| 986 | B &es(int k, float v) { w[W_E_S0 + k] = bits_of(v); return *this; } |
| 987 | B &eoct(int o) { w[W_E_OCT] = (uint32_t)o; return *this; } |
| 988 | B &eround(float a_, float b_, float c_, float d_) { |
| 989 | w[W_E_R0] = bits_of(a_); w[W_E_R0 + 1] = bits_of(b_); |
| 990 | w[W_E_R0 + 2] = bits_of(c_); w[W_E_R0 + 3] = bits_of(d_); return *this; |
| 991 | } |
| 992 | B &eonset(float a_, float b_, float c_, float d_) { |
| 993 | w[W_E_O0] = bits_of(a_); w[W_E_O0 + 1] = bits_of(b_); |
| 994 | w[W_E_O0 + 2] = bits_of(c_); w[W_E_O0 + 3] = bits_of(d_); return *this; |
| 995 | } |
| 996 | B &eslope(float a_, float b_) { |
| 997 | w[W_E_A0] = bits_of(a_); w[W_E_A0 + 1] = bits_of(b_); return *this; |
| 998 | } |
| 999 | |
| 746 | 1000 | B &frac(float detail, float rough, float lac, float off, float gain, float dist) { |
| 747 | 1001 | w[W_DETAIL] = bits_of(detail); w[W_ROUGH] = bits_of(rough); |
| 748 | 1002 | w[W_LAC] = bits_of(lac); w[W_OFF] = bits_of(off); |
| … | … | @@ -1046,6 +1300,213 @@ static void build_simplex_cases() { |
| 1046 | 1300 | } |
| 1047 | 1301 | } |
| 1048 | 1302 | |
| 1303 | // ============================================================================ |
| 1304 | // erosion_filter.inc, bottom-up. |
| 1305 | // |
| 1306 | // The layering is the one the Blender port was built in and for the same |
| 1307 | // reason: a divergence in erosion_hash is unreadable once it has been through |
| 1308 | // eight octaves, a fade and a gully mask, and each layer here is checked before |
| 1309 | // the one that consumes it. |
| 1310 | // |
| 1311 | // The parameter bundles are the SHIPPED ones -- runevision's reference values, |
| 1312 | // read off get_erosion_library_glsl()'s globals and ErosionNoiseNode's socket |
| 1313 | // defaults, not invented for the test. A gate on parameters nothing uses proves |
| 1314 | // the arithmetic and not the surface. |
| 1315 | // |
| 1316 | // --------------------------------------------------------------------------- |
| 1317 | // ⚠ HOW TO MUTATION-TEST THIS FAMILY, because the obvious way proves nothing. |
| 1318 | // |
| 1319 | // Every other gate in this repo is mutated by editing the thing under test. |
| 1320 | // Here the thing under test is ONE TEXT compiled twice, so editing the .inc |
| 1321 | // changes BOTH sides and the gate stays green -- "a transcription used as the |
| 1322 | // reference can only ever agree with itself", one level up. The mutation that |
| 1323 | // works is to edit the .inc and NOT regenerate the .gen.h: the CPU picks the |
| 1324 | // change up through the shim, the GPU keeps the shipped text, and they part. |
| 1325 | // That is also exactly the stale-generated-header scenario the generator's own |
| 1326 | // docstring promises cannot happen silently. |
| 1327 | // |
| 1328 | // Measured 2026-09-06, every family caught by its own case: |
| 1329 | // |
| 1330 | // erosion_hash 2^24 -> 2^24-1 (both lines) 9 red erosion_hash |
| 1331 | // quintic fade +10.0 -> +11.0 105 red grad_noised_2d |
| 1332 | // fade derivative 30.0 -> 29.0 62 red grad_noised, fractal |
| 1333 | // clamp(t, 0.0, 1.0) -> (t, 0.0, 0.9) 521 red clamp01 + downstream |
| 1334 | // ease_out 1 - v*v -> 1 - v*v*v 541 red ease_out + downstream |
| 1335 | // pow_inv 1 - pow() -> 1 - 0.5*pow() 408 red pow_inv + wrappers |
| 1336 | // smooth_start 0.5*s -> 0.4*s 357 red filter, _full, _grad |
| 1337 | // normalize_safe 1e-20 -> 1e-1 2 red normalize_safe |
| 1338 | // phacelle TAU 6.2831853 -> 6.2831 92 red phacelle_noise |
| 1339 | // |
| 1340 | // ⛔ AND ONE THAT PROVED NOTHING. TAU 6.28318530717958647 -> 6.28318530717000000 |
| 1341 | // left the gate GREEN, and it is not a coverage gap: both spellings are the |
| 1342 | // same float32 (6.2831855), so the mutation never happened. It is recorded |
| 1343 | // because a sweep that had stopped at "9 of 10 caught" would have gone looking |
| 1344 | // for a hole in the phacelle cases that is not there. |
| 1345 | // --------------------------------------------------------------------------- |
| 1346 | // ============================================================================ |
| 1347 | static void build_erosion_cases() { |
| 1348 | // Points chosen where this filter actually breaks: the origin (a lattice |
| 1349 | // point, where erosion_hash's floor lands exactly), the negative quadrant |
| 1350 | // (GLSL fract is x - floor(x), which for negative x is neither fmod nor |
| 1351 | // truncation), a diagonal, and coordinates large enough that the hash's |
| 1352 | // 16777216.0 = 2^24 matters. |
| 1353 | const float P[][2] = { |
| 1354 | { 0.0f, 0.0f }, { 0.5f, 0.5f }, { 1.0f, 0.0f }, |
| 1355 | { 0.37f, 0.81f }, { -0.7f, 1.3f }, { -3.3f, -4.1f }, |
| 1356 | { 12.25f, -7.75f }, { 123.5f, 456.25f }, { 1e-4f, -1e-4f }, |
| 1357 | }; |
| 1358 | const int NP = (int)(sizeof(P) / sizeof(P[0])); |
| 1359 | |
| 1360 | // EXACT for the hash: it is integer arithmetic to a float conversion, the |
| 1361 | // same class as the blender hash layer, and a near-miss is a finding. |
| 1362 | for (int i = 0; i < NP; i++) { |
| 1363 | for (uint32_t c = 0; c < 2; c++) { |
| 1364 | add("erosion_hash", true, B(OP_ERO_HASH).comp(c).ep(P[i][0], P[i][1])); |
| 1365 | } |
| 1366 | } |
| 1367 | |
| 1368 | // grad_noised_2d is where the f-suffix hazard is sharpest: the quintic fade |
| 1369 | // `t*t*t*(t*(t*6.0 - 15.0) + 10.0)` and its derivative `30.0*t*t*(t*(t-2.0) |
| 1370 | // + 1.0)` are chained expressions containing bare literals, so on the C++ |
| 1371 | // side the whole chain evaluates in DOUBLE and narrows once at the end, |
| 1372 | // where the shader rounds at every step in float. |
| 1373 | for (int i = 0; i < NP; i++) { |
| 1374 | for (uint32_t c = 0; c < 3; c++) { |
| 1375 | add("grad_noised_2d", false, B(OP_ERO_GRAD_NOISED).comp(c).ep(P[i][0], P[i][1])); |
| 1376 | } |
| 1377 | } |
| 1378 | |
| 1379 | // The scalar helpers. clamp01 and ease_out are one expression each; pow_inv |
| 1380 | // carries a pow(), and smooth_start has the branch whose t >= smoothing side |
| 1381 | // is the guard against a division by zero. |
| 1382 | const float T[] = { -0.5f, 0.0f, 0.25f, 0.5f, 1.0f, 1.5f, 7.0f }; |
| 1383 | const int NT = (int)(sizeof(T) / sizeof(T[0])); |
| 1384 | for (int i = 0; i < NT; i++) { |
| 1385 | add("clamp01", false, B(OP_ERO_CLAMP01).es(0, T[i])); |
| 1386 | add("ease_out", false, B(OP_ERO_EASE_OUT).es(0, T[i])); |
| 1387 | for (int k = 0; k < 3; k++) { |
| 1388 | const float pw[3] = { 0.5f, 1.0f, 2.5f }; |
| 1389 | add("pow_inv", false, B(OP_ERO_POW_INV).es(0, T[i]).es(1, pw[k])); |
| 1390 | const float sm[3] = { 0.0f, 0.1f, 1.0f }; |
| 1391 | add("smooth_start", false, B(OP_ERO_SMOOTH_START).es(0, T[i]).es(1, sm[k])); |
| 1392 | } |
| 1393 | } |
| 1394 | // normalize_safe's whole reason for existing is the l > 1e-20 branch, so |
| 1395 | // the zero vector and a vector just under the threshold are both here. |
| 1396 | const float NV[][2] = { { 0.0f, 0.0f }, { 3.0f, 4.0f }, { -1e-21f, 0.0f }, { 1e-8f, -1e-8f } }; |
| 1397 | for (int i = 0; i < 4; i++) { |
| 1398 | for (uint32_t c = 0; c < 2; c++) { |
| 1399 | add("normalize_safe", false, B(OP_ERO_NORMALIZE_SAFE).comp(c).ep(NV[i][0], NV[i][1])); |
| 1400 | } |
| 1401 | } |
| 1402 | |
| 1403 | // fractal_noise_2d: the octave loop over grad_noised_2d, which is where a |
| 1404 | // per-step rounding difference would accumulate if there were one. |
| 1405 | for (int i = 0; i < NP; i++) { |
| 1406 | for (int oct = 1; oct <= 7; oct += 2) { |
| 1407 | for (uint32_t c = 0; c < 3; c++) { |
| 1408 | add("fractal_noise_2d", false, B(OP_ERO_FRACTAL).comp(c) |
| 1409 | .ep(P[i][0], P[i][1]).eoct(oct) |
| 1410 | .es(0, 3.0f).es(1, 2.0f).es(2, 0.5f)); |
| 1411 | } |
| 1412 | } |
| 1413 | } |
| 1414 | |
| 1415 | // phacelle_noise: sixteen samples around a cell, TAU written to nineteen |
| 1416 | // digits -- more than float32 holds -- and 0.01111. The normalisation |
| 1417 | // parameter is swept because 0 and 1 take different branches. |
| 1418 | for (int i = 0; i < NP; i++) { |
| 1419 | for (int k = 0; k < 2; k++) { |
| 1420 | for (uint32_t c = 0; c < 4; c++) { |
| 1421 | add("phacelle_noise", false, B(OP_ERO_PHACELLE).comp(c) |
| 1422 | .ep(P[i][0], P[i][1]).en(0.6f, 0.8f, 0.0f) |
| 1423 | .es(0, 1.0f).es(1, k ? 0.37f : 0.0f).es(2, k ? 1.0f : 0.0f)); |
| 1424 | } |
| 1425 | } |
| 1426 | } |
| 1427 | |
| 1428 | // The filter and both wrappers, on the shipped bundles. |
| 1429 | // |
| 1430 | // rounding = (crease 0.1, ridge 0.0, input feature 0.1, per octave 2.0) |
| 1431 | // onset = (gully 1.25, octave 1.25, ridge 2.8, ridge octave 1.5) |
| 1432 | // assumed = (0.7, 1.0) |
| 1433 | // |
| 1434 | // max_octaves 999 is what every shipping caller passes and makes each |
| 1435 | // octave weight exactly 1.0; 4.5 exercises the fractional taper, which is |
| 1436 | // the branch that weights the last partial octave. |
| 1437 | const float GN[][3] = { |
| 1438 | { 0.0f, 0.0f, 0.0f }, // flat: the assumed-slope path, since the |
| 1439 | // real slope is zero and normalize_safe |
| 1440 | // returns the zero vector |
| 1441 | { 0.3f, 0.1f, -0.2f }, |
| 1442 | { -0.8f, 0.45f, 0.6f }, |
| 1443 | { 0.95f, -1.2f, 0.7f }, |
| 1444 | }; |
| 1445 | const float MO[] = { 999.0f, 4.5f }; |
| 1446 | for (int i = 0; i < NP; i++) { |
| 1447 | for (int g = 0; g < 4; g++) { |
| 1448 | for (int m = 0; m < 2; m++) { |
| 1449 | for (uint32_t c = 0; c < 5; c++) { |
| 1450 | add("erosion_filter", false, B(OP_ERO_FILTER).comp(c) |
| 1451 | .ep(P[i][0], P[i][1]).en(GN[g][0], GN[g][1], GN[g][2]) |
| 1452 | .es(0, 0.25f) // fade_target |
| 1453 | .es(1, 0.16f) // strength |
| 1454 | .es(2, 0.5f) // gully_weight |
| 1455 | .es(3, 1.5f) // detail |
| 1456 | .es(4, 5.0f) // scale |
| 1457 | .es(5, 2.0f) // lacunarity |
| 1458 | .es(6, 0.5f) // gain |
| 1459 | .es(7, 1.0f) // cell_scale |
| 1460 | .es(8, 0.5f) // normalization |
| 1461 | .es(9, MO[m]) // max_octaves |
| 1462 | .eoct(5) |
| 1463 | .eround(0.1f, 0.0f, 0.1f, 2.0f) |
| 1464 | .eonset(1.25f, 1.25f, 2.8f, 1.5f) |
| 1465 | .eslope(0.7f, 1.0f), 5e-5); |
| 1466 | } |
| 1467 | for (uint32_t c = 0; c < 2; c++) { |
| 1468 | add("erosion_node_eval_full", false, B(OP_ERO_EVAL_FULL).comp(c) |
| 1469 | .ep(P[i][0], P[i][1]).en(GN[g][0], GN[g][1], GN[g][2]) |
| 1470 | .es(0, 5.0f) // height_scale |
| 1471 | .es(1, 0.16f) // erosion_strength |
| 1472 | .es(2, 1.0f) // cell_size |
| 1473 | .es(3, 0.5f) // gain |
| 1474 | .es(4, 0.6f) // peak_height_divisor (pn_fade_div) |
| 1475 | .es(5, 0.15f) // gully_band_scale (pn_erosion_scale) |
| 1476 | .es(6, 0.25f) // input_height_amplitude |
| 1477 | .es(7, 2.0f) // octave_lacunarity |
| 1478 | .es(8, 0.5f) // gully_depth |
| 1479 | .es(9, 1.5f) // fine_gully_slope_restriction |
| 1480 | .es(10, 0.5f) // gully_normalize_threshold |
| 1481 | .es(11, -0.65f) // height_offset |
| 1482 | .es(12, 0.0f) // height_offset_follows_altitude |
| 1483 | .es(13, MO[m]) // max_octaves |
| 1484 | .eoct(5) |
| 1485 | .eround(0.1f, 0.0f, 0.1f, 2.0f) |
| 1486 | .eonset(1.25f, 1.25f, 2.8f, 1.5f) |
| 1487 | .eslope(0.7f, 1.0f), 5e-5); |
| 1488 | } |
| 1489 | // _grad exists because reconstructing the gradient by central |
| 1490 | // differences cost six more full evaluations per edge -- 491 us |
| 1491 | // to 3795 us per 32^3 grid. It is a different function from |
| 1492 | // _full and has to be checked as one. |
| 1493 | for (uint32_t c = 0; c < 4; c++) { |
| 1494 | add("erosion_node_eval_grad", false, B(OP_ERO_EVAL_GRAD).comp(c) |
| 1495 | .ep(P[i][0], P[i][1]).en(GN[g][0], GN[g][1], GN[g][2]) |
| 1496 | .es(0, 5.0f).es(1, 0.16f).es(2, 1.0f).es(3, 0.5f) |
| 1497 | .es(4, 0.6f).es(5, 0.15f).es(6, 0.25f).es(7, 2.0f) |
| 1498 | .es(8, 0.5f).es(9, 1.5f).es(10, 0.5f) |
| 1499 | .es(11, -0.65f).es(12, 0.0f).es(13, MO[m]) |
| 1500 | .eoct(5) |
| 1501 | .eround(0.1f, 0.0f, 0.1f, 2.0f) |
| 1502 | .eonset(1.25f, 1.25f, 2.8f, 1.5f) |
| 1503 | .eslope(0.7f, 1.0f), 5e-5); |
| 1504 | } |
| 1505 | } |
| 1506 | } |
| 1507 | } |
| 1508 | } |
| 1509 | |
| 1049 | 1510 | static long peak_rss_kb() { |
| 1050 | 1511 | std::ifstream f("/proc/self/status"); |
| 1051 | 1512 | std::string line; |
| … | … | @@ -1094,6 +1555,7 @@ int main(int argc, char **argv) { |
| 1094 | 1555 | build_voronoi_cases(); |
| 1095 | 1556 | build_ramp_cases(); |
| 1096 | 1557 | build_simplex_cases(); |
| 1558 | build_erosion_cases(); |
| 1097 | 1559 | if (g_cases.empty()) { fprintf(stderr, "FATAL: no cases\n"); return 1; } |
| 1098 | 1560 | |
| 1099 | 1561 | int n_exact = 0; |
| … | … | @@ -1113,6 +1575,12 @@ int main(int argc, char **argv) { |
| 1113 | 1575 | snprintf(b, sizeof(b), "#define %s %uu\n", OP_NAMES[i].name, (unsigned)OP_NAMES[i].op); |
| 1114 | 1576 | defs += b; |
| 1115 | 1577 | } |
| 1578 | // And the erosion block's slots, for the same reason. |
| 1579 | for (size_t i = 0; i < sizeof(SLOT_NAMES) / sizeof(SLOT_NAMES[0]); i++) { |
| 1580 | snprintf(b, sizeof(b), "#define %s %uu\n", SLOT_NAMES[i].name, |
| 1581 | (unsigned)SLOT_NAMES[i].slot); |
| 1582 | defs += b; |
| 1583 | } |
| 1116 | 1584 | } |
| 1117 | 1585 | const std::string shader = defs + lib + DISPATCH_GLSL; |
| 1118 | 1586 | |
| … | … | @@ -1248,13 +1716,27 @@ int main(int argc, char **argv) { |
| 1248 | 1716 | if (!ok) bad_exact++; |
| 1249 | 1717 | } else { |
| 1250 | 1718 | const double a = float_of(k.cpu), b = float_of(got[i]); |
| 1719 | const bool bitident = (got[i] == k.cpu); |
| 1720 | if (bitident) n_bitident++; |
| 1721 | // IDENTICAL BITS ARE AGREEMENT, and that clause has to come first. |
| 1722 | // |
| 1723 | // Without it two identical INFINITIES read as a disagreement: |
| 1724 | // inf - inf is nan, and every comparison against nan is false, so |
| 1725 | // the arm below rejects a pair that is the same number. Found |
| 1726 | // 2026-09-06 by smooth_start(-0.5, 0.0), which divides by the |
| 1727 | // smoothing and produces +inf on both sides -- the gate printed |
| 1728 | // `cpu=inf gpu=inf FAIL`, which is a sentence about the comparator |
| 1729 | // and not about the filter. |
| 1251 | 1730 | const double diff = fabs(a - b); |
| 1252 | 1731 | const double scale = fabs(a) > 1.0 ? fabs(a) : 1.0; |
| 1253 | 1732 | const double rel = diff / scale; |
| 1254 | | if (got[i] == k.cpu) n_bitident++; |
| 1255 | | if (rel > worst_rel) { worst_rel = rel; worst_name = k.name; } |
| 1733 | // A non-finite rel must not become the reported worst: it would |
| 1734 | // print as the headline number for a pair that agrees. |
| 1735 | if (std::isfinite(rel) && rel > worst_rel) { |
| 1736 | worst_rel = rel; worst_name = k.name; |
| 1737 | } |
| 1256 | 1738 | const double abs_tol = (k.tol_abs > 0.0) ? k.tol_abs : TOL_ABS; |
| 1257 | | ok = (diff <= abs_tol) || (rel <= TOL_REL); |
| 1739 | ok = bitident || (diff <= abs_tol) || (rel <= TOL_REL); |
| 1258 | 1740 | } |
| 1259 | 1741 | if (!ok) { |
| 1260 | 1742 | bad++; |