| … | … | @@ -16,10 +16,13 @@ src/gen/ |
| 16 | 16 | *.swp |
| 17 | 17 | *~ |
| 18 | 18 | |
| 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. | |
| 22 | src/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. | |
| 25 | src/gpu/*.gen.h | |
| 23 | 26 | |
| 24 | 27 | # Godot import artifacts from running the demo project headlessly |
| 25 | 28 | demo/.godot/ |
| … | … | @@ -43,11 +43,22 @@ env.Append(CPPPATH=["src/", "src/shared/"]) |
| 43 | 43 | # the header is the point — a checked-in copy is a copy, and three copies of |
| 44 | 44 | # this algorithm is exactly what shipped a 60x divergence between the meshed |
| 45 | 45 | # surface and the collidable one. |
| 46 | erosion_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 | ) | |
| 46 | glsl_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 | ] | |
| 51 | 62 | |
| 52 | 63 | sources = ( |
| 53 | 64 | Glob("src/*.cpp") |
| … | … | @@ -64,7 +75,7 @@ if env["target"] in ["editor", "template_debug"]: |
| 64 | 75 | # Every TU may pull in the generated header transitively; make the dependency |
| 65 | 76 | # explicit rather than relying on scan order for a file that may not exist yet |
| 66 | 77 | # on a clean checkout. |
| 67 | env.Depends(sources, erosion_glsl_gen) | |
| 78 | env.Depends(sources, glsl_inc_gen) | |
| 68 | 79 | |
| 69 | 80 | if env["target"] in ["editor", "template_debug"]: |
| 70 | 81 | try: |
| … | … | @@ -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 | ||
| 58 | BN_FN uint bn_hash_bit_rotate(uint x, uint k) { | |
| 59 | return (x << k) | (x >> (32u - k)); | |
| 60 | } | |
| 61 | ||
| 62 | BN_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 | ||
| 73 | BN_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 | ||
| 85 | BN_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 | ||
| 92 | BN_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 | ||
| 100 | BN_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 | ||
| 109 | BN_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. | |
| 121 | BN_FN uint bn_hash_float(float k) { return bn_hash(floatBitsToUint(k)); } | |
| 122 | BN_FN uint bn_hash_float(vec2 k) { | |
| 123 | return bn_hash(floatBitsToUint(k.x), floatBitsToUint(k.y)); | |
| 124 | } | |
| 125 | BN_FN uint bn_hash_float(vec3 k) { | |
| 126 | return bn_hash(floatBitsToUint(k.x), floatBitsToUint(k.y), floatBitsToUint(k.z)); | |
| 127 | } | |
| 128 | BN_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 | ||
| 133 | BN_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. | |
| 141 | BN_FN float bn_hash_to_float(uint kx) { return bn_uint_to_float_01(bn_hash(kx)); } | |
| 142 | BN_FN float bn_hash_to_float(uint kx, uint ky) { | |
| 143 | return bn_uint_to_float_01(bn_hash(kx, ky)); | |
| 144 | } | |
| 145 | BN_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 | } | |
| 148 | BN_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 | ||
| 152 | BN_FN float bn_hash_float_to_float(float k) { return bn_uint_to_float_01(bn_hash_float(k)); } | |
| 153 | BN_FN float bn_hash_float_to_float(vec2 k) { return bn_uint_to_float_01(bn_hash_float(k)); } | |
| 154 | BN_FN float bn_hash_float_to_float(vec3 k) { return bn_uint_to_float_01(bn_hash_float(k)); } | |
| 155 | BN_FN float bn_hash_float_to_float(vec4 k) { return bn_uint_to_float_01(bn_hash_float(k)); } | |
| 156 | ||
| 157 | BN_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 | } | |
| 161 | BN_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 | } | |
| 165 | BN_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 | ||
| 170 | BN_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 | } | |
| 175 | BN_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 | } | |
| 180 | BN_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 | } | |
| 185 | BN_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 | ||
| 191 | BN_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 | ||
| 214 | BN_FN int bn_wrap_mul(int a, int b) { | |
| 215 | return int(uint(a) * uint(b)); | |
| 216 | } | |
| 217 | BN_FN int bn_wrap_add(int a, int b) { | |
| 218 | return int(uint(a) + uint(b)); | |
| 219 | } | |
| 220 | ||
| 221 | BN_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 | ||
| 233 | BN_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 | ||
| 249 | BN_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. | |
| 278 | BN_FN float bn_int_to_float_01(int k) { | |
| 279 | return float(k & 0x7fffffff) * (float(1) / float(0x7fffffff)); | |
| 280 | } | |
| 281 | ||
| 282 | BN_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 | } | |
| 286 | BN_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 | } | |
| 290 | BN_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 | } | |
| 295 | BN_FN vec3 bn_hash_int_to_float3(ivec2 k) { | |
| 296 | return bn_hash_int_to_float3(ivec3(k.x, k.y, 0)); | |
| 297 | } | |
| 298 | BN_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 | } |
| … | … | @@ -1,12 +1,15 @@ |
| 1 | 1 | #ifndef PROC_NODES_BLENDER_NOISE_H |
| 2 | 2 | #define PROC_NODES_BLENDER_NOISE_H |
| 3 | 3 | |
| 4 | // BLENDER'S NOISE, PORTED FROM BLENDER'S SOURCE. | |
| 4 | // BLENDER'S NOISE HASH — NOW AN ADAPTER, NOT AN IMPLEMENTATION. | |
| 5 | 5 | // |
| 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. | |
| 10 | 13 | // |
| 11 | 14 | // WHY THIS EXISTS. proc-nodes' Blender-facing texture nodes had their own |
| 12 | 15 | // arithmetic: White Noise was `fract(sin(dot(...)))` and Voronoi was a |
| … | … | @@ -15,272 +18,159 @@ |
| 15 | 18 | // graph crosses, evaluates, produces plausible terrain, and is a different |
| 16 | 19 | // function from the one the author was looking at in Blender. |
| 17 | 20 | // |
| 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. | |
| 24 | 37 | // |
| 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" | |
| 29 | 43 | |
| 30 | 44 | #include <godot_cpp/variant/vector2.hpp> |
| 31 | 45 | #include <godot_cpp/variant/vector3.hpp> |
| 32 | 46 | #include <godot_cpp/variant/vector4.hpp> |
| 33 | 47 | |
| 34 | 48 | #include <cstdint> |
| 35 | #include <cstring> | |
| 36 | 49 | |
| 37 | 50 | namespace godot { |
| 38 | 51 | namespace bnoise { |
| 39 | 52 | |
| 40 | // --- Jenkins Lookup3, https://burtleburtle.net/bob/c/lookup3.c --- | |
| 41 | ||
| 42 | static inline uint32_t hash_bit_rotate(uint32_t x, uint32_t k) { | |
| 43 | return (x << k) | (x >> (32 - k)); | |
| 44 | } | |
| 45 | ||
| 46 | static 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 | ||
| 55 | static 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 | ||
| 65 | static 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 | ||
| 73 | static 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. | |
| 56 | namespace detail { | |
| 57 | inline bnoisegl::vec2 gl(const Vector2 &v) { return bnoisegl::vec2(v.x, v.y); } | |
| 58 | inline bnoisegl::vec3 gl(const Vector3 &v) { return bnoisegl::vec3(v.x, v.y, v.z); } | |
| 59 | inline bnoisegl::vec4 gl(const Vector4 &v) { return bnoisegl::vec4(v.x, v.y, v.z, v.w); } | |
| 60 | inline Vector2 gd(const bnoisegl::vec2 &v) { return Vector2(v.x, v.y); } | |
| 61 | inline Vector3 gd(const bnoisegl::vec3 &v) { return Vector3(v.x, v.y, v.z); } | |
| 62 | inline Vector4 gd(const bnoisegl::vec4 &v) { return Vector4(v.x, v.y, v.z, v.w); } | |
| 63 | } // namespace detail | |
| 64 | ||
| 65 | // --- Jenkins Lookup3 --- | |
| 66 | ||
| 67 | static inline uint32_t hash(uint32_t kx) { return bnoisegl::bn_hash(kx); } | |
| 68 | static inline uint32_t hash(uint32_t kx, uint32_t ky) { return bnoisegl::bn_hash(kx, ky); } | |
| 82 | 69 | static 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); | |
| 90 | 71 | } |
| 91 | ||
| 92 | 72 | static 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); | |
| 102 | 74 | } |
| 103 | 75 | |
| 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. | |
| 106 | static inline uint32_t float_as_uint(float f) { | |
| 107 | uint32_t i; | |
| 108 | memcpy(&i, &f, sizeof(i)); | |
| 109 | return i; | |
| 110 | } | |
| 76 | static inline uint32_t float_as_uint(float f) { return bnoisegl::floatBitsToUint(f); } | |
| 111 | 77 | |
| 112 | static inline uint32_t hash_float(float k) { return hash(float_as_uint(k)); } | |
| 113 | static inline uint32_t hash_float(const Vector2 &k) { | |
| 114 | return hash(float_as_uint(k.x), float_as_uint(k.y)); | |
| 115 | } | |
| 116 | static 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 | } | |
| 119 | static 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 | } | |
| 78 | static inline uint32_t hash_float(float k) { return bnoisegl::bn_hash_float(k); } | |
| 79 | static inline uint32_t hash_float(const Vector2 &k) { return bnoisegl::bn_hash_float(detail::gl(k)); } | |
| 80 | static inline uint32_t hash_float(const Vector3 &k) { return bnoisegl::bn_hash_float(detail::gl(k)); } | |
| 81 | static inline uint32_t hash_float(const Vector4 &k) { return bnoisegl::bn_hash_float(detail::gl(k)); } | |
| 122 | 82 | |
| 123 | static inline float uint_to_float_01(uint32_t k) { | |
| 124 | return (float)k / (float)0xFFFFFFFFu; | |
| 125 | } | |
| 83 | static inline float uint_to_float_01(uint32_t k) { return bnoisegl::bn_uint_to_float_01(k); } | |
| 126 | 84 | |
| 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. | |
| 131 | static 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. | |
| 88 | static inline float hash_to_float(uint32_t kx) { return bnoisegl::bn_hash_to_float(kx); } | |
| 132 | 89 | static 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); | |
| 134 | 91 | } |
| 135 | 92 | static 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); | |
| 137 | 94 | } |
| 138 | 95 | static 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); | |
| 140 | 97 | } |
| 141 | 98 | |
| 142 | static inline float hash_float_to_float(float k) { return uint_to_float_01(hash_float(k)); } | |
| 143 | static inline float hash_float_to_float(const Vector2 &k) { return uint_to_float_01(hash_float(k)); } | |
| 144 | static inline float hash_float_to_float(const Vector3 &k) { return uint_to_float_01(hash_float(k)); } | |
| 145 | static inline float hash_float_to_float(const Vector4 &k) { return uint_to_float_01(hash_float(k)); } | |
| 99 | static inline float hash_float_to_float(float k) { return bnoisegl::bn_hash_float_to_float(k); } | |
| 100 | static inline float hash_float_to_float(const Vector2 &k) { | |
| 101 | return bnoisegl::bn_hash_float_to_float(detail::gl(k)); | |
| 102 | } | |
| 103 | static inline float hash_float_to_float(const Vector3 &k) { | |
| 104 | return bnoisegl::bn_hash_float_to_float(detail::gl(k)); | |
| 105 | } | |
| 106 | static inline float hash_float_to_float(const Vector4 &k) { | |
| 107 | return bnoisegl::bn_hash_float_to_float(detail::gl(k)); | |
| 108 | } | |
| 146 | 109 | |
| 147 | 110 | static 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))); | |
| 149 | 112 | } |
| 150 | 113 | static 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))); | |
| 153 | 115 | } |
| 154 | 116 | static 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))); | |
| 157 | 118 | } |
| 158 | 119 | |
| 159 | 120 | static 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)); | |
| 163 | 122 | } |
| 164 | 123 | static 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))); | |
| 168 | 125 | } |
| 169 | 126 | static 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))); | |
| 173 | 128 | } |
| 174 | 129 | static 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))); | |
| 178 | 131 | } |
| 179 | 132 | |
| 180 | 133 | static 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))); | |
| 185 | 135 | } |
| 186 | 136 | |
| 187 | 137 | // --- PCG integer hashes --- |
| 188 | 138 | // |
| 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 | // | |
| 193 | 139 | // 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. | |
| 197 | 143 | // |
| 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. | |
| 201 | 146 | |
| 202 | struct IVec2 { int32_t x = 0, y = 0; }; | |
| 203 | struct IVec3 { int32_t x = 0, y = 0, z = 0; }; | |
| 204 | struct IVec4 { int32_t x = 0, y = 0, z = 0, w = 0; }; | |
| 147 | typedef bnoisegl::ivec2 IVec2; | |
| 148 | typedef bnoisegl::ivec3 IVec3; | |
| 149 | typedef bnoisegl::ivec4 IVec4; | |
| 205 | 150 | |
| 206 | static inline int32_t wrap_mul(int32_t a, int32_t b) { | |
| 207 | return (int32_t)((uint32_t)a * (uint32_t)b); | |
| 208 | } | |
| 209 | static inline int32_t wrap_add(int32_t a, int32_t b) { | |
| 210 | return (int32_t)((uint32_t)a + (uint32_t)b); | |
| 211 | } | |
| 212 | ||
| 213 | static 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 | } | |
| 151 | static inline int32_t wrap_mul(int32_t a, int32_t b) { return bnoisegl::bn_wrap_mul(a, b); } | |
| 152 | static inline int32_t wrap_add(int32_t a, int32_t b) { return bnoisegl::bn_wrap_add(a, b); } | |
| 224 | 153 | |
| 225 | static 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 | ||
| 241 | static 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 | } | |
| 154 | static inline IVec2 hash_pcg2d_i(IVec2 v) { return bnoisegl::bn_hash_pcg2d_i(v); } | |
| 155 | static inline IVec3 hash_pcg3d_i(IVec3 v) { return bnoisegl::bn_hash_pcg3d_i(v); } | |
| 156 | static inline IVec4 hash_pcg4d_i(IVec4 v) { return bnoisegl::bn_hash_pcg4d_i(v); } | |
| 260 | 157 | |
| 261 | static inline float int_to_float_01(int32_t k) { | |
| 262 | return (float)(k & 0x7fffffff) * (1.0f / (float)0x7fffffff); | |
| 263 | } | |
| 158 | static inline float int_to_float_01(int32_t k) { return bnoisegl::bn_int_to_float_01(k); } | |
| 264 | 159 | |
| 265 | 160 | static 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)); | |
| 268 | 162 | } |
| 269 | 163 | static 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)); | |
| 272 | 165 | } |
| 273 | 166 | static 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)); | |
| 277 | 168 | } |
| 278 | 169 | static 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)); | |
| 280 | 171 | } |
| 281 | 172 | static 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)); | |
| 284 | 174 | } |
| 285 | 175 | |
| 286 | 176 | } // namespace bnoise |
| … | … | @@ -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 | ||
| 33 | namespace godot { | |
| 34 | namespace 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 | ||
| 43 | typedef uint32_t uint; | |
| 44 | ||
| 45 | struct 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 | ||
| 52 | struct 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 | ||
| 59 | struct 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. | |
| 69 | struct 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 | ||
| 76 | struct 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 | ||
| 82 | struct 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 | ||
| 88 | struct 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. | |
| 96 | inline 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 |
| … | … | @@ -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. | |
| 8 | namespace godot { | |
| 9 | struct 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 |
| … | … | @@ -1,8 +0,0 @@ |
| 1 | #pragma once | |
| 2 | namespace godot { | |
| 3 | struct 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 |
| … | … | @@ -1,8 +0,0 @@ |
| 1 | #pragma once | |
| 2 | namespace godot { | |
| 3 | struct 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 |
| … | … | @@ -16,12 +16,19 @@ |
| 16 | 16 | // wrap and C++ defines it as modulo 2^32, so the two languages compute the |
| 17 | 17 | // same integers or they do not. There is no tolerance to hide in. |
| 18 | 18 | // |
| 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. | |
| 25 | 32 | // |
| 26 | 33 | // Build (no engine, no godot-cpp, no scons): |
| 27 | 34 | // g++ -O2 -std=c++17 -I tests/stub_include -I src/shared \ |
| … | … | @@ -45,16 +52,16 @@ |
| 45 | 52 | #include <string> |
| 46 | 53 | #include <vector> |
| 47 | 54 | |
| 48 | #include "blender_noise.h" | |
| 55 | #include "blender_noise_cpp_shim.h" | |
| 49 | 56 | |
| 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. | |
| 54 | static const char *SUBJECT_FILE = "src/gpu/gpu_compiler.cpp"; | |
| 55 | static const char *SUBJECT_MARK = "get_noise_library_glsl"; | |
| 56 | // The name the subject text gives the 3-argument Jenkins hash. | |
| 57 | static 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. | |
| 63 | static const char *SUBJECT_FILE = "src/gpu/blender_hash_glsl.gen.h"; | |
| 64 | static const char *SUBJECT_MARK = "BLENDER_HASH_GLSL"; | |
| 58 | 65 | |
| 59 | 66 | static void die(const char *what, VkResult r) { |
| 60 | 67 | 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 |
| 79 | 86 | fprintf(stderr, "FATAL: marker '%s' not found\n", marker.c_str()); |
| 80 | 87 | exit(1); |
| 81 | 88 | } |
| 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()); | |
| 85 | 102 | exit(1); |
| 86 | 103 | } |
| 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); | |
| 89 | 107 | 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()); | |
| 91 | 110 | exit(1); |
| 92 | 111 | } |
| 93 | return src.substr(open, close - open); | |
| 112 | return src.substr(lp + 1, close - (lp + 1)); | |
| 94 | 113 | } |
| 95 | 114 | |
| 96 | 115 | static std::vector<uint32_t> compile_glsl(const std::string &src) { |
| … | … | @@ -184,18 +203,32 @@ static void init_vulkan(Ctx &c) { |
| 184 | 203 | VK(vkCreateCommandPool(c.dev, &ci, nullptr, &c.cmdpool)); |
| 185 | 204 | } |
| 186 | 205 | |
| 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. | |
| 188 | 220 | // |
| 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 | ||
| 196 | struct 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 | ||
| 228 | struct Case { | |
| 229 | std::string name; | |
| 230 | std::string glsl; // an expression of type uint | |
| 231 | uint32_t cpu; // the CPU's answer, as bits | |
| 199 | 232 | }; |
| 200 | 233 | |
| 201 | 234 | static uint32_t bits_of(float f) { |
| … | … | @@ -203,54 +236,240 @@ static uint32_t bits_of(float f) { |
| 203 | 236 | memcpy(&u, &f, sizeof(u)); |
| 204 | 237 | return u; |
| 205 | 238 | } |
| 239 | static 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. | |
| 246 | static std::string F(float v) { | |
| 247 | char b[64]; | |
| 248 | snprintf(b, sizeof(b), "uintBitsToFloat(0x%08xu)", bits_of(v)); | |
| 249 | return b; | |
| 250 | } | |
| 251 | static std::string U(uint32_t v) { | |
| 252 | char b[32]; | |
| 253 | snprintf(b, sizeof(b), "%uu", v); | |
| 254 | return b; | |
| 255 | } | |
| 256 | static 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 | } | |
| 206 | 264 | |
| 207 | 265 | int main(int argc, char **argv) { |
| 266 | using namespace godot::bnoisegl; | |
| 267 | ||
| 208 | 268 | const std::string file = (argc > 1) ? argv[1] : SUBJECT_FILE; |
| 209 | 269 | const std::string mark = (argc > 2) ? argv[2] : SUBJECT_MARK; |
| 210 | 270 | |
| 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"); | |
| 214 | 276 | return 1; |
| 215 | 277 | } |
| 216 | 278 | |
| 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}); | |
| 240 | 296 | }; |
| 241 | 297 | |
| 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. | |
| 246 | 466 | std::string calls; |
| 247 | 467 | 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"; | |
| 252 | 471 | } |
| 253 | std::string src = | |
| 472 | std::string shader = | |
| 254 | 473 | "#version 450\n" |
| 255 | 474 | "layout(local_size_x = 1) in;\n" |
| 256 | 475 | "layout(std430, binding = 0) buffer Out { uint v[]; } out_v;\n" + |
| … | … | @@ -260,11 +479,11 @@ int main(int argc, char **argv) { |
| 260 | 479 | Ctx c{}; |
| 261 | 480 | init_vulkan(c); |
| 262 | 481 | 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()); | |
| 266 | 485 | |
| 267 | std::vector<uint32_t> spv = compile_glsl(src); | |
| 486 | std::vector<uint32_t> spv = compile_glsl(shader); | |
| 268 | 487 | |
| 269 | 488 | VkBuffer buf; |
| 270 | 489 | VkDeviceMemory mem; |
| … | … | @@ -345,14 +564,19 @@ int main(int argc, char **argv) { |
| 345 | 564 | VK(vkQueueSubmit(c.queue, 1, &si, VK_NULL_HANDLE)); |
| 346 | 565 | VK(vkQueueWaitIdle(c.queue)); |
| 347 | 566 | |
| 567 | ||
| 348 | 568 | const uint32_t *got = (const uint32_t *)mapped; |
| 349 | 569 | int bad = 0; |
| 570 | std::string worst; | |
| 350 | 571 | 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) { | |
| 353 | 573 | 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 | } | |
| 356 | 580 | } |
| 357 | 581 | } |
| 358 | 582 | printf("\n%zu/%zu cases agree exactly\n", cases.size() - (size_t)bad, cases.size()); |
| … | … | @@ -1,57 +0,0 @@ |
| 1 | #!/usr/bin/env python3 | |
| 2 | """Generate erosion_filter_glsl.gen.h from src/shared/erosion_filter.inc. | |
| 3 | ||
| 4 | The .inc is THE erosion filter. dc-terrain compiles it as C++ through | |
| 5 | erosion_cpp_shim.h; the GPU has to compile it as GLSL, and a GDExtension | |
| 6 | cannot read a source file off disk at run time. So the text is embedded in a | |
| 7 | generated header at build time. | |
| 8 | ||
| 9 | Generated, not checked in and hand-synced, for the obvious reason: a | |
| 10 | checked-in copy is a copy, and copies of this particular algorithm are what | |
| 11 | the whole exercise exists to remove. If this file is stale the build is stale; | |
| 12 | there is no path where the shader and the C++ silently disagree. | |
| 13 | ||
| 14 | Usage: gen_erosion_glsl.py <input.inc> <output.gen.h> | |
| 15 | """ | |
| 16 | import sys | |
| 17 | ||
| 18 | ||
| 19 | def 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 | ||
| 54 | if __name__ == "__main__": | |
| 55 | if len(sys.argv) != 3: | |
| 56 | raise SystemExit(__doc__) | |
| 57 | sys.exit(main(sys.argv[1], sys.argv[2])) |
| … | … | @@ -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 | ||
| 4 | A .inc under src/shared/ is written in the subset GLSL and C++ agree on. C++ | |
| 5 | compiles it directly through a shim; the GPU has to compile it as GLSL, and a | |
| 6 | GDExtension cannot read a source file off disk at run time. So the text is | |
| 7 | embedded in a generated header at build time. | |
| 8 | ||
| 9 | Generated, not checked in and hand-synced, for the obvious reason: a | |
| 10 | checked-in copy is a copy, and copies of these algorithms are what the whole | |
| 11 | exercise exists to remove. If this file is stale the build is stale; there is | |
| 12 | no path where the shader and the C++ silently disagree. | |
| 13 | ||
| 14 | Usage: gen_glsl_inc.py <input.inc> <output.gen.h> <SYMBOL> | |
| 15 | ||
| 16 | Was gen_erosion_glsl.py, which knew the erosion filter's name. The hash layer | |
| 17 | became the second .inc on 2026-09-05 and a second copy of this script would | |
| 18 | have been the same mistake one level up. | |
| 19 | """ | |
| 20 | import re | |
| 21 | import sys | |
| 22 | ||
| 23 | ||
| 24 | def 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 | ||
| 73 | if __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])) |