Files
test/source/blender/nodes/intern/geometry_nodes_repeat_zone.cc
Jacques Lucke 8ec9c62d3e Geometry Nodes: add Closures and Bundles behind experimental feature flag
This implements bundles and closures which are described in more detail in this
blog post: https://code.blender.org/2024/11/geometry-nodes-workshop-october-2024/

tl;dr:
* Bundles are containers that allow storing multiple socket values in a single
  value. Each value in the bundle is identified by a name. Bundles can be
  nested.
* Closures are functions that are created with the Closure Zone and can be
  evaluated with the Evaluate Closure node.

To use the patch, the `Bundle and Closure Nodes` experimental feature has to be
enabled. This is necessary, because these features are not fully done yet and
still need iterations to improve the workflow before they can be officially
released. These iterations are easier to do in `main` than in a separate branch
though. That's because this patch is quite large and somewhat prone to merge
conflicts. Also other work we want to do, depends on this.

This adds the following new nodes:
* Combine Bundle: can pack multiple values into one.
* Separate Bundle: extracts values from a bundle.
* Closure Zone: outputs a closure zone for use in the `Evaluate Closure` node.
* Evaluate Closure: evaluates the passed in closure.

Things that will be added soon after this lands:
* Fields in bundles and closures. The way this is done changes with #134811, so
  I rather implement this once both are in `main`.
* UI features for keeping sockets in sync (right now there are warnings only).

One bigger issue is the limited support for lazyness. For example, all inputs of
a Combine Bundle node will be evaluated, even if they are not all needed. The
same is true for all captured values of a closure. This is a deeper limitation
that needs to be resolved at some point. This will likely be done after an
initial version of this patch is done.

Pull Request: https://projects.blender.org/blender/blender/pulls/128340
2025-04-03 15:44:06 +02:00

429 lines
18 KiB
C++

/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "NOD_geometry_nodes_lazy_function.hh"
#include "BKE_compute_contexts.hh"
#include "BKE_node_runtime.hh"
#include "BKE_node_socket_value.hh"
#include "FN_lazy_function_execute.hh"
#include "BLT_translation.hh"
#include "BLI_array_utils.hh"
#include "DEG_depsgraph_query.hh"
#include "FN_lazy_function_graph_executor.hh"
namespace blender::nodes {
using bke::SocketValueVariant;
/**
* Wraps the execution of a repeat loop body. The purpose is to setup the correct #ComputeContext
* inside of the loop body. This is necessary to support correct logging inside of a repeat zone.
* An alternative would be to use a separate `LazyFunction` for every iteration, but that would
* have higher overhead.
*/
class RepeatBodyNodeExecuteWrapper : public lf::GraphExecutorNodeExecuteWrapper {
public:
const bNode *repeat_output_bnode_ = nullptr;
VectorSet<lf::FunctionNode *> *lf_body_nodes_ = nullptr;
void execute_node(const lf::FunctionNode &node,
lf::Params &params,
const lf::Context &context) const override
{
GeoNodesLFUserData &user_data = *static_cast<GeoNodesLFUserData *>(context.user_data);
const int iteration = lf_body_nodes_->index_of_try(const_cast<lf::FunctionNode *>(&node));
const LazyFunction &fn = node.function();
if (iteration == -1) {
/* The node is not a loop body node, just execute it normally. */
fn.execute(params, context);
return;
}
/* Setup context for the loop body evaluation. */
bke::RepeatZoneComputeContext body_compute_context{
user_data.compute_context, *repeat_output_bnode_, iteration};
GeoNodesLFUserData body_user_data = user_data;
body_user_data.compute_context = &body_compute_context;
body_user_data.log_socket_values = should_log_socket_values_for_context(
user_data, body_compute_context.hash());
GeoNodesLFLocalUserData body_local_user_data{body_user_data};
lf::Context body_context{context.storage, &body_user_data, &body_local_user_data};
fn.execute(params, body_context);
}
};
/**
* Knows which iterations of the loop evaluation have side effects.
*/
class RepeatZoneSideEffectProvider : public lf::GraphExecutorSideEffectProvider {
public:
const bNode *repeat_output_bnode_ = nullptr;
Span<lf::FunctionNode *> lf_body_nodes_;
Vector<const lf::FunctionNode *> get_nodes_with_side_effects(
const lf::Context &context) const override
{
GeoNodesLFUserData &user_data = *static_cast<GeoNodesLFUserData *>(context.user_data);
const GeoNodesCallData &call_data = *user_data.call_data;
if (!call_data.side_effect_nodes) {
return {};
}
const ComputeContextHash &context_hash = user_data.compute_context->hash();
const Span<int> iterations_with_side_effects =
call_data.side_effect_nodes->iterations_by_iteration_zone.lookup(
{context_hash, repeat_output_bnode_->identifier});
Vector<const lf::FunctionNode *> lf_nodes;
for (const int i : iterations_with_side_effects) {
if (i >= 0 && i < lf_body_nodes_.size()) {
lf_nodes.append(lf_body_nodes_[i]);
}
}
return lf_nodes;
}
};
struct RepeatEvalStorage {
LinearAllocator<> allocator;
VectorSet<lf::FunctionNode *> lf_body_nodes;
lf::Graph graph;
std::optional<LazyFunctionForLogicalOr> or_function;
std::optional<RepeatZoneSideEffectProvider> side_effect_provider;
std::optional<RepeatBodyNodeExecuteWrapper> body_execute_wrapper;
std::optional<lf::GraphExecutor> graph_executor;
Array<SocketValueVariant> index_values;
void *graph_executor_storage = nullptr;
bool multi_threading_enabled = false;
Vector<int> input_index_map;
Vector<int> output_index_map;
};
class LazyFunctionForRepeatZone : public LazyFunction {
private:
const bNodeTree &btree_;
const bke::bNodeTreeZone &zone_;
const bNode &repeat_output_bnode_;
const ZoneBuildInfo &zone_info_;
const ZoneBodyFunction &body_fn_;
public:
LazyFunctionForRepeatZone(const bNodeTree &btree,
const bke::bNodeTreeZone &zone,
ZoneBuildInfo &zone_info,
const ZoneBodyFunction &body_fn)
: btree_(btree),
zone_(zone),
repeat_output_bnode_(*zone.output_node),
zone_info_(zone_info),
body_fn_(body_fn)
{
debug_name_ = "Repeat Zone";
initialize_zone_wrapper(zone, zone_info, body_fn, true, inputs_, outputs_);
/* Iterations input is always used. */
inputs_[zone_info.indices.inputs.main[0]].usage = lf::ValueUsage::Used;
}
void *init_storage(LinearAllocator<> &allocator) const override
{
return allocator.construct<RepeatEvalStorage>().release();
}
void destruct_storage(void *storage) const override
{
RepeatEvalStorage *s = static_cast<RepeatEvalStorage *>(storage);
if (s->graph_executor_storage) {
s->graph_executor->destruct_storage(s->graph_executor_storage);
}
std::destroy_at(s);
}
void execute_impl(lf::Params &params, const lf::Context &context) const override
{
const ScopedNodeTimer node_timer{context, repeat_output_bnode_};
auto &user_data = *static_cast<GeoNodesLFUserData *>(context.user_data);
auto &local_user_data = *static_cast<GeoNodesLFLocalUserData *>(context.local_user_data);
const NodeGeometryRepeatOutput &node_storage = *static_cast<const NodeGeometryRepeatOutput *>(
repeat_output_bnode_.storage);
RepeatEvalStorage &eval_storage = *static_cast<RepeatEvalStorage *>(context.storage);
const int iterations_usage_index = zone_info_.indices.outputs.input_usages[0];
if (!params.output_was_set(iterations_usage_index)) {
/* The iterations input is always used. */
params.set_output(iterations_usage_index, true);
}
if (!eval_storage.graph_executor) {
/* Create the execution graph in the first evaluation. */
this->initialize_execution_graph(
params, eval_storage, node_storage, user_data, local_user_data);
}
/* Execute the graph for the repeat zone. */
lf::RemappedParams eval_graph_params{*eval_storage.graph_executor,
params,
eval_storage.input_index_map,
eval_storage.output_index_map,
eval_storage.multi_threading_enabled};
lf::Context eval_graph_context{
eval_storage.graph_executor_storage, context.user_data, context.local_user_data};
eval_storage.graph_executor->execute(eval_graph_params, eval_graph_context);
}
/**
* Generate a lazy-function graph that contains the loop body (`body_fn_`) as many times
* as there are iterations. Since this graph depends on the number of iterations, it can't be
* reused in general. We could consider caching a version of this graph per number of iterations,
* but right now that doesn't seem worth it. In practice, it takes much less time to create the
* graph than to execute it (for intended use cases of this generic implementation, more special
* case repeat loop evaluations could be implemented separately).
*/
void initialize_execution_graph(lf::Params &params,
RepeatEvalStorage &eval_storage,
const NodeGeometryRepeatOutput &node_storage,
GeoNodesLFUserData &user_data,
GeoNodesLFLocalUserData &local_user_data) const
{
const int num_repeat_items = node_storage.items_num;
const int num_border_links = body_fn_.indices.inputs.border_links.size();
/* Number of iterations to evaluate. */
const int iterations = std::max<int>(
0, params.get_input<SocketValueVariant>(zone_info_.indices.inputs.main[0]).get<int>());
if (iterations >= 10) {
/* Constructing and running the repeat zone has some overhead so that it's probably worth
* trying to do something else in the meantime already. */
lazy_threading::send_hint();
}
/* Show a warning when the inspection index is out of range. */
if (node_storage.inspection_index > 0) {
if (node_storage.inspection_index >= iterations) {
if (geo_eval_log::GeoTreeLogger *tree_logger = local_user_data.try_get_tree_logger(
user_data))
{
tree_logger->node_warnings.append(
*tree_logger->allocator,
{repeat_output_bnode_.identifier,
{geo_eval_log::NodeWarningType::Info, N_("Inspection index is out of range")}});
}
}
}
/* Take iterations input into account. */
const int main_inputs_offset = 1;
const int body_inputs_offset = 1;
lf::Graph &lf_graph = eval_storage.graph;
Vector<lf::GraphInputSocket *> lf_inputs;
Vector<lf::GraphOutputSocket *> lf_outputs;
for (const int i : inputs_.index_range()) {
const lf::Input &input = inputs_[i];
lf_inputs.append(&lf_graph.add_input(*input.type, this->input_name(i)));
}
for (const int i : outputs_.index_range()) {
const lf::Output &output = outputs_[i];
lf_outputs.append(&lf_graph.add_output(*output.type, this->output_name(i)));
}
/* Create body nodes. */
VectorSet<lf::FunctionNode *> &lf_body_nodes = eval_storage.lf_body_nodes;
for ([[maybe_unused]] const int i : IndexRange(iterations)) {
lf::FunctionNode &lf_node = lf_graph.add_function(*body_fn_.function);
lf_body_nodes.add_new(&lf_node);
}
/* Create nodes for combining border link usages. A border link is used when any of the loop
* bodies uses the border link, so an "or" node is necessary. */
Array<lf::FunctionNode *> lf_border_link_usage_or_nodes(num_border_links);
eval_storage.or_function.emplace(iterations);
for (const int i : IndexRange(num_border_links)) {
lf::FunctionNode &lf_node = lf_graph.add_function(*eval_storage.or_function);
lf_border_link_usage_or_nodes[i] = &lf_node;
}
const bool use_index_values = zone_.input_node->output_socket(0).is_directly_linked();
if (use_index_values) {
eval_storage.index_values.reinitialize(iterations);
threading::parallel_for(IndexRange(iterations), 1024, [&](const IndexRange range) {
for (const int i : range) {
eval_storage.index_values[i].set(i);
}
});
}
/* Handle body nodes one by one. */
static const SocketValueVariant static_unused_index{-1};
for (const int iter_i : lf_body_nodes.index_range()) {
lf::FunctionNode &lf_node = *lf_body_nodes[iter_i];
const SocketValueVariant *index_value = use_index_values ?
&eval_storage.index_values[iter_i] :
&static_unused_index;
lf_node.input(body_fn_.indices.inputs.main[0]).set_default_value(index_value);
for (const int i : IndexRange(num_border_links)) {
lf_graph.add_link(*lf_inputs[zone_info_.indices.inputs.border_links[i]],
lf_node.input(body_fn_.indices.inputs.border_links[i]));
lf_graph.add_link(lf_node.output(body_fn_.indices.outputs.border_link_usages[i]),
lf_border_link_usage_or_nodes[i]->input(iter_i));
}
/* Handle reference sets. */
for (const auto &item : body_fn_.indices.inputs.reference_sets.items()) {
lf_graph.add_link(*lf_inputs[zone_info_.indices.inputs.reference_sets.lookup(item.key)],
lf_node.input(item.value));
}
}
static bool static_true = true;
/* Handle body nodes pair-wise. */
for (const int iter_i : lf_body_nodes.index_range().drop_back(1)) {
lf::FunctionNode &lf_node = *lf_body_nodes[iter_i];
lf::FunctionNode &lf_next_node = *lf_body_nodes[iter_i + 1];
for (const int i : IndexRange(num_repeat_items)) {
lf_graph.add_link(
lf_node.output(body_fn_.indices.outputs.main[i]),
lf_next_node.input(body_fn_.indices.inputs.main[i + body_inputs_offset]));
/* TODO: Add back-link after being able to check for cyclic dependencies. */
// lf_graph.add_link(lf_next_node.output(body_fn_.indices.outputs.input_usages[i]),
// lf_node.input(body_fn_.indices.inputs.output_usages[i]));
lf_node.input(body_fn_.indices.inputs.output_usages[i]).set_default_value(&static_true);
}
}
/* Handle border link usage outputs. */
for (const int i : IndexRange(num_border_links)) {
lf_graph.add_link(lf_border_link_usage_or_nodes[i]->output(0),
*lf_outputs[zone_info_.indices.outputs.border_link_usages[i]]);
}
if (iterations > 0) {
{
/* Link first body node to input/output nodes. */
lf::FunctionNode &lf_first_body_node = *lf_body_nodes[0];
for (const int i : IndexRange(num_repeat_items)) {
lf_graph.add_link(
*lf_inputs[zone_info_.indices.inputs.main[i + main_inputs_offset]],
lf_first_body_node.input(body_fn_.indices.inputs.main[i + body_inputs_offset]));
lf_graph.add_link(
lf_first_body_node.output(
body_fn_.indices.outputs.input_usages[i + body_inputs_offset]),
*lf_outputs[zone_info_.indices.outputs.input_usages[i + main_inputs_offset]]);
}
}
{
/* Link last body node to input/output nodes. */
lf::FunctionNode &lf_last_body_node = *lf_body_nodes.as_span().last();
for (const int i : IndexRange(num_repeat_items)) {
lf_graph.add_link(lf_last_body_node.output(body_fn_.indices.outputs.main[i]),
*lf_outputs[zone_info_.indices.outputs.main[i]]);
lf_graph.add_link(*lf_inputs[zone_info_.indices.inputs.output_usages[i]],
lf_last_body_node.input(body_fn_.indices.inputs.output_usages[i]));
}
}
}
else {
/* There are no iterations, just link the input directly to the output. */
for (const int i : IndexRange(num_repeat_items)) {
lf_graph.add_link(*lf_inputs[zone_info_.indices.inputs.main[i + main_inputs_offset]],
*lf_outputs[zone_info_.indices.outputs.main[i]]);
lf_graph.add_link(
*lf_inputs[zone_info_.indices.inputs.output_usages[i]],
*lf_outputs[zone_info_.indices.outputs.input_usages[i + main_inputs_offset]]);
}
for (const int i : IndexRange(num_border_links)) {
static bool static_false = false;
lf_outputs[zone_info_.indices.outputs.border_link_usages[i]]->set_default_value(
&static_false);
}
}
lf_outputs[zone_info_.indices.outputs.input_usages[0]]->set_default_value(&static_true);
/* The graph is ready, update the node indices which are required by the executor. */
lf_graph.update_node_indices();
// std::cout << "\n\n" << lf_graph.to_dot() << "\n\n";
/* Create a mapping from parameter indices inside of this graph to parameters of the repeat
* zone. The main complexity below stems from the fact that the iterations input is handled
* outside of this graph. */
eval_storage.output_index_map.reinitialize(outputs_.size() - 1);
eval_storage.input_index_map.resize(inputs_.size() - 1);
array_utils::fill_index_range<int>(eval_storage.input_index_map, 1);
Vector<const lf::GraphInputSocket *> lf_graph_inputs = lf_inputs.as_span().drop_front(1);
const int iteration_usage_index = zone_info_.indices.outputs.input_usages[0];
array_utils::fill_index_range<int>(
eval_storage.output_index_map.as_mutable_span().take_front(iteration_usage_index));
array_utils::fill_index_range<int>(
eval_storage.output_index_map.as_mutable_span().drop_front(iteration_usage_index),
iteration_usage_index + 1);
Vector<const lf::GraphOutputSocket *> lf_graph_outputs = lf_outputs.as_span().take_front(
iteration_usage_index);
lf_graph_outputs.extend(lf_outputs.as_span().drop_front(iteration_usage_index + 1));
eval_storage.body_execute_wrapper.emplace();
eval_storage.body_execute_wrapper->repeat_output_bnode_ = &repeat_output_bnode_;
eval_storage.body_execute_wrapper->lf_body_nodes_ = &lf_body_nodes;
eval_storage.side_effect_provider.emplace();
eval_storage.side_effect_provider->repeat_output_bnode_ = &repeat_output_bnode_;
eval_storage.side_effect_provider->lf_body_nodes_ = lf_body_nodes;
eval_storage.graph_executor.emplace(lf_graph,
std::move(lf_graph_inputs),
std::move(lf_graph_outputs),
nullptr,
&*eval_storage.side_effect_provider,
&*eval_storage.body_execute_wrapper);
eval_storage.graph_executor_storage = eval_storage.graph_executor->init_storage(
eval_storage.allocator);
/* Log graph for debugging purposes. */
bNodeTree &btree_orig = *reinterpret_cast<bNodeTree *>(
DEG_get_original_id(const_cast<ID *>(&btree_.id)));
if (btree_orig.runtime->logged_zone_graphs) {
std::lock_guard lock{btree_orig.runtime->logged_zone_graphs->mutex};
btree_orig.runtime->logged_zone_graphs->graph_by_zone_id.lookup_or_add_cb(
repeat_output_bnode_.identifier, [&]() { return lf_graph.to_dot(); });
}
}
std::string input_name(const int i) const override
{
return zone_wrapper_input_name(zone_info_, zone_, inputs_, i);
}
std::string output_name(const int i) const override
{
return zone_wrapper_output_name(zone_info_, zone_, outputs_, i);
}
};
LazyFunction &build_repeat_zone_lazy_function(ResourceScope &scope,
const bNodeTree &btree,
const bke::bNodeTreeZone &zone,
ZoneBuildInfo &zone_info,
const ZoneBodyFunction &body_fn)
{
return scope.construct<LazyFunctionForRepeatZone>(btree, zone, zone_info, body_fn);
}
} // namespace blender::nodes