OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / graph / graph_executor.h

225 lines · c ref:
#ifndef PROC_NODES_GRAPH_EXECUTOR_H
#define PROC_NODES_GRAPH_EXECUTOR_H

#include "../core/linear_allocator.h"
#include "../gpu/gpu_buffer_manager.h"
#include "../gpu/gpu_compiler.h"
#include "proc_node_graph.h"
#include "socket_value.h"
#include <godot_cpp/variant/dictionary.hpp>

namespace godot {

// Per-evaluation context — one per thread
class ProcEvalContext {
public:
	// The graph being evaluated
	const ProcNodeGraph *graph = nullptr;

	// Socket value storage: [node_idx * max_sockets + socket_idx]
	Vector<SocketValue> output_values;
	int max_output_sockets = 0;

	// GPU resources
	GpuBufferManager gpu_buffers;
	bool use_gpu = true;

	// Arena allocator for temporaries
	LinearAllocator arena;

	// The geometry a field is currently being evaluated against. Null outside a
	// materialize() call, which is how a field producer knows whether it is being
	// asked for a value or merely being run in topological order.
	Ref<ProcGeometry> context_geometry;

	// AND THE DOMAIN IT IS BEING ASKED FOR. Blender evaluates a field on the
	// consumer's domain, so Index consumed on the corner domain is the CORNER
	// index -- 0..corner_count-1 -- not a vertex index adapted afterwards.
	// Measured: storing Index on CORNER over a 3x2 grid gives Blender 0..7,
	// where we produced the six point indices and then two zeros.
	AttrDomain context_domain = AttrDomain::Point;

	// THE GRAPH'S INPUT GEOMETRY -- what a Blender geometry tree gets from its
	// Group Input, i.e. the object's own mesh. execute() has accepted a
	// p_input_overrides Dictionary since it was written and never read it, and
	// GroupInputNode has always returned empty geometry with a comment saying
	// the executor handled it. Nothing did, so any graph that starts from Group
	// Input -- which is EVERY Blender geometry tree -- evaluated to nothing.
	Ref<ProcGeometry> input_geometry;

	// Simulation zone state
	float delta_time = 0.016f; // Seconds since last frame, set by ProcGeometryNode
	HashMap<int, Ref<ProcGeometry>> *simulation_cache = nullptr; // zone_id -> prev frame geometry (owned by ProcGeometryNode)

	// Read an input socket (follows connections, applies type conversion)
	SocketValue read_input(int p_node_idx, int p_socket_idx);
	// EVERY value on a MULTI-INPUT socket, in link order. `read_input` returns
	// only the first, which for Join Geometry meant a three-way join produced
	// one mesh.
	Vector<SocketValue> read_input_multi(int p_node_idx, int p_socket_idx);
	SocketValue _read_input_raw(int p_node_idx, int p_socket_idx);

	// Is this input actually wired? Several Blender nodes have IMPLICIT FIELD
	// inputs -- Set Position's Position, for one -- where "unconnected" does not
	// mean "the default value", it means "leave the existing attribute alone".
	// A default cannot express that, so the node has to ask.
	bool is_input_connected(int p_node_idx, int p_socket_idx) const;

	// ── DOES ANYONE READ THIS OUTPUT? ───────────────────────────────────────
	//
	// Nodes wrote every output whether or not the graph read it. Measured on
	// the bridged voronoi mountain, where EVERY node reads exactly one port:
	// Voronoi Texture writes five arrays over 2.58M elements -- a 41 MB Colour,
	// a 31 MB position and two more -- for one float anybody wanted. Noise
	// Texture and Gradient Texture each write a 41 MB Colour nobody reads.
	// ~175 MB of materialisation an evaluation, plus the arithmetic behind it.
	//
	// Blender does not have this problem because a field is a FUNCTION and
	// nothing is materialised until a consumer asks over its own domain. This
	// is the same answer at socket granularity: skip what has no reader.
	//
	// Empty means "no information" -- a graph evaluated without a liveness pass
	// gets true for everything, so the old behaviour is what an unanalysed
	// graph still does.
	bool is_output_needed(int p_node_idx, int p_socket_idx) const;
	Vector<bool> output_needed; // node * max_output_sockets + socket

	// Redeem a context field against a specific geometry. A value that is not
	// lazy is returned unchanged, so callers need not care which they have.
	SocketValue materialize(const SocketValue &p_val, const Ref<ProcGeometry> &p_geo);
	// Redeem against a geometry AND a domain, then adapt whatever comes back to
	// that domain. This is the whole "evaluate a field on the consumer's
	// domain" operation, in one call, for consumers that work off-Point.
	SocketValue materialize_on(const SocketValue &p_val, const Ref<ProcGeometry> &p_geo,
			AttrDomain p_domain);


	// Read input typed
	float read_float(int p_node_idx, int p_socket_idx);
	int32_t read_int(int p_node_idx, int p_socket_idx);
	bool read_bool(int p_node_idx, int p_socket_idx);
	Vector3 read_vector(int p_node_idx, int p_socket_idx);
	Color read_color(int p_node_idx, int p_socket_idx);
	Ref<ProcGeometry> read_geometry(int p_node_idx, int p_socket_idx);
	Quaternion read_rotation(int p_node_idx, int p_socket_idx);
	Projection read_matrix(int p_node_idx, int p_socket_idx);

	// Write an output socket
	void write_output(int p_node_idx, int p_socket_idx, const SocketValue &p_val);
	void write_float(int p_node_idx, int p_socket_idx, float p_val);
	void write_int(int p_node_idx, int p_socket_idx, int32_t p_val);
	void write_bool(int p_node_idx, int p_socket_idx, bool p_val);
	void write_vector(int p_node_idx, int p_socket_idx, const Vector3 &p_val);
	void write_color(int p_node_idx, int p_socket_idx, const Color &p_val);
	void write_geometry(int p_node_idx, int p_socket_idx, const Ref<ProcGeometry> &p_val);
	void write_rotation(int p_node_idx, int p_socket_idx, const Quaternion &p_val);
	void write_matrix(int p_node_idx, int p_socket_idx, const Projection &p_val);

	// Field (per-element) write
	void write_float_field(int p_node_idx, int p_socket_idx, const PackedFloat32Array &p_arr,
			AttrDomain p_domain = AttrDomain::Point);
	void write_int_field(int p_node_idx, int p_socket_idx, const PackedInt32Array &p_arr,
			AttrDomain p_domain = AttrDomain::Point);
	void write_vec_field(int p_node_idx, int p_socket_idx, const PackedVector3Array &p_arr,
			AttrDomain p_domain = AttrDomain::Point);
	void write_color_field(int p_node_idx, int p_socket_idx, const PackedColorArray &p_arr,
			AttrDomain p_domain = AttrDomain::Point);
	void write_rot_field(int p_node_idx, int p_socket_idx, const PackedVector4Array &p_arr,
			AttrDomain p_domain = AttrDomain::Point);
	// 16 floats per element, column-major, matching SocketValue::mat_at.
	void write_mat_field(int p_node_idx, int p_socket_idx, const PackedFloat32Array &p_arr,
			AttrDomain p_domain = AttrDomain::Point);
};

// Compiled execution plan
struct ExecutionPlan {
	Vector<int> execution_order; // Topologically sorted node indices
	int output_node = -1;
	int output_socket = 0;

	// GPU batches
	Vector<GpuBatch> gpu_batches;
	int find_gpu_batch(int p_exec_order_index) const;
	void destroy_gpu_resources(RenderingDevice *p_rd);

	// Zones
	Vector<ProcZoneInfo> zones;
	Vector<int> node_zone_map; // node_idx → zone index (-1 if not in zone)

	bool has_simulation_zones() const;
};

class GraphExecutor {
public:
	// ⛔ THE ONLY WAY TO SEE WHETHER A NODE ACTUALLY RAN ON THE GPU.
	//
	// The batch path falls back to the CPU per node, silently, for four
	// separate reasons -- so a CPU-vs-GPU equivalence test passes trivially by
	// comparing the CPU against itself. Nothing observed that until now, which
	// is why the batch could contain three node types for months without
	// anyone noticing. node type -> "gpu" | "cpu", rebuilt every execute().
	//
	// A static because evaluation is single-threaded and one report per
	// process is what a gate reads; the alternative is threading a Dictionary
	// through every signature for the benefit of a test.
	// A FUNCTION-LOCAL static, not a class-level one: a class-level Dictionary
	// is constructed when the .so loads, before godot-cpp's bindings exist, and
	// the constructor calls straight into the engine -- signal 11 on the first
	// touch, with a backtrace that names libc and nothing else. Constructed on
	// first use, it is built after initialisation like everything else.
	static Dictionary &exec_report();
	// Compile graph to execution plan (topological sort)
	static ExecutionPlan compile(const ProcNodeGraph &p_graph);

	// Execute graph and return result geometry
	// p_rd: optional pre-created local RenderingDevice (persisted by caller for reuse)
	// If null and p_use_gpu is true, GPU batches fall back to CPU
	// Evaluate the graph far enough to read ONE output socket, and return it as
	// a Variant.
	//
	// execute() cannot be used for this: it returns early unless the graph has a
	// geometry output node, and a graph built to exercise a single value node has
	// none. This runs the same topological order, on the CPU, with no zones and
	// no GPU batching -- which is all a value node needs, and keeps a test hook
	// from reaching into the production path.
	//
	// Returns nil if the indices are out of range.
	//
	// ⚠ p_input_overrides IS LOAD-BEARING, and its absence was a defect. This
	// used to take no overrides, so it evaluated against NO GEOMETRY -- and a
	// graph whose value depends on position (every SDF, every noise field) then
	// answered from an empty context. evaluate_density_batch called it after
	// carefully building a point cloud, and got a constant. Measured
	// 2026-09-06: a unit-sphere SDF returned 0.0 at every one of five points,
	// inside and outside. Nothing had ever called that path, which is why it
	// stood.
	static Variant evaluate_socket_value(const ProcNodeGraph &p_graph,
			int p_node_idx, int p_socket_idx,
			const Dictionary &p_input_overrides = Dictionary());

	static Vector<bool> _compute_live_set(const ProcNodeGraph &p_graph,
			ExecutionPlan &p_plan, ProcEvalContext &p_ctx);

	static Ref<ProcGeometry> execute(
			const ProcNodeGraph &p_graph,
			ExecutionPlan &p_plan,
			const Dictionary &p_input_overrides = Dictionary(),
			bool p_use_gpu = true,
			RenderingDevice *p_rd = nullptr,
			float p_delta_time = 0.016f,
			HashMap<int, Ref<ProcGeometry>> *p_simulation_cache = nullptr);

private:
	static void _detect_gpu_batches(const ProcNodeGraph &p_graph, ExecutionPlan &p_plan);
	static void _execute_gpu_batch(ProcEvalContext &p_ctx, const GpuBatch &p_batch, const ExecutionPlan &p_plan);
	static void _detect_zones(const ProcNodeGraph &p_graph, ExecutionPlan &p_plan);
	static void _execute_zone(ProcEvalContext &p_ctx, const ProcZoneInfo &p_zone, const ExecutionPlan &p_plan);
	static void _execute_repeat_zone(ProcEvalContext &p_ctx, const ProcZoneInfo &p_zone, const ExecutionPlan &p_plan);
	static void _execute_foreach_zone(ProcEvalContext &p_ctx, const ProcZoneInfo &p_zone, const ExecutionPlan &p_plan);
	static void _execute_simulation_zone(ProcEvalContext &p_ctx, const ProcZoneInfo &p_zone, const ExecutionPlan &p_plan);
};

} // namespace godot

#endif // PROC_NODES_GRAPH_EXECUTOR_H