OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / 3dd5989

hash: the layer becomes ONE text, compiled as both GLSL and C++
The .inc route, chosen over hand-written GLSL because the hash is the one place a second implementation cannot be seen from above. Everything inherits it -- Voronoi's cell offsets, Perlin's lattice gradients, and the whole of White Noise -- so a disagreement looks like the structure rather than the hash. src/shared/blender_hash.inc is now the only implementation, compiled the same three ways erosion_filter.inc is: as C++ through a shim, as GLSL through a generated header, and by a gate that does both and compares. blender_noise.h is no longer an implementation, it is the godot-typed face of the .inc -- 289 lines to 179, all of them conversion. Every name and signature it had is still there, so blender_perlin.h, blender_voronoi.h, noise_nodes.cpp and selection_nodes.cpp did not change. WHAT WAS MEASURED, in the order it was measured. the two Jenkins hashes agreed 22/22 exact, before any of this .inc vs the old header 13,000,000/13,000,000 bit-exact adapter vs the old header 13,400,000/13,400,000 bit-exact CPU vs GPU, every family 1056/1056 exact, no epsilon emitted golden shader 12/12, byte-identical emitted-op parity 658/658 white noise / voronoi / noise texture / random value / gradient noise 8+31+15+62+11 = 127/127 The old header was restored from git under a second namespace to get the middle two, because a refactor's only real question is whether the numbers moved, and there is exactly one moment when both texts exist to be asked. Every one of those was then mutated and re-run. The equivalence harness discriminates -- one rotate constant takes it to 5,400,001/13,000,000, and the survivors are precisely the PCG family, which does not go through Jenkins. A swapped argument in one adapter forwarder fails hash_to_float2 and nothing else. The gate's float path fails 254 cases when the divisor changes. TWO THINGS I GOT WRONG, both caught by measurement rather than review. A C++ RAW-STRING DELIMITER MAY BE AT MOST 16 CHARACTERS. Generalising gen_erosion_glsl.py, I derived the delimiter from the symbol, and EROSION_FILTER_GLSL is 19. The generated erosion header stopped being valid C++ and the compiler reported an incomplete RDShaderSource 700 lines away, in an unrelated header, in a file I had not touched -- so the visible error named neither the cause nor even the right file. I read the tail of the build log first and started reasoning about godot-cpp; the first error said it plainly. The delimiter is a short constant now and the symbol stays descriptive. THE LITERAL PRECAUTION IS NOT LOAD-BEARING HERE, and the .inc says so. `1.0` is a float in GLSL and a double in C++, so a reciprocal can round differently -- real, and the reason bn_int_to_float_01 writes `float(1)`. But writing `1.0` gives the same 1056/1056, because float(0x7fffffff) is 2147483648.0 = 2^31 (2147483647 has no float32 spelling) and a power of two has an exact reciprocal. Claiming the precaution fixed a divergence would have been a number I did not have. The first mutation I tried to prove it with, 0x7ffffffe, was a NO-OP for the same reason -- both round to 2^31 -- and would have been recorded as a passing gate meaning nothing. The gate compares FLOAT results as bit patterns and float INPUTS cross as uintBitsToFloat of an exact word, never as decimal literals -- otherwise it would be measuring whether glslc and g++ round "3.14159265" the same way. There is deliberately no epsilon in the file: every float here comes from an integer conversion, a division and a multiply, all correctly rounded by IEEE 754 on both sides, so a near-miss is a finding and not noise. tests/stub_include is gone, as promised when it went in: the gate's CPU side is the shim now, so it needs no godot types to stand in for. .gitignore takes a wildcard, because the exact-path rule covered only the erosion header and would have quietly committed the new one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3dd5989809f5b7add37b3ebab0d94de59c054940
by plankatron <fleeky@gmail.com> · 2026-09-05T23:08:06.000Z

Changed files (11) +906 -380

.gitignore +7 -4
……@@ -16,10 +16,13 @@ src/gen/
1616*.swp
1717*~
1818
19# Generated from src/shared/erosion_filter.inc by tools/gen_erosion_glsl.py at
20# build time. Not checked in on purpose: a committed copy of the erosion filter
21# is a copy, and copies of it are what shipped a 60x GPU/CPU divergence.
22src/gpu/erosion_filter_glsl.gen.h
19# Generated at build time from the dual-compiled .inc files in src/shared/ by
20# tools/gen_glsl_inc.py. Not checked in on purpose: a committed copy of one of
21# these algorithms is a copy, and copies of the erosion filter are what shipped
22# a 60x GPU/CPU divergence. A wildcard rather than a path, because the hash
23# layer became the second one on 2026-09-05 and an exact-path rule would have
24# quietly committed it.
25src/gpu/*.gen.h
2326
2427# Godot import artifacts from running the demo project headlessly
2528demo/.godot/
SConstruct +17 -6
……@@ -43,11 +43,22 @@ env.Append(CPPPATH=["src/", "src/shared/"])
4343# the header is the point — a checked-in copy is a copy, and three copies of
4444# this algorithm is exactly what shipped a 60x divergence between the meshed
4545# surface and the collidable one.
46erosion_glsl_gen = env.Command(
47 "src/gpu/erosion_filter_glsl.gen.h",
48 "src/shared/erosion_filter.inc",
49 "python3 tools/gen_erosion_glsl.py $SOURCE $TARGET",
50)
46glsl_inc_gen = [
47 env.Command(
48 "src/gpu/erosion_filter_glsl.gen.h",
49 "src/shared/erosion_filter.inc",
50 "python3 tools/gen_glsl_inc.py $SOURCE $TARGET EROSION_FILTER_GLSL",
51 ),
52 # The hash layer, second .inc since 2026-09-05. Voronoi's cell offsets,
53 # Perlin's lattice gradients and the whole of White Noise are this text,
54 # so a CPU copy and a GPU copy agreeing by review is the one thing that
55 # cannot be allowed here.
56 env.Command(
57 "src/gpu/blender_hash_glsl.gen.h",
58 "src/shared/blender_hash.inc",
59 "python3 tools/gen_glsl_inc.py $SOURCE $TARGET BLENDER_HASH_GLSL",
60 ),
61]
5162
5263sources = (
5364 Glob("src/*.cpp")
……@@ -64,7 +75,7 @@ if env["target"] in ["editor", "template_debug"]:
6475# Every TU may pull in the generated header transitively; make the dependency
6576# explicit rather than relying on scan order for a file that may not exist yet
6677# on a clean checkout.
67env.Depends(sources, erosion_glsl_gen)
78env.Depends(sources, glsl_inc_gen)
6879
6980if env["target"] in ["editor", "template_debug"]:
7081 try:
src/shared/blender_hash.inc +301 -0
……@@ -0,0 +1,301 @@
1// ============================================================================
2// BLENDER'S HASH LAYER -- ONE TEXT, COMPILED AS BOTH GLSL AND C++.
3//
4// Ported from `source/blender/blenlib/intern/noise.cc` in the Blender tree at
5// /storage/projects/code/blender-source, commit 61a36e08, BLENDER_VERSION 502.
6// Structure and order follow that file deliberately, so the two can be diffed
7// by eye when Blender moves. Names follow Blender's with a `bn_` prefix,
8// because GLSL has no namespaces and a bare `hash` in the global scope of a
9// concatenated density shader is an accident waiting for a second prelude.
10//
11// WHY IT IS ONE TEXT. Everything above this file inherits it: Voronoi's cell
12// offsets are this hash, Perlin's lattice gradients are this hash, and
13// Blender's White Noise Texture node is this hash and nothing else. A CPU copy
14// and a GPU copy that agree by review are how ProcNodes_Hash2D shipped an iq
15// float hash against an integer LCG -- the right shape, unrelated numbers, and
16// an erosion chain above it tuned against a correlation that could not
17// converge. There is no version of that failure available here, because there
18// is only one implementation.
19//
20// It compiles three ways, exactly as erosion_filter.inc does:
21// C++ blender_noise_cpp_shim.h supplies the GLSL types and builtins
22// GPU src/gpu/blender_hash_glsl.gen.h, GENERATED at build time
23// gate tests/test_hash_parity.cpp compiles both and compares uint-exactly
24//
25// ---------------------------------------------------------------------------
26// THE SUBSET, AND THE TWO WAYS OUT OF IT
27//
28// This file may only use what GLSL and C++ spell identically. Two constructs
29// in the original C++ are outside that subset and are written differently
30// here. Both changes are shape, not arithmetic:
31//
32// 1. C++ REFERENCES. hash_bit_mix/final took `uint32_t &`. GLSL's `inout` is
33// not the same thing, and erosion_filter.inc records the same decision:
34// they take and RETURN a uvec3 instead.
35//
36// 2. THE BIT CAST. C++ needs memcpy (a union or reinterpret_cast is UB);
37// GLSL has floatBitsToUint. The shim supplies floatBitsToUint over the
38// memcpy, so this file names the GLSL builtin.
39//
40// ⚠ A BARE DECIMAL LITERAL IS A HAZARD IN THIS FILE. In GLSL `1.0` is a
41// float; in C++ it is a DOUBLE, so `1.0 / float(x)` divides in double on one
42// side and in float on the other, and the two results need not round to the
43// same float. 1.0 and 2.0 survive it -- they are exactly representable, so
44// narrowing them is exact -- but a reciprocal like 1/2147483647 does not.
45// That is why bn_int_to_float_01 below writes `float(1)` and not `1.0`.
46// ============================================================================
47
48// The C++ side defines this as `inline`; GLSL leaves it empty. blender_noise.h
49// reaches more than one translation unit, so without it the second one is a
50// multiple-definition link error -- the trap erosion_cpp_shim.h already has,
51// and the reason its debug_* statics can only live in noise_nodes.cpp.
52#ifndef BN_FN
53#define BN_FN
54#endif
55
56// --- Jenkins Lookup3, https://burtleburtle.net/bob/c/lookup3.c ---
57
58BN_FN uint bn_hash_bit_rotate(uint x, uint k) {
59 return (x << k) | (x >> (32u - k));
60}
61
62BN_FN uvec3 bn_hash_bit_mix(uvec3 v) {
63 uint a = v.x, b = v.y, c = v.z;
64 a -= c; a ^= bn_hash_bit_rotate(c, 4u); c += b;
65 b -= a; b ^= bn_hash_bit_rotate(a, 6u); a += c;
66 c -= b; c ^= bn_hash_bit_rotate(b, 8u); b += a;
67 a -= c; a ^= bn_hash_bit_rotate(c, 16u); c += b;
68 b -= a; b ^= bn_hash_bit_rotate(a, 19u); a += c;
69 c -= b; c ^= bn_hash_bit_rotate(b, 4u); b += a;
70 return uvec3(a, b, c);
71}
72
73BN_FN uvec3 bn_hash_bit_final(uvec3 v) {
74 uint a = v.x, b = v.y, c = v.z;
75 c ^= b; c -= bn_hash_bit_rotate(b, 14u);
76 a ^= c; a -= bn_hash_bit_rotate(c, 11u);
77 b ^= a; b -= bn_hash_bit_rotate(a, 25u);
78 c ^= b; c -= bn_hash_bit_rotate(b, 16u);
79 a ^= c; a -= bn_hash_bit_rotate(c, 4u);
80 b ^= a; b -= bn_hash_bit_rotate(a, 14u);
81 c ^= b; c -= bn_hash_bit_rotate(b, 24u);
82 return uvec3(a, b, c);
83}
84
85BN_FN uint bn_hash(uint kx) {
86 uvec3 v = uvec3(0xdeadbeefu + (1u << 2u) + 13u);
87 v.x += kx;
88 v = bn_hash_bit_final(v);
89 return v.z;
90}
91
92BN_FN uint bn_hash(uint kx, uint ky) {
93 uvec3 v = uvec3(0xdeadbeefu + (2u << 2u) + 13u);
94 v.y += ky;
95 v.x += kx;
96 v = bn_hash_bit_final(v);
97 return v.z;
98}
99
100BN_FN uint bn_hash(uint kx, uint ky, uint kz) {
101 uvec3 v = uvec3(0xdeadbeefu + (3u << 2u) + 13u);
102 v.z += kz;
103 v.y += ky;
104 v.x += kx;
105 v = bn_hash_bit_final(v);
106 return v.z;
107}
108
109BN_FN uint bn_hash(uint kx, uint ky, uint kz, uint kw) {
110 uvec3 v = uvec3(0xdeadbeefu + (4u << 2u) + 13u);
111 v.x += kx;
112 v.y += ky;
113 v.z += kz;
114 v = bn_hash_bit_mix(v);
115 v.x += kw;
116 v = bn_hash_bit_final(v);
117 return v.z;
118}
119
120// The hash is over the float's BIT PATTERN, not its value.
121BN_FN uint bn_hash_float(float k) { return bn_hash(floatBitsToUint(k)); }
122BN_FN uint bn_hash_float(vec2 k) {
123 return bn_hash(floatBitsToUint(k.x), floatBitsToUint(k.y));
124}
125BN_FN uint bn_hash_float(vec3 k) {
126 return bn_hash(floatBitsToUint(k.x), floatBitsToUint(k.y), floatBitsToUint(k.z));
127}
128BN_FN uint bn_hash_float(vec4 k) {
129 return bn_hash(floatBitsToUint(k.x), floatBitsToUint(k.y), floatBitsToUint(k.z),
130 floatBitsToUint(k.w));
131}
132
133BN_FN float bn_uint_to_float_01(uint k) {
134 return float(k) / float(0xFFFFFFFFu);
135}
136
137// Hashing INTEGERS into [0, 1] -- Blender's `hash_to_float`, as against the
138// `hash_float_to_float` family below which hashes the BIT PATTERNS of floats.
139// Random Value uses these: its inputs are an integer id and an integer seed,
140// not coordinates.
141BN_FN float bn_hash_to_float(uint kx) { return bn_uint_to_float_01(bn_hash(kx)); }
142BN_FN float bn_hash_to_float(uint kx, uint ky) {
143 return bn_uint_to_float_01(bn_hash(kx, ky));
144}
145BN_FN float bn_hash_to_float(uint kx, uint ky, uint kz) {
146 return bn_uint_to_float_01(bn_hash(kx, ky, kz));
147}
148BN_FN float bn_hash_to_float(uint kx, uint ky, uint kz, uint kw) {
149 return bn_uint_to_float_01(bn_hash(kx, ky, kz, kw));
150}
151
152BN_FN float bn_hash_float_to_float(float k) { return bn_uint_to_float_01(bn_hash_float(k)); }
153BN_FN float bn_hash_float_to_float(vec2 k) { return bn_uint_to_float_01(bn_hash_float(k)); }
154BN_FN float bn_hash_float_to_float(vec3 k) { return bn_uint_to_float_01(bn_hash_float(k)); }
155BN_FN float bn_hash_float_to_float(vec4 k) { return bn_uint_to_float_01(bn_hash_float(k)); }
156
157BN_FN vec2 bn_hash_float_to_float2(vec2 k) {
158 return vec2(bn_hash_float_to_float(k),
159 bn_hash_float_to_float(vec3(k.x, k.y, 1.0)));
160}
161BN_FN vec2 bn_hash_float_to_float2(vec3 k) {
162 return vec2(bn_hash_float_to_float(vec3(k.x, k.y, k.z)),
163 bn_hash_float_to_float(vec3(k.z, k.x, k.y)));
164}
165BN_FN vec2 bn_hash_float_to_float2(vec4 k) {
166 return vec2(bn_hash_float_to_float(vec4(k.x, k.y, k.z, k.w)),
167 bn_hash_float_to_float(vec4(k.z, k.x, k.w, k.y)));
168}
169
170BN_FN vec3 bn_hash_float_to_float3(float k) {
171 return vec3(bn_hash_float_to_float(k),
172 bn_hash_float_to_float(vec2(k, 1.0)),
173 bn_hash_float_to_float(vec2(k, 2.0)));
174}
175BN_FN vec3 bn_hash_float_to_float3(vec2 k) {
176 return vec3(bn_hash_float_to_float(k),
177 bn_hash_float_to_float(vec3(k.x, k.y, 1.0)),
178 bn_hash_float_to_float(vec3(k.x, k.y, 2.0)));
179}
180BN_FN vec3 bn_hash_float_to_float3(vec3 k) {
181 return vec3(bn_hash_float_to_float(k),
182 bn_hash_float_to_float(vec4(k.x, k.y, k.z, 1.0)),
183 bn_hash_float_to_float(vec4(k.x, k.y, k.z, 2.0)));
184}
185BN_FN vec3 bn_hash_float_to_float3(vec4 k) {
186 return vec3(bn_hash_float_to_float(k),
187 bn_hash_float_to_float(vec4(k.z, k.x, k.w, k.y)),
188 bn_hash_float_to_float(vec4(k.w, k.z, k.y, k.x)));
189}
190
191BN_FN vec4 bn_hash_float_to_float4(vec4 k) {
192 return vec4(bn_hash_float_to_float(k),
193 bn_hash_float_to_float(vec4(k.w, k.x, k.y, k.z)),
194 bn_hash_float_to_float(vec4(k.z, k.w, k.x, k.y)),
195 bn_hash_float_to_float(vec4(k.y, k.z, k.w, k.x)));
196}
197
198// --- PCG integer hashes ---
199//
200// "Hash Functions for GPU Rendering", JCGT 2020, https://jcgt.org/published/0009/03/02/
201// Blender's variants are modified to use SIGNED integers only, so they can also
202// be written in OSL, and the shift is therefore ARITHMETIC.
203//
204// VORONOI USES THESE, NOT THE JENKINS FLOAT HASH ABOVE -- except in 1D, which
205// uses hash_float_to_float. Reading that off the source rather than assuming
206// one hash per file is the difference between a port that matches and one that
207// is the right shape with unrelated numbers.
208//
209// The multiplies and adds are done through uint and reinterpreted: signed
210// overflow is undefined behaviour in C++, and GLSL leaves it undefined too, so
211// neither language may be trusted to wrap a signed int. Both define the
212// unsigned wrap.
213
214BN_FN int bn_wrap_mul(int a, int b) {
215 return int(uint(a) * uint(b));
216}
217BN_FN int bn_wrap_add(int a, int b) {
218 return int(uint(a) + uint(b));
219}
220
221BN_FN ivec2 bn_hash_pcg2d_i(ivec2 v) {
222 v.x = bn_wrap_add(bn_wrap_mul(v.x, 1664525), 1013904223);
223 v.y = bn_wrap_add(bn_wrap_mul(v.y, 1664525), 1013904223);
224 v.x = bn_wrap_add(v.x, bn_wrap_mul(v.y, 1664525));
225 v.y = bn_wrap_add(v.y, bn_wrap_mul(v.x, 1664525));
226 v.x ^= v.x >> 16;
227 v.y ^= v.y >> 16;
228 v.x = bn_wrap_add(v.x, bn_wrap_mul(v.y, 1664525));
229 v.y = bn_wrap_add(v.y, bn_wrap_mul(v.x, 1664525));
230 return v;
231}
232
233BN_FN ivec3 bn_hash_pcg3d_i(ivec3 v) {
234 v.x = bn_wrap_add(bn_wrap_mul(v.x, 1664525), 1013904223);
235 v.y = bn_wrap_add(bn_wrap_mul(v.y, 1664525), 1013904223);
236 v.z = bn_wrap_add(bn_wrap_mul(v.z, 1664525), 1013904223);
237 v.x = bn_wrap_add(v.x, bn_wrap_mul(v.y, v.z));
238 v.y = bn_wrap_add(v.y, bn_wrap_mul(v.z, v.x));
239 v.z = bn_wrap_add(v.z, bn_wrap_mul(v.x, v.y));
240 v.x ^= v.x >> 16;
241 v.y ^= v.y >> 16;
242 v.z ^= v.z >> 16;
243 v.x = bn_wrap_add(v.x, bn_wrap_mul(v.y, v.z));
244 v.y = bn_wrap_add(v.y, bn_wrap_mul(v.z, v.x));
245 v.z = bn_wrap_add(v.z, bn_wrap_mul(v.x, v.y));
246 return v;
247}
248
249BN_FN ivec4 bn_hash_pcg4d_i(ivec4 v) {
250 v.x = bn_wrap_add(bn_wrap_mul(v.x, 1664525), 1013904223);
251 v.y = bn_wrap_add(bn_wrap_mul(v.y, 1664525), 1013904223);
252 v.z = bn_wrap_add(bn_wrap_mul(v.z, 1664525), 1013904223);
253 v.w = bn_wrap_add(bn_wrap_mul(v.w, 1664525), 1013904223);
254 v.x = bn_wrap_add(v.x, bn_wrap_mul(v.y, v.w));
255 v.y = bn_wrap_add(v.y, bn_wrap_mul(v.z, v.x));
256 v.z = bn_wrap_add(v.z, bn_wrap_mul(v.x, v.y));
257 v.w = bn_wrap_add(v.w, bn_wrap_mul(v.y, v.z));
258 v.x ^= v.x >> 16;
259 v.y ^= v.y >> 16;
260 v.z ^= v.z >> 16;
261 v.w ^= v.w >> 16;
262 v.x = bn_wrap_add(v.x, bn_wrap_mul(v.y, v.w));
263 v.y = bn_wrap_add(v.y, bn_wrap_mul(v.z, v.x));
264 v.z = bn_wrap_add(v.z, bn_wrap_mul(v.x, v.y));
265 v.w = bn_wrap_add(v.w, bn_wrap_mul(v.y, v.z));
266 return v;
267}
268
269// float(1), not 1.0 -- see the literal hazard at the top of this file.
270//
271// ⚠ MEASURED 2026-09-05, and the precaution is NOT load-bearing HERE: writing
272// `1.0` gives the same 1056/1056. The reason is worth keeping, because it is
273// a property of this constant and not of the rule -- float(0x7fffffff) is
274// 2147483648.0, i.e. 2^31, because 2147483647 has no float32 spelling. A
275// power of two has an exact reciprocal, so double and float arrive at the
276// same bits. Change the divisor to anything that is not a power of two and
277// the rule starts mattering again, which is why it stays written this way.
278BN_FN float bn_int_to_float_01(int k) {
279 return float(k & 0x7fffffff) * (float(1) / float(0x7fffffff));
280}
281
282BN_FN vec2 bn_hash_int_to_float2(ivec2 k) {
283 ivec2 h = bn_hash_pcg2d_i(k);
284 return vec2(bn_int_to_float_01(h.x), bn_int_to_float_01(h.y));
285}
286BN_FN vec3 bn_hash_int_to_float3(ivec3 k) {
287 ivec3 h = bn_hash_pcg3d_i(k);
288 return vec3(bn_int_to_float_01(h.x), bn_int_to_float_01(h.y), bn_int_to_float_01(h.z));
289}
290BN_FN vec4 bn_hash_int_to_float4(ivec4 k) {
291 ivec4 h = bn_hash_pcg4d_i(k);
292 return vec4(bn_int_to_float_01(h.x), bn_int_to_float_01(h.y),
293 bn_int_to_float_01(h.z), bn_int_to_float_01(h.w));
294}
295BN_FN vec3 bn_hash_int_to_float3(ivec2 k) {
296 return bn_hash_int_to_float3(ivec3(k.x, k.y, 0));
297}
298BN_FN vec3 bn_hash_int_to_float3(ivec4 k) {
299 vec4 h = bn_hash_int_to_float4(k);
300 return vec3(h.x, h.y, h.z);
301}
src/shared/blender_noise.h +97 -207
……@@ -1,12 +1,15 @@
11#ifndef PROC_NODES_BLENDER_NOISE_H
22#define PROC_NODES_BLENDER_NOISE_H
33
4// BLENDER'S NOISE, PORTED FROM BLENDER'S SOURCE.
4// BLENDER'S NOISE HASH — NOW AN ADAPTER, NOT AN IMPLEMENTATION.
55//
6// Ported from `source/blender/blenlib/intern/noise.cc` in the Blender tree at
7// /storage/projects/code/blender-source, commit 61a36e08, BLENDER_VERSION 502 --
8// the same 5.2 the bridge is checked against. Structure and naming follow that
9// file deliberately, so the two can be diffed by eye when Blender moves.
6// The arithmetic that used to live here is in src/shared/blender_hash.inc,
7// which is compiled BOTH as GLSL and as C++ — the same arrangement
8// erosion_filter.inc has had since the erosion filter stopped existing twice.
9// This header is the godot-typed face of it: it converts Vector2/3/4 to the
10// shim's vec2/3/4 at the call site and calls the one implementation. Every
11// name and signature that was here before is still here, so blender_perlin.h,
12// blender_voronoi.h, noise_nodes.cpp and selection_nodes.cpp did not change.
1013//
1114// WHY THIS EXISTS. proc-nodes' Blender-facing texture nodes had their own
1215// arithmetic: White Noise was `fract(sin(dot(...)))` and Voronoi was a
……@@ -15,272 +18,159 @@
1518// graph crosses, evaluates, produces plausible terrain, and is a different
1619// function from the one the author was looking at in Blender.
1720//
18// THE HASH IS THE BOTTOM LAYER AND IT IS CHECKED ON ITS OWN. Voronoi's cell
19// offsets are this hash; so are Perlin's lattice gradients. Get it wrong and
20// nothing above it can ever match, and the failure looks like the structure
21// rather than the hash. Blender's White Noise Texture node is
22// `hash_float_to_float` and `hash_float_to_float3` and nothing else, which
23// makes it an exact oracle -- see blender-bridge/test/test_white_noise_parity.py.
21// WHY IT MOVED. That fix left the hash written TWICE more: this port, and a
22// hand-written GLSL copy in GpuCompiler::get_noise_library_glsl(). They
23// agreed -- measured, 22/22 exact, by tests/test_hash_parity.cpp -- but they
24// agreed the way two transcriptions agree, which is to say nothing was
25// stopping them from drifting the next time either was touched. The hash is
26// the layer where that matters most: Voronoi's cell offsets are it, Perlin's
27// lattice gradients are it, and Blender's White Noise Texture node is it and
28// nothing else, so a disagreement cannot be seen from above -- it looks like
29// the structure. This repo has already paid that bill once, in
30// ProcNodes_Hash2D, at a correlation of -0.0043.
31//
32// THE HASH IS THE BOTTOM LAYER AND IT IS CHECKED ON ITS OWN, and because it
33// is integer arithmetic the check is BIT-EXACT: GLSL requires unsigned
34// overflow to wrap and C++ defines it as modulo 2^32, so there is no
35// tolerance to hide in. tests/test_hash_parity.cpp compiles the .inc both
36// ways and compares.
2437//
25// This repo has paid for that lesson already, in the other direction:
26// ProcNodes_Hash2D implemented iq's float hash where proc-nodes uses an integer
27// LCG. Same character, unrelated values, correlation -0.0043, and the erosion
28// chain above it was tuned against a number that could not converge.
38// Ported from `source/blender/blenlib/intern/noise.cc` in the Blender tree at
39// /storage/projects/code/blender-source, commit 61a36e08, BLENDER_VERSION 502
40// -- the same 5.2 the bridge is checked against.
41
42#include "blender_noise_cpp_shim.h"
2943
3044#include <godot_cpp/variant/vector2.hpp>
3145#include <godot_cpp/variant/vector3.hpp>
3246#include <godot_cpp/variant/vector4.hpp>
3347
3448#include <cstdint>
35#include <cstring>
3649
3750namespace godot {
3851namespace bnoise {
3952
40// --- Jenkins Lookup3, https://burtleburtle.net/bob/c/lookup3.c ---
41
42static inline uint32_t hash_bit_rotate(uint32_t x, uint32_t k) {
43 return (x << k) | (x >> (32 - k));
44}
45
46static inline void hash_bit_mix(uint32_t &a, uint32_t &b, uint32_t &c) {
47 a -= c; a ^= hash_bit_rotate(c, 4); c += b;
48 b -= a; b ^= hash_bit_rotate(a, 6); a += c;
49 c -= b; c ^= hash_bit_rotate(b, 8); b += a;
50 a -= c; a ^= hash_bit_rotate(c, 16); c += b;
51 b -= a; b ^= hash_bit_rotate(a, 19); a += c;
52 c -= b; c ^= hash_bit_rotate(b, 4); b += a;
53}
54
55static inline void hash_bit_final(uint32_t &a, uint32_t &b, uint32_t &c) {
56 c ^= b; c -= hash_bit_rotate(b, 14);
57 a ^= c; a -= hash_bit_rotate(c, 11);
58 b ^= a; b -= hash_bit_rotate(a, 25);
59 c ^= b; c -= hash_bit_rotate(b, 16);
60 a ^= c; a -= hash_bit_rotate(c, 4);
61 b ^= a; b -= hash_bit_rotate(a, 14);
62 c ^= b; c -= hash_bit_rotate(b, 24);
63}
64
65static inline uint32_t hash(uint32_t kx) {
66 uint32_t a, b, c;
67 a = b = c = 0xdeadbeefu + (1 << 2) + 13;
68 a += kx;
69 hash_bit_final(a, b, c);
70 return c;
71}
72
73static inline uint32_t hash(uint32_t kx, uint32_t ky) {
74 uint32_t a, b, c;
75 a = b = c = 0xdeadbeefu + (2 << 2) + 13;
76 b += ky;
77 a += kx;
78 hash_bit_final(a, b, c);
79 return c;
80}
81
53// -- the conversion, and it is the ONLY thing this file does. Kept private:
54// the shim's types are an implementation detail of the dual compilation
55// and nothing outside should be holding one.
56namespace detail {
57inline bnoisegl::vec2 gl(const Vector2 &v) { return bnoisegl::vec2(v.x, v.y); }
58inline bnoisegl::vec3 gl(const Vector3 &v) { return bnoisegl::vec3(v.x, v.y, v.z); }
59inline bnoisegl::vec4 gl(const Vector4 &v) { return bnoisegl::vec4(v.x, v.y, v.z, v.w); }
60inline Vector2 gd(const bnoisegl::vec2 &v) { return Vector2(v.x, v.y); }
61inline Vector3 gd(const bnoisegl::vec3 &v) { return Vector3(v.x, v.y, v.z); }
62inline Vector4 gd(const bnoisegl::vec4 &v) { return Vector4(v.x, v.y, v.z, v.w); }
63} // namespace detail
64
65// --- Jenkins Lookup3 ---
66
67static inline uint32_t hash(uint32_t kx) { return bnoisegl::bn_hash(kx); }
68static inline uint32_t hash(uint32_t kx, uint32_t ky) { return bnoisegl::bn_hash(kx, ky); }
8269static inline uint32_t hash(uint32_t kx, uint32_t ky, uint32_t kz) {
83 uint32_t a, b, c;
84 a = b = c = 0xdeadbeefu + (3 << 2) + 13;
85 c += kz;
86 b += ky;
87 a += kx;
88 hash_bit_final(a, b, c);
89 return c;
70 return bnoisegl::bn_hash(kx, ky, kz);
9071}
91
9272static inline uint32_t hash(uint32_t kx, uint32_t ky, uint32_t kz, uint32_t kw) {
93 uint32_t a, b, c;
94 a = b = c = 0xdeadbeefu + (4 << 2) + 13;
95 a += kx;
96 b += ky;
97 c += kz;
98 hash_bit_mix(a, b, c);
99 a += kw;
100 hash_bit_final(a, b, c);
101 return c;
73 return bnoisegl::bn_hash(kx, ky, kz, kw);
10274}
10375
104// The hash is over the float's BIT PATTERN, not its value. memcpy rather than a
105// union or a reinterpret_cast, which is the only spelling that is not UB.
106static inline uint32_t float_as_uint(float f) {
107 uint32_t i;
108 memcpy(&i, &f, sizeof(i));
109 return i;
110}
76static inline uint32_t float_as_uint(float f) { return bnoisegl::floatBitsToUint(f); }
11177
112static inline uint32_t hash_float(float k) { return hash(float_as_uint(k)); }
113static inline uint32_t hash_float(const Vector2 &k) {
114 return hash(float_as_uint(k.x), float_as_uint(k.y));
115}
116static inline uint32_t hash_float(const Vector3 &k) {
117 return hash(float_as_uint(k.x), float_as_uint(k.y), float_as_uint(k.z));
118}
119static inline uint32_t hash_float(const Vector4 &k) {
120 return hash(float_as_uint(k.x), float_as_uint(k.y), float_as_uint(k.z), float_as_uint(k.w));
121}
78static inline uint32_t hash_float(float k) { return bnoisegl::bn_hash_float(k); }
79static inline uint32_t hash_float(const Vector2 &k) { return bnoisegl::bn_hash_float(detail::gl(k)); }
80static inline uint32_t hash_float(const Vector3 &k) { return bnoisegl::bn_hash_float(detail::gl(k)); }
81static inline uint32_t hash_float(const Vector4 &k) { return bnoisegl::bn_hash_float(detail::gl(k)); }
12282
123static inline float uint_to_float_01(uint32_t k) {
124 return (float)k / (float)0xFFFFFFFFu;
125}
83static inline float uint_to_float_01(uint32_t k) { return bnoisegl::bn_uint_to_float_01(k); }
12684
127// Hashing INTEGERS into [0, 1] -- Blender's `hash_to_float`, as against the
128// `hash_float_to_float` family below which hashes the BIT PATTERNS of floats.
129// Random Value uses these: its inputs are an integer id and an integer seed,
130// not coordinates.
131static inline float hash_to_float(uint32_t kx) { return uint_to_float_01(hash(kx)); }
85// Hashing INTEGERS into [0, 1] -- as against the `hash_float_to_float` family
86// below, which hashes the BIT PATTERNS of floats. Random Value uses these:
87// its inputs are an integer id and an integer seed, not coordinates.
88static inline float hash_to_float(uint32_t kx) { return bnoisegl::bn_hash_to_float(kx); }
13289static inline float hash_to_float(uint32_t kx, uint32_t ky) {
133 return uint_to_float_01(hash(kx, ky));
90 return bnoisegl::bn_hash_to_float(kx, ky);
13491}
13592static inline float hash_to_float(uint32_t kx, uint32_t ky, uint32_t kz) {
136 return uint_to_float_01(hash(kx, ky, kz));
93 return bnoisegl::bn_hash_to_float(kx, ky, kz);
13794}
13895static inline float hash_to_float(uint32_t kx, uint32_t ky, uint32_t kz, uint32_t kw) {
139 return uint_to_float_01(hash(kx, ky, kz, kw));
96 return bnoisegl::bn_hash_to_float(kx, ky, kz, kw);
14097}
14198
142static inline float hash_float_to_float(float k) { return uint_to_float_01(hash_float(k)); }
143static inline float hash_float_to_float(const Vector2 &k) { return uint_to_float_01(hash_float(k)); }
144static inline float hash_float_to_float(const Vector3 &k) { return uint_to_float_01(hash_float(k)); }
145static inline float hash_float_to_float(const Vector4 &k) { return uint_to_float_01(hash_float(k)); }
99static inline float hash_float_to_float(float k) { return bnoisegl::bn_hash_float_to_float(k); }
100static inline float hash_float_to_float(const Vector2 &k) {
101 return bnoisegl::bn_hash_float_to_float(detail::gl(k));
102}
103static inline float hash_float_to_float(const Vector3 &k) {
104 return bnoisegl::bn_hash_float_to_float(detail::gl(k));
105}
106static inline float hash_float_to_float(const Vector4 &k) {
107 return bnoisegl::bn_hash_float_to_float(detail::gl(k));
108}
146109
147110static inline Vector2 hash_float_to_float2(const Vector2 &k) {
148 return Vector2(hash_float_to_float(k), hash_float_to_float(Vector3(k.x, k.y, 1.0f)));
111 return detail::gd(bnoisegl::bn_hash_float_to_float2(detail::gl(k)));
149112}
150113static inline Vector2 hash_float_to_float2(const Vector3 &k) {
151 return Vector2(hash_float_to_float(Vector3(k.x, k.y, k.z)),
152 hash_float_to_float(Vector3(k.z, k.x, k.y)));
114 return detail::gd(bnoisegl::bn_hash_float_to_float2(detail::gl(k)));
153115}
154116static inline Vector2 hash_float_to_float2(const Vector4 &k) {
155 return Vector2(hash_float_to_float(Vector4(k.x, k.y, k.z, k.w)),
156 hash_float_to_float(Vector4(k.z, k.x, k.w, k.y)));
117 return detail::gd(bnoisegl::bn_hash_float_to_float2(detail::gl(k)));
157118}
158119
159120static inline Vector3 hash_float_to_float3(float k) {
160 return Vector3(hash_float_to_float(k),
161 hash_float_to_float(Vector2(k, 1.0f)),
162 hash_float_to_float(Vector2(k, 2.0f)));
121 return detail::gd(bnoisegl::bn_hash_float_to_float3(k));
163122}
164123static inline Vector3 hash_float_to_float3(const Vector2 &k) {
165 return Vector3(hash_float_to_float(k),
166 hash_float_to_float(Vector3(k.x, k.y, 1.0f)),
167 hash_float_to_float(Vector3(k.x, k.y, 2.0f)));
124 return detail::gd(bnoisegl::bn_hash_float_to_float3(detail::gl(k)));
168125}
169126static inline Vector3 hash_float_to_float3(const Vector3 &k) {
170 return Vector3(hash_float_to_float(k),
171 hash_float_to_float(Vector4(k.x, k.y, k.z, 1.0f)),
172 hash_float_to_float(Vector4(k.x, k.y, k.z, 2.0f)));
127 return detail::gd(bnoisegl::bn_hash_float_to_float3(detail::gl(k)));
173128}
174129static inline Vector3 hash_float_to_float3(const Vector4 &k) {
175 return Vector3(hash_float_to_float(k),
176 hash_float_to_float(Vector4(k.z, k.x, k.w, k.y)),
177 hash_float_to_float(Vector4(k.w, k.z, k.y, k.x)));
130 return detail::gd(bnoisegl::bn_hash_float_to_float3(detail::gl(k)));
178131}
179132
180133static inline Vector4 hash_float_to_float4(const Vector4 &k) {
181 return Vector4(hash_float_to_float(k),
182 hash_float_to_float(Vector4(k.w, k.x, k.y, k.z)),
183 hash_float_to_float(Vector4(k.z, k.w, k.x, k.y)),
184 hash_float_to_float(Vector4(k.y, k.z, k.w, k.x)));
134 return detail::gd(bnoisegl::bn_hash_float_to_float4(detail::gl(k)));
185135}
186136
187137// --- PCG integer hashes ---
188138//
189// "Hash Functions for GPU Rendering", JCGT 2020, https://jcgt.org/published/0009/03/02/
190// Blender's variants are modified to use SIGNED integers only, so they can also
191// be written in OSL, and the shift is therefore ARITHMETIC.
192//
193139// VORONOI USES THESE, NOT THE JENKINS FLOAT HASH ABOVE -- except in 1D, which
194// uses hash_float_to_float. Reading that off the source rather than assuming
195// one hash per file is the difference between a port that matches and one that
196// is the right shape with unrelated numbers.
140// uses hash_float_to_float. Reading that off Blender's source rather than
141// assuming one hash per file is the difference between a port that matches and
142// one that is the right shape with unrelated numbers.
197143//
198// The multiplies and adds are done in uint32_t and reinterpreted: signed
199// overflow is undefined behaviour in C++, and Blender relies on the
200// two's-complement wrap that uint32_t gives by definition.
144// The types ARE the shim's, not copies of it: blender_voronoi.h writes
145// `IVec3{ x, y, z }` and that has to keep meaning the thing the .inc hashes.
201146
202struct IVec2 { int32_t x = 0, y = 0; };
203struct IVec3 { int32_t x = 0, y = 0, z = 0; };
204struct IVec4 { int32_t x = 0, y = 0, z = 0, w = 0; };
147typedef bnoisegl::ivec2 IVec2;
148typedef bnoisegl::ivec3 IVec3;
149typedef bnoisegl::ivec4 IVec4;
205150
206static inline int32_t wrap_mul(int32_t a, int32_t b) {
207 return (int32_t)((uint32_t)a * (uint32_t)b);
208}
209static inline int32_t wrap_add(int32_t a, int32_t b) {
210 return (int32_t)((uint32_t)a + (uint32_t)b);
211}
212
213static inline IVec2 hash_pcg2d_i(IVec2 v) {
214 v.x = wrap_add(wrap_mul(v.x, 1664525), 1013904223);
215 v.y = wrap_add(wrap_mul(v.y, 1664525), 1013904223);
216 v.x = wrap_add(v.x, wrap_mul(v.y, 1664525));
217 v.y = wrap_add(v.y, wrap_mul(v.x, 1664525));
218 v.x ^= v.x >> 16;
219 v.y ^= v.y >> 16;
220 v.x = wrap_add(v.x, wrap_mul(v.y, 1664525));
221 v.y = wrap_add(v.y, wrap_mul(v.x, 1664525));
222 return v;
223}
151static inline int32_t wrap_mul(int32_t a, int32_t b) { return bnoisegl::bn_wrap_mul(a, b); }
152static inline int32_t wrap_add(int32_t a, int32_t b) { return bnoisegl::bn_wrap_add(a, b); }
224153
225static inline IVec3 hash_pcg3d_i(IVec3 v) {
226 v.x = wrap_add(wrap_mul(v.x, 1664525), 1013904223);
227 v.y = wrap_add(wrap_mul(v.y, 1664525), 1013904223);
228 v.z = wrap_add(wrap_mul(v.z, 1664525), 1013904223);
229 v.x = wrap_add(v.x, wrap_mul(v.y, v.z));
230 v.y = wrap_add(v.y, wrap_mul(v.z, v.x));
231 v.z = wrap_add(v.z, wrap_mul(v.x, v.y));
232 v.x ^= v.x >> 16;
233 v.y ^= v.y >> 16;
234 v.z ^= v.z >> 16;
235 v.x = wrap_add(v.x, wrap_mul(v.y, v.z));
236 v.y = wrap_add(v.y, wrap_mul(v.z, v.x));
237 v.z = wrap_add(v.z, wrap_mul(v.x, v.y));
238 return v;
239}
240
241static inline IVec4 hash_pcg4d_i(IVec4 v) {
242 v.x = wrap_add(wrap_mul(v.x, 1664525), 1013904223);
243 v.y = wrap_add(wrap_mul(v.y, 1664525), 1013904223);
244 v.z = wrap_add(wrap_mul(v.z, 1664525), 1013904223);
245 v.w = wrap_add(wrap_mul(v.w, 1664525), 1013904223);
246 v.x = wrap_add(v.x, wrap_mul(v.y, v.w));
247 v.y = wrap_add(v.y, wrap_mul(v.z, v.x));
248 v.z = wrap_add(v.z, wrap_mul(v.x, v.y));
249 v.w = wrap_add(v.w, wrap_mul(v.y, v.z));
250 v.x ^= v.x >> 16;
251 v.y ^= v.y >> 16;
252 v.z ^= v.z >> 16;
253 v.w ^= v.w >> 16;
254 v.x = wrap_add(v.x, wrap_mul(v.y, v.w));
255 v.y = wrap_add(v.y, wrap_mul(v.z, v.x));
256 v.z = wrap_add(v.z, wrap_mul(v.x, v.y));
257 v.w = wrap_add(v.w, wrap_mul(v.y, v.z));
258 return v;
259}
154static inline IVec2 hash_pcg2d_i(IVec2 v) { return bnoisegl::bn_hash_pcg2d_i(v); }
155static inline IVec3 hash_pcg3d_i(IVec3 v) { return bnoisegl::bn_hash_pcg3d_i(v); }
156static inline IVec4 hash_pcg4d_i(IVec4 v) { return bnoisegl::bn_hash_pcg4d_i(v); }
260157
261static inline float int_to_float_01(int32_t k) {
262 return (float)(k & 0x7fffffff) * (1.0f / (float)0x7fffffff);
263}
158static inline float int_to_float_01(int32_t k) { return bnoisegl::bn_int_to_float_01(k); }
264159
265160static inline Vector2 hash_int2_to_float2(IVec2 k) {
266 IVec2 h = hash_pcg2d_i(k);
267 return Vector2(int_to_float_01(h.x), int_to_float_01(h.y));
161 return detail::gd(bnoisegl::bn_hash_int_to_float2(k));
268162}
269163static inline Vector3 hash_int3_to_float3(IVec3 k) {
270 IVec3 h = hash_pcg3d_i(k);
271 return Vector3(int_to_float_01(h.x), int_to_float_01(h.y), int_to_float_01(h.z));
164 return detail::gd(bnoisegl::bn_hash_int_to_float3(k));
272165}
273166static inline Vector4 hash_int4_to_float4(IVec4 k) {
274 IVec4 h = hash_pcg4d_i(k);
275 return Vector4(int_to_float_01(h.x), int_to_float_01(h.y),
276 int_to_float_01(h.z), int_to_float_01(h.w));
167 return detail::gd(bnoisegl::bn_hash_int_to_float4(k));
277168}
278169static inline Vector3 hash_int2_to_float3(IVec2 k) {
279 return hash_int3_to_float3(IVec3{ k.x, k.y, 0 });
170 return detail::gd(bnoisegl::bn_hash_int_to_float3(k));
280171}
281172static inline Vector3 hash_int4_to_float3(IVec4 k) {
282 Vector4 h = hash_int4_to_float4(k);
283 return Vector3(h.x, h.y, h.z);
173 return detail::gd(bnoisegl::bn_hash_int_to_float3(k));
284174}
285175
286176} // namespace bnoise
src/shared/blender_noise_cpp_shim.h +108 -0
……@@ -0,0 +1,108 @@
1#ifndef PROC_NODES_BLENDER_NOISE_CPP_SHIM_H
2#define PROC_NODES_BLENDER_NOISE_CPP_SHIM_H
3
4// =========================================================================
5// blender_noise_cpp_shim.h — the C++ half of blender_hash.inc's dual
6// compilation, and the exact counterpart of erosion_cpp_shim.h.
7//
8// blender_hash.inc is written in the subset GLSL and C++ agree on. In a
9// shader every type and builtin it names is native. In C++ nothing called
10// uvec3 or floatBitsToUint exists, so this header supplies them — with the
11// SAME semantics — and then the .inc compiles unchanged.
12//
13// THE ONE RULE FOR THIS FILE: every type and function here must match its
14// GLSL spelling exactly, including on the edges. The hash layer is integer
15// arithmetic, so "exactly" is available and is the standard: GLSL requires
16// unsigned overflow to wrap and C++ defines it as modulo 2^32, which is
17// precisely why the .inc routes every signed multiply through uint.
18//
19// DELIBERATELY MINIMAL. The vector types carry construction and component
20// access and NOTHING else, because the hash layer only ever builds one and
21// reads its parts. An arithmetic operator nobody calls is somewhere for GLSL
22// and C++ to differ unnoticed — the same reasoning that left erosion's vec4
23// without operators.
24//
25// This header is godot-free by design, so an oracle with no engine can
26// evaluate the hash. Do not add godot_cpp includes; convert at the call
27// site, which is what blender_noise.h now is.
28// =========================================================================
29
30#include <cstdint>
31#include <cstring>
32
33namespace godot {
34namespace bnoisegl {
35
36// GLSL leaves BN_FN empty. Here it is `inline`, because blender_noise.h is
37// included by more than one translation unit and a non-inline definition in
38// a header is a multiple-definition link error the moment a second one
39// appears — the trap erosion_cpp_shim.h has, and the reason its debug_*
40// statics can only be defined in noise_nodes.cpp.
41#define BN_FN inline
42
43typedef uint32_t uint;
44
45struct vec2 {
46 float x, y;
47 vec2() : x(0.0f), y(0.0f) {}
48 explicit vec2(float s) : x(s), y(s) {}
49 vec2(float a, float b) : x(a), y(b) {}
50};
51
52struct vec3 {
53 float x, y, z;
54 vec3() : x(0.0f), y(0.0f), z(0.0f) {}
55 explicit vec3(float s) : x(s), y(s), z(s) {}
56 vec3(float a, float b, float c) : x(a), y(b), z(c) {}
57};
58
59struct vec4 {
60 float x, y, z, w;
61 vec4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) {}
62 explicit vec4(float s) : x(s), y(s), z(s), w(s) {}
63 vec4(float a, float b, float c, float d) : x(a), y(b), z(c), w(d) {}
64};
65
66// uvec3 exists for one reason: hash_bit_mix and hash_bit_final took C++
67// references, GLSL's `inout` is not the same thing, and erosion_filter.inc
68// records the same decision — carry the three words in and out by value.
69struct uvec3 {
70 uint x, y, z;
71 uvec3() : x(0u), y(0u), z(0u) {}
72 explicit uvec3(uint s) : x(s), y(s), z(s) {}
73 uvec3(uint a, uint b, uint c) : x(a), y(b), z(c) {}
74};
75
76struct ivec2 {
77 int32_t x, y;
78 ivec2() : x(0), y(0) {}
79 ivec2(int32_t a, int32_t b) : x(a), y(b) {}
80};
81
82struct ivec3 {
83 int32_t x, y, z;
84 ivec3() : x(0), y(0), z(0) {}
85 ivec3(int32_t a, int32_t b, int32_t c) : x(a), y(b), z(c) {}
86};
87
88struct ivec4 {
89 int32_t x, y, z, w;
90 ivec4() : x(0), y(0), z(0), w(0) {}
91 ivec4(int32_t a, int32_t b, int32_t c, int32_t d) : x(a), y(b), z(c), w(d) {}
92};
93
94// GLSL's bit cast. memcpy rather than a union or a reinterpret_cast, which
95// is the only spelling that is not undefined behaviour.
96inline uint floatBitsToUint(float f) {
97 uint i;
98 memcpy(&i, &f, sizeof(i));
99 return i;
100}
101
102// The algorithm itself. Same text the shader compiles.
103#include "blender_hash.inc"
104
105} // namespace bnoisegl
106} // namespace godot
107
108#endif // PROC_NODES_BLENDER_NOISE_CPP_SHIM_H
tests/stub_include/godot_cpp/variant/vector2.hpp +0 -14
……@@ -1,14 +0,0 @@
1#pragma once
2// Minimal stand-in for godot_cpp's Vector2, so a godot-free gate can include
3// src/shared/blender_noise.h and call the SHIPPING hash rather than a
4// transcription of it. Only the members that header touches.
5//
6// TEMPORARY: it exists because blender_noise.h is godot-typed. Once the hash
7// layer moves into the dual-compiled .inc this stub has no reader and goes.
8namespace godot {
9struct Vector2 {
10 float x = 0.0f, y = 0.0f;
11 Vector2() {}
12 Vector2(float a, float b) : x(a), y(b) {}
13};
14} // namespace godot
tests/stub_include/godot_cpp/variant/vector3.hpp +0 -8
……@@ -1,8 +0,0 @@
1#pragma once
2namespace godot {
3struct Vector3 {
4 float x = 0.0f, y = 0.0f, z = 0.0f;
5 Vector3() {}
6 Vector3(float a, float b, float c) : x(a), y(b), z(c) {}
7};
8} // namespace godot
tests/stub_include/godot_cpp/variant/vector4.hpp +0 -8
……@@ -1,8 +0,0 @@
1#pragma once
2namespace godot {
3struct Vector4 {
4 float x = 0.0f, y = 0.0f, z = 0.0f, w = 0.0f;
5 Vector4() {}
6 Vector4(float a, float b, float c, float d) : x(a), y(b), z(c), w(d) {}
7};
8} // namespace godot
tests/test_hash_parity.cpp +300 -76
……@@ -16,12 +16,19 @@
1616// wrap and C++ defines it as modulo 2^32, so the two languages compute the
1717// same integers or they do not. There is no tolerance to hide in.
1818//
19// NEITHER SIDE IS WRITTEN FOR THE TEST.
20// CPU src/shared/blender_noise.h, the header the nodes call, included
21// through tests/stub_include so no engine is needed.
22// GPU text EXTRACTED from the shipping source at run time, never
23// retyped here. The erosion filter's lesson was that a transcription
24// used AS the reference can only ever agree with itself.
19// THERE IS ONLY ONE IMPLEMENTATION, AND THIS COMPILES IT TWICE.
20// CPU src/shared/blender_hash.inc through blender_noise_cpp_shim.h
21// GPU the SAME .inc, extracted from the generated header the engine
22// hands the driver
23//
24// That is a different question from the one this gate asked on 2026-09-05,
25// when the subject was GpuCompiler::get_noise_library_glsl()'s independently
26// hand-written Jenkins hash and the answer was 22/22 -- two implementations
27// that happened to agree. What it can still catch is the part that survives
28// having one text: the places GLSL and C++ read the SAME characters
29// differently. Arithmetic vs logical shift, signed overflow, the bit cast,
30// and the literal hazard -- in GLSL `1.0` is a float and in C++ it is a
31// double, so a reciprocal computed from one can round to a different float.
2532//
2633// Build (no engine, no godot-cpp, no scons):
2734// g++ -O2 -std=c++17 -I tests/stub_include -I src/shared \
……@@ -45,16 +52,16 @@
4552#include <string>
4653#include <vector>
4754
48#include "blender_noise.h"
55#include "blender_noise_cpp_shim.h"
4956
50// The subject, by default: the hand-written GLSL noise library that ships in
51// GpuCompiler::get_noise_library_glsl(). It carries its own Jenkins hash,
52// written independently of blender_noise.h's port of the same function from
53// Blender. Nothing has ever compared them. That is the question this run asks.
54static const char *SUBJECT_FILE = "src/gpu/gpu_compiler.cpp";
55static const char *SUBJECT_MARK = "get_noise_library_glsl";
56// The name the subject text gives the 3-argument Jenkins hash.
57static const char *SUBJECT_FN3 = "jenkins_hash3";
57// The subject: the GENERATED header, because that is the text the engine
58// hands the driver. Pointing at src/shared/blender_hash.inc instead would
59// compare the source and skip the generator, and a stale .gen.h is exactly
60// the failure the generate-don't-check-in rule exists to make impossible.
61// Either spelling works -- a path ending in .inc is taken whole, anything
62// else has its raw string extracted.
63static const char *SUBJECT_FILE = "src/gpu/blender_hash_glsl.gen.h";
64static const char *SUBJECT_MARK = "BLENDER_HASH_GLSL";
5865
5966static void die(const char *what, VkResult r) {
6067 fprintf(stderr, "FATAL: %s -> VkResult %d\n", what, (int)r);
……@@ -79,18 +86,30 @@ static std::string extract_raw_string(const std::string &src, const std::string
7986 fprintf(stderr, "FATAL: marker '%s' not found\n", marker.c_str());
8087 exit(1);
8188 }
82 size_t open = src.find("R\"(", m);
83 if (open == std::string::npos) {
84 fprintf(stderr, "FATAL: no R\"( after '%s'\n", marker.c_str());
89 size_t rq = src.find("R\"", m);
90 if (rq == std::string::npos) {
91 fprintf(stderr, "FATAL: no raw string after '%s'\n", marker.c_str());
92 exit(1);
93 }
94 // A raw string may carry a delimiter -- R"DELIM( ... )DELIM" -- and the
95 // generated headers do, because their payload is GLSL full of parentheses.
96 // Reading the delimiter off the text rather than assuming R"( is the
97 // difference between this gate running and this gate looking like the
98 // subject is missing.
99 size_t lp = src.find('(', rq);
100 if (lp == std::string::npos) {
101 fprintf(stderr, "FATAL: malformed raw string after '%s'\n", marker.c_str());
85102 exit(1);
86103 }
87 open += 3;
88 size_t close = src.find(")\"", open);
104 const std::string delim = src.substr(rq + 2, lp - (rq + 2));
105 const std::string term = ")" + delim + "\"";
106 size_t close = src.find(term, lp + 1);
89107 if (close == std::string::npos) {
90 fprintf(stderr, "FATAL: unterminated raw string after '%s'\n", marker.c_str());
108 fprintf(stderr, "FATAL: unterminated raw string (delimiter '%s') after '%s'\n",
109 delim.c_str(), marker.c_str());
91110 exit(1);
92111 }
93 return src.substr(open, close - open);
112 return src.substr(lp + 1, close - (lp + 1));
94113}
95114
96115static std::vector<uint32_t> compile_glsl(const std::string &src) {
……@@ -184,18 +203,32 @@ static void init_vulkan(Ctx &c) {
184203 VK(vkCreateCommandPool(c.dev, &ci, nullptr, &c.cmdpool));
185204}
186205
187// --- the cases -------------------------------------------------------------
206// --- the cases ---
207//
208// Every family, and the inputs are chosen where an integer hash actually
209// breaks: zero, the all-ones word a shift can eat, single-bit words that
210// expose a rotate written as a shift, the two's-complement spelling of a
211// negative lattice coordinate (an ORDINARY input -- Blender indexes its
212// lattice with signed ints cast to uint), and real float bit patterns, since
213// hash_float feeds exactly those.
214//
215// FLOAT INPUTS CROSS AS BIT PATTERNS, never as decimal literals. Writing
216// 3.14159265 into the shader would ask glslc to round a decimal string and
217// C++ to round the same string, and nothing guarantees they land on the same
218// float -- the gate would then be measuring the two parsers. uintBitsToFloat
219// of an exact word cannot be rounded by anyone.
188220//
189// Chosen for where an integer hash actually breaks: zero, the all-ones word a
190// shift can eat, single-bit words that expose a rotate written as a shift, the
191// two's-complement spelling of a negative lattice coordinate (Blender indexes
192// its lattice with signed ints cast to uint, so these are ORDINARY inputs, not
193// exotic ones), and the bit patterns of real floats, since hash_float feeds
194// exactly those.
195
196struct Case3 {
197 uint32_t x, y, z;
198 const char *why;
221// FLOAT RESULTS COME BACK AS BIT PATTERNS TOO, so the comparison stays
222// exact for them as well. That is a strong claim to make of a GPU, and it is
223// the right one here: every float this layer produces comes from an integer
224// conversion, a division and a multiply, all correctly rounded by IEEE 754 on
225// both sides. A near-miss would be a real finding, not noise to be tolerated,
226// so there is deliberately no epsilon anywhere in this file.
227
228struct Case {
229 std::string name;
230 std::string glsl; // an expression of type uint
231 uint32_t cpu; // the CPU's answer, as bits
199232};
200233
201234static uint32_t bits_of(float f) {
……@@ -203,54 +236,240 @@ static uint32_t bits_of(float f) {
203236 memcpy(&u, &f, sizeof(u));
204237 return u;
205238}
239static float float_of(uint32_t u) {
240 float f;
241 memcpy(&f, &u, sizeof(f));
242 return f;
243}
244
245// A float as a GLSL expression that cannot be misparsed.
246static std::string F(float v) {
247 char b[64];
248 snprintf(b, sizeof(b), "uintBitsToFloat(0x%08xu)", bits_of(v));
249 return b;
250}
251static std::string U(uint32_t v) {
252 char b[32];
253 snprintf(b, sizeof(b), "%uu", v);
254 return b;
255}
256static std::string I(int32_t v) {
257 char b[32];
258 // GLSL has no negative literal; it is a unary minus on a positive one,
259 // and -2147483648 has no positive spelling that fits an int.
260 if (v == INT32_MIN) return "(-2147483647 - 1)";
261 snprintf(b, sizeof(b), "%d", v);
262 return b;
263}
206264
207265int main(int argc, char **argv) {
266 using namespace godot::bnoisegl;
267
208268 const std::string file = (argc > 1) ? argv[1] : SUBJECT_FILE;
209269 const std::string mark = (argc > 2) ? argv[2] : SUBJECT_MARK;
210270
211 std::string lib = extract_raw_string(read_file(file), mark);
212 if (lib.find(SUBJECT_FN3) == std::string::npos) {
213 fprintf(stderr, "FATAL: extracted text has no %s\n", SUBJECT_FN3);
271 std::string src_text = read_file(file);
272 const bool raw_inc = file.size() > 4 && file.compare(file.size() - 4, 4, ".inc") == 0;
273 std::string lib = raw_inc ? src_text : extract_raw_string(src_text, mark);
274 if (lib.find("bn_hash_bit_final") == std::string::npos) {
275 fprintf(stderr, "FATAL: extracted text has no bn_hash_bit_final -- wrong subject?\n");
214276 return 1;
215277 }
216278
217 std::vector<Case3> cases = {
218 { 0u, 0u, 0u, "zero" },
219 { 1u, 0u, 0u, "one bit, low word" },
220 { 0u, 1u, 0u, "one bit, mid word" },
221 { 0u, 0u, 1u, "one bit, high word" },
222 { 1u, 2u, 3u, "small lattice" },
223 { 7u, 11u, 13u, "small lattice, odd" },
224 { 0xFFFFFFFFu, 0u, 0u, "all ones, low" },
225 { 0u, 0xFFFFFFFFu, 0u, "all ones, mid" },
226 { 0u, 0u, 0xFFFFFFFFu, "all ones, high" },
227 { 0xFFFFFFFFu, 0xFFFFFFFFu, 0xFFFFFFFFu, "all ones" },
228 { 0x80000000u, 0x80000000u, 0x80000000u, "sign bit only" },
229 { (uint32_t)-1, (uint32_t)-2, (uint32_t)-3, "negative lattice coords" },
230 { (uint32_t)-7, 5u, (uint32_t)-11, "mixed sign lattice" },
231 { 0xdeadbeefu, 0xcafebabeu, 0x12345678u, "the seed's own constant" },
232 { 0xdeadbeefu + (3u << 2) + 13u, 0u, 0u, "the initialiser itself" },
233 { bits_of(0.0f), bits_of(0.0f), bits_of(0.0f), "float bits: +0" },
234 { bits_of(-0.0f), bits_of(0.0f), bits_of(0.0f), "float bits: -0 (differs from +0)" },
235 { bits_of(0.5f), bits_of(1.5f), bits_of(2.5f), "float bits: exact halves" },
236 { bits_of(1.0f), bits_of(-1.0f), bits_of(0.25f), "float bits: unit" },
237 { bits_of(1e-7f), bits_of(1e7f), bits_of(3.14159265f), "float bits: wide range" },
238 { 16u, 32u, 64u, "shift-sized words" },
239 { 0x0000FFFFu, 0xFFFF0000u, 0x00FF00FFu, "half words" },
279 const uint32_t KU[] = {
280 0u, 1u, 2u, 7u, 16u, 255u,
281 0x0000FFFFu, 0xFFFF0000u, 0x80000000u, 0xFFFFFFFFu,
282 0xdeadbeefu, 0xcafebabeu, 0x12345678u,
283 (uint32_t)-1, (uint32_t)-7, (uint32_t)-1000000
284 };
285 const int NU = (int)(sizeof(KU) / sizeof(KU[0]));
286
287 const float KF[] = {
288 0.0f, -0.0f, 1.0f, -1.0f, 0.5f, -0.25f, 2.5f, 7.0f,
289 3.14159265f, -7.25f, 1e7f, -1e7f, 1e-7f, 123456.789f, -0.001f, 65536.0f
290 };
291 const int NF = (int)(sizeof(KF) / sizeof(KF[0]));
292
293 std::vector<Case> cases;
294 auto add = [&](const std::string &name, const std::string &glsl, uint32_t cpu) {
295 cases.push_back(Case{name, glsl, cpu});
240296 };
241297
242 // One shader: the subject's own text, then one wrapper per case. Baking
243 // the inputs in as literals keeps this to a single buffer -- there is
244 // nothing to get wrong in an upload that could be mistaken for a hash
245 // disagreeing.
298 // -- Jenkins, the four arities. Integer in, integer out.
299 for (int i = 0; i < NU; i++) {
300 uint32_t a = KU[i], b = KU[(i + 3) % NU], c = KU[(i + 7) % NU], d = KU[(i + 11) % NU];
301 add("bn_hash1", "bn_hash(" + U(a) + ")", bn_hash(a));
302 add("bn_hash2", "bn_hash(" + U(a) + "," + U(b) + ")", bn_hash(a, b));
303 add("bn_hash3", "bn_hash(" + U(a) + "," + U(b) + "," + U(c) + ")", bn_hash(a, b, c));
304 add("bn_hash4", "bn_hash(" + U(a) + "," + U(b) + "," + U(c) + "," + U(d) + ")",
305 bn_hash(a, b, c, d));
306 add("bn_uint_to_float_01", "floatBitsToUint(bn_uint_to_float_01(" + U(a) + "))",
307 bits_of(bn_uint_to_float_01(a)));
308 add("bn_hash_to_float1", "floatBitsToUint(bn_hash_to_float(" + U(a) + "))",
309 bits_of(bn_hash_to_float(a)));
310 add("bn_hash_to_float2", "floatBitsToUint(bn_hash_to_float(" + U(a) + "," + U(b) + "))",
311 bits_of(bn_hash_to_float(a, b)));
312 add("bn_hash_to_float3",
313 "floatBitsToUint(bn_hash_to_float(" + U(a) + "," + U(b) + "," + U(c) + "))",
314 bits_of(bn_hash_to_float(a, b, c)));
315 add("bn_hash_to_float4",
316 "floatBitsToUint(bn_hash_to_float(" + U(a) + "," + U(b) + "," + U(c) + "," + U(d) + "))",
317 bits_of(bn_hash_to_float(a, b, c, d)));
318 }
319
320 // -- the float-keyed families. The bit cast is the interesting part: C++
321 // needs memcpy where GLSL has floatBitsToUint, so this is the one place
322 // the two texts are doing visibly different things underneath.
323 for (int i = 0; i < NF; i++) {
324 float x = KF[i], y = KF[(i + 3) % NF], z = KF[(i + 5) % NF], w = KF[(i + 9) % NF];
325 const std::string sx = F(x), sy = F(y), sz = F(z), sw = F(w);
326 const std::string v2 = "vec2(" + sx + "," + sy + ")";
327 const std::string v3 = "vec3(" + sx + "," + sy + "," + sz + ")";
328 const std::string v4 = "vec4(" + sx + "," + sy + "," + sz + "," + sw + ")";
329 const vec2 c2(x, y);
330 const vec3 c3(x, y, z);
331 const vec4 c4(x, y, z, w);
332
333 add("bn_hash_float(f)", "bn_hash_float(" + sx + ")", bn_hash_float(x));
334 add("bn_hash_float(v2)", "bn_hash_float(" + v2 + ")", bn_hash_float(c2));
335 add("bn_hash_float(v3)", "bn_hash_float(" + v3 + ")", bn_hash_float(c3));
336 add("bn_hash_float(v4)", "bn_hash_float(" + v4 + ")", bn_hash_float(c4));
337
338 add("bn_hff(f)", "floatBitsToUint(bn_hash_float_to_float(" + sx + "))", bits_of(bn_hash_float_to_float(x)));
339 add("bn_hff(v2)", "floatBitsToUint(bn_hash_float_to_float(" + v2 + "))", bits_of(bn_hash_float_to_float(c2)));
340 add("bn_hff(v3)", "floatBitsToUint(bn_hash_float_to_float(" + v3 + "))", bits_of(bn_hash_float_to_float(c3)));
341 add("bn_hff(v4)", "floatBitsToUint(bn_hash_float_to_float(" + v4 + "))", bits_of(bn_hash_float_to_float(c4)));
342
343 const char *comp[4] = {"x", "y", "z", "w"};
344 {
345 vec2 r = bn_hash_float_to_float2(c2);
346 const float rv[2] = {r.x, r.y};
347 for (int k = 0; k < 2; k++)
348 add("bn_hff2(v2)", "floatBitsToUint(bn_hash_float_to_float2(" + v2 + ")." + comp[k] + ")", bits_of(rv[k]));
349 }
350 {
351 vec2 r = bn_hash_float_to_float2(c3);
352 const float rv[2] = {r.x, r.y};
353 for (int k = 0; k < 2; k++)
354 add("bn_hff2(v3)", "floatBitsToUint(bn_hash_float_to_float2(" + v3 + ")." + comp[k] + ")", bits_of(rv[k]));
355 }
356 {
357 vec2 r = bn_hash_float_to_float2(c4);
358 const float rv[2] = {r.x, r.y};
359 for (int k = 0; k < 2; k++)
360 add("bn_hff2(v4)", "floatBitsToUint(bn_hash_float_to_float2(" + v4 + ")." + comp[k] + ")", bits_of(rv[k]));
361 }
362 {
363 vec3 r = bn_hash_float_to_float3(x);
364 const float rv[3] = {r.x, r.y, r.z};
365 for (int k = 0; k < 3; k++)
366 add("bn_hff3(f)", "floatBitsToUint(bn_hash_float_to_float3(" + sx + ")." + comp[k] + ")", bits_of(rv[k]));
367 }
368 {
369 vec3 r = bn_hash_float_to_float3(c2);
370 const float rv[3] = {r.x, r.y, r.z};
371 for (int k = 0; k < 3; k++)
372 add("bn_hff3(v2)", "floatBitsToUint(bn_hash_float_to_float3(" + v2 + ")." + comp[k] + ")", bits_of(rv[k]));
373 }
374 {
375 vec3 r = bn_hash_float_to_float3(c3);
376 const float rv[3] = {r.x, r.y, r.z};
377 for (int k = 0; k < 3; k++)
378 add("bn_hff3(v3)", "floatBitsToUint(bn_hash_float_to_float3(" + v3 + ")." + comp[k] + ")", bits_of(rv[k]));
379 }
380 {
381 vec3 r = bn_hash_float_to_float3(c4);
382 const float rv[3] = {r.x, r.y, r.z};
383 for (int k = 0; k < 3; k++)
384 add("bn_hff3(v4)", "floatBitsToUint(bn_hash_float_to_float3(" + v4 + ")." + comp[k] + ")", bits_of(rv[k]));
385 }
386 {
387 vec4 r = bn_hash_float_to_float4(c4);
388 const float rv[4] = {r.x, r.y, r.z, r.w};
389 for (int k = 0; k < 4; k++)
390 add("bn_hff4(v4)", "floatBitsToUint(bn_hash_float_to_float4(" + v4 + ")." + comp[k] + ")", bits_of(rv[k]));
391 }
392 }
393
394 // -- PCG. Signed throughout, so the shift is ARITHMETIC and the wrap goes
395 // through uint on both sides. This is where a language difference would
396 // actually bite: neither C++ nor GLSL defines signed overflow.
397 for (int i = 0; i < NU; i++) {
398 int32_t a = (int32_t)KU[i], b = (int32_t)KU[(i + 3) % NU];
399 int32_t c = (int32_t)KU[(i + 7) % NU], d = (int32_t)KU[(i + 11) % NU];
400 const std::string i2 = "ivec2(" + I(a) + "," + I(b) + ")";
401 const std::string i3 = "ivec3(" + I(a) + "," + I(b) + "," + I(c) + ")";
402 const std::string i4 = "ivec4(" + I(a) + "," + I(b) + "," + I(c) + "," + I(d) + ")";
403 const ivec2 c2(a, b);
404 const ivec3 c3(a, b, c);
405 const ivec4 c4(a, b, c, d);
406 const char *comp[4] = {"x", "y", "z", "w"};
407
408 add("bn_wrap_mul", "uint(bn_wrap_mul(" + I(a) + "," + I(b) + "))", (uint32_t)bn_wrap_mul(a, b));
409 add("bn_wrap_add", "uint(bn_wrap_add(" + I(a) + "," + I(b) + "))", (uint32_t)bn_wrap_add(a, b));
410 add("bn_int_to_float_01", "floatBitsToUint(bn_int_to_float_01(" + I(a) + "))",
411 bits_of(bn_int_to_float_01(a)));
412
413 {
414 ivec2 r = bn_hash_pcg2d_i(c2);
415 const int32_t rv[2] = {r.x, r.y};
416 for (int k = 0; k < 2; k++)
417 add("bn_hash_pcg2d_i", "uint(bn_hash_pcg2d_i(" + i2 + ")." + comp[k] + ")", (uint32_t)rv[k]);
418 }
419 {
420 ivec3 r = bn_hash_pcg3d_i(c3);
421 const int32_t rv[3] = {r.x, r.y, r.z};
422 for (int k = 0; k < 3; k++)
423 add("bn_hash_pcg3d_i", "uint(bn_hash_pcg3d_i(" + i3 + ")." + comp[k] + ")", (uint32_t)rv[k]);
424 }
425 {
426 ivec4 r = bn_hash_pcg4d_i(c4);
427 const int32_t rv[4] = {r.x, r.y, r.z, r.w};
428 for (int k = 0; k < 4; k++)
429 add("bn_hash_pcg4d_i", "uint(bn_hash_pcg4d_i(" + i4 + ")." + comp[k] + ")", (uint32_t)rv[k]);
430 }
431 {
432 vec2 r = bn_hash_int_to_float2(c2);
433 const float rv[2] = {r.x, r.y};
434 for (int k = 0; k < 2; k++)
435 add("bn_hash_int_to_float2", "floatBitsToUint(bn_hash_int_to_float2(" + i2 + ")." + comp[k] + ")", bits_of(rv[k]));
436 }
437 {
438 vec3 r = bn_hash_int_to_float3(c3);
439 const float rv[3] = {r.x, r.y, r.z};
440 for (int k = 0; k < 3; k++)
441 add("bn_hash_int_to_float3(i3)", "floatBitsToUint(bn_hash_int_to_float3(" + i3 + ")." + comp[k] + ")", bits_of(rv[k]));
442 }
443 {
444 vec4 r = bn_hash_int_to_float4(c4);
445 const float rv[4] = {r.x, r.y, r.z, r.w};
446 for (int k = 0; k < 4; k++)
447 add("bn_hash_int_to_float4", "floatBitsToUint(bn_hash_int_to_float4(" + i4 + ")." + comp[k] + ")", bits_of(rv[k]));
448 }
449 {
450 vec3 r = bn_hash_int_to_float3(c2);
451 const float rv[3] = {r.x, r.y, r.z};
452 for (int k = 0; k < 3; k++)
453 add("bn_hash_int_to_float3(i2)", "floatBitsToUint(bn_hash_int_to_float3(" + i2 + ")." + comp[k] + ")", bits_of(rv[k]));
454 }
455 {
456 vec3 r = bn_hash_int_to_float3(c4);
457 const float rv[3] = {r.x, r.y, r.z};
458 for (int k = 0; k < 3; k++)
459 add("bn_hash_int_to_float3(i4)", "floatBitsToUint(bn_hash_int_to_float3(" + i4 + ")." + comp[k] + ")", bits_of(rv[k]));
460 }
461 }
462
463 // One shader: the subject's own text, then one line per case. Baking the
464 // inputs in keeps this to a single buffer -- there is nothing to get wrong
465 // in an upload that could be mistaken for the hash disagreeing.
246466 std::string calls;
247467 for (size_t i = 0; i < cases.size(); i++) {
248 char c[256];
249 snprintf(c, sizeof(c), " out_v.v[%zu] = %s(%uu, %uu, %uu);\n",
250 i, SUBJECT_FN3, cases[i].x, cases[i].y, cases[i].z);
251 calls += c;
468 char c[64];
469 snprintf(c, sizeof(c), " out_v.v[%zu] = ", i);
470 calls += std::string(c) + cases[i].glsl + ";\n";
252471 }
253 std::string src =
472 std::string shader =
254473 "#version 450\n"
255474 "layout(local_size_x = 1) in;\n"
256475 "layout(std430, binding = 0) buffer Out { uint v[]; } out_v;\n" +
……@@ -260,11 +479,11 @@ int main(int argc, char **argv) {
260479 Ctx c{};
261480 init_vulkan(c);
262481 printf("device : %s\n", c.devname);
263 printf("subject: %s :: %s (%zu bytes of GLSL, extracted not retyped)\n",
264 file.c_str(), mark.c_str(), lib.size());
265 printf("cases : %zu, compared EXACTLY\n\n", cases.size());
482 printf("subject: %s%s (%zu bytes of GLSL, extracted not retyped)\n",
483 file.c_str(), raw_inc ? "" : (" :: " + mark).c_str(), lib.size());
484 printf("cases : %zu, compared EXACTLY (bit patterns, no epsilon)\n\n", cases.size());
266485
267 std::vector<uint32_t> spv = compile_glsl(src);
486 std::vector<uint32_t> spv = compile_glsl(shader);
268487
269488 VkBuffer buf;
270489 VkDeviceMemory mem;
……@@ -345,14 +564,19 @@ int main(int argc, char **argv) {
345564 VK(vkQueueSubmit(c.queue, 1, &si, VK_NULL_HANDLE));
346565 VK(vkQueueWaitIdle(c.queue));
347566
567
348568 const uint32_t *got = (const uint32_t *)mapped;
349569 int bad = 0;
570 std::string worst;
350571 for (size_t i = 0; i < cases.size(); i++) {
351 const uint32_t want = godot::bnoise::hash(cases[i].x, cases[i].y, cases[i].z);
352 if (got[i] != want) {
572 if (got[i] != cases[i].cpu) {
353573 bad++;
354 printf(" FAIL hash(%10u, %10u, %10u) cpu=%10u gpu=%10u [%s]\n",
355 cases[i].x, cases[i].y, cases[i].z, want, got[i], cases[i].why);
574 if (bad <= 20) {
575 printf(" FAIL %-28s cpu=0x%08x (%.9g) gpu=0x%08x (%.9g)\n",
576 cases[i].name.c_str(), cases[i].cpu, float_of(cases[i].cpu),
577 got[i], float_of(got[i]));
578 printf(" %s\n", cases[i].glsl.c_str());
579 }
356580 }
357581 }
358582 printf("\n%zu/%zu cases agree exactly\n", cases.size() - (size_t)bad, cases.size());
tools/gen_erosion_glsl.py +0 -57
……@@ -1,57 +0,0 @@
1#!/usr/bin/env python3
2"""Generate erosion_filter_glsl.gen.h from src/shared/erosion_filter.inc.
3
4The .inc is THE erosion filter. dc-terrain compiles it as C++ through
5erosion_cpp_shim.h; the GPU has to compile it as GLSL, and a GDExtension
6cannot read a source file off disk at run time. So the text is embedded in a
7generated header at build time.
8
9Generated, not checked in and hand-synced, for the obvious reason: a
10checked-in copy is a copy, and copies of this particular algorithm are what
11the whole exercise exists to remove. If this file is stale the build is stale;
12there is no path where the shader and the C++ silently disagree.
13
14Usage: gen_erosion_glsl.py <input.inc> <output.gen.h>
15"""
16import sys
17
18
19def main(src_path: str, dst_path: str) -> int:
20 with open(src_path, "r", encoding="utf-8") as f:
21 text = f.read()
22
23 # The raw-string delimiter must not appear in the payload, or the
24 # generated header silently ends early and takes half the algorithm with
25 # it. Check rather than hope.
26 delim = "EROSIONGLSL"
27 if ")" + delim + '"' in text:
28 raise SystemExit(
29 "gen_erosion_glsl: %s contains the raw-string terminator )%s\" — "
30 "pick a different delimiter" % (src_path, delim)
31 )
32
33 out = [
34 "// GENERATED by tools/gen_erosion_glsl.py from %s\n" % src_path,
35 "// DO NOT EDIT. Edit the .inc; this is rebuilt from it.\n",
36 "//\n",
37 "// This is the same text dc-terrain compiles as C++ via\n",
38 "// erosion_cpp_shim.h. That is the entire point: the GPU and the CPU\n",
39 "// evaluate one algorithm, not two that agree by review.\n",
40 "#ifndef PROC_NODES_EROSION_FILTER_GLSL_GEN_H\n",
41 "#define PROC_NODES_EROSION_FILTER_GLSL_GEN_H\n",
42 "\n",
43 "static const char *EROSION_FILTER_GLSL = R\"%s(\n" % delim,
44 text,
45 ")%s\";\n" % delim,
46 "\n",
47 "#endif // PROC_NODES_EROSION_FILTER_GLSL_GEN_H\n",
48 ]
49 with open(dst_path, "w", encoding="utf-8") as f:
50 f.write("".join(out))
51 return 0
52
53
54if __name__ == "__main__":
55 if len(sys.argv) != 3:
56 raise SystemExit(__doc__)
57 sys.exit(main(sys.argv[1], sys.argv[2]))
tools/gen_glsl_inc.py +76 -0
……@@ -0,0 +1,76 @@
1#!/usr/bin/env python3
2"""Embed a dual-compiled .inc as a GLSL string in a generated C++ header.
3
4A .inc under src/shared/ is written in the subset GLSL and C++ agree on. C++
5compiles it directly through a shim; the GPU has to compile it as GLSL, and a
6GDExtension cannot read a source file off disk at run time. So the text is
7embedded in a generated header at build time.
8
9Generated, not checked in and hand-synced, for the obvious reason: a
10checked-in copy is a copy, and copies of these algorithms are what the whole
11exercise exists to remove. If this file is stale the build is stale; there is
12no path where the shader and the C++ silently disagree.
13
14Usage: gen_glsl_inc.py <input.inc> <output.gen.h> <SYMBOL>
15
16Was gen_erosion_glsl.py, which knew the erosion filter's name. The hash layer
17became the second .inc on 2026-09-05 and a second copy of this script would
18have been the same mistake one level up.
19"""
20import re
21import sys
22
23
24def main(src_path: str, dst_path: str, symbol: str) -> int:
25 if not re.fullmatch(r"[A-Z][A-Z0-9_]*", symbol):
26 raise SystemExit("gen_glsl_inc: symbol %r is not a C identifier" % symbol)
27
28 with open(src_path, "r", encoding="utf-8") as f:
29 text = f.read()
30
31 # ⚠ A C++ RAW-STRING DELIMITER MAY BE AT MOST 16 CHARACTERS. [lex.string]
32 # says so, and exceeding it does not fail where you are looking: the
33 # generated header stops being valid C++, every declaration after it is
34 # swallowed, and the compiler reports an unrelated incomplete type 700
35 # lines away in whatever was included next. That is exactly how this went
36 # wrong when the delimiter was first derived from the symbol —
37 # EROSION_FILTER_GLSL is 19 characters — so the delimiter is a short
38 # constant and the symbol is free to be as descriptive as it likes.
39 delim = "INCGLSL"
40 assert len(delim) <= 16
41
42 # The delimiter must also not appear in the payload, or the generated
43 # header silently ends early and takes half the algorithm with it. Check
44 # rather than hope.
45 if ")" + delim + '"' in text:
46 raise SystemExit(
47 "gen_glsl_inc: %s contains the raw-string terminator )%s\" — "
48 "pick a different delimiter" % (src_path, delim)
49 )
50
51 guard = "PROC_NODES_%s_GEN_H" % symbol
52 out = [
53 "// GENERATED by tools/gen_glsl_inc.py from %s\n" % src_path,
54 "// DO NOT EDIT. Edit the .inc; this is rebuilt from it.\n",
55 "//\n",
56 "// This is the same text the C++ side compiles through its shim. That is\n",
57 "// the entire point: the GPU and the CPU evaluate one algorithm, not two\n",
58 "// that agree by review.\n",
59 "#ifndef %s\n" % guard,
60 "#define %s\n" % guard,
61 "\n",
62 'static const char *%s = R"%s(\n' % (symbol, delim),
63 text,
64 ')%s";\n' % delim,
65 "\n",
66 "#endif // %s\n" % guard,
67 ]
68 with open(dst_path, "w", encoding="utf-8") as f:
69 f.write("".join(out))
70 return 0
71
72
73if __name__ == "__main__":
74 if len(sys.argv) != 4:
75 raise SystemExit(__doc__)
76 sys.exit(main(sys.argv[1], sys.argv[2], sys.argv[3]))