OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / d556349

glsl: Noise Texture comes back on -- three of five, and the second Perlin can go
Third of the five texture/random nodes. supports_gpu() went false -> true and the comment that stood there -- "the GLSL is the OLD arithmetic, a different lattice, no per-dimension scale constant, no distortion" -- is deleted rather than reworded, because the hand-written GLSL Perlin it described is no longer what this node emits. compile_glsl calls into blender_perlin.inc, the text execute() runs. op parity 708 -> 716/716 golden 12/12, byte-identical blender 15/15 noise texture ALL FOUR DIMENSIONS, because the dimension picks two things at once: the coordinate built from Vector and W -- 1D takes W ALONE scaled, 2D drops z, 4D appends W -- and the per-dimension function, each with its own scale constant inside perlin_signed (0.2500, 0.6616, 0.9820, 0.8344). A dimension mix-up is therefore a SCALE error, not merely different noise, and it would look entirely plausible. Verified by reading the emitted text rather than only the totals: the four cases name bp_perlin_fractal_distorted_{1,2,3,4} and build four different coordinates (float from W*scale, vec2 dropping z, vec3, vec4 appending W), with the inputs arriving as connection variables and not as the node's own defaults. Since the CPU picks its function from the same property, agreement on all eight cases excludes a wrong pick. ⚠ ONLY EIGHT CASES HERE, ON PURPOSE. test_emitted_op_parity emits one function per case, and a perlin case carries an octave loop -- that combination is exactly what took a 31 GB machine to global OOM twice today when the sibling gate was built the same way. The ARITHMETIC is gated next door by test_shared_noise_parity (2496 cases, data-driven, 149 MiB); what belongs here is only what this gate alone can see, which is whether the emitter wires the sockets and picks the dimension. So test_emitted_op_parity now caps its own address space too, with the reason written above the cap. Both gates fail alone now instead of taking the machine with them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
d556349bb0606fa4840d6e5b1384c9a7fe87a5e8
by plankatron <fleeky@gmail.com> · 2026-09-06T00:11:39.000Z

Changed files (4) +141 -4

demo/glsl_op_gate_dump.gd +46 -0
……@@ -304,6 +304,52 @@ func _init() -> void:
304304 PackedStringArray()),
305305 })
306306
307 # --- Noise Texture ----------------------------------------------------
308 # DELIBERATELY FEW. This gate emits one function per case and a perlin case
309 # carries an octave loop, which is how its sibling came to OOM a 31 GB
310 # machine. The ARITHMETIC is gated exhaustively next door by
311 # tests/test_shared_noise_parity.cpp (2496 cases, data-driven); what is
312 # checked HERE is the thing only this gate can see -- that the emitter
313 # wires the sockets and picks the dimension the node is set to.
314 #
315 # All four dimensions, because each builds a different coordinate from
316 # Vector and W and calls a different per-dimension function, and each of
317 # those has its own scale constant.
318 for dims in [1, 2, 3, 4]:
319 for ntype in [1, 3]:
320 var g7 := ProcNodeGraph.new()
321 var pn := PerlinNoiseNode.new()
322 pn.set("noise_dimensions", dims)
323 pn.set("noise_type", ntype)
324 pn.set("normalize", true)
325 var pn_idx := g7.add_node(pn)
326 var vn2 := VectorNode.new()
327 vn2.set("value", Vector3(1.5, -2.25, 0.75))
328 g7.connect_sockets(g7.add_node(vn2), 0, pn_idx, 0)
329 var wv2 := ValueNode.new()
330 wv2.set("value", 0.5)
331 g7.connect_sockets(g7.add_node(wv2), 0, pn_idx, 1)
332 var sc := ValueNode.new()
333 sc.set("value", 3.0)
334 g7.connect_sockets(g7.add_node(sc), 0, pn_idx, 2)
335 var dt := ValueNode.new()
336 dt.set("value", 2.5) # fractional: takes the extra-octave branch
337 g7.connect_sockets(g7.add_node(dt), 0, pn_idx, 3)
338 var ds := ValueNode.new()
339 ds.set("value", 0.4) # non-zero distortion
340 g7.connect_sockets(g7.add_node(ds), 0, pn_idx, 8)
341 var go7 := GroupOutputNode.new()
342 go7.set_interface([{"name": "Density", "type": 0}])
343 var go7_idx := g7.add_node(go7)
344 g7.connect_sockets(pn_idx, 0, go7_idx, 0)
345 cases.append({
346 "node": "NoiseTexture.d%d.t%d" % [dims, ntype], "op": dims,
347 "inputs": [3.0, 2.5, 0.4],
348 "cpu": float(g7.evaluate_socket(pn_idx, 0)),
349 "glsl": g7.compile_to_glsl_function("proc_density", false,
350 PackedStringArray()),
351 })
352
307353 # A DIRECTORY, not JSON: stage 2 is a standalone C++ program with no JSON
308354 # parser, and adding one to read our own test data would be more code than
309355 # the gate. One manifest line per case, one .glsl file per case.
src/nodes/noise_nodes.cpp +58 -0
……@@ -11,6 +11,7 @@
1111#include "../graph/proc_node_graph.h"
1212#include <cmath>
1313#include "../gpu/blender_hash_glsl.gen.h"
14#include "../gpu/blender_perlin_glsl.gen.h"
1415
1516namespace godot {
1617
……@@ -427,6 +428,63 @@ PerlinNoiseNode::PerlinNoiseNode() {
427428// in grey, and the seeds differ per dimension. Ours wrote grey, so every
428429// Color-driven graph across the bridge was wrong even where the Factor was
429430// exact -- which it was, for 3D FBM, and only for 3D FBM.
431String PerlinNoiseNode::compile_glsl_prelude(int p_node_idx) const {
432 // BOTH, in dependency order: perlin's lattice gradients ARE the hash, and
433 // GLSL has no #include to say so. Each carries its own #ifndef, so a graph
434 // holding this node and a White Noise node emits the hash once.
435 return String(BLENDER_HASH_GLSL) + String(BLENDER_PERLIN_GLSL);
436}
437
438String PerlinNoiseNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
439 // Mirrors execute() case for case: node_shader_tex_noise.cc's dispatch
440 // over blender_perlin.inc. The dimension picks BOTH the coordinate built
441 // from Vector and W and the per-dimension function -- 1D takes W alone
442 // scaled, 2D drops z, 4D appends W -- and each has its own scale constant
443 // inside perlin_signed, so a dimension mix-up is a scale error and not
444 // merely different noise.
445 const String v = p_ctx.in(p_node_idx, 0);
446 const String w = p_ctx.in(p_node_idx, 1);
447 const String scale = p_ctx.in(p_node_idx, 2);
448 const String args = String(", ") + p_ctx.in(p_node_idx, 3) + String(", ") +
449 p_ctx.in(p_node_idx, 4) + String(", ") + p_ctx.in(p_node_idx, 5) + String(", ") +
450 p_ctx.in(p_node_idx, 6) + String(", ") + p_ctx.in(p_node_idx, 7) + String(", ") +
451 p_ctx.in(p_node_idx, 8) + String(", ") + itos(noise_type) + String(", ") +
452 String(normalize ? "true" : "false") + String(")");
453
454 // The coordinate is named, not inlined: the Factor and the Colour both
455 // need it, and Blender's scale is applied to the coordinate rather than
456 // inside the noise.
457 const String pv = String("pn") + itos(p_node_idx) + String("_p");
458 String decl_p;
459 String fn;
460 switch (noise_dimensions) {
461 case 1:
462 decl_p = String("float ") + pv + String(" = (") + w + String(") * (") + scale +
463 String(");");
464 fn = "bp_perlin_fractal_distorted_1(";
465 break;
466 case 2:
467 decl_p = String("vec2 ") + pv + String(" = vec2((") + v + String(").x, (") + v +
468 String(").y) * (") + scale + String(");");
469 fn = "bp_perlin_fractal_distorted_2(";
470 break;
471 case 4:
472 decl_p = String("vec4 ") + pv + String(" = vec4(") + v + String(", ") + w +
473 String(") * (") + scale + String(");");
474 fn = "bp_perlin_fractal_distorted_4(";
475 break;
476 default:
477 decl_p = String("vec3 ") + pv + String(" = (") + v + String(") * (") + scale +
478 String(");");
479 fn = "bp_perlin_fractal_distorted_3(";
480 break;
481 }
482
483 return decl_p + String("\n\t") + p_ctx.declare(p_node_idx, 0) + String(" = ") + fn + pv +
484 args + String(";\n\t") + p_ctx.declare(p_node_idx, 1) +
485 String(" = vec4(bp_perlin_float3_fractal_distorted(") + pv + args + String(", 1.0);");
486}
487
430488void PerlinNoiseNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
431489 SocketValue in[9];
432490 bool is_field = false;
src/nodes/noise_nodes.h +8 -4
……@@ -59,10 +59,14 @@ public:
5959 StringName get_node_type() const override { return "NoiseTexture"; }
6060 String get_node_title() const override { return "Noise Texture"; }
6161 String get_node_category() const override { return "Texture"; }
62 // The GLSL is the OLD arithmetic -- a different lattice, no per-dimension
63 // scale constant, no distortion. Off until it is ported, so a batch cannot
64 // silently evaluate a different function from the CPU path.
65 bool supports_gpu() const override { return false; }
62 // ON since 2026-09-05. It used to emit the OLD arithmetic -- a different
63 // lattice, no per-dimension scale constant, no distortion -- from a
64 // hand-written GLSL Perlin that nothing compared against this node's C++.
65 // There is no second implementation now: compile_glsl calls into
66 // blender_perlin.inc, the same text execute() runs through the shim.
67 bool supports_gpu() const override { return true; }
68 String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
69 String compile_glsl_prelude(int p_node_idx) const override;
6670
6771 void set_noise_dimensions(int p_dim) { noise_dimensions = CLAMP(p_dim, 1, 4); }
6872 int get_noise_dimensions() const { return noise_dimensions; }
tests/test_emitted_op_parity.cpp +29 -0
……@@ -40,6 +40,7 @@
4040// ============================================================================
4141
4242#include <vulkan/vulkan.h>
43#include <sys/resource.h>
4344
4445#include <cmath>
4546#include <cstdio>
……@@ -57,8 +58,35 @@
5758static const double TOL_REL = 1e-5;
5859static const double TOL_ABS = 1e-6;
5960
61// ⚠ THIS GATE EMITS ONE FUNCTION PER CASE, so its shader grows with the case
62// list. That is fine for cheap arithmetic and NOT fine for anything with an
63// octave loop in it: the sibling gate, built the same way, twice took a 31 GB
64// machine to global OOM once perlin cases were added to it
65// (tests/test_shared_noise_parity.cpp, which is now data-driven and does not).
66// Keep the expensive families over there and only enough cases here to show
67// the EMITTER wires a node correctly. The cap makes a mistake fail alone.
68static const rlim_t MEM_LIMIT_BYTES = (rlim_t)4 * 1024 * 1024 * 1024;
69
70static void cap_own_memory() {
71 struct rlimit rl;
72 if (getrlimit(RLIMIT_AS, &rl) != 0) { return; }
73 if (rl.rlim_cur != RLIM_INFINITY && rl.rlim_cur <= MEM_LIMIT_BYTES) { return; }
74 rl.rlim_cur = (rl.rlim_max == RLIM_INFINITY || rl.rlim_max > MEM_LIMIT_BYTES)
75 ? MEM_LIMIT_BYTES : rl.rlim_max;
76 if (setrlimit(RLIMIT_AS, &rl) != 0) {
77 fprintf(stderr, "WARNING: setrlimit failed; running with NO memory cap\n");
78 }
79}
80
6081static void die(const char *what, VkResult r) {
6182 fprintf(stderr, "FATAL: %s -> VkResult %d\n", what, (int)r);
83 struct rlimit rl;
84 if (getrlimit(RLIMIT_AS, &rl) == 0 && rl.rlim_cur != RLIM_INFINITY) {
85 fprintf(stderr, " Capped at %.1f GiB of address space, deliberately.\n"
86 " If it is being hit, the case list has outgrown a gate that\n"
87 " emits one function per case.\n",
88 (double)rl.rlim_cur / (1024.0 * 1024.0 * 1024.0));
89 }
6290 exit(1);
6391}
6492#define VK(x) do { VkResult _r = (x); if (_r != VK_SUCCESS) die(#x, _r); } while (0)
……@@ -184,6 +212,7 @@ static void replace_all(std::string &s, const std::string &from, const std::stri
184212}
185213
186214int main(int argc, char **argv) {
215 cap_own_memory();
187216 const std::string dir = (argc > 1) ? argv[1] : "/tmp/proc_op_gate";
188217
189218 std::vector<Case> cases;