diff options
-rw-r--r-- | source/blender/functions/CMakeLists.txt | 3 | ||||
-rw-r--r-- | source/blender/functions/FN_field.hh | 136 | ||||
-rw-r--r-- | source/blender/functions/intern/field.cc | 115 | ||||
-rw-r--r-- | source/blender/functions/tests/FN_field_test.cc | 73 |
4 files changed, 327 insertions, 0 deletions
diff --git a/source/blender/functions/CMakeLists.txt b/source/blender/functions/CMakeLists.txt index a44086cfec1..5941034de78 100644 --- a/source/blender/functions/CMakeLists.txt +++ b/source/blender/functions/CMakeLists.txt @@ -28,6 +28,7 @@ set(INC_SYS set(SRC intern/cpp_types.cc + intern/field.cc intern/generic_vector_array.cc intern/generic_virtual_array.cc intern/generic_virtual_vector_array.cc @@ -39,6 +40,7 @@ set(SRC FN_cpp_type.hh FN_cpp_type_make.hh + FN_field.hh FN_generic_pointer.hh FN_generic_span.hh FN_generic_value_map.hh @@ -66,6 +68,7 @@ blender_add_lib(bf_functions "${SRC}" "${INC}" "${INC_SYS}" "${LIB}") if(WITH_GTESTS) set(TEST_SRC tests/FN_cpp_type_test.cc + tests/FN_field_test.cc tests/FN_generic_span_test.cc tests/FN_generic_vector_array_test.cc tests/FN_multi_function_procedure_test.cc diff --git a/source/blender/functions/FN_field.hh b/source/blender/functions/FN_field.hh new file mode 100644 index 00000000000..e43a6769ee5 --- /dev/null +++ b/source/blender/functions/FN_field.hh @@ -0,0 +1,136 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#pragma once + +/** \file + * \ingroup fn + * + * Field serve as an intermediate representation for a calculation of a group of functions. Having + * an intermediate representation is helpful mainly to separate the execution system from the + * system that describes the necessary computations. Fields can be executed in different contexts, + * and optimization might mean executing the fields differently based on some factors like the + * number of elements. + * + * For now, fields are very tied to the multi-function system, but in the future the #FieldFunction + * class could be extended to use different descriptions of its outputs and computation besides + * the embedded multi-function. + */ + +#include "BLI_vector.hh" + +#include "FN_multi_function_procedure.hh" +#include "FN_multi_function_procedure_builder.hh" +#include "FN_multi_function_procedure_executor.hh" + +namespace blender::fn { + +class Field; + +/** + * An operation acting on data described by fields. Generally corresponds + * to a node or a subset of a node in a node graph. + */ +class FieldFunction { + /** + * The function used to calculate the + */ + std::unique_ptr<MultiFunction> function_; + + /** + * References to descriptions of the results from the functions this function depends on. + */ + blender::Vector<Field *> inputs_; + + public: + FieldFunction(std::unique_ptr<MultiFunction> function, Span<Field *> inputs) + : function_(std::move(function)), inputs_(inputs) + { + } + + Span<Field *> inputs() const + { + return inputs_; + } + + const MultiFunction &multi_function() const + { + return *function_; + } +}; + +/** + * Descibes the output of a function. Generally corresponds to the combination of an output socket + * and link combination in a node graph. + */ +class Field { + /** + * The type of this field's result. + */ + const fn::CPPType *type_; + + /** + * The function that calculates this field's values. Many fields can share the same function, + * since a function can have many outputs, just like a node graph, where a single output can be + * used as multiple inputs. This avoids calling the same function many times, only using one of + * its results. + */ + const FieldFunction *function_; + /** + * Which output of the function this field corresponds to. + */ + int output_index_; + + std::string debug_name_ = ""; + + public: + Field(const fn::CPPType &type, const FieldFunction &function, const int output_index) + : type_(&type), function_(&function), output_index_(output_index) + { + } + + const fn::CPPType &type() const + { + BLI_assert(type_ != nullptr); + return *type_; + } + + const FieldFunction &function() const + { + BLI_assert(function_ != nullptr); + return *function_; + } + + int function_output_index() const + { + return output_index_; + } + + blender::StringRef debug_name() const + { + return debug_name_; + } +}; + +/** + * Evaluate more than one field at a time, as an optimization + * in case they share inputs or various intermediate values. + */ +void evaluate_fields(blender::Span<Field> fields, + blender::IndexMask mask, + blender::MutableSpan<GMutableSpan> outputs); + +} // namespace blender::fn
\ No newline at end of file diff --git a/source/blender/functions/intern/field.cc b/source/blender/functions/intern/field.cc new file mode 100644 index 00000000000..29f5148c01a --- /dev/null +++ b/source/blender/functions/intern/field.cc @@ -0,0 +1,115 @@ +/* + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#include "BLI_map.hh" +#include "BLI_multi_value_map.hh" + +#include "FN_field.hh" + +namespace blender::fn { + +/* A map to hold the output variables for each function so they can be reused. */ +using OutputMap = MultiValueMap<const FieldFunction *, MFVariable *>; + +static MFVariable *get_field_variable(const Field &field, const OutputMap &output_map) +{ + const FieldFunction &input_field_function = field.function(); + const Span<MFVariable *> input_function_outputs = output_map.lookup(&input_field_function); + return input_function_outputs[field.function_output_index()]; +} + +/** + * Traverse the fields recursively. Eventually there will be a field whose function has no + * inputs. Start adding multi-function variables there. Those outputs are then used as inputs + * for the dependent functions, and the rest of the field tree is built up from there. + */ +static void add_field_variables(const Field &field, + MFProcedureBuilder &builder, + OutputMap &output_map) +{ + const FieldFunction &function = field.function(); + for (const Field *input_field : function.inputs()) { + add_field_variables(*input_field, builder, output_map); + } + + /* Add the immediate inputs to this field, which were added before in the recursive call. + * This will be skipped for functions with no inputs. */ + Vector<MFVariable *> inputs; + for (const Field *input_field : function.inputs()) { + MFVariable *input = get_field_variable(*input_field, output_map); + builder.add_input_parameter(input->data_type()); + inputs.append(input); + } + + Vector<MFVariable *> outputs = builder.add_call(function.multi_function(), inputs); + + builder.add_destruct(inputs); + + output_map.add_multiple(&function, outputs); +} + +static void build_procedure(const Span<Field> fields, MFProcedure &procedure) +{ + MFProcedureBuilder builder{procedure}; + + OutputMap output_map; + + for (const Field &field : fields) { + add_field_variables(field, builder, output_map); + } + + builder.add_return(); + + for (const Field &field : fields) { + MFVariable *input = get_field_variable(field, output_map); + builder.add_output_parameter(*input); + } + + std::cout << procedure.to_dot(); + + BLI_assert(procedure.validate()); +} + +static void evaluate_procedure(MFProcedure &procedure, + const IndexMask mask, + const MutableSpan<GMutableSpan> outputs) +{ + MFProcedureExecutor executor{"Evaluate Field", procedure}; + MFParamsBuilder params{executor, mask.min_array_size()}; + MFContextBuilder context; + + /* Add the output arrays. */ + for (const int i : outputs.index_range()) { + params.add_uninitialized_single_output(outputs[i]); + } + + executor.call(mask, params, context); +} + +/** + * Evaluate more than one prodecure at a time + */ +void evaluate_fields(const Span<Field> fields, + const IndexMask mask, + const MutableSpan<GMutableSpan> outputs) +{ + MFProcedure procedure; + build_procedure(fields, procedure); + + evaluate_procedure(procedure, mask, outputs); +} + +} // namespace blender::fn diff --git a/source/blender/functions/tests/FN_field_test.cc b/source/blender/functions/tests/FN_field_test.cc new file mode 100644 index 00000000000..da275f7ab92 --- /dev/null +++ b/source/blender/functions/tests/FN_field_test.cc @@ -0,0 +1,73 @@ +/* Apache License, Version 2.0 */ + +#include "testing/testing.h" + +#include "FN_cpp_type.hh" +#include "FN_field.hh" +#include "FN_multi_function_builder.hh" + +namespace blender::fn::tests { + +TEST(field, ConstantInput) +{ + FieldFunction function = FieldFunction(std::make_unique<CustomMF_Constant<int>>(10), {}); + Field constant_field = Field(CPPType::get<int>(), function, 0); + + Array<int> result(4); + GMutableSpan result_generic(result.as_mutable_span()); + evaluate_fields({&constant_field, 1}, IndexMask(IndexRange(4)), {&result_generic, 1}); + + EXPECT_EQ(result[0], 10); + EXPECT_EQ(result[1], 10); + EXPECT_EQ(result[2], 10); + EXPECT_EQ(result[3], 10); +} + +class IndexFunction : public MultiFunction { + public: + IndexFunction() + { + static MFSignature signature = create_signature(); + this->set_signature(&signature); + } + + static MFSignature create_signature() + { + MFSignatureBuilder signature("Index"); + signature.single_output<int>("Index"); + return signature.build(); + } + + void call(IndexMask mask, MFParams params, MFContext UNUSED(context)) const override + { + MutableSpan<int> result = params.uninitialized_single_output<int>(0, "Index"); + for (int64_t i : mask) { + result[i] = i; + } + } +}; + +TEST(field, VArrayInput) +{ + + FieldFunction function = FieldFunction(std::make_unique<IndexFunction>(), {}); + Field index_field = Field(CPPType::get<int>(), function, 0); + + Array<int> result_1(4); + GMutableSpan result_generic_1(result_1.as_mutable_span()); + evaluate_fields({&index_field, 1}, IndexMask(IndexRange(4)), {&result_generic_1, 1}); + EXPECT_EQ(result_1[0], 0); + EXPECT_EQ(result_1[1], 1); + EXPECT_EQ(result_1[2], 2); + EXPECT_EQ(result_1[3], 3); + + Array<int> result_2(4); + GMutableSpan result_generic_2(result_2.as_mutable_span()); + evaluate_fields({&index_field, 1}, {20, 30, 40, 50}, {&result_generic_2, 1}); + EXPECT_EQ(result_2[0], 20); + EXPECT_EQ(result_2[1], 30); + EXPECT_EQ(result_2[2], 40); + EXPECT_EQ(result_2[3], 50); +} + +} // namespace blender::fn::tests |