| … | … | @@ -847,6 +847,26 @@ Dictionary &GraphExecutor::exec_report() { |
| 847 | 847 | return report; |
| 848 | 848 | } |
| 849 | 849 | |
| 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. |
| 855 | static 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 | |
| 850 | 870 | void GraphExecutor::_detect_gpu_batches(const ProcNodeGraph &p_graph, ExecutionPlan &p_plan) { |
| 851 | 871 | // Find contiguous runs of GPU-batchable nodes in execution order |
| 852 | 872 | int batch_start = -1; |
| … | … | @@ -880,11 +900,89 @@ void GraphExecutor::_detect_gpu_batches(const ProcNodeGraph &p_graph, ExecutionP |
| 880 | 900 | p_plan.gpu_batches.push_back(batch); |
| 881 | 901 | } |
| 882 | 902 | |
| 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 | |
| 883 | 972 | if (p_plan.gpu_batches.size() > 0) { |
| 884 | 973 | UtilityFunctions::print("ProcNodes: Detected ", p_plan.gpu_batches.size(), " GPU batch(es)"); |
| 885 | 974 | } |
| 886 | 975 | } |
| 887 | 976 | |
| 977 | static 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 | |
| 888 | 986 | void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p_batch, const ExecutionPlan &p_plan) { |
| 889 | 987 | RenderingDevice *rd = p_ctx.gpu_buffers.get_rd(); |
| 890 | 988 | if (!rd) { |
| … | … | @@ -903,6 +1001,14 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p |
| 903 | 1001 | |
| 904 | 1002 | StringName type = node->get_node_type(); |
| 905 | 1003 | |
| 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 | |
| 906 | 1012 | // TransformGeometry is too complex for Phase 4 — fall back to CPU |
| 907 | 1013 | if (type == StringName("TransformGeometry")) { |
| 908 | 1014 | exec_report()[String(type)] = "cpu"; |
| … | … | @@ -946,6 +1052,12 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p |
| 946 | 1052 | // and the mesh was simply wrong. |
| 947 | 1053 | // |
| 948 | 1054 | // 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 | } |
| 949 | 1061 | const SocketValue raw = p_ctx._read_input_raw(node_idx, si); |
| 950 | 1062 | if (raw.is_field || raw.is_lazy) { |
| 951 | 1063 | unrepresentable = true; |
| … | … | @@ -988,10 +1100,25 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p |
| 988 | 1100 | |
| 989 | 1101 | // For Phase 4 single-node batches, compile per node type |
| 990 | 1102 | 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. |
| 991 | 1107 | Vector<Ref<ProcNode>> batch_nodes; |
| 992 | 1108 | 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 | } |
| 995 | 1122 | |
| 996 | 1123 | String glsl = GpuCompiler::compile_batch(batch_nodes, batch_indices, p_ctx.graph); |
| 997 | 1124 | if (glsl.is_empty()) { |
| … | … | @@ -1090,10 +1217,20 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p |
| 1090 | 1217 | for (int m = 0; m < pc_map.size(); m++) { |
| 1091 | 1218 | const int sock = pc_map[m]; |
| 1092 | 1219 | 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)); |
| 1093 | 1226 | if (stype == (int)SocketType::Vector) { |
| 1094 | 1227 | if (slot + 3 > 7) { |
| 1095 | 1228 | break; |
| 1096 | 1229 | } |
| 1230 | if (fused_src) { |
| 1231 | slot += 3; |
| 1232 | continue; |
| 1233 | } |
| 1097 | 1234 | const Vector3 v = p_ctx.read_vector(node_idx, sock); |
| 1098 | 1235 | const float comp[3] = { (float)v.x, (float)v.y, (float)v.z }; |
| 1099 | 1236 | for (int c = 0; c < 3; c++, slot++) { |
| … | … | @@ -1103,6 +1240,10 @@ void GraphExecutor::_execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p |
| 1103 | 1240 | if (slot + 1 > 7) { |
| 1104 | 1241 | break; |
| 1105 | 1242 | } |
| 1243 | if (fused_src) { |
| 1244 | slot += 1; |
| 1245 | continue; |
| 1246 | } |
| 1106 | 1247 | const float f = (stype == (int)SocketType::Int) |
| 1107 | 1248 | ? (float)p_ctx.read_int(node_idx, sock) |
| 1108 | 1249 | : p_ctx.read_float(node_idx, sock); |