Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJacques Lucke <jacques@blender.org>2021-09-14 15:52:44 +0300
committerJacques Lucke <jacques@blender.org>2021-09-14 15:52:44 +0300
commitfd60f6713a9d9e6f7d706b53bf1311f2f1cd9031 (patch)
treebb762d4ce5e5ad76a52d594249e8c6ec33b08312
parent90a48fa06414ccf5fc5dd6092917413180ff30d1 (diff)
Functions: support optional outputs in multi-function
Sometimes not all outputs of a multi-function are required by the caller. In those cases it would be a waste of compute resources to calculate the unused values anyway. Now, the caller of a multi-function can specify when a specific output is not used. The called function can check if an output is unused and may ignore it. Multi-functions can still computed unused outputs as before if they don't want to check if a specific output is unused. The multi-function procedure system has been updated to support ignored outputs in call instructions. An ignored output just has no variable assigned to it. The field system has been updated to generate a multi-function procedure where unused outputs are ignored.
-rw-r--r--source/blender/blenlib/BLI_resource_scope.hh11
-rw-r--r--source/blender/functions/FN_multi_function.hh9
-rw-r--r--source/blender/functions/FN_multi_function_params.hh68
-rw-r--r--source/blender/functions/intern/field.cc46
-rw-r--r--source/blender/functions/intern/multi_function_procedure.cc16
-rw-r--r--source/blender/functions/intern/multi_function_procedure_executor.cc31
-rw-r--r--source/blender/functions/tests/FN_field_test.cc16
-rw-r--r--source/blender/functions/tests/FN_multi_function_test.cc27
-rw-r--r--source/blender/functions/tests/FN_multi_function_test_common.hh29
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 &param_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