From 80441190c6e04ec2c2e220d91bd0b32701417f03 Mon Sep 17 00:00:00 2001 From: Jacques Lucke Date: Tue, 21 Jan 2025 12:53:24 +0100 Subject: [PATCH] Nodes: automatically gray out input values that don't affect the output This patch automatically grays out input values which can't affect the output currently. It works with inputs of group nodes, geometry nodes modifiers and node tools. To achieve this, it analyses the node tree and partially evaluates it to figure out which group inputs are currently not linked to an output or are disabled by e.g. some switch node. Original proposal: https://devtalk.blender.org/t/dynamic-socket-visibility/31874 Related info in blog post: https://code.blender.org/2023/11/geometry-nodes-workshop-november-2023/#dynamic-socket-visibility Follow up task for designing a UI that allows hiding sockets: #132706 Limitations: * The inferencing does not update correctly when a socket starts being animated/driven. I haven't found a good way to invalidate the cache in a good way reliably yet. It's only a very short term problem though. It fixes itself after the next modification of the node tree and is only noticeable when animating some specific sockets such as the switch node condition. * Whether a socket is grayed out is not exposed in the Python API yet. That will be done separately. * Only a partial evaluation is done to determine if an input affects an output. There should be no cases where a socket is found to be unused when it can actually affect the output. However, there can be cases where a socket is inferenced to be used even if it is not due to some complex condition. Depending on the exact circumstances, this can either be improved or the condition in the node tree should be simplified. Pull Request: https://projects.blender.org/blender/blender/pulls/132219 --- source/blender/blenkernel/BKE_node_runtime.hh | 30 + .../blender/blenkernel/intern/node_runtime.cc | 15 + .../blenkernel/intern/node_tree_update.cc | 1 + source/blender/blenlib/BLI_multi_value_map.hh | 9 + .../editors/geometry/node_group_operator.cc | 20 +- .../interface_template_node_inputs.cc | 24 + source/blender/editors/space_node/drawnode.cc | 4 + .../blender/editors/space_node/node_draw.cc | 32 +- source/blender/makesdna/DNA_node_types.h | 7 + source/blender/modifiers/intern/MOD_nodes.cc | 34 +- source/blender/nodes/CMakeLists.txt | 2 + .../nodes/NOD_geometry_nodes_execute.hh | 13 + .../nodes/NOD_socket_usage_inference.hh | 52 + .../nodes/intern/geometry_nodes_execute.cc | 65 ++ .../nodes/intern/socket_usage_inference.cc | 907 ++++++++++++++++++ 15 files changed, 1209 insertions(+), 6 deletions(-) create mode 100644 source/blender/nodes/NOD_socket_usage_inference.hh create mode 100644 source/blender/nodes/intern/socket_usage_inference.cc diff --git a/source/blender/blenkernel/BKE_node_runtime.hh b/source/blender/blenkernel/BKE_node_runtime.hh index 6b6d8d0ae7c..7a05be67a7c 100644 --- a/source/blender/blenkernel/BKE_node_runtime.hh +++ b/source/blender/blenkernel/BKE_node_runtime.hh @@ -161,6 +161,14 @@ class bNodeTreeRuntime : NonCopyable, NonMovable { std::unique_ptr reference_lifetimes_info; std::unique_ptr gizmo_propagation; + /** + * A bool for each input socket (indexed by `index_in_all_inputs()`) that indicates whether this + * socket is used by the node it belongs to. Sockets for which this is false may e.g. be grayed + * out. + */ + blender::Array inferenced_input_socket_usage; + CacheMutex inferenced_input_socket_usage_mutex; + /** * For geometry nodes, a lazy function graph with some additional info is cached. This is used to * evaluate the node group. Caching it here allows us to reuse the preprocessed node tree in case @@ -714,6 +722,28 @@ inline blender::IndexRange bNode::output_socket_indices_in_tree() const return blender::IndexRange::from_begin_size(this->output_socket(0).index_in_tree(), num_outputs); } +inline blender::IndexRange bNode::input_socket_indices_in_all_inputs() const +{ + BLI_assert(blender::bke::node_tree_runtime::topology_cache_is_available(*this)); + const int num_inputs = this->runtime->inputs.size(); + if (num_inputs == 0) { + return {}; + } + return blender::IndexRange::from_begin_size(this->input_socket(0).index_in_all_inputs(), + num_inputs); +} + +inline blender::IndexRange bNode::output_socket_indices_in_all_outputs() const +{ + BLI_assert(blender::bke::node_tree_runtime::topology_cache_is_available(*this)); + const int num_outputs = this->runtime->outputs.size(); + if (num_outputs == 0) { + return {}; + } + return blender::IndexRange::from_begin_size(this->output_socket(0).index_in_all_outputs(), + num_outputs); +} + inline bNodeSocket &bNode::input_socket(int index) { BLI_assert(blender::bke::node_tree_runtime::topology_cache_is_available(*this)); diff --git a/source/blender/blenkernel/intern/node_runtime.cc b/source/blender/blenkernel/intern/node_runtime.cc index aedf11e62a2..d27de7c5a34 100644 --- a/source/blender/blenkernel/intern/node_runtime.cc +++ b/source/blender/blenkernel/intern/node_runtime.cc @@ -13,6 +13,7 @@ #include "NOD_geometry_nodes_lazy_function.hh" #include "NOD_node_declaration.hh" +#include "NOD_socket_usage_inference.hh" namespace blender::bke::node_tree_runtime { @@ -662,3 +663,17 @@ bNodeSocket &bNode::socket_by_decl(const blender::nodes::SocketDeclaration &decl { return decl.in_out == SOCK_IN ? this->input_socket(decl.index) : this->output_socket(decl.index); } + +bool bNodeSocket::affects_node_output() const +{ + BLI_assert(this->is_input()); + BLI_assert(blender::bke::node_tree_runtime::topology_cache_is_available(*this)); + const bNodeTree &tree = this->owner_tree(); + + tree.runtime->inferenced_input_socket_usage_mutex.ensure([&]() { + tree.runtime->inferenced_input_socket_usage = + blender::nodes::socket_usage_inference::infer_all_input_sockets_usage(tree); + }); + + return tree.runtime->inferenced_input_socket_usage[this->index_in_all_inputs()]; +} diff --git a/source/blender/blenkernel/intern/node_tree_update.cc b/source/blender/blenkernel/intern/node_tree_update.cc index 345b432d7d5..20b21c0e1cc 100644 --- a/source/blender/blenkernel/intern/node_tree_update.cc +++ b/source/blender/blenkernel/intern/node_tree_update.cc @@ -67,6 +67,7 @@ static void add_tree_tag(bNodeTree *ntree, const eNodeTreeChangedFlag flag) ntree->runtime->changed_flag |= flag; ntree->runtime->topology_cache_mutex.tag_dirty(); ntree->runtime->tree_zones_cache_mutex.tag_dirty(); + ntree->runtime->inferenced_input_socket_usage_mutex.tag_dirty(); } static void add_node_tag(bNodeTree *ntree, bNode *node, const eNodeTreeChangedFlag flag) diff --git a/source/blender/blenlib/BLI_multi_value_map.hh b/source/blender/blenlib/BLI_multi_value_map.hh index 259c9b5158e..b66af542db8 100644 --- a/source/blender/blenlib/BLI_multi_value_map.hh +++ b/source/blender/blenlib/BLI_multi_value_map.hh @@ -125,6 +125,15 @@ template class MultiValueMap { return map_.size(); } + /** + * Returns true if there are no keys in the map. + * NOTE: There may be keys without values. In this case the map is not empty. + */ + bool is_empty() const + { + return map_.is_empty(); + } + /** * NOTE: This signature will change when the implementation changes. */ diff --git a/source/blender/editors/geometry/node_group_operator.cc b/source/blender/editors/geometry/node_group_operator.cc index 430de37ccf2..9afa6aa30b2 100644 --- a/source/blender/editors/geometry/node_group_operator.cc +++ b/source/blender/editors/geometry/node_group_operator.cc @@ -66,6 +66,7 @@ #include "NOD_geometry_nodes_dependencies.hh" #include "NOD_geometry_nodes_execute.hh" #include "NOD_geometry_nodes_lazy_function.hh" +#include "NOD_socket_usage_inference.hh" #include "AS_asset_catalog.hh" #include "AS_asset_catalog_path.hh" @@ -715,7 +716,8 @@ static void draw_property_for_socket(const bNodeTree &node_tree, PointerRNA *bmain_ptr, PointerRNA *op_ptr, const bNodeTreeInterfaceSocket &socket, - const int socket_index) + const int socket_index, + const bool affects_output) { bke::bNodeSocketType *typeinfo = bke::node_socket_type_find(socket.socket_type); const eNodeSocketDatatype socket_type = eNodeSocketDatatype(typeinfo->type); @@ -736,6 +738,7 @@ static void draw_property_for_socket(const bNodeTree &node_tree, SNPRINTF(rna_path, "[\"%s\"]", socket_id_esc); uiLayout *row = uiLayoutRow(layout, true); + uiLayoutSetActive(row, affects_output); uiLayoutSetPropDecorate(row, false); /* Use #uiItemPointerR to draw pointer properties because #uiItemR would not have enough @@ -786,10 +789,21 @@ static void run_node_group_ui(bContext *C, wmOperator *op) } node_tree->ensure_interface_cache(); + + Array input_usages(node_tree->interface_inputs().size()); + nodes::socket_usage_inference::infer_group_interface_inputs_usage( + *node_tree, op->properties, input_usages); + int input_index = 0; for (const bNodeTreeInterfaceSocket *io_socket : node_tree->interface_inputs()) { - draw_property_for_socket( - *node_tree, layout, op->properties, &bmain_ptr, op->ptr, *io_socket, input_index); + draw_property_for_socket(*node_tree, + layout, + op->properties, + &bmain_ptr, + op->ptr, + *io_socket, + input_index, + input_usages[input_index]); ++input_index; } } diff --git a/source/blender/editors/interface/templates/interface_template_node_inputs.cc b/source/blender/editors/interface/templates/interface_template_node_inputs.cc index 245aa579cd1..eb99b778464 100644 --- a/source/blender/editors/interface/templates/interface_template_node_inputs.cc +++ b/source/blender/editors/interface/templates/interface_template_node_inputs.cc @@ -73,6 +73,28 @@ static void draw_node_input(bContext *C, socket.typeinfo->draw(C, row, &socket_ptr, node_ptr, text); } +static bool panel_has_input_affecting_node_output( + const bNode &node, const blender::nodes::PanelDeclaration &panel_decl) +{ + for (const blender::nodes::ItemDeclaration *item_decl : panel_decl.items) { + if (const auto *socket_decl = dynamic_cast(item_decl)) { + if (socket_decl->in_out == SOCK_OUT) { + continue; + } + const bNodeSocket &socket = node.socket_by_decl(*socket_decl); + if (socket.affects_node_output()) { + return true; + } + } + else if (const auto *sub_panel_decl = dynamic_cast(item_decl)) { + if (panel_has_input_affecting_node_output(node, *sub_panel_decl)) { + return true; + } + } + } + return false; +} + static void draw_node_inputs_recursive(bContext *C, uiLayout *layout, bNode &node, @@ -82,6 +104,8 @@ static void draw_node_inputs_recursive(bContext *C, /* TODO: Use flag on the panel state instead which is better for dynamic panel amounts. */ const std::string panel_idname = "NodePanel" + std::to_string(panel_decl.identifier); PanelLayout panel = uiLayoutPanel(C, layout, panel_idname.c_str(), panel_decl.default_collapsed); + const bool has_used_inputs = panel_has_input_affecting_node_output(node, panel_decl); + uiLayoutSetActive(panel.header, has_used_inputs); uiItemL(panel.header, IFACE_(panel_decl.name.c_str()), ICON_NONE); if (!panel.body) { return; diff --git a/source/blender/editors/space_node/drawnode.cc b/source/blender/editors/space_node/drawnode.cc index 6095fbf7585..24aa1322c6c 100644 --- a/source/blender/editors/space_node/drawnode.cc +++ b/source/blender/editors/space_node/drawnode.cc @@ -1284,6 +1284,10 @@ static void std_node_socket_draw( int type = sock->typeinfo->type; // int subtype = sock->typeinfo->subtype; + if (sock->is_input() && !sock->affects_node_output()) { + uiLayoutSetActive(layout, false); + } + /* XXX not nice, eventually give this node its own socket type ... */ if (node->type_legacy == CMP_NODE_OUTPUT_FILE) { node_file_output_socket_draw(C, layout, ptr, node_ptr); diff --git a/source/blender/editors/space_node/node_draw.cc b/source/blender/editors/space_node/node_draw.cc index 69f922b3bb3..662095a94b2 100644 --- a/source/blender/editors/space_node/node_draw.cc +++ b/source/blender/editors/space_node/node_draw.cc @@ -2445,6 +2445,34 @@ static void node_draw_panels_background(const bNode &node) } } +/** + * Note that this is different from #panel_has_input_affecting_node_output in how it treats output + * sockets. Within the node UI, the panel should not be grayed out if it has an output socket. + * However, the sidebar only shows inputs, so output sockets should be ignored. + */ +static bool panel_has_only_inactive_inputs(const bNode &node, + const nodes::PanelDeclaration &panel_decl) +{ + for (const nodes::ItemDeclaration *item_decl : panel_decl.items) { + if (const auto *socket_decl = dynamic_cast(item_decl)) { + if (socket_decl->in_out == SOCK_OUT) { + return false; + } + const bNodeSocket &socket = node.socket_by_decl(*socket_decl); + if (socket.affects_node_output()) { + return false; + } + } + else if (const auto *sub_panel_decl = dynamic_cast(item_decl)) + { + if (!panel_has_only_inactive_inputs(node, *sub_panel_decl)) { + return false; + } + } + } + return true; +} + static void node_draw_panels(bNodeTree &ntree, const bNode &node, uiBlock &block) { BLI_assert(is_node_panels_supported(node)); @@ -2494,7 +2522,9 @@ static void node_draw_panels(bNodeTree &ntree, const bNode &node, uiBlock &block 0, 0, ""); - if (node.is_muted()) { + + const bool only_inactive_inputs = panel_has_only_inactive_inputs(node, panel_decl); + if (node.is_muted() || only_inactive_inputs) { UI_but_flag_enable(label_but, UI_BUT_INACTIVE); } diff --git a/source/blender/makesdna/DNA_node_types.h b/source/blender/makesdna/DNA_node_types.h index d660b0d1f75..89bb7176dac 100644 --- a/source/blender/makesdna/DNA_node_types.h +++ b/source/blender/makesdna/DNA_node_types.h @@ -207,6 +207,11 @@ typedef struct bNodeSocket { bool is_input() const; bool is_output() const; + /** + * False when this input socket definitely does not affect the output. + */ + bool affects_node_output() const; + /** Utility to access the value of the socket. */ template T *default_value_typed(); template const T *default_value_typed() const; @@ -501,10 +506,12 @@ typedef struct bNode { blender::Span input_sockets(); blender::Span input_sockets() const; blender::IndexRange input_socket_indices_in_tree() const; + blender::IndexRange input_socket_indices_in_all_inputs() const; /** A span containing all output sockets of the node (including unavailable sockets). */ blender::Span output_sockets(); blender::Span output_sockets() const; blender::IndexRange output_socket_indices_in_tree() const; + blender::IndexRange output_socket_indices_in_all_outputs() const; /** Utility to get an input socket by its index. */ bNodeSocket &input_socket(int index); const bNodeSocket &input_socket(int index) const; diff --git a/source/blender/modifiers/intern/MOD_nodes.cc b/source/blender/modifiers/intern/MOD_nodes.cc index 80f8df5eba3..3f4daed5949 100644 --- a/source/blender/modifiers/intern/MOD_nodes.cc +++ b/source/blender/modifiers/intern/MOD_nodes.cc @@ -97,6 +97,7 @@ #include "NOD_geometry_nodes_gizmos.hh" #include "NOD_geometry_nodes_lazy_function.hh" #include "NOD_node_declaration.hh" +#include "NOD_socket_usage_inference.hh" #include "FN_field.hh" #include "FN_lazy_function_execute.hh" @@ -2014,6 +2015,7 @@ struct DrawGroupInputsContext { NodesModifierData &nmd; PointerRNA *md_ptr; PointerRNA *bmain_ptr; + Array input_usages; }; static void add_attribute_search_button(DrawGroupInputsContext &ctx, @@ -2159,10 +2161,11 @@ static void draw_property_for_socket(DrawGroupInputsContext &ctx, char rna_path[sizeof(socket_id_esc) + 4]; SNPRINTF(rna_path, "[\"%s\"]", socket_id_esc); + const int input_index = ctx.nmd.node_group->interface_input_index(socket); + uiLayout *row = uiLayoutRow(layout, true); uiLayoutSetPropDecorate(row, true); - - const int input_index = ctx.nmd.node_group->interface_input_index(socket); + uiLayoutSetActive(row, ctx.input_usages[input_index]); /* Use #uiItemPointerR to draw pointer properties because #uiItemR would not have enough * information about what type of ID to select for editing the values. This is because @@ -2263,6 +2266,27 @@ static bool interface_panel_has_socket(const bNodeTreeInterfacePanel &interface_ return false; } +static bool interface_panel_affects_output(DrawGroupInputsContext &ctx, + const bNodeTreeInterfacePanel &panel) +{ + for (const bNodeTreeInterfaceItem *item : panel.items()) { + if (item->item_type == NODE_INTERFACE_SOCKET) { + const auto &socket = *reinterpret_cast(item); + const int input_index = ctx.nmd.node_group->interface_input_index(socket); + if (ctx.input_usages[input_index]) { + return true; + } + } + else if (item->item_type == NODE_INTERFACE_PANEL) { + const auto &sub_interface_panel = *reinterpret_cast(item); + if (interface_panel_affects_output(ctx, sub_interface_panel)) { + return true; + } + } + } + return false; +} + static void draw_interface_panel_content(DrawGroupInputsContext &ctx, uiLayout *layout, const bNodeTreeInterfacePanel &interface_panel) @@ -2278,6 +2302,9 @@ static void draw_interface_panel_content(DrawGroupInputsContext &ctx, ctx.md_ptr->owner_id, &RNA_NodesModifierPanel, panel); PanelLayout panel_layout = uiLayoutPanelProp(&ctx.C, layout, &panel_ptr, "is_open"); uiItemL(panel_layout.header, IFACE_(sub_interface_panel.name), ICON_NONE); + if (!interface_panel_affects_output(ctx, sub_interface_panel)) { + uiLayoutSetActive(panel_layout.header, false); + } uiLayoutSetTooltipFunc( panel_layout.header, [](bContext * /*C*/, void *panel_arg, const char * /*tip*/) -> std::string { @@ -2500,6 +2527,9 @@ static void panel_draw(const bContext *C, Panel *panel) if (nmd->node_group != nullptr && nmd->settings.properties != nullptr) { nmd->node_group->ensure_interface_cache(); + ctx.input_usages.reinitialize(nmd->node_group->interface_inputs().size()); + nodes::socket_usage_inference::infer_group_interface_inputs_usage( + *nmd->node_group, nmd->settings.properties, ctx.input_usages); draw_interface_panel_content(ctx, layout, nmd->node_group->tree_interface.root_panel); } diff --git a/source/blender/nodes/CMakeLists.txt b/source/blender/nodes/CMakeLists.txt index 0492a905375..a157067c6b3 100644 --- a/source/blender/nodes/CMakeLists.txt +++ b/source/blender/nodes/CMakeLists.txt @@ -89,6 +89,7 @@ set(SRC intern/node_util.cc intern/partial_eval.cc intern/socket_search_link.cc + intern/socket_usage_inference.cc intern/value_elem.cc NOD_common.hh @@ -121,6 +122,7 @@ set(SRC NOD_socket_items_ops.hh NOD_socket_items_ui.hh NOD_socket_search_link.hh + NOD_socket_usage_inference.hh NOD_texture.h NOD_value_elem.hh NOD_value_elem_eval.hh diff --git a/source/blender/nodes/NOD_geometry_nodes_execute.hh b/source/blender/nodes/NOD_geometry_nodes_execute.hh index adc7ce837c0..d3f58e2852d 100644 --- a/source/blender/nodes/NOD_geometry_nodes_execute.hh +++ b/source/blender/nodes/NOD_geometry_nodes_execute.hh @@ -6,7 +6,9 @@ #include "BLI_compute_context.hh" #include "BLI_function_ref.hh" +#include "BLI_generic_pointer.hh" #include "BLI_multi_value_map.hh" +#include "BLI_resource_scope.hh" #include "BLI_set.hh" #include "BKE_idprop.hh" @@ -66,4 +68,15 @@ void update_output_properties_from_node_tree(const bNodeTree &tree, const IDProperty *old_properties, IDProperty &properties); +/** + * Get the "base" input values that are passed into geometry nodes. In this context, "base" means + * that the retrieved input types are #bNodeSocketType::base_cpp_type (e.g. `float` for float + * sockets). If the input value can't be represented as base value, null is returned instead (e.g. + * for attribute inputs). + */ +void get_geometry_nodes_input_base_values(const bNodeTree &btree, + const IDProperty *properties, + ResourceScope &scope, + MutableSpan r_values); + } // namespace blender::nodes diff --git a/source/blender/nodes/NOD_socket_usage_inference.hh b/source/blender/nodes/NOD_socket_usage_inference.hh new file mode 100644 index 00000000000..05fbc5e16a7 --- /dev/null +++ b/source/blender/nodes/NOD_socket_usage_inference.hh @@ -0,0 +1,52 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "BLI_array.hh" +#include "BLI_generic_pointer.hh" + +struct bNodeTree; +struct bNodeSocket; +struct IDProperty; + +namespace blender::nodes::socket_usage_inference { + +/** + * Get a boolean value for each input socket in the given tree that indicates whether that input is + * used. It is assumed that all output sockets in the tree are used. + */ +Array infer_all_input_sockets_usage(const bNodeTree &tree); + +/** + * Get a boolean value for each node group input that indicates whether that input is used by the + * outputs. The result can be used to e.g. gray out or hide individual inputs that are unused. + * + * \param group: The node group that is called. + * \param group_input_values: An optional input value for each node group input. The type is + * expected to be `bNodeSocketType::base_cpp_type`. If the input value for a socket is not known + * or can't be represented as base type, null has to be passed instead. + * \param r_input_usages: The destination array where the inferred usages are written. + */ +void infer_group_interface_inputs_usage(const bNodeTree &group, + Span group_input_values, + MutableSpan r_input_usages); + +/** + * Same as above, but automatically retrieves the input values from the given sockets.. + * This is used for group nodes. + */ +void infer_group_interface_inputs_usage(const bNodeTree &group, + Span input_sockets, + MutableSpan r_input_usages); + +/** + * Same as above, but automatically retrieves the input values from the given properties. + * This is used with the geometry nodes modifier and node tools. + */ +void infer_group_interface_inputs_usage(const bNodeTree &group, + const IDProperty *properties, + MutableSpan r_input_usages); + +} // namespace blender::nodes::socket_usage_inference diff --git a/source/blender/nodes/intern/geometry_nodes_execute.cc b/source/blender/nodes/intern/geometry_nodes_execute.cc index be2d21d04c5..24d9b99e6d2 100644 --- a/source/blender/nodes/intern/geometry_nodes_execute.cc +++ b/source/blender/nodes/intern/geometry_nodes_execute.cc @@ -1041,4 +1041,69 @@ void update_output_properties_from_node_tree(const bNodeTree &tree, } } +void get_geometry_nodes_input_base_values(const bNodeTree &btree, + const IDProperty *properties, + ResourceScope &scope, + MutableSpan r_values) +{ + if (!properties) { + return; + } + + /* Assume that all inputs have unknown values by default. */ + r_values.fill(nullptr); + + btree.ensure_interface_cache(); + for (const int input_i : btree.interface_inputs().index_range()) { + const bNodeTreeInterfaceSocket &io_input = *btree.interface_inputs()[input_i]; + const bke::bNodeSocketType *stype = io_input.socket_typeinfo(); + if (!stype) { + continue; + } + const eNodeSocketDatatype socket_type = eNodeSocketDatatype(stype->type); + if (!stype->base_cpp_type || !stype->geometry_nodes_cpp_type) { + continue; + } + const IDProperty *property = IDP_GetPropertyFromGroup(properties, io_input.identifier); + if (!property) { + continue; + } + if (!id_property_type_matches_socket(io_input, *property)) { + continue; + } + if (input_attribute_name_get(*properties, io_input).has_value()) { + /* Attributes don't have a single base value, so ignore them here.*/ + continue; + } + if (is_layer_selection_field(io_input)) { + /* Can't get a single value for layer selections. */ + continue; + } + + void *value_buffer = scope.linear_allocator().allocate( + stype->geometry_nodes_cpp_type->size(), stype->geometry_nodes_cpp_type->alignment()); + init_socket_cpp_value_from_property(*property, socket_type, value_buffer); + if (!stype->geometry_nodes_cpp_type->is_trivially_destructible()) { + scope.add_destruct_call([type = stype->geometry_nodes_cpp_type, value_buffer]() { + type->destruct(value_buffer); + }); + } + if (stype->geometry_nodes_cpp_type == stype->base_cpp_type) { + r_values[input_i] = {stype->base_cpp_type, value_buffer}; + continue; + } + if (stype->geometry_nodes_cpp_type == &CPPType::get()) { + const bke::SocketValueVariant &socket_value = *static_cast( + value_buffer); + if (!socket_value.is_single()) { + continue; + } + const GPointer single_value = socket_value.get_single_ptr(); + BLI_assert(single_value.type() == stype->base_cpp_type); + r_values[input_i] = single_value; + continue; + } + } +} + } // namespace blender::nodes diff --git a/source/blender/nodes/intern/socket_usage_inference.cc b/source/blender/nodes/intern/socket_usage_inference.cc new file mode 100644 index 00000000000..2df53eae238 --- /dev/null +++ b/source/blender/nodes/intern/socket_usage_inference.cc @@ -0,0 +1,907 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include + +#include "NOD_geometry_nodes_execute.hh" +#include "NOD_multi_function.hh" +#include "NOD_node_in_compute_context.hh" +#include "NOD_socket_usage_inference.hh" + +#include "DNA_anim_types.h" +#include "DNA_node_types.h" + +#include "BKE_compute_contexts.hh" +#include "BKE_node_legacy_types.hh" +#include "BKE_node_runtime.hh" +#include "BKE_type_conversions.hh" + +#include "ANIM_action.hh" +#include "ANIM_action_iterators.hh" + +#include "BLI_stack.hh" + +namespace blender::nodes::socket_usage_inference { + +/** Utility class to simplify passing global state into all the functions during inferencing. */ +struct SocketUsageInferencer { + private: + /** Owns e.g. intermediate evaluated values. */ + ResourceScope scope_; + + /** Root node tree. */ + const bNodeTree &root_tree_; + + /** + * Stack of tasks that allows depth-first (partial) evaluation of the tree. + */ + Stack usage_tasks_; + Stack value_tasks_; + + /** + * If the usage of a socket is known, it is added to this map. Sockets not in this map are not + * known yet. + */ + Map all_socket_usages_; + + /** + * If the value of a socket is known, it is added to this map. The value may be null, which means + * that the value can be anything. Sockets not in this map have not been evaluated yet. + */ + Map all_socket_values_; + + /** + * All sockets that have animation data and thus their value is not fixed statically. This can + * contain sockets from multiple different trees. + */ + Set animated_sockets_; + Set trees_with_handled_animation_data_; + + /** Some inline storage to reduce the number of allocations. */ + AlignedBuffer<1024, 8> scope_buffer_; + + public: + SocketUsageInferencer(const bNodeTree &tree, + const std::optional> tree_input_values) + : root_tree_(tree) + { + scope_.linear_allocator().provide_buffer(scope_buffer_); + root_tree_.ensure_topology_cache(); + root_tree_.ensure_interface_cache(); + this->ensure_animation_data_processed(root_tree_); + + for (const bNode *node : root_tree_.group_input_nodes()) { + for (const int i : root_tree_.interface_inputs().index_range()) { + const bNodeSocket &socket = node->output_socket(i); + const void *input_value = nullptr; + if (tree_input_values.has_value()) { + input_value = (*tree_input_values)[i].get(); + } + all_socket_values_.add_new({nullptr, &socket}, input_value); + } + } + } + + void mark_top_level_node_outputs_as_used() + { + for (const bNodeSocket *socket : root_tree_.all_output_sockets()) { + all_socket_usages_.add_new({nullptr, socket}, true); + } + } + + bool is_socket_used(const SocketInContext &socket) + { + const std::optional is_used = all_socket_usages_.lookup_try(socket); + if (is_used.has_value()) { + return *is_used; + } + if (socket->owner_tree().has_available_link_cycle()) { + return false; + } + + BLI_assert(usage_tasks_.is_empty()); + usage_tasks_.push(socket); + + while (!usage_tasks_.is_empty()) { + const SocketInContext &socket = usage_tasks_.peek(); + this->usage_task(socket); + if (&socket == &usage_tasks_.peek()) { + /* The task is finished if it hasn't added any new task it depends on.*/ + usage_tasks_.pop(); + } + } + + return all_socket_usages_.lookup(socket); + } + + const void *get_socket_value(const SocketInContext &socket) + { + const std::optional value = all_socket_values_.lookup_try(socket); + if (value.has_value()) { + return *value; + } + if (socket->owner_tree().has_available_link_cycle()) { + return nullptr; + } + + BLI_assert(value_tasks_.is_empty()); + value_tasks_.push(socket); + + while (!value_tasks_.is_empty()) { + const SocketInContext &socket = value_tasks_.peek(); + this->value_task(socket); + if (&socket == &value_tasks_.peek()) { + /* The task is finished if it hasn't added any new task it depends on.*/ + value_tasks_.pop(); + } + } + + return all_socket_values_.lookup(socket); + } + + private: + void usage_task(const SocketInContext &socket) + { + if (all_socket_usages_.contains(socket)) { + return; + } + if (socket->is_input()) { + this->usage_task__input(socket); + } + else { + this->usage_task__output(socket); + } + } + + void usage_task__input(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + switch (node->type_legacy) { + case NODE_GROUP: + case NODE_CUSTOM_GROUP: { + this->usage_task__input__group_node(socket); + break; + } + case NODE_GROUP_OUTPUT: { + this->usage_task__input__group_output_node(socket); + break; + } + case GEO_NODE_SWITCH: { + this->usage_task__input__generic_switch(socket, switch__is_socket_selected); + break; + } + case GEO_NODE_INDEX_SWITCH: { + this->usage_task__input__generic_switch(socket, index_switch__is_socket_selected); + break; + } + case GEO_NODE_MENU_SWITCH: { + this->usage_task__input__generic_switch(socket, menu_switch__is_socket_selected); + break; + } + case GEO_NODE_SIMULATION_INPUT: { + this->usage_task__input__simulation_input_node(socket); + break; + } + case GEO_NODE_REPEAT_INPUT: { + this->usage_task__input__repeat_input_node(socket); + break; + } + case GEO_NODE_FOREACH_GEOMETRY_ELEMENT_INPUT: { + this->usage_task__input__foreach_element_input_node(socket); + break; + } + case GEO_NODE_FOREACH_GEOMETRY_ELEMENT_OUTPUT: { + this->usage_task__input__foreach_element_output_node(socket); + break; + } + case GEO_NODE_CAPTURE_ATTRIBUTE: { + this->usage_task__input__capture_attribute_node(socket); + break; + } + default: { + this->usage_task__input__fallback(socket); + break; + } + } + } + + /** + * Assumes that the first input is a condition that selects one of the remaining inputs which is + * then output. If necessary, this can trigger a value task for the condition socket. + */ + void usage_task__input__generic_switch( + const SocketInContext &socket, + const FunctionRef + is_selected_socket) + { + const NodeInContext node = socket.owner_node(); + BLI_assert(node->input_sockets().size() >= 1); + BLI_assert(node->output_sockets().size() == 1); + + if (socket->type == SOCK_CUSTOM && STREQ(socket->idname, "NodeSocketVirtual")) { + all_socket_usages_.add_new(socket, false); + return; + } + const SocketInContext output_socket = node.output_socket(0); + const std::optional output_is_used = all_socket_usages_.lookup_try(output_socket); + if (!output_is_used.has_value()) { + this->push_usage_task(output_socket); + return; + } + if (!*output_is_used) { + all_socket_usages_.add_new(socket, false); + return; + } + const SocketInContext condition_socket = node.input_socket(0); + if (socket == condition_socket) { + all_socket_usages_.add_new(socket, true); + return; + } + const void *condition_value = this->get_socket_value(condition_socket); + if (condition_value == nullptr) { + /* The exact condition value is unknown, so any input may be used. */ + all_socket_usages_.add_new(socket, true); + return; + } + const bool is_used = is_selected_socket(socket, condition_value); + all_socket_usages_.add_new(socket, is_used); + } + + void usage_task__input__group_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + const bNodeTree *group = reinterpret_cast(node->id); + if (!group || ID_MISSING(&group->id)) { + all_socket_usages_.add_new(socket, false); + return; + } + group->ensure_topology_cache(); + if (group->has_available_link_cycle()) { + all_socket_usages_.add_new(socket, false); + return; + } + this->ensure_animation_data_processed(*group); + + /* The group node input is used iff any of the matching group inputs within the group is + * used. */ + const ComputeContext &group_context = scope_.construct( + socket.context, *node, node->owner_tree()); + Vector dependent_sockets; + for (const bNode *group_input_node : group->group_input_nodes()) { + dependent_sockets.append(&group_input_node->output_socket(socket->index())); + } + this->usage_task__with_dependent_sockets(socket, dependent_sockets, &group_context); + } + + void usage_task__input__group_output_node(const SocketInContext &socket) + { + const int output_i = socket->index(); + if (socket.context == nullptr) { + /* This is a final output which is always used. */ + all_socket_usages_.add_new(socket, true); + return; + } + /* The group output node is used iff the matching output of the parent group node is used. */ + const bke::GroupNodeComputeContext &group_context = + *static_cast(socket.context); + const bNodeSocket &group_node_output = group_context.caller_group_node()->output_socket( + output_i); + this->usage_task__with_dependent_sockets(socket, {&group_node_output}, group_context.parent()); + } + + void usage_task__output(const SocketInContext &socket) + { + /* An output socket is used if any of the sockets it is connected to is used. */ + Vector dependent_sockets; + for (const bNodeLink *link : socket->directly_linked_links()) { + if (link->is_used()) { + dependent_sockets.append(link->tosock); + } + } + this->usage_task__with_dependent_sockets(socket, dependent_sockets, socket.context); + } + + void usage_task__input__simulation_input_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + const bNodeTree &tree = socket->owner_tree(); + + const NodeGeometrySimulationInput &storage = *static_cast( + node->storage); + const bNode *sim_output_node = tree.node_by_id(storage.output_node_id); + if (!sim_output_node) { + all_socket_usages_.add_new(socket, false); + return; + } + /* Simulation inputs are also used when any of the simulation outputs are used. */ + Vector dependent_sockets; + dependent_sockets.extend(node->output_sockets()); + dependent_sockets.extend(sim_output_node->output_sockets()); + this->usage_task__with_dependent_sockets(socket, dependent_sockets, socket.context); + } + + void usage_task__input__repeat_input_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + const bNodeTree &tree = socket->owner_tree(); + + const NodeGeometryRepeatInput &storage = *static_cast( + node->storage); + const bNode *repeat_output_node = tree.node_by_id(storage.output_node_id); + if (!repeat_output_node) { + all_socket_usages_.add_new(socket, false); + return; + } + /* Assume that all repeat inputs are used when any of the outputs are used. This check could + * become more precise in the future if necessary. */ + Vector dependent_sockets; + dependent_sockets.extend(node->output_sockets()); + dependent_sockets.extend(repeat_output_node->output_sockets()); + this->usage_task__with_dependent_sockets(socket, dependent_sockets, socket.context); + } + + void usage_task__input__foreach_element_output_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + this->usage_task__with_dependent_sockets( + socket, {&node->output_by_identifier(socket->identifier)}, socket.context); + } + + void usage_task__input__capture_attribute_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + this->usage_task__with_dependent_sockets( + socket, {&node->output_socket(socket->index())}, socket.context); + } + + void usage_task__input__fallback(const SocketInContext &socket) + { + this->usage_task__with_dependent_sockets( + socket, socket->owner_node().output_sockets(), socket.context); + } + + void usage_task__input__foreach_element_input_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + const bNodeTree &tree = socket->owner_tree(); + + const NodeGeometryForeachGeometryElementInput &storage = + *static_cast(node->storage); + const bNode *foreach_output_node = tree.node_by_id(storage.output_node_id); + if (!foreach_output_node) { + all_socket_usages_.add_new(socket, false); + return; + } + Vector dependent_sockets; + if (StringRef(socket->identifier).startswith("Input_")) { + dependent_sockets.append(&node->output_by_identifier(socket->identifier)); + } + else { + /* The geometry and selection inputs are used whenever any of the zone outputs is used. */ + dependent_sockets.extend(node->output_sockets()); + dependent_sockets.extend(foreach_output_node->output_sockets()); + } + this->usage_task__with_dependent_sockets(socket, dependent_sockets, socket.context); + } + + /** + * Utility that handles simple cases where a socket is used if any of its dependent sockets is + * used. + */ + void usage_task__with_dependent_sockets(const SocketInContext &socket, + const Span dependent_sockets, + const ComputeContext *dependent_socket_context) + { + /* Check if any of the dependent sockets is used. */ + SocketInContext next_unknown_socket; + for (const bNodeSocket *dependent_socket_ptr : dependent_sockets) { + const SocketInContext dependent_socket{dependent_socket_context, dependent_socket_ptr}; + const std::optional is_used = all_socket_usages_.lookup_try(dependent_socket); + if (!is_used.has_value() && !next_unknown_socket) { + next_unknown_socket = dependent_socket; + continue; + } + if (is_used.value_or(false)) { + all_socket_usages_.add_new(socket, true); + return; + } + } + if (next_unknown_socket) { + /* Create a task that checks if the next dependent socket is used. Intentionally only create + * a task for the very next one and not for all, because that could potentially trigger a lot + * of unnecessary evaluations. */ + this->push_usage_task(next_unknown_socket); + return; + } + /* None of the dependent sockets is used, so the current socket is not used either. */ + all_socket_usages_.add_new(socket, false); + } + + void value_task(const SocketInContext &socket) + { + if (all_socket_values_.contains(socket)) { + /* Task is done already. */ + return; + } + const CPPType *base_type = socket->typeinfo->base_cpp_type; + if (!base_type) { + /* The socket type is unknown for some reason (maybe a socket type from the future?).*/ + all_socket_values_.add_new(socket, nullptr); + return; + } + if (socket->is_input()) { + this->value_task__input(socket); + } + else { + this->value_task__output(socket); + } + } + + void value_task__output(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + if (node->is_muted()) { + this->value_task__output__muted_node(socket); + return; + } + switch (node->type_legacy) { + case NODE_GROUP: + case NODE_CUSTOM_GROUP: { + this->value_task__output__group_node(socket); + return; + } + case NODE_GROUP_INPUT: { + this->value_task__output__group_input_node(socket); + return; + } + case GEO_NODE_SWITCH: { + this->value_task__output__generic_switch(socket, switch__is_socket_selected); + return; + } + case GEO_NODE_INDEX_SWITCH: { + this->value_task__output__generic_switch(socket, index_switch__is_socket_selected); + return; + } + case GEO_NODE_MENU_SWITCH: { + this->value_task__output__generic_switch(socket, menu_switch__is_socket_selected); + return; + } + default: { + if (node->typeinfo->build_multi_function) { + this->value_task__output__multi_function_node(socket); + return; + } + break; + } + } + /* If none of the above cases work, the socket value is set to null which means that it is + * unknown/dynamic. */ + all_socket_values_.add_new(socket, nullptr); + } + + void value_task__output__group_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + const bNodeTree *group = reinterpret_cast(node->id); + if (!group || ID_MISSING(&group->id)) { + all_socket_values_.add_new(socket, nullptr); + return; + } + if (group->has_available_link_cycle()) { + all_socket_values_.add_new(socket, nullptr); + return; + } + this->ensure_animation_data_processed(*group); + const bNode *group_output_node = group->group_output_node(); + if (!group_output_node) { + /* Can't compute the value if the group does not have an output node. */ + all_socket_values_.add_new(socket, nullptr); + return; + } + const ComputeContext &group_context = scope_.construct( + socket.context, *node, node->owner_tree()); + const SocketInContext socket_in_group{&group_context, + &group_output_node->input_socket(socket->index())}; + const std::optional value = all_socket_values_.lookup_try(socket_in_group); + if (!value.has_value()) { + this->push_value_task(socket_in_group); + return; + } + all_socket_values_.add_new(socket, *value); + } + + void value_task__output__group_input_node(const SocketInContext &socket) + { + /* Group inputs for the root context should be initialized already. */ + BLI_assert(socket.context != nullptr); + + const bke::GroupNodeComputeContext &group_context = + *static_cast(socket.context); + const SocketInContext group_node_input{ + group_context.parent(), &group_context.caller_group_node()->input_socket(socket->index())}; + const std::optional value = all_socket_values_.lookup_try(group_node_input); + if (!value.has_value()) { + this->push_value_task(group_node_input); + return; + } + all_socket_values_.add_new(socket, *value); + } + + /** + * Assumes that the first input is a condition that selects one of the remaining inputs which is + * then output. If necessary, this can trigger a value task for the condition socket. + */ + void value_task__output__generic_switch( + const SocketInContext &socket, + const FunctionRef + is_selected_socket) + { + const NodeInContext node = socket.owner_node(); + BLI_assert(node->input_sockets().size() >= 1); + BLI_assert(node->output_sockets().size() == 1); + + const SocketInContext condition_socket = node.input_socket(0); + const std::optional condition_value = all_socket_values_.lookup_try( + condition_socket); + if (!condition_value.has_value()) { + this->push_value_task(condition_socket); + return; + } + if (!*condition_value) { + /* The condition value is not a simple static value, so the output is unknown. */ + all_socket_values_.add_new(socket, nullptr); + return; + } + for (const int input_i : node->input_sockets().index_range().drop_front(1)) { + const SocketInContext input_socket = node.input_socket(input_i); + if (input_socket->type == SOCK_CUSTOM && STREQ(input_socket->idname, "NodeSocketVirtual")) { + continue; + } + const bool is_selected = is_selected_socket(input_socket, *condition_value); + if (!is_selected) { + continue; + } + const std::optional input_value = all_socket_values_.lookup_try(input_socket); + if (!input_value.has_value()) { + this->push_value_task(input_socket); + return; + } + all_socket_values_.add_new(socket, *input_value); + return; + } + /* The condition did not match any of the inputs, so the output is unknown. */ + all_socket_values_.add_new(socket, nullptr); + } + + void value_task__output__multi_function_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + const int inputs_num = node->input_sockets().size(); + + /* Gather all input values are return early if any of them is not known.*/ + Vector input_values(inputs_num); + for (const int input_i : IndexRange(inputs_num)) { + const SocketInContext input_socket = node.input_socket(input_i); + const std::optional input_value = all_socket_values_.lookup_try(input_socket); + if (!input_value.has_value()) { + this->push_value_task(input_socket); + return; + } + if (*input_value == nullptr) { + all_socket_values_.add_new(socket, nullptr); + return; + } + input_values[input_i] = *input_value; + } + + /* Get the multi-function for the node. */ + NodeMultiFunctionBuilder builder{*node.node, node->owner_tree()}; + node->typeinfo->build_multi_function(builder); + const mf::MultiFunction &fn = builder.function(); + + /* We only evaluate the node for a single value here. */ + const IndexMask mask(1); + + /* Prepare parameters for the multi-function evaluation. */ + mf::ParamsBuilder params{fn, &mask}; + for (const int input_i : IndexRange(inputs_num)) { + const SocketInContext input_socket = node.input_socket(input_i); + if (!input_socket->is_available()) { + continue; + } + params.add_readonly_single_input( + GPointer(input_socket->typeinfo->base_cpp_type, input_values[input_i])); + } + for (const int output_i : node->output_sockets().index_range()) { + const SocketInContext output_socket = node.output_socket(output_i); + if (!output_socket->is_available()) { + continue; + } + /* Allocate memory for the output value. */ + const CPPType &base_type = *output_socket->typeinfo->base_cpp_type; + void *value = scope_.linear_allocator().allocate(base_type.size(), base_type.alignment()); + params.add_uninitialized_single_output(GMutableSpan(base_type, value, 1)); + all_socket_values_.add_new(output_socket, value); + if (!base_type.is_trivially_destructible()) { + scope_.add_destruct_call( + [type = &base_type, value]() { type->destruct(const_cast(value)); }); + } + } + mf::ContextBuilder context; + /* Actually evaluate the multi-function. The outputs will be written into the memory allocated + * earlier, which has been added to #all_socket_values_ already. */ + fn.call(mask, params, context); + } + + void value_task__output__muted_node(const SocketInContext &socket) + { + const NodeInContext node = socket.owner_node(); + + SocketInContext input_socket; + for (const bNodeLink &internal_link : node->internal_links()) { + if (internal_link.tosock == socket.socket) { + input_socket = SocketInContext{socket.context, internal_link.fromsock}; + break; + } + } + if (!input_socket) { + /* The output does not have an internal link to an input. */ + all_socket_values_.add_new(socket, nullptr); + return; + } + const std::optional input_value = all_socket_values_.lookup_try(input_socket); + if (!input_value.has_value()) { + this->push_value_task(input_socket); + return; + } + const void *converted_value = this->convert_type_if_necessary( + *input_value, *input_socket.socket, *socket.socket); + all_socket_values_.add_new(socket, converted_value); + } + + void value_task__input(const SocketInContext &socket) + { + if (socket->is_multi_input()) { + /* Can't know the single value of a multi-input. */ + all_socket_values_.add_new(socket, nullptr); + return; + } + const bNodeLink *source_link = nullptr; + const Span connected_links = socket->directly_linked_links(); + for (const bNodeLink *link : connected_links) { + if (!link->is_used()) { + continue; + } + if (link->fromnode->is_dangling_reroute()) { + continue; + } + source_link = link; + break; + } + if (!source_link) { + this->value_task__input__unlinked(socket); + return; + } + this->value_task__input__linked({socket.context, source_link->fromsock}, socket); + } + + void value_task__input__unlinked(const SocketInContext &socket) + { + if (animated_sockets_.contains(socket.socket)) { + /* The value of animated sockets is not known statically. */ + all_socket_values_.add_new(socket, nullptr); + return; + } + + const CPPType &base_type = *socket->typeinfo->base_cpp_type; + void *value_buffer = scope_.linear_allocator().allocate(base_type.size(), + base_type.alignment()); + socket->typeinfo->get_base_cpp_value(socket->default_value, value_buffer); + all_socket_values_.add_new(socket, value_buffer); + if (!base_type.is_trivially_destructible()) { + scope_.add_destruct_call( + [type = &base_type, value_buffer]() { type->destruct(value_buffer); }); + } + } + + void value_task__input__linked(const SocketInContext &from_socket, + const SocketInContext &to_socket) + { + const std::optional from_value = all_socket_values_.lookup_try(from_socket); + if (!from_value.has_value()) { + this->push_value_task(from_socket); + return; + } + const void *converted_value = this->convert_type_if_necessary( + *from_value, *from_socket.socket, *to_socket.socket); + all_socket_values_.add_new(to_socket, converted_value); + } + + const void *convert_type_if_necessary(const void *src, + const bNodeSocket &from_socket, + const bNodeSocket &to_socket) + { + if (!src) { + return nullptr; + } + const CPPType *from_type = from_socket.typeinfo->base_cpp_type; + const CPPType *to_type = to_socket.typeinfo->base_cpp_type; + if (from_type == to_type) { + return src; + } + if (!to_type) { + return nullptr; + } + const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions(); + if (!conversions.is_convertible(*from_type, *to_type)) { + return nullptr; + } + void *dst = scope_.linear_allocator().allocate(to_type->size(), to_type->alignment()); + conversions.convert_to_uninitialized(*from_type, *to_type, src, dst); + if (!to_type->is_trivially_destructible()) { + scope_.add_destruct_call([to_type, dst]() { to_type->destruct(dst); }); + } + return dst; + } + + static bool switch__is_socket_selected(const SocketInContext &socket, const void *condition) + { + const bool is_true = *static_cast(condition); + const int selected_index = is_true ? 2 : 1; + return socket->index() == selected_index; + } + + static bool index_switch__is_socket_selected(const SocketInContext &socket, + const void *condition) + { + const int index = *static_cast(condition); + return socket->index() == index + 1; + } + + static bool menu_switch__is_socket_selected(const SocketInContext &socket, const void *condition) + { + const NodeMenuSwitch &storage = *static_cast( + socket->owner_node().storage); + const int menu_value = *static_cast(condition); + const NodeEnumItem &item = storage.enum_definition.items_array[socket->index() - 1]; + return menu_value == item.identifier; + } + + void push_usage_task(const SocketInContext &socket) + { + usage_tasks_.push(socket); + } + + void push_value_task(const SocketInContext &socket) + { + value_tasks_.push(socket); + } + + void ensure_animation_data_processed(const bNodeTree &tree) + { + if (!trees_with_handled_animation_data_.add(&tree)) { + return; + } + if (!tree.adt) { + return; + } + + static std::regex pattern(R"#(nodes\["(.*)"\].inputs\[(\d+)\].default_value)#"); + MultiValueMap animated_inputs_by_node_name; + auto handle_rna_path = [&](const char *rna_path) { + std::cmatch match; + if (!std::regex_match(rna_path, match, pattern)) { + return; + } + const StringRef node_name{match[1].first, match[1].second - match[1].first}; + const int socket_index = std::stoi(match[2]); + animated_inputs_by_node_name.add(node_name, socket_index); + }; + + /* Gather all inputs controlled by fcurves. */ + if (tree.adt->action) { + animrig::foreach_fcurve_in_action_slot( + tree.adt->action->wrap(), tree.adt->slot_handle, [&](const FCurve &fcurve) { + handle_rna_path(fcurve.rna_path); + }); + } + /* Gather all inputs controlled by drivers. */ + LISTBASE_FOREACH (const FCurve *, driver, &tree.adt->drivers) { + handle_rna_path(driver->rna_path); + } + + /* Actually find the #bNodeSocket for each controlled input. */ + if (!animated_inputs_by_node_name.is_empty()) { + for (const bNode *node : tree.all_nodes()) { + const Span animated_inputs = animated_inputs_by_node_name.lookup(node->name); + for (const int socket_index : animated_inputs) { + const bNodeSocket &socket = node->input_socket(socket_index); + animated_sockets_.add(&socket); + } + } + } + } +}; + +Array infer_all_input_sockets_usage(const bNodeTree &tree) +{ + tree.ensure_topology_cache(); + const Span all_input_sockets = tree.all_input_sockets(); + Array all_usages(all_input_sockets.size()); + + SocketUsageInferencer inferencer{tree, std::nullopt}; + inferencer.mark_top_level_node_outputs_as_used(); + + for (const int i : all_input_sockets.index_range()) { + const bNodeSocket &socket = *all_input_sockets[i]; + all_usages[i] = inferencer.is_socket_used({nullptr, &socket}); + } + + return all_usages; +} + +void infer_group_interface_inputs_usage(const bNodeTree &group, + const Span group_input_values, + const MutableSpan r_input_usages) +{ + SocketUsageInferencer inferencer{group, group_input_values}; + + r_input_usages.fill(false); + for (const bNode *node : group.group_input_nodes()) { + for (const int i : group.interface_inputs().index_range()) { + const bNodeSocket &socket = node->output_socket(i); + r_input_usages[i] |= inferencer.is_socket_used({nullptr, &socket}); + } + } +} + +void infer_group_interface_inputs_usage(const bNodeTree &group, + Span input_sockets, + MutableSpan r_input_usages) +{ + BLI_assert(group.interface_inputs().size() == input_sockets.size()); + + AlignedBuffer<1024, 8> allocator_buffer; + LinearAllocator<> allocator; + allocator.provide_buffer(allocator_buffer); + + Array input_values(input_sockets.size()); + for (const int i : input_sockets.index_range()) { + const bNodeSocket &socket = *input_sockets[i]; + if (socket.is_directly_linked()) { + continue; + } + + const bke::bNodeSocketType &stype = *socket.typeinfo; + const CPPType *base_type = stype.base_cpp_type; + if (base_type == nullptr) { + continue; + } + void *value = allocator.allocate(base_type->size(), base_type->alignment()); + stype.get_base_cpp_value(socket.default_value, value); + input_values[i] = GPointer(base_type, value); + } + + infer_group_interface_inputs_usage(group, input_values, r_input_usages); + + for (GPointer &value : input_values) { + if (const void *data = value.get()) { + value.type()->destruct(const_cast(data)); + } + } +} + +void infer_group_interface_inputs_usage(const bNodeTree &group, + const IDProperty *properties, + MutableSpan r_input_usages) +{ + const int inputs_num = group.interface_inputs().size(); + Array input_values(inputs_num); + ResourceScope scope; + nodes::get_geometry_nodes_input_base_values(group, properties, scope, input_values); + nodes::socket_usage_inference::infer_group_interface_inputs_usage( + group, input_values, r_input_usages); +} + +} // namespace blender::nodes::socket_usage_inference