diff options
9 files changed, 226 insertions, 27 deletions
diff --git a/source/blender/blenlib/BLI_resource_scope.hh b/source/blender/blenlib/BLI_resource_scope.hh index 6a98c2dcc1c..c3c0af4af50 100644 --- a/source/blender/blenlib/BLI_resource_scope.hh +++ b/source/blender/blenlib/BLI_resource_scope.hh @@ -137,6 +137,17 @@ class ResourceScope : NonCopyable, NonMovable { } /** + * The passed in function will be called when the scope is destructed. + */ + template<typename Func> void add_destruct_call(Func func, const char *name) + { + void *buffer = m_allocator.allocate(sizeof(func), alignof(func)); + new (buffer) Func(std::move(func)); + this->add( + buffer, [](void *data) { (*(Func *)data)(); }, name); + } + + /** * Returns a reference to a linear allocator that is owned by the ResourcesCollector. Memory * allocated through this allocator will be freed when the collector is destructed. */ diff --git a/source/blender/functions/FN_multi_function.hh b/source/blender/functions/FN_multi_function.hh index f6c4addfb52..98788025558 100644 --- a/source/blender/functions/FN_multi_function.hh +++ b/source/blender/functions/FN_multi_function.hh @@ -121,8 +121,13 @@ class MultiFunction { } }; -inline MFParamsBuilder::MFParamsBuilder(const class MultiFunction &fn, int64_t min_array_size) - : MFParamsBuilder(fn.signature(), min_array_size) +inline MFParamsBuilder::MFParamsBuilder(const MultiFunction &fn, int64_t mask_size) + : MFParamsBuilder(fn.signature(), IndexMask(mask_size)) +{ +} + +inline MFParamsBuilder::MFParamsBuilder(const MultiFunction &fn, const IndexMask *mask) + : MFParamsBuilder(fn.signature(), *mask) { } diff --git a/source/blender/functions/FN_multi_function_params.hh b/source/blender/functions/FN_multi_function_params.hh index 5af86c7c284..cae105e7c19 100644 --- a/source/blender/functions/FN_multi_function_params.hh +++ b/source/blender/functions/FN_multi_function_params.hh @@ -38,6 +38,7 @@ class MFParamsBuilder { private: ResourceScope scope_; const MFSignature *signature_; + IndexMask mask_; int64_t min_array_size_; Vector<const GVArray *> virtual_arrays_; Vector<GMutableSpan> mutable_spans_; @@ -46,13 +47,18 @@ class MFParamsBuilder { friend class MFParams; - public: - MFParamsBuilder(const MFSignature &signature, int64_t min_array_size) - : signature_(&signature), min_array_size_(min_array_size) + MFParamsBuilder(const MFSignature &signature, const IndexMask mask) + : signature_(&signature), mask_(mask), min_array_size_(mask.min_array_size()) { } - MFParamsBuilder(const class MultiFunction &fn, int64_t min_array_size); + public: + MFParamsBuilder(const class MultiFunction &fn, int64_t size); + /** + * The indices referenced by the #mask has to live longer than the params builder. This is + * because the it might have to destruct elements for all masked indices in the end. + */ + MFParamsBuilder(const class MultiFunction &fn, const IndexMask *mask); template<typename T> void add_readonly_single_input_value(T value, StringRef expected_name = "") { @@ -112,6 +118,17 @@ class MFParamsBuilder { BLI_assert(ref.size() >= min_array_size_); mutable_spans_.append(ref); } + void add_ignored_single_output(StringRef expected_name = "") + { + this->assert_current_param_name(expected_name); + const int param_index = this->current_param_index(); + const MFParamType ¶m_type = signature_->param_types[param_index]; + BLI_assert(param_type.category() == MFParamType::SingleOutput); + const CPPType &type = param_type.data_type().single_type(); + /* An empty span indicates that this is ignored. */ + const GMutableSpan dummy_span{type}; + mutable_spans_.append(dummy_span); + } void add_vector_output(GVectorArray &vector_array, StringRef expected_name = "") { @@ -176,6 +193,19 @@ class MFParamsBuilder { #endif } + void assert_current_param_name(StringRef expected_name) + { + UNUSED_VARS_NDEBUG(expected_name); +#ifdef DEBUG + if (expected_name.is_empty()) { + return; + } + const int param_index = this->current_param_index(); + StringRef actual_name = signature_->param_names[param_index]; + BLI_assert(actual_name == expected_name); +#endif + } + int current_param_index() const { return virtual_arrays_.size() + mutable_spans_.size() + virtual_vector_arrays_.size() + @@ -204,6 +234,19 @@ class MFParams { return *builder_->virtual_arrays_[data_index]; } + /** + * \return True when the caller provided a buffer for this output parameter. This allows the + * called multi-function to skip some computation. It is still valid to call + * #uninitialized_single_output when this returns false. In this case a new temporary buffer is + * allocated. + */ + bool single_output_is_required(int param_index, StringRef name = "") + { + this->assert_correct_param(param_index, name, MFParamType::SingleOutput); + int data_index = builder_->signature_->data_index(param_index); + return !builder_->mutable_spans_[data_index].is_empty(); + } + template<typename T> MutableSpan<T> uninitialized_single_output(int param_index, StringRef name = "") { @@ -213,7 +256,22 @@ class MFParams { { this->assert_correct_param(param_index, name, MFParamType::SingleOutput); int data_index = builder_->signature_->data_index(param_index); - return builder_->mutable_spans_[data_index]; + GMutableSpan span = builder_->mutable_spans_[data_index]; + if (span.is_empty()) { + /* The output is ignored by the caller, but the multi-function does not handle this case. So + * create a temporary buffer that the multi-function can write to. */ + const CPPType &type = span.type(); + void *buffer = builder_->scope_.linear_allocator().allocate( + builder_->min_array_size_ * type.size(), type.alignment()); + if (!type.is_trivially_destructible()) { + /* Make sure the temporary elements will be destructed in the end. */ + builder_->scope_.add_destruct_call( + [&type, buffer, mask = builder_->mask_]() { type.destruct_indices(buffer, mask); }, + __func__); + } + span = GMutableSpan{type, buffer, builder_->min_array_size_}; + } + return span; } template<typename T> diff --git a/source/blender/functions/intern/field.cc b/source/blender/functions/intern/field.cc index 6133658d8e3..574a9e6284f 100644 --- a/source/blender/functions/intern/field.cc +++ b/source/blender/functions/intern/field.cc @@ -189,17 +189,43 @@ static void build_multi_function_procedure_for_fields(MFProcedure &procedure, field_with_index.current_input_index++; } else { - /* All inputs variables are ready, now add the function call. */ - Vector<MFVariable *> input_variables; - for (const GField &field : operation_inputs) { - input_variables.append(variable_by_field.lookup(field)); - } + /* All inputs variables are ready, now gather all variables that are used by the function + * and call it. */ const MultiFunction &multi_function = operation.multi_function(); - Vector<MFVariable *> output_variables = builder.add_call(multi_function, input_variables); - /* Add newly created variables to the map. */ - for (const int i : output_variables.index_range()) { - variable_by_field.add_new({operation, i}, output_variables[i]); + Vector<MFVariable *> variables(multi_function.param_amount()); + + int param_input_index = 0; + int param_output_index = 0; + for (const int param_index : multi_function.param_indices()) { + const MFParamType param_type = multi_function.param_type(param_index); + const MFParamType::InterfaceType interface_type = param_type.interface_type(); + if (interface_type == MFParamType::Input) { + const GField &input_field = operation_inputs[param_input_index]; + variables[param_index] = variable_by_field.lookup(input_field); + param_input_index++; + } + else if (interface_type == MFParamType::Output) { + const GFieldRef output_field{operation, param_output_index}; + const bool output_is_ignored = + field_tree_info.field_users.lookup(output_field).is_empty() && + !output_fields.contains(output_field); + if (output_is_ignored) { + /* Ignored outputs don't need a variable. */ + variables[param_index] = nullptr; + } + else { + /* Create a new variable for used outputs. */ + MFVariable &new_variable = procedure.new_variable(param_type.data_type()); + variables[param_index] = &new_variable; + variable_by_field.add_new(output_field, &new_variable); + } + param_output_index++; + } + else { + BLI_assert_unreachable(); + } } + builder.add_call_with_all_variables(multi_function, variables); } } } @@ -334,7 +360,7 @@ Vector<const GVArray *> evaluate_fields(ResourceScope &scope, build_multi_function_procedure_for_fields( procedure, scope, field_tree_info, varying_fields_to_evaluate); MFProcedureExecutor procedure_executor{"Procedure", procedure}; - MFParamsBuilder mf_params{procedure_executor, array_size}; + MFParamsBuilder mf_params{procedure_executor, &mask}; MFContextBuilder mf_context; /* Provide inputs to the procedure executor. */ diff --git a/source/blender/functions/intern/multi_function_procedure.cc b/source/blender/functions/intern/multi_function_procedure.cc index 2aa760a494f..fa95e8de71e 100644 --- a/source/blender/functions/intern/multi_function_procedure.cc +++ b/source/blender/functions/intern/multi_function_procedure.cc @@ -325,7 +325,14 @@ bool MFProcedure::validate_all_instruction_pointers_set() const bool MFProcedure::validate_all_params_provided() const { for (const MFCallInstruction *instruction : call_instructions_) { - for (const MFVariable *variable : instruction->params_) { + const MultiFunction &fn = instruction->fn(); + for (const int param_index : fn.param_indices()) { + const MFParamType param_type = fn.param_type(param_index); + if (param_type.category() == MFParamType::SingleOutput) { + /* Single outputs are optional. */ + continue; + } + const MFVariable *variable = instruction->params_[param_index]; if (variable == nullptr) { return false; } @@ -351,6 +358,9 @@ bool MFProcedure::validate_same_variables_in_one_call() const for (const int param_index : fn.param_indices()) { const MFParamType param_type = fn.param_type(param_index); const MFVariable *variable = instruction->params_[param_index]; + if (variable == nullptr) { + continue; + } for (const int other_param_index : fn.param_indices()) { if (other_param_index == param_index) { continue; @@ -681,7 +691,9 @@ class MFProcedureDotExport { if (instruction.prev().size() != 1) { return true; } - if (instruction.prev()[0].type() == MFInstructionCursor::Type::Branch) { + if (ELEM(instruction.prev()[0].type(), + MFInstructionCursor::Type::Branch, + MFInstructionCursor::Type::Entry)) { return true; } return false; diff --git a/source/blender/functions/intern/multi_function_procedure_executor.cc b/source/blender/functions/intern/multi_function_procedure_executor.cc index 38b26415779..b97282accdd 100644 --- a/source/blender/functions/intern/multi_function_procedure_executor.cc +++ b/source/blender/functions/intern/multi_function_procedure_executor.cc @@ -978,7 +978,7 @@ static bool evaluate_as_one(const MultiFunction &fn, return false; } for (VariableState *state : param_variable_states) { - if (!state->is_one()) { + if (state != nullptr && !state->is_one()) { return false; } } @@ -997,8 +997,13 @@ static void execute_call_instruction(const MFCallInstruction &instruction, for (const int param_index : fn.param_indices()) { const MFVariable *variable = instruction.params()[param_index]; - VariableState &variable_state = variable_states.get_variable_state(*variable); - param_variable_states[param_index] = &variable_state; + if (variable == nullptr) { + param_variable_states[param_index] = nullptr; + } + else { + VariableState &variable_state = variable_states.get_variable_state(*variable); + param_variable_states[param_index] = &variable_state; + } } /* If all inputs to the function are constant, it's enough to call the function only once instead @@ -1008,19 +1013,29 @@ static void execute_call_instruction(const MFCallInstruction &instruction, for (const int param_index : fn.param_indices()) { const MFParamType param_type = fn.param_type(param_index); - VariableState &variable_state = *param_variable_states[param_index]; - variable_states.add_as_param__one(variable_state, params, param_type, mask); + VariableState *variable_state = param_variable_states[param_index]; + if (variable_state == nullptr) { + params.add_ignored_single_output(); + } + else { + variable_states.add_as_param__one(*variable_state, params, param_type, mask); + } } fn.call(IndexRange(1), params, context); } else { - MFParamsBuilder params(fn, mask.min_array_size()); + MFParamsBuilder params(fn, &mask); for (const int param_index : fn.param_indices()) { const MFParamType param_type = fn.param_type(param_index); - VariableState &variable_state = *param_variable_states[param_index]; - variable_states.add_as_param(variable_state, params, param_type, mask); + VariableState *variable_state = param_variable_states[param_index]; + if (variable_state == nullptr) { + params.add_ignored_single_output(); + } + else { + variable_states.add_as_param(*variable_state, params, param_type, mask); + } } fn.call(mask, params, context); diff --git a/source/blender/functions/tests/FN_field_test.cc b/source/blender/functions/tests/FN_field_test.cc index 212b79e75d3..1c2d5c8eaad 100644 --- a/source/blender/functions/tests/FN_field_test.cc +++ b/source/blender/functions/tests/FN_field_test.cc @@ -5,6 +5,7 @@ #include "FN_cpp_type.hh" #include "FN_field.hh" #include "FN_multi_function_builder.hh" +#include "FN_multi_function_test_common.hh" namespace blender::fn::tests { @@ -275,4 +276,19 @@ TEST(field, SameFieldTwice) EXPECT_EQ(varray2->get(1), 10); } +TEST(field, IgnoredOutput) +{ + static OptionalOutputsFunction fn; + Field<int> field{std::make_shared<FieldOperation>(fn), 0}; + + FieldContext field_context; + FieldEvaluator field_evaluator{field_context, 10}; + const VArray<int> *results = nullptr; + field_evaluator.add(field, &results); + field_evaluator.evaluate(); + + EXPECT_EQ(results->get(0), 5); + EXPECT_EQ(results->get(3), 5); +} + } // namespace blender::fn::tests diff --git a/source/blender/functions/tests/FN_multi_function_test.cc b/source/blender/functions/tests/FN_multi_function_test.cc index 204dbfab6aa..d99993b35ac 100644 --- a/source/blender/functions/tests/FN_multi_function_test.cc +++ b/source/blender/functions/tests/FN_multi_function_test.cc @@ -328,5 +328,32 @@ TEST(multi_function, CustomMF_Convert) EXPECT_EQ(outputs[2], 9); } +TEST(multi_function, IgnoredOutputs) +{ + OptionalOutputsFunction fn; + { + MFParamsBuilder params(fn, 10); + params.add_ignored_single_output("Out 1"); + params.add_ignored_single_output("Out 2"); + MFContextBuilder context; + fn.call(IndexRange(10), params, context); + } + { + Array<int> results_1(10); + Array<std::string> results_2(10, NoInitialization()); + + MFParamsBuilder params(fn, 10); + params.add_uninitialized_single_output(results_1.as_mutable_span(), "Out 1"); + params.add_uninitialized_single_output(results_2.as_mutable_span(), "Out 2"); + MFContextBuilder context; + fn.call(IndexRange(10), params, context); + + EXPECT_EQ(results_1[0], 5); + EXPECT_EQ(results_1[3], 5); + EXPECT_EQ(results_1[9], 5); + EXPECT_EQ(results_2[0], "hello, this is a long string"); + } +} + } // namespace } // namespace blender::fn::tests diff --git a/source/blender/functions/tests/FN_multi_function_test_common.hh b/source/blender/functions/tests/FN_multi_function_test_common.hh index 51c8fac8a96..2a64cc302fe 100644 --- a/source/blender/functions/tests/FN_multi_function_test_common.hh +++ b/source/blender/functions/tests/FN_multi_function_test_common.hh @@ -171,4 +171,33 @@ class SumVectorFunction : public MultiFunction { } }; +class OptionalOutputsFunction : public MultiFunction { + public: + OptionalOutputsFunction() + { + static MFSignature signature = create_signature(); + this->set_signature(&signature); + } + + static MFSignature create_signature() + { + MFSignatureBuilder signature{"Optional Outputs"}; + signature.single_output<int>("Out 1"); + signature.single_output<std::string>("Out 2"); + return signature.build(); + } + + void call(IndexMask mask, MFParams params, MFContext UNUSED(context)) const override + { + if (params.single_output_is_required(0, "Out 1")) { + MutableSpan<int> values = params.uninitialized_single_output<int>(0, "Out 1"); + values.fill_indices(mask, 5); + } + MutableSpan<std::string> values = params.uninitialized_single_output<std::string>(1, "Out 2"); + for (const int i : mask) { + new (&values[i]) std::string("hello, this is a long string"); + } + } +}; + } // namespace blender::fn::tests |