#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