| … | … | @@ -117,6 +117,174 @@ func dump_value_graph(path: String) -> void: |
| 117 | 117 | g.compile_to_glsl_function("proc_density", false, PackedStringArray())) |
| 118 | 118 | |
| 119 | 119 | |
| 120 | ## H. NO EMITTED DENSITY SHADER MAY REFERENCE `pc.`. | |
| 121 | ## | |
| 122 | ## compile_to_function produces a standalone `float proc_density(vec3 pos)` | |
| 123 | ## with no push-constant block. compile_batch produces a compute shader that | |
| 124 | ## HAS one. `pc.paramN` in a node's GLSL means a push-constant SLOT to the | |
| 125 | ## second and, through a textual substitution in the first, SOCKET N -- two | |
| 126 | ## conventions, and every node that used both was wrong in the density path. | |
| 127 | ## Measured 2026-09-06: Displace shifted every argument one socket and | |
| 128 | ## multiplied its displacement by 0.0, Set Position emitted `pos += vec3(0,0,0)`, | |
| 129 | ## and Simplex Noise left `pc.param3` in a function where `pc` does not exist, | |
| 130 | ## so the shader could not compile at all. | |
| 131 | ## | |
| 132 | ## The sweep is over EVERY instantiable node that claims GPU support, not the | |
| 133 | ## three that were broken, because the next node to be written is the one this | |
| 134 | ## has to catch. | |
| 135 | ## | |
| 136 | ## ⚠ IT CATCHES THE LEAK, NOT THE SHIFT, and the mutation sweep is what said so. | |
| 137 | ## Reverting Displace to `pc.param1`/`pc.param2` leaves this case GREEN: both | |
| 138 | ## indices are inside the node's socket count, so compile_to_function's textual | |
| 139 | ## substitution consumes them and nothing survives to be found -- the emitted | |
| 140 | ## text is merely reading the wrong sockets. Only Simplex Noise's `pc.param3` | |
| 141 | ## was visible here, because it addressed a fourth socket the node has not got. | |
| 142 | ## Case J is what catches the shift. This one is the guard against the next | |
| 143 | ## node that writes a slot number it has no socket for. | |
| 144 | func _no_push_constants_anywhere() -> void: | |
| 145 | var checked := 0 | |
| 146 | var offenders: Array[String] = [] | |
| 147 | for cls in ClassDB.get_inheriters_from_class("ProcNode"): | |
| 148 | if not ClassDB.can_instantiate(cls): | |
| 149 | continue | |
| 150 | var n: ProcNode = ClassDB.instantiate(cls) | |
| 151 | if n == null or not n.supports_gpu(): | |
| 152 | continue | |
| 153 | var g := ProcNodeGraph.new() | |
| 154 | g.add_node(n) | |
| 155 | var t: String = g.compile_to_glsl_function("proc_density", false, PackedStringArray()) | |
| 156 | if t.is_empty(): | |
| 157 | continue # refusal is case B's subject, not this one | |
| 158 | checked += 1 | |
| 159 | if t.find("pc.") != -1: | |
| 160 | offenders.append("%s (%s)" % [cls, n.get_node_type()]) | |
| 161 | _check("no emitted `pc.` anywhere", offenders.is_empty(), | |
| 162 | "%d GPU-capable nodes emit, none references pc." % checked if offenders.is_empty() | |
| 163 | else "%d of %d leave a push-constant reference: %s" | |
| 164 | % [offenders.size(), checked, ", ".join(offenders)]) | |
| 165 | ||
| 166 | ||
| 167 | ## I. THE SLOT A NODE READS IS THE SLOT THE EXECUTOR FILLS. | |
| 168 | ## | |
| 169 | ## GlslEmitContext::param() and GraphExecutor both walk | |
| 170 | ## get_push_constant_sockets(), so they cannot disagree -- but the MAP itself | |
| 171 | ## can still be wrong, and it was: Set Position's packer read socket 1 and | |
| 172 | ## called it the offset vector, while socket 1 has been SELECTION since its | |
| 173 | ## sockets were matched to Blender's. So assert the map names sockets that | |
| 174 | ## exist, are not Geometry, and fit the seven-float block. | |
| 175 | func _push_constant_map_is_sane(cls: String) -> void: | |
| 176 | var n: ProcNode = ClassDB.instantiate(cls) | |
| 177 | var map: PackedInt32Array = n.get_push_constant_sockets() | |
| 178 | var slots := 0 | |
| 179 | var bad := "" | |
| 180 | for sock in map: | |
| 181 | if sock < 0 or sock >= n.get_input_count(): | |
| 182 | bad = "socket %d does not exist" % sock | |
| 183 | break | |
| 184 | if n.get_input_type(sock) == 6: # Geometry | |
| 185 | bad = "socket %d is Geometry and cannot cross" % sock | |
| 186 | break | |
| 187 | slots += 3 if n.get_input_type(sock) == 3 else 1 | |
| 188 | if bad.is_empty() and slots > 7: | |
| 189 | bad = "%d slots, block holds 7" % slots | |
| 190 | _check("%s push-constant map" % n.get_node_type(), bad.is_empty(), | |
| 191 | "%d socket(s), %d slot(s)" % [map.size(), slots] if bad.is_empty() else bad) | |
| 192 | ||
| 193 | ||
| 194 | ## J. THE DENSITY PATH READS THE RIGHT SOCKET, socket by socket. | |
| 195 | ## | |
| 196 | ## A wrong index emits text that still compiles and still looks like noise, so | |
| 197 | ## the only way to see it is to give each socket a value nothing else has and | |
| 198 | ## look for it where it belongs. These are the values that were shifted. | |
| 199 | func _reads_the_right_sockets() -> void: | |
| 200 | var g := ProcNodeGraph.new() | |
| 201 | var d := g.add_node(DisplaceNode.new()) | |
| 202 | g.get_node(d).set_input_default(1, 11.0) # Strength | |
| 203 | g.get_node(d).set_input_default(2, 22.0) # Scale | |
| 204 | g.get_node(d).set_input_default(3, 33) # Detail (Int) | |
| 205 | g.get_node(d).set_input_default(4, 44.0) # Roughness | |
| 206 | g.get_node(d).set_input_default(5, 55.0) # Lacunarity | |
| 207 | var t: String = g.compile_to_glsl_function("proc_density", false, PackedStringArray()) | |
| 208 | var body := t.substr(max(0, t.find("float proc_density"))) | |
| 209 | _check("Displace socket order", | |
| 210 | body.find("pos.y) * 22.0") != -1 | |
| 211 | and body.find("float(int(33))") != -1 | |
| 212 | and body.find("33)), 44.0, 55.0,") != -1 | |
| 213 | and body.find("* n * 11.0;") != -1, | |
| 214 | "scale 22, detail 33, roughness 44, lacunarity 55, strength 11") | |
| 215 | ||
| 216 | var g2 := ProcNodeGraph.new() | |
| 217 | var sp := g2.add_node(SetPositionNode.new()) | |
| 218 | g2.get_node(sp).set_input_default(3, Vector3(1.5, 2.5, 3.5)) # Offset | |
| 219 | var t2: String = g2.compile_to_glsl_function("proc_density", false, PackedStringArray()) | |
| 220 | _check("SetPosition reads Offset (3)", | |
| 221 | t2.find("pos += vec3(1.5") != -1, | |
| 222 | "the Offset default reaches the emitted text") | |
| 223 | ||
| 224 | # Selection is socket 1. It used to be what the executor packed as the | |
| 225 | # offset, so a gate that only checked Offset would still pass with the old | |
| 226 | # index if Offset happened to be zero. Set BOTH, and only Offset may appear. | |
| 227 | var g3 := ProcNodeGraph.new() | |
| 228 | var sp3 := g3.add_node(SetPositionNode.new()) | |
| 229 | g3.get_node(sp3).set_input_default(2, Vector3(9.75, 9.75, 9.75)) # Position | |
| 230 | g3.get_node(sp3).set_input_default(3, Vector3(4.25, 5.25, 6.25)) # Offset | |
| 231 | var t3: String = g3.compile_to_glsl_function("proc_density", false, PackedStringArray()) | |
| 232 | _check("SetPosition ignores Position (2)", | |
| 233 | t3.find("4.25") != -1 and t3.find("9.75") == -1, | |
| 234 | "Offset crosses, the unconnected Position does not") | |
| 235 | ||
| 236 | var g4 := ProcNodeGraph.new() | |
| 237 | var sx := g4.add_node(NoiseTextureNode.new()) | |
| 238 | g4.get_node(sx).set_input_default(0, Vector3(0.5, 0.25, 0.125)) # Vector | |
| 239 | g4.get_node(sx).set_input_default(1, 66.0) # Scale | |
| 240 | g4.get_node(sx).set_input_default(2, 7.0) # Detail | |
| 241 | var t4: String = g4.compile_to_glsl_function("proc_density", false, PackedStringArray()) | |
| 242 | _check("SimplexNoise socket order", | |
| 243 | t4.find("vec3(0.5, 0.25, 0.125) * 66.0") != -1 | |
| 244 | and t4.find("int(7.0)") != -1, | |
| 245 | "Vector 0, Scale 1, Detail 2 -- and it reads socket 0, not `pos`") | |
| 246 | ||
| 247 | ||
| 248 | ## K. THE BATCH PATH STILL SPELLS SLOTS, and the right ones. | |
| 249 | ## | |
| 250 | ## The density and batch paths must emit DIFFERENT text from the same node: | |
| 251 | ## a literal or a producer variable in one, `pc.paramN` in the other, because | |
| 252 | ## compile_batch's pipeline is cached in the ExecutionPlan and | |
| 253 | ## set_input_default() emits no `changed` -- a baked literal there would freeze | |
| 254 | ## at whatever the value was when the plan was built. | |
| 255 | ## | |
| 256 | ## So this asserts the opposite of case H on the same nodes: the batch text MUST | |
| 257 | ## carry push constants, in the slots get_push_constant_sockets() declares. | |
| 258 | ## Displace's five sockets are param0..param4 in socket order; Set Position's | |
| 259 | ## single Vector socket is param0..param2. Both are the text these nodes | |
| 260 | ## emitted before the map existed, which is what makes the change a relocation | |
| 261 | ## on this side and a fix on the other. | |
| 262 | func _batch_still_uses_push_constants() -> void: | |
| 263 | var g := ProcNodeGraph.new() | |
| 264 | var d := g.add_node(DisplaceNode.new()) | |
| 265 | g.get_node(d).set_input_default(1, 11.0) | |
| 266 | g.get_node(d).set_input_default(2, 22.0) | |
| 267 | var bt: String = g.compile_batch_glsl(PackedInt32Array([d])) | |
| 268 | # Slot order is SOCKET order: strength(1)->param0, scale(2)->param1, | |
| 269 | # detail(3)->param2, roughness(4)->param3, lacunarity(5)->param4. | |
| 270 | _check("Displace batch slots", | |
| 271 | bt.find("pos.y) * pc.param1") != -1 | |
| 272 | and bt.find("float(int(pc.param2))") != -1 | |
| 273 | and bt.find("pc.param3, pc.param4,") != -1 | |
| 274 | and bt.find("* n * pc.param0;") != -1 | |
| 275 | and bt.find("11.0") == -1 and bt.find("22.0") == -1, | |
| 276 | "five slots in socket order, and no default baked in") | |
| 277 | ||
| 278 | var g2 := ProcNodeGraph.new() | |
| 279 | var sp := g2.add_node(SetPositionNode.new()) | |
| 280 | g2.get_node(sp).set_input_default(3, Vector3(4.25, 5.25, 6.25)) | |
| 281 | var bt2: String = g2.compile_batch_glsl(PackedInt32Array([sp])) | |
| 282 | _check("SetPosition batch slots", | |
| 283 | bt2.find("pos += vec3(pc.param0, pc.param1, pc.param2);") != -1 | |
| 284 | and bt2.find("4.25") == -1, | |
| 285 | "Offset occupies param0..2, and its default is not baked in") | |
| 286 | ||
| 287 | ||
| 120 | 288 | func _init() -> void: |
| 121 | 289 | for a in OS.get_cmdline_user_args(): |
| 122 | 290 | if a == "--write": |
| … | … | @@ -256,5 +424,13 @@ func _init() -> void: |
| 256 | 424 | var mt2: String = mg2.compile_to_glsl_function("proc_density", true, empty) |
| 257 | 425 | _check("vector Mix emits", not mt2.is_empty(), "emission accepted") |
| 258 | 426 | |
| 427 | # H, I, J -- the push-constant audit. See the functions above. | |
| 428 | _no_push_constants_anywhere() | |
| 429 | _push_constant_map_is_sane("DisplaceNode") | |
| 430 | _push_constant_map_is_sane("SetPositionNode") | |
| 431 | _push_constant_map_is_sane("NoiseTextureNode") | |
| 432 | _reads_the_right_sockets() | |
| 433 | _batch_still_uses_push_constants() | |
| 434 | ||
| 259 | 435 | print("\n%d/%d passed" % [_ran - _fail, _ran]) |
| 260 | 436 | quit(1 if _fail > 0 else 0) |
| … | … | @@ -59,6 +59,46 @@ String GlslEmitContext::literal(int p_node_idx, int p_socket) const { |
| 59 | 59 | } |
| 60 | 60 | } |
| 61 | 61 | |
| 62 | String GlslEmitContext::param(int p_node_idx, int p_socket) const { | |
| 63 | if (!push_constants || graph == nullptr) { | |
| 64 | return in(p_node_idx, p_socket); | |
| 65 | } | |
| 66 | Ref<ProcNode> node = graph->get_node(p_node_idx); | |
| 67 | if (node.is_null()) { | |
| 68 | return in(p_node_idx, p_socket); | |
| 69 | } | |
| 70 | const PackedInt32Array map = node->get_push_constant_sockets(); | |
| 71 | ||
| 72 | // Walk the map summing widths. A Vector takes three slots; everything else | |
| 73 | // takes one. This is the same walk GraphExecutor does when it packs, and | |
| 74 | // deliberately so -- a second spelling of it is the drift this ends. | |
| 75 | int slot = 0; | |
| 76 | for (int i = 0; i < map.size(); i++) { | |
| 77 | const int sock = map[i]; | |
| 78 | const int width = (node->get_input_type(sock) == (int)SocketType::Vector) ? 3 : 1; | |
| 79 | if (sock == p_socket) { | |
| 80 | // The block is element_count + seven floats. A map that overruns it | |
| 81 | // would silently read a neighbour's slot, so say so and fall back to | |
| 82 | // the literal rather than emit a wrong address. | |
| 83 | if (slot + width > 7) { | |
| 84 | UtilityFunctions::push_error( | |
| 85 | "proc-nodes: node ", p_node_idx, " socket ", p_socket, | |
| 86 | " is declared at push-constant slot ", slot, " width ", width, | |
| 87 | ", past the seven-float block. Emitting its default instead."); | |
| 88 | return in(p_node_idx, p_socket); | |
| 89 | } | |
| 90 | if (width == 3) { | |
| 91 | return String("vec3(pc.param") + itos(slot) + ", pc.param" + itos(slot + 1) + | |
| 92 | ", pc.param" + itos(slot + 2) + ")"; | |
| 93 | } | |
| 94 | return String("pc.param") + itos(slot); | |
| 95 | } | |
| 96 | slot += width; | |
| 97 | } | |
| 98 | // Not declared: an ordinary input. | |
| 99 | return in(p_node_idx, p_socket); | |
| 100 | } | |
| 101 | ||
| 62 | 102 | String GlslEmitContext::out(int p_node_idx, int p_socket) const { |
| 63 | 103 | return String("n") + itos(p_node_idx) + "_s" + itos(p_socket); |
| 64 | 104 | } |
| … | … | @@ -40,11 +40,56 @@ struct GlslEmitContext { |
| 40 | 40 | // density path passes true; the batch path does not have that uniform. |
| 41 | 41 | bool clamp_octaves = false; |
| 42 | 42 | |
| 43 | // WHICH PATH IS BEING EMITTED, and it is not cosmetic. | |
| 44 | // | |
| 45 | // compile_batch produces a compute shader with a push-constant block that | |
| 46 | // GraphExecutor refills on every dispatch, so a scalar a node reads from a | |
| 47 | // socket must arrive through `pc.paramN` -- the pipeline is cached in the | |
| 48 | // ExecutionPlan and set_input_default() emits no `changed`, so a literal | |
| 49 | // baked into that shader would freeze at the value it had when the plan was | |
| 50 | // built. compile_to_function produces a standalone `float proc_density` | |
| 51 | // with NO push-constant block at all, so the same `pc.paramN` there is a | |
| 52 | // reference to something that does not exist. | |
| 53 | // | |
| 54 | // The two used to be reconciled by compile_to_function TEXTUALLY replacing | |
| 55 | // `pc.paramN` with socket N's default. That is a different convention from | |
| 56 | // the batch path's, where N is a push-constant SLOT the executor packs by | |
| 57 | // hand, and the two collided on every node that used both. Measured | |
| 58 | // 2026-09-06 on the emitted density text: | |
| 59 | // | |
| 60 | // Displace every argument shifted one socket, and `* pc.param0` | |
| 61 | // became `* 0.0` (socket 0 is Geometry, which has no | |
| 62 | // literal) -- the displacement multiplied by zero | |
| 63 | // SimplexNoise the same shift, plus `pc.param3` left VERBATIM because | |
| 64 | // the node has only three sockets, in a function where `pc` | |
| 65 | // is not declared: the shader cannot compile | |
| 66 | // SetPosition `pos += vec3(0.0, 0.0, 0.0)` | |
| 67 | // | |
| 68 | // So the choice is made once, here, and `param()` below is the only place a | |
| 69 | // node has to know about it. | |
| 70 | bool push_constants = false; | |
| 71 | ||
| 43 | 72 | // The expression for an input socket. Connected -> the producer's variable, |
| 44 | 73 | // coerced if the types differ; unconnected -> the socket default as a |
| 45 | 74 | // literal. |
| 46 | 75 | String in(int p_node_idx, int p_socket) const; |
| 47 | 76 | |
| 77 | // An input socket that the BATCH path carries in the push-constant block. | |
| 78 | // | |
| 79 | // push_constants -> "pc.param2", or "vec3(pc.param0, pc.param1, pc.param2)" | |
| 80 | // otherwise -> in(node, socket), the same as any other input | |
| 81 | // | |
| 82 | // The slot is computed by walking the node's own get_push_constant_sockets() | |
| 83 | // and summing the widths, and GraphExecutor packs by walking the SAME list. | |
| 84 | // One list, so the slot a node reads and the slot the executor fills cannot | |
| 85 | // drift -- which they had, on Set Position, for as long as its sockets have | |
| 86 | // matched Blender's (see graph_executor.cpp). | |
| 87 | // | |
| 88 | // A socket the node does not declare falls through to in(): a node with no | |
| 89 | // map at all -- Simplex Noise is one, it is not a geometry node and never | |
| 90 | // reaches compile_batch -- behaves exactly as if this did not exist. | |
| 91 | String param(int p_node_idx, int p_socket) const; | |
| 92 | ||
| 48 | 93 | // The variable an output socket is written to, and its declaration. |
| 49 | 94 | String out(int p_node_idx, int p_socket) const; |
| 50 | 95 | String declare(int p_node_idx, int p_socket) const; |
| … | … | @@ -158,6 +158,9 @@ String GpuCompiler::compile_batch( |
| 158 | 158 | // emitted as plain literals here. |
| 159 | 159 | GlslEmitContext ctx; |
| 160 | 160 | ctx.clamp_octaves = false; |
| 161 | // This path HAS the push-constant block the template declares, and | |
| 162 | // GraphExecutor refills it on every dispatch. See GlslEmitContext. | |
| 163 | ctx.push_constants = true; | |
| 161 | 164 | ctx.graph = p_graph; |
| 162 | 165 | |
| 163 | 166 | String main_body; |
| … | … | @@ -254,9 +257,19 @@ String GpuCompiler::compile_to_function( |
| 254 | 257 | needs_noise = true; |
| 255 | 258 | } |
| 256 | 259 | |
| 257 | // Replace pc.paramN with literal defaults PER-NODE before appending. | |
| 258 | // Each node uses pc.param0..6 for its own sockets — replacing globally | |
| 259 | // would clobber other nodes' parameters. | |
| 260 | // ⛔ THE TEXTUAL pc.paramN SUBSTITUTION BELOW IS A LEGACY FALLBACK AND | |
| 261 | // MUST NOT BE REACHED BY A NODE THAT IS STILL MAINTAINED. | |
| 262 | // | |
| 263 | // It replaces `pc.paramN` with SOCKET N's default, which is a different | |
| 264 | // convention from the batch path's, where N is a push-constant SLOT. | |
| 265 | // Every node that used both conventions was silently wrong here: | |
| 266 | // Displace shifted every argument by one socket and multiplied its | |
| 267 | // displacement by 0.0, Set Position emitted `pos += vec3(0,0,0)`, and | |
| 268 | // Simplex Noise left `pc.param3` verbatim in a function with no `pc`. | |
| 269 | // All three now emit through GlslEmitContext::param()/in() and never | |
| 270 | // reach this loop. It is kept only so a node nobody has migrated | |
| 271 | // degrades the way it always did rather than emitting a dangling `pc.`, | |
| 272 | // and glsl_golden_gate.gd asserts that no GPU-capable node leaves one. | |
| 260 | 273 | for (int s = 0; s < node->get_input_count() && s < 7; s++) { |
| 261 | 274 | const SocketDef &def = node->get_input_def(s); |
| 262 | 275 | String param_name = "pc.param" + itos(s); |
| … | … | @@ -904,6 +904,36 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p |
| 904 | 904 | continue; |
| 905 | 905 | } |
| 906 | 906 | |
| 907 | // A VALUE THE SHADER CANNOT SEE MEANS THE CPU RUNS THIS NODE. | |
| 908 | // | |
| 909 | // The batch shader's only channel for a socket is the push-constant | |
| 910 | // block, so a non-geometry input that is CONNECTED and not in the | |
| 911 | // node's map is a value the dispatch would silently ignore. Set | |
| 912 | // Position's Selection and Position are exactly that: the CPU applies | |
| 913 | // `new = Selection ? (Position + Offset) : old` per element, and the | |
| 914 | // shader can express none of it. Falling back is the only answer that | |
| 915 | // is not a different function. | |
| 916 | { | |
| 917 | const PackedInt32Array pc_map = node->get_push_constant_sockets(); | |
| 918 | bool unrepresentable = false; | |
| 919 | for (int si = 0; si < node->get_input_count(); si++) { | |
| 920 | if (node->get_input_type(si) == (int)SocketType::Geometry) { | |
| 921 | continue; | |
| 922 | } | |
| 923 | if (pc_map.has(si)) { | |
| 924 | continue; | |
| 925 | } | |
| 926 | if (p_ctx.is_input_connected(node_idx, si)) { | |
| 927 | unrepresentable = true; | |
| 928 | break; | |
| 929 | } | |
| 930 | } | |
| 931 | if (unrepresentable) { | |
| 932 | node->execute(p_ctx, node_idx); | |
| 933 | continue; | |
| 934 | } | |
| 935 | } | |
| 936 | ||
| 907 | 937 | // Read input geometry (socket 0 for Displace and SetPosition) |
| 908 | 938 | Ref<ProcGeometry> input_geom = p_ctx.read_geometry(node_idx, 0); |
| 909 | 939 | if (input_geom.is_null() || input_geom->get_mesh().is_null()) { |
| … | … | @@ -1011,28 +1041,48 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p |
| 1011 | 1041 | uint32_t count = (uint32_t)vert_count; |
| 1012 | 1042 | memcpy(push_ptr, &count, 4); |
| 1013 | 1043 | |
| 1014 | // Pack node-specific parameters | |
| 1015 | if (type == StringName("Displace")) { | |
| 1016 | // DisplaceNode: param0=strength, param1=scale, param2=detail, | |
| 1017 | // param3=roughness, param4=lacunarity | |
| 1018 | float strength = p_ctx.read_float(node_idx, 1); | |
| 1019 | float noise_scale = p_ctx.read_float(node_idx, 2); | |
| 1020 | float octaves = (float)p_ctx.read_int(node_idx, 3); | |
| 1021 | float roughness = p_ctx.read_float(node_idx, 4); | |
| 1022 | float lacunarity = p_ctx.read_float(node_idx, 5); | |
| 1023 | ||
| 1024 | memcpy(push_ptr + 4, &strength, 4); | |
| 1025 | memcpy(push_ptr + 8, &noise_scale, 4); | |
| 1026 | memcpy(push_ptr + 12, &octaves, 4); | |
| 1027 | memcpy(push_ptr + 16, &roughness, 4); | |
| 1028 | memcpy(push_ptr + 20, &lacunarity, 4); | |
| 1029 | } else if (type == StringName("SetPosition")) { | |
| 1030 | // SetPositionNode: offset vector (socket 1) | |
| 1031 | Vector3 offset = p_ctx.read_vector(node_idx, 1); | |
| 1032 | ||
| 1033 | memcpy(push_ptr + 4, &offset.x, 4); | |
| 1034 | memcpy(push_ptr + 8, &offset.y, 4); | |
| 1035 | memcpy(push_ptr + 12, &offset.z, 4); | |
| 1044 | // PACK FROM THE NODE'S OWN MAP, not from a per-type `if`. | |
| 1045 | // | |
| 1046 | // GlslEmitContext::param() walks this same list to decide which | |
| 1047 | // `pc.paramN` the node's GLSL reads, so the slot written here and the | |
| 1048 | // slot read there are the same walk over the same list. | |
| 1049 | // | |
| 1050 | // ⛔ THEY WERE NOT. This block used to name sockets by hand, and Set | |
| 1051 | // Position's line read socket 1 and called it "the offset vector". Its | |
| 1052 | // sockets have been Blender's -- Geometry, Selection, Position, Offset | |
| 1053 | // -- since they were matched to Blender's, so socket 1 is SELECTION and | |
| 1054 | // the offset is socket 3. Every GPU-batched Set Position was | |
| 1055 | // displacing by a boolean and ignoring Offset entirely, and nothing | |
| 1056 | // could see it because the CPU path is a different function. | |
| 1057 | // | |
| 1058 | // Float, Int and Bool take one slot; Vector takes three. Nothing else | |
| 1059 | // can cross, which is what the connectivity refusal above enforces. | |
| 1060 | { | |
| 1061 | const PackedInt32Array pc_map = node->get_push_constant_sockets(); | |
| 1062 | int slot = 0; | |
| 1063 | for (int m = 0; m < pc_map.size(); m++) { | |
| 1064 | const int sock = pc_map[m]; | |
| 1065 | const int stype = node->get_input_type(sock); | |
| 1066 | if (stype == (int)SocketType::Vector) { | |
| 1067 | if (slot + 3 > 7) { | |
| 1068 | break; | |
| 1069 | } | |
| 1070 | const Vector3 v = p_ctx.read_vector(node_idx, sock); | |
| 1071 | const float comp[3] = { (float)v.x, (float)v.y, (float)v.z }; | |
| 1072 | for (int c = 0; c < 3; c++, slot++) { | |
| 1073 | memcpy(push_ptr + 4 + slot * 4, &comp[c], 4); | |
| 1074 | } | |
| 1075 | } else { | |
| 1076 | if (slot + 1 > 7) { | |
| 1077 | break; | |
| 1078 | } | |
| 1079 | const float f = (stype == (int)SocketType::Int) | |
| 1080 | ? (float)p_ctx.read_int(node_idx, sock) | |
| 1081 | : p_ctx.read_float(node_idx, sock); | |
| 1082 | memcpy(push_ptr + 4 + slot * 4, &f, 4); | |
| 1083 | slot++; | |
| 1084 | } | |
| 1085 | } | |
| 1036 | 1086 | } |
| 1037 | 1087 | |
| 1038 | 1088 | uint64_t t_dispatch = Time::get_singleton()->get_ticks_usec(); |
| … | … | @@ -6,6 +6,10 @@ void ProcNode::_bind_methods() { |
| 6 | 6 | ClassDB::bind_method(D_METHOD("get_node_type"), &ProcNode::get_node_type); |
| 7 | 7 | ClassDB::bind_method(D_METHOD("get_node_title"), &ProcNode::get_node_title); |
| 8 | 8 | ClassDB::bind_method(D_METHOD("supports_gpu"), &ProcNode::supports_gpu); |
| 9 | // Bound so glsl_golden_gate.gd can assert that the slot a node's GLSL reads | |
| 10 | // is the slot GraphExecutor packs -- the drift this map exists to end is | |
| 11 | // only visible if the gate can see the map. | |
| 12 | ClassDB::bind_method(D_METHOD("get_push_constant_sockets"), &ProcNode::get_push_constant_sockets); | |
| 9 | 13 | ClassDB::bind_method(D_METHOD("get_input_count"), &ProcNode::get_input_count); |
| 10 | 14 | ClassDB::bind_method(D_METHOD("get_output_count"), &ProcNode::get_output_count); |
| 11 | 15 | ClassDB::bind_method(D_METHOD("get_graph_position"), &ProcNode::get_graph_position); |
| … | … | @@ -4,6 +4,7 @@ |
| 4 | 4 | #include "socket_value.h" |
| 5 | 5 | #include "../gpu/glsl_emit_context.h" |
| 6 | 6 | #include <godot_cpp/classes/resource.hpp> |
| 7 | #include <godot_cpp/variant/packed_int32_array.hpp> | |
| 7 | 8 | #include <godot_cpp/variant/vector2.hpp> |
| 8 | 9 | |
| 9 | 10 | namespace godot { |
| … | … | @@ -101,6 +102,25 @@ public: |
| 101 | 102 | // did -- see docs/glsl-emitter-design.md. |
| 102 | 103 | virtual String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const { return ""; } |
| 103 | 104 | |
| 105 | // THE INPUT SOCKETS THIS NODE CARRIES IN THE BATCH PATH'S PUSH-CONSTANT | |
| 106 | // BLOCK, in slot order. Float, Int and Bool take one slot; Vector takes | |
| 107 | // three. Empty means the node has none -- which is every node that is not a | |
| 108 | // GPU GEOMETRY node, because compile_batch is only ever reached through | |
| 109 | // _is_gpu_geometry_node(). | |
| 110 | // | |
| 111 | // This list is the ONLY statement of the mapping. GlslEmitContext::param() | |
| 112 | // walks it to spell `pc.paramN`, and GraphExecutor walks it to pack the | |
| 113 | // bytes, so the slot a node reads is the slot the executor fills BY | |
| 114 | // CONSTRUCTION. It used to be stated twice, in the node's GLSL literal and | |
| 115 | // in a per-type `if` in the executor, and they drifted: Set Position's | |
| 116 | // packer read socket 1 -- which has been SELECTION since its sockets were | |
| 117 | // matched to Blender's -- and called it the offset vector. | |
| 118 | // | |
| 119 | // A socket left out of this list cannot reach the batch shader at all, so | |
| 120 | // GraphExecutor falls back to the CPU when one of them is connected rather | |
| 121 | // than dispatching a shader that cannot see it. | |
| 122 | virtual PackedInt32Array get_push_constant_sockets() const { return PackedInt32Array(); } | |
| 123 | ||
| 104 | 124 | // TWO DIFFERENT REASONS TO EMIT NOTHING, and conflating them is what made |
| 105 | 125 | // the emitter drop nodes silently. A node with missing arithmetic must make |
| 106 | 126 | // emission refuse; a node that is STRUCTURALLY TRANSPARENT -- Group Input |
| … | … | @@ -155,6 +155,7 @@ void ProcNodeGraph::_bind_methods() { |
| 155 | 155 | ClassDB::bind_method(D_METHOD("get_input_link_count", "node_idx", "socket_idx"), |
| 156 | 156 | &ProcNodeGraph::get_input_link_count); |
| 157 | 157 | ClassDB::bind_method(D_METHOD("compile_to_glsl_function", "function_name", "clamp_octaves", "skip_types"), &ProcNodeGraph::compile_to_glsl_function, DEFVAL("proc_density"), DEFVAL(false), DEFVAL(PackedStringArray())); |
| 158 | ClassDB::bind_method(D_METHOD("compile_batch_glsl", "node_indices"), &ProcNodeGraph::compile_batch_glsl); | |
| 158 | 159 | } |
| 159 | 160 | |
| 160 | 161 | int ProcNodeGraph::add_node(const Ref<ProcNode> &p_node) { |
| … | … | @@ -336,6 +337,20 @@ int ProcNodeGraph::find_output_node() const { |
| 336 | 337 | return nodes.size() > 0 ? nodes.size() - 1 : -1; |
| 337 | 338 | } |
| 338 | 339 | |
| 340 | String ProcNodeGraph::compile_batch_glsl(const PackedInt32Array &p_node_indices) const { | |
| 341 | Vector<Ref<ProcNode>> batch_nodes; | |
| 342 | Vector<int> batch_indices; | |
| 343 | for (int i = 0; i < p_node_indices.size(); i++) { | |
| 344 | const int idx = p_node_indices[i]; | |
| 345 | if (idx < 0 || idx >= nodes.size() || nodes[idx].is_null()) { | |
| 346 | continue; | |
| 347 | } | |
| 348 | batch_nodes.push_back(nodes[idx]); | |
| 349 | batch_indices.push_back(idx); | |
| 350 | } | |
| 351 | return GpuCompiler::compile_batch(batch_nodes, batch_indices, this); | |
| 352 | } | |
| 353 | ||
| 339 | 354 | String ProcNodeGraph::compile_to_glsl_function(const String &p_function_name, |
| 340 | 355 | bool p_clamp_octaves, const PackedStringArray &p_skip_types) const { |
| 341 | 356 | // TOPOLOGICAL ORDER, not graph index order. A value cannot be read before it |
| … | … | @@ -173,6 +173,16 @@ public: |
| 173 | 173 | bool p_clamp_octaves = false, |
| 174 | 174 | const PackedStringArray &p_skip_types = PackedStringArray()) const; |
| 175 | 175 | |
| 176 | // THE BATCH PATH'S TEXT, FOR THE GATE. | |
| 177 | // | |
| 178 | // compile_batch's output is otherwise reachable only through a GPU | |
| 179 | // dispatch, so the one path where `pc.paramN` is CORRECT had no instrument | |
| 180 | // that could read it -- which is how Set Position's packer came to read | |
| 181 | // socket 1 and call it the offset vector, for as long as socket 1 has been | |
| 182 | // Selection. This returns the same String GraphExecutor hands to | |
| 183 | // create_pipeline. It compiles nothing and touches no RenderingDevice. | |
| 184 | String compile_batch_glsl(const PackedInt32Array &p_node_indices) const; | |
| 185 | ||
| 176 | 186 | // Access |
| 177 | 187 | const Vector<Ref<ProcNode>> &get_nodes() const { return nodes; } |
| 178 | 188 |
| … | … | @@ -186,7 +186,22 @@ void SetPositionNode::execute(ProcEvalContext &p_ctx, int p_node_idx) { |
| 186 | 186 | } |
| 187 | 187 | |
| 188 | 188 | String SetPositionNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const { |
| 189 | return "pos += vec3(pc.param0, pc.param1, pc.param2);"; | |
| 189 | // Offset is socket 3. In the batch path param() spells it | |
| 190 | // `vec3(pc.param0, pc.param1, pc.param2)` -- the same text as before -- and | |
| 191 | // in the density path it becomes the socket's literal or the producer's | |
| 192 | // variable. | |
| 193 | // | |
| 194 | // ⛔ THE DENSITY PATH EMITTED `pos += vec3(0.0, 0.0, 0.0);`. The old text | |
| 195 | // named push-constant SLOTS, and compile_to_function replaced `pc.paramN` | |
| 196 | // with socket N's default: socket 0 is Geometry and sockets 1 and 2 are | |
| 197 | // Bool and Vector, none of which has a float literal, so all three came out | |
| 198 | // 0.0. Set Position was a no-op in every emitted density shader. | |
| 199 | // | |
| 200 | // It matches execute() only while Selection and Position are unconnected -- | |
| 201 | // `base = has_position ? pos_val : pos[i]`, and unconnected Position means | |
| 202 | // "keep the position this element has", which is what `pos +=` does. | |
| 203 | // GraphExecutor refuses the batch when either is wired. | |
| 204 | return String("pos += ") + p_ctx.param(p_node_idx, 3) + ";"; | |
| 190 | 205 | } |
| 191 | 206 | |
| 192 | 207 | // --- JoinGeometryNode --- |
| … | … | @@ -37,6 +37,18 @@ public: |
| 37 | 37 | String get_node_category() const override { return "Geometry"; } |
| 38 | 38 | bool supports_gpu() const override { return true; } |
| 39 | 39 | |
| 40 | // Offset ONLY -- socket 3, three floats in param0..param2. | |
| 41 | // | |
| 42 | // Selection (1) and Position (2) are deliberately absent: the batch shader | |
| 43 | // has no way to express a per-element selection or an implicit position | |
| 44 | // field, so GraphExecutor falls back to the CPU when either is connected | |
| 45 | // rather than dispatching a shader that silently ignores them. | |
| 46 | PackedInt32Array get_push_constant_sockets() const override { | |
| 47 | PackedInt32Array m; | |
| 48 | m.push_back(3); | |
| 49 | return m; | |
| 50 | } | |
| 51 | ||
| 40 | 52 | void execute(ProcEvalContext &p_ctx, int p_node_idx) override; |
| 41 | 53 | String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override; |
| 42 | 54 |
| … | … | @@ -260,8 +260,36 @@ String NoiseTextureNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_i |
| 260 | 260 | // noise_fbm() from the hand-written library, whose C++ twin was a |
| 261 | 261 | // file-local copy in this file -- 240000/240000 identical, measured, which |
| 262 | 262 | // is what made deleting the copy a relocation and not a change. |
| 263 | return "vec3 nsp = pos * pc.param0;\n\tfloat noise_val = ps_fbm_noise_3d(nsp.x, nsp.y, " | |
| 264 | "nsp.z, int(pc.param1), pc.param2, pc.param3) * 0.5 + 0.5;"; | |
| 263 | // | |
| 264 | // ⛔ THIS USED TO EMIT `pc.paramN` AND IT COULD NOT COMPILE. The node is | |
| 265 | // not a geometry node, so it never reaches compile_batch and there is no | |
| 266 | // push-constant block anywhere it can appear. In the density path | |
| 267 | // compile_to_function replaced `pc.paramN` with socket N's default, which | |
| 268 | // is a different convention -- so `pc.param0` became socket 0 (Vector, | |
| 269 | // which has no float literal) = 0.0, `pc.param1` became Scale, `pc.param2` | |
| 270 | // became Detail, and `pc.param3` addressed a fourth socket that does not | |
| 271 | // exist and was therefore LEFT VERBATIM. Measured 2026-09-06: | |
| 272 | // | |
| 273 | // vec3 nsp = pos * 0.0; | |
| 274 | // float noise_val = ps_fbm_noise_3d(nsp.x, nsp.y, nsp.z, int(5.0), 4.0, | |
| 275 | // pc.param3) * 0.5 + 0.5; | |
| 276 | // | |
| 277 | // A shader containing that cannot be compiled, so no density graph using | |
| 278 | // this node has ever worked -- which is also why fixing it can regress | |
| 279 | // nothing. | |
| 280 | // | |
| 281 | // It reads socket 0, NOT the shader's `pos`. execute() reads | |
| 282 | // read_vector(idx, 0), and the two paths agreeing is the point; an | |
| 283 | // unconnected Vector socket is (0,0,0) on both, which looks surprising and | |
| 284 | // is what the CPU does. | |
| 285 | // | |
| 286 | // Detail is a FLOAT socket truncated by int(), matching | |
| 287 | // `static_cast<int>(read_float(...))`, so it is not subject to the octave | |
| 288 | // clamp -- that applies to Int sockets, and this one is not. | |
| 289 | return String("vec3 nsp = ") + p_ctx.in(p_node_idx, 0) + " * " + | |
| 290 | p_ctx.in(p_node_idx, 1) + ";\n\tfloat noise_val = ps_fbm_noise_3d(nsp.x, nsp.y, " | |
| 291 | "nsp.z, int(" + p_ctx.in(p_node_idx, 2) + "), " + String::num(roughness, 9) + ", " + | |
| 292 | String::num(lacunarity, 9) + ") * 0.5 + 0.5;"; | |
| 265 | 293 | } |
| 266 | 294 | |
| 267 | 295 | // --- DisplaceNode --- |
| … | … | @@ -339,9 +367,26 @@ String DisplaceNode::compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) |
| 339 | 367 | // It used to be perlin_fbm() from the hand-written library, against a |
| 340 | 368 | // file-local C++ Perlin with its own Jenkins hash -- 660000/660000 |
| 341 | 369 | // identical, measured, so this is a relocation and not a change. |
| 342 | return "vec3 npos = vec3(-pos.x, pos.z, pos.y) * pc.param1;\n\tfloat n = " | |
| 343 | "bp_perlin_fbm_3(npos, float(int(pc.param2)), pc.param3, pc.param4, true) * 2.0 - " | |
| 344 | "1.0;\n\tpos += normal * n * pc.param0;"; | |
| 370 | // Sockets: 1 Strength, 2 Scale, 3 Detail, 4 Roughness, 5 Lacunarity, and | |
| 371 | // get_push_constant_sockets() declares them in exactly that order, so in | |
| 372 | // the batch path this emits the same text it always did -- param0 strength, | |
| 373 | // param1 scale, param2 detail, param3 roughness, param4 lacunarity. | |
| 374 | // | |
| 375 | // ⛔ IN THE DENSITY PATH IT DID NOT. compile_to_function replaced | |
| 376 | // `pc.paramN` with socket N's default, one convention against the other, so | |
| 377 | // every argument arrived one socket early and `* pc.param0` -- the strength | |
| 378 | // -- became socket 0, which is GEOMETRY and has no float literal, so it was | |
| 379 | // emitted as `0.0`. Measured 2026-09-06: | |
| 380 | // | |
| 381 | // vec3 npos = vec3(-pos.x, pos.z, pos.y) * 1.0; // Strength as Scale | |
| 382 | // float n = bp_perlin_fbm_3(npos, float(int(5.0)), 4.0, 0.5, true) ... | |
| 383 | // pos += normal * n * 0.0; // displacement x 0 | |
| 384 | // | |
| 385 | // Displace was a silent no-op in every emitted density shader. | |
| 386 | return String("vec3 npos = vec3(-pos.x, pos.z, pos.y) * ") + p_ctx.param(p_node_idx, 2) + | |
| 387 | ";\n\tfloat n = bp_perlin_fbm_3(npos, float(int(" + p_ctx.param(p_node_idx, 3) + | |
| 388 | ")), " + p_ctx.param(p_node_idx, 4) + ", " + p_ctx.param(p_node_idx, 5) + | |
| 389 | ", true) * 2.0 - 1.0;\n\tpos += normal * n * " + p_ctx.param(p_node_idx, 1) + ";"; | |
| 345 | 390 | } |
| 346 | 391 | |
| 347 | 392 | // --- CPU helper: hash-based random for Voronoi and white noise --- |
| … | … | @@ -98,6 +98,14 @@ public: |
| 98 | 98 | bool supports_gpu() const override { return true; } |
| 99 | 99 | String compile_glsl_prelude(int p_node_idx) const override; |
| 100 | 100 | |
| 101 | // Strength, Scale, Detail, Roughness, Lacunarity -- five floats in | |
| 102 | // param0..param4. See ProcNode::get_push_constant_sockets(). | |
| 103 | PackedInt32Array get_push_constant_sockets() const override { | |
| 104 | PackedInt32Array m; | |
| 105 | m.push_back(1); m.push_back(2); m.push_back(3); m.push_back(4); m.push_back(5); | |
| 106 | return m; | |
| 107 | } | |
| 108 | ||
| 101 | 109 | void execute(ProcEvalContext &p_ctx, int p_node_idx) override; |
| 102 | 110 | String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const override; |
| 103 | 111 |