#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