Dynamic node declaration

I noticed bNodeType has a declaration_is_dynamic field that make the declare callback be called dynamically upon node instance creation and copy. But I could not find any existing node that is using it, so I tried to improvise.

My first issue was that the declare callback only takes a NodeDeclarationBuilder, so it has no way to behave differently during different calls. I thus added a node field to the NodeDeclarationBuilder class in NOD_node_declaration.hh:

// 1. We add a new field holding a pointer to the node for which we are building the declaration.
const bNode *node_; /* Only available in dynamic declarations */

// 2. Add an extra argument to the constructor.
// Node that the node is null by default, namely when the declaration is not dynamic.
NodeDeclarationBuilder(NodeDeclaration &declaration, const bNode *node = nullptr);
// [...]
inline NodeDeclarationBuilder::NodeDeclarationBuilder(NodeDeclaration &declaration,
                                                      const bNode *node)
    : declaration_(declaration), node_(node)
{
}

// 3. We add an accessor for this node
const bNode *node() const;
// [...]
inline const bNode *NodeDeclarationBuilder::node() const
{
  return this->node_;
}

When using the builder in nodeDeclarationEnsureOnOutdatedNode (node.cc), we provide the current node to the constructor arguments:

blender::nodes::NodeDeclarationBuilder builder{*node->declaration, node};

Once we have this, we can use it in the declare callback:

static void node_declare(NodeDeclarationBuilder &b)
{
  if (b.node() == nullptr || b.node()->storage == nullptr)
    return;
  const NodeGeometryPizza &storage = node_storage(b.node());
  // Now use storage to dynamically call b.add_input and b.add_output
  // [...]
}

This was not enough to have the input/output effectively dynamically redefined, we also have to trigger it in the update callback by calling this function:

static void force_redeclare(bNodeTree *ntree, bNode *node)
{
  BLI_assert(node->typeinfo->declaration_is_dynamic);
  if (node->declaration != nullptr)
  {
    delete node->declaration;
    node->declaration = nullptr;
  }
  node_verify_sockets(ntree, node, true);
}

The naive solution consists in calling it all the time:

static void node_update(bNodeTree *ntree, bNode *node)
{
  force_redeclare(ntree, node);
  // [...]
}

This works! Even when saving/reloading a file etc. What I am wondering is:

  1. Is this idiomatic enough?
  2. Is there a document listing the intended plans for dynamic node declaration? It seems to be an ongoing work so I would understand it is still mostly in chat logs.
  3. I’d like to store runtime data to check whether to force redeclare the node, what would be the recommended way? A runtime field in me NodeGeometryPizza DNA struct?

PS: A bit of context: I am doing this in the course of creating a node that loads an OpenMfx plugin at runtime. As one can see in this guide the logic is close enough, there is a Describe action that corresponds to Blender’s declare and a Cook that is geometry_node_execute. I need the node to be redeclared whenever the path of the plugin changes.

10 Likes

I explored the idea of storing the runtime data as a field in the DNA of the node. So in DNA_node_types.h, I add:

typedef struct NodeGeometryPizza {
  // [...]
  NodeGeometryPizzaRuntimeHandle *runtime;
} NodeGeometryPizza;

In order to define NodeGeometryPizzaRuntimeHandle, I use the same workaround than for NodeDeclarationHandle, at the beginning of the file (but I guess I could have simply set the type to void*):

/** Same workaround than for NodeDeclarationHandle. */
#ifdef __cplusplus
namespace blender::nodes::node_geo_pizza_cc{
class RuntimeData;
}  // namespace blender::nodes::node_geo_pizza_cc
using NodeGeometryOpenMfxRuntimeHandle = blender::nodes::node_geo_pizza_cc::RuntimeData;
#else
typedef struct NodeGeometryOpenMfxRuntimeHandle NodeGeometryOpenMfxRuntimeHandle;
#endif

Then, in my node_geo_pizza.cc file I can define the runtime data:

namespace blender::nodes::node_geo_pizza_cc {
class RuntimeData { /* [...] */ };
}

And regarding memory allocation, this requires me to change 3 callbacks, namely (i) init, (ii) free and (iii) copy:

static void node_init(bNodeTree *UNUSED(tree), bNode *node)
{
  NodeGeometryPizza *data = MEM_cnew<NodeGeometryPizza>(__func__);
  data->olive_count = 5;
  data->runtime = MEM_new<RuntimeData>(__func__); // Alloc runtime data
  node->storage = data;
}

static void node_free_storage(struct bNode *node)
{
  NodeGeometryPizza &storage = node_storage(*node);
  // If there is some runtime data, delete it
  if (storage.runtime != nullptr) {
    MEM_delete<RuntimeData>(storage.runtime);
    storage.runtime = nullptr;
  }
  // Then call the regular free callback
  node_free_standard_storage(node);
}

static void node_copy_storage(struct bNodeTree *dest_ntree,
                       struct bNode *dest_node,
                       const struct bNode *src_node)
{
  // First copy the storage data using the standard callback
  node_copy_standard_storage(dest_ntree, dest_node, src_node);

  // Then duplicate runtime memory and call RuntimeData::operator=()
  NodeGeometryOpenMfx &dest_storage = node_storage(*dest_node);
  const NodeGeometryOpenMfx &src_storage = node_storage(*src_node);
  dest_storage.runtime = MEM_new<RuntimeData>(__func__);
  *dest_storage.runtime = *src_storage.runtime;
}

and in register_node_type_geo_pizza():

node_type_storage(&ntype,
                  "NodeGeometryPizza",
                  file_ns::node_free_storage,
                  file_ns::node_copy_storage);

PS The pizza-related naming assumes that this tutorial was used as a base.

looking forward to his appearance

So far so good, the approach is working :slight_smile:

6 Likes

Oh god
C++ node plugin for blender ? YES PLEASE :innocent:
@JuanGea check this out

1 Like

One annoying issue with the appropach I have described so far: the RuntimeData pointer in storage is saved in the blend file. With modifiers there are blendWrite and blendRead callbacks to make sure we reset this kind of data, is there something similar for nodes?

edit: I found a big switch in void ntreeBlendReadData(BlendDataReader *reader, bNodeTree *ntree) in blenkernel/intern/node.cc that seems to be the place to handle this.