OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / graph / proc_node.h

159 lines · c ref:
#ifndef PROC_NODES_PROC_NODE_H
#define PROC_NODES_PROC_NODE_H

#include "socket_value.h"
#include "../gpu/glsl_emit_context.h"
#include <godot_cpp/classes/resource.hpp>
#include <godot_cpp/variant/packed_int32_array.hpp>
#include <godot_cpp/variant/vector2.hpp>

namespace godot {

class ProcEvalContext;

class ProcNode : public Resource {
	GDCLASS(ProcNode, Resource);

protected:
	static void _bind_methods();

	Vector<SocketDef> input_defs;
	Vector<SocketDef> output_defs;
	Vector2 graph_position;

	// THE ID THE AUTHORING TOOL GAVE THIS NODE, -1 when it has none.
	//
	// GraphImporter builds a json-id -> graph-index map for connections and then
	// THREW IT AWAY, so a consumer holding an authoring id had no way to reach
	// the node. blender-bridge's parameter deltas carry exactly that id and were
	// passing it straight to get_node() as though it were an index -- which is
	// only correct while nothing is skipped. One unmapped node (SetMaterial)
	// shifts every index after it, so a delta wrote the WRONG node, and an id
	// past the end took the editor down with SIGSEGV.
	int source_id = -1;
	Vector<Variant> _constructor_defaults; // Captured lazily on first set_input_default

	void _capture_constructor_defaults() {
		if (_constructor_defaults.is_empty() && input_defs.size() > 0) {
			for (int i = 0; i < input_defs.size(); i++) {
				_constructor_defaults.push_back(input_defs[i].default_value);
			}
		}
	}

public:
	// Node identity
	virtual StringName get_node_type() const { return "ProcNode"; }
	virtual String get_node_title() const { return "Node"; }
	virtual String get_node_category() const { return "General"; }

	// Socket access
	int get_input_count() const { return input_defs.size(); }
	int get_output_count() const { return output_defs.size(); }
	const SocketDef &get_input_def(int p_idx) const { return input_defs[p_idx]; }
	bool is_input_multi(int p_idx) const {
		return p_idx >= 0 && p_idx < input_defs.size() && input_defs[p_idx].multi;
	}
	const SocketDef &get_output_def(int p_idx) const { return output_defs[p_idx]; }
	Variant get_input_default(int p_idx) const {
		if (p_idx >= 0 && p_idx < input_defs.size()) return input_defs[p_idx].default_value;
		return Variant();
	}
	String get_input_name(int p_idx) const {
		if (p_idx >= 0 && p_idx < input_defs.size()) return String(input_defs[p_idx].name);
		return "";
	}
	int get_input_type(int p_idx) const {
		if (p_idx >= 0 && p_idx < input_defs.size()) return (int)input_defs[p_idx].type;
		return -1;
	}
	// Outputs, for the socket audit. Only the input side was reachable from
	// script, so an audit could not see that Gradient Texture declared one
	// output where Blender's has two -- and a graph reading Fac read nothing.
	String get_output_name(int p_idx) const {
		if (p_idx >= 0 && p_idx < output_defs.size()) return String(output_defs[p_idx].name);
		return "";
	}
	int get_output_type(int p_idx) const {
		if (p_idx >= 0 && p_idx < output_defs.size()) return (int)output_defs[p_idx].type;
		return -1;
	}

	// Graph editor position
	Vector2 get_graph_position() const { return graph_position; }
	void set_graph_position(const Vector2 &p_pos) { graph_position = p_pos; }
	int get_source_id() const { return source_id; }
	void set_source_id(int p_id) { source_id = p_id; }

	// Set default value for an input socket (used by GraphImporter and serialization)
	void set_input_default(int p_idx, const Variant &p_value) {
		if (p_idx >= 0 && p_idx < input_defs.size()) {
			_capture_constructor_defaults();
			input_defs.ptrw()[p_idx].default_value = p_value;
		}
	}

	// Check if an input default was modified from its constructor value
	bool is_input_default_modified(int p_idx) const {
		if (_constructor_defaults.is_empty()) {
			return false;
		}
		if (p_idx < 0 || p_idx >= input_defs.size() || p_idx >= _constructor_defaults.size()) {
			return false;
		}
		return input_defs[p_idx].default_value != _constructor_defaults[p_idx];
	}

	// Execution (called by GraphExecutor)
	virtual void execute(ProcEvalContext &p_ctx, int p_node_idx) = 0;

	// GPU support
	virtual bool supports_gpu() const { return false; }
	// p_ctx resolves this node's inputs to GLSL expressions: the producer's
	// variable when a socket is connected, its default as a literal when it is
	// not. Nodes written before it exists ignore it and emit what they always
	// did -- see docs/glsl-emitter-design.md.
	virtual String compile_glsl(const GlslEmitContext &p_ctx, int p_node_idx) const { return ""; }

	// THE INPUT SOCKETS THIS NODE CARRIES IN THE BATCH PATH'S PUSH-CONSTANT
	// BLOCK, in slot order. Float, Int and Bool take one slot; Vector takes
	// three. Empty means the node has none -- which is every node that is not a
	// GPU GEOMETRY node, because compile_batch is only ever reached through
	// _is_gpu_geometry_node().
	//
	// This list is the ONLY statement of the mapping. GlslEmitContext::param()
	// walks it to spell `pc.paramN`, and GraphExecutor walks it to pack the
	// bytes, so the slot a node reads is the slot the executor fills BY
	// CONSTRUCTION. It used to be stated twice, in the node's GLSL literal and
	// in a per-type `if` in the executor, and they drifted: Set Position's
	// packer read socket 1 -- which has been SELECTION since its sockets were
	// matched to Blender's -- and called it the offset vector.
	//
	// A socket left out of this list cannot reach the batch shader at all, so
	// GraphExecutor falls back to the CPU when one of them is connected rather
	// than dispatching a shader that cannot see it.
	virtual PackedInt32Array get_push_constant_sockets() const { return PackedInt32Array(); }

	// TWO DIFFERENT REASONS TO EMIT NOTHING, and conflating them is what made
	// the emitter drop nodes silently. A node with missing arithmetic must make
	// emission refuse; a node that is STRUCTURALLY TRANSPARENT -- Group Input
	// and Group Output carry the graph's boundary and no operation -- has
	// nothing to emit and must not. Every bridged graph has both of them, so
	// without this distinction the whole Blender path would refuse.
	virtual bool gpu_transparent() const { return false; }

	// FILE-SCOPE GLSL this node needs: constants, helper functions, buffer
	// declarations. compile_glsl's output is spliced into the BODY of
	// proc_density, and GLSL has no nested functions -- a node that emitted a
	// helper there produced a shader that could not compile at all, silently,
	// with the map falling back to whatever was there before. Anything that is
	// not a statement belongs here.
	virtual String compile_glsl_prelude(int p_node_idx) const { return ""; }

	ProcNode() = default;
};

} // namespace godot

#endif // PROC_NODES_PROC_NODE_H