#ifndef PROC_NODES_PROC_NODE_GRAPH_H
#define PROC_NODES_PROC_NODE_GRAPH_H
#include "proc_node.h"
#include "../core/proc_geometry.h"
#include <godot_cpp/variant/packed_string_array.hpp>
#include <godot_cpp/classes/resource.hpp>
namespace godot {
struct ExecutionPlan;
struct ProcConnection {
int from_node = -1;
int from_socket = -1;
int to_node = -1;
int to_socket = -1;
};
struct ProcZoneInfo {
int zone_id = -1;
enum ZoneType { ZONE_REPEAT, ZONE_FOREACH, ZONE_SIMULATION };
ZoneType zone_type = ZONE_REPEAT;
int input_node = -1; // Index of ZoneInput node in graph
int output_node = -1; // Index of ZoneOutput node in graph
Vector<int> body_nodes; // Indices of nodes inside zone (topological order)
int parent_zone = -1; // For nesting (-1 = root level)
};
class ProcNodeGraph : public Resource {
GDCLASS(ProcNodeGraph, Resource);
protected:
static void _bind_methods();
bool _set(const StringName &p_name, const Variant &p_property);
bool _get(const StringName &p_name, Variant &r_property) const;
void _get_property_list(List<PropertyInfo> *p_list) const;
Vector<Ref<ProcNode>> nodes;
Vector<ProcConnection> connections;
// WHICH FRAME THIS GRAPH'S ARITHMETIC IS IN.
//
// A graph is a program over numbers; almost nothing in it knows what "up"
// means. Math, Vector Math, Combine XYZ and every texture are pure
// arithmetic and produce whatever numbers they are given. Only two things
// carry a frame: the mesh PRIMITIVES, which have to lay their vertices out
// somewhere, and the Godot-only terrain family, which treats .y as height.
//
// So a bridged graph is handled the way an exporter handles a mesh: run it
// UNCHANGED, so it computes exactly Blender's numbers, and convert once at
// the end. The alternative -- translating vector values as they cross --
// cannot work, because a vector assembled by Combine XYZ has no value to
// translate and its three scalars are individually meaningless.
//
// GODOT is the default, so every graph authored in the Godot editor, and
// every .tres already on disk, behaves exactly as before.
int source_frame = 0; // 0 GODOT, 1 BLENDER
public:
// Node management
int add_node(const Ref<ProcNode> &p_node);
void remove_node(int p_idx);
Ref<ProcNode> get_node(int p_idx) const;
void set_source_frame(int p_frame) { source_frame = p_frame; }
int get_source_frame() const { return source_frame; }
bool is_blender_frame() const { return source_frame == 1; }
int get_node_count() const { return nodes.size(); }
// Connection management
void connect_sockets(int p_from_node, int p_from_socket, int p_to_node, int p_to_socket);
void disconnect_sockets(int p_from_node, int p_from_socket, int p_to_node, int p_to_socket);
void clear_connections_for_node(int p_node_idx);
// Find what's connected to a specific input socket
// Returns -1 if nothing connected
int get_input_source_node(int p_node_idx, int p_socket_idx) const;
int get_input_source_socket(int p_node_idx, int p_socket_idx) const;
// Get all connections
const Vector<ProcConnection> &get_connections() const { return connections; }
// Evaluate the graph and read one output socket. For tests and debugging:
// it does not need a geometry output node, so a single value node can be
// exercised on its own.
Variant evaluate_socket(int p_node_idx, int p_socket_idx) const;
// Evaluate phacelle_noise from src/shared/erosion_filter.inc directly.
//
// This exists so the Blender port can be checked against THE REAL FILTER
// rather than against a Python transcription of it. Three bugs this session
// were faithful ports of a wrong reference, and a transcription used as the
// reference can only agree with itself.
//
// Returns (interpolated.x, interpolated.y, side_dir.x, side_dir.y).
static Vector4 debug_phacelle_noise(Vector2 p, Vector2 dir, float freq,
float offset, float normalization);
// erosion_filter, same reasoning. Returns
// [delta.x, delta.y, delta.z, magnitude, ridge].
static PackedFloat32Array debug_erosion_filter(Vector2 p, Vector3 height_and_slope,
float fade_target, float strength, float gully_weight, float detail,
Vector4 rounding, Vector4 onset, Vector2 assumed_slope,
float scale, int octaves, float lacunarity, float gain,
float cell_scale, float normalization, float max_octaves);
// erosion_node_eval_full -- the wrapper ErosionNoiseNode is built on, and
// the last layer of the filter the Blender port had no reference for.
// Everything under it (erosion_hash, the noise, phacelle_noise,
// erosion_filter) is already checked against the two probes above; this
// closes the stack.
//
// Nineteen parameters because that is what the function takes. Packing them
// into an array would make the probe shorter and the call site unreadable,
// and the whole point of these statics is that the test names the same
// arguments the shipping code does.
//
// Returns (eroded * height_scale, ridge).
static Vector2 debug_erosion_node_eval_full(Vector2 p, Vector3 gn,
float height_scale, float erosion_strength, float cell_size,
int octaves, float gain, float peak_height_divisor,
float gully_band_scale, float input_height_amplitude,
float octave_lacunarity, float gully_depth,
float fine_gully_slope_restriction, float gully_normalize_threshold,
Vector2 height_offset_pair, Vector4 rounding_set, Vector4 onset_set,
Vector2 assumed_slope_pair, float max_octaves);
int get_connection_count() const { return connections.size(); }
PackedInt32Array get_connections_flat() const;
// Multi-input support: how many links reach one input socket, and each in
// the order they were made.
int get_input_link_count(int p_node_idx, int p_socket_idx) const;
bool get_input_link_at(int p_node_idx, int p_socket_idx, int p_k,
int &r_from_node, int &r_from_socket) const;
// Find output node (first node with no outputs connected to anything downstream)
int find_output_node() const;
// Compile all GPU-capable nodes to a standalone GLSL function
// Returns GLSL code with: float <function_name>(vec3 pos) { ... }
// For use by external systems (dc-terrain density evaluation, etc.)
// When p_clamp_octaves is true, octave counts use min(N, pn_max_octaves)
// so the caller can enforce Nyquist limits per voxel resolution.
// p_skip_types names node TYPES to leave out of the emitted function --
// get_node_type(), e.g. "ErosionNoise". A layer that is skipped contributes
// nothing AND drags nothing in: with no erosion node the noise library is
// not emitted either, because nothing left mentions noise. That is what
// makes an in-editor "erosion on/off" a real toggle rather than a strength
// of zero, which costs the same octave loop either way.
// THE CPU SIDE OF proc_density, and the reason it exists: dc-terrain answered
// every CPU density query -- collision, sample_density, the mesher's grid
// fill -- from a hand-written replica that recognised two node TITLES, so
// any other graph rendered one thing and collided with another.
//
// The result is chosen by EXACTLY the rules the GLSL emitter uses: an
// explicit Group Output float if the graph has one, otherwise the
// displacement of each point's y, which is what `pos.y - query_y` means in
// the shader. Two conventions would be two answers.
//
// BATCH IS THE PRIMARY FORM. Measured over a 35,937-point chunk fill, the
// executor's own overhead is 0.1 ms -- about 3 ns a sample -- while the
// erosion filter those points run is 206 ms, which the replica pays too. The
// machinery is not the expensive part; the algorithm is.
PackedFloat32Array evaluate_density_batch(const PackedVector3Array &p_positions);
// One point, for collision and the other scalar queries. Holds its geometry
// and its plan resident, because re-converting an input mesh per call is
// what made the generic host path cost 15.8 us.
float evaluate_density_at(const Vector3 &p_pos);
String compile_to_glsl_function(const String &p_function_name = "proc_density",
bool p_clamp_octaves = false,
const PackedStringArray &p_skip_types = PackedStringArray()) const;
// WHICH VALUE THE GRAPH'S DENSITY IS -- the one rule, in one place.
//
// An explicit Group Output with a CONNECTED FLOAT input is the graph saying
// what it produces; anything else means "the displacement of pos.y". This
// was written TWICE -- here for the CPU evaluator and again as a loop in
// gpu_compiler.cpp for the emitter -- with a comment on one of them saying
// it followed "the same rules gpu_compiler.cpp uses", which asserts
// agreement rather than enforcing it. A mutation to one left the other
// answering the old way, which is how the duplication was found
// (hyper-games #65 NEXT 5).
bool find_density_output(int &r_node, int &r_socket) const;
// The graph index of the node the authoring tool called p_source_id, or -1.
// An id is NOT an index: the importer skips nodes it cannot map, so every
// index after a skipped one is shifted.
int find_node_by_source_id(int p_source_id) const;
// THE GRAPH'S OWN EXTENT, declared beside its density.
//
// A Blender group interface takes any number of named, typed outputs, so a
// graph says how big it is the same way it says what its density is: four
// sockets on the Group Output, read BY NAME -- "Bounds Min", "Bounds Max",
// "Voxel Size". Nothing in the bridge needed changing for this; the Group
// Output node already crosses carrying its whole interface.
//
// Returns an empty Dictionary when the graph declares no bounds, which is
// the honest answer for an unbounded field and is what
// DCGenerator::get_bounds already defaults to.
//
// ⚠ THE BOUNDS ARE IN THE GRAPH'S FRAME. A bridged graph is authored in
// Blender's, so the values are Blender coordinates and a consumer wants
// world. M = (x, y, z) -> (-x, z, y) is applied here, and because M NEGATES
// x a converted pair is no longer sorted -- min/max are recomputed
// componentwise afterwards. Skipping that yields an inverted box, which
// reads downstream as "the terrain is missing" rather than as a frame error.
Dictionary get_density_bounds() const;
// THE BATCH PATH'S TEXT, FOR THE GATE.
//
// compile_batch's output is otherwise reachable only through a GPU
// dispatch, so the one path where `pc.paramN` is CORRECT had no instrument
// that could read it -- which is how Set Position's packer came to read
// socket 1 and call it the offset vector, for as long as socket 1 has been
// Selection. This returns the same String GraphExecutor hands to
// create_pipeline. It compiles nothing and touches no RenderingDevice.
String compile_batch_glsl(const PackedInt32Array &p_node_indices) const;
// Access
const Vector<Ref<ProcNode>> &get_nodes() const { return nodes; }
ProcNodeGraph() = default;
~ProcNodeGraph();
private:
// Scratch for the density entry points.
//
// ⚠ THE PLAN, not the geometry, is what a scalar query cannot afford to
// rebuild. Measured: compiling it per call put a single query at 18.84 us --
// WORSE than the 15.8 us of the generic host path this was meant to beat,
// and the mesh conversion I had blamed was not the cost at all.
Ref<ProcGeometry> _density_scratch;
// By POINTER, behind a forward declaration: including graph_executor.h here
// is a cycle -- it needs ProcNodeGraph -- and the error it produces names
// ProcZoneInfo rather than the cycle.
ExecutionPlan *_density_plan = nullptr;
bool _density_plan_dirty = true;
};
} // namespace godot
#endif // PROC_NODES_PROC_NODE_GRAPH_H