OverviewFilesCommitsIssuesDocsReleasesPeers

proc-nodes / src / scene / proc_geometry_node.cpp

400 lines · cpp ref:
#include "proc_geometry_node.h"
#include "../graph/graph_importer.h"
#include <godot_cpp/classes/engine.hpp>
#include <godot_cpp/classes/rendering_server.hpp>
#include <godot_cpp/classes/time.hpp>
#include <godot_cpp/variant/utility_functions.hpp>

namespace godot {


Dictionary ProcGeometryNode::_build_overrides() const {
	Dictionary d;
	// The mesh channel first. An ArrayMesh reaching this function has already
	// lost its faces and, when it came over the bridge, its shared vertices --
	// so if the real mesh is here, the ArrayMesh is not a second opinion, it is
	// a worse copy of the same thing.
	Ref<ProcMesh> pm;
	if (input_mesh_data.has("points")) {
		pm = ProcMesh::from_mesh_data(input_mesh_data);
		// ⛔ TURN IT ROUND ON THE WAY IN, ONCE.
		//
		// A face is a list of corners, and that order is the only thing saying
		// which side is the front. The two engines read it oppositely --
		// measured, an outward sphere is +4.1219 in Blender and Godot's own is
		// -0.5216 -- so a mesh Blender wrote has to be reversed to mean the same
		// thing here. Everything downstream then follows ONE rule, which is the
		// rule proc-nodes already had: compute_normals negates the cross product
		// because Godot's front face is clockwise.
		//
		// Doing it on the way OUT instead, in _to_godot_frame, was wrong twice:
		// it left the graph computing normals that pointed into the surface (so
		// they had to be negated back, two wrongs cancelling), and it would have
		// reversed geometry the graph BUILT ITSELF -- a primitive made here was
		// never Blender's and has nothing to turn round.
		if (pm.is_valid() && graph.is_valid() && graph->is_blender_frame()) {
			pm->reverse_faces();
		}
	} else if (input_mesh.is_valid()) {
		pm = ProcMesh::from_array_mesh(input_mesh);
	}
	if (pm.is_valid()) {
		Ref<ProcGeometry> g;
		g.instantiate();
		g->set_mesh(pm);
		d["geometry"] = g;
	}
	return d;
}

Dictionary ProcGeometryNode::get_output_mesh_data() {
	Ref<ProcGeometry> g = bake_geometry();
	if (g.is_null()) {
		return Dictionary();
	}
	Ref<ProcMesh> m = g->get_mesh();
	if (m.is_null()) {
		return Dictionary();
	}
	return m->to_mesh_data();
}

Dictionary ProcGeometryNode::get_last_exec_report() {
	return GraphExecutor::exec_report();
}

void ProcGeometryNode::_bind_methods() {
	ClassDB::bind_method(D_METHOD("set_input_mesh", "mesh"), &ProcGeometryNode::set_input_mesh);
	ClassDB::bind_method(D_METHOD("get_input_mesh"), &ProcGeometryNode::get_input_mesh);
	ClassDB::bind_method(D_METHOD("set_input_mesh_data", "data"), &ProcGeometryNode::set_input_mesh_data);
	ClassDB::bind_method(D_METHOD("get_input_mesh_data"), &ProcGeometryNode::get_input_mesh_data);
	ClassDB::bind_method(D_METHOD("get_output_mesh_data"), &ProcGeometryNode::get_output_mesh_data);
	ClassDB::bind_method(D_METHOD("set_graph", "graph"), &ProcGeometryNode::set_graph);
	ClassDB::bind_method(D_METHOD("get_graph"), &ProcGeometryNode::get_graph);
	ClassDB::bind_method(D_METHOD("set_auto_update", "auto_update"), &ProcGeometryNode::set_auto_update);
	ClassDB::bind_method(D_METHOD("get_auto_update"), &ProcGeometryNode::get_auto_update);
	ClassDB::bind_method(D_METHOD("set_live_evaluation", "live"), &ProcGeometryNode::set_live_evaluation);
	ClassDB::bind_method(D_METHOD("get_live_evaluation"), &ProcGeometryNode::get_live_evaluation);
	ClassDB::bind_method(D_METHOD("set_use_gpu", "use_gpu"), &ProcGeometryNode::set_use_gpu);
	ClassDB::bind_method(D_METHOD("get_use_gpu"), &ProcGeometryNode::get_use_gpu);
	ClassDB::bind_method(D_METHOD("evaluate"), &ProcGeometryNode::evaluate);
	ClassDB::bind_method(D_METHOD("bake_mesh"), &ProcGeometryNode::bake_mesh);
	ClassDB::bind_method(D_METHOD("bake_geometry"), &ProcGeometryNode::bake_geometry);
	ClassDB::bind_method(D_METHOD("mark_dirty"), &ProcGeometryNode::mark_dirty);
	ClassDB::bind_method(D_METHOD("set_graph_from_json", "json"), &ProcGeometryNode::set_graph_from_json);
	ClassDB::bind_method(D_METHOD("invalidate_plan"), &ProcGeometryNode::invalidate_plan);
	// node type -> "gpu" | "cpu..." for the LAST evaluation. See
	// GraphExecutor::last_exec_report: without it a CPU-vs-GPU gate cannot tell
	// agreement from a silent fallback comparing the CPU against itself.
	ClassDB::bind_static_method("ProcGeometryNode",
			D_METHOD("get_last_exec_report"), &ProcGeometryNode::get_last_exec_report);
	ClassDB::bind_method(D_METHOD("_on_graph_changed"), &ProcGeometryNode::_on_graph_changed);
	ClassDB::bind_method(D_METHOD("push_param_delta", "node_idx", "socket_idx", "value"), &ProcGeometryNode::push_param_delta);
	ClassDB::bind_method(D_METHOD("drain_param_deltas"), &ProcGeometryNode::drain_param_deltas);
	ClassDB::bind_method(D_METHOD("suppress_param_deltas", "suppress"), &ProcGeometryNode::suppress_param_deltas);

	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "graph", PROPERTY_HINT_RESOURCE_TYPE, "ProcNodeGraph"), "set_graph", "get_graph");
	ADD_PROPERTY(PropertyInfo(Variant::OBJECT, "input_mesh", PROPERTY_HINT_RESOURCE_TYPE, "Mesh"), "set_input_mesh", "get_input_mesh");
	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "auto_update"), "set_auto_update", "get_auto_update");
	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "live_evaluation"), "set_live_evaluation", "get_live_evaluation");
	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "use_gpu"), "set_use_gpu", "get_use_gpu");

	ADD_SIGNAL(MethodInfo("defaults_changed"));
}

void ProcGeometryNode::mark_dirty() {
	needs_update = true;
	emit_signal("defaults_changed");
}

void ProcGeometryNode::push_param_delta(int p_node_idx, int p_socket_idx, const Variant &p_value) {
	if (_suppress_param_deltas) return;
	Dictionary d;
	d["node_idx"] = p_node_idx;
	d["socket_idx"] = p_socket_idx;
	d["value"] = p_value;
	_param_delta_queue.push_back(d);
}

TypedArray<Dictionary> ProcGeometryNode::drain_param_deltas() {
	TypedArray<Dictionary> result;
	for (int i = 0; i < _param_delta_queue.size(); i++) {
		result.push_back(_param_delta_queue[i]);
	}
	_param_delta_queue.clear();
	return result;
}

ProcGeometryNode::~ProcGeometryNode() {
	_destroy_gpu_device();
}

void ProcGeometryNode::_ensure_gpu_device() {
	if (gpu_rd) {
		return; // Already created
	}
	RenderingServer *rs = RenderingServer::get_singleton();
	if (rs) {
		gpu_rd = rs->create_local_rendering_device();
		if (gpu_rd) {
			UtilityFunctions::print("ProcNodes: Local RenderingDevice created for GPU compute");
		}
	}
}

void ProcGeometryNode::_destroy_gpu_device() {
	if (gpu_rd) {
		// Free any cached pipelines before destroying the device
		cached_plan.destroy_gpu_resources(gpu_rd);
		memdelete(gpu_rd);
		gpu_rd = nullptr;
	}
}

void ProcGeometryNode::_notification(int p_what) {
	switch (p_what) {
		case NOTIFICATION_READY: {
			_ensure_children();
			if (graph.is_valid()) {
				plan_dirty = true;
				needs_update = true;
			}
		} break;

		case NOTIFICATION_PROCESS: {
			if (auto_update && live_evaluation && needs_update) {
				_evaluate();
				// If graph has simulation zones, keep evaluating every frame
				if (cached_plan.has_simulation_zones()) {
					needs_update = true;
				} else {
					needs_update = false;
				}
			}
		} break;

		case NOTIFICATION_ENTER_TREE: {
			set_process(true);
		} break;

		case NOTIFICATION_EXIT_TREE: {
			_destroy_gpu_device();
		} break;
	}
}

void ProcGeometryNode::set_use_gpu(bool p_gpu) {
	if (use_gpu == p_gpu) {
		return;
	}
	use_gpu = p_gpu;
	// Keep GPU device alive for fast toggle-back (destroyed on exit/graph change only)
	needs_update = true;
	UtilityFunctions::print("ProcNodes: use_gpu = ", use_gpu);
}

void ProcGeometryNode::_on_graph_changed() {
	plan_dirty = true;
	needs_update = true;
}

void ProcGeometryNode::set_graph(const Ref<ProcNodeGraph> &p_graph) {
	// Disconnect previous graph signal
	if (graph.is_valid() && graph->is_connected("changed", Callable(this, "_on_graph_changed"))) {
		graph->disconnect("changed", Callable(this, "_on_graph_changed"));
	}

	// Graph changed — destroy GPU pipelines (they cache graph-specific shaders)
	if (gpu_rd) {
		cached_plan.destroy_gpu_resources(gpu_rd);
	}
	graph = p_graph;
	plan_dirty = true;
	needs_update = true;
	// Invalidate cached display mesh — topology will likely change
	cached_display_mesh = Ref<ArrayMesh>();
	cached_vert_count = 0;
	cached_index_count = 0;
	// Clear simulation cache — new graph has different zone IDs
	simulation_cache.clear();

	// Connect to graph changed signal for live updates
	if (graph.is_valid() && !graph->is_connected("changed", Callable(this, "_on_graph_changed"))) {
		graph->connect("changed", Callable(this, "_on_graph_changed"));
	}
}

void ProcGeometryNode::_ensure_children() {
	// ⛔ ADOPT THE ONE THAT IS ALREADY THERE.
	//
	// These children are built at runtime and, in a live session, belong to
	// nobody -- which is why a saved scene does not carry the millions of
	// vertices they hold. A PUBLISHED scene is the opposite case: it carries
	// MeshOutput on purpose, with the baked mesh beside it, so that opening it
	// needs neither this graph nor a bake. Creating a second one on _ready
	// would leave the baked mesh on a node nothing draws into and the next
	// evaluation writing into a stranger next to it.
	if (mesh_output == nullptr) {
		mesh_output = Object::cast_to<MeshInstance3D>(get_node_or_null(NodePath("MeshOutput")));
	}
	if (mesh_output == nullptr) {
		mesh_output = memnew(MeshInstance3D);
		mesh_output->set_name("MeshOutput");
		add_child(mesh_output);
	}
	if (multimesh_output == nullptr) {
		multimesh_output = Object::cast_to<MultiMeshInstance3D>(get_node_or_null(NodePath("MultiMeshOutput")));
	}
	if (multimesh_output == nullptr) {
		multimesh_output = memnew(MultiMeshInstance3D);
		multimesh_output->set_name("MultiMeshOutput");
		add_child(multimesh_output);
	}
}

bool ProcGeometryNode::set_graph_from_json(const String &p_json) {
	Ref<ProcNodeGraph> new_graph = GraphImporter::import_from_json(p_json);
	if (new_graph.is_valid()) {
		set_graph(new_graph);
		mark_dirty();
		return true;
	}
	return false;
}

void ProcGeometryNode::_evaluate() {
	if (graph.is_null() || graph->get_node_count() == 0) {
		return;
	}

	_ensure_children();

	// Recompile plan if dirty (also invalidates GPU pipelines)
	if (plan_dirty) {
		if (gpu_rd) {
			cached_plan.destroy_gpu_resources(gpu_rd);
		}
		cached_plan = GraphExecutor::compile(*graph.ptr());
		plan_dirty = false;
	}

	// Ensure local RenderingDevice exists for GPU path
	RenderingDevice *rd = nullptr;
	if (use_gpu && cached_plan.gpu_batches.size() > 0) {
		_ensure_gpu_device();
		rd = gpu_rd;
	}

	// Execute graph with timing
	uint64_t t0 = Time::get_singleton()->get_ticks_usec();
	float dt = 0.016f;
	if (is_inside_tree()) {
		dt = (float)get_process_delta_time();
	}
	Ref<ProcGeometry> result = GraphExecutor::execute(
			*graph.ptr(), cached_plan, _build_overrides(), use_gpu, rd,
			dt, &simulation_cache);
	uint64_t t1 = Time::get_singleton()->get_ticks_usec();
	if (result.is_null()) {
		return;
	}

	_update_display();

	// Convert mesh to ArrayMesh — reuse existing if topology unchanged
	// NOTE: surface_update_vertex_region only updates positions (stride=12),
	// normals are in a separate buffer with no public update API in Godot 4.6.
	// So we use clear_surfaces + add_surface_from_arrays which is ~13ms for 250K verts.
	uint64_t t2 = t1;
	if (result->get_mesh().is_valid() && mesh_output != nullptr) {
		Ref<ProcMesh> proc_mesh = result->get_mesh();
		bool updated = false;

		if (cached_display_mesh.is_valid() &&
				cached_vert_count == proc_mesh->vertex_count() &&
				cached_index_count == (int)proc_mesh->indices.size()) {
			// Same topology — update the existing ArrayMesh in place
			updated = proc_mesh->update_array_mesh(cached_display_mesh);
		}

		if (!updated) {
			// Topology changed or first frame — create new ArrayMesh
			cached_display_mesh = proc_mesh->to_array_mesh();
			cached_vert_count = proc_mesh->vertex_count();
			cached_index_count = proc_mesh->indices.size();
			mesh_output->set_mesh(cached_display_mesh);
		}
		t2 = Time::get_singleton()->get_ticks_usec();
	}

	// Display instances as MultiMesh
	if (result->get_instances().is_valid() && multimesh_output != nullptr) {
		Ref<MultiMesh> mm = result->get_instances()->to_multimesh();
		if (mm.is_valid()) {
			multimesh_output->set_multimesh(mm);
			multimesh_output->set_visible(true);
		} else {
			multimesh_output->set_visible(false);
		}
	} else if (multimesh_output != nullptr) {
		multimesh_output->set_visible(false);
	}

	uint64_t t3 = Time::get_singleton()->get_ticks_usec();

	UtilityFunctions::print(
			"ProcNodes [GPU=", use_gpu, "]: execute=", (t1 - t0) / 1000.0,
			"ms  to_array_mesh=", (t2 - t1) / 1000.0,
			"ms  set_mesh=", (t3 - t2) / 1000.0,
			"ms  TOTAL=", (t3 - t0) / 1000.0, "ms");
}

void ProcGeometryNode::_update_display() {
	// If using baked mesh and not live, show baked
	if (!live_evaluation && baked_mesh.is_valid() && mesh_output != nullptr) {
		mesh_output->set_mesh(baked_mesh);
	}
}

void ProcGeometryNode::evaluate() {
	_evaluate();
	needs_update = false;
}

Ref<ProcGeometry> ProcGeometryNode::bake_geometry() {
	if (graph.is_null()) {
		return Ref<ProcGeometry>();
	}

	if (plan_dirty) {
		if (gpu_rd) {
			cached_plan.destroy_gpu_resources(gpu_rd);
		}
		cached_plan = GraphExecutor::compile(*graph.ptr());
		plan_dirty = false;
	}

	RenderingDevice *rd = nullptr;
	if (use_gpu && cached_plan.gpu_batches.size() > 0) {
		_ensure_gpu_device();
		rd = gpu_rd;
	}

	return GraphExecutor::execute(*graph.ptr(), cached_plan, _build_overrides(), use_gpu, rd, 0.016f, nullptr);
}

Ref<ArrayMesh> ProcGeometryNode::bake_mesh() {
	// ONE evaluation path. bake_mesh is bake_geometry plus the ArrayMesh
	// conversion; keeping a second copy of the compile/GPU/execute sequence is
	// how the two would drift.
	Ref<ProcGeometry> result = bake_geometry();
	if (result.is_null() || result->get_mesh().is_null()) {
		return Ref<ArrayMesh>();
	}

	baked_mesh = result->get_mesh()->to_array_mesh();
	return baked_mesh;
}

} // namespace godot