OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes commits / 37c71e1

gpu: the batch fuses the value cone, so a field reaches the GPU at all
The batch admitted three node types BY NAME and ran one node per dispatch, whose only channel for a socket was the push-constant block. So a field feeding a batched geometry node -- Set Position's Offset, the ordinary way to displace anything -- had nowhere to go, and both it and its consumer fell to the CPU. It was not missing GLSL: every one of the twenty-one nodes involved emits GLSL, and GpuCompiler::compile_batch has always looped over a node LIST threading values through GlslEmitContext. The executor simply never handed it more than one node. Now a batch is a geometry node PLUS the run of value nodes feeding it, emitted into one shader and dispatched once. Three pieces: GpuBatch::fused_nodes the graph nodes in this shader, topological, geometry node last GlslEmitContext knows who is in the shader with it, so param() resolves a connected socket to the producer's LOCAL instead of a push-constant slot -- and only when that producer is actually emitted here _execute_gpu_batch value nodes contribute arithmetic and nothing else: no dispatch, no buffers, no CPU execution execution report before { MeshGrid: cpu, Position: cpu, SetPosition: cpu } after { MeshGrid: cpu, Position: gpu (fused), VectorMath: gpu (fused), SetPosition: gpu } CPU vs GPU worst |dp| = 0.0 MUTATION-CHECKED: making param() ignore is_emitted() reproduces the exact divergence this started from, worst |dp| = 3.191786289, and fails that one line while everything else stays green. ⚠ Two restrictions, both stated rather than discovered later: * CONTIGUOUS ONLY. A batch is a SPAN of the execution order and the main loop skips it by advancing the index, so a cone with an unrelated node interleaved cannot be expressed without changing that contract. Topological order groups a producer with its consumer in the ordinary case; when it does not, the value node stays on the CPU. Slower, never wrong. * EVERY CONSUMER MUST BE INSIDE THE BATCH. A fused node's CPU execute() never runs, so it writes no output socket -- absorbing one that something downstream also reads would hand that reader a silently empty value. The walk stops at the first candidate that fails this. Unmoved by the change: glsl_golden_gate 32/32, volume_mesh_gate 4/4, sdf_grid_gate 3/3, and the bridged mountain still bakes 48149 verts across y -20.46 .. -0.05. ⛔ A trap worth the line: `git checkout --` to undo a MUTATION also reverts the FIX in the same file when the fix is not yet committed. It took the param() change with it and the gate was rebuilt against a library missing the very thing it was proving. Check what the file contains, not that the mutation is gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37c71e10fcb677cd10da0c495a51fc6288952cb8
by plankatron <fleeky@gmail.com> · 2026-09-06T19:08:34.000Z

Changed files (5) +173 -2

src/gpu/glsl_emit_context.cpp +10 -0
……@@ -63,6 +63,16 @@ String GlslEmitContext::param(int p_node_idx, int p_socket) const {
6363 if (!push_constants || graph == nullptr) {
6464 return in(p_node_idx, p_socket);
6565 }
66 // ⛔ A PUSH CONSTANT IS ONE VALUE FOR EVERY VERTEX, so a socket driven by a
67 // per-vertex producer must not read one. When that producer is emitted into
68 // this same shader its result is a local, and reading the local is both
69 // correct and free. When it is NOT -- a single-node batch fed from outside
70 // -- the push constant is all there is, and the executor refuses the batch
71 // if the value behind it is a field.
72 const int src = graph->get_input_source_node(p_node_idx, p_socket);
73 if (src >= 0 && is_emitted(src)) {
74 return in(p_node_idx, p_socket);
75 }
6676 Ref<ProcNode> node = graph->get_node(p_node_idx);
6777 if (node.is_null()) {
6878 return in(p_node_idx, p_socket);
src/gpu/glsl_emit_context.h +14 -0
……@@ -36,6 +36,20 @@ class ProcNodeGraph;
3636struct GlslEmitContext {
3737 const ProcNodeGraph *graph = nullptr;
3838
39 // Graph node indices emitted into THIS shader. A connected socket whose
40 // producer is in here resolves to that producer's local; one whose producer
41 // is not has to keep coming through the push-constant block, because the
42 // local it would name does not exist in this translation unit.
43 Vector<int> emitted_nodes;
44 bool is_emitted(int p_node_idx) const {
45 for (int i = 0; i < emitted_nodes.size(); i++) {
46 if (emitted_nodes[i] == p_node_idx) {
47 return true;
48 }
49 }
50 return false;
51 }
52
3953 // Wrap integer (octave/detail) parameters in min(N, pn_max_octaves). The
4054 // density path passes true; the batch path does not have that uniform.
4155 bool clamp_octaves = false;
src/gpu/gpu_compiler.cpp +1 -0
……@@ -162,6 +162,7 @@ String GpuCompiler::compile_batch(
162162 // GraphExecutor refills it on every dispatch. See GlslEmitContext.
163163 ctx.push_constants = true;
164164 ctx.graph = p_graph;
165 ctx.emitted_nodes = p_node_indices;
165166
166167 String main_body;
167168 String preludes;
src/gpu/gpu_compiler.h +5 -0
……@@ -10,6 +10,11 @@ namespace godot {
1010struct GpuBatch {
1111 int first_index = 0; // Index into ExecutionPlan::execution_order
1212 int node_count = 0; // Number of consecutive nodes in this batch
13 // THE NODES EMITTED INTO THIS SHADER, in topological order, ending with the
14 // geometry node that writes `pos`. Everything before it is a value/field
15 // producer whose arithmetic is inlined rather than computed on the CPU and
16 // squeezed through a push constant.
17 Vector<int> fused_nodes;
1318 RID shader; // Compiled compute shader
1419 RID pipeline; // Compute pipeline
1520 String compiled_glsl; // For debugging
src/graph/graph_executor.cpp +143 -2
……@@ -847,6 +847,26 @@ Dictionary &GraphExecutor::exec_report() {
847847 return report;
848848}
849849
850// A node that contributes ARITHMETIC to a batch rather than geometry: it has
851// GLSL, it is not one of the geometry ops, and it produces no geometry. Fusing
852// these is the whole point -- a field feeding Set Position's Offset used to be
853// computed per vertex on the CPU and then squeezed through a push constant that
854// can only carry ONE value, which is why it could not come along.
855static bool _is_gpu_value_node(const Ref<ProcNode> &p_node) {
856 if (p_node.is_null() || _is_gpu_geometry_node(p_node)) {
857 return false;
858 }
859 if (!p_node->supports_gpu() || p_node->gpu_transparent()) {
860 return false;
861 }
862 for (int i = 0; i < p_node->get_output_count(); i++) {
863 if (p_node->get_output_def(i).type == SocketType::Geometry) {
864 return false;
865 }
866 }
867 return true;
868}
869
850870void GraphExecutor::_detect_gpu_batches(const ProcNodeGraph &p_graph, ExecutionPlan &p_plan) {
851871 // Find contiguous runs of GPU-batchable nodes in execution order
852872 int batch_start = -1;
……@@ -880,11 +900,89 @@ void GraphExecutor::_detect_gpu_batches(const ProcNodeGraph &p_graph, ExecutionP
880900 p_plan.gpu_batches.push_back(batch);
881901 }
882902
903 // ── EXTEND EACH BATCH BACKWARDS OVER THE VALUE NODES FEEDING IT ─────────
904 //
905 // ⚠ CONTIGUOUS ONLY, and deliberately: a batch is a SPAN of the execution
906 // order (first_index + node_count) and the main loop skips it by advancing
907 // the index, so a cone with an unrelated node interleaved cannot be
908 // expressed without changing that contract. Topological order groups a
909 // producer with its consumer in the ordinary case; when it does not, the
910 // value node simply stays on the CPU as before. Nothing gets WRONG, only
911 // slower, which is the right way round for a restriction.
912 for (int b = 0; b < p_plan.gpu_batches.size(); b++) {
913 GpuBatch &batch = p_plan.gpu_batches.ptrw()[b];
914 int start = batch.first_index;
915 while (start > 0) {
916 const int prev_order = start - 1;
917 // Never swallow a node another batch already owns.
918 bool taken = false;
919 for (int o = 0; o < p_plan.gpu_batches.size(); o++) {
920 const GpuBatch &other = p_plan.gpu_batches[o];
921 if (o != b && prev_order >= other.first_index &&
922 prev_order < other.first_index + other.node_count) {
923 taken = true;
924 break;
925 }
926 }
927 if (taken) {
928 break;
929 }
930 const int prev_idx = p_plan.execution_order[prev_order];
931 Ref<ProcNode> prev = p_graph.get_node(prev_idx);
932 if (!_is_gpu_value_node(prev)) {
933 break;
934 }
935 // ⛔ EVERY CONSUMER MUST BE INSIDE THE BATCH. A fused node is part of
936 // the SPAN the main loop skips, so its CPU execute() never runs and
937 // it writes no output socket. Absorb one that something downstream
938 // also reads and that reader gets nothing -- a silently empty value,
939 // which is the worst shape of bug this file can produce.
940 bool all_consumers_inside = true;
941 const PackedInt32Array conns = p_graph.get_connections_flat();
942 for (int c = 0; c + 3 < conns.size(); c += 4) {
943 if (conns[c] != prev_idx) {
944 continue; // not this node's output
945 }
946 const int consumer = conns[c + 2];
947 bool inside = false;
948 for (int k = prev_order; k < batch.first_index + batch.node_count; k++) {
949 if (p_plan.execution_order[k] == consumer) {
950 inside = true;
951 break;
952 }
953 }
954 if (!inside) {
955 all_consumers_inside = false;
956 break;
957 }
958 }
959 if (!all_consumers_inside) {
960 break;
961 }
962 start = prev_order;
963 }
964 batch.node_count += batch.first_index - start;
965 batch.first_index = start;
966 batch.fused_nodes.clear();
967 for (int k = 0; k < batch.node_count; k++) {
968 batch.fused_nodes.push_back(p_plan.execution_order[batch.first_index + k]);
969 }
970 }
971
883972 if (p_plan.gpu_batches.size() > 0) {
884973 UtilityFunctions::print("ProcNodes: Detected ", p_plan.gpu_batches.size(), " GPU batch(es)");
885974 }
886975}
887976
977static bool _batch_contains(const GpuBatch &p_batch, int p_node_idx) {
978 for (int i = 0; i < p_batch.fused_nodes.size(); i++) {
979 if (p_batch.fused_nodes[i] == p_node_idx) {
980 return true;
981 }
982 }
983 return false;
984}
985
888986void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p_batch, const ExecutionPlan &p_plan) {
889987 RenderingDevice *rd = p_ctx.gpu_buffers.get_rd();
890988 if (!rd) {
……@@ -903,6 +1001,14 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p
9031001
9041002 StringName type = node->get_node_type();
9051003
1004 // A fused VALUE node contributes arithmetic to the shader and nothing
1005 // else: no dispatch, no buffers, no CPU execution. Its result is a local
1006 // the geometry node below reads.
1007 if (!_is_gpu_geometry_node(node)) {
1008 exec_report()[String(type)] = "gpu (fused)";
1009 continue;
1010 }
1011
9061012 // TransformGeometry is too complex for Phase 4 — fall back to CPU
9071013 if (type == StringName("TransformGeometry")) {
9081014 exec_report()[String(type)] = "cpu";
……@@ -946,6 +1052,12 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p
9461052 // and the mesh was simply wrong.
9471053 //
9481054 // A connected CONSTANT is still fine -- one value is what it is.
1055 // ...unless the producer is emitted into this same shader, in
1056 // which case the socket reads that producer's LOCAL and the
1057 // push-constant slot behind it is never looked at.
1058 if (_batch_contains(p_batch, p_ctx.graph->get_input_source_node(node_idx, si))) {
1059 continue;
1060 }
9491061 const SocketValue raw = p_ctx._read_input_raw(node_idx, si);
9501062 if (raw.is_field || raw.is_lazy) {
9511063 unrepresentable = true;
……@@ -988,10 +1100,25 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p
9881100
9891101 // For Phase 4 single-node batches, compile per node type
9901102 if (!mutable_batch_ref.pipeline.is_valid()) {
1103 // EVERY fused node, in topological order, ending with this one.
1104 // GpuCompiler::compile_batch has always looped over a list and
1105 // threaded values through GlslEmitContext; it was simply never
1106 // handed more than one node.
9911107 Vector<Ref<ProcNode>> batch_nodes;
9921108 Vector<int> batch_indices;
993 batch_nodes.push_back(node);
994 batch_indices.push_back(node_idx);
1109 for (int f = 0; f < p_batch.fused_nodes.size(); f++) {
1110 const int fidx = p_batch.fused_nodes[f];
1111 Ref<ProcNode> fnode = p_ctx.graph->get_node(fidx);
1112 if (fnode.is_null()) {
1113 continue;
1114 }
1115 batch_nodes.push_back(fnode);
1116 batch_indices.push_back(fidx);
1117 }
1118 if (batch_indices.is_empty()) {
1119 batch_nodes.push_back(node);
1120 batch_indices.push_back(node_idx);
1121 }
9951122
9961123 String glsl = GpuCompiler::compile_batch(batch_nodes, batch_indices, p_ctx.graph);
9971124 if (glsl.is_empty()) {
……@@ -1090,10 +1217,20 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p
10901217 for (int m = 0; m < pc_map.size(); m++) {
10911218 const int sock = pc_map[m];
10921219 const int stype = node->get_input_type(sock);
1220 // A socket whose producer is fused is read from that producer's
1221 // local, so its slot is never sampled. Skip the READ but keep
1222 // the slot -- param() walks the same map to compute addresses,
1223 // and dropping a slot here would shift every one after it.
1224 const bool fused_src = _batch_contains(
1225 p_batch, p_ctx.graph->get_input_source_node(node_idx, sock));
10931226 if (stype == (int)SocketType::Vector) {
10941227 if (slot + 3 > 7) {
10951228 break;
10961229 }
1230 if (fused_src) {
1231 slot += 3;
1232 continue;
1233 }
10971234 const Vector3 v = p_ctx.read_vector(node_idx, sock);
10981235 const float comp[3] = { (float)v.x, (float)v.y, (float)v.z };
10991236 for (int c = 0; c < 3; c++, slot++) {
……@@ -1103,6 +1240,10 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p
11031240 if (slot + 1 > 7) {
11041241 break;
11051242 }
1243 if (fused_src) {
1244 slot += 1;
1245 continue;
1246 }
11061247 const float f = (stype == (int)SocketType::Int)
11071248 ? (float)p_ctx.read_int(node_idx, sock)
11081249 : p_ctx.read_float(node_idx, sock);