diff options
Diffstat (limited to 'source/blender/modifiers/intern/MOD_nodes_evaluator.cc')
-rw-r--r-- | source/blender/modifiers/intern/MOD_nodes_evaluator.cc | 290 |
1 files changed, 141 insertions, 149 deletions
diff --git a/source/blender/modifiers/intern/MOD_nodes_evaluator.cc b/source/blender/modifiers/intern/MOD_nodes_evaluator.cc index 5f82cd9ba27..e8677c7ce1a 100644 --- a/source/blender/modifiers/intern/MOD_nodes_evaluator.cc +++ b/source/blender/modifiers/intern/MOD_nodes_evaluator.cc @@ -264,13 +264,10 @@ struct NodeWithState { class GeometryNodesEvaluator; /** - * Utility class that locks the state of a node. Having this is a separate class is useful because - * it allows methods to communicate that they expect the node to be locked. + * Utility class that wraps a node whose state is locked. Having this is a separate class is useful + * because it allows methods to communicate that they expect the node to be locked. */ class LockedNode : NonCopyable, NonMovable { - private: - GeometryNodesEvaluator &evaluator_; - public: /** * This is the node that is currently locked. @@ -290,13 +287,9 @@ class LockedNode : NonCopyable, NonMovable { Vector<DOutputSocket> delayed_unused_outputs; Vector<DNode> delayed_scheduled_nodes; - LockedNode(GeometryNodesEvaluator &evaluator, const DNode node, NodeState &node_state) - : evaluator_(evaluator), node(node), node_state(node_state) + LockedNode(const DNode node, NodeState &node_state) : node(node), node_state(node_state) { - node_state.mutex.lock(); } - - ~LockedNode(); }; static const CPPType *get_socket_cpp_type(const DSocket socket) @@ -352,7 +345,7 @@ class GeometryNodesEvaluator { * on cache line boundaries. Note, just because a value is allocated in one specific thread, * does not mean that it will only be used by that thread. */ - EnumerableThreadSpecific<LinearAllocator<>> local_allocators_; + threading::EnumerableThreadSpecific<LinearAllocator<>> local_allocators_; /** * Every node that is reachable from the output gets its own state. Once all states have been @@ -380,7 +373,7 @@ class GeometryNodesEvaluator { void execute() { - task_pool_ = BLI_task_pool_create(this, TASK_PRIORITY_HIGH, TASK_ISOLATION_OFF); + task_pool_ = BLI_task_pool_create(this, TASK_PRIORITY_HIGH); this->create_states_for_reachable_nodes(); this->forward_group_inputs(); @@ -426,12 +419,13 @@ class GeometryNodesEvaluator { /* Initialize the more complex parts of the node states in parallel. At this point no new * node states are added anymore, so it is safe to lookup states from `node_states_` from * multiple threads. */ - parallel_for(IndexRange(node_states_.size()), 50, [&, this](const IndexRange range) { - LinearAllocator<> &allocator = this->local_allocators_.local(); - for (const NodeWithState &item : node_states_.as_span().slice(range)) { - this->initialize_node_state(item.node, *item.state, allocator); - } - }); + threading::parallel_for( + IndexRange(node_states_.size()), 50, [&, this](const IndexRange range) { + LinearAllocator<> &allocator = this->local_allocators_.local(); + for (const NodeWithState &item : node_states_.as_span().slice(range)) { + this->initialize_node_state(item.node, *item.state, allocator); + } + }); } void initialize_node_state(const DNode node, NodeState &node_state, LinearAllocator<> &allocator) @@ -507,11 +501,12 @@ class GeometryNodesEvaluator { void destruct_node_states() { - parallel_for(IndexRange(node_states_.size()), 50, [&, this](const IndexRange range) { - for (const NodeWithState &item : node_states_.as_span().slice(range)) { - this->destruct_node_state(item.node, *item.state); - } - }); + threading::parallel_for( + IndexRange(node_states_.size()), 50, [&, this](const IndexRange range) { + for (const NodeWithState &item : node_states_.as_span().slice(range)) { + this->destruct_node_state(item.node, *item.state); + } + }); } void destruct_node_state(const DNode node, NodeState &node_state) @@ -568,9 +563,10 @@ class GeometryNodesEvaluator { for (const DInputSocket &socket : params_.output_sockets) { const DNode node = socket.node(); NodeState &node_state = this->get_node_state(node); - LockedNode locked_node{*this, node, node_state}; - /* Setting an input as required will schedule any linked node. */ - this->set_input_required(locked_node, socket); + this->with_locked_node(node, node_state, [&](LockedNode &locked_node) { + /* Setting an input as required will schedule any linked node. */ + this->set_input_required(locked_node, socket); + }); } } @@ -632,30 +628,33 @@ class GeometryNodesEvaluator { bool node_task_preprocessing(const DNode node, NodeState &node_state) { - LockedNode locked_node{*this, node, node_state}; - BLI_assert(node_state.schedule_state == NodeScheduleState::Scheduled); - node_state.schedule_state = NodeScheduleState::Running; + bool do_execute_node = false; + this->with_locked_node(node, node_state, [&](LockedNode &locked_node) { + BLI_assert(node_state.schedule_state == NodeScheduleState::Scheduled); + node_state.schedule_state = NodeScheduleState::Running; - /* Early return if the node has finished already. */ - if (locked_node.node_state.node_has_finished) { - return false; - } - /* Prepare outputs and check if actually any new outputs have to be computed. */ - if (!this->prepare_node_outputs_for_execution(locked_node)) { - return false; - } - /* Initialize nodes that don't support laziness. This is done after at least one output is - * required and before we check that all required inputs are provided. This reduces the - * number of "round-trips" through the task pool by one for most nodes. */ - if (!node_state.non_lazy_node_is_initialized && !node_supports_laziness(node)) { - this->initialize_non_lazy_node(locked_node); - node_state.non_lazy_node_is_initialized = true; - } - /* Prepare inputs and check if all required inputs are provided. */ - if (!this->prepare_node_inputs_for_execution(locked_node)) { - return false; - } - return true; + /* Early return if the node has finished already. */ + if (locked_node.node_state.node_has_finished) { + return; + } + /* Prepare outputs and check if actually any new outputs have to be computed. */ + if (!this->prepare_node_outputs_for_execution(locked_node)) { + return; + } + /* Initialize nodes that don't support laziness. This is done after at least one output is + * required and before we check that all required inputs are provided. This reduces the + * number of "round-trips" through the task pool by one for most nodes. */ + if (!node_state.non_lazy_node_is_initialized && !node_supports_laziness(node)) { + this->initialize_non_lazy_node(locked_node); + node_state.non_lazy_node_is_initialized = true; + } + /* Prepare inputs and check if all required inputs are provided. */ + if (!this->prepare_node_inputs_for_execution(locked_node)) { + return; + } + do_execute_node = true; + }); + return do_execute_node; } /* A node is finished when it has computed all outputs that may be used. */ @@ -896,18 +895,18 @@ class GeometryNodesEvaluator { void node_task_postprocessing(const DNode node, NodeState &node_state) { - LockedNode locked_node{*this, node, node_state}; - - const bool node_has_finished = this->finish_node_if_possible(locked_node); - const bool reschedule_requested = node_state.schedule_state == - NodeScheduleState::RunningAndRescheduled; - node_state.schedule_state = NodeScheduleState::NotScheduled; - if (reschedule_requested && !node_has_finished) { - /* Either the node rescheduled itself or another node tried to schedule it while it ran. */ - this->schedule_node(locked_node); - } + this->with_locked_node(node, node_state, [&](LockedNode &locked_node) { + const bool node_has_finished = this->finish_node_if_possible(locked_node); + const bool reschedule_requested = node_state.schedule_state == + NodeScheduleState::RunningAndRescheduled; + node_state.schedule_state = NodeScheduleState::NotScheduled; + if (reschedule_requested && !node_has_finished) { + /* Either the node rescheduled itself or another node tried to schedule it while it ran. */ + this->schedule_node(locked_node); + } - this->assert_expected_outputs_have_been_computed(locked_node); + this->assert_expected_outputs_have_been_computed(locked_node); + }); } void assert_expected_outputs_have_been_computed(LockedNode &locked_node) @@ -1042,13 +1041,14 @@ class GeometryNodesEvaluator { this->load_unlinked_input_value(locked_node, input_socket, input_state, origin_socket); locked_node.node_state.missing_required_inputs -= 1; this->schedule_node(locked_node); - return; } - /* The value has not been computed yet, so when it will be forwarded by another node, this - * node will be triggered. */ - will_be_triggered_by_other_node = true; + else { + /* The value has not been computed yet, so when it will be forwarded by another node, this + * node will be triggered. */ + will_be_triggered_by_other_node = true; - locked_node.delayed_required_outputs.append(DOutputSocket(origin_socket)); + locked_node.delayed_required_outputs.append(DOutputSocket(origin_socket)); + } } /* If this node will be triggered by another node, we don't have to schedule it now. */ if (!will_be_triggered_by_other_node) { @@ -1095,15 +1095,16 @@ class GeometryNodesEvaluator { NodeState &node_state = this->get_node_state(node); OutputState &output_state = node_state.outputs[socket->index()]; - LockedNode locked_node{*this, node, node_state}; - if (output_state.output_usage == ValueUsage::Required) { - /* Output is marked as required already. So the node is scheduled already. */ - return; - } - /* The origin node needs to be scheduled so that it provides the requested input - * eventually. */ - output_state.output_usage = ValueUsage::Required; - this->schedule_node(locked_node); + this->with_locked_node(node, node_state, [&](LockedNode &locked_node) { + if (output_state.output_usage == ValueUsage::Required) { + /* Output is marked as required already. So the node is scheduled already. */ + return; + } + /* The origin node needs to be scheduled so that it provides the requested input + * eventually. */ + output_state.output_usage = ValueUsage::Required; + this->schedule_node(locked_node); + }); } void send_output_unused_notification(const DOutputSocket socket) @@ -1112,14 +1113,15 @@ class GeometryNodesEvaluator { NodeState &node_state = this->get_node_state(node); OutputState &output_state = node_state.outputs[socket->index()]; - LockedNode locked_node{*this, node, node_state}; - output_state.potential_users -= 1; - if (output_state.potential_users == 0) { - /* The output socket has no users anymore. */ - output_state.output_usage = ValueUsage::Unused; - /* Schedule the origin node in case it wants to set its inputs as unused as well. */ - this->schedule_node(locked_node); - } + this->with_locked_node(node, node_state, [&](LockedNode &locked_node) { + output_state.potential_users -= 1; + if (output_state.potential_users == 0) { + /* The output socket has no users anymore. */ + output_state.output_usage = ValueUsage::Unused; + /* Schedule the origin node in case it wants to set its inputs as unused as well. */ + this->schedule_node(locked_node); + } + }); } void add_node_to_task_pool(const DNode node) @@ -1247,28 +1249,27 @@ class GeometryNodesEvaluator { NodeState &node_state = this->get_node_state(node); InputState &input_state = node_state.inputs[socket->index()]; - /* Lock the node because we want to change its state. */ - LockedNode locked_node{*this, node, node_state}; - - if (socket->is_multi_input_socket()) { - /* Add a new value to the multi-input. */ - MultiInputValue &multi_value = *input_state.value.multi; - multi_value.items.append({origin, value.get()}); - } - else { - /* Assign the value to the input. */ - SingleInputValue &single_value = *input_state.value.single; - BLI_assert(single_value.value == nullptr); - single_value.value = value.get(); - } + this->with_locked_node(node, node_state, [&](LockedNode &locked_node) { + if (socket->is_multi_input_socket()) { + /* Add a new value to the multi-input. */ + MultiInputValue &multi_value = *input_state.value.multi; + multi_value.items.append({origin, value.get()}); + } + else { + /* Assign the value to the input. */ + SingleInputValue &single_value = *input_state.value.single; + BLI_assert(single_value.value == nullptr); + single_value.value = value.get(); + } - if (input_state.usage == ValueUsage::Required) { - node_state.missing_required_inputs--; - if (node_state.missing_required_inputs == 0) { - /* Schedule node if all the required inputs have been provided. */ - this->schedule_node(locked_node); + if (input_state.usage == ValueUsage::Required) { + node_state.missing_required_inputs--; + if (node_state.missing_required_inputs == 0) { + /* Schedule node if all the required inputs have been provided. */ + this->schedule_node(locked_node); + } } - } + }); } void load_unlinked_input_value(LockedNode &locked_node, @@ -1363,44 +1364,33 @@ class GeometryNodesEvaluator { { this->log_socket_value(socket, Span<GPointer>(&value, 1)); } -}; -LockedNode::~LockedNode() -{ - /* First unlock the current node. */ - node_state.mutex.unlock(); - /* Then send notifications to the other nodes. */ - for (const DOutputSocket &socket : delayed_required_outputs) { - evaluator_.send_output_required_notification(socket); - } - for (const DOutputSocket &socket : delayed_unused_outputs) { - evaluator_.send_output_unused_notification(socket); - } - for (const DNode &node : delayed_scheduled_nodes) { - evaluator_.add_node_to_task_pool(node); - } -} + /* In most cases when `NodeState` is accessed, the node has to be locked first to avoid race + * conditions. */ + template<typename Function> + void with_locked_node(const DNode node, NodeState &node_state, const Function &function) + { + LockedNode locked_node{node, node_state}; -/* TODO: Use a map data structure or so to make this faster. */ -static DInputSocket get_input_by_identifier(const DNode node, const StringRef identifier) -{ - for (const InputSocketRef *socket : node->inputs()) { - if (socket->identifier() == identifier) { - return {node.context(), socket}; - } - } - return {}; -} + node_state.mutex.lock(); + /* Isolate this thread because we don't want it to start executing another node. This other + * node might want to lock the same mutex leading to a deadlock. */ + threading::isolate_task([&] { function(locked_node); }); + node_state.mutex.unlock(); -static DOutputSocket get_output_by_identifier(const DNode node, const StringRef identifier) -{ - for (const OutputSocketRef *socket : node->outputs()) { - if (socket->identifier() == identifier) { - return {node.context(), socket}; + /* Then send notifications to the other nodes after the node state is unlocked. This avoids + * locking two nodes at the same time on this thread and helps to prevent deadlocks. */ + for (const DOutputSocket &socket : locked_node.delayed_required_outputs) { + this->send_output_required_notification(socket); + } + for (const DOutputSocket &socket : locked_node.delayed_unused_outputs) { + this->send_output_unused_notification(socket); + } + for (const DNode &node : locked_node.delayed_scheduled_nodes) { + this->add_node_to_task_pool(node); } } - return {}; -} +}; NodeParamsProvider::NodeParamsProvider(GeometryNodesEvaluator &evaluator, DNode dnode, @@ -1415,7 +1405,7 @@ NodeParamsProvider::NodeParamsProvider(GeometryNodesEvaluator &evaluator, bool NodeParamsProvider::can_get_input(StringRef identifier) const { - const DInputSocket socket = get_input_by_identifier(this->dnode, identifier); + const DInputSocket socket = this->dnode.input_by_identifier(identifier); BLI_assert(socket); InputState &input_state = node_state_.inputs[socket->index()]; @@ -1433,7 +1423,7 @@ bool NodeParamsProvider::can_get_input(StringRef identifier) const bool NodeParamsProvider::can_set_output(StringRef identifier) const { - const DOutputSocket socket = get_output_by_identifier(this->dnode, identifier); + const DOutputSocket socket = this->dnode.output_by_identifier(identifier); BLI_assert(socket); OutputState &output_state = node_state_.outputs[socket->index()]; @@ -1442,7 +1432,7 @@ bool NodeParamsProvider::can_set_output(StringRef identifier) const GMutablePointer NodeParamsProvider::extract_input(StringRef identifier) { - const DInputSocket socket = get_input_by_identifier(this->dnode, identifier); + const DInputSocket socket = this->dnode.input_by_identifier(identifier); BLI_assert(socket); BLI_assert(!socket->is_multi_input_socket()); BLI_assert(this->can_get_input(identifier)); @@ -1456,7 +1446,7 @@ GMutablePointer NodeParamsProvider::extract_input(StringRef identifier) Vector<GMutablePointer> NodeParamsProvider::extract_multi_input(StringRef identifier) { - const DInputSocket socket = get_input_by_identifier(this->dnode, identifier); + const DInputSocket socket = this->dnode.input_by_identifier(identifier); BLI_assert(socket); BLI_assert(socket->is_multi_input_socket()); BLI_assert(this->can_get_input(identifier)); @@ -1487,7 +1477,7 @@ Vector<GMutablePointer> NodeParamsProvider::extract_multi_input(StringRef identi GPointer NodeParamsProvider::get_input(StringRef identifier) const { - const DInputSocket socket = get_input_by_identifier(this->dnode, identifier); + const DInputSocket socket = this->dnode.input_by_identifier(identifier); BLI_assert(socket); BLI_assert(!socket->is_multi_input_socket()); BLI_assert(this->can_get_input(identifier)); @@ -1505,7 +1495,7 @@ GMutablePointer NodeParamsProvider::alloc_output_value(const CPPType &type) void NodeParamsProvider::set_output(StringRef identifier, GMutablePointer value) { - const DOutputSocket socket = get_output_by_identifier(this->dnode, identifier); + const DOutputSocket socket = this->dnode.output_by_identifier(identifier); BLI_assert(socket); evaluator_.log_socket_value(socket, value); @@ -1519,30 +1509,32 @@ void NodeParamsProvider::set_output(StringRef identifier, GMutablePointer value) bool NodeParamsProvider::lazy_require_input(StringRef identifier) { BLI_assert(node_supports_laziness(this->dnode)); - const DInputSocket socket = get_input_by_identifier(this->dnode, identifier); + const DInputSocket socket = this->dnode.input_by_identifier(identifier); BLI_assert(socket); InputState &input_state = node_state_.inputs[socket->index()]; if (input_state.was_ready_for_execution) { return false; } - LockedNode locked_node{evaluator_, this->dnode, node_state_}; - evaluator_.set_input_required(locked_node, socket); + evaluator_.with_locked_node(this->dnode, node_state_, [&](LockedNode &locked_node) { + evaluator_.set_input_required(locked_node, socket); + }); return true; } void NodeParamsProvider::set_input_unused(StringRef identifier) { - const DInputSocket socket = get_input_by_identifier(this->dnode, identifier); + const DInputSocket socket = this->dnode.input_by_identifier(identifier); BLI_assert(socket); - LockedNode locked_node{evaluator_, this->dnode, node_state_}; - evaluator_.set_input_unused(locked_node, socket); + evaluator_.with_locked_node(this->dnode, node_state_, [&](LockedNode &locked_node) { + evaluator_.set_input_unused(locked_node, socket); + }); } bool NodeParamsProvider::output_is_required(StringRef identifier) const { - const DOutputSocket socket = get_output_by_identifier(this->dnode, identifier); + const DOutputSocket socket = this->dnode.output_by_identifier(identifier); BLI_assert(socket); OutputState &output_state = node_state_.outputs[socket->index()]; @@ -1555,7 +1547,7 @@ bool NodeParamsProvider::output_is_required(StringRef identifier) const bool NodeParamsProvider::lazy_output_is_required(StringRef identifier) const { BLI_assert(node_supports_laziness(this->dnode)); - const DOutputSocket socket = get_output_by_identifier(this->dnode, identifier); + const DOutputSocket socket = this->dnode.output_by_identifier(identifier); BLI_assert(socket); OutputState &output_state = node_state_.outputs[socket->index()]; |