OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / e87e2f1

simplex: the node was computing a different function on CPU than on GPU -- fixed
⛔ CORRECTION TO THE PREVIOUS COMMIT. It said the two simplex implementations "were wrong in the same way ... and they agreed", and that both returned ~±1e5. That was inferred, not measured, and it is wrong. Measured now, by recovering the deleted GLSL from git and running it against the CPU on the same points: scalar C++ (CPU) simplex(1.20, 0.36, 2.04) = 5557.13 vector GLSL (GPU) simplex(1.20, 0.36, 2.04) = -0.5551 The GPU was RIGHT. Only the CPU was broken, they never agreed, and the Simplex Noise node has been evaluating a different function depending on which machine ran it -- a mesh baked on the CPU and the same graph rendered on the GPU were different noise. Retiring the library made the GPU match the broken CPU, so the previous commit made that half worse for one commit. WHAT WAS ACTUALLY WRONG, and it is instructive: the C++ was a "scalarisation" of the vector Ashima snoise, and scalarising it cannot work. 1. THE GRADIENT CONSTANTS. `ns = n_ * D.wyz - D.xzx` with D = (0, .5, 1, 2) is (2/7, 0.5/7 - 1, 1/7) = (0.2857, -0.9286, 0.1429). The scalar copy used (2/7, -1.0, 0.5/7) -- two of the three wrong. 2. THE LANES CROSS. `b0 = vec4(x.xy, y.xy)` and `a0 = b0.xzyw + s0.xzyw * sh.xxyy` mix components of DIFFERENT corners into one vec4. A per-corner scalar rewrite has no way to express that, so the scalarisation was doomed rather than merely mistyped. proc_simplex.inc is now the VECTOR form -- the one that was right -- with every swizzle written out as an explicit constructor, because GLSL has swizzles and C++ does not. The shim grew floor/abs/min/max/step over vec3 and vec4 to carry it. range [-0.90, 0.90] at every magnitude, 1e5 included (was ~±1e5 at every magnitude, |p| ~ 1 included) .inc CPU vs the recovered CORRECT GLSL on the GPU 5300/5300 noise parity 5364/5364, eight-octave fbm coverage restored op parity 770/770 · golden 14/14 · terrain 1.000000 / 1.967e-07 ⚠ THIS CHANGES WHAT THE SIMPLEX NOISE NODE OUTPUTS on the CPU, and that is the point: its "Value" output is documented 0..1 and was returning 7611. Any graph relying on the old CPU numbers was relying on a bug, and was already getting different results from the GPU path. One explicit tolerance was added to the gate, and it is the only one in the file: an fbm sums eight octaves of magnitude ~0.9 into a result that can be ~0.01, so scaling the difference by the RESULT measures the cancellation rather than the agreement. 5e-05 absolute, from a measured worst of 1.6e-05 -- a number taken from a measurement, not chosen until the gate went green. THE LESSON IS THE SAME ONE, ONE LEVEL UP. I wrote that two copies agreeing cannot detect a shared error. The real failure here was smaller and worse: I asserted the two copies agreed WITHOUT COMPARING THEM, when comparing them was one gate invocation away and would have said the opposite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
e87e2f19f3aa20fdda8ec71ec7d9f7df9016ce17
by plankatron <fleeky@gmail.com> · 2026-09-06T01:12:36.000Z

Changed files (3) +177 -200

src/shared/blender_noise_cpp_shim.h +30 -0
……@@ -160,6 +160,36 @@ inline vec2 operator-(vec2 a, float s) { return vec2(a.x - s, a.y - s); }
160160inline vec3 operator-(vec3 a, float s) { return vec3(a.x - s, a.y - s, a.z - s); }
161161inline vec4 operator-(vec4 a, float s) { return vec4(a.x - s, a.y - s, a.z - s, a.w - s); }
162162
163inline vec3 floor(vec3 v) { return vec3(std::floor(v.x), std::floor(v.y), std::floor(v.z)); }
164inline vec4 floor(vec4 v) {
165 return vec4(std::floor(v.x), std::floor(v.y), std::floor(v.z), std::floor(v.w));
166}
167inline vec4 abs(vec4 v) {
168 return vec4(std::fabs(v.x), std::fabs(v.y), std::fabs(v.z), std::fabs(v.w));
169}
170inline vec3 min(vec3 a, vec3 b) {
171 return vec3(a.x < b.x ? a.x : b.x, a.y < b.y ? a.y : b.y, a.z < b.z ? a.z : b.z);
172}
173inline vec3 max(vec3 a, vec3 b) {
174 return vec3(a.x > b.x ? a.x : b.x, a.y > b.y ? a.y : b.y, a.z > b.z ? a.z : b.z);
175}
176inline vec4 max(vec4 a, vec4 b) {
177 return vec4(a.x > b.x ? a.x : b.x, a.y > b.y ? a.y : b.y,
178 a.z > b.z ? a.z : b.z, a.w > b.w ? a.w : b.w);
179}
180// GLSL step(edge, x) is x < edge ? 0 : 1 -- note the ARGUMENT ORDER, which is
181// the opposite way round from how it reads.
182inline float step(float edge, float x) { return x < edge ? 0.0f : 1.0f; }
183inline vec3 step(vec3 e, vec3 x) {
184 return vec3(step(e.x, x.x), step(e.y, x.y), step(e.z, x.z));
185}
186inline vec4 step(vec4 e, vec4 x) {
187 return vec4(step(e.x, x.x), step(e.y, x.y), step(e.z, x.z), step(e.w, x.w));
188}
189inline vec3 &operator*=(vec3 &a, float s) { a = a * s; return a; }
190inline vec4 operator-(float s, vec4 a) { return vec4(s - a.x, s - a.y, s - a.z, s - a.w); }
191inline vec3 operator-(float s, vec3 a) { return vec3(s - a.x, s - a.y, s - a.z); }
192
163193inline float dot(vec2 a, vec2 b) { return a.x * b.x + a.y * b.y; }
164194inline float dot(vec3 a, vec3 b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
165195inline float dot(vec4 a, vec4 b) { return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; }
src/shared/proc_simplex.inc +113 -171
……@@ -1,49 +1,42 @@
11// ============================================================================
22// ASHIMA SIMPLEX -- ONE TEXT, COMPILED AS BOTH GLSL AND C++.
33//
4// ⚠ THIS ONE IS NOT BLENDER'S. Blender's Noise Texture is Perlin
5// (blender_perlin.inc); this is the Ashima Arts polynomial-permutation simplex
6// that proc-nodes' own "Simplex Noise" node uses, and that blender-bridge
7// reproduces on the Blender side as the ProcNodes_SimplexNoise node group. It
8// lives here for the same reason the others do: it existed TWICE, as a
9// file-local C++ copy in noise_nodes.cpp and as a hand-written GLSL copy in
10// GpuCompiler's hand-written noise library, and nothing compared them.
4// ⚠ NOT BLENDER'S. Blender's Noise Texture is Perlin (blender_perlin.inc);
5// this is the Ashima Arts / webgl-noise `snoise` that proc-nodes' own Simplex
6// Noise node uses, and that blender-bridge mirrors as the
7// ProcNodes_SimplexNoise group.
118//
12// IEEE 754 deterministic: no lookup tables, no sin(), only multiply, add, mod,
13// floor and dot -- which is why the same text can run on both machines.
9// ---------------------------------------------------------------------------
10// ⛔ THIS FILE REPLACES A BROKEN SCALAR PORT, AND THE HISTORY IS THE POINT.
1411//
15// TRANSFORMED from the C++ that was in noise_nodes.cpp, not retyped. The
16// equivalence run against that original is what says the transformation was
17// faithful.
12// proc-nodes had TWO implementations: a hand-written GLSL copy of the vector
13// form below, and a file-local C++ "scalarisation" of it in noise_nodes.cpp.
14// They were never compared -- and they were not the same function:
1815//
19// ⛔ THIS FUNCTION IS BROKEN, AND IT WAS BROKEN BEFORE IT MOVED HERE.
16// scalar C++ (CPU) simplex(1.20, 0.36, 2.04) = 5557.13
17// vector GLSL (GPU) simplex(1.20, 0.36, 2.04) = -0.5551
2018//
21// It returns values in the THOUSANDS where a simplex must be in [-1, 1]:
19// The CPU was out of range by four orders of magnitude, so the node's Value
20// output -- documented 0..1 -- was in the thousands on the CPU and correct on
21// the GPU. Every mesh baked on the CPU had different noise from the same graph
22// rendered on the GPU, and nothing in the codebase could see it.
2223//
23// simplex(0.50, 0.15, 0.85) = -7349.26
24// simplex(1.20, 0.36, 2.04) = 5557.13
25// fbm, 4 octaves = 15221.4
24// TWO THINGS BROKE IN THE SCALARISATION, both visible above:
2625//
27// and SimplexNoiseNode maps that as n * 0.5 + 0.5, so its "Value" output --
28// documented 0..1 -- is 7611 for an ordinary point. Not a large-coordinate
29// precision effect: the range is ~±1e5 at EVERY magnitude tested, |p| ~ 1
30// included. Something in the port of Ashima's gradient or its 105.0 scaling is
31// wrong.
26// 1. THE GRADIENT CONSTANTS. `ns = n_ * D.wyz - D.xzx` with D = (0, .5, 1, 2)
27// is (2/7, 0.5/7 - 1, 1/7) = (0.2857, -0.9286, 0.1429). The scalar copy
28// used (2/7, -1.0, 0.5/7) -- two of the three wrong.
3229//
33// ⚠ NOT INTRODUCED BY THE MOVE. This text is bit-identical to the file-local
34// C++ it replaced -- 240000/240000 -- and the hand-written GLSL twin agreed
35// with it too, which is exactly why nobody noticed: both implementations were
36// wrong in the same way, and a bridge that compares them to each other cannot
37// see it. It took comparing against the DEFINITION of a simplex to find.
30// 2. THE LANE SHUFFLES CANNOT BE DONE PER CORNER. `b0 = vec4(x.xy, y.xy)`
31// and `a0 = b0.xzyw + s0.xzyw * sh.xxyy` mix the x and y components of
32// DIFFERENT corners into one vec4. A per-corner scalar rewrite has no way
33// to express that, which is why the scalarisation was doomed rather than
34// merely mistyped.
3835//
39// The parity gate therefore covers what it can honestly hold -- the raw
40// lattice and low octave counts, where CPU and GPU agree to 1e-5 -- and stops
41// short of pretending the accumulated fbm is meaningful. Fixing the arithmetic
42// changes what the node outputs and is its own change.
43//
44// THE SUBSET: the per-corner gradient was a LAMBDA with three reference
45// out-parameters. GLSL has neither, so it is a function returning a vec3; its
46// captures were constants derived from 1/7 and are recomputed inside it.
36// So this file is the VECTOR form -- the one that was right -- with every
37// swizzle written out as an explicit constructor, because GLSL has swizzles
38// and C++ does not. `vec3(x0.y, x0.z, x0.x)` is what `x0.yzx` means and both
39// languages accept it.
4740// ============================================================================
4841
4942#ifndef PN_PROC_SIMPLEX
……@@ -53,162 +46,111 @@
5346#define BN_FN
5447#endif
5548
56// --- Simplex noise implementation (3D) ---
57// Ashima Arts polynomial permutation — matches GPU (gpu_compiler.cpp) and
58// Blender GeoNodes ProcNodes_SimplexNoise group. IEEE 754 deterministic:
59// no lookup tables, no sin(), only multiply/add/mod/floor/dot.
49BN_FN vec3 ps_mod289_v3(vec3 x) {
50 return x - floor(x * (1.0f / 289.0f)) * 289.0f;
51}
6052
61BN_FN float ps_mod289(float x) {
53BN_FN vec4 ps_mod289_v4(vec4 x) {
6254 return x - floor(x * (1.0f / 289.0f)) * 289.0f;
6355}
6456
65BN_FN float ps_permute(float x) {
66 return ps_mod289(((x * 34.0f) + 10.0f) * x);
57BN_FN vec4 ps_permute(vec4 x) {
58 return ps_mod289_v4(((x * 34.0f) + 10.0f) * x);
6759}
6860
69BN_FN float ps_taylor_inv_sqrt(float r) {
61BN_FN vec4 ps_taylor_inv_sqrt(vec4 r) {
7062 return 1.79284291400159f - 0.85373472095314f * r;
7163}
7264
73BN_FN float ps_dot3(float ax, float ay, float az, float bx, float by, float bz) {
74 return ax * bx + ay * by + az * bz;
75}
65BN_FN float ps_simplex_noise_3d_v(vec3 v) {
66 // C = (1/6, 1/3), D = (0, 0.5, 1, 2) in the original; written out below
67 // wherever a swizzle of them was used.
68 float Cx = 1.0f / 6.0f;
69 float Cy = 1.0f / 3.0f;
7670
77BN_FN vec3 ps_grad_from_p(float pv) {
78 float n_ = 1.0f / 7.0f;
79 float ns_xx = 2.0f * n_;
80 float ns_yy = -1.0f;
81 float ns_zz = 0.5f * n_;
82 float gx; float gy; float gz;
83
84 float j = pv - 49.0f * floor(pv * ns_zz * ns_zz);
85 float x_ = floor(j * ns_zz);
86 float y_ = floor(j - 7.0f * x_);
87 float gxx = x_ * ns_xx + ns_yy;
88 float gyy = y_ * ns_xx + ns_yy;
89 float hh = 1.0f - abs(gxx) - abs(gyy);
90
91 // Sign correction
92 float sx = (gxx < 0.0f) ? -1.0f : 1.0f;
93 float sy = (gyy < 0.0f) ? -1.0f : 1.0f;
94 float b0x = gxx;
95 float b0y = gyy;
96 float sh = (hh < 0.0f) ? -1.0f : 0.0f;
97 gx = b0x + floor(b0x) * 2.0f * sh + sh;
98 // Simpler: match GLSL exactly
99 // a0 = b0.xzyw + s0.xzyw * sh.xxyy
100 // For a single corner, this simplifies to:
101 float s0x = floor(b0x) * 2.0f + 1.0f;
102 float s0y = floor(b0y) * 2.0f + 1.0f;
103 float shv = (hh < 0.0f) ? -1.0f : 0.0f;
104 gx = b0x + s0x * shv;
105 gy = b0y + s0y * shv;
106 gz = hh;
107
108 // Normalize
109 float len2 = gx * gx + gy * gy + gz * gz;
110 float inv_len = ps_taylor_inv_sqrt(len2);
111 gx *= inv_len;
112 gy *= inv_len;
113 gz *= inv_len;
114 return vec3(gx, gy, gz);
71 // First corner
72 vec3 i = floor(v + vec3(dot(v, vec3(Cy, Cy, Cy))));
73 vec3 x0 = v - i + vec3(dot(i, vec3(Cx, Cx, Cx)));
74
75 // Other corners
76 vec3 g = step(vec3(x0.y, x0.z, x0.x), vec3(x0.x, x0.y, x0.z));
77 vec3 l = 1.0f - g;
78 vec3 i1 = min(vec3(g.x, g.y, g.z), vec3(l.z, l.x, l.y));
79 vec3 i2 = max(vec3(g.x, g.y, g.z), vec3(l.z, l.x, l.y));
80
81 vec3 x1 = x0 - i1 + vec3(Cx, Cx, Cx);
82 vec3 x2 = x0 - i2 + vec3(Cy, Cy, Cy);
83 vec3 x3 = x0 - vec3(0.5f, 0.5f, 0.5f); // D.yyy
84
85 // Permutations
86 i = ps_mod289_v3(i);
87 vec4 p = ps_permute(ps_permute(ps_permute(
88 vec4(i.z, i.z, i.z, i.z) + vec4(0.0f, i1.z, i2.z, 1.0f))
89 + vec4(i.y, i.y, i.y, i.y) + vec4(0.0f, i1.y, i2.y, 1.0f))
90 + vec4(i.x, i.x, i.x, i.x) + vec4(0.0f, i1.x, i2.x, 1.0f));
91
92 // Gradients: 7x7 points over a square, mapped onto an octahedron.
93 // ⛔ ns = n_ * D.wyz - D.xzx = n_ * (2, 0.5, 1) - (0, 1, 0). The scalar
94 // port that this file replaces got two of these three wrong.
95 float n_ = 0.142857142857f; // 1/7
96 vec3 ns = vec3(n_ * 2.0f - 0.0f, n_ * 0.5f - 1.0f, n_ * 1.0f - 0.0f);
97
98 vec4 j = p - 49.0f * floor(p * ns.z * ns.z);
99
100 vec4 x_ = floor(j * ns.z);
101 vec4 y_ = floor(j - 7.0f * x_);
102
103 vec4 x = x_ * ns.x + vec4(ns.y, ns.y, ns.y, ns.y);
104 vec4 y = y_ * ns.x + vec4(ns.y, ns.y, ns.y, ns.y);
105 vec4 h = 1.0f - abs(x) - abs(y);
106
107 // ⛔ THE LANES CROSS HERE. b0 holds x.xy and y.xy -- two components of one
108 // corner pair and two of another -- and a0 then shuffles them again. This
109 // is what a per-corner scalarisation cannot represent.
110 vec4 b0 = vec4(x.x, x.y, y.x, y.y);
111 vec4 b1 = vec4(x.z, x.w, y.z, y.w);
112
113 vec4 s0 = floor(b0) * 2.0f + 1.0f;
114 vec4 s1 = floor(b1) * 2.0f + 1.0f;
115 vec4 sh = -step(h, vec4(0.0f));
116
117 vec4 a0 = vec4(b0.x, b0.z, b0.y, b0.w) +
118 vec4(s0.x, s0.z, s0.y, s0.w) * vec4(sh.x, sh.x, sh.y, sh.y);
119 vec4 a1 = vec4(b1.x, b1.z, b1.y, b1.w) +
120 vec4(s1.x, s1.z, s1.y, s1.w) * vec4(sh.z, sh.z, sh.w, sh.w);
121
122 vec3 p0 = vec3(a0.x, a0.y, h.x);
123 vec3 p1 = vec3(a0.z, a0.w, h.y);
124 vec3 p2 = vec3(a1.x, a1.y, h.z);
125 vec3 p3 = vec3(a1.z, a1.w, h.w);
126
127 // Normalise gradients
128 vec4 norm = ps_taylor_inv_sqrt(vec4(dot(p0, p0), dot(p1, p1), dot(p2, p2), dot(p3, p3)));
129 p0 *= norm.x;
130 p1 *= norm.y;
131 p2 *= norm.z;
132 p3 *= norm.w;
133
134 // Mix contributions from the four corners
135 vec4 m = max(0.5f - vec4(dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), vec4(0.0f));
136 m = m * m;
137 return 105.0f * dot(m * m, vec4(dot(p0, x0), dot(p1, x1), dot(p2, x2), dot(p3, x3)));
115138}
116139
117BN_FN float ps_simplex_noise_3d(float xin, float yin, float zin) {
118 float C1 = 1.0f / 6.0f; // G3
119 float C2 = 1.0f / 3.0f; // F3
120
121 // First corner
122 float s = (xin + yin + zin) * C2;
123 float ix = floor(xin + s);
124 float iy = floor(yin + s);
125 float iz = floor(zin + s);
126
127 float t = (ix + iy + iz) * C1;
128 float x0 = xin - ix + t;
129 float y0 = yin - iy + t;
130 float z0 = zin - iz + t;
131
132 // Simplex traversal (branchless Ashima step/min/max)
133 float gx = (x0 >= y0) ? 1.0f : 0.0f; // step(y0, x0)
134 float gy = (y0 >= z0) ? 1.0f : 0.0f; // step(z0, y0)
135 float gz = (z0 >= x0) ? 1.0f : 0.0f; // step(x0, z0)
136 float lx = 1.0f - gx;
137 float ly = 1.0f - gy;
138 float lz = 1.0f - gz;
139
140 float i1x = min(gx, lz);
141 float i1y = min(gy, lx);
142 float i1z = min(gz, ly);
143 float i2x = max(gx, lz);
144 float i2y = max(gy, lx);
145 float i2z = max(gz, ly);
146
147 float x1 = x0 - i1x + C1;
148 float y1 = y0 - i1y + C1;
149 float z1 = z0 - i1z + C1;
150 float x2 = x0 - i2x + C2;
151 float y2 = y0 - i2y + C2;
152 float z2 = z0 - i2z + C2;
153 float x3 = x0 - 0.5f;
154 float y3 = y0 - 0.5f;
155 float z3 = z0 - 0.5f;
156
157 // Permutations (polynomial, no lookup table)
158 ix = ps_mod289(ix);
159 iy = ps_mod289(iy);
160 iz = ps_mod289(iz);
161
162 float p0 = ps_permute(ps_permute(ps_permute(iz) + iy) + ix);
163 float p1 = ps_permute(ps_permute(ps_permute(iz + i1z) + iy + i1y) + ix + i1x);
164 float p2 = ps_permute(ps_permute(ps_permute(iz + i2z) + iy + i2y) + ix + i2x);
165 float p3 = ps_permute(ps_permute(ps_permute(iz + 1.0f) + iy + 1.0f) + ix + 1.0f);
166
167 // Gradients: 7x7 points over octahedron
168 float n_ = 1.0f / 7.0f;
169 // ns = n_ * D.wyz - D.xzx where D = (0, 0.5, 1, 2)
170 float ns_x = n_ * 2.0f - 0.0f; // 2/7
171 float ns_y = n_ * 0.5f - 1.0f; // -0.928571...
172 float ns_z = n_ * 0.0f - 0.0f; // 0 -- wait, let me recalculate
173 // D.wyz = (2.0, 0.0, 0.5), D.xzx = (0.0, 1.0, 0.0)
174 // ns = n_ * (2.0, 0.0, 0.5) - (0.0, 1.0, 0.0)
175 // ns = (2/7, -1, 1/14) = (0.285714, -1.0, 0.071428)
176 float ns_xx = 2.0f * n_; // 0.285714
177 float ns_yy = -1.0f; // -1.0
178 float ns_zz = 0.5f * n_; // 0.071428
179
180 // For each corner, compute gradient from permutation value
181
182 float g0x, g0y, g0z, g1x, g1y, g1z, g2x, g2y, g2z, g3x, g3y, g3z;
183 vec3 p0_v = ps_grad_from_p(p0); g0x = p0_v.x; g0y = p0_v.y; g0z = p0_v.z;
184 vec3 p1_v = ps_grad_from_p(p1); g1x = p1_v.x; g1y = p1_v.y; g1z = p1_v.z;
185 vec3 p2_v = ps_grad_from_p(p2); g2x = p2_v.x; g2y = p2_v.y; g2z = p2_v.z;
186 vec3 p3_v = ps_grad_from_p(p3); g3x = p3_v.x; g3y = p3_v.y; g3z = p3_v.z;
187
188 // Mix contributions from four corners
189 float m0 = max(0.5f - ps_dot3(x0, y0, z0, x0, y0, z0), 0.0f);
190 float m1 = max(0.5f - ps_dot3(x1, y1, z1, x1, y1, z1), 0.0f);
191 float m2 = max(0.5f - ps_dot3(x2, y2, z2, x2, y2, z2), 0.0f);
192 float m3 = max(0.5f - ps_dot3(x3, y3, z3, x3, y3, z3), 0.0f);
193
194 m0 *= m0; m1 *= m1; m2 *= m2; m3 *= m3;
195
196 return 105.0f * (
197 m0 * m0 * ps_dot3(g0x, g0y, g0z, x0, y0, z0) +
198 m1 * m1 * ps_dot3(g1x, g1y, g1z, x1, y1, z1) +
199 m2 * m2 * ps_dot3(g2x, g2y, g2z, x2, y2, z2) +
200 m3 * m3 * ps_dot3(g3x, g3y, g3z, x3, y3, z3));
140// The scalar entry point the nodes call, so the call sites did not change.
141BN_FN float ps_simplex_noise_3d(float x, float y, float z) {
142 return ps_simplex_noise_3d_v(vec3(x, y, z));
201143}
202144
203// fBM (fractal Brownian motion) using simplex noise
204BN_FN float ps_fbm_noise_3d(float x, float y, float z, int octaves, float roughness, float lacunarity) {
145BN_FN float ps_fbm_noise_3d(float x, float y, float z, int octaves, float roughness,
146 float lacunarity) {
205147 float value = 0.0f;
206148 float amplitude = 1.0f;
207149 float frequency = 1.0f;
208150 float max_amp = 0.0f;
209151
210152 for (int i = 0; i < octaves; i++) {
211 value += amplitude * ps_simplex_noise_3d(x * frequency, y * frequency, z * frequency);
153 value += amplitude * ps_simplex_noise_3d_v(vec3(x, y, z) * frequency);
212154 max_amp += amplitude;
213155 amplitude *= roughness;
214156 frequency *= lacunarity;
tests/test_shared_noise_parity.cpp +34 -29
……@@ -347,6 +347,7 @@ struct Rec {
347347 uint32_t w[STRIDE];
348348 std::string name;
349349 bool exact;
350 double tol_abs = 0.0;
350351 uint32_t cpu;
351352};
352353
……@@ -751,11 +752,12 @@ struct B {
751752
752753static std::vector<Rec> g_cases;
753754
754static void add(const std::string &name, bool exact, const B &b) {
755static void add(const std::string &name, bool exact, const B &b, double tol_abs = 0.0) {
755756 Rec r;
756757 memcpy(r.w, b.w, sizeof(r.w));
757758 r.name = name;
758759 r.exact = exact;
760 r.tol_abs = tol_abs;
759761 r.cpu = eval_cpu(r.w);
760762 g_cases.push_back(r);
761763}
……@@ -1008,37 +1010,39 @@ static void build_simplex_cases() {
10081010 { -7.25f, 13.75f, -0.5f },
10091011 { 123.456f, -78.9f, 0.001f },
10101012 };
1011 // ⚠ The coordinates stop at ~123 for the same reason the octave loop is
1012 // not here at all: at |p| = 250 even a SINGLE evaluation parts company at
1013 // 4.7e-05, because the values being differenced are ~1e5 rather than ~1.
1014 // A function four orders of magnitude out of range has no last bits worth
1015 // comparing, and widening the tolerance until it passes would be reporting
1016 // agreement rather than measuring it.
1017 // ⚠ NOT gated beyond this magnitude, and the reason is a property of the
1018 // algorithm rather than of the port. Ashima's polynomial permutation
1019 // (multiply by 34, add 10, mod 289) loses float precision as the effective
1020 // coordinate -- position x frequency -- grows, and the SHIPPED simplex
1021 // stops being bounded: measured, ps_fbm_noise_3d at |p| = 1e3 with 8
1022 // octaves returns 3281 where simplex should be in [-1, 1], and at 1e5 it
1023 // returns 20714. The CPU and the GPU still AGREE there to 6.8e-5, so this
1024 // is not two implementations diverging -- it is one implementation outside
1025 // its usable range on both machines, which no tolerance should paper over.
1013 // ⚠ Coordinates stop at 250, which with eight octaves is an effective
1014 // magnitude of ~3e4. Beyond roughly 1e6 the two machines part company by
1015 // ~1.3e-03: Ashima's permutation (multiply by 34, add 10, mod 289) runs
1016 // out of float precision there and each rounds its own way. That is a
1017 // property of the algorithm in float32, not a divergence between the CPU
1018 // and the GPU texts -- they are the same text -- so it is recorded rather
1019 // than tolerated.
10261020 const int NSC = (int)(sizeof(SC) / sizeof(SC[0]));
10271021 for (int i = 0; i < NSC; i++) {
10281022 const float x = SC[i][0], y = SC[i][1], z = SC[i][2];
10291023 add("ps_simplex_noise_3d", false, B(OP_SIMPLEX).f(x, y, z, 0.0f));
1030 // ⛔ THE OCTAVE LOOP IS NOT GATED, and the reason is not the GPU.
1031 // ps_simplex_noise_3d returns ~±1e5 instead of [-1, 1] (see
1032 // proc_simplex.inc), so an fbm sum is an accumulation of values four
1033 // orders of magnitude too large, and two float machines summing that
1034 // part company at ~6.5e-05 relative however few octaves you use --
1035 // measured at 4, 6 and 8. Holding it to a tolerance would be choosing
1036 // a number that makes a broken function look agreed-upon.
1037 //
1038 // What IS gated is the raw lattice above: one evaluation, where the
1039 // two machines agree to 1e-5 and the question "is this the same
1040 // function on both sides" has a real answer. The fbm goes back in when
1041 // the arithmetic is fixed.
1024 // The octave loop is back. It was withdrawn while the CPU side was the
1025 // broken scalar port -- which returned ~±1e5 instead of [-1, 1], so an
1026 // fbm sum was an accumulation of nonsense whose last bits no tolerance
1027 // should have been asked to cover. With the vector form in place the
1028 // function is bounded again and the sum means something.
1029 // ⚠ AN EXPLICIT ABSOLUTE TOLERANCE, and this is the one place in the
1030 // file with one. An fbm sums N octaves of magnitude ~0.9 into a result
1031 // that can be ~0.01 -- the terms largely cancel -- so scaling the
1032 // difference by the RESULT measures the cancellation rather than the
1033 // agreement, and Ashima's permutation loses integer exactness in
1034 // float32 as the effective coordinate grows besides. Measured worst
1035 // over this set: 1.6e-05 absolute at eight octaves. 5e-05 is that with
1036 // headroom, and it is a number taken from a measurement rather than
1037 // one chosen until the gate went green.
1038 for (int oct = 1; oct <= 8; oct++) {
1039 for (int k = 0; k < 2; k++) {
1040 const float rough = k ? 0.75f : 0.5f;
1041 const float lac = k ? 1.75f : 2.0f;
1042 add("ps_fbm_noise_3d", false, B(OP_SIMPLEX_FBM).type(oct)
1043 .f(x, y, z, 0.0f).frac(0.0f, rough, lac, 0.0f, 0.0f, 0.0f), 5e-5);
1044 }
1045 }
10421046 }
10431047}
10441048
……@@ -1249,7 +1253,8 @@ int main(int argc, char **argv) {
12491253 const double rel = diff / scale;
12501254 if (got[i] == k.cpu) n_bitident++;
12511255 if (rel > worst_rel) { worst_rel = rel; worst_name = k.name; }
1252 ok = (diff <= TOL_ABS) || (rel <= TOL_REL);
1256 const double abs_tol = (k.tol_abs > 0.0) ? k.tol_abs : TOL_ABS;
1257 ok = (diff <= abs_tol) || (rel <= TOL_REL);
12531258 }
12541259 if (!ok) {
12551260 bad++;