OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / graph / proc_node_graph.cpp

907 lines · cpp ref:
#include "proc_node_graph.h"

#include "graph_executor.h"
#include "../gpu/gpu_compiler.h"
#include <godot_cpp/variant/array.hpp>
#include <godot_cpp/variant/dictionary.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

namespace godot {

// --- Serialization ---

void ProcNodeGraph::_get_property_list(List<PropertyInfo> *p_list) const {
	// Order matters: nodes first (connections and defaults reference node indices)
	p_list->push_back(PropertyInfo(Variant::ARRAY, "nodes", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE));
	p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "connections", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE));
	p_list->push_back(PropertyInfo(Variant::DICTIONARY, "input_defaults", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE));
	p_list->push_back(PropertyInfo(Variant::INT, "source_frame", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_STORAGE));
}

bool ProcNodeGraph::_set(const StringName &p_name, const Variant &p_property) {
	if (p_name == StringName("nodes")) {
		Array arr = p_property;
		nodes.clear();
		for (int i = 0; i < arr.size(); i++) {
			Ref<ProcNode> node = arr[i];
			nodes.push_back(node);
		}
		return true;
	}
	if (p_name == StringName("connections")) {
		PackedInt32Array arr = p_property;
		connections.clear();
		for (int i = 0; i + 3 < arr.size(); i += 4) {
			ProcConnection c;
			c.from_node = arr[i];
			c.from_socket = arr[i + 1];
			c.to_node = arr[i + 2];
			c.to_socket = arr[i + 3];
			connections.push_back(c);
		}
		return true;
	}
	if (p_name == StringName("source_frame")) {
		source_frame = p_property;
		return true;
	}
	if (p_name == StringName("input_defaults")) {
		Dictionary dict = p_property;
		Array keys = dict.keys();
		for (int i = 0; i < keys.size(); i++) {
			String key = keys[i];
			int colon = key.find(":");
			if (colon < 0) {
				continue;
			}
			int node_idx = key.substr(0, colon).to_int();
			int socket_idx = key.substr(colon + 1).to_int();
			if (node_idx >= 0 && node_idx < nodes.size() && nodes[node_idx].is_valid()) {
				nodes[node_idx]->set_input_default(socket_idx, dict[keys[i]]);
			}
		}
		return true;
	}
	return false;
}

bool ProcNodeGraph::_get(const StringName &p_name, Variant &r_property) const {
	if (p_name == StringName("nodes")) {
		Array arr;
		for (int i = 0; i < nodes.size(); i++) {
			arr.push_back(nodes[i]);
		}
		r_property = arr;
		return true;
	}
	if (p_name == StringName("connections")) {
		PackedInt32Array arr;
		for (int i = 0; i < connections.size(); i++) {
			const ProcConnection &c = connections[i];
			arr.push_back(c.from_node);
			arr.push_back(c.from_socket);
			arr.push_back(c.to_node);
			arr.push_back(c.to_socket);
		}
		r_property = arr;
		return true;
	}
	if (p_name == StringName("source_frame")) {
		r_property = source_frame;
		return true;
	}
	if (p_name == StringName("input_defaults")) {
		Dictionary dict;
		for (int i = 0; i < nodes.size(); i++) {
			const Ref<ProcNode> &node = nodes[i];
			if (node.is_null()) {
				continue;
			}
			for (int s = 0; s < node->get_input_count(); s++) {
				if (node->is_input_default_modified(s)) {
					dict[String::num_int64(i) + ":" + String::num_int64(s)] = node->get_input_def(s).default_value;
				}
			}
		}
		r_property = dict;
		return true;
	}
	return false;
}

// --- Binding ---

Variant ProcNodeGraph::evaluate_socket(int p_node_idx, int p_socket_idx) const {
	return GraphExecutor::evaluate_socket_value(*this, p_node_idx, p_socket_idx);
}

void ProcNodeGraph::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_source_frame", "frame"), &ProcNodeGraph::set_source_frame);
	ClassDB::bind_method(D_METHOD("get_source_frame"), &ProcNodeGraph::get_source_frame);
	ClassDB::bind_method(D_METHOD("add_node", "node"), &ProcNodeGraph::add_node);
	ClassDB::bind_method(D_METHOD("remove_node", "index"), &ProcNodeGraph::remove_node);
	ClassDB::bind_method(D_METHOD("get_node", "index"), &ProcNodeGraph::get_node);
	ClassDB::bind_method(D_METHOD("get_node_count"), &ProcNodeGraph::get_node_count);
	ClassDB::bind_method(D_METHOD("connect_sockets", "from_node", "from_socket", "to_node", "to_socket"), &ProcNodeGraph::connect_sockets);
	ClassDB::bind_method(D_METHOD("disconnect_sockets", "from_node", "from_socket", "to_node", "to_socket"), &ProcNodeGraph::disconnect_sockets);
	ClassDB::bind_method(D_METHOD("evaluate_socket", "node_idx", "socket_idx"), &ProcNodeGraph::evaluate_socket);
	ClassDB::bind_method(D_METHOD("evaluate_density_batch", "positions"), &ProcNodeGraph::evaluate_density_batch);
	ClassDB::bind_method(D_METHOD("get_density_bounds"), &ProcNodeGraph::get_density_bounds);
	ClassDB::bind_method(D_METHOD("find_node_by_source_id", "source_id"), &ProcNodeGraph::find_node_by_source_id);
	ClassDB::bind_method(D_METHOD("evaluate_density_at", "pos"), &ProcNodeGraph::evaluate_density_at);
	ClassDB::bind_static_method("ProcNodeGraph",
			D_METHOD("debug_phacelle_noise", "p", "dir", "freq", "offset", "normalization"),
			&ProcNodeGraph::debug_phacelle_noise);
	ClassDB::bind_static_method("ProcNodeGraph",
			D_METHOD("debug_erosion_filter", "p", "height_and_slope", "fade_target",
					"strength", "gully_weight", "detail", "rounding", "onset",
					"assumed_slope", "scale", "octaves", "lacunarity", "gain",
					"cell_scale", "normalization", "max_octaves"),
			&ProcNodeGraph::debug_erosion_filter);
	ClassDB::bind_static_method("ProcNodeGraph",
			D_METHOD("debug_erosion_node_eval_full", "p", "gn", "height_scale",
					"erosion_strength", "cell_size", "octaves", "gain",
					"peak_height_divisor", "gully_band_scale",
					"input_height_amplitude", "octave_lacunarity", "gully_depth",
					"fine_gully_slope_restriction", "gully_normalize_threshold",
					"height_offset_pair", "rounding_set", "onset_set",
					"assumed_slope_pair", "max_octaves"),
			&ProcNodeGraph::debug_erosion_node_eval_full);
	ClassDB::bind_method(D_METHOD("get_connection_count"), &ProcNodeGraph::get_connection_count);
	// Read-only, flat [from_node, from_socket, to_node, to_socket] quadruples --
	// the same shape serialization already writes. Bound because a node whose
	// socket TYPE depends on a property cannot be checked from values alone: a
	// refused link and a narrowed one look identical downstream.
	ClassDB::bind_method(D_METHOD("get_connections_flat"), &ProcNodeGraph::get_connections_flat);
	ClassDB::bind_method(D_METHOD("get_input_link_count", "node_idx", "socket_idx"),
			&ProcNodeGraph::get_input_link_count);
	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()));
	ClassDB::bind_method(D_METHOD("compile_batch_glsl", "node_indices"), &ProcNodeGraph::compile_batch_glsl);
}

int ProcNodeGraph::add_node(const Ref<ProcNode> &p_node) {
	_density_plan_dirty = true;
	nodes.push_back(p_node);
	return nodes.size() - 1;
}

void ProcNodeGraph::remove_node(int p_idx) {
	_density_plan_dirty = true;
	if (p_idx < 0 || p_idx >= nodes.size()) {
		return;
	}

	// Remove connections involving this node
	clear_connections_for_node(p_idx);

	nodes.remove_at(p_idx);

	// Fix up connection indices
	for (int i = 0; i < connections.size(); i++) {
		ProcConnection &c = connections.ptrw()[i];
		if (c.from_node > p_idx) c.from_node--;
		if (c.to_node > p_idx) c.to_node--;
	}
}

Ref<ProcNode> ProcNodeGraph::get_node(int p_idx) const {
	if (p_idx < 0 || p_idx >= nodes.size()) {
		return Ref<ProcNode>();
	}
	return nodes[p_idx];
}

void ProcNodeGraph::connect_sockets(int p_from_node, int p_from_socket, int p_to_node, int p_to_socket) {
	_density_plan_dirty = true;
	// A CONNECTION TO A SOCKET THAT DOES NOT EXIST IS NOT MERELY DROPPED -- it
	// used to be ACCEPTED, and `_read_input_raw` then computed
	// `output_values[src_node * max_output_sockets + src_socket]` with no bound
	// on src_socket. With max_output_sockets small, socket 2 of a one-output
	// node lands on a DIFFERENT NODE'S slot and reads whatever is there; past
	// the end of the array it is out of bounds outright.
	//
	// Found as a SEGFAULT: a bridged graph read Accumulate Field's Trailing and
	// Total, which our node did not have, and the aliased slot happened to
	// contain a lazy promise naming an unrelated producer. Before Index became
	// a field the same read returned harmless garbage, so the defect had been
	// there all along and was invisible.
	//
	// Refused and named, rather than silently dropped: a graph asking for a
	// socket we do not implement is exactly what the missing-socket ledger is
	// for, and it should be visible.
	{
		Ref<ProcNode> from = get_node(p_from_node);
		Ref<ProcNode> to = get_node(p_to_node);
		if (from.is_null() || to.is_null()) {
			UtilityFunctions::push_warning(
					"ProcNodeGraph: refusing a connection with a missing node (",
					p_from_node, " -> ", p_to_node, ").");
			return;
		}
		if (p_from_socket < 0 || p_from_socket >= from->get_output_count()) {
			UtilityFunctions::push_warning(
					"ProcNodeGraph: refusing ", from->get_node_type(), " output socket ",
					p_from_socket, " -> ", to->get_node_type(), " input ", p_to_socket,
					": the producer has only ", from->get_output_count(), " outputs.");
			return;
		}
		if (p_to_socket < 0 || p_to_socket >= to->get_input_count()) {
			UtilityFunctions::push_warning(
					"ProcNodeGraph: refusing ", from->get_node_type(), " output ",
					p_from_socket, " -> ", to->get_node_type(), " input socket ",
					p_to_socket, ": the consumer has only ", to->get_input_count(),
					" inputs.");
			return;
		}
	}

	// A MULTI-INPUT SOCKET KEEPS EVERY LINK, in the order they arrive. For an
	// ordinary socket a second link still replaces the first, which is
	// Blender's behaviour too.
	{
		Ref<ProcNode> to = get_node(p_to_node);
		const bool multi = to.is_valid() && to->is_input_multi(p_to_socket);
		if (multi) {
			for (int i = 0; i < connections.size(); i++) {
				const ProcConnection &c = connections[i];
				if (c.to_node == p_to_node && c.to_socket == p_to_socket &&
						c.from_node == p_from_node && c.from_socket == p_from_socket) {
					return; // already linked; do not duplicate
				}
			}
		} else {
			for (int i = connections.size() - 1; i >= 0; i--) {
				const ProcConnection &c = connections[i];
				if (c.to_node == p_to_node && c.to_socket == p_to_socket) {
					connections.remove_at(i);
				}
			}
		}
	}

	ProcConnection c;
	c.from_node = p_from_node;
	c.from_socket = p_from_socket;
	c.to_node = p_to_node;
	c.to_socket = p_to_socket;
	connections.push_back(c);
}

void ProcNodeGraph::disconnect_sockets(int p_from_node, int p_from_socket, int p_to_node, int p_to_socket) {
	_density_plan_dirty = true;
	for (int i = connections.size() - 1; i >= 0; i--) {
		const ProcConnection &c = connections[i];
		if (c.from_node == p_from_node && c.from_socket == p_from_socket &&
				c.to_node == p_to_node && c.to_socket == p_to_socket) {
			connections.remove_at(i);
			return;
		}
	}
}

void ProcNodeGraph::clear_connections_for_node(int p_node_idx) {
	for (int i = connections.size() - 1; i >= 0; i--) {
		const ProcConnection &c = connections[i];
		if (c.from_node == p_node_idx || c.to_node == p_node_idx) {
			connections.remove_at(i);
		}
	}
}

int ProcNodeGraph::get_input_source_node(int p_node_idx, int p_socket_idx) const {
	for (int i = 0; i < connections.size(); i++) {
		const ProcConnection &c = connections[i];
		if (c.to_node == p_node_idx && c.to_socket == p_socket_idx) {
			return c.from_node;
		}
	}
	return -1;
}

int ProcNodeGraph::get_input_source_socket(int p_node_idx, int p_socket_idx) const {
	for (int i = 0; i < connections.size(); i++) {
		const ProcConnection &c = connections[i];
		if (c.to_node == p_node_idx && c.to_socket == p_socket_idx) {
			return c.from_socket;
		}
	}
	return -1;
}

int ProcNodeGraph::find_output_node() const {
	// Find the last node that has Geometry outputs but no downstream connections
	for (int i = nodes.size() - 1; i >= 0; i--) {
		const Ref<ProcNode> &node = nodes[i];
		if (node.is_null()) continue;

		bool has_geo_output = false;
		for (int o = 0; o < node->get_output_count(); o++) {
			if (node->get_output_def(o).type == SocketType::Geometry) {
				has_geo_output = true;
				break;
			}
		}
		if (!has_geo_output) continue;

		// Check if any connection uses this node as source
		bool is_source = false;
		for (int c = 0; c < connections.size(); c++) {
			if (connections[c].from_node == i) {
				is_source = true;
				break;
			}
		}
		if (!is_source) {
			return i;
		}
	}
	return nodes.size() > 0 ? nodes.size() - 1 : -1;
}

String ProcNodeGraph::compile_batch_glsl(const PackedInt32Array &p_node_indices) const {
	Vector<Ref<ProcNode>> batch_nodes;
	Vector<int> batch_indices;
	for (int i = 0; i < p_node_indices.size(); i++) {
		const int idx = p_node_indices[i];
		if (idx < 0 || idx >= nodes.size() || nodes[idx].is_null()) {
			continue;
		}
		batch_nodes.push_back(nodes[idx]);
		batch_indices.push_back(idx);
	}
	return GpuCompiler::compile_batch(batch_nodes, batch_indices, this);
}

String ProcNodeGraph::compile_to_glsl_function(const String &p_function_name,
		bool p_clamp_octaves, const PackedStringArray &p_skip_types) const {
	// TOPOLOGICAL ORDER, not graph index order. A value cannot be read before it
	// is written, and until inputs were resolved through connections the
	// distinction did not exist -- every graph on disk happens to be in index
	// order, which is what makes this change provable rather than hopeful.
	Vector<int> order;
	{
		Vector<int> indegree;
		indegree.resize(nodes.size());
		for (int i = 0; i < nodes.size(); i++) {
			indegree.write[i] = 0;
		}
		for (int c = 0; c < connections.size(); c++) {
			const ProcConnection &conn = connections[c];
			if (conn.to_node >= 0 && conn.to_node < indegree.size()) {
				indegree.write[conn.to_node]++;
			}
		}
		// Ties are broken by node index, so a graph with no ordering constraint
		// emits exactly what it always did.
		Vector<int> ready;
		for (int i = 0; i < nodes.size(); i++) {
			if (indegree[i] == 0) {
				ready.push_back(i);
			}
		}
		while (!ready.is_empty()) {
			int best = 0;
			for (int k = 1; k < ready.size(); k++) {
				if (ready[k] < ready[best]) {
					best = k;
				}
			}
			const int idx = ready[best];
			ready.remove_at(best);
			order.push_back(idx);
			for (int c = 0; c < connections.size(); c++) {
				const ProcConnection &conn = connections[c];
				if (conn.from_node != idx) {
					continue;
				}
				if (conn.to_node < 0 || conn.to_node >= indegree.size()) {
					continue;
				}
				if (--indegree.write[conn.to_node] == 0) {
					ready.push_back(conn.to_node);
				}
			}
		}
		// A cycle leaves nodes unvisited. Emit them in index order afterwards
		// rather than dropping them: a cycle is a graph bug, and losing nodes
		// silently is the failure this whole change exists to remove.
		if (order.size() != nodes.size()) {
			for (int i = 0; i < nodes.size(); i++) {
				if (!order.has(i)) {
					order.push_back(i);
				}
			}
		}
	}

	// ONE GRAPH, TWO PRODUCTS.
	//
	// A tree bridged from Blender is a MESH tree that also declares a density:
	// the mountain subdivides a cage, displaces it and solidifies it through an
	// SDF grid, and separately says "this float is my density" on its group
	// interface. Emitting every node then refuses on SubdivideMesh -- a node
	// that cannot affect the density and has no business being in the density
	// shader at all.
	//
	// So when the graph DECLARES a density, only the cone that feeds it is
	// emitted. This is not the silent drop the refusal below exists to catch:
	// a node INSIDE the cone that cannot emit still refuses loudly, and the
	// nodes excluded are excluded because the density provably does not read
	// them. With no declared density the whole graph is emitted exactly as
	// before, so every existing heightfield graph is untouched.
	bool cone_only = false;
	Vector<bool> in_cone;
	{
		int dn = -1, ds = -1;
		if (find_density_output(dn, ds)) {
			const int src = get_input_source_node(dn, ds);
			if (src >= 0) {
				cone_only = true;
				in_cone.resize(nodes.size());
				for (int i = 0; i < in_cone.size(); i++) {
					in_cone.write[i] = false;
				}
				// Backward reachability over connections, iterated to a fixed
				// point. The graph is small and this runs once per compile.
				in_cone.write[src] = true;
				bool grew = true;
				while (grew) {
					grew = false;
					for (int c = 0; c < connections.size(); c++) {
						const ProcConnection &conn = connections[c];
						if (conn.to_node < 0 || conn.to_node >= in_cone.size()) continue;
						if (conn.from_node < 0 || conn.from_node >= in_cone.size()) continue;
						if (in_cone[conn.to_node] && !in_cone[conn.from_node]) {
							in_cone.write[conn.from_node] = true;
							grew = true;
						}
					}
				}
			}
		}
	}

	Vector<Ref<ProcNode>> gpu_nodes;
	Vector<int> gpu_indices;

	for (int k = 0; k < order.size(); k++) {
		const int i = order[k];
		if (nodes[i].is_null()) {
			continue;
		}
		// A SKIP LIST ENTRY IS DELIBERATE -- dc-terrain passes ErosionNoise when
		// the erosion toggle is off -- and stays a skip.
		if (p_skip_types.has(String(nodes[i]->get_node_type()))) {
			continue;
		}
		// Outside the declared density's cone: it cannot change the answer.
		if (cone_only && i < in_cone.size() && !in_cone[i]) {
			continue;
		}
		// ANYTHING ELSE THAT CANNOT BE EMITTED IS A REFUSAL, NOT A SKIP. This
		// used to drop the node and emit the rest: adding a Math node to
		// map_dc_erosion's graph moved the emitted text by one comment line and
		// the node's arithmetic was simply gone, so the GPU evaluated a
		// different field from the CPU with nothing said. An empty return here
		// leaves the consumer's previous shader running -- dc-terrain's
		// _recompile_glsl already treats empty as "keep what you have" -- which
		// is a stale world rather than a wrong one.
		// A boundary node carries no operation, so emitting nothing is correct
		// for it and is not the silent drop this refusal exists to catch.
		//
		// ⛔ IT IS STILL INCLUDED. Skipping it outright meant Group Input --
		// which carries the modifier's exposed PARAMETERS on its outputs, and
		// writes them in execute() -- never declared them, so every consumer
		// referenced `n0_sN` and the shader would not compile:
		//
		//     error: 'n0_s19' : undeclared identifier   (x16, bridged mountain)
		//
		// compile_to_function accepts an empty emission from a transparent node
		// and refuses it from any other, so a boundary node that has nothing to
		// say still says nothing.
		if (nodes[i]->gpu_transparent() && !nodes[i]->supports_gpu()) {
			continue;
		}
		if (!nodes[i]->supports_gpu()) {
			UtilityFunctions::push_error(
					"proc-nodes: node ", i, " (", nodes[i]->get_node_type(),
					") has no GPU implementation, so this graph cannot be compiled to GLSL. "
					"Emission refused rather than silently dropping the node.");
			return String();
		}
		gpu_nodes.push_back(nodes[i]);
		gpu_indices.push_back(i);
	}

	if (gpu_nodes.size() == 0) {
		return "float " + p_function_name + "(vec3 pos) { return -pos.y; }\n";
	}

	return GpuCompiler::compile_to_function(gpu_nodes, gpu_indices, p_function_name, p_clamp_octaves, this);
}

// --- The CPU side of proc_density ------------------------------------------

// Which value the graph's density IS, by the same rules gpu_compiler.cpp uses
// for the emitted function. Returns the node and socket of an explicit Group
// Output float, or false for "the displacement of pos.y".
bool ProcNodeGraph::find_density_output(int &r_node, int &r_socket) const {
	for (int i = 0; i < get_node_count(); i++) {
		Ref<ProcNode> n = get_node(i);
		// TWO WAYS TO SAY IT, one rule. A Group Output float is the graph
		// declaring its result through its interface; a Volume to Mesh's
		// Density input is an authored VOLUME declaring the same thing at a
		// terminal. Both are "this float is my density", and a volume graph
		// should not have to carry a Group Output to be compilable.
		if (n.is_null()) {
			continue;
		}
		const StringName ty = n->get_node_type();
		if (ty == StringName("VolumeToMesh")) {
			// SOCKET 0 EXPLICITLY. Volume to Mesh has Resolution, Threshold and
			// bounds sockets too, and "the first connected Float" would happily
			// pick Threshold if Density were left unwired -- a density that is
			// silently the iso level. Its Density input is socket 0 by
			// construction, so name it.
			if (n->get_input_count() > 0 && get_input_source_node(i, 0) >= 0) {
				r_node = i;
				r_socket = 0;
				return true;
			}
			continue;
		}
		if (ty != StringName("GroupOutput")) {
			continue;
		}
		for (int s = 0; s < n->get_input_count(); s++) {
			if (n->get_input_def(s).type != SocketType::Float) {
				continue;
			}
			if (get_input_source_node(i, s) < 0) {
				continue;
			}
			r_node = i;
			r_socket = s;
			return true;
		}
	}
	return false;
}

int ProcNodeGraph::find_node_by_source_id(int p_source_id) const {
	if (p_source_id < 0) {
		return -1;
	}
	for (int i = 0; i < get_node_count(); i++) {
		Ref<ProcNode> n = get_node(i);
		if (n.is_valid() && n->get_source_id() == p_source_id) {
			return i;
		}
	}
	return -1;
}

Dictionary ProcNodeGraph::get_density_bounds() const {
	Dictionary out;
	// The same terminal the density comes from: the graph's Group Output. Read
	// by NAME, because a group interface's socket ORDER is the author's and
	// changing it in Blender must not silently re-point a bound.
	for (int i = 0; i < get_node_count(); i++) {
		Ref<ProcNode> n = get_node(i);
		if (n.is_null() || n->get_node_type() != StringName("GroupOutput")) {
			continue;
		}
		bool have_min = false, have_max = false;
		Vector3 bmin, bmax;
		float voxel = 0.0f;
		for (int s = 0; s < n->get_input_count(); s++) {
			const StringName nm = n->get_input_def(s).name;
			// A bound may be wired from a value node or left as the socket's
			// own default; read whichever the author used.
			Variant v;
			const int src = get_input_source_node(i, s);
			if (src >= 0) {
				v = GraphExecutor::evaluate_socket_value(
						*this, src, get_input_source_socket(i, s), Dictionary());
			} else {
				v = n->get_input_default(s);
			}
			if (nm == StringName("Bounds Min")) {
				bmin = v;
				have_min = true;
			} else if (nm == StringName("Bounds Max")) {
				bmax = v;
				have_max = true;
			} else if (nm == StringName("Voxel Size")) {
				voxel = (float)(double)v;
			}
		}
		if (!have_min || !have_max) {
			// Half a box is not a box. An unbounded field is a legitimate
			// state; a graph that names one corner and not the other is an
			// authoring mistake, and answering with a default corner would
			// place the world somewhere nobody asked for.
			if (have_min != have_max) {
				UtilityFunctions::push_warning(
						"proc-nodes: the graph declares only one of Bounds Min / "
						"Bounds Max on its Group Output. Both are needed; treating "
						"the field as unbounded.");
			}
			continue;
		}
		if (is_blender_frame()) {
			// M, self-inverse, the same conversion _to_godot_frame applies to
			// geometry and the emitter applies to the query point.
			bmin = Vector3(-bmin.x, bmin.z, bmin.y);
			bmax = Vector3(-bmax.x, bmax.z, bmax.y);
			// ⛔ M NEGATES x, so the converted pair is NOT sorted. Re-sort
			// componentwise or the box comes out inverted on that axis.
			const Vector3 lo(MIN(bmin.x, bmax.x), MIN(bmin.y, bmax.y), MIN(bmin.z, bmax.z));
			const Vector3 hi(MAX(bmin.x, bmax.x), MAX(bmin.y, bmax.y), MAX(bmin.z, bmax.z));
			bmin = lo;
			bmax = hi;
		}
		out["min"] = bmin;
		out["max"] = bmax;
		out["voxel_size"] = voxel;
		return out;
	}
	return out;
}

static bool _density_output_socket(const ProcNodeGraph &p_graph, int &r_node, int &r_socket) {
	return p_graph.find_density_output(r_node, r_socket);
}

ProcNodeGraph::~ProcNodeGraph() {
	delete _density_plan;
	_density_plan = nullptr;
}

PackedFloat32Array ProcNodeGraph::evaluate_density_batch(const PackedVector3Array &p_positions) {
	PackedFloat32Array out;
	const int n = p_positions.size();
	out.resize(n);
	if (n == 0) {
		return out;
	}
	float *w = out.ptrw();
	for (int i = 0; i < n; i++) {
		w[i] = 0.0f;
	}

	// One point of geometry per position. The graph displaces them exactly as
	// it displaces a mesh's vertices, which is what the shader's `pos` is.
	Ref<ProcGeometry> in_geo;
	in_geo.instantiate();
	Ref<ProcMesh> in_mesh = in_geo->ensure_mesh();
	// ⛔ THE QUERY POINT IS IN THE CONSUMER'S FRAME, THE GRAPH IS IN ITS OWN.
	//
	// A bridged graph runs in Blender's frame and GraphExecutor converts its
	// RESULT on the way out (_to_godot_frame). The input was never converted,
	// so this compared an M-converted dst against an unconverted src and the
	// two axes did not correspond: with zero displacement the density came back
	// as src.z - src.y instead of 0.
	//
	// M on the way in, self-inverse, matching what compile_to_function now
	// emits. src stays the caller's own point, so dst.y - src.y below is still
	// the displacement along the CONSUMER's up.
	if (is_blender_frame()) {
		PackedVector3Array conv;
		conv.resize(n);
		Vector3 *cw = conv.ptrw();
		const Vector3 *pr = p_positions.ptr();
		for (int i = 0; i < n; i++) {
			cw[i] = Vector3(-pr[i].x, pr[i].z, pr[i].y);
		}
		in_mesh->positions = conv;
	} else {
		in_mesh->positions = p_positions;
	}
	in_mesh->normals.resize(n);
	for (int i = 0; i < n; i++) {
		in_mesh->normals.ptrw()[i] = Vector3(0, 1, 0);
	}

	Dictionary overrides;
	overrides["geometry"] = in_geo;

	if (_density_plan == nullptr) {
		_density_plan = new ExecutionPlan();
		_density_plan_dirty = true;
	}
	if (_density_plan_dirty) {
		*_density_plan = GraphExecutor::compile(*this);
		_density_plan_dirty = false;
	}
	// use_gpu FALSE: this IS the CPU answer, and the point of it is to agree
	// with the shader rather than to be the shader.
	Ref<ProcGeometry> result = GraphExecutor::execute(*this, *_density_plan, overrides, false,
			nullptr, 0.016f, nullptr);
	// ⛔ A NULL GEOMETRY IS NOT A FAILURE FOR A GRAPH THAT DECLARES A DENSITY.
	//
	// This used to `return out` right here, BEFORE the density lookup below ever
	// ran. A graph whose density is declared on its own Group Output interface
	// has no geometry chain -- that is the point of it -- so execute()
	// legitimately returns nothing and every point came back 0.0.
	//
	// Measured 2026-09-08 on the bridged voronoi mountain terminated that way:
	// the emitted GLSL produced a correct signed 3D field while this returned
	// zero at all 100 points, and find_density_output was finding the socket
	// perfectly well a few lines further down than the function ever reached.
	// The GPU and CPU halves of one graph disagreed completely, and the reason
	// was an early return, not any arithmetic.
	//
	// Only the DISPLACEMENT path needs a mesh, so the check moves down to it.
	int out_node = -1, out_socket = -1;
	if (_density_output_socket(*this, out_node, out_socket)) {
		// ⛔ TWO BUGS IN ONE LINE, both fixed here, in BOTH density entry points.
		//
		// out_socket is an INPUT socket of the terminal (Group Output's float,
		// Volume to Mesh's Density). evaluate_socket_value reads an OUTPUT socket,
		// so this read a different slot entirely. What the density IS lives on the
		// node FEEDING that input.
		//
		// And it passed no overrides, so the fresh run evaluated against NO
		// GEOMETRY -- discarding the points built immediately above and answering
		// from an empty context. Measured 2026-09-06: a unit-sphere SDF returned
		// 0.0 at every one of five points, inside and outside, through both the
		// Group Output and the Volume to Mesh spelling. Nothing in proc-nodes,
		// dc-terrain or hyper-tps calls these, which is why it stood.
		const int src_node = get_input_source_node(out_node, out_socket);
		const int src_socket = get_input_source_socket(out_node, out_socket);
		if (src_node < 0) {
			return out; // declared an output and wired nothing to it
		}
		const Variant v = GraphExecutor::evaluate_socket_value(
				*this, src_node, src_socket, overrides);
		// A scalar output is the same value at every point; a field is per point.
		if (v.get_type() == Variant::PACKED_FLOAT32_ARRAY) {
			PackedFloat32Array f = v;
			for (int i = 0; i < n && i < f.size(); i++) {
				w[i] = f[i];
			}
		} else {
			const float c = (float)(double)v;
			for (int i = 0; i < n; i++) {
				w[i] = c;
			}
		}
		return out;
	}

	if (result.is_null()) {
		// No density terminal AND no geometry -- nothing to answer with.
		return out;
	}
	Ref<ProcMesh> out_mesh = result->get_mesh();
	if (out_mesh.is_null() || out_mesh->positions.size() != n) {
		// A graph that changed the point COUNT has no per-input answer, and
		// pairing them by index would invent one.
		UtilityFunctions::push_warning(
				"proc-nodes: evaluate_density_batch got ",
				out_mesh.is_null() ? 0 : out_mesh->positions.size(),
				" points back for ", n, " in. A graph that changes the point count "
				"cannot answer a density per input point.");
		return out;
	}
	const Vector3 *src = p_positions.ptr();
	const Vector3 *dst = out_mesh->positions.ptr();
	for (int i = 0; i < n; i++) {
		w[i] = dst[i].y - src[i].y;
	}
	return out;
}

float ProcNodeGraph::evaluate_density_at(const Vector3 &p_pos) {
	// The scratch geometry is built ONCE and its single position rewritten per
	// query. Going through evaluate_density_batch instead allocates a fresh
	// ProcGeometry and ProcMesh every call, and a collision query cannot pay
	// for that.
	if (_density_scratch.is_null()) {
		_density_scratch.instantiate();
		Ref<ProcMesh> m = _density_scratch->ensure_mesh();
		m->positions.resize(1);
		m->normals.resize(1);
		m->normals.ptrw()[0] = Vector3(0, 1, 0);
	}
	Ref<ProcMesh> mesh = _density_scratch->get_mesh();
	mesh->positions.ptrw()[0] = p_pos;

	if (_density_plan == nullptr) {
		_density_plan = new ExecutionPlan();
		_density_plan_dirty = true;
	}
	if (_density_plan_dirty) {
		*_density_plan = GraphExecutor::compile(*this);
		_density_plan_dirty = false;
	}

	Dictionary overrides;
	overrides["geometry"] = _density_scratch;
	Ref<ProcGeometry> result = GraphExecutor::execute(*this, *_density_plan, overrides, false,
			nullptr, 0.016f, nullptr);
	if (result.is_null()) {
		return 0.0f;
	}

	int out_node = -1, out_socket = -1;
	if (_density_output_socket(*this, out_node, out_socket)) {
		// ⛔ TWO BUGS IN ONE LINE, both fixed here, in BOTH density entry points.
		//
		// out_socket is an INPUT socket of the terminal (Group Output's float,
		// Volume to Mesh's Density). evaluate_socket_value reads an OUTPUT socket,
		// so this read a different slot entirely. What the density IS lives on the
		// node FEEDING that input.
		//
		// And it passed no overrides, so the fresh run evaluated against NO
		// GEOMETRY -- discarding the points built immediately above and answering
		// from an empty context. Measured 2026-09-06: a unit-sphere SDF returned
		// 0.0 at every one of five points, inside and outside, through both the
		// Group Output and the Volume to Mesh spelling. Nothing in proc-nodes,
		// dc-terrain or hyper-tps calls these, which is why it stood.
		const int src_node = get_input_source_node(out_node, out_socket);
		const int src_socket = get_input_source_socket(out_node, out_socket);
		if (src_node < 0) {
			return 0.0f; // declared an output and wired nothing to it
		}
		const Variant v = GraphExecutor::evaluate_socket_value(
				*this, src_node, src_socket, overrides);
		if (v.get_type() == Variant::PACKED_FLOAT32_ARRAY) {
			PackedFloat32Array f = v;
			return f.size() > 0 ? f[0] : 0.0f;
		}
		return (float)(double)v;
	}

	Ref<ProcMesh> out_mesh = result->get_mesh();
	if (out_mesh.is_null() || out_mesh->positions.size() != 1) {
		return 0.0f;
	}
	return out_mesh->positions[0].y - p_pos.y;
}

PackedInt32Array ProcNodeGraph::get_connections_flat() const {
	PackedInt32Array out;
	out.resize(connections.size() * 4);
	int *w = out.ptrw();
	for (int i = 0; i < connections.size(); i++) {
		w[i * 4 + 0] = connections[i].from_node;
		w[i * 4 + 1] = connections[i].from_socket;
		w[i * 4 + 2] = connections[i].to_node;
		w[i * 4 + 3] = connections[i].to_socket;
	}
	return out;
}

int ProcNodeGraph::get_input_link_count(int p_node_idx, int p_socket_idx) const {
	int n = 0;
	for (int i = 0; i < connections.size(); i++) {
		const ProcConnection &c = connections[i];
		if (c.to_node == p_node_idx && c.to_socket == p_socket_idx) {
			n++;
		}
	}
	return n;
}

bool ProcNodeGraph::get_input_link_at(int p_node_idx, int p_socket_idx, int p_k,
		int &r_from_node, int &r_from_socket) const {
	int n = 0;
	for (int i = 0; i < connections.size(); i++) {
		const ProcConnection &c = connections[i];
		if (c.to_node == p_node_idx && c.to_socket == p_socket_idx) {
			if (n == p_k) {
				r_from_node = c.from_node;
				r_from_socket = c.from_socket;
				return true;
			}
			n++;
		}
	}
	return false;
}

} // namespace godot