OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / 0c40cad

glsl: Random Value, all four data types, and the node that started the audit
Second of the five off the list. This is the node whose supports_gpu() used to return TRUE with no compile_glsl override at all -- the GPU path emitted none of its arithmetic and said nothing -- which is what prompted checking the other eight. It is true again now, with something behind it: calls into blender_hash.inc, the same text execute() runs through the C++ shim. op parity 678 -> 708/708, the 30 new cases being 4 types x 5 id/seed pairs, the vector one componentwise golden 12/12, byte-identical blender 62/62 random value parity, 8/8 crossing ALL FOUR TYPES ARE GATED, because they are four different functions wearing one node, and both of the ways they differ are invisible in a diff: - the SOCKET LIST changes. Boolean drops Min and Max for a single Probability, so ID and Seed move from sockets 2 and 3 to 1 and 2. Read them from the wrong place and ID gets the seed's link. - the HASH ARGUMENT ORDER changes. Float and vector hash (seed, id); int and boolean hash (id, seed). Off node_fn_random_value.cc, not inferred -- one order agrees with Blender on half the types and looks plausible on the rest. Neither mistake stops the node producing well-distributed random numbers, which is the whole reason it needs a gate rather than a reading. Checked by mutation: swapping the float order fails exactly 4 of its 5 cases, 704/708. The fifth is id=0, seed=0, where a swap is undetectable by construction -- worth saying out loud, because a case list of only symmetric inputs would have called that mutation a pass. ⚠ A NAMED `const String` PLUS A BARE LITERAL IS AN AMBIGUOUS OVERLOAD in godot-cpp: the char array converts equally well to String and to StringName. The prvalue form every other node happens to use -- `p_ctx.declare(p_node_idx, 0) + " = "` -- does not, so the same expression compiles or does not depending on whether the left side was given a name. Confirmed by changing one line to the prvalue and watching that error alone disappear while the others stayed. Every literal following a named String here is wrapped in String(), with the reason written where the next person will hit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0c40cadd6393b47721928d46fc7d2ca74dbdd7df
by plankatron <fleeky@gmail.com> · 2026-09-05T23:18:09.000Z

Changed files (3) +158 -6

demo/glsl_op_gate_dump.gd +73 -0
……@@ -231,6 +231,79 @@ func _init() -> void:
231231 PackedStringArray()),
232232 })
233233
234 # --- Random Value -----------------------------------------------------
235 # All FOUR data types, because they are four different functions wearing
236 # one node: the socket LIST changes (boolean drops Min and Max for a
237 # single Probability, so ID and Seed move from 2/3 to 1/2) and the hash
238 # ARGUMENT ORDER changes (float and vector hash (seed, id); int and
239 # boolean hash (id, seed)). Getting either wrong still produces
240 # well-distributed random numbers, which is exactly why it needs a gate
241 # and not a reading.
242 var rv_ids := [[0, 0], [1, 0], [0, 1], [7, 13], [-3, 99]]
243 for rv_type in [10, 11, 48, 50]:
244 for ids in rv_ids:
245 var comps := 3 if rv_type == 48 else 1
246 for comp in range(comps):
247 var g6 := ProcNodeGraph.new()
248 var rv := RandomValueNode.new()
249 rv.set("data_type", rv_type)
250 var rv_idx := g6.add_node(rv)
251 var is_bool: bool = rv_type == 50
252 # Min/Max, or Probability alone.
253 if is_bool:
254 var pn := ValueNode.new()
255 pn.set("value", 0.5)
256 g6.connect_sockets(g6.add_node(pn), 0, rv_idx, 0)
257 elif rv_type == 48:
258 var mn := VectorNode.new()
259 mn.set("value", Vector3(-1.0, 0.0, 2.5))
260 g6.connect_sockets(g6.add_node(mn), 0, rv_idx, 0)
261 var mx := VectorNode.new()
262 mx.set("value", Vector3(1.0, 10.0, 3.5))
263 g6.connect_sockets(g6.add_node(mx), 0, rv_idx, 1)
264 elif rv_type == 11:
265 var mn2 := IntegerNode.new()
266 mn2.set("value", -5)
267 g6.connect_sockets(g6.add_node(mn2), 0, rv_idx, 0)
268 var mx2 := IntegerNode.new()
269 mx2.set("value", 42)
270 g6.connect_sockets(g6.add_node(mx2), 0, rv_idx, 1)
271 else:
272 var mn3 := ValueNode.new()
273 mn3.set("value", -2.5)
274 g6.connect_sockets(g6.add_node(mn3), 0, rv_idx, 0)
275 var mx3 := ValueNode.new()
276 mx3.set("value", 7.5)
277 g6.connect_sockets(g6.add_node(mx3), 0, rv_idx, 1)
278 # ID and Seed, wherever this data type keeps them.
279 var id_sock := 1 if is_bool else 2
280 var idn := IntegerNode.new()
281 idn.set("value", ids[0])
282 g6.connect_sockets(g6.add_node(idn), 0, rv_idx, id_sock)
283 var sdn := IntegerNode.new()
284 sdn.set("value", ids[1])
285 g6.connect_sockets(g6.add_node(sdn), 0, rv_idx, id_sock + 1)
286
287 var rsrc := rv_idx
288 var rsock := 0
289 if rv_type == 48:
290 var sep3 := SeparateXYZNode.new()
291 var sep3_idx := g6.add_node(sep3)
292 g6.connect_sockets(rv_idx, 0, sep3_idx, 0)
293 rsrc = sep3_idx
294 rsock = comp
295 var go6 := GroupOutputNode.new()
296 go6.set_interface([{"name": "Density", "type": 0}])
297 var go6_idx := g6.add_node(go6)
298 g6.connect_sockets(rsrc, rsock, go6_idx, 0)
299 cases.append({
300 "node": "RandomValue.t%d" % rv_type, "op": comp,
301 "inputs": [float(ids[0]), float(ids[1]), 0.0],
302 "cpu": float(g6.evaluate_socket(rsrc, rsock)),
303 "glsl": g6.compile_to_glsl_function("proc_density", false,
304 PackedStringArray()),
305 })
306
234307 # A DIRECTORY, not JSON: stage 2 is a standalone C++ program with no JSON
235308 # parser, and adding one to read our own test data would be more code than
236309 # the gate. One manifest line per case, one .glsl file per case.
src/nodes/selection_nodes.cpp +76 -0
……@@ -3,6 +3,7 @@
33#include "../shared/blender_noise.h"
44#include <godot_cpp/variant/utility_functions.hpp>
55#include <cmath>
6#include "../gpu/blender_hash_glsl.gen.h"
67
78namespace godot {
89
……@@ -517,6 +518,81 @@ void RandomValueNode::set_data_type(int p_type) {
517518 output_defs = out;
518519}
519520
521String RandomValueNode::compile_glsl_prelude(int p_node_idx) const {
522 return String(BLENDER_HASH_GLSL);
523}
524
525String RandomValueNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const {
526 // Mirrors execute() case for case, including the two things about this
527 // node that cannot be guessed:
528 //
529 // 1. BOOLEAN HAS THREE INPUTS, so ID and Seed sit at 1 and 2 rather
530 // than 2 and 3.
531 // 2. THE HASH ARGUMENT ORDER IS NOT THE SAME FOR ALL FOUR TYPES. Float
532 // and vector hash (seed, id); int and boolean hash (id, seed). Read
533 // off node_fn_random_value.cc, not inferred -- one order agrees with
534 // Blender on half the types and looks plausible on the rest.
535 //
536 // ⚠ EVERY LITERAL AFTER A NAMED String IS WRAPPED IN String(). A named
537 // `const String` plus a bare "literal" is an AMBIGUOUS overload in
538 // godot-cpp -- the char array converts equally well to String and to
539 // StringName -- while the prvalue `p_ctx.declare(...) + " = "` that every
540 // other node in this tree happens to use does not. Same expression, and
541 // the only difference is whether the left side was given a name.
542 const bool is_bool = (data_type == 50);
543 const String id = "uint(" + p_ctx.in(p_node_idx, is_bool ? 1 : 2) + ")";
544 const String seed = "uint(" + p_ctx.in(p_node_idx, is_bool ? 2 : 3) + ")";
545 const String a = "(" + p_ctx.in(p_node_idx, 0) + ")";
546 const String decl = p_ctx.declare(p_node_idx, 0);
547
548 switch (data_type) {
549 case 10: { // FLOAT
550 const String b = "(" + p_ctx.in(p_node_idx, 1) + ")";
551 return decl + String(" = bn_hash_to_float(") + seed + String(", ") + id +
552 String(") * (") + b + String(" - ") + a + String(") + ") + a + String(";");
553 }
554 case 48: { // FLOAT_VECTOR -- three hashes, one per component.
555 const String b = "(" + p_ctx.in(p_node_idx, 1) + ")";
556 String e = "vec3(";
557 const char *c[3] = { "x", "y", "z" };
558 for (int i = 0; i < 3; i++) {
559 if (i > 0) {
560 e += ", ";
561 }
562 const String comp = String(".") + c[i];
563 e += String("bn_hash_to_float(") + seed + String(", ") + id + String(", ") +
564 itos(i) + String("u) * (") + b + comp + String(" - ") + a + comp +
565 String(") + ") + a + comp;
566 }
567 return decl + String(" = ") + e + String(");");
568 }
569 case 11: { // INT
570 const String b = "(" + p_ctx.in(p_node_idx, 1) + ")";
571 const String pre = String("rv") + itos(p_node_idx);
572 // UNSIGNED throughout, so the whole 32-bit span fits and the range
573 // wraps to 0 for INT_MIN..INT_MAX, where a modulo would divide by
574 // zero. Blender SWAPS rather than erroring when Min > Max.
575 return String("int ") + pre + String("_mn = min(") + a + String(", ") + b +
576 String(");\n\tint ") + pre + String("_mx = max(") + a + String(", ") + b +
577 String(");\n\tuint ") + pre + String("_h = bn_hash(") + id + String(", ") +
578 seed + String(");\n\tuint ") + pre + String("_r = uint(") + pre +
579 String("_mx) - uint(") + pre + String("_mn) + 1u;\n\t") + decl +
580 String(" = int(uint(") + pre + String("_mn) + ((") + pre +
581 String("_r == 0u) ? ") + pre + String("_h : (") + pre + String("_h % ") +
582 pre + String("_r)));");
583 }
584 case 50: // BOOLEAN
585 // `<=`, not `<`, so a probability of exactly 0 can still be true
586 // when the hash is exactly 0.
587 return decl + String(" = bn_hash_to_float(") + id + String(", ") + seed +
588 String(") <= ") + a + String(";");
589 default:
590 // set_data_type refuses anything else, so this is unreachable
591 // rather than a silent fallback.
592 return String();
593 }
594}
595
520596void RandomValueNode::execute(ProcEvalContext &p_ctx, int p_node_idx) {
521597 // BOOLEAN HAS THREE INPUTS, so ID and Seed are not where they are for the
522598 // other three. Retyping the sockets without moving them would have ID
src/nodes/selection_nodes.h +9 -6
……@@ -134,12 +134,15 @@ public:
134134 StringName get_node_type() const override { return "RandomValue"; }
135135 String get_node_title() const override { return "Random Value"; }
136136 String get_node_category() const override { return "Utility"; }
137 // The GLSL claim was false: supports_gpu() returned true and there is no
138 // compile_glsl override at all, so the GPU path never emitted this node's
139 // arithmetic. Now that the CPU side is Blender's hash it would be a
140 // different function again. Off until it is ported, as MathNode and
141 // VectorMathNode are.
142 bool supports_gpu() const override { return false; }
137 // ON since 2026-09-05. This node's history is the reason the rest of the
138 // list was audited: supports_gpu() used to return true with NO
139 // compile_glsl override at all, so the GPU path emitted none of its
140 // arithmetic and said nothing. It is true again now because there is
141 // something behind it -- calls into blender_hash.inc, the same text
142 // execute() runs.
143 bool supports_gpu() const override { return true; }
144 String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override;
145 String compile_glsl_prelude(int p_node_idx) const override;
143146
144147 // Rebuilds the socket LIST, not just its types.
145148 void set_data_type(int p_type);