Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,82 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
PUBLIC .
)
set(INC_SYS
)
set(SRC
intern/field.cc
intern/field_evaluation.cc
intern/lazy_function.cc
intern/lazy_function_execute.cc
intern/lazy_function_graph.cc
intern/lazy_function_graph_executor.cc
intern/lazy_function_graph_executor_generic.hh
intern/multi_function.cc
intern/multi_function_builder.cc
intern/multi_function_common.cc
intern/multi_function_params.cc
intern/multi_function_procedure.cc
intern/multi_function_procedure_builder.cc
intern/multi_function_procedure_executor.cc
intern/multi_function_procedure_optimization.cc
intern/multi_function_registry.cc
intern/user_data.cc
FN_field.hh
FN_field_evaluation.hh
FN_init.hh
FN_lazy_function.hh
FN_lazy_function_execute.hh
FN_lazy_function_graph.hh
FN_lazy_function_graph_executor.hh
FN_lazy_function_graph_executor_generic.hh
FN_multi_function.hh
FN_multi_function_builder.hh
FN_multi_function_context.hh
FN_multi_function_data_type.hh
FN_multi_function_param_type.hh
FN_multi_function_params.hh
FN_multi_function_procedure.hh
FN_multi_function_procedure_builder.hh
FN_multi_function_procedure_executor.hh
FN_multi_function_procedure_optimization.hh
FN_multi_function_registry.hh
FN_multi_function_signature.hh
FN_user_data.hh
)
set(LIB
PRIVATE bf::blenlib
PRIVATE bf::dna
PRIVATE bf::intern::guardedalloc
PRIVATE bf::intern::clog
PUBLIC bf::intern::profile
PRIVATE bf::extern::xxhash
)
blender_add_lib(bf_functions "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
add_library(bf::functions ALIAS bf_functions)
if(WITH_GTESTS)
set(TEST_INC
)
set(TEST_SRC
tests/FN_field_test.cc
tests/FN_lazy_function_test.cc
tests/FN_multi_function_procedure_test.cc
tests/FN_multi_function_test.cc
tests/FN_multi_function_test_common.hh
)
set(TEST_LIB
bf_functions
PRIVATE bf::blenkernel
)
blender_add_test_suite_lib(function "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
endif()

View File

@@ -0,0 +1,695 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* A #Field represents a function that outputs a value based on an arbitrary number of inputs. The
* inputs for a specific field evaluation are provided by a #FieldContext.
*
* A typical example is a field that computes a displacement vector for every vertex on a mesh
* based on its position.
*
* Fields can be built, composed and evaluated at run-time. They are stored in a directed tree
* graph data structure. A field may generally depend on other fields.
*
* When fields are evaluated, they are converted into a multi-function procedure which allows
* efficient computation. In the future, we might support different field evaluation mechanisms for
* e.g. the following scenarios:
* - Latency of a single evaluation is more important than throughput.
* - Evaluation should happen on other hardware like GPUs.
*
* Whenever possible, multiple fields should be evaluated together to avoid duplicate work when
* they share common sub-fields and a common context.
*/
#include "BLI_cache_mutex.hh"
#include "BLI_implicit_sharing_ptr.hh"
#include "FN_multi_function.hh"
namespace blender::fn {
class GField;
class FieldInput;
class FieldOperation;
class FieldInputs;
class FieldContext;
using FieldInputPtr = ImplicitSharingPtr<FieldInput>;
using FieldOperationPtr = ImplicitSharingPtr<FieldOperation>;
using FieldInputsPtr = ImplicitSharingPtr<FieldInputs>;
template<typename T> class Field;
/**
* A field with a type that is only known at runtime which can be accessed through the #cpp_type
* method. If the type is known at compile time, it is recommended to use #Field<T> instead.
*
* It is designed to support various internal storage representations to avoid unnecessary
* allocations or reference counting in many common cases.
*/
class GField {
public:
struct Input {
FieldInputPtr node;
};
struct MultiFn {
FieldOperationPtr node;
int output_i = 0;
};
/**
* Allows referencing another field without owning it. This helps with fields that are highly
* reused like the position field because it avoids reference counting..
*/
struct FieldRef {
const GField *field_ref = nullptr;
};
struct ConstantRef {
const CPPType *type = nullptr;
/** This value is not owned. Typically it has static lifetime. */
const void *value = nullptr;
};
/**
* Allows storing constants inside of #GField without any additional memory allocation.
*/
struct TrivialInlineConstant {
static constexpr int64_t inline_size = 16;
static constexpr int64_t inline_alignment = 8;
template<typename T>
static constexpr bool type_supported_v = std::is_trivially_destructible_v<T> &&
std::is_trivially_copyable_v<T> &&
sizeof(T) <= inline_size &&
alignof(T) <= inline_alignment;
static bool cpp_type_supported(const CPPType &type);
const CPPType *type = nullptr;
AlignedBuffer<inline_size, inline_alignment> value;
};
/** Used for storing constants that can't be inlined. */
struct OwnedConstant {
const CPPType *type = nullptr;
/* This value is owned by the #GField. */
void *value = nullptr;
};
template<typename T>
static constexpr bool is_constant_value_v =
is_same_any_v<T, ConstantRef, TrivialInlineConstant, OwnedConstant>;
using Variant =
std::variant<Input, MultiFn, FieldRef, ConstantRef, TrivialInlineConstant, OwnedConstant>;
private:
Variant variant_;
public:
/**
* #GField is expected to always have a valid #CPPType. Therefore, it can't be default
* constructed.
*/
GField() = delete;
/** Construct a field that just outputs the default value of the given type. */
explicit GField(const CPPType &type) noexcept;
/** Construct a field owning a field input. */
explicit GField(FieldInputPtr node) noexcept;
/** Construct a field that owns a field operation and outputs one of its outputs. */
explicit GField(FieldOperationPtr node, int output_i = 0) noexcept;
/** Construct directly from a #Variant, mostly for internal use. */
explicit GField(Variant variant) noexcept;
/**
* Wraps the given field in a new field. This is used to avoid reference counting for some field
* fields which have static lifetime.
*/
static GField from_non_owning_ref(const GField &field);
/** Construct a field that just outputs the given constant value. */
static GField from_constant(const CPPType &type, const void *value);
/** Construct a field that just outputs the given constant value without owning it. */
static GField from_non_owning_constant(const CPPType &type, const void *value);
/** Build a new #FieldInput with the given arguments. */
template<typename InputT, typename... Args> static GField from_input(Args &&...args);
/**
* #GField requires manual memory management due to inlined values and to support move semantics
* without making #GField nullable.
*/
GField(const GField &other);
GField(GField &&other) noexcept;
GField &operator=(const GField &other);
GField &operator=(GField &&other) noexcept;
~GField();
/** The value type the field outputs for each element, e.g. float. */
const CPPType &cpp_type() const;
/** Root #FieldInput nodes that this field depends on. */
const FieldInputsPtr &field_inputs() const;
/**
* This "normalizes" the field. Specifically, if this field is just a non-owning reference to
* some other field, the referenced field is returned.
*/
const GField &deref_field_ref() const;
/** Get the underlying #Variant. */
const Variant &variant() const;
/** Returns true when the field depends on some input. */
bool depends_on_input() const;
/** Utility to access a specific input type if this field is just an input. */
template<typename InputT> const InputT *get_input_if() const;
/**
* This only implements shallow comparison. A more deep comparison could reveal that two fields
* are semantically the same even if this comparison is false. Deep comparison is much more
* expensive though.
*/
friend bool operator==(const GField &a, const GField &b);
uint64_t hash() const;
/**
* Get a typed reference to this field. Note that #Field<T> happens to be identical to #GField on
* a bit-level. So this is just a cast.
*/
template<typename T> const Field<T> &typed() const;
template<typename T> Field<T> &typed();
/**
* Attempts to take ownership of a FieldOperation stored in this field, leaving the field input.
* It's expected to be deleted shortly after. This is necessary to avoid deep recursion when
* destructing a field tree.
*/
FieldOperationPtr try_extract_operation();
};
/** A version of #GField that should be used when the field type is known at compile time. */
template<typename T> class Field {
public:
using base_type = T;
using generic_type = GField;
private:
/**
* #Field<T> just stores a #GField. This makes converting between the two types easy.
*/
GField field_;
friend GField;
public:
/**
* Unlike #GField, default construction is allowed here, because the type is known without extra
* arguments.
*/
Field();
/** Same as corresponding #GField constructors. */
explicit Field(FieldInputPtr node);
explicit Field(FieldOperationPtr node, int output_i = 0);
/** Construct a field that just outputs the given value. */
explicit Field(T value);
/** This is implicitly cast to #GField which is always valid. */
operator const GField &() const;
/** These are the same as the corresponding #GField methods. */
bool depends_on_input() const;
template<typename InputT, typename... Args> static Field from_input(Args &&...args);
template<typename InputT> const InputT *get_input_if() const;
uint64_t hash() const;
static Field from_non_owning_ref(const Field &field);
};
/**
* A version of #GField that only references data from other fields but does not own any data
* itself. This allows it to be smaller and trivially copyable making it more efficient in some
* contexts. This is mainly used during field evaluation.
*/
class GFieldRef {
public:
struct Value {
const CPPType *type = nullptr;
const void *value = nullptr;
};
struct Input {
const FieldInput *node = nullptr;
};
struct MultiFn {
const FieldOperation *node = nullptr;
int output_i = 0;
};
using Variant = std::variant<Value, Input, MultiFn>;
private:
Variant variant_;
public:
/**
* Create a reference to the given fields. The caller is responsible for making sure that the
* referenced data stays valid.
*/
GFieldRef(const GField &field);
template<typename T> GFieldRef(const Field<T> &field);
explicit GFieldRef(const FieldInput &field_input);
explicit GFieldRef(const FieldOperation &field_multi_fn, int output_i = 0);
explicit GFieldRef(Variant variant);
/** Get access to the underlying #Variant. */
const Variant &variant() const;
/** These are the same as the corresponding #GField methods. */
const CPPType &cpp_type() const;
const FieldInputsPtr &field_inputs() const;
uint64_t hash() const;
static GFieldRef from_constant(const CPPType &type, const void *value);
};
/**
* A field is always evaluated in some context. This context determines the value of the field
* inputs.
*/
class FieldContext {
public:
virtual ~FieldContext() = default;
virtual GVArray get_varray_for_input(const FieldInput &field_input,
const IndexMask &mask,
ResourceScope &scope) const;
};
/**
* "Deep" hashing for fields that considers the operation and inputs semantically, rather than
* just the shallow data (i.e. memory address) of the field data, like the default "hash()"
* implementation. Because common field reuse would give this potentially exponential cost, this
* struct caches the hashes of intermediate fields.
*/
struct FieldHashDeep {
Map<GFieldRef, UniqueHash> cache;
UniqueHash ensure(const GFieldRef &field);
UniqueHash lookup(const GFieldRef &field) const
{
return this->cache.lookup(field);
}
bool contains(const GFieldRef &field) const
{
return this->cache.contains(field);
}
};
/**
* Cache of field inputs. This is used quite often and is therefore computed eagerly for
* intermediate operations. Otherwise one would have to parse the field tree every time the set of
* inputs is required. Since many fields share the same set of inputs, this is often shared.
*/
class FieldInputs : public ImplicitSharingMixin {
public:
/** Deduplicated set of field inputs. */
VectorSet<std::reference_wrapper<const FieldInput>> inputs;
void delete_self() override;
};
/**
* This is an abstract class which concrete field inputs have to derive from. When a field is
* evaluated, this can provide values based on the provided context.
*
* Since there is no better way yet, #FieldInput is also often used to process the output of
* intermediate fields, in which case this is not technically an "input".
*/
class FieldInput : public ImplicitSharingMixin {
protected:
const CPPType *type_;
std::string debug_name_;
/**
* Field inputs are initialized lazily because it can't be done in the constructor because the
* derived class constructor has not run yet.
*/
mutable CacheMutex field_inputs_mutex_;
mutable FieldInputsPtr field_inputs_;
public:
FieldInput(const CPPType &type, std::string debug_name = "");
~FieldInput() override;
StringRefNull debug_name() const;
virtual std::string socket_inspection_name() const;
const CPPType &cpp_type() const;
const FieldInputsPtr &field_inputs() const;
uint64_t hash() const;
virtual void hash_unique(UniqueHashBytes &hash, FieldHashDeep &deep_hash_cache) const;
/**
* If this #FieldInput depends on other fields, this function should be overridden.
*/
virtual void foreach_recursive_field(FunctionRef<void(const GField &)> fn) const;
/**
* Output a virtual array for the given index mask in the given context.
*/
virtual GVArray get_varray_for_context(const FieldContext &context,
const IndexMask &mask,
ResourceScope &scope) const = 0;
void delete_self() override;
};
/**
* This is an intermediate node in a field tree which executes a #MultiFunction on each value. The
* #MultiFunction can either be owned or just referenced.
*
* It also stores a #GField for every input of the multi-function. Other fields may reference
* individual outputs.
*/
class FieldOperation : public ImplicitSharingMixin {
private:
/** One #GField for every input of the multi-function. */
Vector<GField> inputs_;
/** Optionally owned multi-function. */
std::shared_ptr<const mf::MultiFunction> owned_fn_;
const mf::MultiFunction *fn_;
/** Cached field inputs. */
FieldInputsPtr field_inputs_;
public:
/** Prefer `from*` constructor functions instead. */
FieldOperation(std::shared_ptr<const mf::MultiFunction> fn, Vector<GField> inputs);
FieldOperation(const mf::MultiFunction &fn, Vector<GField> inputs);
static FieldOperationPtr from(std::shared_ptr<const mf::MultiFunction> fn,
Vector<GField> inputs);
static FieldOperationPtr from(const mf::MultiFunction &fn, Vector<GField> inputs);
/** Get the type of a specific output. */
const CPPType &output_cpp_type(int output_i) const;
const mf::MultiFunction &multi_function() const;
const FieldInputsPtr &field_inputs() const;
Span<GField> inputs() const;
void delete_self() override;
private:
void delete_input_fields();
};
bool operator==(const GField &a, const GField &b);
bool operator==(const GFieldRef &a, const GFieldRef &b);
/** Type trait to detect field types. */
template<typename T> constexpr bool is_field_v = false;
template<typename T> constexpr bool is_field_v<Field<T>> = true;
Field<bool> invert_boolean_field(const Field<bool> &field);
class IndexFieldInput final : public FieldInput {
public:
IndexFieldInput();
static GVArray get_index_varray(const IndexMask &mask);
GVArray get_varray_for_context(const FieldContext &context,
const IndexMask &mask,
ResourceScope &scope) const final;
void hash_unique(UniqueHashBytes &hash, FieldHashDeep &deep_hash_cache) const override;
/** Cached index field to avoid allocating a new one every time. */
static const Field<int> &get_field();
};
/* -------------------------------------------------------------------- */
/** \name Inline Methods
* \{ */
inline GField::GField(const CPPType &type) noexcept
: variant_(ConstantRef{&type, type.default_value()})
{
}
inline GField::GField(FieldInputPtr node) noexcept : variant_(Input{std::move(node)}) {}
inline GField::GField(Variant variant) noexcept : variant_(std::move(variant)) {}
inline GField::GField(FieldOperationPtr node, const int output_i) noexcept
: variant_(MultiFn{std::move(node), output_i})
{
}
inline GField GField::from_non_owning_ref(const GField &field)
{
return GField(FieldRef{&field});
}
inline bool GField::TrivialInlineConstant::cpp_type_supported(const CPPType &type)
{
return type.is_trivial && type.size <= TrivialInlineConstant::inline_size &&
type.alignment <= TrivialInlineConstant::inline_alignment;
}
inline GField GField::from_non_owning_constant(const CPPType &type, const void *value)
{
return GField(ConstantRef{&type, value});
}
template<typename T> inline Field<T> Field<T>::from_non_owning_ref(const Field &field)
{
return GField::from_non_owning_ref(field).template typed<T>();
}
template<typename InputT, typename... Args> inline GField GField::from_input(Args &&...args)
{
FieldInputPtr input{MEM_new<InputT>(__func__, std::forward<Args>(args)...)};
return GField(Input{std::move(input)});
}
template<typename T>
template<typename InputT, typename... Args>
inline Field<T> Field<T>::from_input(Args &&...args)
{
return GField::from_input<InputT>(std::forward<Args>(args)...).template typed<T>();
}
template<typename T>
inline Field<T>::Field(T value)
: field_([&]() {
const CPPType &type = CPPType::get<T>();
if constexpr (GField::TrivialInlineConstant::type_supported_v<T>) {
GField::TrivialInlineConstant constant;
constant.type = &type;
new (constant.value.ptr()) T(std::move(value));
return GField(constant);
}
else {
T *new_value = MEM_new<T>(__func__, std::move(new_value));
return GField(GField::OwnedConstant{&type, new_value});
}
}())
{
}
template<typename T> inline bool Field<T>::depends_on_input() const
{
return field_.depends_on_input();
}
inline const CPPType &GField::cpp_type() const
{
return std::visit(
[]<typename T>(const T &v) -> const CPPType & {
if constexpr (std::is_same_v<T, Input>) {
return v.node->cpp_type();
}
else if constexpr (std::is_same_v<T, MultiFn>) {
return v.node->output_cpp_type(v.output_i);
}
else if constexpr (std::is_same_v<T, FieldRef>) {
return v.field_ref->cpp_type();
}
else if constexpr (is_same_any_v<T, ConstantRef, TrivialInlineConstant, OwnedConstant>) {
return *v.type;
}
else {
BLI_assert_unreachable_static_t(T);
}
},
this->variant_);
}
inline const GField &GField::deref_field_ref() const
{
if (const auto *field_ref = std::get_if<FieldRef>(&this->variant_)) {
return field_ref->field_ref->deref_field_ref();
}
return *this;
}
template<typename T> inline bool operator==(const Field<T> &a, const Field<T> &b)
{
return static_cast<const GField &>(a) == static_cast<const GField &>(b);
}
template<typename T> inline uint64_t Field<T>::hash() const
{
return field_.hash();
}
inline const CPPType &FieldInput::cpp_type() const
{
return *this->type_;
}
inline const FieldInputsPtr &FieldOperation::field_inputs() const
{
return field_inputs_;
}
inline StringRefNull FieldInput::debug_name() const
{
return debug_name_;
}
inline std::string FieldInput::socket_inspection_name() const
{
return debug_name_;
}
template<typename T> inline Field<T>::operator const GField &() const
{
return field_;
}
template<typename T> inline const Field<T> &GField::typed() const
{
static_assert(sizeof(GField) == sizeof(Field<T>));
BLI_assert(this->cpp_type().is<T>());
return reinterpret_cast<const Field<T> &>(*this);
}
template<typename T> inline Field<T> &GField::typed()
{
static_assert(sizeof(GField) == sizeof(Field<T>));
BLI_assert(this->cpp_type().is<T>());
return reinterpret_cast<Field<T> &>(*this);
}
inline const GField::Variant &GField::variant() const
{
return variant_;
}
template<typename T> inline Field<T>::Field() : field_(CPPType::get<T>()) {}
template<typename T> inline Field<T>::Field(FieldInputPtr node) : field_(GField(std::move(node)))
{
}
template<typename T>
inline Field<T>::Field(FieldOperationPtr node, const int output_i)
: field_(GField(std::move(node), output_i))
{
}
inline bool GField::depends_on_input() const
{
const FieldInputsPtr &inputs = this->field_inputs();
if (!inputs) {
return false;
}
return !inputs->inputs.is_empty();
}
template<typename InputT> inline const InputT *GField::get_input_if() const
{
const GField &deref_field = this->deref_field_ref();
if (const auto *input = std::get_if<Input>(&deref_field.variant())) {
return dynamic_cast<const InputT *>(input->node.get());
}
return nullptr;
}
template<typename T> template<typename InputT> inline const InputT *Field<T>::get_input_if() const
{
return field_.get_input_if<InputT>();
}
inline Span<GField> FieldOperation::inputs() const
{
return inputs_;
}
inline GFieldRef::GFieldRef(const FieldInput &field_input) : variant_(Input{&field_input}) {}
inline GFieldRef::GFieldRef(const FieldOperation &field_multi_fn, int output_i)
: variant_(MultiFn{&field_multi_fn, output_i})
{
}
inline GFieldRef::GFieldRef(Variant variant) : variant_(std::move(variant)) {}
inline GFieldRef GFieldRef::from_constant(const CPPType &type, const void *value)
{
return GFieldRef(Value{&type, value});
}
template<typename T>
inline GFieldRef::GFieldRef(const Field<T> &field) : GFieldRef(static_cast<const GField &>(field))
{
}
inline const GFieldRef::Variant &GFieldRef::variant() const
{
return variant_;
}
inline const CPPType &GFieldRef::cpp_type() const
{
return std::visit(
[]<typename T>(const T &v) -> const CPPType & {
if constexpr (std::is_same_v<T, Value>) {
return *v.type;
}
else if constexpr (std::is_same_v<T, Input>) {
return v.node->cpp_type();
}
else if constexpr (std::is_same_v<T, MultiFn>) {
return v.node->output_cpp_type(v.output_i);
}
else {
BLI_assert_unreachable_static_t(T);
}
},
variant_);
}
inline bool operator==(const FieldInput &a, const FieldInput &b)
{
return &a == &b;
}
inline const mf::MultiFunction &FieldOperation::multi_function() const
{
return *this->fn_;
}
/** \} */
} // namespace blender::fn

View File

@@ -0,0 +1,217 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_generic_virtual_array.hh"
#include "BLI_vector.hh"
#include "FN_field.hh"
namespace blender::fn {
/**
* Utility class that makes it easier to evaluate fields.
*/
class FieldEvaluator : NonMovable, NonCopyable {
struct OutputPointerInfo {
void *dst = nullptr;
/* When a destination virtual array is provided for an input, this is
* unnecessary, otherwise this is used to construct the required virtual array. */
void (*set)(void *dst, const GVArray &varray, ResourceScope &scope) = nullptr;
};
ResourceScope scope_;
const FieldContext &context_;
const IndexMask &mask_;
Vector<GField> fields_to_evaluate_;
Vector<GVMutableArray> dst_varrays_;
Vector<GVArray> evaluated_varrays_;
Vector<OutputPointerInfo> output_pointer_infos_;
bool is_evaluated_ = false;
std::optional<Field<bool>> selection_field_;
IndexMask selection_mask_;
public:
/** Takes #mask by pointer because the mask has to live longer than the evaluator. */
FieldEvaluator(const FieldContext &context, const IndexMask *mask)
: context_(context), mask_(*mask)
{
}
/** Construct a field evaluator for all indices less than #size. */
FieldEvaluator(const FieldContext &context, const int64_t size)
: context_(context), mask_(scope_.construct<IndexMask>(size))
{
}
~FieldEvaluator()
{
/* While this assert isn't strictly necessary, and could be replaced with a warning,
* it will catch cases where someone forgets to call #evaluate(). */
BLI_assert(is_evaluated_);
}
/**
* The selection field is evaluated first to determine which indices of the other fields should
* be evaluated. Calling this method multiple times will just replace the previously set
* selection field. Only the elements selected by both this selection and the selection provided
* in the constructor are calculated. If no selection field is set, it is assumed that all
* indices passed to the constructor are selected.
*/
void set_selection(Field<bool> selection)
{
selection_field_ = std::move(selection);
}
/**
* \param field: Field to add to the evaluator.
* \param dst: Mutable virtual array that the evaluated result for this field is be written into.
*/
int add_with_destination(GField field, GVMutableArray dst);
/** Same as #add_with_destination but typed. */
template<typename T> int add_with_destination(Field<T> field, VMutableArray<T> dst)
{
return this->add_with_destination(GField(std::move(field)), GVMutableArray(std::move(dst)));
}
/**
* \param field: Field to add to the evaluator.
* \param dst: Mutable span that the evaluated result for this field is be written into.
* \note When the output may only be used as a single value, the version of this function with
* a virtual array result array should be used.
*/
int add_with_destination(GField field, GMutableSpan dst);
/**
* \param field: Field to add to the evaluator.
* \param dst: Mutable span that the evaluated result for this field is be written into.
* \note When the output may only be used as a single value, the version of this function with
* a virtual array result array should be used.
*/
template<typename T> int add_with_destination(Field<T> field, MutableSpan<T> dst)
{
return this->add_with_destination(std::move(field), VMutableArray<T>::from_span(dst));
}
int add(GField field, GVArray *varray_ptr);
/**
* \param field: Field to add to the evaluator.
* \param varray_ptr: Once #evaluate is called, the resulting virtual array will be will be
* assigned to the given position.
* \return Index of the field in the evaluator which can be used in the #get_evaluated methods.
*/
template<typename T> int add(Field<T> field, VArray<T> *varray_ptr)
{
const int field_index = fields_to_evaluate_.append_and_get_index(std::move(field));
dst_varrays_.append({});
output_pointer_infos_.append(OutputPointerInfo{
varray_ptr, [](void *dst, const GVArray &varray, ResourceScope & /*scope*/) {
*static_cast<VArray<T> *>(dst) = varray.typed<T>();
}});
return field_index;
}
template<typename T> int add(Field<T> field, VArraySpan<T> *varray_span_ptr)
{
const int field_index = fields_to_evaluate_.append_and_get_index(std::move(field));
dst_varrays_.append({});
output_pointer_infos_.append(OutputPointerInfo{
varray_span_ptr, [](void *dst, const GVArray &varray, ResourceScope & /*scope*/) {
*static_cast<VArraySpan<T> *>(dst) = varray.typed<T>();
}});
return field_index;
}
/**
* \return Index of the field in the evaluator which can be used in the #get_evaluated methods.
*/
int add(GField field);
/**
* Evaluate all fields on the evaluator. This can only be called once.
*/
void evaluate();
const GVArray &get_evaluated(const int field_index) const
{
BLI_assert(is_evaluated_);
return evaluated_varrays_[field_index];
}
template<typename T> VArray<T> get_evaluated(const int field_index) const
{
return this->get_evaluated(field_index).typed<T>();
}
IndexMask get_evaluated_selection_as_mask() const;
/**
* Retrieve the output of an evaluated boolean field and convert it to a mask, which can be used
* to avoid calculations for unnecessary elements later on. The evaluator will own the indices in
* some cases, so it must live at least as long as the returned mask.
*/
IndexMask get_evaluated_as_mask(int field_index);
const IndexMask &evaluation_mask() const
{
return mask_;
}
};
/**
* Evaluate fields in the given context. If possible, multiple fields should be evaluated together,
* because that can be more efficient when they share common sub-fields.
*
* \param scope: The resource scope that owns data that makes up the output virtual arrays. Make
* sure the scope is not destructed when the output virtual arrays are still used.
* \param fields_to_evaluate: The fields that should be evaluated together.
* \param mask: Determines which indices are computed. The mask may be referenced by the returned
* virtual arrays. So the underlying indices (if applicable) should live longer then #scope.
* \param context: The context that the field is evaluated in. Used to retrieve data from each
* #FieldInput in the field network.
* \param dst_varrays: If provided, the computed data will be written into those virtual arrays
* instead of into newly created ones. That allows making the computed data live longer than
* #scope and is more efficient when the data will be written into those virtual arrays
* later anyway.
* \return The computed virtual arrays for each provided field. If #dst_varrays is passed, the
* provided virtual arrays are returned.
*/
Vector<GVArray> evaluate_fields(ResourceScope &scope,
Span<GFieldRef> fields_to_evaluate,
const IndexMask &mask,
const FieldContext &context,
Span<GVMutableArray> dst_varrays = {});
/* -------------------------------------------------------------------- */
/** \name Utility functions for simple field creation and evaluation
* \{ */
void evaluate_constant_field(const GField &field, void *r_value);
template<typename T> T evaluate_constant_field(const Field<T> &field)
{
T value;
value.~T();
evaluate_constant_field(field, &value);
return value;
}
/**
* If the field depends on some input, the same field is returned.
* Otherwise the field is evaluated and a new field is created that just computes this constant.
*
* Making the field constant has two benefits:
* - The field-tree becomes a single node, which is more efficient when the field is evaluated many
* times.
* - Memory of the input fields may be freed.
*/
GField make_field_constant_if_possible(GField field);
/** \} */
} // namespace blender::fn

View File

@@ -0,0 +1,11 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
namespace blender::fn::multi_function {
void register_common_functions();
}

View File

@@ -0,0 +1,479 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* A `LazyFunction` encapsulates a computation which has inputs, outputs and potentially side
* effects. Most importantly, a `LazyFunction` supports laziness in its inputs and outputs:
* - Only outputs that are actually used have to be computed.
* - Inputs can be requested lazily based on which outputs are used or what side effects the
* function has.
*
* A lazy-function that uses laziness may be executed more than once. The most common example is
* the geometry nodes switch node. Depending on a condition input, it decides which one of the
* other inputs is actually used. From the perspective of the switch node, its execution works as
* follows:
* 1. The switch node is first executed. It sees that the output is used. Now it requests the
* condition input from the caller and exits.
* 2. Once the caller is able to provide the condition input the switch node is executed again.
* This time it retrieves the condition and requests one of the other inputs. Then the node
* exits again, giving back control to the caller.
* 3. When the caller computed the second requested input the switch node executes a last time.
* This time it retrieves the new input and forwards it to the output.
*
* In some sense, a lazy-function can be thought of like a state machine. Every time it is
* executed, it advances its state until all required outputs are ready.
*
* The lazy-function interface is designed to support composition of many such functions into a new
* lazy-functions, all while keeping the laziness working. For example, in geometry nodes a switch
* node in a node group should still be able to decide whether a node in the parent group will be
* executed or not. This is essential to avoid doing unnecessary work.
*
* The lazy-function system consists of multiple core components:
* - The interface of a lazy-function itself including its calling convention.
* - A graph data structure that allows composing many lazy-functions by connecting their inputs
* and outputs.
* - An executor that allows multi-threaded execution or such a graph.
*/
#include "BLI_cpp_type.hh"
#include "BLI_function_ref.hh"
#include "BLI_linear_allocator.hh"
#include "BLI_vector.hh"
#include "PRF_profile.hh"
#include "FN_user_data.hh"
#ifndef NDEBUG
# include <atomic>
# include <thread>
# define FN_LAZY_FUNCTION_DEBUG_THREADS
#endif
namespace blender {
namespace fn::lazy_function {
enum class ValueUsage : uint8_t {
/**
* The value is definitely used and therefore has to be computed.
*/
Used,
/**
* It's unknown whether this value will be used or not. Computing it is ok but the result may be
* discarded.
*/
Maybe,
/**
* The value will definitely not be used. It can still be computed but the result will be
* discarded in all cases.
*/
Unused,
};
class LazyFunction;
/**
* Passed to the lazy-function when it is executed.
*/
struct Context {
/**
* If the lazy-function has some state (which only makes sense when it is executed more than once
* to finish its job), the state is stored here. This points to memory returned from
* #LazyFunction::init_storage.
*/
void *storage;
/**
* Custom user data that can be used in the function.
*/
UserData *user_data;
/**
* Custom user data that is local to the thread that executes the lazy-function.
*/
LocalUserData *local_user_data;
Context(void *storage, UserData *user_data, LocalUserData *local_user_data)
: storage(storage), user_data(user_data), local_user_data(local_user_data)
{
}
};
/**
* Defines the calling convention for a lazy-function. During execution, a lazy-function retrieves
* its inputs and sets the outputs through #Params.
*/
class Params {
public:
/**
* The lazy-function this #Params has been prepared for.
*/
const LazyFunction &fn_;
#ifdef FN_LAZY_FUNCTION_DEBUG_THREADS
std::thread::id main_thread_id_;
std::atomic<bool> allow_multi_threading_;
#endif
Params(const LazyFunction &fn, bool allow_multi_threading_initially);
/**
* Get a pointer to an input value if the value is available already. Otherwise null is returned.
*
* The #LazyFunction must leave returned object in an initialized state, but can move from it.
*/
void *try_get_input_data_ptr(int index) const;
/**
* Same as #try_get_input_data_ptr, but if the data is not yet available, request it. This makes
* sure that the data will be available in a future execution of the #LazyFunction.
*/
void *try_get_input_data_ptr_or_request(int index);
/**
* Get a pointer to where the output value should be stored.
* The value at the pointer is in an uninitialized state at first.
* The #LazyFunction is responsible for initializing the value.
* After the output has been initialized to its final value, #output_set has to be called.
*/
void *get_output_data_ptr(int index);
/**
* Call this after the output value is initialized. After this is called, the value must not be
* touched anymore. It may be moved or destructed immediately.
*/
void output_set(int index);
/**
* Allows the #LazyFunction to check whether an output was computed already without keeping
* track of it itself.
*/
bool output_was_set(int index) const;
/**
* Can be used to detect which outputs have to be computed.
*/
ValueUsage get_output_usage(int index) const;
/**
* Tell the caller of the #LazyFunction that a specific input will definitely not be used.
* Only an input that was not #ValueUsage::Used can become unused.
*/
void set_input_unused(int index);
/**
* Typed utility methods that wrap the methods above.
*/
template<typename T> T extract_input(int index);
template<typename T> T &get_input(int index) const;
template<typename T> T *try_get_input_data_ptr(int index) const;
template<typename T> T *try_get_input_data_ptr_or_request(int index);
template<typename T> void set_output(int index, T &&value);
/**
* Returns true when the lazy-function is now allowed to use multi-threading when interacting
* with this #Params. That means, it is allowed to call non-const methods from different threads.
*/
bool try_enable_multi_threading();
private:
void assert_valid_thread() const;
/**
* Methods that need to be implemented by subclasses. Those are separate from the non-virtual
* methods above to make it easy to insert additional debugging logic on top of the
* implementations.
*/
virtual void *try_get_input_data_ptr_impl(int index) const = 0;
virtual void *try_get_input_data_ptr_or_request_impl(int index) = 0;
virtual void *get_output_data_ptr_impl(int index) = 0;
virtual void output_set_impl(int index) = 0;
virtual bool output_was_set_impl(int index) const = 0;
virtual ValueUsage get_output_usage_impl(int index) const = 0;
virtual void set_input_unused_impl(int index) = 0;
virtual bool try_enable_multi_threading_impl();
};
/**
* Describes an input of a #LazyFunction.
*/
struct Input {
/**
* Name used for debugging purposes. The string has to be static or has to be owned by something
* else.
*/
const char *debug_name;
/**
* Data type of this input.
*/
const CPPType *type;
/**
* Can be used to indicate a caller or this function if this input is used statically before
* executing it the first time. This is technically not needed but can improve efficiency because
* a round-trip through the `execute` method can be avoided.
*
* When this is #ValueUsage::Used, the caller has to ensure that the input is definitely
* available when the #execute method is first called. The #execute method does not have to check
* whether the value is actually available.
*/
ValueUsage usage;
Input(const char *debug_name, const CPPType &type, const ValueUsage usage = ValueUsage::Used)
: debug_name(debug_name), type(&type), usage(usage)
{
}
};
struct Output {
/**
* Name used for debugging purposes. The string has to be static or has to be owned by something
* else.
*/
const char *debug_name;
/**
* Data type of this output.
*/
const CPPType *type = nullptr;
Output(const char *debug_name, const CPPType &type) : debug_name(debug_name), type(&type) {}
};
/**
* A function that can compute outputs and request inputs lazily. For more details see the comment
* at the top of the file.
*/
class LazyFunction {
protected:
const char *debug_name_ = "unknown";
Vector<Input> inputs_;
Vector<Output> outputs_;
/**
* Allow executing the function even if previously requested values are not yet available.
*/
bool allow_missing_requested_inputs_ = false;
public:
virtual ~LazyFunction() = default;
/**
* Get a name of the function or an input or output. This is mainly used for debugging.
* These are virtual functions because the names are often not used outside of debugging
* workflows. This way the names are only generated when they are actually needed.
*/
virtual std::string name() const;
virtual std::string input_name(int index) const;
virtual std::string output_name(int index) const;
/**
* Allocates storage for this function. The storage will be passed to every call to #execute.
* If the function does not keep track of any state, this does not have to be implemented.
*/
virtual void *init_storage(LinearAllocator<> &allocator) const;
/**
* Destruct the storage created in #init_storage.
*/
virtual void destruct_storage(void *storage) const;
/**
* Calls `fn` with the input indices that the given `output_index` may depend on. By default
* every output depends on every input.
*/
virtual void possible_output_dependencies(int output_index,
FunctionRef<void(Span<int>)> fn) const;
/**
* Inputs of the function.
*/
Span<Input> inputs() const;
/**
* Outputs of the function.
*/
Span<Output> outputs() const;
/**
* During execution the function retrieves inputs and sets outputs in #params. For some
* functions, this method is called more than once. After execution, the function either has
* computed all required outputs or is waiting for more inputs.
*/
void execute(Params &params, const Context &context) const;
/**
* Utility to check that the guarantee by #Input::usage is followed.
*/
bool always_used_inputs_available(const Params &params) const;
/**
* If true, the function can be executed even when some requested inputs are not available yet.
* This allows the function to make some progress and maybe to compute some outputs that are
* passed into this function again (lazy-function graphs may contain cycles as long as there
* aren't actually data dependencies).
*/
bool allow_missing_requested_inputs() const
{
return allow_missing_requested_inputs_;
}
private:
/**
* Needs to be implemented by subclasses. This is separate from #execute so that additional
* debugging logic can be implemented in #execute.
*/
virtual void execute_impl(Params &params, const Context &context) const = 0;
};
/* -------------------------------------------------------------------- */
/** \name #LazyFunction Inline Methods
* \{ */
inline Span<Input> LazyFunction::inputs() const
{
return inputs_;
}
inline Span<Output> LazyFunction::outputs() const
{
return outputs_;
}
inline void LazyFunction::execute(Params &params, const Context &context) const
{
PRF_scope_with_name("LazyFunction", ProfileCategory::Default);
PRF_scope_set_dynamic_name("%s", debug_name_);
BLI_assert(this->always_used_inputs_available(params));
this->execute_impl(params, context);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #Params Inline Methods
* \{ */
inline Params::Params(const LazyFunction &fn,
[[maybe_unused]] bool allow_multi_threading_initially)
: fn_(fn)
#ifdef FN_LAZY_FUNCTION_DEBUG_THREADS
,
main_thread_id_(std::this_thread::get_id()),
allow_multi_threading_(allow_multi_threading_initially)
#endif
{
}
inline void *Params::try_get_input_data_ptr(const int index) const
{
BLI_assert(index >= 0 && index < fn_.inputs().size());
return this->try_get_input_data_ptr_impl(index);
}
inline void *Params::try_get_input_data_ptr_or_request(const int index)
{
BLI_assert(index >= 0 && index < fn_.inputs().size());
this->assert_valid_thread();
return this->try_get_input_data_ptr_or_request_impl(index);
}
inline void *Params::get_output_data_ptr(const int index)
{
BLI_assert(index >= 0 && index < fn_.outputs().size());
this->assert_valid_thread();
return this->get_output_data_ptr_impl(index);
}
inline void Params::output_set(const int index)
{
BLI_assert(index >= 0 && index < fn_.outputs().size());
this->assert_valid_thread();
this->output_set_impl(index);
}
inline bool Params::output_was_set(const int index) const
{
BLI_assert(index >= 0 && index < fn_.outputs().size());
return this->output_was_set_impl(index);
}
inline ValueUsage Params::get_output_usage(const int index) const
{
BLI_assert(index >= 0 && index < fn_.outputs().size());
return this->get_output_usage_impl(index);
}
inline void Params::set_input_unused(const int index)
{
BLI_assert(index >= 0 && index < fn_.inputs().size());
this->assert_valid_thread();
this->set_input_unused_impl(index);
}
template<typename T> inline T Params::extract_input(const int index)
{
this->assert_valid_thread();
void *data = this->try_get_input_data_ptr(index);
BLI_assert(data != nullptr);
T return_value = std::move(*static_cast<T *>(data));
return return_value;
}
template<typename T> inline T &Params::get_input(const int index) const
{
void *data = this->try_get_input_data_ptr(index);
BLI_assert(data != nullptr);
return *static_cast<T *>(data);
}
template<typename T> inline T *Params::try_get_input_data_ptr(const int index) const
{
this->assert_valid_thread();
return static_cast<T *>(this->try_get_input_data_ptr(index));
}
template<typename T> inline T *Params::try_get_input_data_ptr_or_request(const int index)
{
this->assert_valid_thread();
return static_cast<T *>(this->try_get_input_data_ptr_or_request(index));
}
template<typename T> inline void Params::set_output(const int index, T &&value)
{
using DecayT = std::decay_t<T>;
this->assert_valid_thread();
void *data = this->get_output_data_ptr(index);
new (data) DecayT(std::forward<T>(value));
this->output_set(index);
}
inline bool Params::try_enable_multi_threading()
{
this->assert_valid_thread();
const bool success = this->try_enable_multi_threading_impl();
#ifdef FN_LAZY_FUNCTION_DEBUG_THREADS
if (success) {
allow_multi_threading_ = true;
}
#endif
return success;
}
inline void Params::assert_valid_thread() const
{
#ifdef FN_LAZY_FUNCTION_DEBUG_THREADS
if (allow_multi_threading_) {
return;
}
if (main_thread_id_ != std::this_thread::get_id()) {
BLI_assert_unreachable();
}
#endif
}
/** \} */
} // namespace fn::lazy_function
namespace lf = fn::lazy_function;
} // namespace blender

View File

@@ -0,0 +1,158 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* This file contains common utilities for actually executing a lazy-function.
*/
#include "BLI_generic_pointer.hh"
#include "FN_lazy_function.hh"
namespace blender::fn::lazy_function {
/**
* Most basic implementation of #Params. It does not actually implement any logic for how to
* retrieve inputs or set outputs. Instead, code using #BasicParams has to implement that.
*/
class BasicParams : public Params {
private:
const Span<GMutablePointer> inputs_;
const Span<GMutablePointer> outputs_;
MutableSpan<std::optional<ValueUsage>> input_usages_;
Span<ValueUsage> output_usages_;
MutableSpan<bool> set_outputs_;
public:
BasicParams(const LazyFunction &fn,
Span<GMutablePointer> inputs,
Span<GMutablePointer> outputs,
MutableSpan<std::optional<ValueUsage>> input_usages,
Span<ValueUsage> output_usages,
MutableSpan<bool> set_outputs);
void *try_get_input_data_ptr_impl(const int index) const override;
void *try_get_input_data_ptr_or_request_impl(const int index) override;
void *get_output_data_ptr_impl(const int index) override;
void output_set_impl(const int index) override;
bool output_was_set_impl(const int index) const override;
ValueUsage get_output_usage_impl(const int index) const override;
void set_input_unused_impl(const int index) override;
bool try_enable_multi_threading_impl() override;
};
/**
* Wraps an existing #Params. This should be used when a lazy-function internally contains another
* lazy-function that handles a subset or the inputs and outputs.
*/
class RemappedParams : public Params {
private:
Params &base_params_;
Span<int> input_map_;
Span<int> output_map_;
bool &multi_threading_enabled_;
public:
RemappedParams(const LazyFunction &fn,
Params &base_params,
Span<int> input_map,
Span<int> output_map,
bool &multi_threading_enabled);
void *try_get_input_data_ptr_impl(const int index) const override;
void *try_get_input_data_ptr_or_request_impl(const int index) override;
void *get_output_data_ptr_impl(const int index) override;
void output_set_impl(const int index) override;
bool output_was_set_impl(const int index) const override;
ValueUsage get_output_usage_impl(const int index) const override;
void set_input_unused_impl(const int index) override;
bool try_enable_multi_threading_impl() override;
};
namespace detail {
/**
* Utility to implement #execute_lazy_function_eagerly.
*/
template<typename... Inputs, typename... Outputs, size_t... InIndices, size_t... OutIndices>
inline void execute_lazy_function_eagerly_impl(const LazyFunction &fn,
UserData *user_data,
LocalUserData *local_user_data,
std::tuple<Inputs...> &inputs,
std::tuple<Outputs *...> &outputs,
std::index_sequence<InIndices...> /*in_indices*/,
std::index_sequence<OutIndices...> /*out_indices*/)
{
constexpr size_t InputsNum = sizeof...(Inputs);
constexpr size_t OutputsNum = sizeof...(Outputs);
std::array<GMutablePointer, InputsNum> input_pointers;
std::array<GMutablePointer, OutputsNum> output_pointers;
std::array<std::optional<ValueUsage>, InputsNum> input_usages;
std::array<ValueUsage, OutputsNum> output_usages;
std::array<bool, OutputsNum> set_outputs;
(
[&]() {
constexpr size_t I = InIndices;
/* Use `typedef` instead of `using` to work around a compiler bug. */
using T = Inputs;
const CPPType &type = CPPType::get<T>();
input_pointers[I] = {type, &std::get<I>(inputs)};
}(),
...);
(
[&]() {
constexpr size_t I = OutIndices;
/* Use `typedef` instead of `using` to work around a compiler bug. */
using T = Outputs;
const CPPType &type = CPPType::get<T>();
output_pointers[I] = {type, std::get<I>(outputs)};
}(),
...);
output_usages.fill(ValueUsage::Used);
set_outputs.fill(false);
LinearAllocator<> allocator;
Context context(fn.init_storage(allocator), user_data, local_user_data);
BasicParams params{
fn, input_pointers, output_pointers, input_usages, output_usages, set_outputs};
fn.execute(params, context);
fn.destruct_storage(context.storage);
/* Make sure all outputs have been computed. */
BLI_assert(!Span<bool>(set_outputs).contains(false));
}
} // namespace detail
/**
* In some cases (mainly for tests), the set of inputs and outputs for a lazy-function is known at
* compile time and one just wants to compute the outputs based on the inputs, without any
* laziness.
*
* This function does exactly that. It takes all inputs in a tuple and writes the outputs to points
* provided in a second tuple. Since all inputs have to be provided, the lazy-function has to
* compute all outputs.
*/
template<typename... Inputs, typename... Outputs>
inline void execute_lazy_function_eagerly(const LazyFunction &fn,
UserData *user_data,
LocalUserData *local_user_data,
std::tuple<Inputs...> inputs,
std::tuple<Outputs *...> outputs)
{
BLI_assert(fn.inputs().size() == sizeof...(Inputs));
BLI_assert(fn.outputs().size() == sizeof...(Outputs));
detail::execute_lazy_function_eagerly_impl(fn,
user_data,
local_user_data,
inputs,
outputs,
std::make_index_sequence<sizeof...(Inputs)>(),
std::make_index_sequence<sizeof...(Outputs)>());
}
} // namespace blender::fn::lazy_function

View File

@@ -0,0 +1,554 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* This file contains a graph data structure that allows composing multiple lazy-functions into a
* combined lazy-function.
*
* There are two types of nodes in the graph:
* - #FunctionNode: Corresponds to a #LazyFunction. The inputs and outputs of the function become
* input and output sockets of the node.
* - #InterfaceNode: Is used to indicate inputs and outputs of the entire graph. It can have an
* arbitrary number of sockets.
*/
#include "BLI_linear_allocator.hh"
#include "FN_lazy_function.hh"
namespace blender {
namespace dot_export {
class DirectedEdge;
}
namespace fn::lazy_function {
class Socket;
class InputSocket;
class OutputSocket;
class Node;
class Graph;
/**
* A #Socket is the interface of a #Node. Every #Socket is either an #InputSocket or #OutputSocket.
* Links can be created from output sockets to input sockets.
*/
class Socket : NonCopyable, NonMovable {
protected:
/**
* The node the socket belongs to.
*/
Node *node_;
/**
* Data type of the socket. Only sockets with the same type can be linked.
*/
const CPPType *type_;
/**
* Indicates whether this is an #InputSocket or #OutputSocket.
*/
bool is_input_;
/**
* Index of the socket. E.g. 0 for the first input and the first output socket.
*/
int index_in_node_;
/**
* Index of the socket in the entire graph. Every socket has a different index.
*/
int index_in_graph_;
friend Graph;
public:
bool is_input() const;
bool is_output() const;
int index() const;
int index_in_graph() const;
InputSocket &as_input();
OutputSocket &as_output();
const InputSocket &as_input() const;
const OutputSocket &as_output() const;
const Node &node() const;
Node &node();
const CPPType &type() const;
std::string name() const;
std::string detailed_name() const;
};
class InputSocket : public Socket {
private:
/**
* An input can have at most one link connected to it. The linked socket is the "origin" because
* it's where the data is coming from. The type of the origin must be the same as the type of
* this socket.
*/
OutputSocket *origin_;
/**
* Can be null or a non-owning pointer to a value of the type of the socket. This value will be
* used when the input is used but not linked.
*
* This is technically not needed, because one could just create a separate node that just
* outputs the value, but that would have more overhead. Especially because it's commonly the
* case that most inputs are unlinked.
*/
const void *default_value_ = nullptr;
friend Graph;
public:
OutputSocket *origin();
const OutputSocket *origin() const;
const void *default_value() const;
void set_default_value(const void *value);
};
class OutputSocket : public Socket {
private:
/**
* An output can be linked to an arbitrary number of inputs of the same type.
*/
Vector<InputSocket *> targets_;
friend Graph;
public:
Span<InputSocket *> targets();
Span<const InputSocket *> targets() const;
};
/**
* A #Node has input and output sockets. Every node is either a #FunctionNode or an #InterfaceNode.
*/
class Node : NonCopyable, NonMovable {
protected:
/**
* The function this node corresponds to. If this is null, the node is an #InterfaceNode.
* The function is not owned by this #Node nor by the #Graph.
*/
const LazyFunction *fn_ = nullptr;
/**
* Input sockets of the node.
*/
Span<InputSocket *> inputs_;
/**
* Output sockets of the node.
*/
Span<OutputSocket *> outputs_;
/**
* An index that is set when calling #Graph::update_node_indices. This can be used to create
* efficient mappings from nodes to other data using just an array instead of a hash map.
*
* This is technically not necessary but has better performance than always using hash maps.
*/
int index_in_graph_ = -1;
friend Graph;
public:
bool is_interface() const;
bool is_function() const;
int index_in_graph() const;
Span<const InputSocket *> inputs() const;
Span<const OutputSocket *> outputs() const;
Span<InputSocket *> inputs();
Span<OutputSocket *> outputs();
const InputSocket &input(int index) const;
const OutputSocket &output(int index) const;
InputSocket &input(int index);
OutputSocket &output(int index);
std::string name() const;
};
/**
* A #Node that corresponds to a specific #LazyFunction.
*/
class FunctionNode final : public Node {
public:
const LazyFunction &function() const;
};
/**
* A #Node that does *not* correspond to a #LazyFunction. Instead it can be used to indicate inputs
* and outputs of the entire graph. It can have an arbitrary number of inputs and outputs.
*/
class InterfaceNode final : public Node {
private:
friend Node;
friend Socket;
friend Graph;
Vector<std::string> socket_names_;
};
/**
* Interface input sockets are actually output sockets on the input node. This renaming makes the
* code less confusing.
*/
using GraphInputSocket = OutputSocket;
using GraphOutputSocket = InputSocket;
/**
* A container for an arbitrary number of nodes and links between their sockets.
*/
class Graph : NonCopyable, NonMovable {
private:
/**
* Used to allocate nodes and sockets in the graph.
*/
LinearAllocator<> allocator_;
/**
* Name of the graph for debugging purposes.
*/
StringRefNull name_;
/**
* Contains all nodes in the graph so that it is efficient to iterate over them.
* The first two nodes are the interface input and output nodes.
*/
Vector<Node *> nodes_;
InterfaceNode *graph_input_node_ = nullptr;
InterfaceNode *graph_output_node_ = nullptr;
Vector<GraphInputSocket *> graph_inputs_;
Vector<GraphOutputSocket *> graph_outputs_;
/**
* Number of sockets in the graph. Can be used as array size when indexing using
* `Socket::index_in_graph`.
*/
int socket_num_ = 0;
public:
Graph(StringRef name = "unknown");
~Graph();
StringRefNull name() const;
/**
* Get all nodes in the graph. The index in the span corresponds to #Node::index_in_graph.
*/
Span<const Node *> nodes() const;
Span<Node *> nodes();
Span<const FunctionNode *> function_nodes() const;
Span<FunctionNode *> function_nodes();
Span<GraphInputSocket *> graph_inputs();
Span<GraphOutputSocket *> graph_outputs();
Span<const GraphInputSocket *> graph_inputs() const;
Span<const GraphOutputSocket *> graph_outputs() const;
/**
* Add a new function node with sockets that match the passed in #LazyFunction.
*/
FunctionNode &add_function(const LazyFunction &fn);
/**
* Add inputs and outputs to the graph.
*/
GraphInputSocket &add_input(const CPPType &type, std::string name = "");
GraphOutputSocket &add_output(const CPPType &type, std::string name = "");
/**
* Add a link between the two given sockets.
* This has undefined behavior when the input is linked to something else already.
*/
void add_link(OutputSocket &from, InputSocket &to);
/**
* If the socket is linked, remove the link.
*/
void clear_origin(InputSocket &socket);
/**
* Make sure that #Node::index_in_graph is up to date.
*/
void update_node_indices();
/**
* Make sure that #Socket::index_in_graph is up to date.
*/
void update_socket_indices();
/**
* Number of sockets in the graph.
*/
int socket_num() const;
/**
* Can be used to assert that #update_node_indices has been called.
*/
bool node_indices_are_valid() const;
/**
* Optional configuration options for the dot graph generation. This allows creating
* visualizations for specific purposes.
*/
class ToDotOptions {
public:
virtual std::string socket_name(const Socket &socket) const;
virtual std::optional<std::string> socket_font_color(const Socket &socket) const;
virtual void add_edge_attributes(const OutputSocket &from,
const InputSocket &to,
dot_export::DirectedEdge &dot_edge) const;
};
/**
* Utility to generate a dot graph string for the graph. This can be used for debugging.
*/
std::string to_dot(const ToDotOptions &options = {}) const;
};
/* -------------------------------------------------------------------- */
/** \name #Socket Inline Methods
* \{ */
inline bool Socket::is_input() const
{
return is_input_;
}
inline bool Socket::is_output() const
{
return !is_input_;
}
inline int Socket::index() const
{
return index_in_node_;
}
inline int Socket::index_in_graph() const
{
return index_in_graph_;
}
inline InputSocket &Socket::as_input()
{
BLI_assert(this->is_input());
return *static_cast<InputSocket *>(this);
}
inline OutputSocket &Socket::as_output()
{
BLI_assert(this->is_output());
return *static_cast<OutputSocket *>(this);
}
inline const InputSocket &Socket::as_input() const
{
BLI_assert(this->is_input());
return *static_cast<const InputSocket *>(this);
}
inline const OutputSocket &Socket::as_output() const
{
BLI_assert(this->is_output());
return *static_cast<const OutputSocket *>(this);
}
inline const Node &Socket::node() const
{
return *node_;
}
inline Node &Socket::node()
{
return *node_;
}
inline const CPPType &Socket::type() const
{
return *type_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #InputSocket Inline Methods
* \{ */
inline const OutputSocket *InputSocket::origin() const
{
return origin_;
}
inline OutputSocket *InputSocket::origin()
{
return origin_;
}
inline const void *InputSocket::default_value() const
{
return default_value_;
}
inline void InputSocket::set_default_value(const void *value)
{
default_value_ = value;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #OutputSocket Inline Methods
* \{ */
inline Span<const InputSocket *> OutputSocket::targets() const
{
return targets_;
}
inline Span<InputSocket *> OutputSocket::targets()
{
return targets_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #Node Inline Methods
* \{ */
inline bool Node::is_interface() const
{
return fn_ == nullptr;
}
inline bool Node::is_function() const
{
return fn_ != nullptr;
}
inline int Node::index_in_graph() const
{
return index_in_graph_;
}
inline Span<const InputSocket *> Node::inputs() const
{
return inputs_;
}
inline Span<const OutputSocket *> Node::outputs() const
{
return outputs_;
}
inline Span<InputSocket *> Node::inputs()
{
return inputs_;
}
inline Span<OutputSocket *> Node::outputs()
{
return outputs_;
}
inline const InputSocket &Node::input(const int index) const
{
return *inputs_[index];
}
inline const OutputSocket &Node::output(const int index) const
{
return *outputs_[index];
}
inline InputSocket &Node::input(const int index)
{
return *inputs_[index];
}
inline OutputSocket &Node::output(const int index)
{
return *outputs_[index];
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #FunctionNode Inline Methods
* \{ */
inline const LazyFunction &FunctionNode::function() const
{
BLI_assert(fn_ != nullptr);
return *fn_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #Graph Inline Methods
* \{ */
inline StringRefNull Graph::name() const
{
return name_;
}
inline Span<const Node *> Graph::nodes() const
{
return nodes_;
}
inline Span<Node *> Graph::nodes()
{
return nodes_;
}
inline Span<const FunctionNode *> Graph::function_nodes() const
{
return nodes_.as_span().drop_front(2).cast<const FunctionNode *>();
}
inline Span<FunctionNode *> Graph::function_nodes()
{
return nodes_.as_span().drop_front(2).cast<FunctionNode *>();
}
inline Span<GraphInputSocket *> Graph::graph_inputs()
{
return graph_inputs_;
}
inline Span<GraphOutputSocket *> Graph::graph_outputs()
{
return graph_outputs_;
}
inline Span<const GraphInputSocket *> Graph::graph_inputs() const
{
return graph_inputs_;
}
inline Span<const GraphOutputSocket *> Graph::graph_outputs() const
{
return graph_outputs_;
}
inline int Graph::socket_num() const
{
return socket_num_;
}
/** \} */
} // namespace fn::lazy_function
} // namespace blender

View File

@@ -0,0 +1,155 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* This file provides means to create a #LazyFunction from #Graph (which could then e.g. be used in
* another #Graph again).
*/
#include "BLI_generic_pointer.hh"
#include "BLI_vector.hh"
#include "FN_lazy_function_graph.hh"
#include "FN_lazy_function_graph_executor_generic.hh"
namespace blender::fn::lazy_function {
/**
* Can be implemented to log values produced during graph evaluation.
*/
class GraphExecutorLogger {
public:
virtual ~GraphExecutorLogger() = default;
struct LoggingEnabledState {
bool socket_values = true;
bool before_node_execute = true;
bool after_node_execute = true;
explicit LoggingEnabledState(const bool enabled)
: socket_values(enabled), before_node_execute(enabled), after_node_execute(enabled)
{
}
};
virtual LoggingEnabledState get_logging_enabled_state(const Context &context) const;
virtual void log_socket_value(const Socket &socket,
GPointer value,
const Context &context) const;
virtual void log_before_node_execute(const FunctionNode &node,
const Params &params,
const Context &context) const;
virtual void log_after_node_execute(const FunctionNode &node,
const Params &params,
const Context &context) const;
virtual void dump_when_outputs_are_missing(const FunctionNode &node,
Span<const OutputSocket *> missing_sockets,
const Context &context) const;
virtual void dump_when_input_is_set_twice(const InputSocket &target_socket,
const OutputSocket &from_socket,
const Context &context) const;
};
/**
* Has to be implemented when some of the nodes in the graph may have side effects. The
* #GraphExecutor has to know about that to make sure that these nodes will be executed even though
* their outputs are not needed.
*/
class GraphExecutorSideEffectProvider {
public:
virtual ~GraphExecutorSideEffectProvider() = default;
virtual Vector<const FunctionNode *> get_nodes_with_side_effects(const Context &context) const;
};
/**
* Can be used to pass extra context into the execution of a function. The main alternative to this
* is to create a wrapper `LazyFunction` for the `FunctionNode`s. Using this light weight wrapper
* is preferable if possible.
*/
class GraphExecutorNodeExecuteWrapper {
public:
virtual ~GraphExecutorNodeExecuteWrapper() = default;
/**
* Is expected to run `node.function().execute(params, context)` but might do some extra work,
* like adjusting the context.
*/
virtual void execute_node(const FunctionNode &node,
Params &params,
const Context &context) const = 0;
};
class GraphExecutor : public LazyFunction {
public:
using Logger = GraphExecutorLogger;
using SideEffectProvider = GraphExecutorSideEffectProvider;
using NodeExecuteWrapper = GraphExecutorNodeExecuteWrapper;
using GenericExecutor = generic_graph_executor::GenericGraphExecutor;
private:
/**
* The graph that is evaluated.
*/
const Graph &graph_;
/**
* Input and output sockets of the entire graph.
*/
Vector<const GraphInputSocket *> graph_inputs_;
Vector<const GraphOutputSocket *> graph_outputs_;
Array<int> graph_input_index_by_socket_index_;
Array<int> graph_output_index_by_socket_index_;
/**
* Optional logger for events that happen during execution.
*/
const Logger *logger_;
/**
* Optional side effect provider. It knows which nodes have side effects based on the context
* during evaluation.
*/
const SideEffectProvider *side_effect_provider_;
/**
* Optional wrapper for node execution functions.
*/
const NodeExecuteWrapper *node_execute_wrapper_;
/**
* The graph executor implementation does some preprocessing for the graph. This only has to be
* done once even if the graph is executed multiple times.
*/
generic_graph_executor::PreprocessData preprocess_data_;
friend GenericExecutor;
public:
GraphExecutor(const Graph &graph,
const Logger *logger,
const SideEffectProvider *side_effect_provider,
const NodeExecuteWrapper *node_execute_wrapper);
GraphExecutor(const Graph &graph,
Vector<const GraphInputSocket *> graph_inputs,
Vector<const GraphOutputSocket *> graph_outputs,
const Logger *logger,
const SideEffectProvider *side_effect_provider,
const NodeExecuteWrapper *node_execute_wrapper);
void *init_storage(LinearAllocator<> &allocator) const override;
void destruct_storage(void *storage) const override;
std::string input_name(int index) const override;
std::string output_name(int index) const override;
private:
void execute_impl(Params &params, const Context &context) const override;
};
} // namespace blender::fn::lazy_function

View File

@@ -0,0 +1,25 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_array.hh"
namespace blender::fn::lazy_function::generic_graph_executor {
class GenericGraphExecutor;
/**
* When a graph is executed, various things have to be allocated (e.g. the state of all nodes).
* Instead of doing many small allocations, a single bigger allocation is done. This struct
* contains the preprocessed offsets into that bigger buffer.
*/
struct PreprocessData {
int node_states_array_offset;
int loaded_inputs_array_offset;
Array<int> node_states_offsets;
int total_size;
};
} // namespace blender::fn::lazy_function::generic_graph_executor

View File

@@ -0,0 +1,152 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* A `MultiFunction` encapsulates a function that is optimized for throughput (instead of latency).
* The throughput is optimized by always processing many elements at once, instead of each element
* separately. This is ideal for functions that are evaluated often (e.g. for every particle).
*
* By processing a lot of data at once, individual functions become easier to optimize for humans
* and for the compiler. Furthermore, performance profiles become easier to understand and show
* better where bottlenecks are.
*
* Every multi-function has a name and an ordered list of parameters. Parameters are used for input
* and output. In fact, there are three kinds of parameters: inputs, outputs and mutable (which is
* combination of input and output).
*
* To call a multi-function, one has to provide three things:
* - `Params`: This references the input and output arrays that the function works with. The
* arrays are not owned by Params.
* - `IndexMask`: An array of indices indicating which indices in the provided arrays should be
* touched/processed.
* - `Context`: Further information for the called function.
*
* A new multi-function is generally implemented as follows:
* 1. Create a new subclass of MultiFunction.
* 2. Implement a constructor that initialized the signature of the function.
* 3. Override the `call` function.
*/
#include "BLI_unique_hash.hh"
#include "FN_multi_function_context.hh"
#include "FN_multi_function_params.hh"
namespace blender {
namespace fn::multi_function {
class MultiFunction : NonCopyable, NonMovable {
private:
const Signature *signature_ref_ = nullptr;
public:
virtual ~MultiFunction() = default;
/**
* The result is the same as using #call directly but this method has some additional features.
* - Automatic multi-threading when possible and appropriate.
* - Automatic index mask offsetting to avoid large temporary intermediate arrays that are mostly
* unused.
*/
void call_auto(const IndexMask &mask, Params params, Context context) const;
virtual void call(const IndexMask &mask, Params params, Context context) const = 0;
virtual void hash_unique(UniqueHashBytes &hash) const;
virtual bool equals(const MultiFunction &other) const;
int param_amount() const
{
return signature_ref_->params.size();
}
IndexRange param_indices() const
{
return signature_ref_->params.index_range();
}
ParamType param_type(int param_index) const
{
return signature_ref_->params[param_index].type;
}
StringRefNull param_name(int param_index) const
{
return signature_ref_->params[param_index].name;
}
StringRefNull name() const
{
return signature_ref_->function_name;
}
virtual std::string debug_name() const;
const Signature &signature() const
{
BLI_assert(signature_ref_ != nullptr);
return *signature_ref_;
}
/**
* Information about how the multi-function behaves that help a caller to execute it efficiently.
*/
struct ExecutionHints {
/**
* Suggested minimum workload under which multi-threading does not really help.
* This should be lowered when the multi-function is doing something computationally expensive.
*/
int64_t min_grain_size = 10000;
/**
* Indicates that the multi-function will allocate an array large enough to hold all indices
* passed in as mask. This tells the caller that it would be preferable to pass in smaller
* indices. Also maybe the full mask should be split up into smaller segments to decrease peak
* memory usage.
*/
bool allocates_array = false;
/**
* Tells the caller that every execution takes about the same time. This helps making a more
* educated guess about a good grain size.
*/
bool uniform_execution_time = true;
};
ExecutionHints execution_hints() const;
/**
* For performance reasons it might make sense to delay construction of data inside the node
* until we can be sure that the function will be evaluated. This method should be called before
* execution. The work must be protected by a lock though, since it may be called from multiple
* threads.
*/
virtual void prepare_for_execution() const {}
protected:
/* Make the function use the given signature. This should be called once in the constructor of
* child classes. No copy of the signature is made, so the caller has to make sure that the
* signature lives as long as the multi function. It is ok to embed the signature into the child
* class. */
void set_signature(const Signature *signature)
{
/* Take a pointer as argument, so that it is more obvious that no copy is created. */
BLI_assert(signature != nullptr);
signature_ref_ = signature;
}
virtual ExecutionHints get_execution_hints() const;
};
inline ParamsBuilder::ParamsBuilder(const MultiFunction &fn, const IndexMask *mask)
: ParamsBuilder(fn.signature(), *mask)
{
}
} // namespace fn::multi_function
namespace mf = fn::multi_function;
} // namespace blender

View File

@@ -0,0 +1,922 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* This file contains several utilities to create multi-functions with less redundant code.
*/
#include "FN_multi_function.hh"
namespace blender {
namespace fn::multi_function::build {
/**
* These presets determine what code is generated for a #CustomMF. Different presets make different
* trade-offs between run-time performance and compile-time/binary size.
*/
namespace exec_presets {
/** Method to execute a function in case devirtualization was not possible. */
enum class FallbackMode {
/** Access all elements in virtual arrays through virtual function calls. */
Simple,
/** Process elements in chunks to reduce virtual function call overhead. */
Materialized,
};
/**
* The "naive" method for executing a #CustomMF. Every element is processed separately and input
* values are retrieved from the virtual arrays one by one. This generates the least amount of
* code, but is also the slowest method.
*/
struct Simple {
static constexpr bool use_devirtualization = false;
static constexpr FallbackMode fallback_mode = FallbackMode::Simple;
};
/**
* This is an improvement over the #Simple method. It still generates a relatively small amount of
* code, because the function is only instantiated once. It's generally faster than #Simple,
* because inputs are retrieved from the virtual arrays in chunks, reducing virtual method call
* overhead.
*/
struct Materialized {
static constexpr bool use_devirtualization = false;
static constexpr FallbackMode fallback_mode = FallbackMode::Materialized;
};
/**
* The most efficient preset, but also potentially generates a lot of code (exponential in the
* number of inputs of the function). It generates separate optimized loops for all combinations of
* inputs. This should be used for small functions of which all inputs are likely to be single
* values or spans, and the number of inputs is relatively small.
*/
struct AllSpanOrSingle {
static constexpr bool use_devirtualization = true;
static constexpr FallbackMode fallback_mode = FallbackMode::Materialized;
template<typename... ParamTags, typename... LoadedParams, size_t... I>
auto create_devirtualizers(TypeSequence<ParamTags...> /*param_tags*/,
std::index_sequence<I...> /*indices*/,
const std::tuple<LoadedParams...> &loaded_params) const
{
return std::make_tuple([&]() {
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
if constexpr (ParamTag::category == ParamCategory::SingleInput) {
const GVArrayImpl &varray_impl = *std::get<I>(loaded_params);
return GVArrayDevirtualizer<T, true, true>{varray_impl};
}
else if constexpr (ELEM(ParamTag::category,
ParamCategory::SingleOutput,
ParamCategory::SingleMutable))
{
T *ptr = std::get<I>(loaded_params);
return BasicDevirtualizer<T *>{ptr};
}
}()...);
}
};
/**
* A slightly weaker variant of #AllSpanOrSingle. It generates less code, because it assumes that
* some of the inputs are most likely single values. It should be used for small functions which
* have too many inputs to make #AllSingleOrSpan a reasonable choice.
*/
template<size_t... Indices> struct SomeSpanOrSingle {
static constexpr bool use_devirtualization = true;
static constexpr FallbackMode fallback_mode = FallbackMode::Materialized;
template<typename... ParamTags, typename... LoadedParams, size_t... I>
auto create_devirtualizers(TypeSequence<ParamTags...> /*param_tags*/,
std::index_sequence<I...> /*indices*/,
const std::tuple<LoadedParams...> &loaded_params) const
{
return std::make_tuple([&]() {
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
if constexpr (ParamTag::category == ParamCategory::SingleInput) {
constexpr bool UseSpan = ValueSequence<size_t, Indices...>::template contains<I>();
const GVArrayImpl &varray_impl = *std::get<I>(loaded_params);
return GVArrayDevirtualizer<T, true, UseSpan>{varray_impl};
}
else if constexpr (ELEM(ParamTag::category,
ParamCategory::SingleOutput,
ParamCategory::SingleMutable))
{
T *ptr = std::get<I>(loaded_params);
return BasicDevirtualizer<T *>{ptr};
}
}()...);
}
};
} // namespace exec_presets
namespace detail {
/**
* Executes #element_fn for all indices in the mask. The passed in #args contain the input as well
* as output parameters. Usually types in #args are devirtualized (e.g. a `Span<int>` is passed in
* instead of a `VArray<int>`).
*/
template<typename MaskT, typename... Args, typename ElementFn>
/* Perform additional optimizations on this loop because it is a very hot loop. For example, the
* math node in geometry nodes is processed here. */
#if (defined(__GNUC__) && !defined(__clang__))
[[gnu::optimize("-funroll-loops")]] [[gnu::optimize("O3")]]
#endif
inline void execute_array(const ElementFn &element_fn,
MaskT mask,
/* Use restrict to tell the compiler that pointer inputs do not alias
* each other. This is important for some compiler optimizations. */
Args &&__restrict... args)
{
if constexpr (std::is_same_v<std::decay_t<MaskT>, IndexRange>) {
/* Having this explicit loop is necessary for MSVC to be able to vectorize this. */
const int64_t start = mask.start();
const int64_t end = mask.one_after_last();
for (int64_t i = start; i < end; i++) {
element_fn(args[i]...);
}
}
else {
for (const int64_t i : mask) {
element_fn(args[i]...);
}
}
}
enum class MaterializeArgMode {
Unknown,
Single,
Span,
Materialized,
};
template<typename ParamTag> struct MaterializeArgInfo {
MaterializeArgMode mode = MaterializeArgMode::Unknown;
const typename ParamTag::base_type *internal_span_data;
};
/**
* Similar to #execute_array but is only used with arrays and does not need a mask.
*/
template<typename... ParamTags, typename ElementFn, typename... Chunks>
#if (defined(__GNUC__) && !defined(__clang__))
[[gnu::optimize("-funroll-loops")]] [[gnu::optimize("O3")]]
#endif
inline void execute_materialized_impl(TypeSequence<ParamTags...> /*param_tags*/,
const ElementFn &element_fn,
const int64_t size,
Chunks &&__restrict... chunks)
{
for (int64_t i = 0; i < size; i++) {
element_fn(chunks[i]...);
}
}
/**
* Executes #element_fn for all indices in #mask. However, instead of processing every element
* separately, processing happens in chunks. This allows retrieving from input virtual arrays in
* chunks, which reduces virtual function call overhead.
*/
template<typename... ParamTags, size_t... I, typename ElementFn, typename... LoadedParams>
inline void execute_materialized(TypeSequence<ParamTags...> /*param_tags*/,
std::index_sequence<I...> /*indices*/,
const ElementFn &element_fn,
const IndexMaskSegment mask,
const std::tuple<LoadedParams...> &loaded_params)
{
/* In theory, all elements could be processed in one chunk. However, that has the disadvantage
* that large temporary arrays are needed. Using small chunks allows using small arrays, which
* are reused multiple times, which improves cache efficiency. The chunk size also shouldn't be
* too small, because then overhead of the outer loop over chunks becomes significant again. */
static constexpr int64_t MaxChunkSize = 64;
const int64_t mask_size = mask.size();
const int64_t tmp_buffer_size = std::min(mask_size, MaxChunkSize);
/* Local buffers that are used to temporarily store values for processing. */
std::tuple<TypedBuffer<typename ParamTags::base_type, MaxChunkSize>...> temporary_buffers;
/* Information about every parameter. */
std::tuple<MaterializeArgInfo<ParamTags>...> args_info;
(
/* Setup information for all parameters. */
[&] {
/* Use `typedef` instead of `using` to work around a compiler bug. */
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
[[maybe_unused]] MaterializeArgInfo<ParamTags> &arg_info = std::get<I>(args_info);
if constexpr (ParamTag::category == ParamCategory::SingleInput) {
const GVArrayImpl &varray_impl = *std::get<I>(loaded_params);
const CommonVArrayInfo common_info = varray_impl.common_info();
if (common_info.type == CommonVArrayInfo::Type::Single) {
/* If an input #VArray is a single value, we have to fill the buffer with that value
* only once. The same unchanged buffer can then be reused in every chunk. */
const T &in_single = *static_cast<const T *>(common_info.data);
T *tmp_buffer = std::get<I>(temporary_buffers).ptr();
uninitialized_fill_n(tmp_buffer, tmp_buffer_size, in_single);
arg_info.mode = MaterializeArgMode::Single;
}
else if (common_info.type == CommonVArrayInfo::Type::Span) {
/* Remember the span so that it doesn't have to be retrieved in every iteration. */
arg_info.internal_span_data = static_cast<const T *>(common_info.data);
}
else {
arg_info.internal_span_data = nullptr;
}
}
}(),
...);
IndexMaskFromSegment index_mask_from_segment;
const int64_t segment_offset = mask.offset();
/* Outer loop over all chunks. */
for (int64_t chunk_start = 0; chunk_start < mask_size; chunk_start += MaxChunkSize) {
const int64_t chunk_end = std::min<int64_t>(chunk_start + MaxChunkSize, mask_size);
const int64_t chunk_size = chunk_end - chunk_start;
const IndexMaskSegment sliced_mask = mask.slice(chunk_start, chunk_size);
const int64_t mask_start = sliced_mask[0];
const bool sliced_mask_is_range = unique_sorted_indices::non_empty_is_range(
sliced_mask.base_span());
/* Move mutable data into temporary array. */
if (!sliced_mask_is_range) {
(
[&] {
/* Use `typedef` instead of `using` to work around a compiler bug. */
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
if constexpr (ParamTag::category == ParamCategory::SingleMutable) {
T *tmp_buffer = std::get<I>(temporary_buffers).ptr();
T *param_buffer = std::get<I>(loaded_params);
for (int64_t i = 0; i < chunk_size; i++) {
new (tmp_buffer + i) T(std::move(param_buffer[sliced_mask[i]]));
}
}
}(),
...);
}
const IndexMask *current_segment_mask = nullptr;
execute_materialized_impl(
TypeSequence<ParamTags...>(),
element_fn,
chunk_size,
/* Prepare every parameter for this chunk. */
[&] {
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
[[maybe_unused]] MaterializeArgInfo<ParamTags> &arg_info = std::get<I>(args_info);
T *tmp_buffer = std::get<I>(temporary_buffers);
if constexpr (ParamTag::category == ParamCategory::SingleInput) {
if (arg_info.mode == MaterializeArgMode::Single) {
/* The single value has been filled into a buffer already reused for every chunk. */
return const_cast<const T *>(tmp_buffer);
}
if (sliced_mask_is_range && arg_info.internal_span_data != nullptr) {
/* In this case we can just use an existing span instead of "compressing" it into
* a new temporary buffer. */
arg_info.mode = MaterializeArgMode::Span;
return arg_info.internal_span_data + mask_start;
}
const GVArrayImpl &varray_impl = *std::get<I>(loaded_params);
if (current_segment_mask == nullptr) {
current_segment_mask = &index_mask_from_segment.update(
{segment_offset, sliced_mask.base_span()});
}
/* As a fallback, do a virtual function call to retrieve all elements in the current
* chunk. The elements are stored in a temporary buffer reused for every chunk. */
varray_impl.materialize_compressed(*current_segment_mask, tmp_buffer, true);
/* Remember that this parameter has been materialized, so that the values are
* destructed properly when the chunk is done. */
arg_info.mode = MaterializeArgMode::Materialized;
return const_cast<const T *>(tmp_buffer);
}
else if constexpr (ELEM(ParamTag::category,
ParamCategory::SingleOutput,
ParamCategory::SingleMutable))
{
/* For outputs, just pass a pointer. This is important so that `__restrict` works. */
if (sliced_mask_is_range) {
/* Can write into the caller-provided buffer directly. */
T *param_buffer = std::get<I>(loaded_params);
return param_buffer + mask_start;
}
/* Use the temporary buffer. The values will have to be copied out of that
* buffer into the caller-provided buffer afterwards. */
return tmp_buffer;
}
}()...);
/* Relocate outputs from temporary buffers to buffers provided by caller. */
if (!sliced_mask_is_range) {
(
[&] {
/* Use `typedef` instead of `using` to work around a compiler bug. */
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
if constexpr (ELEM(ParamTag::category,
ParamCategory::SingleOutput,
ParamCategory::SingleMutable))
{
T *tmp_buffer = std::get<I>(temporary_buffers).ptr();
T *param_buffer = std::get<I>(loaded_params);
for (int64_t i = 0; i < chunk_size; i++) {
new (param_buffer + sliced_mask[i]) T(std::move(tmp_buffer[i]));
std::destroy_at(tmp_buffer + i);
}
}
}(),
...);
}
(
/* Destruct values that have been materialized before. */
[&] {
/* Use `typedef` instead of `using` to work around a compiler bug. */
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
[[maybe_unused]] MaterializeArgInfo<ParamTags> &arg_info = std::get<I>(args_info);
if constexpr (ParamTag::category == ParamCategory::SingleInput) {
if (arg_info.mode == MaterializeArgMode::Materialized) {
T *tmp_buffer = std::get<I>(temporary_buffers).ptr();
destruct_n(tmp_buffer, chunk_size);
}
}
}(),
...);
}
(
/* Destruct buffers for single value inputs. */
[&] {
/* Use `typedef` instead of `using` to work around a compiler bug. */
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
[[maybe_unused]] MaterializeArgInfo<ParamTags> &arg_info = std::get<I>(args_info);
if constexpr (ParamTag::category == ParamCategory::SingleInput) {
if (arg_info.mode == MaterializeArgMode::Single) {
T *tmp_buffer = std::get<I>(temporary_buffers).ptr();
destruct_n(tmp_buffer, tmp_buffer_size);
}
}
}(),
...);
}
template<typename ElementFn, typename ExecPreset, typename... ParamTags, size_t... I>
inline void execute_element_fn_as_multi_function(const ElementFn &element_fn,
const ExecPreset exec_preset,
const IndexMask &mask,
Params params,
TypeSequence<ParamTags...> /*param_tags*/,
std::index_sequence<I...> /*indices*/)
{
/* Load parameters from #Params. */
/* Contains `const GVArrayImpl *` for inputs and `T *` for outputs. */
const auto loaded_params = std::make_tuple([&]() {
/* Use `typedef` instead of `using` to work around a compiler bug. */
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
if constexpr (ParamTag::category == ParamCategory::SingleInput) {
return params.readonly_single_input(I).get_implementation();
}
else if constexpr (ParamTag::category == ParamCategory::SingleOutput) {
return static_cast<T *>(params.uninitialized_single_output(I).data());
}
else if constexpr (ParamTag::category == ParamCategory::SingleMutable) {
return static_cast<T *>(params.single_mutable(I).data());
}
}()...);
/* Try execute devirtualized if enabled and the input types allow it. */
bool executed_devirtualized = false;
if constexpr (ExecPreset::use_devirtualization) {
/* Get segments before devirtualization to avoid generating this code multiple times. */
const Vector<std::variant<IndexRange, IndexMaskSegment>, 16> mask_segments =
mask.to_spans_and_ranges<16>();
const auto devirtualizers = exec_preset.create_devirtualizers(
TypeSequence<ParamTags...>(), std::index_sequence<I...>(), loaded_params);
executed_devirtualized = call_with_devirtualized_parameters(
devirtualizers, [&](auto &&...args) {
for (const std::variant<IndexRange, IndexMaskSegment> &segment : mask_segments) {
if (std::holds_alternative<IndexRange>(segment)) {
const auto segment_range = std::get<IndexRange>(segment);
execute_array(element_fn, segment_range, std::forward<decltype(args)>(args)...);
}
else {
const auto segment_indices = std::get<IndexMaskSegment>(segment);
execute_array(element_fn, segment_indices, std::forward<decltype(args)>(args)...);
}
}
});
}
else {
UNUSED_VARS(exec_preset);
}
/* If devirtualized execution was disabled or not possible, use a fallback method which is
* slower but always works. */
if (!executed_devirtualized) {
/* The materialized method is most common because it avoids most virtual function overhead but
* still instantiates the function only once. */
if constexpr (ExecPreset::fallback_mode == exec_presets::FallbackMode::Materialized) {
mask.foreach_segment([&](const IndexMaskSegment segment) {
execute_materialized(TypeSequence<ParamTags...>(),
std::index_sequence<I...>(),
element_fn,
segment,
loaded_params);
});
}
else {
/* This fallback is slower because it uses virtual method calls for every element. */
mask.foreach_segment([&](const IndexMaskSegment segment) {
execute_array(element_fn, segment, [&]() {
/* Use `typedef` instead of `using` to work around a compiler bug. */
using ParamTag = ParamTags;
using T = typename ParamTag::base_type;
if constexpr (ParamTag::category == ParamCategory::SingleInput) {
const GVArrayImpl &varray_impl = *std::get<I>(loaded_params);
return GVArray(&varray_impl).typed<T>();
}
else if constexpr (ELEM(ParamTag::category,
ParamCategory::SingleOutput,
ParamCategory::SingleMutable))
{
T *ptr = std::get<I>(loaded_params);
return ptr;
}
}()...);
});
}
}
}
/**
* `element_fn` is expected to return nothing and to have the following parameters:
* - For single-inputs: const value or reference.
* - For single-mutables: non-const reference.
* - For single-outputs: non-const pointer.
*/
template<typename ElementFn, typename ExecPreset, typename... ParamTags>
inline auto build_multi_function_call_from_element_fn(ElementFn &&element_fn,
const ExecPreset exec_preset,
TypeSequence<ParamTags...> /*param_tags*/)
{
return [element_fn = std::forward<ElementFn>(element_fn), exec_preset](const IndexMask &mask,
Params params) {
execute_element_fn_as_multi_function(element_fn,
exec_preset,
mask,
params,
TypeSequence<ParamTags...>(),
std::make_index_sequence<sizeof...(ParamTags)>());
};
}
/**
* A multi function that just invokes the provided function in its #call method.
*/
template<typename CallFn, typename... ParamTags> class CustomMF : public MultiFunction {
private:
Signature signature_;
CallFn call_fn_;
public:
CustomMF(const char *name, CallFn call_fn, TypeSequence<ParamTags...> /*param_tags*/)
: call_fn_(std::move(call_fn))
{
SignatureBuilder builder{name, signature_};
/* Loop over all parameter types and add an entry for each in the signature. */
([&] { builder.add(ParamTags(), ""); }(), ...);
this->set_signature(&signature_);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
call_fn_(mask, params);
}
};
template<typename Out, typename... In, typename ElementFn, typename ExecPreset>
inline auto build_multi_function_with_n_inputs_one_output(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset,
TypeSequence<In...> /*in_types*/)
{
constexpr auto param_tags = TypeSequence<ParamTag<ParamCategory::SingleInput, In>...,
ParamTag<ParamCategory::SingleOutput, Out>>();
auto call_fn = build_multi_function_call_from_element_fn(
[element_fn = std::forward<ElementFn>(element_fn)](const In &...in, Out &out) {
new (&out) Out(element_fn(in...));
},
exec_preset,
param_tags);
return CustomMF(name, std::move(call_fn), param_tags);
}
template<typename Out1, typename Out2, typename... In, typename ElementFn, typename ExecPreset>
inline auto build_multi_function_with_n_inputs_two_outputs(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset,
TypeSequence<In...> /*in_types*/)
{
constexpr auto param_tags = TypeSequence<ParamTag<ParamCategory::SingleInput, In>...,
ParamTag<ParamCategory::SingleOutput, Out1>,
ParamTag<ParamCategory::SingleOutput, Out2>>();
auto call_fn = build_multi_function_call_from_element_fn(
std::forward<ElementFn>(element_fn), exec_preset, param_tags);
return CustomMF(name, call_fn, param_tags);
}
} // namespace detail
/** Build multi-function with 1 single-input and 1 single-output parameter. */
template<typename In1,
typename Out1,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI1_SO(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_one_output<Out1>(
name, std::forward<ElementFn>(element_fn), exec_preset, TypeSequence<In1>());
}
/** Build multi-function with 2 single-input and 1 single-output parameter. */
template<typename In1,
typename In2,
typename Out1,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI2_SO(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_one_output<Out1>(
name, std::forward<ElementFn>(element_fn), exec_preset, TypeSequence<In1, In2>());
}
/** Build multi-function with 3 single-input and 1 single-output parameter. */
template<typename In1,
typename In2,
typename In3,
typename Out1,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI3_SO(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_one_output<Out1>(
name, std::forward<ElementFn>(element_fn), exec_preset, TypeSequence<In1, In2, In3>());
}
/** Build multi-function with 4 single-input and 1 single-output parameter. */
template<typename In1,
typename In2,
typename In3,
typename In4,
typename Out1,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI4_SO(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_one_output<Out1>(
name, std::forward<ElementFn>(element_fn), exec_preset, TypeSequence<In1, In2, In3, In4>());
}
/** Build multi-function with 5 single-input and 1 single-output parameter. */
template<typename In1,
typename In2,
typename In3,
typename In4,
typename In5,
typename Out1,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI5_SO(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_one_output<Out1>(
name,
std::forward<ElementFn>(element_fn),
exec_preset,
TypeSequence<In1, In2, In3, In4, In5>());
}
/** Build multi-function with 6 single-input and 1 single-output parameter. */
template<typename In1,
typename In2,
typename In3,
typename In4,
typename In5,
typename In6,
typename Out1,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI6_SO(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_one_output<Out1>(
name,
std::forward<ElementFn>(element_fn),
exec_preset,
TypeSequence<In1, In2, In3, In4, In5, In6>());
}
/** Build multi-function with 8 single-input and 1 single-output parameter. */
template<typename In1,
typename In2,
typename In3,
typename In4,
typename In5,
typename In6,
typename In7,
typename In8,
typename Out1,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI8_SO(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_one_output<Out1>(
name,
std::forward<ElementFn>(element_fn),
exec_preset,
TypeSequence<In1, In2, In3, In4, In5, In6, In7, In8>());
}
/** Build multi-function with 1 single-mutable parameter. */
template<typename Mut1, typename ElementFn, typename ExecPreset = exec_presets::AllSpanOrSingle>
inline auto SM(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::AllSpanOrSingle())
{
constexpr auto param_tags = TypeSequence<ParamTag<ParamCategory::SingleMutable, Mut1>>();
auto call_fn = detail::build_multi_function_call_from_element_fn(
std::forward<ElementFn>(element_fn), exec_preset, param_tags);
return detail::CustomMF(name, call_fn, param_tags);
}
/** Build multi-function with 1 single-input and 2 single-output parameter. */
template<typename In1,
typename Out1,
typename Out2,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI1_SO2(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_two_outputs<Out1, Out2>(
name, std::forward<ElementFn>(element_fn), exec_preset, TypeSequence<In1>());
}
/** Build multi-function with 2 single-input and 2 single-output parameter. */
template<typename In1,
typename In2,
typename Out1,
typename Out2,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI2_SO2(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_two_outputs<Out1, Out2>(
name, std::forward<ElementFn>(element_fn), exec_preset, TypeSequence<In1, In2>());
}
/** Build multi-function with 3 single-input and 2 single-output parameter. */
template<typename In1,
typename In2,
typename In3,
typename Out1,
typename Out2,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI3_SO2(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_two_outputs<Out1, Out2>(
name, std::forward<ElementFn>(element_fn), exec_preset, TypeSequence<In1, In2, In3>());
}
/** Build multi-function with 4 single-input and 2 single-output parameter. */
template<typename In1,
typename In2,
typename In3,
typename In4,
typename Out1,
typename Out2,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI4_SO2(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_two_outputs<Out1, Out2>(
name, std::forward<ElementFn>(element_fn), exec_preset, TypeSequence<In1, In2, In3, In4>());
}
/** Build multi-function with 5 single-input and 2 single-output parameter. */
template<typename In1,
typename In2,
typename In3,
typename In4,
typename In5,
typename Out1,
typename Out2,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI5_SO2(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_two_outputs<Out1, Out2>(
name,
std::forward<ElementFn>(element_fn),
exec_preset,
TypeSequence<In1, In2, In3, In4, In5>());
}
/** Build multi-function with 1 single-input and 3 single output parameter. */
template<typename In1,
typename Out1,
typename Out2,
typename Out3,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI1_SO3(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
constexpr auto param_tags = TypeSequence<ParamTag<ParamCategory::SingleInput, In1>,
ParamTag<ParamCategory::SingleOutput, Out1>,
ParamTag<ParamCategory::SingleOutput, Out2>,
ParamTag<ParamCategory::SingleOutput, Out3>>();
auto call_fn = detail::build_multi_function_call_from_element_fn(
std::forward<ElementFn>(element_fn), exec_preset, param_tags);
return detail::CustomMF(name, call_fn, param_tags);
}
/** Build multi-function with 1 single-input and 4 single output parameter. */
template<typename In1,
typename Out1,
typename Out2,
typename Out3,
typename Out4,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI1_SO4(const char *name,
ElementFn &&element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
constexpr auto param_tags = TypeSequence<ParamTag<ParamCategory::SingleInput, In1>,
ParamTag<ParamCategory::SingleOutput, Out1>,
ParamTag<ParamCategory::SingleOutput, Out2>,
ParamTag<ParamCategory::SingleOutput, Out3>,
ParamTag<ParamCategory::SingleOutput, Out4>>();
auto call_fn = detail::build_multi_function_call_from_element_fn(
std::forward<ElementFn>(element_fn), exec_preset, param_tags);
return detail::CustomMF(name, call_fn, param_tags);
}
} // namespace fn::multi_function::build
namespace fn::multi_function {
/**
* A multi-function that outputs the same value every time. The value is not owned by an instance
* of this function. If #make_value_copy is false, the caller is responsible for destructing and
* freeing the value.
*/
class CustomMF_GenericConstant : public MultiFunction {
public:
/* For compatible hash with typed class. */
static constexpr int8_t HASH_ID = 0;
private:
const CPPType &type_;
const void *value_;
Signature signature_;
bool owns_value_;
template<typename T> friend class CustomMF_Constant;
public:
CustomMF_GenericConstant(const CPPType &type, const void *value, bool make_value_copy);
~CustomMF_GenericConstant() override;
void call(const IndexMask &mask, Params params, Context context) const override;
void hash_unique(UniqueHashBytes &hash) const override;
bool equals(const MultiFunction &other) const override;
};
/**
* A multi-function that outputs the same array every time. The array is not owned by in instance
* of this function. The caller is responsible for destructing and freeing the values.
*/
class CustomMF_GenericConstantArray : public MultiFunction {
private:
GSpan array_;
Signature signature_;
public:
CustomMF_GenericConstantArray(GSpan array);
void call(const IndexMask &mask, Params params, Context context) const override;
};
/**
* Generates a multi-function that outputs a constant value.
*/
template<typename T> class CustomMF_Constant : public MultiFunction {
private:
T value_;
Signature signature_;
public:
template<typename U> CustomMF_Constant(U &&value) : value_(std::forward<U>(value))
{
SignatureBuilder builder{"Constant", signature_};
builder.single_output<T>("Value");
this->set_signature(&signature_);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
MutableSpan<T> output = params.uninitialized_single_output<T>(0);
mask.foreach_index_optimized<int64_t>([&](const int64_t i) { new (&output[i]) T(value_); });
}
void hash_unique(UniqueHashBytes &hash) const override
{
hash.add(&CustomMF_GenericConstant::HASH_ID);
hash_unique_default(value_, hash);
hash.add(&CPPType::get<T>());
}
bool equals(const MultiFunction &other) const override
{
const CustomMF_Constant *other1 = dynamic_cast<const CustomMF_Constant *>(&other);
if (other1 != nullptr) {
return value_ == other1->value_;
}
const CustomMF_GenericConstant *other2 = dynamic_cast<const CustomMF_GenericConstant *>(
&other);
if (other2 != nullptr) {
const CPPType &type = CPPType::get<T>();
if (type == other2->type_) {
return type.is_equal_or_false(static_cast<const void *>(&value_), other2->value_);
}
}
return false;
}
};
class CustomMF_DefaultOutput : public MultiFunction {
private:
int output_amount_;
Signature signature_;
public:
CustomMF_DefaultOutput(Span<DataType> input_types, Span<DataType> output_types);
void call(const IndexMask &mask, Params params, Context context) const override;
};
class CustomMF_GenericCopy : public MultiFunction {
private:
Signature signature_;
public:
CustomMF_GenericCopy(DataType data_type);
void call(const IndexMask &mask, Params params, Context context) const override;
};
} // namespace fn::multi_function
} // namespace blender

View File

@@ -0,0 +1,58 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* An #Context is passed along with every call to a multi-function. Right now it does nothing,
* but it can be used for the following purposes:
* - Pass debug information up and down the function call stack.
* - Pass reusable memory buffers to sub-functions to increase performance.
* - Pass cached data to called functions.
*/
#include "FN_user_data.hh"
namespace blender::fn::multi_function {
class Context;
class ContextBuilder;
class Context {
public:
/**
* Custom user data that can be used in the function.
*/
UserData *user_data = nullptr;
friend ContextBuilder;
private:
Context() = default;
public:
Context(ContextBuilder & /*builder*/);
};
class ContextBuilder {
private:
Context context_;
friend Context;
public:
void user_data(UserData *user_data)
{
context_.user_data = user_data;
}
};
inline Context::Context(ContextBuilder &builder)
{
*this = builder.context_;
}
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,131 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* A DataType describes what type of data a multi-function gets as input, outputs or mutates.
* Currently, only individual elements or vectors of elements are supported. Adding more data types
* is possible when necessary.
*/
#include "BLI_cpp_type.hh"
namespace blender::fn::multi_function {
class DataType {
public:
enum Category {
Single,
Vector,
};
private:
Category category_;
const CPPType *type_;
DataType(Category category, const CPPType &type);
public:
DataType() = default;
static DataType ForSingle(const CPPType &type);
static DataType ForVector(const CPPType &type);
template<typename T> static DataType ForSingle();
template<typename T> static DataType ForVector();
bool is_single() const;
bool is_vector() const;
Category category() const;
const CPPType &single_type() const;
const CPPType &vector_base_type() const;
friend bool operator==(const DataType &a, const DataType &b) = default;
std::string to_string() const;
uint64_t hash() const;
};
/* -------------------------------------------------------------------- */
/** \name #DataType Inline Methods
* \{ */
inline DataType::DataType(Category category, const CPPType &type)
: category_(category), type_(&type)
{
}
inline DataType DataType::ForSingle(const CPPType &type)
{
return DataType(Single, type);
}
inline DataType DataType::ForVector(const CPPType &type)
{
return DataType(Vector, type);
}
template<typename T> inline DataType DataType::ForSingle()
{
return DataType::ForSingle(CPPType::get<T>());
}
template<typename T> inline DataType DataType::ForVector()
{
return DataType::ForVector(CPPType::get<T>());
}
inline bool DataType::is_single() const
{
return category_ == Single;
}
inline bool DataType::is_vector() const
{
return category_ == Vector;
}
inline DataType::Category DataType::category() const
{
return category_;
}
inline const CPPType &DataType::single_type() const
{
BLI_assert(this->is_single());
return *type_;
}
inline const CPPType &DataType::vector_base_type() const
{
BLI_assert(this->is_vector());
return *type_;
}
inline std::string DataType::to_string() const
{
switch (category_) {
case Single:
return type_->name();
case Vector:
return type_->name() + " Vector";
}
BLI_assert(false);
return "";
}
inline uint64_t DataType::hash() const
{
return get_default_hash(*type_, category_);
}
/** \} */
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,170 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* A multi-function has an arbitrary amount of parameters. Every parameter belongs to one of three
* interface types:
* - Input: An input parameter is readonly inside the function. The values have to be provided by
* the caller.
* - Output: An output parameter has to be initialized by the function. However, the caller
* provides the memory where the data has to be constructed.
* - Mutable: A mutable parameter can be considered to be an input and output. The caller has to
* initialize the data, but the function is allowed to modify it.
*
* Furthermore, every parameter has a DataType that describes what kind of data is being passed
* around.
*/
#include "FN_multi_function_data_type.hh"
namespace blender::fn::multi_function {
enum class ParamCategory {
SingleInput,
VectorInput,
SingleOutput,
VectorOutput,
SingleMutable,
VectorMutable,
};
template<ParamCategory Category, typename T> struct ParamTag {
static constexpr ParamCategory category = Category;
using base_type = T;
};
class ParamType {
public:
enum InterfaceType {
Input,
Output,
Mutable,
};
private:
InterfaceType interface_type_;
DataType data_type_;
public:
ParamType(InterfaceType interface_type, DataType data_type);
static ParamType ForSingleInput(const CPPType &type);
static ParamType ForVectorInput(const CPPType &base_type);
static ParamType ForSingleOutput(const CPPType &type);
static ParamType ForVectorOutput(const CPPType &base_type);
static ParamType ForMutableSingle(const CPPType &type);
static ParamType ForMutableVector(const CPPType &base_type);
const DataType &data_type() const;
InterfaceType interface_type() const;
ParamCategory category() const;
bool is_input_or_mutable() const;
bool is_output_or_mutable() const;
bool is_output() const;
friend bool operator==(const ParamType &a, const ParamType &b) = default;
};
/* -------------------------------------------------------------------- */
/** \name #ParamType Inline Methods
* \{ */
inline ParamType::ParamType(InterfaceType interface_type, DataType data_type)
: interface_type_(interface_type), data_type_(data_type)
{
}
inline ParamType ParamType::ForSingleInput(const CPPType &type)
{
return ParamType(InterfaceType::Input, DataType::ForSingle(type));
}
inline ParamType ParamType::ForVectorInput(const CPPType &base_type)
{
return ParamType(InterfaceType::Input, DataType::ForVector(base_type));
}
inline ParamType ParamType::ForSingleOutput(const CPPType &type)
{
return ParamType(InterfaceType::Output, DataType::ForSingle(type));
}
inline ParamType ParamType::ForVectorOutput(const CPPType &base_type)
{
return ParamType(InterfaceType::Output, DataType::ForVector(base_type));
}
inline ParamType ParamType::ForMutableSingle(const CPPType &type)
{
return ParamType(InterfaceType::Mutable, DataType::ForSingle(type));
}
inline ParamType ParamType::ForMutableVector(const CPPType &base_type)
{
return ParamType(InterfaceType::Mutable, DataType::ForVector(base_type));
}
inline const DataType &ParamType::data_type() const
{
return data_type_;
}
inline ParamType::InterfaceType ParamType::interface_type() const
{
return interface_type_;
}
inline ParamCategory ParamType::category() const
{
switch (data_type_.category()) {
case DataType::Single: {
switch (interface_type_) {
case Input:
return ParamCategory::SingleInput;
case Output:
return ParamCategory::SingleOutput;
case Mutable:
return ParamCategory::SingleMutable;
}
break;
}
case DataType::Vector: {
switch (interface_type_) {
case Input:
return ParamCategory::VectorInput;
case Output:
return ParamCategory::VectorOutput;
case Mutable:
return ParamCategory::VectorMutable;
}
break;
}
}
BLI_assert_unreachable();
return ParamCategory::SingleInput;
}
inline bool ParamType::is_input_or_mutable() const
{
return ELEM(interface_type_, Input, Mutable);
}
inline bool ParamType::is_output_or_mutable() const
{
return ELEM(interface_type_, Output, Mutable);
}
inline bool ParamType::is_output() const
{
return interface_type_ == Output;
}
/** \} */
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,456 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* This file provides an Params and ParamsBuilder structure.
*
* `ParamsBuilder` is used by a function caller to be prepare all parameters that are passed into
* the function. `Params` is then used inside the called function to access the parameters.
*/
#include <variant>
#include "BLI_generic_pointer.hh"
#include "BLI_generic_vector_array.hh"
#include "BLI_generic_virtual_vector_array.hh"
#include "BLI_resource_scope.hh"
#include "FN_multi_function_signature.hh"
namespace blender::fn::multi_function {
class ParamsBuilder {
private:
std::unique_ptr<ResourceScope> scope_;
const Signature *signature_;
const IndexMask &mask_;
int64_t min_array_size_;
Vector<std::variant<GVArray, GMutableSpan, const GVVectorArray *, GVectorArray *>>
actual_params_;
friend class Params;
ParamsBuilder(const Signature &signature, const IndexMask &mask);
public:
/**
* 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.
*/
ParamsBuilder(const class MultiFunction &fn, const IndexMask *mask);
template<typename T> void add_readonly_single_input_value(T value, StringRef expected_name = "");
template<typename T>
void add_readonly_single_input(const T *value, StringRef expected_name = "");
void add_readonly_single_input(const GSpan span, StringRef expected_name = "");
void add_readonly_single_input(GPointer value, StringRef expected_name = "");
void add_readonly_single_input(GVArray varray, StringRef expected_name = "");
void add_readonly_vector_input(const GVectorArray &vector_array, StringRef expected_name = "");
void add_readonly_vector_input(const GSpan single_vector, StringRef expected_name = "");
void add_readonly_vector_input(const GVVectorArray &ref, StringRef expected_name = "");
template<typename T>
void add_uninitialized_single_output(T *value, StringRef expected_name = "");
void add_uninitialized_single_output(GMutableSpan ref, StringRef expected_name = "");
void add_ignored_single_output(StringRef expected_name = "");
void add_vector_output(GVectorArray &vector_array, StringRef expected_name = "");
void add_single_mutable(GMutableSpan ref, StringRef expected_name = "");
void add_vector_mutable(GVectorArray &vector_array, StringRef expected_name = "");
int next_param_index() const;
GMutableSpan computed_array(int param_index);
GVectorArray &computed_vector_array(int param_index);
private:
void assert_current_param_type(ParamType param_type, StringRef expected_name = "");
void assert_current_param_name(StringRef expected_name);
ResourceScope &resource_scope();
void add_unused_output_for_unsupporting_function(const CPPType &type);
};
class Params {
private:
ParamsBuilder *builder_;
public:
Params(ParamsBuilder &builder) : builder_(&builder) {}
template<typename T> VArray<T> readonly_single_input(int param_index, StringRef name = "");
const GVArray &readonly_single_input(int param_index, StringRef name = "");
/**
* \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 = "");
template<typename T>
MutableSpan<T> uninitialized_single_output(int param_index, StringRef name = "");
GMutableSpan uninitialized_single_output(int param_index, StringRef name = "");
/**
* Same as #uninitialized_single_output, but returns an empty span when the output is not
* required.
*/
template<typename T>
MutableSpan<T> uninitialized_single_output_if_required(int param_index, StringRef name = "");
GMutableSpan uninitialized_single_output_if_required(int param_index, StringRef name = "");
template<typename T>
const VVectorArray<T> &readonly_vector_input(int param_index, StringRef name = "");
const GVVectorArray &readonly_vector_input(int param_index, StringRef name = "");
template<typename T>
GVectorArray_TypedMutableRef<T> vector_output(int param_index, StringRef name = "");
GVectorArray &vector_output(int param_index, StringRef name = "");
template<typename T> MutableSpan<T> single_mutable(int param_index, StringRef name = "");
GMutableSpan single_mutable(int param_index, StringRef name = "");
template<typename T>
GVectorArray_TypedMutableRef<T> vector_mutable(int param_index, StringRef name = "");
GVectorArray &vector_mutable(int param_index, StringRef name = "");
private:
void assert_correct_param(int param_index, StringRef name, ParamType param_type);
void assert_correct_param(int param_index, StringRef name, ParamCategory category);
};
/* -------------------------------------------------------------------- */
/** \name #Paramsbuilder Inline Methods
* \{ */
inline ParamsBuilder::ParamsBuilder(const Signature &signature, const IndexMask &mask)
: signature_(&signature), mask_(mask), min_array_size_(mask.min_array_size())
{
actual_params_.reserve(signature.params.size());
}
template<typename T>
inline void ParamsBuilder::add_readonly_single_input_value(T value, StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForSingleInput(CPPType::get<T>()), expected_name);
actual_params_.append_unchecked_as(std::in_place_type<GVArray>,
varray_tag::single{},
CPPType::get<T>(),
min_array_size_,
&value);
}
template<typename T>
inline void ParamsBuilder::add_readonly_single_input(const T *value, StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForSingleInput(CPPType::get<T>()), expected_name);
actual_params_.append_unchecked_as(std::in_place_type<GVArray>,
varray_tag::single_ref{},
CPPType::get<T>(),
min_array_size_,
value);
}
inline void ParamsBuilder::add_readonly_single_input(const GSpan span, StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForSingleInput(span.type()), expected_name);
BLI_assert(span.size() >= min_array_size_);
actual_params_.append_unchecked_as(std::in_place_type<GVArray>, varray_tag::span{}, span);
}
inline void ParamsBuilder::add_readonly_single_input(GPointer value, StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForSingleInput(*value.type()), expected_name);
actual_params_.append_unchecked_as(std::in_place_type<GVArray>,
varray_tag::single_ref{},
*value.type(),
min_array_size_,
value.get());
}
inline void ParamsBuilder::add_readonly_single_input(GVArray varray, StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForSingleInput(varray.type()), expected_name);
BLI_assert(varray.size() >= min_array_size_);
actual_params_.append_unchecked_as(std::in_place_type<GVArray>, std::move(varray));
}
inline void ParamsBuilder::add_readonly_vector_input(const GVectorArray &vector_array,
StringRef expected_name)
{
this->add_readonly_vector_input(
this->resource_scope().construct<GVVectorArray_For_GVectorArray>(vector_array),
expected_name);
}
inline void ParamsBuilder::add_readonly_vector_input(const GSpan single_vector,
StringRef expected_name)
{
this->add_readonly_vector_input(this->resource_scope().construct<GVVectorArray_For_SingleGSpan>(
single_vector, min_array_size_),
expected_name);
}
inline void ParamsBuilder::add_readonly_vector_input(const GVVectorArray &ref,
StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForVectorInput(ref.type()), expected_name);
BLI_assert(ref.size() >= min_array_size_);
actual_params_.append_unchecked_as(std::in_place_type<const GVVectorArray *>, &ref);
}
template<typename T>
inline void ParamsBuilder::add_uninitialized_single_output(T *value, StringRef expected_name)
{
this->add_uninitialized_single_output(GMutableSpan(CPPType::get<T>(), value, 1), expected_name);
}
inline void ParamsBuilder::add_uninitialized_single_output(GMutableSpan ref,
StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForSingleOutput(ref.type()), expected_name);
BLI_assert(ref.size() >= min_array_size_);
actual_params_.append_unchecked_as(std::in_place_type<GMutableSpan>, ref);
}
inline void ParamsBuilder::add_ignored_single_output(StringRef expected_name)
{
this->assert_current_param_name(expected_name);
const int param_index = this->next_param_index();
const ParamType &param_type = signature_->params[param_index].type;
BLI_assert(param_type.category() == ParamCategory::SingleOutput);
const DataType data_type = param_type.data_type();
const CPPType &type = data_type.single_type();
if (flag_is_set(signature_->params[param_index].flag, ParamFlag::SupportsUnusedOutput)) {
/* An empty span indicates that this is ignored. */
const GMutableSpan dummy_span{type};
actual_params_.append_unchecked_as(std::in_place_type<GMutableSpan>, dummy_span);
}
else {
this->add_unused_output_for_unsupporting_function(type);
}
}
inline void ParamsBuilder::add_vector_output(GVectorArray &vector_array, StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForVectorOutput(vector_array.type()), expected_name);
BLI_assert(vector_array.size() >= min_array_size_);
actual_params_.append_unchecked_as(std::in_place_type<GVectorArray *>, &vector_array);
}
inline void ParamsBuilder::add_single_mutable(GMutableSpan ref, StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForMutableSingle(ref.type()), expected_name);
BLI_assert(ref.size() >= min_array_size_);
actual_params_.append_unchecked_as(std::in_place_type<GMutableSpan>, ref);
}
inline void ParamsBuilder::add_vector_mutable(GVectorArray &vector_array, StringRef expected_name)
{
this->assert_current_param_type(ParamType::ForMutableVector(vector_array.type()), expected_name);
BLI_assert(vector_array.size() >= min_array_size_);
actual_params_.append_unchecked_as(std::in_place_type<GVectorArray *>, &vector_array);
}
inline int ParamsBuilder::next_param_index() const
{
return actual_params_.size();
}
inline GMutableSpan ParamsBuilder::computed_array(int param_index)
{
BLI_assert(ELEM(signature_->params[param_index].type.category(),
ParamCategory::SingleOutput,
ParamCategory::SingleMutable));
return std::get<GMutableSpan>(actual_params_[param_index]);
}
inline GVectorArray &ParamsBuilder::computed_vector_array(int param_index)
{
BLI_assert(ELEM(signature_->params[param_index].type.category(),
ParamCategory::VectorOutput,
ParamCategory::VectorMutable));
return *std::get<GVectorArray *>(actual_params_[param_index]);
}
inline void ParamsBuilder::assert_current_param_type(ParamType param_type, StringRef expected_name)
{
UNUSED_VARS_NDEBUG(param_type, expected_name);
#ifndef NDEBUG
int param_index = this->next_param_index();
if (expected_name != "") {
StringRef actual_name = signature_->params[param_index].name;
BLI_assert(actual_name == expected_name);
}
ParamType expected_type = signature_->params[param_index].type;
BLI_assert(expected_type == param_type);
#endif
}
inline void ParamsBuilder::assert_current_param_name(StringRef expected_name)
{
UNUSED_VARS_NDEBUG(expected_name);
#ifndef NDEBUG
if (expected_name.is_empty()) {
return;
}
const int param_index = this->next_param_index();
StringRef actual_name = signature_->params[param_index].name;
BLI_assert(actual_name == expected_name);
#endif
}
inline ResourceScope &ParamsBuilder::resource_scope()
{
if (!scope_) {
scope_ = std::make_unique<ResourceScope>();
}
return *scope_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #Params Inline Methods
* \{ */
template<typename T>
inline VArray<T> Params::readonly_single_input(int param_index, StringRef name)
{
const GVArray &varray = this->readonly_single_input(param_index, name);
return varray.typed<T>();
}
inline const GVArray &Params::readonly_single_input(int param_index, StringRef name)
{
this->assert_correct_param(param_index, name, ParamCategory::SingleInput);
return std::get<GVArray>(builder_->actual_params_[param_index]);
}
inline bool Params::single_output_is_required(int param_index, StringRef name)
{
this->assert_correct_param(param_index, name, ParamCategory::SingleOutput);
return !std::get<GMutableSpan>(builder_->actual_params_[param_index]).is_empty();
}
template<typename T>
inline MutableSpan<T> Params::uninitialized_single_output(int param_index, StringRef name)
{
return this->uninitialized_single_output(param_index, name).typed<T>();
}
inline GMutableSpan Params::uninitialized_single_output(int param_index, StringRef name)
{
this->assert_correct_param(param_index, name, ParamCategory::SingleOutput);
BLI_assert(!flag_is_set(builder_->signature_->params[param_index].flag,
ParamFlag::SupportsUnusedOutput));
GMutableSpan span = std::get<GMutableSpan>(builder_->actual_params_[param_index]);
BLI_assert(span.size() >= builder_->min_array_size_);
return span;
}
template<typename T>
inline MutableSpan<T> Params::uninitialized_single_output_if_required(int param_index,
StringRef name)
{
return this->uninitialized_single_output_if_required(param_index, name).typed<T>();
}
inline GMutableSpan Params::uninitialized_single_output_if_required(int param_index,
StringRef name)
{
this->assert_correct_param(param_index, name, ParamCategory::SingleOutput);
BLI_assert(flag_is_set(builder_->signature_->params[param_index].flag,
ParamFlag::SupportsUnusedOutput));
return std::get<GMutableSpan>(builder_->actual_params_[param_index]);
}
template<typename T>
inline const VVectorArray<T> &Params::readonly_vector_input(int param_index, StringRef name)
{
const GVVectorArray &vector_array = this->readonly_vector_input(param_index, name);
return builder_->resource_scope().construct<VVectorArray_For_GVVectorArray<T>>(vector_array);
}
inline const GVVectorArray &Params::readonly_vector_input(int param_index, StringRef name)
{
this->assert_correct_param(param_index, name, ParamCategory::VectorInput);
return *std::get<const GVVectorArray *>(builder_->actual_params_[param_index]);
}
template<typename T>
inline GVectorArray_TypedMutableRef<T> Params::vector_output(int param_index, StringRef name)
{
return {this->vector_output(param_index, name)};
}
inline GVectorArray &Params::vector_output(int param_index, StringRef name)
{
this->assert_correct_param(param_index, name, ParamCategory::VectorOutput);
return *std::get<GVectorArray *>(builder_->actual_params_[param_index]);
}
template<typename T> inline MutableSpan<T> Params::single_mutable(int param_index, StringRef name)
{
return this->single_mutable(param_index, name).typed<T>();
}
inline GMutableSpan Params::single_mutable(int param_index, StringRef name)
{
this->assert_correct_param(param_index, name, ParamCategory::SingleMutable);
return std::get<GMutableSpan>(builder_->actual_params_[param_index]);
}
template<typename T>
inline GVectorArray_TypedMutableRef<T> Params::vector_mutable(int param_index, StringRef name)
{
return {this->vector_mutable(param_index, name)};
}
inline GVectorArray &Params::vector_mutable(int param_index, StringRef name)
{
this->assert_correct_param(param_index, name, ParamCategory::VectorMutable);
return *std::get<GVectorArray *>(builder_->actual_params_[param_index]);
}
inline void Params::assert_correct_param(int param_index, StringRef name, ParamType param_type)
{
UNUSED_VARS_NDEBUG(param_index, name, param_type);
#ifndef NDEBUG
BLI_assert(builder_->signature_->params[param_index].type == param_type);
if (name.size() > 0) {
BLI_assert(builder_->signature_->params[param_index].name == name);
}
#endif
}
inline void Params::assert_correct_param(int param_index, StringRef name, ParamCategory category)
{
UNUSED_VARS_NDEBUG(param_index, name, category);
#ifndef NDEBUG
BLI_assert(builder_->signature_->params[param_index].type.category() == category);
if (name.size() > 0) {
BLI_assert(builder_->signature_->params[param_index].name == name);
}
#endif
}
/** \} */
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,536 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*/
#include "FN_multi_function.hh"
namespace blender::fn::multi_function {
class Variable;
class Instruction;
class CallInstruction;
class BranchInstruction;
class DestructInstruction;
class DummyInstruction;
class ReturnInstruction;
class Procedure;
/** Every instruction has exactly one of these types. */
enum class InstructionType {
Call,
Branch,
Destruct,
Dummy,
Return,
};
/**
* An #InstructionCursor points to a position in a multi-function procedure, where an instruction
* can be inserted.
*/
class InstructionCursor {
public:
enum Type {
None,
Entry,
Call,
Destruct,
Branch,
Dummy,
};
private:
Type type_ = None;
Instruction *instruction_ = nullptr;
/* Only used when it is a branch instruction. */
bool branch_output_ = false;
public:
InstructionCursor() = default;
InstructionCursor(CallInstruction &instruction);
InstructionCursor(DestructInstruction &instruction);
InstructionCursor(BranchInstruction &instruction, bool branch_output);
InstructionCursor(DummyInstruction &instruction);
static InstructionCursor ForEntry();
Instruction *next(Procedure &procedure) const;
void set_next(Procedure &procedure, Instruction *new_instruction) const;
Instruction *instruction() const;
Type type() const;
friend bool operator==(const InstructionCursor &a, const InstructionCursor &b) = default;
};
/**
* A variable is similar to a virtual register in other libraries. During evaluation, every is
* either uninitialized or contains a value for every index (remember, a multi-function procedure
* is always evaluated for many indices at the same time).
*/
class Variable : NonCopyable, NonMovable {
private:
DataType data_type_;
Vector<Instruction *> users_;
std::string name_;
int index_in_graph_;
friend Procedure;
friend CallInstruction;
friend BranchInstruction;
friend DestructInstruction;
public:
DataType data_type() const;
Span<Instruction *> users();
StringRefNull name() const;
void set_name(std::string name);
int index_in_procedure() const;
};
/** Base class for all instruction types. */
class Instruction : NonCopyable, NonMovable {
protected:
InstructionType type_;
Vector<InstructionCursor> prev_;
friend Procedure;
friend CallInstruction;
friend BranchInstruction;
friend DestructInstruction;
friend DummyInstruction;
friend ReturnInstruction;
public:
InstructionType type() const;
/**
* Other instructions that come before this instruction. There can be multiple previous
* instructions when branching is used in the procedure.
*/
Span<InstructionCursor> prev() const;
};
/**
* References a multi-function that is evaluated when the instruction is executed. It also
* references the variables whose data will be passed into the multi-function.
*/
class CallInstruction : public Instruction {
private:
const MultiFunction *fn_ = nullptr;
Instruction *next_ = nullptr;
MutableSpan<Variable *> params_;
friend Procedure;
public:
const MultiFunction &fn() const;
Instruction *next();
const Instruction *next() const;
void set_next(Instruction *instruction);
void set_param_variable(int param_index, Variable *variable);
void set_params(Span<Variable *> variables);
Span<Variable *> params();
Span<const Variable *> params() const;
};
/**
* What makes a branch instruction special is that it has two successor instructions. One that will
* be used when a condition variable was true, and one otherwise.
*/
class BranchInstruction : public Instruction {
private:
Variable *condition_ = nullptr;
Instruction *branch_true_ = nullptr;
Instruction *branch_false_ = nullptr;
friend Procedure;
public:
Variable *condition();
const Variable *condition() const;
void set_condition(Variable *variable);
Instruction *branch_true();
const Instruction *branch_true() const;
void set_branch_true(Instruction *instruction);
Instruction *branch_false();
const Instruction *branch_false() const;
void set_branch_false(Instruction *instruction);
};
/**
* A destruct instruction destructs a single variable. So the variable value will be uninitialized
* after this instruction. All variables that are not output variables of the procedure, have to be
* destructed before the procedure ends. Destructing early is generally a good thing, because it
* might help with memory buffer reuse, which decreases memory-usage and increases performance.
*/
class DestructInstruction : public Instruction {
private:
Variable *variable_ = nullptr;
Instruction *next_ = nullptr;
friend Procedure;
public:
Variable *variable();
const Variable *variable() const;
void set_variable(Variable *variable);
Instruction *next();
const Instruction *next() const;
void set_next(Instruction *instruction);
};
/**
* This instruction does nothing, it just exists to building a procedure simpler in some cases.
*/
class DummyInstruction : public Instruction {
private:
Instruction *next_ = nullptr;
friend Procedure;
public:
Instruction *next();
const Instruction *next() const;
void set_next(Instruction *instruction);
};
/**
* This instruction ends the procedure.
*/
class ReturnInstruction : public Instruction {};
/**
* Inputs and outputs of the entire procedure network.
*/
struct Parameter {
ParamType::InterfaceType type;
Variable *variable;
};
struct ConstParameter {
ParamType::InterfaceType type;
const Variable *variable;
};
/**
* A multi-function procedure allows composing multi-functions in arbitrary ways. It consists of
* variables and instructions that operate on those variables. Branching and looping within the
* procedure is supported as well.
*
* Typically, a #Procedure should be constructed using a #ProcedureBuilder, which has many more
* utility methods for common use cases.
*/
class Procedure : NonCopyable, NonMovable {
private:
LinearAllocator<> allocator_;
Vector<CallInstruction *> call_instructions_;
Vector<BranchInstruction *> branch_instructions_;
Vector<DestructInstruction *> destruct_instructions_;
Vector<DummyInstruction *> dummy_instructions_;
Vector<ReturnInstruction *> return_instructions_;
Vector<Variable *> variables_;
Vector<Parameter> params_;
Vector<destruct_ptr<MultiFunction>> owned_functions_;
Instruction *entry_ = nullptr;
friend class ProcedureDotExport;
public:
Procedure() = default;
~Procedure();
Variable &new_variable(DataType data_type, std::string name = "");
CallInstruction &new_call_instruction(const MultiFunction &fn);
BranchInstruction &new_branch_instruction();
DestructInstruction &new_destruct_instruction();
DummyInstruction &new_dummy_instruction();
ReturnInstruction &new_return_instruction();
void add_parameter(ParamType::InterfaceType interface_type, Variable &variable);
Span<ConstParameter> params() const;
template<typename T, typename... Args> const MultiFunction &construct_function(Args &&...args);
Instruction *entry();
const Instruction *entry() const;
void set_entry(Instruction &entry);
Span<Variable *> variables();
Span<const Variable *> variables() const;
std::string to_dot() const;
bool validate() const;
void prepare_for_execution();
private:
bool validate_all_instruction_pointers_set() const;
bool validate_all_params_provided() const;
bool validate_same_variables_in_one_call() const;
bool validate_parameters() const;
bool validate_initialization() const;
struct InitState {
bool can_be_initialized = false;
bool can_be_uninitialized = false;
};
InitState find_initialization_state_before_instruction(const Instruction &target_instruction,
const Variable &variable) const;
};
/* -------------------------------------------------------------------- */
/** \name #InstructionCursor Inline Methods
* \{ */
inline InstructionCursor::InstructionCursor(CallInstruction &instruction)
: type_(Call), instruction_(&instruction)
{
}
inline InstructionCursor::InstructionCursor(DestructInstruction &instruction)
: type_(Destruct), instruction_(&instruction)
{
}
inline InstructionCursor::InstructionCursor(BranchInstruction &instruction, bool branch_output)
: type_(Branch), instruction_(&instruction), branch_output_(branch_output)
{
}
inline InstructionCursor::InstructionCursor(DummyInstruction &instruction)
: type_(Dummy), instruction_(&instruction)
{
}
inline InstructionCursor InstructionCursor::ForEntry()
{
InstructionCursor cursor;
cursor.type_ = Type::Entry;
return cursor;
}
inline Instruction *InstructionCursor::instruction() const
{
/* This isn't really const correct unfortunately, because to make it correct we'll need a const
* version of #InstructionCursor. */
return instruction_;
}
inline InstructionCursor::Type InstructionCursor::type() const
{
return type_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #Variable Inline Methods
* \{ */
inline DataType Variable::data_type() const
{
return data_type_;
}
inline Span<Instruction *> Variable::users()
{
return users_;
}
inline StringRefNull Variable::name() const
{
return name_;
}
inline int Variable::index_in_procedure() const
{
return index_in_graph_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #Instruction Inline Methods
* \{ */
inline InstructionType Instruction::type() const
{
return type_;
}
inline Span<InstructionCursor> Instruction::prev() const
{
return prev_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #CallInstruction Inline Methods
* \{ */
inline const MultiFunction &CallInstruction::fn() const
{
return *fn_;
}
inline Instruction *CallInstruction::next()
{
return next_;
}
inline const Instruction *CallInstruction::next() const
{
return next_;
}
inline Span<Variable *> CallInstruction::params()
{
return params_;
}
inline Span<const Variable *> CallInstruction::params() const
{
return params_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #BranchInstruction Inline Methods
* \{ */
inline Variable *BranchInstruction::condition()
{
return condition_;
}
inline const Variable *BranchInstruction::condition() const
{
return condition_;
}
inline Instruction *BranchInstruction::branch_true()
{
return branch_true_;
}
inline const Instruction *BranchInstruction::branch_true() const
{
return branch_true_;
}
inline Instruction *BranchInstruction::branch_false()
{
return branch_false_;
}
inline const Instruction *BranchInstruction::branch_false() const
{
return branch_false_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #DestructInstruction Inline Methods
* \{ */
inline Variable *DestructInstruction::variable()
{
return variable_;
}
inline const Variable *DestructInstruction::variable() const
{
return variable_;
}
inline Instruction *DestructInstruction::next()
{
return next_;
}
inline const Instruction *DestructInstruction::next() const
{
return next_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #DummyInstruction Inline Methods
* \{ */
inline Instruction *DummyInstruction::next()
{
return next_;
}
inline const Instruction *DummyInstruction::next() const
{
return next_;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #Procedure Inline Methods
* \{ */
inline Span<ConstParameter> Procedure::params() const
{
static_assert(sizeof(Parameter) == sizeof(ConstParameter));
return params_.as_span().cast<ConstParameter>();
}
inline Instruction *Procedure::entry()
{
return entry_;
}
inline const Instruction *Procedure::entry() const
{
return entry_;
}
inline Span<Variable *> Procedure::variables()
{
return variables_;
}
inline Span<const Variable *> Procedure::variables() const
{
return variables_;
}
template<typename T, typename... Args>
inline const MultiFunction &Procedure::construct_function(Args &&...args)
{
destruct_ptr<T> fn = allocator_.construct<T>(std::forward<Args>(args)...);
const MultiFunction &fn_ref = *fn;
owned_functions_.append(std::move(fn));
return fn_ref;
}
/** \} */
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,190 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*/
#include "FN_multi_function_procedure.hh"
namespace blender::fn::multi_function {
/**
* Utility class to build a #Procedure.
*/
class ProcedureBuilder {
private:
/** Procedure that is being build. */
Procedure *procedure_ = nullptr;
/** Cursors where the next instruction should be inserted. */
Vector<InstructionCursor> cursors_;
public:
struct Branch;
struct Loop;
ProcedureBuilder(Procedure &procedure,
InstructionCursor initial_cursor = InstructionCursor::ForEntry());
ProcedureBuilder(Span<ProcedureBuilder *> builders);
ProcedureBuilder(Branch &branch);
void set_cursor(const InstructionCursor &cursor);
void set_cursor(Span<InstructionCursor> cursors);
void set_cursor(Span<ProcedureBuilder *> builders);
void set_cursor_after_branch(Branch &branch);
void set_cursor_after_loop(Loop &loop);
void add_destruct(Variable &variable);
void add_destruct(Span<Variable *> variables);
ReturnInstruction &add_return();
Branch add_branch(Variable &condition);
Loop add_loop();
void add_loop_continue(Loop &loop);
void add_loop_break(Loop &loop);
CallInstruction &add_call_with_no_variables(const MultiFunction &fn);
CallInstruction &add_call_with_all_variables(const MultiFunction &fn,
Span<Variable *> param_variables);
Vector<Variable *> add_call(const MultiFunction &fn,
Span<Variable *> input_and_mutable_variables = {});
template<int OutputN>
std::array<Variable *, OutputN> add_call(const MultiFunction &fn,
Span<Variable *> input_and_mutable_variables = {});
void add_parameter(ParamType::InterfaceType interface_type, Variable &variable);
Variable &add_parameter(ParamType param_type, std::string name = "");
Variable &add_input_parameter(DataType data_type, std::string name = "");
template<typename T> Variable &add_single_input_parameter(std::string name = "");
template<typename T> Variable &add_single_mutable_parameter(std::string name = "");
void add_output_parameter(Variable &variable);
private:
void link_to_cursors(Instruction *instruction);
};
struct ProcedureBuilder::Branch {
ProcedureBuilder branch_true;
ProcedureBuilder branch_false;
};
struct ProcedureBuilder::Loop {
Instruction *begin = nullptr;
DummyInstruction *end = nullptr;
};
/* --------------------------------------------------------------------
* ProcedureBuilder inline methods.
*/
inline ProcedureBuilder::ProcedureBuilder(Branch &branch)
: ProcedureBuilder(*branch.branch_true.procedure_)
{
this->set_cursor_after_branch(branch);
}
inline ProcedureBuilder::ProcedureBuilder(Procedure &procedure, InstructionCursor initial_cursor)
: procedure_(&procedure), cursors_({initial_cursor})
{
}
inline ProcedureBuilder::ProcedureBuilder(Span<ProcedureBuilder *> builders)
: ProcedureBuilder(*builders[0]->procedure_)
{
this->set_cursor(builders);
}
inline void ProcedureBuilder::set_cursor(const InstructionCursor &cursor)
{
cursors_ = {cursor};
}
inline void ProcedureBuilder::set_cursor(Span<InstructionCursor> cursors)
{
cursors_ = cursors;
}
inline void ProcedureBuilder::set_cursor_after_branch(Branch &branch)
{
this->set_cursor({&branch.branch_false, &branch.branch_true});
}
inline void ProcedureBuilder::set_cursor_after_loop(Loop &loop)
{
this->set_cursor(InstructionCursor{*loop.end});
}
inline void ProcedureBuilder::set_cursor(Span<ProcedureBuilder *> builders)
{
cursors_.clear();
for (ProcedureBuilder *builder : builders) {
cursors_.extend(builder->cursors_);
}
}
template<int OutputN>
inline std::array<Variable *, OutputN> ProcedureBuilder::add_call(
const MultiFunction &fn, Span<Variable *> input_and_mutable_variables)
{
Vector<Variable *> output_variables = this->add_call(fn, input_and_mutable_variables);
BLI_assert(output_variables.size() == OutputN);
std::array<Variable *, OutputN> output_array;
initialized_copy_n(output_variables.data(), OutputN, output_array.data());
return output_array;
}
inline void ProcedureBuilder::add_parameter(ParamType::InterfaceType interface_type,
Variable &variable)
{
procedure_->add_parameter(interface_type, variable);
}
inline Variable &ProcedureBuilder::add_parameter(ParamType param_type, std::string name)
{
Variable &variable = procedure_->new_variable(param_type.data_type(), std::move(name));
this->add_parameter(param_type.interface_type(), variable);
return variable;
}
inline Variable &ProcedureBuilder::add_input_parameter(DataType data_type, std::string name)
{
return this->add_parameter(ParamType(ParamType::Input, data_type), std::move(name));
}
template<typename T>
inline Variable &ProcedureBuilder::add_single_input_parameter(std::string name)
{
return this->add_parameter(ParamType::ForSingleInput(CPPType::get<T>()), std::move(name));
}
template<typename T>
inline Variable &ProcedureBuilder::add_single_mutable_parameter(std::string name)
{
return this->add_parameter(ParamType::ForMutableSingle(CPPType::get<T>()), std::move(name));
}
inline void ProcedureBuilder::add_output_parameter(Variable &variable)
{
this->add_parameter(ParamType::Output, variable);
}
inline void ProcedureBuilder::link_to_cursors(Instruction *instruction)
{
for (InstructionCursor &cursor : cursors_) {
cursor.set_next(*procedure_, instruction);
}
}
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,30 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*/
#include "FN_multi_function_procedure.hh"
namespace blender::fn::multi_function {
/** A multi-function that executes a procedure internally. */
class ProcedureExecutor : public MultiFunction {
private:
Signature signature_;
const Procedure &procedure_;
public:
ProcedureExecutor(const Procedure &procedure);
void call(const IndexMask &mask, Params params, Context context) const override;
private:
ExecutionHints get_execution_hints() const override;
};
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,49 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* A #Procedure optimization pass takes an existing procedure and changes it in a way that
* improves its performance when executed.
*
* Oftentimes it would also be possible to implement a specific optimization directly during
* construction of the initial #Procedure. There is a trade-off between doing that or just
* building a "simple" procedure and then optimizing it uses separate optimization passes.
* - Doing optimizations directly during construction is typically faster than doing it as a
* separate pass. However, it would be much harder to turn the optimization off when it is not
* necessary, making the construction potentially slower in those cases.
* - Doing optimizations directly would also make code more complex, because it mixes the logic
* that generates the procedure from some other data with optimization decisions.
* - Having a separate pass allows us to use it in different places when necessary.
* - Having a separate pass allows us to enable and disable it easily to better understand its
* impact on performance.
*/
#include "FN_multi_function_procedure.hh"
namespace blender::fn::multi_function::procedure_optimization {
/**
* When generating a procedure, destruct instructions (#DestructInstruction) have to be inserted
* for all variables that are not outputs. Often the simplest approach is to add these instructions
* at the very end. However, when the procedure is executed this is not optimal, because many more
* variables are initialized at the same time than necessary. This inhibits the reuse of memory
* buffers which decreases performance and increases memory use.
*
* This optimization pass moves destruct instructions up in the procedure. The goal is to destruct
* each variable right after its last use.
*
* For simplicity, and because this is the most common use case, this optimization currently only
* works on a single chain of instructions. Destruct instructions are not moved across branches.
*
* \param procedure: The procedure that should be optimized.
* \param block_end_instr: The instruction that points to the last instruction within a linear
* chain of instructions. The algorithm moves instructions backward starting at this instruction.
*/
void move_destructs_up(Procedure &procedure, Instruction &block_end_instr);
} // namespace blender::fn::multi_function::procedure_optimization

View File

@@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_ustring.hh"
#include "FN_multi_function.hh"
namespace blender::fn::multi_function::registry {
/**
* Add a new multi-function to the registry. The #MultiFunction::name is used as identifier.
* This multi-function is expected to have static storage duration.
*/
void add_new(const MultiFunction &fn);
/**
* Utility to create a multi-function with static storage duration that is added to the registry.
*/
template<typename CreateFn> inline void add_new_cb(CreateFn &&create_fn)
{
static auto fn = create_fn();
registry::add_new(fn);
}
/**
* Find the multi-function with the given identifier. The multi-function is expected to exist.
*/
const MultiFunction &lookup(UString id);
} // namespace blender::fn::multi_function::registry

View File

@@ -0,0 +1,226 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
/** \file
* \ingroup fn
*
* The signature of a multi-function contains the functions name and expected parameters. New
* signatures should be build using the #SignatureBuilder class.
*/
#include "FN_multi_function_param_type.hh"
#include "BLI_enum_flags.hh"
#include "BLI_vector.hh"
namespace blender::fn::multi_function {
enum class ParamFlag {
None = 0,
/**
* If set, the multi-function parameter can be accessed using
* #Params::uninitialized_single_output_if_required which can result in better performance
* because the output does not have to be computed when it is not needed.
*/
SupportsUnusedOutput = 1 << 0,
};
ENUM_OPERATORS(ParamFlag);
struct Signature {
struct ParamInfo {
ParamType type;
const char *name;
ParamFlag flag = ParamFlag::None;
};
/**
* The name should be statically allocated so that it lives longer than this signature. This is
* used instead of an #std::string because of the overhead when many functions are created.
* If the name of the function has to be more dynamic for debugging purposes, override
* #MultiFunction::debug_name() instead. Then the dynamic name will only be computed when it is
* actually needed.
*/
const char *function_name;
Vector<ParamInfo> params;
};
class SignatureBuilder {
private:
Signature &signature_;
public:
SignatureBuilder(const char *function_name, Signature &signature_to_build);
/* Input Parameter Types */
template<typename T> void single_input(const char *name);
void single_input(const char *name, const CPPType &type);
template<typename T> void vector_input(const char *name);
void vector_input(const char *name, const CPPType &base_type);
void input(const char *name, DataType data_type);
/* Output Parameter Types */
template<typename T>
void single_output(const char *name, const ParamFlag flag = ParamFlag::None);
void single_output(const char *name,
const CPPType &type,
const ParamFlag flag = ParamFlag::None);
template<typename T>
void vector_output(const char *name, const ParamFlag flag = ParamFlag::None);
void vector_output(const char *name,
const CPPType &base_type,
const ParamFlag flag = ParamFlag::None);
void output(const char *name, DataType data_type, const ParamFlag flag = ParamFlag::None);
/* Mutable Parameter Types */
template<typename T> void single_mutable(const char *name);
void single_mutable(const char *name, const CPPType &type);
template<typename T> void vector_mutable(const char *name);
void vector_mutable(const char *name, const CPPType &base_type);
void mutable_(const char *name, DataType data_type);
template<ParamCategory Category, typename T>
void add(ParamTag<Category, T> /*tag*/, const char *name);
void add(const char *name, const ParamType &param_type);
};
/* -------------------------------------------------------------------- */
/** \name #SignatureBuilder Inline Methods
* \{ */
inline SignatureBuilder::SignatureBuilder(const char *function_name, Signature &signature_to_build)
: signature_(signature_to_build)
{
signature_.function_name = function_name;
}
template<typename T> inline void SignatureBuilder::single_input(const char *name)
{
this->single_input(name, CPPType::get<T>());
}
inline void SignatureBuilder::single_input(const char *name, const CPPType &type)
{
this->input(name, DataType::ForSingle(type));
}
template<typename T> inline void SignatureBuilder::vector_input(const char *name)
{
this->vector_input(name, CPPType::get<T>());
}
inline void SignatureBuilder::vector_input(const char *name, const CPPType &base_type)
{
this->input(name, DataType::ForVector(base_type));
}
inline void SignatureBuilder::input(const char *name, DataType data_type)
{
signature_.params.append({ParamType(ParamType::Input, data_type), name});
}
template<typename T>
inline void SignatureBuilder::single_output(const char *name, const ParamFlag flag)
{
this->single_output(name, CPPType::get<T>(), flag);
}
inline void SignatureBuilder::single_output(const char *name,
const CPPType &type,
const ParamFlag flag)
{
this->output(name, DataType::ForSingle(type), flag);
}
template<typename T>
inline void SignatureBuilder::vector_output(const char *name, const ParamFlag flag)
{
this->vector_output(name, CPPType::get<T>(), flag);
}
inline void SignatureBuilder::vector_output(const char *name,
const CPPType &base_type,
const ParamFlag flag)
{
this->output(name, DataType::ForVector(base_type), flag);
}
inline void SignatureBuilder::output(const char *name, DataType data_type, const ParamFlag flag)
{
signature_.params.append({ParamType(ParamType::Output, data_type), name, flag});
}
template<typename T> inline void SignatureBuilder::single_mutable(const char *name)
{
this->single_mutable(name, CPPType::get<T>());
}
inline void SignatureBuilder::single_mutable(const char *name, const CPPType &type)
{
this->mutable_(name, DataType::ForSingle(type));
}
template<typename T> inline void SignatureBuilder::vector_mutable(const char *name)
{
this->vector_mutable(name, CPPType::get<T>());
}
inline void SignatureBuilder::vector_mutable(const char *name, const CPPType &base_type)
{
this->mutable_(name, DataType::ForVector(base_type));
}
inline void SignatureBuilder::mutable_(const char *name, DataType data_type)
{
signature_.params.append({ParamType(ParamType::Mutable, data_type), name});
}
inline void SignatureBuilder::add(const char *name, const ParamType &param_type)
{
switch (param_type.interface_type()) {
case ParamType::Input:
this->input(name, param_type.data_type());
break;
case ParamType::Mutable:
this->mutable_(name, param_type.data_type());
break;
case ParamType::Output:
this->output(name, param_type.data_type());
break;
}
}
template<ParamCategory Category, typename T>
inline void SignatureBuilder::add(ParamTag<Category, T> /*tag*/, const char *name)
{
switch (Category) {
case ParamCategory::SingleInput:
this->single_input<T>(name);
return;
case ParamCategory::VectorInput:
this->vector_input<T>(name);
return;
case ParamCategory::SingleOutput:
this->single_output<T>(name);
return;
case ParamCategory::VectorOutput:
this->vector_output<T>(name);
return;
case ParamCategory::SingleMutable:
this->single_mutable<T>(name);
return;
case ParamCategory::VectorMutable:
this->vector_mutable<T>(name);
return;
}
BLI_assert_unreachable();
}
/** \} */
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,38 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BLI_linear_allocator.hh"
namespace blender::fn {
/**
* Extension of #UserData that is thread-local. This avoids accessing e.g.
* `EnumerableThreadSpecific.local()` in every nested lazy-function because the thread local
* data is passed in by the caller.
*/
class LocalUserData {
public:
virtual ~LocalUserData() = default;
};
/**
* This allows passing arbitrary data into a function. For that, #UserData has to be subclassed.
* This mainly exists because it's more type safe than passing a `void *` with no type information
* attached.
*
* Some lazy-functions may expect to find a certain type of user data when executed.
*/
class UserData {
public:
virtual ~UserData() = default;
/**
* Get thread local data for this user-data and the current thread.
*/
virtual destruct_ptr<LocalUserData> get_local(LinearAllocator<> &allocator);
};
} // namespace blender::fn

View File

@@ -0,0 +1,567 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_set.hh"
#include "BLI_stack.hh"
#include "FN_field.hh"
#include "FN_multi_function_registry.hh"
#include <xxhash.h>
namespace blender::fn {
FieldInput::FieldInput(const CPPType &type, std::string debug_name)
: type_(&type), debug_name_(std::move(debug_name))
{
}
GField GField::from_constant(const CPPType &type, const void *value)
{
if (TrivialInlineConstant::cpp_type_supported(type)) {
TrivialInlineConstant constant;
constant.type = &type;
type.copy_construct(value, constant.value.ptr());
return GField(constant);
}
void *new_value = MEM_new_uninitialized_aligned(type.size, type.alignment, __func__);
type.copy_construct(value, new_value);
return GField(OwnedConstant{&type, new_value});
}
bool operator==(const GField &a, const GField &b)
{
const GField &a_ref = a.deref_field_ref();
const GField &b_ref = b.deref_field_ref();
return std::visit(
[&]<typename T>(const T &v_a) -> bool {
if constexpr (std::is_same_v<T, GField::Input>) {
if (const auto *v_b = std::get_if<GField::Input>(&b_ref.variant_)) {
return v_a.node == v_b->node;
}
return false;
}
else if constexpr (std::is_same_v<T, GField::MultiFn>) {
if (const auto *v_b = std::get_if<GField::MultiFn>(&b_ref.variant_)) {
return v_a.node == v_b->node && v_a.output_i == v_b->output_i;
}
return false;
}
else if constexpr (std::is_same_v<T, GField::FieldRef>) {
/* Should not exist due to #deref_field_ref above. */
BLI_assert_unreachable();
return false;
}
else if constexpr (GField::is_constant_value_v<T>) {
const CPPType &type_a = *v_a.type;
const void *constant_a = v_a.value;
return std::visit(
[&]<typename U>(const U &v_b) -> bool {
if constexpr (GField::is_constant_value_v<U>) {
const CPPType &type_b = *v_b.type;
if (type_a != type_b) {
return false;
}
const void *constant_b = v_b.value;
return type_a.is_equal_or_false(constant_a, constant_b);
}
else {
return false;
}
},
b_ref.variant_);
}
else {
BLI_assert_unreachable_static_t(T);
}
},
a_ref.variant_);
}
uint64_t GField::hash() const
{
const GField &ref = this->deref_field_ref();
return std::visit(
[&]<typename T>(const T &v) -> uint64_t {
if constexpr (std::is_same_v<T, Input>) {
return get_default_hash(v.node);
}
else if constexpr (std::is_same_v<T, MultiFn>) {
return get_default_hash(v.node, v.output_i);
}
else if constexpr (std::is_same_v<T, FieldRef>) {
/* Should not exist due to #deref_field_ref above. */
BLI_assert_unreachable();
return 0;
}
else if constexpr (is_constant_value_v<T>) {
return v.type->hash_or_fallback(v.value, uint64_t(v.type));
}
else {
BLI_assert_unreachable_static_t(T);
}
},
ref.variant_);
}
UniqueHash FieldHashDeep::ensure(const GFieldRef &field)
{
if (const UniqueHash *cached = cache.lookup_ptr(field)) {
return *cached;
}
/* With a post-order DFS traversal, push each node twice. On the first pop (not yet in
* `visited`), push a field's children. On the second pop (already in `visited`), all children
* will be in `cache`, so compute and store the hash. Checking the cache for a hash avoids
* duplicate work when the same sub-field is reached via multiple paths (e.g. diamond-shaped
* graphs). */
Set<GFieldRef, 8> visited;
Stack<GFieldRef, 16> stack;
stack.push(field);
while (!stack.is_empty()) {
GFieldRef current = stack.pop();
if (cache.contains(current)) {
continue;
}
if (visited.contains(current)) {
UniqueHashBytes hash_context;
std::visit(
[&]<typename T>(const T &v) {
if constexpr (std::is_same_v<T, GFieldRef::Value>) {
v.type->hash_unique(v.value, hash_context);
hash_context.add(v.type);
}
else if constexpr (std::is_same_v<T, GFieldRef::Input>) {
v.node->hash_unique(hash_context, *this);
}
else if constexpr (std::is_same_v<T, GFieldRef::MultiFn>) {
v.node->multi_function().hash_unique(hash_context);
hash_context.add(v.output_i);
for (const GField &input_field : v.node->inputs()) {
hash_context.add(cache.lookup(input_field));
}
}
else {
BLI_assert_unreachable_static_t(T);
}
},
current.variant());
const Span bytes = hash_context.data.as_span();
UniqueHash hash;
const XXH128_hash_t xxhash = XXH3_128bits(bytes.data(), bytes.size());
static_assert(sizeof(UniqueHash) == sizeof(xxhash));
memcpy(static_cast<void *>(&hash), &xxhash, sizeof(xxhash));
cache.add_new(current, hash);
continue;
}
visited.add(current);
stack.push(current);
if (const auto *multi_fn = std::get_if<GFieldRef::MultiFn>(&current.variant())) {
for (const GField &input : multi_fn->node->inputs()) {
stack.push(input);
}
}
}
return cache.lookup(field);
}
const FieldInputsPtr &FieldInput::field_inputs() const
{
field_inputs_mutex_.ensure([&]() {
FieldInputs *inputs = MEM_new<FieldInputs>(__func__);
inputs->inputs.add(*this);
field_inputs_ = FieldInputsPtr(inputs);
});
return field_inputs_;
}
uint64_t FieldInput::hash() const
{
UniqueHashBytes hash_context;
FieldHashDeep deep_hash_cache;
this->hash_unique(hash_context, deep_hash_cache);
return get_default_hash(hash_context.data);
}
FieldInput::~FieldInput() = default;
void FieldInput::foreach_recursive_field(FunctionRef<void(const GField &)> /*fn*/) const {}
void FieldInput::hash_unique(UniqueHashBytes &hash, FieldHashDeep & /*deep_hash_cache*/) const
{
hash.add(this);
}
FieldOperationPtr GField::try_extract_operation()
{
MultiFn *multi_fn = std::get_if<MultiFn>(&variant_);
if (!multi_fn || !multi_fn->node) {
return nullptr;
}
return std::move(multi_fn->node);
}
void FieldInput::delete_self()
{
MEM_delete(this);
}
void FieldOperation::delete_self()
{
this->delete_input_fields();
MEM_delete(this);
}
void FieldOperation::delete_input_fields()
{
BLI_assert(this->is_expired());
/* Some input fields are freed iteratively instead of recursively to avoid a potentially very
* deep call stack. */
Vector<FieldOperationPtr, 16> remaining;
for (GField &input : inputs_) {
if (FieldOperationPtr input_op = input.try_extract_operation()) {
remaining.append(std::move(input_op));
}
}
while (!remaining.is_empty()) {
FieldOperationPtr op = remaining.pop_last();
if (!op->is_mutable()) {
continue;
}
FieldOperation &op_ref = const_cast<FieldOperation &>(*op);
for (GField &input : op_ref.inputs_) {
if (FieldOperationPtr input_op = input.try_extract_operation()) {
remaining.append(std::move(input_op));
}
}
}
}
void FieldInputs::delete_self()
{
MEM_delete(this);
}
FieldOperationPtr FieldOperation::from(std::shared_ptr<const mf::MultiFunction> fn,
Vector<GField> inputs)
{
return FieldOperationPtr(MEM_new<FieldOperation>(__func__, std::move(fn), std::move(inputs)));
}
FieldOperationPtr FieldOperation::from(const mf::MultiFunction &fn, Vector<GField> inputs)
{
return FieldOperationPtr(MEM_new<FieldOperation>(__func__, fn, std::move(inputs)));
}
/**
* Combine the field inputs from multiple fields. If possible, nothing new is allocated.
*/
static FieldInputsPtr combine_field_inputs(const Span<GField> &fields)
{
/* Try to find an existing #FieldInputsPtr that covers all given fields. */
bool candidate_valid = true;
const FieldInputsPtr *candidate = nullptr;
for (const GField &field : fields) {
const FieldInputsPtr &field_inputs_ptr = field.field_inputs();
if (!field_inputs_ptr) {
continue;
}
if (!candidate) {
candidate = &field_inputs_ptr;
continue;
}
if (field_inputs_ptr == *candidate) {
continue;
}
const FieldInputsPtr *smaller_candidate = candidate;
const FieldInputsPtr *larger_candidate = &field_inputs_ptr;
if ((*smaller_candidate)->inputs.size() > (*larger_candidate)->inputs.size()) {
std::swap(smaller_candidate, larger_candidate);
}
/* Check if the smaller candidate is fully contained in the larger one. */
for (const FieldInput &field_input : (*smaller_candidate)->inputs) {
if (!(*larger_candidate)->inputs.contains(field_input)) {
candidate_valid = false;
break;
}
}
if (!candidate_valid) {
break;
}
candidate = larger_candidate;
}
if (candidate_valid) {
if (candidate) {
return *candidate;
}
return {};
}
/* None of the existing #FieldInputs can be reused, create a new #FieldInputs and add all the
* inputs to it. */
FieldInputs *new_field_inputs = MEM_new<FieldInputs>(__func__);
for (const GField &field : fields) {
const FieldInputsPtr &field_inputs_ptr = field.field_inputs();
if (!field_inputs_ptr) {
continue;
}
for (const FieldInput &field_input : field_inputs_ptr->inputs) {
new_field_inputs->inputs.add(field_input);
}
}
return FieldInputsPtr(new_field_inputs);
}
GField::GField(const GField &other) : variant_(other.variant_)
{
std::visit(
[&]<typename T>(T &v) {
if constexpr (std::is_same_v<T, OwnedConstant>) {
void *new_value = MEM_new_uninitialized_aligned(
v.type->size, v.type->alignment, __func__);
v.type->copy_construct(v.value, new_value);
v.value = new_value;
}
},
variant_);
}
GField::GField(GField &&other) noexcept : variant_(std::move(other.variant_))
{
const CPPType &type = this->cpp_type();
other.variant_ = ConstantRef{&type, type.default_value()};
}
GField &GField::operator=(const GField &other)
{
if (this == &other) {
return *this;
}
this->~GField();
new (this) GField(other);
return *this;
}
GField &GField::operator=(GField &&other) noexcept
{
if (this == &other) {
return *this;
}
this->~GField();
new (this) GField(std::move(other));
return *this;
}
GField::~GField()
{
std::visit(
[&]<typename T>(T &v) {
if constexpr (std::is_same_v<T, OwnedConstant>) {
v.type->destruct(v.value);
MEM_delete_void(v.value);
}
},
variant_);
}
GFieldRef::GFieldRef(const GField &field)
: variant_(std::visit(
[]<typename T>(const T &v) -> Variant {
if constexpr (std::is_same_v<T, GField::Input>) {
return Input{v.node.get()};
}
else if constexpr (std::is_same_v<T, GField::MultiFn>) {
return MultiFn{v.node.get(), v.output_i};
}
else if constexpr (std::is_same_v<T, GField::FieldRef>) {
/* Should not exist due to #deref_field_ref. */
BLI_assert_unreachable();
return Value{};
}
else if constexpr (GField::is_constant_value_v<T>) {
return Value{v.type, v.value};
}
else {
BLI_assert_unreachable_static_t(T);
}
},
field.deref_field_ref().variant()))
{
}
const FieldInputsPtr &GFieldRef::field_inputs() const
{
static const ImplicitSharingPtr<FieldInputs> empty_inputs;
return std::visit(
[&]<typename T>(const T &v) -> const FieldInputsPtr & {
if constexpr (std::is_same_v<T, Input>) {
return v.node->field_inputs();
}
else if constexpr (std::is_same_v<T, MultiFn>) {
return v.node->field_inputs();
}
else if constexpr (std::is_same_v<T, Value>) {
return empty_inputs;
}
else {
BLI_assert_unreachable_static_t(T);
}
},
variant_);
}
bool operator==(const GFieldRef &a, const GFieldRef &b)
{
return std::visit(
[&]<typename T>(const T &v_a) -> bool {
if constexpr (std::is_same_v<T, GFieldRef::Value>) {
if (const auto *v_b = std::get_if<GFieldRef::Value>(&b.variant())) {
if (v_a.type != v_b->type) {
return false;
}
if (v_a.value == v_b->value) {
/* This may return true even if the values don't compare equal, e.g. due to NaN
* values. */
return true;
}
return v_a.type->is_equal_or_false(v_a.value, v_b->value);
}
return false;
}
else if constexpr (std::is_same_v<T, GFieldRef::Input>) {
if (const auto *v_b = std::get_if<GFieldRef::Input>(&b.variant())) {
return v_a.node == v_b->node;
}
return false;
}
else if constexpr (std::is_same_v<T, GFieldRef::MultiFn>) {
if (const auto *v_b = std::get_if<GFieldRef::MultiFn>(&b.variant())) {
return v_a.node == v_b->node && v_a.output_i == v_b->output_i;
}
return false;
}
else {
BLI_assert_unreachable_static_t(T);
}
},
a.variant());
}
uint64_t GFieldRef::hash() const
{
return std::visit(
[&]<typename T>(const T &v) -> uint64_t {
if constexpr (std::is_same_v<T, Value>) {
return v.type->hash_or_fallback(v.value, uint64_t(v.type));
}
else if constexpr (std::is_same_v<T, Input>) {
return get_default_hash(v.node);
}
else if constexpr (std::is_same_v<T, MultiFn>) {
return get_default_hash(v.node, v.output_i);
}
else {
BLI_assert_unreachable_static_t(T);
}
},
variant_);
}
FieldOperation::FieldOperation(std::shared_ptr<const mf::MultiFunction> fn, Vector<GField> inputs)
: FieldOperation(*fn, std::move(inputs))
{
owned_fn_ = std::move(fn);
}
FieldOperation::FieldOperation(const mf::MultiFunction &fn, Vector<GField> inputs)
: inputs_(inputs), fn_(&fn)
{
field_inputs_ = combine_field_inputs(inputs_);
}
const CPPType &FieldOperation::output_cpp_type(const int output_i) const
{
int count = 0;
for (const int param_index : fn_->param_indices()) {
const mf::ParamType param_type = fn_->param_type(param_index);
if (param_type.is_output()) {
if (count == output_i) {
return param_type.data_type().single_type();
}
count++;
}
}
BLI_assert_unreachable();
return CPPType::get<float>();
}
const FieldInputsPtr &GField::field_inputs() const
{
static const ImplicitSharingPtr<FieldInputs> empty_inputs;
return std::visit(
[]<typename T>(const T &v) -> const FieldInputsPtr & {
if constexpr (is_same_any_v<T, Input, MultiFn>) {
return v.node->field_inputs();
}
else if constexpr (std::is_same_v<T, FieldRef>) {
return v.field_ref->field_inputs();
}
else if constexpr (is_same_any_v<T, ConstantRef, TrivialInlineConstant, OwnedConstant>) {
return empty_inputs;
}
else {
BLI_assert_unreachable_static_t(T);
}
},
this->variant_);
}
GVArray FieldContext::get_varray_for_input(const FieldInput &field_input,
const IndexMask &mask,
ResourceScope &scope) const
{
/* By default ask the field input to create the varray. Another field context might overwrite
* the context here. */
return field_input.get_varray_for_context(*this, mask, scope);
}
IndexFieldInput::IndexFieldInput() : FieldInput(CPPType::get<int>(), "Index") {}
GVArray IndexFieldInput::get_index_varray(const IndexMask &mask)
{
auto index_func = [](int i) { return i; };
return VArray<int>::from_func(mask.min_array_size(), index_func);
}
GVArray IndexFieldInput::get_varray_for_context(const fn::FieldContext & /*context*/,
const IndexMask &mask,
ResourceScope & /*scope*/) const
{
/* TODO: Investigate a similar method to IndexRange::as_span() */
return get_index_varray(mask);
}
void IndexFieldInput::hash_unique(UniqueHashBytes &hash,
fn::FieldHashDeep & /*deep_hash_cache*/) const
{
static constexpr int8_t id = 0;
hash.add(&id);
}
const Field<int> &IndexFieldInput::get_field()
{
static const Field<int> field = Field<int>::from_input<IndexFieldInput>();
static const Field<int> field_ref = Field<int>::from_non_owning_ref(field);
return field_ref;
}
Field<bool> invert_boolean_field(const Field<bool> &field)
{
const mf::MultiFunction &not_fn = fn::multi_function::registry::lookup("!bool"_ustr);
auto not_op = FieldOperation::from(not_fn, {field});
return GField(not_op, 0).typed<bool>();
}
} // namespace blender::fn

View File

@@ -0,0 +1,639 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_array_utils.hh"
#include "BLI_map.hh"
#include "BLI_multi_value_map.hh"
#include "BLI_set.hh"
#include "BLI_stack.hh"
#include "BLI_vector_set.hh"
#include "FN_field_evaluation.hh"
#include "FN_multi_function.hh"
#include "FN_multi_function_builder.hh"
#include "FN_multi_function_procedure.hh"
#include "FN_multi_function_procedure_builder.hh"
#include "FN_multi_function_procedure_executor.hh"
#include "FN_multi_function_procedure_optimization.hh"
namespace blender::fn {
/* -------------------------------------------------------------------- */
/** \name Field Evaluation
* \{ */
struct FieldTreeInfo {
FieldHashDeep deep_hashes;
/**
* When fields are built, they only have references to the fields that they depend on. This map
* allows traversal of fields in the opposite direction. So for every field it stores the other
* fields that depend on it directly.
*/
MultiValueMap<UniqueHash, UniqueHash> field_users;
/**
* The same field input may exist in the field tree as separate nodes due to the way
* the tree is constructed. This set contains every different input only once.
*/
VectorSet<UniqueHash> deduplicated_input_hashes;
Vector<GFieldRef> deduplicated_inputs;
};
/**
* Collects some information from the field tree that is required by later steps.
*/
static FieldTreeInfo preprocess_field_tree(Span<GFieldRef> entry_fields)
{
PRF_scope(ProfileCategory::Default);
FieldTreeInfo field_tree_info;
Stack<GFieldRef> fields_to_check;
Set<GFieldRef> handled_fields;
for (GFieldRef field : entry_fields) {
if (handled_fields.add(field)) {
fields_to_check.push(field);
}
}
while (!fields_to_check.is_empty()) {
const GFieldRef &field = fields_to_check.pop();
const GFieldRef::Variant &field_variant = field.variant();
const UniqueHash hash = field_tree_info.deep_hashes.ensure(field);
std::visit(
[&]<typename T>(const T &v) {
if constexpr (std::is_same_v<T, GFieldRef::Input>) {
if (field_tree_info.deduplicated_input_hashes.add(hash)) {
field_tree_info.deduplicated_inputs.append(field);
}
}
else if constexpr (std::is_same_v<T, GFieldRef::MultiFn>) {
for (const GField &input_field : v.node->inputs()) {
const UniqueHash input_hash = field_tree_info.deep_hashes.lookup(input_field);
field_tree_info.field_users.add(input_hash, hash);
if (handled_fields.add(input_field)) {
fields_to_check.push(input_field);
}
}
}
else if constexpr (std::is_same_v<T, GFieldRef::Value>) {
/* Nothing to do. */
}
else {
BLI_assert_unreachable_static_t(T);
}
},
field_variant);
}
return field_tree_info;
}
/**
* Retrieves the data from the context that is passed as input into the field.
*/
static Vector<GVArray> get_field_context_inputs(ResourceScope &scope,
const IndexMask &mask,
const FieldContext &context,
const Span<GFieldRef> field_inputs)
{
Vector<GVArray> field_context_inputs;
for (const GFieldRef &input_field : field_inputs) {
const FieldInput &field_input = *std::get<GFieldRef::Input>(input_field.variant()).node;
GVArray varray = context.get_varray_for_input(field_input, mask, scope);
if (!varray) {
const CPPType &type = field_input.cpp_type();
varray = GVArray::from_single_default(type, mask.min_array_size());
}
field_context_inputs.append(std::move(varray));
}
return field_context_inputs;
}
/**
* \return A set that contains all fields from the field tree that depend on an input that varies
* for different indices.
*/
static Set<UniqueHash> find_varying_fields(const FieldTreeInfo &field_tree_info,
const Span<GVArray> field_context_inputs)
{
Set<UniqueHash> found_fields;
Stack<UniqueHash> fields_to_check;
/* The varying fields are the ones that depend on inputs that are not constant. Therefore we
* start the tree search at the non-constant input fields and traverse through all fields that
* depend on them. */
for (const int input_i : field_tree_info.deduplicated_inputs.index_range()) {
const GVArray &varray = field_context_inputs[input_i];
if (varray.is_single()) {
continue;
}
const UniqueHash &field = field_tree_info.deduplicated_input_hashes[input_i];
for (const UniqueHash &user : field_tree_info.field_users.lookup(field)) {
if (found_fields.add(user)) {
fields_to_check.push(user);
}
}
}
while (!fields_to_check.is_empty()) {
const UniqueHash &field = fields_to_check.pop();
for (const UniqueHash &user : field_tree_info.field_users.lookup(field)) {
if (found_fields.add(user)) {
fields_to_check.push(user);
}
}
}
return found_fields;
}
/**
* Builds the #procedure so that it computes the fields.
*/
static void build_multi_function_procedure_for_fields(mf::Procedure &procedure,
ResourceScope &scope,
const FieldTreeInfo &field_tree_info,
Span<GFieldRef> output_fields)
{
PRF_scope(ProfileCategory::Default);
mf::ProcedureBuilder builder{procedure};
/* Every input, intermediate and output field corresponds to a variable in the procedure. */
Map<UniqueHash, mf::Variable *> variable_by_field;
/* Start by adding the field inputs as parameters to the procedure. */
for (const GFieldRef &input_field : field_tree_info.deduplicated_inputs) {
const UniqueHash input_hash = field_tree_info.deep_hashes.lookup(input_field);
const FieldInput &field_input = *std::get<GFieldRef::Input>(input_field.variant()).node;
mf::Variable &variable = builder.add_input_parameter(
mf::DataType::ForSingle(field_input.cpp_type()), field_input.debug_name());
variable_by_field.add_new(input_hash, &variable);
}
/* Utility struct that is used to do proper depth first search traversal of the tree below. */
struct FieldWithIndex {
GFieldRef field;
int current_input_index = 0;
};
for (GFieldRef field : output_fields) {
/* We start a new stack for each output field to make sure that a field pushed later to the
* stack never depends on a field that was pushed before. */
Stack<FieldWithIndex> fields_to_check;
fields_to_check.push({field, 0});
while (!fields_to_check.is_empty()) {
FieldWithIndex &field_with_index = fields_to_check.peek();
const GFieldRef &field = field_with_index.field;
const UniqueHash field_hash = field_tree_info.deep_hashes.lookup(field);
if (variable_by_field.contains(field_hash)) {
/* The field has been handled already. */
fields_to_check.pop();
continue;
}
const GFieldRef::Variant &field_variant = field.variant();
std::visit(
[&]<typename T>(const T &v) {
if constexpr (std::is_same_v<T, GFieldRef::Input>) {
/* Variables for inputs are added above. */
}
else if constexpr (std::is_same_v<T, GFieldRef::MultiFn>) {
const FieldOperation &field_multi_fn = *v.node;
const Span<GField> fn_inputs = field_multi_fn.inputs();
if (field_with_index.current_input_index < fn_inputs.size()) {
/* Not all inputs are handled yet. Push the next input field to the stack and
* increment the input index. */
fields_to_check.push({fn_inputs[field_with_index.current_input_index]});
field_with_index.current_input_index++;
}
else {
/* All inputs variables are ready, now gather all variables that are used by the
* function and call it. */
const mf::MultiFunction &multi_function = field_multi_fn.multi_function();
Array<mf::Variable *, 8> 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 mf::ParamType param_type = multi_function.param_type(param_index);
const mf::ParamType::InterfaceType interface_type = param_type.interface_type();
if (interface_type == mf::ParamType::Input) {
const GField &input_field = fn_inputs[param_input_index];
const UniqueHash input_hash = field_tree_info.deep_hashes.lookup(input_field);
variables[param_index] = variable_by_field.lookup(input_hash);
param_input_index++;
}
else if (interface_type == mf::ParamType::Output) {
const GFieldRef output_field{field_multi_fn, param_output_index};
/* NOTE: This abuses the deep hash cache as a set of the fields in the tree. At
* the cost of either hashing this output field or building a separate set of
* visited GFieldRefs, we wouldn't have to use the cache in this way. */
if (!field_tree_info.deep_hashes.contains(output_field)) {
/* Ignored outputs don't need a variable. */
variables[param_index] = nullptr;
}
else {
/* Create a new variable for used outputs. */
mf::Variable &new_variable = procedure.new_variable(param_type.data_type());
variables[param_index] = &new_variable;
const UniqueHash output_hash = field_tree_info.deep_hashes.lookup(
output_field);
variable_by_field.add_new(output_hash, &new_variable);
}
param_output_index++;
}
else {
BLI_assert_unreachable();
}
}
builder.add_call_with_all_variables(multi_function, variables);
}
}
else if constexpr (std::is_same_v<T, GFieldRef::Value>) {
const mf::MultiFunction &fn =
procedure.construct_function<mf::CustomMF_GenericConstant>(
*v.type, v.value, false);
mf::Variable &new_variable = *builder.add_call<1>(fn)[0];
variable_by_field.add_new(field_hash, &new_variable);
}
else {
BLI_assert_unreachable_static_t(T);
}
},
field_variant);
}
}
/* Add output parameters to the procedure. */
Set<mf::Variable *> output_variables;
for (const GFieldRef &field : output_fields) {
const UniqueHash field_hash = field_tree_info.deep_hashes.lookup(field);
mf::Variable *variable = variable_by_field.lookup(field_hash);
if (!output_variables.add(variable)) {
/* One variable can be output at most once. To output the same value twice, we have to make
* a copy first. */
const mf::MultiFunction &copy_fn = scope.construct<mf::CustomMF_GenericCopy>(
variable->data_type());
variable = builder.add_call<1>(copy_fn, {variable})[0];
output_variables.add(variable);
}
builder.add_output_parameter(*variable);
}
for (mf::Variable *variable : procedure.variables()) {
if (!output_variables.contains(variable)) {
builder.add_destruct(*variable);
}
}
mf::ReturnInstruction &return_instr = builder.add_return();
mf::procedure_optimization::move_destructs_up(procedure, return_instr);
procedure.prepare_for_execution();
// std::cout << procedure.to_dot() << "\n";
BLI_assert(procedure.validate());
}
Vector<GVArray> evaluate_fields(ResourceScope &scope,
Span<GFieldRef> fields_to_evaluate,
const IndexMask &mask,
const FieldContext &context,
Span<GVMutableArray> dst_varrays)
{
PRF_scope(ProfileCategory::Default);
Vector<GVArray> varrays(fields_to_evaluate.size());
Array<bool> is_output_written_to_dst(fields_to_evaluate.size(), false);
const int array_size = mask.min_array_size();
if (mask.is_empty()) {
for (const int i : fields_to_evaluate.index_range()) {
const CPPType &type = fields_to_evaluate[i].cpp_type();
varrays[i] = GVArray::from_empty(type);
}
return varrays;
}
/* Destination arrays are optional. Create a small utility method to access them. */
auto get_dst_varray = [&](int index) -> GVMutableArray {
if (dst_varrays.is_empty()) {
return {};
}
const GVMutableArray &varray = dst_varrays[index];
if (!varray) {
return {};
}
BLI_assert(varray.size() >= array_size);
return varray;
};
/* Traverse the field tree and prepare some data that is used in later steps. */
FieldTreeInfo field_tree_info = preprocess_field_tree(fields_to_evaluate);
/* Get inputs that will be passed into the field when evaluated. */
Vector<GVArray> field_context_inputs = get_field_context_inputs(
scope, mask, context, field_tree_info.deduplicated_inputs);
Set<UniqueHash> varying_fields = find_varying_fields(field_tree_info, field_context_inputs);
/* Process fields that can output a VArray directly, and separate the rest of the fields into
* two categories: those that are constant and need to be evaluated only once, and those that
* need to be evaluated for every index. */
Vector<GFieldRef> varying_fields_to_evaluate;
Vector<int> varying_field_indices;
Vector<GFieldRef> constant_fields_to_evaluate;
Vector<int> constant_field_indices;
for (const int out_index : fields_to_evaluate.index_range()) {
const GFieldRef &field = fields_to_evaluate[out_index];
const GFieldRef::Variant &field_variant = field.variant();
std::visit(
[&]<typename T>(const T &v) {
if constexpr (std::is_same_v<T, GFieldRef::Input>) {
const UniqueHash hash = field_tree_info.deep_hashes.lookup(field);
const int input_i = field_tree_info.deduplicated_input_hashes.index_of(hash);
const GVArray &varray = field_context_inputs[input_i];
varrays[out_index] = varray;
}
else if constexpr (std::is_same_v<T, GFieldRef::MultiFn>) {
const UniqueHash hash = field_tree_info.deep_hashes.lookup(field);
if (varying_fields.contains(hash)) {
varying_fields_to_evaluate.append(field);
varying_field_indices.append(out_index);
}
else {
constant_fields_to_evaluate.append(field);
constant_field_indices.append(out_index);
}
}
else if constexpr (std::is_same_v<T, GFieldRef::Value>) {
varrays[out_index] = GVArray::from_single_ref(*v.type, mask.min_array_size(), v.value);
}
else {
BLI_assert_unreachable_static_t(T);
}
},
field_variant);
}
/* Evaluate varying fields if necessary. */
if (!varying_fields_to_evaluate.is_empty()) {
/* Build the procedure for those fields. */
mf::Procedure procedure;
build_multi_function_procedure_for_fields(
procedure, scope, field_tree_info, varying_fields_to_evaluate);
mf::ProcedureExecutor procedure_executor{procedure};
mf::ParamsBuilder mf_params{procedure_executor, &mask};
mf::ContextBuilder mf_context;
/* Provide inputs to the procedure executor. */
for (const GVArray &varray : field_context_inputs) {
mf_params.add_readonly_single_input(varray);
}
for (const int i : varying_fields_to_evaluate.index_range()) {
const GFieldRef &field = varying_fields_to_evaluate[i];
const CPPType &type = field.cpp_type();
const int out_index = varying_field_indices[i];
/* Try to get an existing virtual array that the result should be written into. */
GVMutableArray dst_varray = get_dst_varray(out_index);
void *buffer;
if (!dst_varray || !dst_varray.is_span()) {
/* Allocate a new buffer for the computed result. */
buffer = scope.allocator().allocate_array(type, array_size);
if (!type.is_trivially_destructible) {
/* Destruct values in the end. */
scope.add_destruct_call(
[buffer, mask, &type]() { type.destruct_indices(buffer, mask); });
}
varrays[out_index] = GVArray::from_span({type, buffer, array_size});
}
else {
/* Write the result into the existing span. */
buffer = dst_varray.get_internal_span().data();
varrays[out_index] = dst_varray;
is_output_written_to_dst[out_index] = true;
}
/* Pass output buffer to the procedure executor. */
const GMutableSpan span{type, buffer, array_size};
mf_params.add_uninitialized_single_output(span);
}
procedure_executor.call_auto(mask, mf_params, mf_context);
}
/* Evaluate constant fields if necessary. */
if (!constant_fields_to_evaluate.is_empty()) {
/* Build the procedure for those fields. */
mf::Procedure procedure;
build_multi_function_procedure_for_fields(
procedure, scope, field_tree_info, constant_fields_to_evaluate);
mf::ProcedureExecutor procedure_executor{procedure};
const IndexMask mask(1);
mf::ParamsBuilder mf_params{procedure_executor, &mask};
mf::ContextBuilder mf_context;
/* Provide inputs to the procedure executor. */
for (const GVArray &varray : field_context_inputs) {
mf_params.add_readonly_single_input(varray);
}
for (const int i : constant_fields_to_evaluate.index_range()) {
const GFieldRef &field = constant_fields_to_evaluate[i];
const CPPType &type = field.cpp_type();
/* Allocate memory where the computed value will be stored in. */
void *buffer = scope.allocate_owned(type);
/* Pass output buffer to the procedure executor. */
mf_params.add_uninitialized_single_output({type, buffer, 1});
/* Create virtual array that can be used after the procedure has been executed below. */
const int out_index = constant_field_indices[i];
varrays[out_index] = GVArray::from_single_ref(type, array_size, buffer);
}
procedure_executor.call(mask, mf_params, mf_context);
}
/* Copy data to supplied destination arrays if necessary. In some cases the evaluation above
* has written the computed data in the right place already. */
if (!dst_varrays.is_empty()) {
for (const int out_index : fields_to_evaluate.index_range()) {
GVMutableArray dst_varray = get_dst_varray(out_index);
if (!dst_varray) {
/* Caller did not provide a destination for this output. */
continue;
}
const GVArray &computed_varray = varrays[out_index];
BLI_assert(computed_varray.type() == dst_varray.type());
if (is_output_written_to_dst[out_index]) {
/* The result has been written into the destination provided by the caller already. */
continue;
}
/* Still have to copy over the data in the destination provided by the caller. */
if (dst_varray.is_span()) {
computed_varray.type().default_construct_indices(dst_varray.get_internal_span().data(),
mask);
array_utils::copy(computed_varray,
mask,
dst_varray.get_internal_span().take_front(mask.min_array_size()));
}
else {
/* Slower materialize into a different structure. */
const CPPType &type = computed_varray.type();
threading::parallel_for(mask.index_range(), 2048, [&](const IndexRange range) {
BUFFER_FOR_CPP_TYPE_VALUE(type, buffer);
mask.slice(range).foreach_segment([&](auto segment) {
for (const int i : segment) {
computed_varray.get_to_uninitialized(i, buffer);
dst_varray.set_by_relocate(i, buffer);
}
});
});
}
varrays[out_index] = dst_varray;
}
}
return varrays;
}
void evaluate_constant_field(const GField &field, void *r_value)
{
if (field.depends_on_input()) {
const CPPType &type = field.cpp_type();
type.value_initialize(r_value);
return;
}
AlignedBuffer<512, 64> local_buffer;
ResourceScope scope(local_buffer);
FieldContext context;
Vector<GVArray> varrays = evaluate_fields(scope, {field}, IndexRange(1), context);
varrays[0].get_to_uninitialized(0, r_value);
}
GField make_field_constant_if_possible(GField field)
{
if (field.depends_on_input()) {
return field;
}
const CPPType &type = field.cpp_type();
BUFFER_FOR_CPP_TYPE_VALUE(type, buffer);
evaluate_constant_field(field, buffer);
GField new_field = GField::from_constant(type, buffer);
type.destruct(buffer);
return new_field;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #FieldEvaluator
* \{ */
static IndexMask index_mask_from_selection(const IndexMask full_mask,
const VArray<bool> &selection,
ResourceScope &scope)
{
return IndexMask::from_bools(full_mask, selection, scope.allocator());
}
int FieldEvaluator::add_with_destination(GField field, GVMutableArray dst)
{
const int field_index = fields_to_evaluate_.append_and_get_index(std::move(field));
dst_varrays_.append(dst);
output_pointer_infos_.append({});
return field_index;
}
int FieldEvaluator::add_with_destination(GField field, GMutableSpan dst)
{
return this->add_with_destination(std::move(field), GVMutableArray::from_span(dst));
}
int FieldEvaluator::add(GField field, GVArray *varray_ptr)
{
const int field_index = fields_to_evaluate_.append_and_get_index(std::move(field));
dst_varrays_.append(nullptr);
output_pointer_infos_.append(OutputPointerInfo{
varray_ptr, [](void *dst, const GVArray &varray, ResourceScope & /*scope*/) {
*static_cast<GVArray *>(dst) = varray;
}});
return field_index;
}
int FieldEvaluator::add(GField field)
{
const int field_index = fields_to_evaluate_.append_and_get_index(std::move(field));
dst_varrays_.append(nullptr);
output_pointer_infos_.append({});
return field_index;
}
static IndexMask evaluate_selection(const Field<bool> &selection_field,
const FieldContext &context,
const IndexMask &full_mask,
ResourceScope &scope)
{
VArray<bool> selection =
evaluate_fields(scope, {selection_field}, full_mask, context)[0].typed<bool>();
return index_mask_from_selection(full_mask, selection, scope);
}
void FieldEvaluator::evaluate()
{
BLI_assert_msg(!is_evaluated_, "Cannot evaluate fields twice.");
selection_mask_ = selection_field_ ?
evaluate_selection(*selection_field_, context_, mask_, scope_) :
mask_;
Vector<GFieldRef> fields;
fields.reserve(fields_to_evaluate_.size());
static constexpr bool true_value = true;
for (const int i : fields_to_evaluate_.index_range()) {
const GField &field = fields_to_evaluate_[i];
if (field == selection_field_) {
/* Avoid evaluating the selection field again. */
fields.append(GFieldRef::from_constant(CPPType::get<bool>(), &true_value));
}
else {
fields.append(field);
}
}
evaluated_varrays_ = evaluate_fields(scope_, fields, selection_mask_, context_, dst_varrays_);
BLI_assert(fields_to_evaluate_.size() == evaluated_varrays_.size());
for (const int i : fields_to_evaluate_.index_range()) {
OutputPointerInfo &info = output_pointer_infos_[i];
if (info.dst != nullptr) {
info.set(info.dst, evaluated_varrays_[i], scope_);
}
}
is_evaluated_ = true;
}
IndexMask FieldEvaluator::get_evaluated_as_mask(const int field_index)
{
VArray<bool> varray = this->get_evaluated(field_index).typed<bool>();
if (varray.is_single()) {
if (varray.get_internal_single()) {
return IndexRange(varray.size());
}
return IndexRange(0);
}
return index_mask_from_selection(mask_, varray, scope_);
}
IndexMask FieldEvaluator::get_evaluated_selection_as_mask() const
{
BLI_assert(is_evaluated_);
return selection_mask_;
}
/** \} */
} // namespace blender::fn

View File

@@ -0,0 +1,71 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup fn
*/
#include "FN_lazy_function.hh"
namespace blender::fn::lazy_function {
std::string LazyFunction::name() const
{
return debug_name_;
}
std::string LazyFunction::input_name(int index) const
{
return inputs_[index].debug_name;
}
std::string LazyFunction::output_name(int index) const
{
return outputs_[index].debug_name;
}
void *LazyFunction::init_storage(LinearAllocator<> & /*allocator*/) const
{
return nullptr;
}
void LazyFunction::destruct_storage(void *storage) const
{
BLI_assert(storage == nullptr);
UNUSED_VARS_NDEBUG(storage);
}
void LazyFunction::possible_output_dependencies(const int /*output_index*/,
const FunctionRef<void(Span<int>)> fn) const
{
/* The output depends on all inputs by default. */
Vector<int, 16> indices(inputs_.size());
for (const int i : inputs_.index_range()) {
indices[i] = i;
}
fn(indices);
}
bool LazyFunction::always_used_inputs_available(const Params &params) const
{
if (allow_missing_requested_inputs_) {
return true;
}
for (const int i : inputs_.index_range()) {
const Input &fn_input = inputs_[i];
if (fn_input.usage == ValueUsage::Used) {
if (params.try_get_input_data_ptr(i) == nullptr) {
return false;
}
}
}
return true;
}
bool Params::try_enable_multi_threading_impl()
{
return false;
}
} // namespace blender::fn::lazy_function

View File

@@ -0,0 +1,144 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup fn
*/
#include "FN_lazy_function_execute.hh"
namespace blender::fn::lazy_function {
/* -------------------------------------------------------------------- */
/** \name BasicParams.
* \{ */
BasicParams::BasicParams(const LazyFunction &fn,
const Span<GMutablePointer> inputs,
const Span<GMutablePointer> outputs,
MutableSpan<std::optional<ValueUsage>> input_usages,
Span<ValueUsage> output_usages,
MutableSpan<bool> set_outputs)
: Params(fn, true),
inputs_(inputs),
outputs_(outputs),
input_usages_(input_usages),
output_usages_(output_usages),
set_outputs_(set_outputs)
{
}
void *BasicParams::try_get_input_data_ptr_impl(const int index) const
{
return inputs_[index].get();
}
void *BasicParams::try_get_input_data_ptr_or_request_impl(const int index)
{
void *value = inputs_[index].get();
if (value == nullptr) {
input_usages_[index] = ValueUsage::Used;
}
return value;
}
void *BasicParams::get_output_data_ptr_impl(const int index)
{
return outputs_[index].get();
}
void BasicParams::output_set_impl(const int index)
{
set_outputs_[index] = true;
}
bool BasicParams::output_was_set_impl(const int index) const
{
return set_outputs_[index];
}
ValueUsage BasicParams::get_output_usage_impl(const int index) const
{
return output_usages_[index];
}
void BasicParams::set_input_unused_impl(const int index)
{
input_usages_[index] = ValueUsage::Unused;
}
bool BasicParams::try_enable_multi_threading_impl()
{
return true;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name RemappedParams.
* \{ */
RemappedParams::RemappedParams(const LazyFunction &fn,
Params &base_params,
const Span<int> input_map,
const Span<int> output_map,
bool &multi_threading_enabled)
: Params(fn, multi_threading_enabled),
base_params_(base_params),
input_map_(input_map),
output_map_(output_map),
multi_threading_enabled_(multi_threading_enabled)
{
}
void *RemappedParams::try_get_input_data_ptr_impl(const int index) const
{
return base_params_.try_get_input_data_ptr(input_map_[index]);
}
void *RemappedParams::try_get_input_data_ptr_or_request_impl(const int index)
{
return base_params_.try_get_input_data_ptr_or_request(input_map_[index]);
}
void *RemappedParams::get_output_data_ptr_impl(const int index)
{
return base_params_.get_output_data_ptr(output_map_[index]);
}
void RemappedParams::output_set_impl(const int index)
{
base_params_.output_set(output_map_[index]);
}
bool RemappedParams::output_was_set_impl(const int index) const
{
return base_params_.output_was_set(output_map_[index]);
}
lf::ValueUsage RemappedParams::get_output_usage_impl(const int index) const
{
return base_params_.get_output_usage(output_map_[index]);
}
void RemappedParams::set_input_unused_impl(const int index)
{
base_params_.set_input_unused(input_map_[index]);
}
bool RemappedParams::try_enable_multi_threading_impl()
{
if (multi_threading_enabled_) {
return true;
}
if (base_params_.try_enable_multi_threading()) {
multi_threading_enabled_ = true;
return true;
}
return false;
}
/** \} */
} // namespace blender::fn::lazy_function

View File

@@ -0,0 +1,255 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_dot_export.hh"
#include "FN_lazy_function_graph.hh"
#include <sstream>
namespace blender::fn::lazy_function {
Graph::Graph(const StringRef name)
{
name_ = allocator_.copy_string(name);
graph_input_node_ = allocator_.construct<InterfaceNode>().release();
graph_output_node_ = allocator_.construct<InterfaceNode>().release();
nodes_.append(graph_input_node_);
nodes_.append(graph_output_node_);
}
Graph::~Graph()
{
for (FunctionNode *node : this->function_nodes()) {
for (InputSocket *socket : node->inputs_) {
std::destroy_at(socket);
}
for (OutputSocket *socket : node->outputs_) {
std::destroy_at(socket);
}
std::destroy_at(node);
}
for (const InterfaceNode *node : {graph_input_node_, graph_output_node_}) {
for (InputSocket *socket : node->inputs_) {
std::destroy_at(socket);
}
for (OutputSocket *socket : node->outputs_) {
std::destroy_at(socket);
}
std::destroy_at(node);
}
}
FunctionNode &Graph::add_function(const LazyFunction &fn)
{
const Span<Input> inputs = fn.inputs();
const Span<Output> outputs = fn.outputs();
FunctionNode &node = *allocator_.construct<FunctionNode>().release();
node.fn_ = &fn;
node.inputs_ = allocator_.construct_elements_and_pointer_array<InputSocket>(inputs.size());
node.outputs_ = allocator_.construct_elements_and_pointer_array<OutputSocket>(outputs.size());
for (const int i : inputs.index_range()) {
InputSocket &socket = *node.inputs_[i];
socket.index_in_node_ = i;
socket.is_input_ = true;
socket.node_ = &node;
socket.type_ = inputs[i].type;
}
for (const int i : outputs.index_range()) {
OutputSocket &socket = *node.outputs_[i];
socket.index_in_node_ = i;
socket.is_input_ = false;
socket.node_ = &node;
socket.type_ = outputs[i].type;
}
nodes_.append(&node);
return node;
}
GraphInputSocket &Graph::add_input(const CPPType &type, std::string name)
{
GraphInputSocket &socket = *allocator_.construct<GraphInputSocket>().release();
socket.is_input_ = false;
socket.node_ = graph_input_node_;
socket.type_ = &type;
socket.index_in_node_ = graph_inputs_.append_and_get_index(&socket);
graph_input_node_->outputs_ = graph_inputs_;
graph_input_node_->socket_names_.append(std::move(name));
return socket;
}
GraphOutputSocket &Graph::add_output(const CPPType &type, std::string name)
{
GraphOutputSocket &socket = *allocator_.construct<GraphOutputSocket>().release();
socket.is_input_ = true;
socket.node_ = graph_output_node_;
socket.type_ = &type;
socket.index_in_node_ = graph_outputs_.append_and_get_index(&socket);
graph_output_node_->inputs_ = graph_outputs_;
graph_output_node_->socket_names_.append(std::move(name));
return socket;
}
void Graph::add_link(OutputSocket &from, InputSocket &to)
{
BLI_assert(to.origin_ == nullptr);
BLI_assert(from.type_ == to.type_);
to.origin_ = &from;
from.targets_.append(&to);
}
void Graph::clear_origin(InputSocket &socket)
{
if (socket.origin_ != nullptr) {
socket.origin_->targets_.remove_first_occurrence_and_reorder(&socket);
socket.origin_ = nullptr;
}
}
void Graph::update_node_indices()
{
for (const int i : nodes_.index_range()) {
nodes_[i]->index_in_graph_ = i;
}
}
void Graph::update_socket_indices()
{
int socket_counter = 0;
for (const int i : nodes_.index_range()) {
for (InputSocket *socket : nodes_[i]->inputs()) {
socket->index_in_graph_ = socket_counter++;
}
for (OutputSocket *socket : nodes_[i]->outputs()) {
socket->index_in_graph_ = socket_counter++;
}
}
socket_num_ = socket_counter;
}
bool Graph::node_indices_are_valid() const
{
for (const int i : nodes_.index_range()) {
if (nodes_[i]->index_in_graph_ != i) {
return false;
}
}
return true;
}
std::string Socket::name() const
{
if (node_->is_function()) {
const FunctionNode &fn_node = static_cast<const FunctionNode &>(*node_);
const LazyFunction &fn = fn_node.function();
if (is_input_) {
return fn.input_name(index_in_node_);
}
return fn.output_name(index_in_node_);
}
const InterfaceNode &interface_node = *static_cast<const InterfaceNode *>(node_);
return interface_node.socket_names_[index_in_node_];
}
std::string Socket::detailed_name() const
{
std::stringstream ss;
ss << node_->name() << ":" << (is_input_ ? "IN" : "OUT") << ":" << index_in_node_ << ":"
<< this->name();
return ss.str();
}
std::string Node::name() const
{
if (this->is_function()) {
return fn_->name();
}
return "Interface";
}
std::string Graph::ToDotOptions::socket_name(const Socket &socket) const
{
return socket.name();
}
std::optional<std::string> Graph::ToDotOptions::socket_font_color(const Socket & /*socket*/) const
{
return std::nullopt;
}
void Graph::ToDotOptions::add_edge_attributes(const OutputSocket & /*from*/,
const InputSocket & /*to*/,
dot_export::DirectedEdge & /*dot_edge*/) const
{
}
std::string Graph::to_dot(const ToDotOptions &options) const
{
dot_export::DirectedGraph digraph;
digraph.set_rankdir(dot_export::Attr_rankdir::LeftToRight);
Map<const Node *, dot_export::NodeWithSocketsRef> dot_nodes;
for (const Node *node : nodes_) {
dot_export::Node &dot_node = digraph.new_node("");
if (node->is_interface()) {
dot_node.set_background_color("lightblue");
}
else {
dot_node.set_background_color("white");
}
dot_export::NodeWithSockets dot_node_with_sockets;
dot_node_with_sockets.node_name = node->name();
for (const InputSocket *socket : node->inputs()) {
dot_export::NodeWithSockets::Input &dot_input = dot_node_with_sockets.add_input(
options.socket_name(*socket));
dot_input.fontcolor = options.socket_font_color(*socket);
}
for (const OutputSocket *socket : node->outputs()) {
dot_export::NodeWithSockets::Output &dot_output = dot_node_with_sockets.add_output(
options.socket_name(*socket));
dot_output.fontcolor = options.socket_font_color(*socket);
}
dot_nodes.add_new(node, dot_export::NodeWithSocketsRef(dot_node, dot_node_with_sockets));
}
for (const Node *node : nodes_) {
for (const InputSocket *socket : node->inputs()) {
const dot_export::NodeWithSocketsRef &to_dot_node = dot_nodes.lookup(&socket->node());
const dot_export::NodePort to_dot_port = to_dot_node.input(socket->index());
if (const OutputSocket *origin = socket->origin()) {
dot_export::NodeWithSocketsRef &from_dot_node = dot_nodes.lookup(&origin->node());
dot_export::DirectedEdge &dot_edge = digraph.new_edge(
from_dot_node.output(origin->index()), to_dot_port);
options.add_edge_attributes(*origin, *socket, dot_edge);
}
else if (const void *default_value = socket->default_value()) {
const CPPType &type = socket->type();
std::string value_string;
if (type.is_printable()) {
value_string = type.to_string(default_value);
}
else {
value_string = type.name();
}
dot_export::Node &default_value_dot_node = digraph.new_node(value_string);
default_value_dot_node.set_shape(dot_export::Attr_shape::Ellipse);
default_value_dot_node.attributes.set("color", "#00000055");
digraph.new_edge(default_value_dot_node, to_dot_port);
}
}
}
return digraph.to_dot_string();
}
} // namespace blender::fn::lazy_function

View File

@@ -0,0 +1,141 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "FN_lazy_function_graph_executor.hh"
/* The entire executor is included here. Otherwise an additional indirection using forward
* declarations of #GenericGraphExecutor would be needed. However, there isn't really a point in
* having that because it's tightly coupled to #GraphExecutor anyway. It's only defined in a
* separate file for code organization purposes. */
#include "lazy_function_graph_executor_generic.hh"
namespace blender::fn::lazy_function {
GraphExecutor::GraphExecutor(const Graph &graph,
const Logger *logger,
const SideEffectProvider *side_effect_provider,
const NodeExecuteWrapper *node_execute_wrapper)
: GraphExecutor(graph,
Vector<const GraphInputSocket *>(graph.graph_inputs()),
Vector<const GraphOutputSocket *>(graph.graph_outputs()),
logger,
side_effect_provider,
node_execute_wrapper)
{
}
GraphExecutor::GraphExecutor(const Graph &graph,
Vector<const GraphInputSocket *> graph_inputs,
Vector<const GraphOutputSocket *> graph_outputs,
const Logger *logger,
const SideEffectProvider *side_effect_provider,
const NodeExecuteWrapper *node_execute_wrapper)
: graph_(graph),
graph_inputs_(std::move(graph_inputs)),
graph_outputs_(std::move(graph_outputs)),
graph_input_index_by_socket_index_(graph.graph_inputs().size(), -1),
graph_output_index_by_socket_index_(graph.graph_outputs().size(), -1),
logger_(logger),
side_effect_provider_(side_effect_provider),
node_execute_wrapper_(node_execute_wrapper)
{
debug_name_ = graph.name().c_str();
/* The graph executor can handle partial execution when there are still missing inputs. */
allow_missing_requested_inputs_ = true;
for (const int i : graph_inputs_.index_range()) {
const OutputSocket &socket = *graph_inputs_[i];
BLI_assert(socket.node().is_interface());
inputs_.append({"In", socket.type(), ValueUsage::Maybe});
graph_input_index_by_socket_index_[socket.index()] = i;
}
for (const int i : graph_outputs_.index_range()) {
const InputSocket &socket = *graph_outputs_[i];
BLI_assert(socket.node().is_interface());
outputs_.append({"Out", socket.type()});
graph_output_index_by_socket_index_[socket.index()] = i;
}
GenericExecutor::preprocess_graph(*this);
}
void GraphExecutor::execute_impl(Params &params, const Context &context) const
{
GenericExecutor &executor = *static_cast<GenericExecutor *>(context.storage);
executor.execute(params, context);
}
void *GraphExecutor::init_storage(LinearAllocator<> &allocator) const
{
GenericExecutor &executor = *allocator.construct<GenericExecutor>(*this).release();
return &executor;
}
void GraphExecutor::destruct_storage(void *storage) const
{
std::destroy_at(static_cast<GenericExecutor *>(storage));
}
std::string GraphExecutor::input_name(const int index) const
{
const lf::OutputSocket &socket = *graph_inputs_[index];
return socket.name();
}
std::string GraphExecutor::output_name(const int index) const
{
const lf::InputSocket &socket = *graph_outputs_[index];
return socket.name();
}
GraphExecutorLogger::LoggingEnabledState GraphExecutorLogger::get_logging_enabled_state(
const Context & /*context*/) const
{
return LoggingEnabledState{true};
}
void GraphExecutorLogger::log_socket_value(const Socket &socket,
const GPointer value,
const Context &context) const
{
UNUSED_VARS(socket, value, context);
}
void GraphExecutorLogger::log_before_node_execute(const FunctionNode &node,
const Params &params,
const Context &context) const
{
UNUSED_VARS(node, params, context);
}
void GraphExecutorLogger::log_after_node_execute(const FunctionNode &node,
const Params &params,
const Context &context) const
{
UNUSED_VARS(node, params, context);
}
Vector<const FunctionNode *> GraphExecutorSideEffectProvider::get_nodes_with_side_effects(
const Context &context) const
{
UNUSED_VARS(context);
return {};
}
void GraphExecutorLogger::dump_when_outputs_are_missing(const FunctionNode &node,
Span<const OutputSocket *> missing_sockets,
const Context &context) const
{
UNUSED_VARS(node, missing_sockets, context);
}
void GraphExecutorLogger::dump_when_input_is_set_twice(const InputSocket &target_socket,
const OutputSocket &from_socket,
const Context &context) const
{
UNUSED_VARS(target_socket, from_socket, context);
}
} // namespace blender::fn::lazy_function

View File

@@ -0,0 +1,178 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "FN_multi_function.hh"
#include "BLI_task.hh"
#include "BLI_threads.h"
namespace blender::fn::multi_function {
using ExecutionHints = MultiFunction::ExecutionHints;
void MultiFunction::hash_unique(UniqueHashBytes &hash) const
{
hash.add(this);
}
bool MultiFunction::equals(const MultiFunction &other) const
{
return this == &other;
}
ExecutionHints MultiFunction::execution_hints() const
{
return this->get_execution_hints();
}
ExecutionHints MultiFunction::get_execution_hints() const
{
return ExecutionHints{};
}
static bool supports_threading_by_slicing_params(const MultiFunction &fn)
{
for (const int i : fn.param_indices()) {
const ParamType param_type = fn.param_type(i);
if (ELEM(param_type.interface_type(),
ParamType::InterfaceType::Mutable,
ParamType::InterfaceType::Output))
{
if (param_type.data_type().is_vector()) {
return false;
}
}
}
return true;
}
static int64_t compute_grain_size(const ExecutionHints &hints, const IndexMask &mask)
{
int64_t grain_size = hints.min_grain_size;
if (hints.uniform_execution_time) {
const int thread_count = BLI_system_thread_count();
/* Avoid using a small grain size even if it is not necessary. */
const int64_t thread_based_grain_size = mask.size() / thread_count / 4;
grain_size = std::max(grain_size, thread_based_grain_size);
}
if (hints.allocates_array) {
const int64_t max_grain_size = 10000;
/* Avoid allocating many large intermediate arrays. Better process data in smaller chunks to
* keep peak memory usage lower. */
grain_size = std::min(grain_size, max_grain_size);
}
return grain_size;
}
static int64_t compute_alignment(const int64_t grain_size)
{
if (grain_size <= 512) {
/* Don't use a number that's too large, or otherwise the work will be split quite unevenly. */
return 8;
}
/* It's not common that more elements are processed in a loop at once. */
return 32;
}
static void add_sliced_parameters(const Signature &signature,
Params &full_params,
const IndexRange slice_range,
ParamsBuilder &r_sliced_params)
{
for (const int param_index : signature.params.index_range()) {
const ParamType &param_type = signature.params[param_index].type;
switch (param_type.category()) {
case ParamCategory::SingleInput: {
const GVArray &varray = full_params.readonly_single_input(param_index);
r_sliced_params.add_readonly_single_input(varray.slice(slice_range));
break;
}
case ParamCategory::SingleMutable: {
const GMutableSpan span = full_params.single_mutable(param_index);
const GMutableSpan sliced_span = span.slice(slice_range);
r_sliced_params.add_single_mutable(sliced_span);
break;
}
case ParamCategory::SingleOutput: {
if (flag_is_set(signature.params[param_index].flag, ParamFlag::SupportsUnusedOutput)) {
const GMutableSpan span = full_params.uninitialized_single_output_if_required(
param_index);
if (span.is_empty()) {
r_sliced_params.add_ignored_single_output();
}
else {
const GMutableSpan sliced_span = span.slice(slice_range);
r_sliced_params.add_uninitialized_single_output(sliced_span);
}
}
else {
const GMutableSpan span = full_params.uninitialized_single_output(param_index);
const GMutableSpan sliced_span = span.slice(slice_range);
r_sliced_params.add_uninitialized_single_output(sliced_span);
}
break;
}
case ParamCategory::VectorInput:
case ParamCategory::VectorMutable:
case ParamCategory::VectorOutput: {
BLI_assert_unreachable();
break;
}
}
}
}
void MultiFunction::call_auto(const IndexMask &mask, Params params, Context context) const
{
if (mask.is_empty()) {
return;
}
const ExecutionHints hints = this->execution_hints();
const int64_t grain_size = compute_grain_size(hints, mask);
if (mask.size() <= grain_size) {
this->call(mask, params, context);
return;
}
const bool supports_threading = supports_threading_by_slicing_params(*this);
if (!supports_threading) {
this->call(mask, params, context);
return;
}
const int64_t alignment = compute_alignment(grain_size);
threading::parallel_for_aligned(
mask.index_range(), grain_size, alignment, [&](const IndexRange sub_range) {
const IndexMask sliced_mask = mask.slice(sub_range);
if (!hints.allocates_array) {
/* There is no benefit to changing indices in this case. */
this->call(sliced_mask, params, context);
return;
}
if (sliced_mask[0] < grain_size) {
/* The indices are low, no need to offset them. */
this->call(sliced_mask, params, context);
return;
}
const int64_t input_slice_start = sliced_mask[0];
const int64_t input_slice_size = sliced_mask.last() - input_slice_start + 1;
const IndexRange input_slice_range{input_slice_start, input_slice_size};
IndexMaskMemory memory;
const int64_t offset = -input_slice_start;
const IndexMask shifted_mask = mask.slice_and_shift(sub_range, offset, memory);
ParamsBuilder sliced_params{*this, &shifted_mask};
add_sliced_parameters(*signature_ref_, params, input_slice_range, sliced_params);
this->call(shifted_mask, sliced_params, context);
});
}
std::string MultiFunction::debug_name() const
{
return signature_ref_->function_name;
}
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,133 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "FN_multi_function_builder.hh"
namespace blender::fn::multi_function {
CustomMF_GenericConstant::CustomMF_GenericConstant(const CPPType &type,
const void *value,
bool make_value_copy)
: type_(type), owns_value_(make_value_copy)
{
if (make_value_copy) {
void *copied_value = MEM_new_uninitialized_aligned(type.size, type.alignment, __func__);
type.copy_construct(value, copied_value);
value = copied_value;
}
value_ = value;
SignatureBuilder builder{"Constant", signature_};
builder.single_output("Value", type);
this->set_signature(&signature_);
}
CustomMF_GenericConstant::~CustomMF_GenericConstant()
{
if (owns_value_) {
signature_.params[0].type.data_type().single_type().destruct(const_cast<void *>(value_));
MEM_delete_void(const_cast<void *>(value_));
}
}
void CustomMF_GenericConstant::call(const IndexMask &mask,
Params params,
Context /*context*/) const
{
GMutableSpan output = params.uninitialized_single_output(0);
type_.fill_construct_indices(value_, output.data(), mask);
}
void CustomMF_GenericConstant::hash_unique(UniqueHashBytes &hash) const
{
hash.add(&HASH_ID);
type_.hash_unique(value_, hash);
hash.add(&type_);
}
bool CustomMF_GenericConstant::equals(const MultiFunction &other) const
{
const CustomMF_GenericConstant *_other = dynamic_cast<const CustomMF_GenericConstant *>(&other);
if (_other == nullptr) {
return false;
}
if (type_ != _other->type_) {
return false;
}
return type_.is_equal(value_, _other->value_);
}
CustomMF_GenericConstantArray::CustomMF_GenericConstantArray(GSpan array) : array_(array)
{
const CPPType &type = array.type();
SignatureBuilder builder{"Constant Vector", signature_};
builder.vector_output("Value", type);
this->set_signature(&signature_);
}
void CustomMF_GenericConstantArray::call(const IndexMask &mask,
Params params,
Context /*context*/) const
{
GVectorArray &vectors = params.vector_output(0);
mask.foreach_index([&](const int64_t i) { vectors.extend(i, array_); });
}
CustomMF_DefaultOutput::CustomMF_DefaultOutput(Span<DataType> input_types,
Span<DataType> output_types)
: output_amount_(output_types.size())
{
SignatureBuilder builder{"Default Output", signature_};
for (DataType data_type : input_types) {
builder.input("Input", data_type);
}
for (DataType data_type : output_types) {
builder.output("Output", data_type);
}
this->set_signature(&signature_);
}
void CustomMF_DefaultOutput::call(const IndexMask &mask, Params params, Context /*context*/) const
{
for (int param_index : this->param_indices()) {
ParamType param_type = this->param_type(param_index);
if (!param_type.is_output()) {
continue;
}
if (param_type.data_type().is_single()) {
GMutableSpan span = params.uninitialized_single_output(param_index);
const CPPType &type = span.type();
type.fill_construct_indices(type.default_value(), span.data(), mask);
}
}
}
CustomMF_GenericCopy::CustomMF_GenericCopy(DataType data_type)
{
SignatureBuilder builder{"Copy", signature_};
builder.input("Input", data_type);
builder.output("Output", data_type);
this->set_signature(&signature_);
}
void CustomMF_GenericCopy::call(const IndexMask &mask, Params params, Context /*context*/) const
{
const DataType data_type = this->param_type(0).data_type();
switch (data_type.category()) {
case DataType::Single: {
const GVArray &inputs = params.readonly_single_input(0, "Input");
GMutableSpan outputs = params.uninitialized_single_output(1, "Output");
inputs.materialize_to_uninitialized(mask, outputs.data());
break;
}
case DataType::Vector: {
const GVVectorArray &inputs = params.readonly_vector_input(0, "Input");
GVectorArray &outputs = params.vector_output(1, "Output");
outputs.extend(mask, inputs);
break;
}
}
}
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,678 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_base_safe.h"
#include "BLI_math_vector.hh"
#include "FN_init.hh"
#include "FN_multi_function_builder.hh"
#include "FN_multi_function_registry.hh"
#include <numeric>
namespace blender::fn::multi_function {
/**
* An multi-function for the powf operation that more optimally handles simple and
* common cases like raising to the power of 2.
*/
class PowFunction : public MultiFunction {
private:
static inline const MultiFunction *pow_generic = nullptr;
static inline const MultiFunction *pow_2 = nullptr;
static inline const MultiFunction *pow_3 = nullptr;
public:
PowFunction()
{
static Signature signature = []() {
pow_2 = &registry::lookup("float ** 2"_ustr);
pow_3 = &registry::lookup("float ** 3"_ustr);
static auto pow_generic_fn = build::SI2_SO<float, float, float>(
"pow generic",
[](const float a, const float b) { return safe_powf(a, b); },
build::exec_presets::Materialized());
pow_generic = &pow_generic_fn;
Signature signature;
SignatureBuilder builder("float ** float", signature);
builder.single_input<float>("Base");
builder.single_input<float>("Exponent");
builder.single_output<float>("Result");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, Params params, Context context) const override
{
/* Use GVArray here to avoid unnecessary conversions to typed virtual arrays. */
const GVArray &base = params.readonly_single_input(0, "Base");
const GVArray &exponent = params.readonly_single_input(1, "Exponent");
MutableSpan<float> result = params.uninitialized_single_output<float>(2, "Result");
if (exponent.is_single()) {
float exponent_single;
exponent.get_internal_single(&exponent_single);
const int exponent_int = int(exponent_single);
/* Handle some exponents without invoking the general powf function. */
if (float(exponent_int) == exponent_single) {
switch (exponent_int) {
case 0: {
index_mask::masked_fill(result, 1.0f, mask);
return;
}
case 1: {
base.materialize_to_uninitialized(mask, result.data());
return;
}
case 2: {
ParamsBuilder sub_params{*pow_2, &mask};
sub_params.add_readonly_single_input(base);
sub_params.add_uninitialized_single_output(result);
pow_2->call(mask, sub_params, context);
return;
}
case 3: {
ParamsBuilder sub_params{*pow_3, &mask};
sub_params.add_readonly_single_input(base);
sub_params.add_uninitialized_single_output(result);
pow_3->call(mask, sub_params, context);
return;
}
default: {
break;
}
}
}
}
pow_generic->call(mask, params, context);
}
};
class DivideFunction : public MultiFunction {
private:
static inline const MultiFunction *multiply = nullptr;
static inline const MultiFunction *divide_generic = nullptr;
public:
DivideFunction()
{
static Signature signature = []() {
multiply = &registry::lookup("float * float"_ustr);
static auto divide_generic_fn = build::SI2_SO<float, float, float>(
"float / float",
[](float a, float b) { return safe_divide(a, b); },
build::exec_presets::AllSpanOrSingle());
divide_generic = &divide_generic_fn;
Signature signature;
SignatureBuilder builder("float / float", signature);
builder.single_input<float>("A");
builder.single_input<float>("B");
builder.single_output<float>("Result");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, mf::Params params, mf::Context context) const override
{
const GVArray &a = params.readonly_single_input(0, "A");
const GVArray &b = params.readonly_single_input(1, "B");
MutableSpan<float> result = params.uninitialized_single_output<float>(2, "Result");
if (b.is_single()) {
float divisor;
b.get_internal_single(&divisor);
if (divisor == 0.0f) {
/* We define the output to be 0 for division by zero. Same as #safe_divide. */
index_mask::masked_fill(result, 0.0f, mask);
return;
}
if (divisor == 1.0f) {
/* If the divisor is 1 the result is the dividend. */
a.materialize_to_uninitialized(mask, result.data());
return;
}
if (is_inverse_exact(divisor)) {
/* Use multiplication by the inverse which is more efficient than division. */
const float inverse = 1.0f / divisor;
ParamsBuilder sub_params{*multiply, &mask};
sub_params.add_readonly_single_input(a);
sub_params.add_readonly_single_input_value(inverse);
sub_params.add_uninitialized_single_output(result);
multiply->call(mask, sub_params, context);
return;
}
}
if (a.is_single()) {
float dividend;
a.get_internal_single(&dividend);
if (dividend == 0.0f) {
/* If the dividend is zero the result is always zero regardless of the divisor. */
index_mask::masked_fill(result, 0.0f, mask);
return;
}
}
/* General case. */
divide_generic->call(mask, params, context);
}
static bool is_inverse_exact(float x)
{
BLI_assert(x != 0.0f);
x = fabsf(x);
int exp;
/* Check that x is a power of two. */
const float fraction = frexpf(x, &exp);
return fraction == 0.5f;
}
};
static void register_common_functions_impl()
{
static constexpr auto exec_fast = build::exec_presets::AllSpanOrSingle();
registry::add_new_cb([]() {
return build::SI1_SO<float, float>(
"float ** 2", [](const float a) { return a * a; }, exec_fast);
});
registry::add_new_cb([]() {
return build::SI1_SO<float, float>(
"float ** 3", [](const float a) { return a * a * a; }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("exp(float)", [](const float a) { return expf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"sqrt(float)", [](const float a) { return safe_sqrtf(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"inverse_sqrt(float)", [](const float a) { return safe_inverse_sqrtf(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"abs(float)", [](const float a) { return fabsf(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"radians(float)", [](const float a) { return float(DEG2RAD(a)); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"degrees(float)", [](const float a) { return float(RAD2DEG(a)); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"sign(float)", [](const float a) { return compatible_signf(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"round(float)", [](const float a) { return floorf(a + 0.5f); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"floor(float)", [](const float a) { return floorf(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"ceil(float)", [](const float a) { return ceilf(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"frac(float)", [](const float a) { return a - floorf(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>(
"trunc(float)", [](const float a) { return a >= 0.0f ? floorf(a) : ceilf(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("sin(float)", [](const float a) { return sinf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("cos(float)", [](const float a) { return cosf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("tan(float)", [](const float a) { return tanf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("sinh(float)", [](const float a) { return sinhf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("cosh(float)", [](const float a) { return coshf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("tanh(float)", [](const float a) { return tanhf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("asin(float)", [](const float a) { return safe_asinf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("acos(float)", [](const float a) { return safe_acosf(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float, float>("atan(float)", [](const float a) { return atanf(a); });
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"float + float", [](const float a, const float b) { return a + b; }, exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"float - float", [](const float a, const float b) { return a - b; }, exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"float * float", [](const float a, const float b) { return a * b; }, exec_fast);
});
registry::add_new_cb([] { return DivideFunction(); });
registry::add_new_cb([] { return PowFunction(); });
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"log(float, float)",
[](const float a, const float b) { return safe_logf(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"min(float, float)",
[](const float a, const float b) { return std::min(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"max(float, float)",
[](const float a, const float b) { return std::max(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"float(float < float)",
[](const float a, const float b) { return float(a < b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"float(float > float)",
[](const float a, const float b) { return float(a > b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"float % float", [](const float a, const float b) { return safe_modf(a, b); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"floor_mod(float, float)",
[](const float a, const float b) { return safe_floored_modf(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"snap(float, float)",
[](const float a, const float b) { return floorf(safe_divide(a, b)) * b; },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"atan2(float, float)",
[](const float a, const float b) { return atan2f(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float, float, float>(
"pingpong(float, float)",
[](const float a, const float b) { return pingpongf(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI3_SO<float, float, float, float>(
"float * float + float",
[](const float a, const float b, const float c) { return a * b + c; },
exec_fast);
});
registry::add_new_cb([] {
return build::SI3_SO<float, float, float, float>(
"compare(float, float, float)",
[](const float a, const float b, const float c) {
return ((a == b) || (fabsf(a - b) <= fmaxf(c, FLT_EPSILON))) ? 1.0f : 0.0f;
},
exec_fast);
});
registry::add_new_cb([] {
return build::SI3_SO<float, float, float, float>(
"smooth_min(float, float, float)",
[](const float a, const float b, const float c) { return smoothminf(a, b, c); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI3_SO<float, float, float, float>(
"smooth_max(float, float, float)",
[](const float a, const float b, const float c) { return -smoothminf(-a, -b, c); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI3_SO<float, float, float, float>(
"wrap(float, float, float)",
[](const float a, const float b, const float c) { return wrapf(a, b, c); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"float3 + float3", [](const float3 &a, const float3 &b) { return a + b; }, exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"float3 - float3", [](const float3 &a, const float3 &b) { return a - b; }, exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"float3 * float3", [](const float3 &a, const float3 &b) { return a * b; }, exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"float3 / float3",
[](const float3 &a, const float3 &b) { return math::safe_divide(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"cross_product(float3, float3)",
[](const float3 &a, const float3 &b) { return math::cross_high_precision(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"project(float3, float3)",
[](const float3 &a, const float3 &b) { return math::project(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"reflect(float3, float3)",
[](const float3 &a, const float3 &b) { return math::reflect(a, math::normalize(b)); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"snap(float3, float3)",
[](const float3 &a, const float3 &b) { return math::floor(math::safe_divide(a, b)) * b; },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"float3 % float3", [](const float3 &a, const float3 &b) { return math::safe_mod(a, b); });
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"min(float3, float3)",
[](const float3 &a, const float3 &b) { return math::min(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>(
"max(float3, float3)",
[](const float3 &a, const float3 &b) { return math::max(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float3>("float3 ** float3", [](float3 a, float3 b) {
return float3(safe_powf(a.x, b.x), safe_powf(a.y, b.y), safe_powf(a.z, b.z));
});
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float>(
"dot_product(float3, float3)",
[](const float3 &a, const float3 &b) { return math::dot(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float3, float>(
"distance(float3, float3)",
[](const float3 &a, const float3 &b) { return math::distance(a, b); },
exec_fast);
});
registry::add_new_cb([] {
return build::SI3_SO<float3, float3, float3, float3>(
"float3 * float3 + float3",
[](const float3 &a, const float3 &b, const float3 &c) { return a * b + c; },
exec_fast);
});
registry::add_new_cb([] {
return build::SI3_SO<float3, float3, float3, float3>(
"wrap(float3, float3, float3)", [](const float3 &a, const float3 &b, const float3 &c) {
return float3(wrapf(a.x, b.x, c.x), wrapf(a.y, b.y, c.y), wrapf(a.z, b.z, c.z));
});
});
registry::add_new_cb([] {
return build::SI3_SO<float3, float3, float3, float3>(
"faceforward(float3, float3, float3)",
[](const float3 &a, const float3 &b, const float3 &c) {
return math::faceforward(a, b, c);
},
exec_fast);
});
registry::add_new_cb([] {
return build::SI3_SO<float3, float3, float, float3>(
"refract(float3, float3, float)", [](const float3 &a, const float3 &b, float c) {
return math::refract(a, math::normalize(b), c);
});
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float>(
"length(float3)", [](const float3 &a) { return math::length(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<float3, float, float3>(
"float3 * float", [](const float3 &a, float b) { return a * b; }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>(
"normalize(float3)", [](const float3 &a) { return math::normalize(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>(
"round(float3)", [](const float3 &a) { return math::floor(a + 0.5f); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>("floor(float3)",
[](const float3 &a) { return math::floor(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>("ceil(float3)",
[](const float3 &a) { return math::ceil(a); });
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>(
"frac(float3)", [](const float3 &a) { return math::fract(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>(
"abs(float3)", [](const float3 &a) { return math::abs(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>(
"sign(float3)", [](const float3 &a) { return math::sign(a); }, exec_fast);
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>(
"sin(float3)", [](const float3 &a) { return float3(sinf(a.x), sinf(a.y), sinf(a.z)); });
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>(
"cos(float3)", [](const float3 &a) { return float3(cosf(a.x), cosf(a.y), cosf(a.z)); });
});
registry::add_new_cb([] {
return build::SI1_SO<float3, float3>(
"tan(float3)", [](const float3 &a) { return float3(tanf(a.x), tanf(a.y), tanf(a.z)); });
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int + int", [](int a, int b) { return a + b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int - int", [](int a, int b) { return a - b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int * int", [](int a, int b) { return a * b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int / int", [](int a, int b) { return math::safe_divide(a, b); }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"floor(int, int)",
[](int a, int b) { return (b != 0) ? divide_floor_i(a, b) : 0; },
exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"divide_ceil(int, int)",
[](int a, int b) { return (b != 0) ? -divide_floor_i(a, -b) : 0; },
exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"divide_round(int, int)",
[](int a, int b) {
/* Derived from `divide_round_i` but fixed to be safe and handle negative inputs. */
const int c = math::abs(b);
return (a >= 0) ? math::safe_divide((2 * a + c), (2 * c)) * math::sign(b) :
-math::safe_divide((2 * -a + c), (2 * c)) * math::sign(b);
},
exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int ** int", [](int a, int b) { return math::pow(a, b); }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI3_SO<int, int, int, int>(
"int * int + int", [](int a, int b, int c) { return a * b + c; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"mod_periodic(int, int)",
[](int a, int b) { return b != 0 ? math::mod_periodic(a, b) : 0; },
exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int % int", [](int a, int b) { return b != 0 ? a % b : 0; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI1_SO<int, int>("abs(int)", [](int a) { return math::abs(a); }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI1_SO<int, int>(
"sign(int)", [](int a) { return math::sign(a); }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"min(int, int)", [](int a, int b) { return math::min(a, b); }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"max(int, int)", [](int a, int b) { return math::max(a, b); }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>("gcd(int, int)",
[](int a, int b) { return std::gcd(a, b); });
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>("lcm(int, int)",
[](int a, int b) { return std::lcm(a, b); });
});
registry::add_new_cb(
[] { return mf::build::SI1_SO<int, int>("-int", [](int a) { return -a; }, exec_fast); });
registry::add_new_cb([] {
return mf::build::SI2_SO<bool, bool, bool>(
"bool && bool", [](bool a, bool b) { return a && b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<bool, bool, bool>(
"bool || bool", [](bool a, bool b) { return a || b; }, exec_fast);
});
registry::add_new_cb(
[] { return mf::build::SI1_SO<bool, bool>("!bool", [](bool a) { return !a; }, exec_fast); });
registry::add_new_cb([] {
return mf::build::SI2_SO<bool, bool, bool>(
"!(bool && bool)", [](bool a, bool b) { return !(a && b); }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<bool, bool, bool>(
"!(bool || bool)", [](bool a, bool b) { return !(a || b); }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<bool, bool, bool>(
"bool == bool", [](bool a, bool b) { return a == b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<bool, bool, bool>(
"bool != bool", [](bool a, bool b) { return a != b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<bool, bool, bool>(
"!bool || bool", [](bool a, bool b) { return !a || b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<bool, bool, bool>(
"bool && !bool", [](bool a, bool b) { return a && !b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int & int", [](int a, int b) { return a & b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int | int", [](int a, int b) { return a | b; }, exec_fast);
});
registry::add_new_cb([] {
return mf::build::SI2_SO<int, int, int>(
"int ^ int", [](int a, int b) { return a ^ b; }, exec_fast);
});
registry::add_new_cb(
[] { return build::SI1_SO<int, int>("~int", [](int a) { return ~a; }, exec_fast); });
registry::add_new_cb([] {
return build::SI2_SO<int, int, int>(
"shift(int, int)",
[](int a, int b) {
const uint32_t value = a;
const int shift = math::clamp(b, -32, 32);
const uint64_t wide_value = uint64_t(value) << 16;
const uint64_t wide_result = shift > 0 ? wide_value << shift : wide_value >> -shift;
return uint32_t(wide_result >> 16);
},
exec_fast);
});
registry::add_new_cb([] {
return build::SI2_SO<int, int, int>(
"rotate(int, int)",
[](int a, int b) {
const uint32_t value = a;
const int shift = math::mod_periodic(b, 32);
const uint64_t wide_value = uint64_t(value) | (uint64_t(value) << 32);
const uint64_t double_result = (wide_value << shift);
return uint32_t((double_result | (double_result >> 32)) & ((uint64_t(1) << 33) - 1));
},
exec_fast);
});
}
void register_common_functions()
{
/* Make sure the functions are only registered once even if called multiple times. */
[[maybe_unused]] static bool registered = []() {
register_common_functions_impl();
return true;
}();
}
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,21 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "FN_multi_function_params.hh"
namespace blender::fn::multi_function {
void ParamsBuilder::add_unused_output_for_unsupporting_function(const CPPType &type)
{
ResourceScope &scope = this->resource_scope();
void *buffer = scope.allocator().allocate_array(type, min_array_size_);
const GMutableSpan span{type, buffer, min_array_size_};
actual_params_.append_unchecked_as(std::in_place_type<GMutableSpan>, span);
if (!type.is_trivially_destructible) {
scope.add_destruct_call(
[&type, buffer, mask = mask_]() { type.destruct_indices(buffer, mask); });
}
}
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,878 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "FN_multi_function_procedure.hh"
#include "BLI_dot_export.hh"
#include "BLI_stack.hh"
#include <sstream>
namespace blender::fn::multi_function {
void InstructionCursor::set_next(Procedure &procedure, Instruction *new_instruction) const
{
switch (type_) {
case Type::None: {
break;
}
case Type::Entry: {
procedure.set_entry(*new_instruction);
break;
}
case Type::Call: {
static_cast<CallInstruction *>(instruction_)->set_next(new_instruction);
break;
}
case Type::Branch: {
BranchInstruction &branch_instruction = *static_cast<BranchInstruction *>(instruction_);
if (branch_output_) {
branch_instruction.set_branch_true(new_instruction);
}
else {
branch_instruction.set_branch_false(new_instruction);
}
break;
}
case Type::Destruct: {
static_cast<DestructInstruction *>(instruction_)->set_next(new_instruction);
break;
}
case Type::Dummy: {
static_cast<DummyInstruction *>(instruction_)->set_next(new_instruction);
break;
}
}
}
Instruction *InstructionCursor::next(Procedure &procedure) const
{
switch (type_) {
case Type::None:
return nullptr;
case Type::Entry:
return procedure.entry();
case Type::Call:
return static_cast<CallInstruction *>(instruction_)->next();
case Type::Branch: {
BranchInstruction &branch_instruction = *static_cast<BranchInstruction *>(instruction_);
if (branch_output_) {
return branch_instruction.branch_true();
}
return branch_instruction.branch_false();
}
case Type::Destruct:
return static_cast<DestructInstruction *>(instruction_)->next();
case Type::Dummy:
return static_cast<DummyInstruction *>(instruction_)->next();
}
return nullptr;
}
void Variable::set_name(std::string name)
{
name_ = std::move(name);
}
void CallInstruction::set_next(Instruction *instruction)
{
if (next_ != nullptr) {
next_->prev_.remove_first_occurrence_and_reorder(*this);
}
if (instruction != nullptr) {
instruction->prev_.append(*this);
}
next_ = instruction;
}
void CallInstruction::set_param_variable(int param_index, Variable *variable)
{
if (params_[param_index] != nullptr) {
params_[param_index]->users_.remove_first_occurrence_and_reorder(this);
}
if (variable != nullptr) {
#ifndef NDEBUG
const ParamType param_type = fn_->param_type(param_index);
BLI_assert(param_type.data_type() == variable->data_type());
#endif
variable->users_.append(this);
}
params_[param_index] = variable;
}
void CallInstruction::set_params(Span<Variable *> variables)
{
BLI_assert(variables.size() == params_.size());
for (const int i : variables.index_range()) {
this->set_param_variable(i, variables[i]);
}
}
void BranchInstruction::set_condition(Variable *variable)
{
if (condition_ != nullptr) {
condition_->users_.remove_first_occurrence_and_reorder(this);
}
if (variable != nullptr) {
variable->users_.append(this);
}
condition_ = variable;
}
void BranchInstruction::set_branch_true(Instruction *instruction)
{
if (branch_true_ != nullptr) {
branch_true_->prev_.remove_first_occurrence_and_reorder({*this, true});
}
if (instruction != nullptr) {
instruction->prev_.append({*this, true});
}
branch_true_ = instruction;
}
void BranchInstruction::set_branch_false(Instruction *instruction)
{
if (branch_false_ != nullptr) {
branch_false_->prev_.remove_first_occurrence_and_reorder({*this, false});
}
if (instruction != nullptr) {
instruction->prev_.append({*this, false});
}
branch_false_ = instruction;
}
void DestructInstruction::set_variable(Variable *variable)
{
if (variable_ != nullptr) {
variable_->users_.remove_first_occurrence_and_reorder(this);
}
if (variable != nullptr) {
variable->users_.append(this);
}
variable_ = variable;
}
void DestructInstruction::set_next(Instruction *instruction)
{
if (next_ != nullptr) {
next_->prev_.remove_first_occurrence_and_reorder(*this);
}
if (instruction != nullptr) {
instruction->prev_.append(*this);
}
next_ = instruction;
}
void DummyInstruction::set_next(Instruction *instruction)
{
if (next_ != nullptr) {
next_->prev_.remove_first_occurrence_and_reorder(*this);
}
if (instruction != nullptr) {
instruction->prev_.append(*this);
}
next_ = instruction;
}
Variable &Procedure::new_variable(DataType data_type, std::string name)
{
Variable &variable = *allocator_.construct<Variable>().release();
variable.name_ = std::move(name);
variable.data_type_ = data_type;
variable.index_in_graph_ = variables_.size();
variables_.append(&variable);
return variable;
}
CallInstruction &Procedure::new_call_instruction(const MultiFunction &fn)
{
CallInstruction &instruction = *allocator_.construct<CallInstruction>().release();
instruction.type_ = InstructionType::Call;
instruction.fn_ = &fn;
instruction.params_ = allocator_.allocate_array<Variable *>(fn.param_amount());
instruction.params_.fill(nullptr);
call_instructions_.append(&instruction);
return instruction;
}
BranchInstruction &Procedure::new_branch_instruction()
{
BranchInstruction &instruction = *allocator_.construct<BranchInstruction>().release();
instruction.type_ = InstructionType::Branch;
branch_instructions_.append(&instruction);
return instruction;
}
DestructInstruction &Procedure::new_destruct_instruction()
{
DestructInstruction &instruction = *allocator_.construct<DestructInstruction>().release();
instruction.type_ = InstructionType::Destruct;
destruct_instructions_.append(&instruction);
return instruction;
}
DummyInstruction &Procedure::new_dummy_instruction()
{
DummyInstruction &instruction = *allocator_.construct<DummyInstruction>().release();
instruction.type_ = InstructionType::Dummy;
dummy_instructions_.append(&instruction);
return instruction;
}
ReturnInstruction &Procedure::new_return_instruction()
{
ReturnInstruction &instruction = *allocator_.construct<ReturnInstruction>().release();
instruction.type_ = InstructionType::Return;
return_instructions_.append(&instruction);
return instruction;
}
void Procedure::add_parameter(ParamType::InterfaceType interface_type, Variable &variable)
{
params_.append({interface_type, &variable});
}
void Procedure::set_entry(Instruction &entry)
{
if (entry_ != nullptr) {
entry_->prev_.remove_first_occurrence_and_reorder(InstructionCursor::ForEntry());
}
entry_ = &entry;
entry_->prev_.append(InstructionCursor::ForEntry());
}
Procedure::~Procedure()
{
for (CallInstruction *instruction : call_instructions_) {
instruction->~CallInstruction();
}
for (BranchInstruction *instruction : branch_instructions_) {
instruction->~BranchInstruction();
}
for (DestructInstruction *instruction : destruct_instructions_) {
instruction->~DestructInstruction();
}
for (DummyInstruction *instruction : dummy_instructions_) {
instruction->~DummyInstruction();
}
for (ReturnInstruction *instruction : return_instructions_) {
instruction->~ReturnInstruction();
}
for (Variable *variable : variables_) {
variable->~Variable();
}
}
bool Procedure::validate() const
{
if (entry_ == nullptr) {
return false;
}
if (!this->validate_all_instruction_pointers_set()) {
return false;
}
if (!this->validate_all_params_provided()) {
return false;
}
if (!this->validate_same_variables_in_one_call()) {
return false;
}
if (!this->validate_parameters()) {
return false;
}
if (!this->validate_initialization()) {
return false;
}
return true;
}
void Procedure::prepare_for_execution()
{
for (const CallInstruction *instruction : call_instructions_) {
instruction->fn().prepare_for_execution();
}
}
bool Procedure::validate_all_instruction_pointers_set() const
{
for (const CallInstruction *instruction : call_instructions_) {
if (instruction->next_ == nullptr) {
return false;
}
}
for (const DestructInstruction *instruction : destruct_instructions_) {
if (instruction->next_ == nullptr) {
return false;
}
}
for (const BranchInstruction *instruction : branch_instructions_) {
if (instruction->branch_true_ == nullptr) {
return false;
}
if (instruction->branch_false_ == nullptr) {
return false;
}
}
for (const DummyInstruction *instruction : dummy_instructions_) {
if (instruction->next_ == nullptr) {
return false;
}
}
return true;
}
bool Procedure::validate_all_params_provided() const
{
for (const CallInstruction *instruction : call_instructions_) {
const MultiFunction &fn = instruction->fn();
for (const int param_index : fn.param_indices()) {
const ParamType param_type = fn.param_type(param_index);
if (param_type.category() == ParamCategory::SingleOutput) {
/* Single outputs are optional. */
continue;
}
const Variable *variable = instruction->params_[param_index];
if (variable == nullptr) {
return false;
}
}
}
for (const BranchInstruction *instruction : branch_instructions_) {
if (instruction->condition_ == nullptr) {
return false;
}
}
for (const DestructInstruction *instruction : destruct_instructions_) {
if (instruction->variable_ == nullptr) {
return false;
}
}
return true;
}
bool Procedure::validate_same_variables_in_one_call() const
{
for (const CallInstruction *instruction : call_instructions_) {
const MultiFunction &fn = *instruction->fn_;
for (const int param_index : fn.param_indices()) {
const ParamType param_type = fn.param_type(param_index);
const Variable *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;
}
const Variable *other_variable = instruction->params_[other_param_index];
if (other_variable != variable) {
continue;
}
if (ELEM(param_type.interface_type(), ParamType::Mutable, ParamType::Output)) {
/* When a variable is used as mutable or output parameter, it can only be used once. */
return false;
}
const ParamType other_param_type = fn.param_type(other_param_index);
/* A variable is allowed to be used as input more than once. */
if (other_param_type.interface_type() != ParamType::Input) {
return false;
}
}
}
}
return true;
}
bool Procedure::validate_parameters() const
{
Set<const Variable *> variables;
for (const Parameter &param : params_) {
/* One variable cannot be used as multiple parameters. */
if (!variables.add(param.variable)) {
return false;
}
}
return true;
}
bool Procedure::validate_initialization() const
{
/* TODO: Issue warning when it maybe wrongly initialized. */
for (const DestructInstruction *instruction : destruct_instructions_) {
const Variable &variable = *instruction->variable_;
const InitState state = this->find_initialization_state_before_instruction(*instruction,
variable);
if (!state.can_be_initialized) {
return false;
}
}
for (const BranchInstruction *instruction : branch_instructions_) {
const Variable &variable = *instruction->condition_;
const InitState state = this->find_initialization_state_before_instruction(*instruction,
variable);
if (!state.can_be_initialized) {
return false;
}
}
for (const CallInstruction *instruction : call_instructions_) {
const MultiFunction &fn = *instruction->fn_;
for (const int param_index : fn.param_indices()) {
const ParamType param_type = fn.param_type(param_index);
/* If the parameter was an unneeded output, it could be null. */
if (!instruction->params_[param_index]) {
continue;
}
const Variable &variable = *instruction->params_[param_index];
const InitState state = this->find_initialization_state_before_instruction(*instruction,
variable);
switch (param_type.interface_type()) {
case ParamType::Input:
case ParamType::Mutable: {
if (!state.can_be_initialized) {
return false;
}
break;
}
case ParamType::Output: {
if (!state.can_be_uninitialized) {
return false;
}
break;
}
}
}
}
Set<const Variable *> variables_that_should_be_initialized_on_return;
for (const Parameter &param : params_) {
if (ELEM(param.type, ParamType::Mutable, ParamType::Output)) {
variables_that_should_be_initialized_on_return.add_new(param.variable);
}
}
for (const ReturnInstruction *instruction : return_instructions_) {
for (const Variable *variable : variables_) {
const InitState init_state = this->find_initialization_state_before_instruction(*instruction,
*variable);
if (variables_that_should_be_initialized_on_return.contains(variable)) {
if (!init_state.can_be_initialized) {
return false;
}
}
else {
if (!init_state.can_be_uninitialized) {
return false;
}
}
}
}
return true;
}
Procedure::InitState Procedure::find_initialization_state_before_instruction(
const Instruction &target_instruction, const Variable &target_variable) const
{
InitState state;
auto check_entry_instruction = [&]() {
bool caller_initialized_variable = false;
for (const Parameter &param : params_) {
if (param.variable == &target_variable) {
if (ELEM(param.type, ParamType::Input, ParamType::Mutable)) {
caller_initialized_variable = true;
break;
}
}
}
if (caller_initialized_variable) {
state.can_be_initialized = true;
}
else {
state.can_be_uninitialized = true;
}
};
if (&target_instruction == entry_) {
check_entry_instruction();
}
Set<const Instruction *> checked_instructions;
Stack<const Instruction *> instructions_to_check;
for (const InstructionCursor &cursor : target_instruction.prev_) {
if (cursor.instruction() != nullptr) {
instructions_to_check.push(cursor.instruction());
}
}
while (!instructions_to_check.is_empty()) {
const Instruction &instruction = *instructions_to_check.pop();
if (!checked_instructions.add(&instruction)) {
/* Skip if the instruction has been checked already. */
continue;
}
bool state_modified = false;
switch (instruction.type_) {
case InstructionType::Call: {
const CallInstruction &call_instruction = static_cast<const CallInstruction &>(
instruction);
const MultiFunction &fn = *call_instruction.fn_;
for (const int param_index : fn.param_indices()) {
if (call_instruction.params_[param_index] == &target_variable) {
const ParamType param_type = fn.param_type(param_index);
if (param_type.interface_type() == ParamType::Output) {
state.can_be_initialized = true;
state_modified = true;
break;
}
}
}
break;
}
case InstructionType::Destruct: {
const DestructInstruction &destruct_instruction = static_cast<const DestructInstruction &>(
instruction);
if (destruct_instruction.variable_ == &target_variable) {
state.can_be_uninitialized = true;
state_modified = true;
}
break;
}
case InstructionType::Branch:
case InstructionType::Dummy:
case InstructionType::Return: {
/* These instruction types don't change the initialization state of variables. */
break;
}
}
if (!state_modified) {
if (&instruction == entry_) {
check_entry_instruction();
}
for (const InstructionCursor &cursor : instruction.prev_) {
if (cursor.instruction() != nullptr) {
instructions_to_check.push(cursor.instruction());
}
}
}
}
return state;
}
class ProcedureDotExport {
private:
const Procedure &procedure_;
dot_export::DirectedGraph digraph_;
Map<const Instruction *, dot_export::Node *> dot_nodes_by_begin_;
Map<const Instruction *, dot_export::Node *> dot_nodes_by_end_;
public:
ProcedureDotExport(const Procedure &procedure) : procedure_(procedure) {}
std::string generate()
{
this->create_nodes();
this->create_edges();
return digraph_.to_dot_string();
}
void create_nodes()
{
Vector<const Instruction *> all_instructions;
auto add_instructions = [&](auto instructions) {
all_instructions.extend(instructions.begin(), instructions.end());
};
add_instructions(procedure_.call_instructions_);
add_instructions(procedure_.branch_instructions_);
add_instructions(procedure_.destruct_instructions_);
add_instructions(procedure_.dummy_instructions_);
add_instructions(procedure_.return_instructions_);
Set<const Instruction *> handled_instructions;
for (const Instruction *representative : all_instructions) {
if (handled_instructions.contains(representative)) {
continue;
}
Vector<const Instruction *> block_instructions = this->get_instructions_in_block(
*representative);
std::stringstream ss;
ss << "<";
for (const Instruction *current : block_instructions) {
handled_instructions.add_new(current);
switch (current->type()) {
case InstructionType::Call: {
this->instruction_to_string(*static_cast<const CallInstruction *>(current), ss);
break;
}
case InstructionType::Destruct: {
this->instruction_to_string(*static_cast<const DestructInstruction *>(current), ss);
break;
}
case InstructionType::Dummy: {
this->instruction_to_string(*static_cast<const DummyInstruction *>(current), ss);
break;
}
case InstructionType::Return: {
this->instruction_to_string(*static_cast<const ReturnInstruction *>(current), ss);
break;
}
case InstructionType::Branch: {
this->instruction_to_string(*static_cast<const BranchInstruction *>(current), ss);
break;
}
}
ss << R"(<br align="left" />)";
}
ss << ">";
dot_export::Node &dot_node = digraph_.new_node(ss.str());
dot_node.set_shape(dot_export::Attr_shape::Rectangle);
dot_nodes_by_begin_.add_new(block_instructions.first(), &dot_node);
dot_nodes_by_end_.add_new(block_instructions.last(), &dot_node);
}
}
void create_edges()
{
auto create_edge = [&](dot_export::Node &from_node,
const Instruction *to_instruction) -> dot_export::DirectedEdge & {
if (to_instruction == nullptr) {
dot_export::Node &to_node = digraph_.new_node("missing");
to_node.set_shape(dot_export::Attr_shape::Diamond);
return digraph_.new_edge(from_node, to_node);
}
dot_export::Node &to_node = *dot_nodes_by_begin_.lookup(to_instruction);
return digraph_.new_edge(from_node, to_node);
};
for (auto item : dot_nodes_by_end_.items()) {
const Instruction &from_instruction = *item.key;
dot_export::Node &from_node = *item.value;
switch (from_instruction.type()) {
case InstructionType::Call: {
const Instruction *to_instruction =
static_cast<const CallInstruction &>(from_instruction).next();
create_edge(from_node, to_instruction);
break;
}
case InstructionType::Destruct: {
const Instruction *to_instruction =
static_cast<const DestructInstruction &>(from_instruction).next();
create_edge(from_node, to_instruction);
break;
}
case InstructionType::Dummy: {
const Instruction *to_instruction =
static_cast<const DummyInstruction &>(from_instruction).next();
create_edge(from_node, to_instruction);
break;
}
case InstructionType::Return: {
break;
}
case InstructionType::Branch: {
const BranchInstruction &branch_instruction = static_cast<const BranchInstruction &>(
from_instruction);
const Instruction *to_true_instruction = branch_instruction.branch_true();
const Instruction *to_false_instruction = branch_instruction.branch_false();
create_edge(from_node, to_true_instruction).attributes.set("color", "#118811");
create_edge(from_node, to_false_instruction).attributes.set("color", "#881111");
break;
}
}
}
dot_export::Node &entry_node = this->create_entry_node();
create_edge(entry_node, procedure_.entry());
}
bool has_to_be_block_begin(const Instruction &instruction)
{
if (instruction.prev().size() != 1) {
return true;
}
if (ELEM(instruction.prev()[0].type(),
InstructionCursor::Type::Branch,
InstructionCursor::Type::Entry))
{
return true;
}
return false;
}
const Instruction &get_first_instruction_in_block(const Instruction &representative)
{
const Instruction *current = &representative;
while (!this->has_to_be_block_begin(*current)) {
current = current->prev()[0].instruction();
if (current == &representative) {
/* There is a loop without entry or exit, just break it up here. */
break;
}
}
return *current;
}
const Instruction *get_next_instruction_in_block(const Instruction &instruction,
const Instruction &block_begin)
{
const Instruction *next = nullptr;
switch (instruction.type()) {
case InstructionType::Call: {
next = static_cast<const CallInstruction &>(instruction).next();
break;
}
case InstructionType::Destruct: {
next = static_cast<const DestructInstruction &>(instruction).next();
break;
}
case InstructionType::Dummy: {
next = static_cast<const DummyInstruction &>(instruction).next();
break;
}
case InstructionType::Return:
case InstructionType::Branch: {
break;
}
}
if (next == nullptr) {
return nullptr;
}
if (next == &block_begin) {
return nullptr;
}
if (this->has_to_be_block_begin(*next)) {
return nullptr;
}
return next;
}
Vector<const Instruction *> get_instructions_in_block(const Instruction &representative)
{
Vector<const Instruction *> instructions;
const Instruction &begin = this->get_first_instruction_in_block(representative);
for (const Instruction *current = &begin; current != nullptr;
current = this->get_next_instruction_in_block(*current, begin))
{
instructions.append(current);
}
return instructions;
}
void variable_to_string(const Variable *variable, std::stringstream &ss)
{
if (variable == nullptr) {
ss << "null";
}
else {
ss << "$" << variable->index_in_procedure();
if (!variable->name().is_empty()) {
ss << "(" << variable->name() << ")";
}
}
}
void instruction_name_format(StringRef name, std::stringstream &ss)
{
ss << name;
}
void instruction_to_string(const CallInstruction &instruction, std::stringstream &ss)
{
const MultiFunction &fn = instruction.fn();
this->instruction_name_format(fn.debug_name() + ": ", ss);
for (const int param_index : fn.param_indices()) {
const ParamType param_type = fn.param_type(param_index);
const Variable *variable = instruction.params()[param_index];
ss << R"(<font color="grey30">)";
switch (param_type.interface_type()) {
case ParamType::Input: {
ss << "in";
break;
}
case ParamType::Mutable: {
ss << "mut";
break;
}
case ParamType::Output: {
ss << "out";
break;
}
}
ss << " </font> ";
variable_to_string(variable, ss);
if (param_index < fn.param_amount() - 1) {
ss << ", ";
}
}
}
void instruction_to_string(const DestructInstruction &instruction, std::stringstream &ss)
{
instruction_name_format("Destruct ", ss);
variable_to_string(instruction.variable(), ss);
}
void instruction_to_string(const DummyInstruction & /*instruction*/, std::stringstream &ss)
{
instruction_name_format("Dummy ", ss);
}
void instruction_to_string(const ReturnInstruction & /*instruction*/, std::stringstream &ss)
{
instruction_name_format("Return ", ss);
Vector<ConstParameter> outgoing_parameters;
for (const ConstParameter &param : procedure_.params()) {
if (ELEM(param.type, ParamType::Mutable, ParamType::Output)) {
outgoing_parameters.append(param);
}
}
for (const int param_index : outgoing_parameters.index_range()) {
const ConstParameter &param = outgoing_parameters[param_index];
variable_to_string(param.variable, ss);
if (param_index < outgoing_parameters.size() - 1) {
ss << ", ";
}
}
}
void instruction_to_string(const BranchInstruction &instruction, std::stringstream &ss)
{
instruction_name_format("Branch ", ss);
variable_to_string(instruction.condition(), ss);
}
dot_export::Node &create_entry_node()
{
std::stringstream ss;
ss << "Entry: ";
Vector<ConstParameter> incoming_parameters;
for (const ConstParameter &param : procedure_.params()) {
if (ELEM(param.type, ParamType::Input, ParamType::Mutable)) {
incoming_parameters.append(param);
}
}
for (const int param_index : incoming_parameters.index_range()) {
const ConstParameter &param = incoming_parameters[param_index];
variable_to_string(param.variable, ss);
if (param_index < incoming_parameters.size() - 1) {
ss << ", ";
}
}
dot_export::Node &node = digraph_.new_node(ss.str());
node.set_shape(dot_export::Attr_shape::Ellipse);
return node;
}
};
std::string Procedure::to_dot() const
{
ProcedureDotExport dot_export{*this};
return dot_export.generate();
}
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,119 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "FN_multi_function_procedure_builder.hh"
namespace blender::fn::multi_function {
void ProcedureBuilder::add_destruct(Variable &variable)
{
DestructInstruction &instruction = procedure_->new_destruct_instruction();
instruction.set_variable(&variable);
this->link_to_cursors(&instruction);
cursors_ = {InstructionCursor{instruction}};
}
void ProcedureBuilder::add_destruct(Span<Variable *> variables)
{
for (Variable *variable : variables) {
this->add_destruct(*variable);
}
}
ReturnInstruction &ProcedureBuilder::add_return()
{
ReturnInstruction &instruction = procedure_->new_return_instruction();
this->link_to_cursors(&instruction);
cursors_ = {};
return instruction;
}
CallInstruction &ProcedureBuilder::add_call_with_no_variables(const MultiFunction &fn)
{
CallInstruction &instruction = procedure_->new_call_instruction(fn);
this->link_to_cursors(&instruction);
cursors_ = {InstructionCursor{instruction}};
return instruction;
}
CallInstruction &ProcedureBuilder::add_call_with_all_variables(const MultiFunction &fn,
Span<Variable *> param_variables)
{
CallInstruction &instruction = this->add_call_with_no_variables(fn);
instruction.set_params(param_variables);
return instruction;
}
Vector<Variable *> ProcedureBuilder::add_call(const MultiFunction &fn,
Span<Variable *> input_and_mutable_variables)
{
Vector<Variable *> output_variables;
CallInstruction &instruction = this->add_call_with_no_variables(fn);
for (const int param_index : fn.param_indices()) {
const ParamType param_type = fn.param_type(param_index);
switch (param_type.interface_type()) {
case ParamType::Input:
case ParamType::Mutable: {
Variable *variable = input_and_mutable_variables.first();
instruction.set_param_variable(param_index, variable);
input_and_mutable_variables = input_and_mutable_variables.drop_front(1);
break;
}
case ParamType::Output: {
Variable &variable = procedure_->new_variable(param_type.data_type(),
fn.param_name(param_index));
instruction.set_param_variable(param_index, &variable);
output_variables.append(&variable);
break;
}
}
}
/* All passed in variables should have been dropped in the loop above. */
BLI_assert(input_and_mutable_variables.is_empty());
return output_variables;
}
ProcedureBuilder::Branch ProcedureBuilder::add_branch(Variable &condition)
{
BranchInstruction &instruction = procedure_->new_branch_instruction();
instruction.set_condition(&condition);
this->link_to_cursors(&instruction);
/* Clear cursors because this builder ends here. */
cursors_.clear();
Branch branch{*procedure_, *procedure_};
branch.branch_true.set_cursor(InstructionCursor{instruction, true});
branch.branch_false.set_cursor(InstructionCursor{instruction, false});
return branch;
}
ProcedureBuilder::Loop ProcedureBuilder::add_loop()
{
DummyInstruction &loop_begin = procedure_->new_dummy_instruction();
DummyInstruction &loop_end = procedure_->new_dummy_instruction();
this->link_to_cursors(&loop_begin);
cursors_ = {InstructionCursor{loop_begin}};
Loop loop;
loop.begin = &loop_begin;
loop.end = &loop_end;
return loop;
}
void ProcedureBuilder::add_loop_continue(Loop &loop)
{
this->link_to_cursors(loop.begin);
/* Clear cursors because this builder ends here. */
cursors_.clear();
}
void ProcedureBuilder::add_loop_break(Loop &loop)
{
this->link_to_cursors(loop.end);
/* Clear cursors because this builder ends here. */
cursors_.clear();
}
} // namespace blender::fn::multi_function

View File

@@ -0,0 +1,77 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "FN_multi_function_procedure_optimization.hh"
namespace blender::fn::multi_function::procedure_optimization {
void move_destructs_up(Procedure &procedure, Instruction &block_end_instr)
{
/* A mapping from a variable to its destruct instruction. */
Map<Variable *, DestructInstruction *> destruct_instructions;
Instruction *current_instr = &block_end_instr;
while (true) {
InstructionType instr_type = current_instr->type();
switch (instr_type) {
case InstructionType::Destruct: {
DestructInstruction &destruct_instr = static_cast<DestructInstruction &>(*current_instr);
Variable *variable = destruct_instr.variable();
if (variable == nullptr) {
continue;
}
/* Remember this destruct instruction so that it can be moved up later on when the last use
* of the variable is found. */
destruct_instructions.add(variable, &destruct_instr);
break;
}
case InstructionType::Call: {
CallInstruction &call_instr = static_cast<CallInstruction &>(*current_instr);
/* For each variable, place the corresponding remembered destruct instruction right after
* this call instruction. */
for (Variable *variable : call_instr.params()) {
if (variable == nullptr) {
continue;
}
DestructInstruction *destruct_instr = destruct_instructions.pop_default(variable,
nullptr);
if (destruct_instr == nullptr) {
continue;
}
/* Unlink destruct instruction from previous position. */
Instruction *after_destruct_instr = destruct_instr->next();
while (!destruct_instr->prev().is_empty()) {
/* Do a copy of the cursor here, because `destruct_instr->prev()` changes when
* #set_next is called below. */
const InstructionCursor cursor = destruct_instr->prev()[0];
cursor.set_next(procedure, after_destruct_instr);
}
/* Insert destruct instruction in new position. */
Instruction *next_instr = call_instr.next();
call_instr.set_next(destruct_instr);
destruct_instr->set_next(next_instr);
}
break;
}
default: {
break;
}
}
const Span<InstructionCursor> prev_cursors = current_instr->prev();
if (prev_cursors.size() != 1) {
/* Stop when there is some branching before this instruction. */
break;
}
const InstructionCursor &prev_cursor = prev_cursors[0];
current_instr = prev_cursor.instruction();
if (current_instr == nullptr) {
/* Stop when there is no previous instruction. E.g. when this is the first instruction. */
break;
}
}
}
} // namespace blender::fn::multi_function::procedure_optimization

View File

@@ -0,0 +1,55 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_concurrent_map.hh"
#include "FN_multi_function_registry.hh"
#include "CLG_log.h"
static CLG_LogRef LOG = {"functions.mf_registry"};
namespace blender::fn::multi_function::registry {
using RegistryMap = ConcurrentMap<UString, const MultiFunction *>;
struct Registry {
RegistryMap map;
};
static Registry &get_registry()
{
static Registry registry;
return registry;
}
void add_new(const MultiFunction &fn)
{
Registry &registry = get_registry();
RegistryMap::MutableAccessor accessor;
const UString id = UString(fn.name());
if (registry.map.add(accessor, id)) {
accessor->second = &fn;
}
else {
/* A function can only be registered once. */
CLOG_ERROR(&LOG, "Multi-function already registered: '%s'", id.c_str());
BLI_assert_unreachable();
}
}
const MultiFunction &lookup(UString id)
{
Registry &registry = get_registry();
RegistryMap::ConstAccessor accessor;
if (registry.map.lookup(accessor, id)) {
return *accessor->second;
}
/* The function is expected to exist when using the #lookup function. */
CLOG_ERROR(&LOG, "Multi-function does not exist: '%s'", id.c_str());
BLI_assert_unreachable();
return *accessor->second;
}
} // namespace blender::fn::multi_function::registry

View File

@@ -0,0 +1,14 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "FN_user_data.hh"
namespace blender::fn {
destruct_ptr<LocalUserData> UserData::get_local(LinearAllocator<> & /*allocator*/)
{
return {};
}
} // namespace blender::fn

View File

@@ -0,0 +1,308 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "testing/testing.h"
#include "BKE_gtest_base.hh"
#include "BLI_cpp_type.hh"
#include "FN_field_evaluation.hh"
#include "FN_multi_function_builder.hh"
#include "FN_multi_function_test_common.hh"
namespace blender::fn::tests {
class FieldTest : public bke::BlenderGTestBase {};
TEST_F(FieldTest, ConstantFunction)
{
GField constant_field{FieldOperation::from(std::make_unique<mf::CustomMF_Constant<int>>(10), {}),
0};
Array<int> result(4);
FieldContext context;
FieldEvaluator evaluator{context, 4};
evaluator.add_with_destination(constant_field, result.as_mutable_span());
evaluator.evaluate();
EXPECT_EQ(result[0], 10);
EXPECT_EQ(result[1], 10);
EXPECT_EQ(result[2], 10);
EXPECT_EQ(result[3], 10);
}
class IndexFieldInput final : public FieldInput {
public:
IndexFieldInput() : FieldInput(CPPType::get<int>(), "Index") {}
GVArray get_varray_for_context(const FieldContext & /*context*/,
const IndexMask &mask,
ResourceScope & /*scope*/) const final
{
auto index_func = [](int i) { return i; };
return VArray<int>::from_func(mask.min_array_size(), index_func);
}
};
TEST_F(FieldTest, VArrayInput)
{
GField index_field = GField::from_input<IndexFieldInput>();
Array<int> result_1(4);
FieldContext context;
FieldEvaluator evaluator{context, 4};
evaluator.add_with_destination(index_field, result_1.as_mutable_span());
evaluator.evaluate();
EXPECT_EQ(result_1[0], 0);
EXPECT_EQ(result_1[1], 1);
EXPECT_EQ(result_1[2], 2);
EXPECT_EQ(result_1[3], 3);
/* Evaluate a second time, just to test that the first didn't break anything. */
Array<int> result_2(10);
const Array<int64_t> indices = {2, 4, 6, 8};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int64_t>(indices, memory);
FieldEvaluator evaluator_2{context, &mask};
evaluator_2.add_with_destination(index_field, result_2.as_mutable_span());
evaluator_2.evaluate();
EXPECT_EQ(result_2[2], 2);
EXPECT_EQ(result_2[4], 4);
EXPECT_EQ(result_2[6], 6);
EXPECT_EQ(result_2[8], 8);
}
TEST_F(FieldTest, VArrayInputMultipleOutputs)
{
FieldInputPtr index_input{MEM_new<IndexFieldInput>(__func__)};
GField field_1{index_input};
GField field_2{index_input};
Array<int> result_1(10);
Array<int> result_2(10);
const Array<int64_t> indices = {2, 4, 6, 8};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int64_t>(indices, memory);
FieldContext context;
FieldEvaluator evaluator{context, &mask};
evaluator.add_with_destination(field_1, result_1.as_mutable_span());
evaluator.add_with_destination(field_2, result_2.as_mutable_span());
evaluator.evaluate();
EXPECT_EQ(result_1[2], 2);
EXPECT_EQ(result_1[4], 4);
EXPECT_EQ(result_1[6], 6);
EXPECT_EQ(result_1[8], 8);
EXPECT_EQ(result_2[2], 2);
EXPECT_EQ(result_2[4], 4);
EXPECT_EQ(result_2[6], 6);
EXPECT_EQ(result_2[8], 8);
}
TEST_F(FieldTest, InputAndFunction)
{
GField index_field = GField::from_input<IndexFieldInput>();
auto add_fn = mf::build::SI2_SO<int, int, int>("add", [](int a, int b) { return a + b; });
GField output_field{FieldOperation::from(add_fn, {index_field, index_field}), 0};
Array<int> result(10);
const Array<int64_t> indices = {2, 4, 6, 8};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int64_t>(indices, memory);
FieldContext context;
FieldEvaluator evaluator{context, &mask};
evaluator.add_with_destination(output_field, result.as_mutable_span());
evaluator.evaluate();
EXPECT_EQ(result[2], 4);
EXPECT_EQ(result[4], 8);
EXPECT_EQ(result[6], 12);
EXPECT_EQ(result[8], 16);
}
TEST_F(FieldTest, TwoFunctions)
{
GField index_field = GField::from_input<IndexFieldInput>();
auto add_fn = mf::build::SI2_SO<int, int, int>("add", [](int a, int b) { return a + b; });
GField add_field{FieldOperation::from(add_fn, {index_field, index_field}), 0};
auto add_10_fn = mf::build::SI1_SO<int, int>("add_10", [](int a) { return a + 10; });
GField result_field{FieldOperation::from(add_10_fn, {add_field}), 0};
Array<int> result(10);
const Array<int64_t> indices = {2, 4, 6, 8};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int64_t>(indices, memory);
FieldContext context;
FieldEvaluator evaluator{context, &mask};
evaluator.add_with_destination(result_field, result.as_mutable_span());
evaluator.evaluate();
EXPECT_EQ(result[2], 14);
EXPECT_EQ(result[4], 18);
EXPECT_EQ(result[6], 22);
EXPECT_EQ(result[8], 26);
}
class TwoOutputFunction : public mf::MultiFunction {
private:
mf::Signature signature_;
public:
TwoOutputFunction()
{
mf::SignatureBuilder builder{"Two Outputs", signature_};
builder.single_input<int>("In1");
builder.single_input<int>("In2");
builder.single_output<int>("Add");
builder.single_output<int>("Add10");
this->set_signature(&signature_);
}
void call(const IndexMask &mask, mf::Params params, mf::Context /*context*/) const override
{
const VArray<int> &in1 = params.readonly_single_input<int>(0, "In1");
const VArray<int> &in2 = params.readonly_single_input<int>(1, "In2");
MutableSpan<int> add = params.uninitialized_single_output<int>(2, "Add");
MutableSpan<int> add_10 = params.uninitialized_single_output<int>(3, "Add10");
mask.foreach_index([&](const int64_t i) {
add[i] = in1[i] + in2[i];
add_10[i] = add[i] + 10;
});
}
};
TEST_F(FieldTest, FunctionTwoOutputs)
{
/* Also use two separate input fields, why not. */
GField index_field_1 = GField::from_input<IndexFieldInput>();
GField index_field_2 = GField::from_input<IndexFieldInput>();
FieldOperationPtr fn = FieldOperation::from(std::make_unique<TwoOutputFunction>(),
{index_field_1, index_field_2});
GField result_field_1{fn, 0};
GField result_field_2{fn, 1};
Array<int> result_1(10);
Array<int> result_2(10);
const Array<int64_t> indices = {2, 4, 6, 8};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int64_t>(indices, memory);
FieldContext context;
FieldEvaluator evaluator{context, &mask};
evaluator.add_with_destination(result_field_1, result_1.as_mutable_span());
evaluator.add_with_destination(result_field_2, result_2.as_mutable_span());
evaluator.evaluate();
EXPECT_EQ(result_1[2], 4);
EXPECT_EQ(result_1[4], 8);
EXPECT_EQ(result_1[6], 12);
EXPECT_EQ(result_1[8], 16);
EXPECT_EQ(result_2[2], 14);
EXPECT_EQ(result_2[4], 18);
EXPECT_EQ(result_2[6], 22);
EXPECT_EQ(result_2[8], 26);
}
TEST_F(FieldTest, TwoFunctionsTwoOutputs)
{
GField index_field = GField::from_input<IndexFieldInput>();
FieldOperationPtr fn = FieldOperation::from(std::make_unique<TwoOutputFunction>(),
{index_field, index_field});
Array<int64_t> mask_indices = {2, 4, 6, 8};
IndexMaskMemory memory;
IndexMask mask = IndexMask::from_indices<int64_t>(mask_indices, memory);
Field<int> result_field_1{fn, 0};
Field<int> intermediate_field{fn, 1};
auto add_10_fn = mf::build::SI1_SO<int, int>("add_10", [](int a) { return a + 10; });
Field<int> result_field_2{FieldOperation::from(add_10_fn, {intermediate_field}), 0};
FieldContext field_context;
FieldEvaluator field_evaluator{field_context, &mask};
VArray<int> result_1;
VArray<int> result_2;
field_evaluator.add(result_field_1, &result_1);
field_evaluator.add(result_field_2, &result_2);
field_evaluator.evaluate();
EXPECT_EQ(result_1.get(2), 4);
EXPECT_EQ(result_1.get(4), 8);
EXPECT_EQ(result_1.get(6), 12);
EXPECT_EQ(result_1.get(8), 16);
EXPECT_EQ(result_2.get(2), 24);
EXPECT_EQ(result_2.get(4), 28);
EXPECT_EQ(result_2.get(6), 32);
EXPECT_EQ(result_2.get(8), 36);
}
TEST_F(FieldTest, SameFieldTwice)
{
GField constant_field{FieldOperation::from(std::make_unique<mf::CustomMF_Constant<int>>(10), {}),
0};
FieldContext field_context;
IndexMask mask{IndexRange(2)};
ResourceScope scope;
Vector<GVArray> results = evaluate_fields(
scope, {constant_field, constant_field}, mask, field_context);
VArray<int> varray1 = results[0].typed<int>();
VArray<int> varray2 = results[1].typed<int>();
EXPECT_EQ(varray1.get(0), 10);
EXPECT_EQ(varray1.get(1), 10);
EXPECT_EQ(varray2.get(0), 10);
EXPECT_EQ(varray2.get(1), 10);
}
TEST_F(FieldTest, IgnoredOutput)
{
static mf::tests::OptionalOutputsFunction fn;
Field<int> field{FieldOperation::from(fn, {}), 0};
FieldContext field_context;
FieldEvaluator field_evaluator{field_context, 10};
VArray<int> results;
field_evaluator.add(field, &results);
field_evaluator.evaluate();
EXPECT_EQ(results.get(0), 5);
EXPECT_EQ(results.get(3), 5);
}
TEST_F(FieldTest, EvaluateWithVArrayPtr)
{
VArray<int> dst_a;
VArraySpan<int> dst_b;
FieldContext field_context;
FieldEvaluator field_evaluator{field_context, 2};
field_evaluator.add(Field<int>(10), &dst_a);
field_evaluator.add(Field<int>(20), &dst_b);
field_evaluator.evaluate();
EXPECT_EQ(dst_a.size(), 2);
EXPECT_EQ(dst_b.size(), 2);
EXPECT_EQ(dst_a[0], 10);
EXPECT_EQ(dst_a[1], 10);
EXPECT_EQ(dst_b[0], 20);
EXPECT_EQ(dst_b[1], 20);
}
} // namespace blender::fn::tests

View File

@@ -0,0 +1,185 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "testing/testing.h"
#include "FN_lazy_function_execute.hh"
#include "FN_lazy_function_graph.hh"
#include "FN_lazy_function_graph_executor.hh"
#include "BLI_task.h"
#include "BKE_gtest_base.hh"
namespace blender::fn::lazy_function::tests {
class LazyFunctionTest : public bke::BlenderGTestBase {};
class AddLazyFunction : public LazyFunction {
public:
AddLazyFunction()
{
debug_name_ = "Add";
inputs_.append({"A", CPPType::get<int>()});
inputs_.append({"B", CPPType::get<int>()});
outputs_.append({"Result", CPPType::get<int>()});
}
void execute_impl(Params &params, const Context & /*context*/) const override
{
const int a = params.get_input<int>(0);
const int b = params.get_input<int>(1);
params.set_output(0, a + b);
}
};
class StoreValueFunction : public LazyFunction {
private:
int *dst1_;
int *dst2_;
public:
StoreValueFunction(int *dst1, int *dst2) : dst1_(dst1), dst2_(dst2)
{
debug_name_ = "Store Value";
inputs_.append({"A", CPPType::get<int>()});
inputs_.append({"B", CPPType::get<int>(), ValueUsage::Maybe});
}
void execute_impl(Params &params, const Context & /*context*/) const override
{
*dst1_ = params.get_input<int>(0);
if (int *value = params.try_get_input_data_ptr_or_request<int>(1)) {
*dst2_ = *value;
}
}
};
class SimpleSideEffectProvider : public GraphExecutor::SideEffectProvider {
private:
Vector<const FunctionNode *> side_effect_nodes_;
public:
SimpleSideEffectProvider(Span<const FunctionNode *> side_effect_nodes)
: side_effect_nodes_(side_effect_nodes)
{
}
Vector<const FunctionNode *> get_nodes_with_side_effects(
const Context & /*context*/) const override
{
return side_effect_nodes_;
}
};
TEST_F(LazyFunctionTest, SimpleAdd)
{
const AddLazyFunction add_fn;
int result = 0;
execute_lazy_function_eagerly(
add_fn, nullptr, nullptr, std::make_tuple(30, 5), std::make_tuple(&result));
EXPECT_EQ(result, 35);
}
TEST_F(LazyFunctionTest, SideEffects)
{
BLI_task_scheduler_init();
int dst1 = 0;
int dst2 = 0;
const AddLazyFunction add_fn;
const StoreValueFunction store_fn{&dst1, &dst2};
Graph graph;
FunctionNode &add_node_1 = graph.add_function(add_fn);
FunctionNode &add_node_2 = graph.add_function(add_fn);
FunctionNode &store_node = graph.add_function(store_fn);
GraphInputSocket &graph_input = graph.add_input(CPPType::get<int>());
graph.add_link(graph_input, add_node_1.input(0));
graph.add_link(graph_input, add_node_2.input(0));
graph.add_link(add_node_1.output(0), store_node.input(0));
graph.add_link(add_node_2.output(0), store_node.input(1));
const int value_10 = 10;
const int value_100 = 100;
add_node_1.input(1).set_default_value(&value_10);
add_node_2.input(1).set_default_value(&value_100);
graph.update_node_indices();
SimpleSideEffectProvider side_effect_provider{{&store_node}};
GraphExecutor executor_fn{graph, {&graph_input}, {}, nullptr, &side_effect_provider, nullptr};
execute_lazy_function_eagerly(
executor_fn, nullptr, nullptr, std::make_tuple(5), std::make_tuple());
EXPECT_EQ(dst1, 15);
EXPECT_EQ(dst2, 105);
}
class PartialEvaluationTestFunction : public LazyFunction {
public:
PartialEvaluationTestFunction()
{
debug_name_ = "Partial Evaluation";
allow_missing_requested_inputs_ = true;
inputs_.append_as("A", CPPType::get<int>(), ValueUsage::Used);
inputs_.append_as("B", CPPType::get<int>(), ValueUsage::Used);
outputs_.append_as("A*2", CPPType::get<int>());
outputs_.append_as("B*5", CPPType::get<int>());
}
void execute_impl(Params &params, const Context & /*context*/) const override
{
if (!params.output_was_set(0)) {
if (int *a = params.try_get_input_data_ptr<int>(0)) {
params.set_output(0, *a * 2);
}
}
if (!params.output_was_set(1)) {
if (int *b = params.try_get_input_data_ptr<int>(1)) {
params.set_output(1, *b * 5);
}
}
}
void possible_output_dependencies(const int output_index,
FunctionRef<void(Span<int>)> fn) const override
{
/* Each output only depends on the input with the same index. */
const int input_index = output_index;
fn({input_index});
}
};
TEST_F(LazyFunctionTest, GraphWithCycle)
{
const PartialEvaluationTestFunction fn;
Graph graph;
FunctionNode &fn_node = graph.add_function(fn);
GraphInputSocket &input_socket = graph.add_input(CPPType::get<int>());
GraphOutputSocket &output_socket = graph.add_output(CPPType::get<int>());
graph.add_link(input_socket, fn_node.input(0));
/* NOTE: This creates a cycle in the graph. However, it should still be possible to evaluate it,
* because there is no actual data dependency in the cycle. */
graph.add_link(fn_node.output(0), fn_node.input(1));
graph.add_link(fn_node.output(1), output_socket);
graph.update_node_indices();
GraphExecutor executor_fn{graph, {&input_socket}, {&output_socket}, nullptr, nullptr, nullptr};
int result = 0;
execute_lazy_function_eagerly(
executor_fn, nullptr, nullptr, std::make_tuple(10), std::make_tuple(&result));
EXPECT_EQ(result, 10 * 2 * 5);
}
} // namespace blender::fn::lazy_function::tests

View File

@@ -0,0 +1,430 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "testing/testing.h"
#include "FN_multi_function_builder.hh"
#include "FN_multi_function_procedure_builder.hh"
#include "FN_multi_function_procedure_executor.hh"
#include "FN_multi_function_test_common.hh"
#include "BKE_gtest_base.hh"
namespace blender::fn::multi_function::tests {
class MultiFunctionProcedureTest : public bke::BlenderGTestBase {};
TEST_F(MultiFunctionProcedureTest, ConstantOutput)
{
/**
* procedure(int *var2) {
* var1 = 5;
* var2 = var1 + var1;
* }
*/
CustomMF_Constant<int> constant_fn{5};
auto add_fn = build::SI2_SO<int, int, int>("Add", [](int a, int b) { return a + b; });
Procedure procedure;
ProcedureBuilder builder{procedure};
auto [var1] = builder.add_call<1>(constant_fn);
auto [var2] = builder.add_call<1>(add_fn, {var1, var1});
builder.add_destruct(*var1);
builder.add_return();
builder.add_output_parameter(*var2);
EXPECT_TRUE(procedure.validate());
ProcedureExecutor executor{procedure};
const IndexMask mask(2);
ParamsBuilder params{executor, &mask};
ContextBuilder context;
Array<int> output_array(2);
params.add_uninitialized_single_output(output_array.as_mutable_span());
executor.call(mask, params, context);
EXPECT_EQ(output_array[0], 10);
EXPECT_EQ(output_array[1], 10);
}
TEST_F(MultiFunctionProcedureTest, SimpleTest)
{
/**
* procedure(int var1, int var2, int *var4) {
* int var3 = var1 + var2;
* var4 = var2 + var3;
* var4 += 10;
* }
*/
auto add_fn = mf::build::SI2_SO<int, int, int>("add", [](int a, int b) { return a + b; });
auto add_10_fn = mf::build::SM<int>("add_10", [](int &a) { a += 10; });
Procedure procedure;
ProcedureBuilder builder{procedure};
Variable *var1 = &builder.add_single_input_parameter<int>();
Variable *var2 = &builder.add_single_input_parameter<int>();
auto [var3] = builder.add_call<1>(add_fn, {var1, var2});
auto [var4] = builder.add_call<1>(add_fn, {var2, var3});
builder.add_call(add_10_fn, {var4});
builder.add_destruct({var1, var2, var3});
builder.add_return();
builder.add_output_parameter(*var4);
EXPECT_TRUE(procedure.validate());
ProcedureExecutor executor{procedure};
const IndexMask mask(3);
ParamsBuilder params{executor, &mask};
ContextBuilder context;
Array<int> input_array = {1, 2, 3};
params.add_readonly_single_input(input_array.as_span());
params.add_readonly_single_input_value(3);
Array<int> output_array(3);
params.add_uninitialized_single_output(output_array.as_mutable_span());
executor.call(mask, params, context);
EXPECT_EQ(output_array[0], 17);
EXPECT_EQ(output_array[1], 18);
EXPECT_EQ(output_array[2], 19);
}
TEST_F(MultiFunctionProcedureTest, BranchTest)
{
/**
* procedure(int &var1, bool var2) {
* if (var2) {
* var1 += 100;
* }
* else {
* var1 += 10;
* }
* var1 += 10;
* }
*/
auto add_10_fn = build::SM<int>("add_10", [](int &a) { a += 10; });
auto add_100_fn = build::SM<int>("add_100", [](int &a) { a += 100; });
Procedure procedure;
ProcedureBuilder builder{procedure};
Variable *var1 = &builder.add_single_mutable_parameter<int>();
Variable *var2 = &builder.add_single_input_parameter<bool>();
ProcedureBuilder::Branch branch = builder.add_branch(*var2);
branch.branch_false.add_call(add_10_fn, {var1});
branch.branch_true.add_call(add_100_fn, {var1});
builder.set_cursor_after_branch(branch);
builder.add_call(add_10_fn, {var1});
builder.add_destruct({var2});
builder.add_return();
EXPECT_TRUE(procedure.validate());
ProcedureExecutor procedure_fn{procedure};
const IndexMask mask(IndexRange(1, 4));
ParamsBuilder params(procedure_fn, &mask);
Array<int> values_a = {1, 5, 3, 6, 2};
Array<bool> values_cond = {true, false, true, true, false};
params.add_single_mutable(values_a.as_mutable_span());
params.add_readonly_single_input(values_cond.as_span());
ContextBuilder context;
procedure_fn.call(mask, params, context);
EXPECT_EQ(values_a[0], 1);
EXPECT_EQ(values_a[1], 25);
EXPECT_EQ(values_a[2], 113);
EXPECT_EQ(values_a[3], 116);
EXPECT_EQ(values_a[4], 22);
}
TEST_F(MultiFunctionProcedureTest, EvaluateOne)
{
/**
* procedure(int var1, int *var2) {
* var2 = var1 + 10;
* }
*/
int tot_evaluations = 0;
const auto add_10_fn = mf::build::SI1_SO<int, int>("add_10", [&](int a) {
tot_evaluations++;
return a + 10;
});
Procedure procedure;
ProcedureBuilder builder{procedure};
Variable *var1 = &builder.add_single_input_parameter<int>();
auto [var2] = builder.add_call<1>(add_10_fn, {var1});
builder.add_destruct(*var1);
builder.add_return();
builder.add_output_parameter(*var2);
ProcedureExecutor procedure_fn{procedure};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 1, 3, 4}, memory);
ParamsBuilder params{procedure_fn, &mask};
Array<int> values_out = {1, 2, 3, 4, 5};
params.add_readonly_single_input_value(1);
params.add_uninitialized_single_output(values_out.as_mutable_span());
ContextBuilder context;
procedure_fn.call(mask, params, context);
EXPECT_EQ(values_out[0], 11);
EXPECT_EQ(values_out[1], 11);
EXPECT_EQ(values_out[2], 3);
EXPECT_EQ(values_out[3], 11);
EXPECT_EQ(values_out[4], 11);
/* We expect only one evaluation, because the input is constant. */
EXPECT_EQ(tot_evaluations, 1);
}
TEST_F(MultiFunctionProcedureTest, SimpleLoop)
{
/**
* procedure(int count, int *out) {
* out = 1;
* int index = 0'
* loop {
* if (index >= count) {
* break;
* }
* out *= 2;
* index += 1;
* }
* out += 1000;
* }
*/
CustomMF_Constant<int> const_1_fn{1};
CustomMF_Constant<int> const_0_fn{0};
auto greater_or_equal_fn = mf::build::SI2_SO<int, int, bool>(
"greater or equal", [](int a, int b) { return a >= b; });
auto double_fn = build::SM<int>("double", [](int &a) { a *= 2; });
auto add_1000_fn = build::SM<int>("add 1000", [](int &a) { a += 1000; });
auto add_1_fn = build::SM<int>("add 1", [](int &a) { a += 1; });
Procedure procedure;
ProcedureBuilder builder{procedure};
Variable *var_count = &builder.add_single_input_parameter<int>("count");
auto [var_out] = builder.add_call<1>(const_1_fn);
var_out->set_name("out");
auto [var_index] = builder.add_call<1>(const_0_fn);
var_index->set_name("index");
ProcedureBuilder::Loop loop = builder.add_loop();
auto [var_condition] = builder.add_call<1>(greater_or_equal_fn, {var_index, var_count});
var_condition->set_name("condition");
ProcedureBuilder::Branch branch = builder.add_branch(*var_condition);
branch.branch_true.add_destruct(*var_condition);
branch.branch_true.add_loop_break(loop);
branch.branch_false.add_destruct(*var_condition);
builder.set_cursor_after_branch(branch);
builder.add_call(double_fn, {var_out});
builder.add_call(add_1_fn, {var_index});
builder.add_loop_continue(loop);
builder.set_cursor_after_loop(loop);
builder.add_call(add_1000_fn, {var_out});
builder.add_destruct({var_count, var_index});
builder.add_return();
builder.add_output_parameter(*var_out);
EXPECT_TRUE(procedure.validate());
ProcedureExecutor procedure_fn{procedure};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 1, 3, 4}, memory);
ParamsBuilder params{procedure_fn, &mask};
Array<int> counts = {4, 3, 7, 6, 4};
Array<int> results(5, -1);
params.add_readonly_single_input(counts.as_span());
params.add_uninitialized_single_output(results.as_mutable_span());
ContextBuilder context;
procedure_fn.call(mask, params, context);
EXPECT_EQ(results[0], 1016);
EXPECT_EQ(results[1], 1008);
EXPECT_EQ(results[2], -1);
EXPECT_EQ(results[3], 1064);
EXPECT_EQ(results[4], 1016);
}
TEST_F(MultiFunctionProcedureTest, Vectors)
{
/**
* procedure(vector<int> v1, vector<int> &v2, vector<int> *v3) {
* v1.extend(v2);
* int constant = 5;
* v2.append(constant);
* v2.extend(v1);
* int len = sum(v2);
* v3 = range(len);
* }
*/
CreateRangeFunction create_range_fn;
ConcatVectorsFunction extend_fn;
GenericAppendFunction append_fn{CPPType::get<int>()};
SumVectorFunction sum_elements_fn;
CustomMF_Constant<int> constant_5_fn{5};
Procedure procedure;
ProcedureBuilder builder{procedure};
Variable *var_v1 = &builder.add_input_parameter(DataType::ForVector<int>());
Variable *var_v2 = &builder.add_parameter(ParamType::ForMutableVector(CPPType::get<int>()));
builder.add_call(extend_fn, {var_v1, var_v2});
auto [var_constant] = builder.add_call<1>(constant_5_fn);
builder.add_call(append_fn, {var_v2, var_constant});
builder.add_destruct(*var_constant);
builder.add_call(extend_fn, {var_v2, var_v1});
auto [var_len] = builder.add_call<1>(sum_elements_fn, {var_v2});
auto [var_v3] = builder.add_call<1>(create_range_fn, {var_len});
builder.add_destruct({var_v1, var_len});
builder.add_return();
builder.add_output_parameter(*var_v3);
EXPECT_TRUE(procedure.validate());
ProcedureExecutor procedure_fn{procedure};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 1, 3, 4}, memory);
ParamsBuilder params{procedure_fn, &mask};
Array<int> v1 = {5, 2, 3};
GVectorArray v2{CPPType::get<int>(), 5};
GVectorArray v3{CPPType::get<int>(), 5};
int value_10 = 10;
v2.append(0, &value_10);
v2.append(4, &value_10);
params.add_readonly_vector_input(v1.as_span());
params.add_vector_mutable(v2);
params.add_vector_output(v3);
ContextBuilder context;
procedure_fn.call(mask, params, context);
EXPECT_EQ(v2[0].size(), 6);
EXPECT_EQ(v2[1].size(), 4);
EXPECT_EQ(v2[2].size(), 0);
EXPECT_EQ(v2[3].size(), 4);
EXPECT_EQ(v2[4].size(), 6);
EXPECT_EQ(v3[0].size(), 35);
EXPECT_EQ(v3[1].size(), 15);
EXPECT_EQ(v3[2].size(), 0);
EXPECT_EQ(v3[3].size(), 15);
EXPECT_EQ(v3[4].size(), 35);
}
TEST_F(MultiFunctionProcedureTest, BufferReuse)
{
/**
* procedure(int a, int *out) {
* int b = a + 10;
* int c = c + 10;
* int d = d + 10;
* int e = d + 10;
* out = e + 10;
* }
*/
auto add_10_fn = build::SI1_SO<int, int>("add 10", [](int a) { return a + 10; });
Procedure procedure;
ProcedureBuilder builder{procedure};
Variable *var_a = &builder.add_single_input_parameter<int>();
auto [var_b] = builder.add_call<1>(add_10_fn, {var_a});
builder.add_destruct(*var_a);
auto [var_c] = builder.add_call<1>(add_10_fn, {var_b});
builder.add_destruct(*var_b);
auto [var_d] = builder.add_call<1>(add_10_fn, {var_c});
builder.add_destruct(*var_c);
auto [var_e] = builder.add_call<1>(add_10_fn, {var_d});
builder.add_destruct(*var_d);
auto [var_out] = builder.add_call<1>(add_10_fn, {var_e});
builder.add_destruct(*var_e);
builder.add_return();
builder.add_output_parameter(*var_out);
EXPECT_TRUE(procedure.validate());
ProcedureExecutor procedure_fn{procedure};
Array<int> inputs = {4, 1, 6, 2, 3};
Array<int> results(5, -1);
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 2, 3, 4}, memory);
ParamsBuilder params{procedure_fn, &mask};
params.add_readonly_single_input(inputs.as_span());
params.add_uninitialized_single_output(results.as_mutable_span());
ContextBuilder context;
procedure_fn.call(mask, params, context);
EXPECT_EQ(results[0], 54);
EXPECT_EQ(results[1], -1);
EXPECT_EQ(results[2], 56);
EXPECT_EQ(results[3], 52);
EXPECT_EQ(results[4], 53);
}
TEST_F(MultiFunctionProcedureTest, OutputBufferReplaced)
{
Procedure procedure;
ProcedureBuilder builder{procedure};
const int output_value = 42;
CustomMF_GenericConstant constant_fn(CPPType::get<int>(), &output_value, false);
Variable &var_o = procedure.new_variable(DataType::ForSingle<int>());
builder.add_output_parameter(var_o);
builder.add_call_with_all_variables(constant_fn, {&var_o});
builder.add_destruct(var_o);
builder.add_call_with_all_variables(constant_fn, {&var_o});
builder.add_return();
EXPECT_TRUE(procedure.validate());
ProcedureExecutor procedure_fn{procedure};
Array<int> output(3, 0);
IndexMask mask(output.size());
mf::ParamsBuilder params(procedure_fn, &mask);
params.add_uninitialized_single_output(output.as_mutable_span());
mf::ContextBuilder context;
procedure_fn.call(mask, params, context);
EXPECT_EQ(output[0], output_value);
EXPECT_EQ(output[1], output_value);
EXPECT_EQ(output[2], output_value);
}
} // namespace blender::fn::multi_function::tests

View File

@@ -0,0 +1,284 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "testing/testing.h"
#include "FN_multi_function.hh"
#include "FN_multi_function_builder.hh"
#include "FN_multi_function_test_common.hh"
#include "BKE_gtest_base.hh"
namespace blender::fn::multi_function::tests {
namespace {
class MultiFunctionTest : public bke::BlenderGTestBase {};
class AddFunction : public MultiFunction {
public:
AddFunction()
{
static Signature signature = []() {
Signature signature;
SignatureBuilder builder("Add", signature);
builder.single_input<int>("A");
builder.single_input<int>("B");
builder.single_output<int>("Result");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
const VArray<int> &a = params.readonly_single_input<int>(0, "A");
const VArray<int> &b = params.readonly_single_input<int>(1, "B");
MutableSpan<int> result = params.uninitialized_single_output<int>(2, "Result");
mask.foreach_index([&](const int64_t i) { result[i] = a[i] + b[i]; });
}
};
TEST_F(MultiFunctionTest, AddFunction)
{
AddFunction fn;
Array<int> input1 = {4, 5, 6};
Array<int> input2 = {10, 20, 30};
Array<int> output(3, -1);
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 2}, memory);
ParamsBuilder params(fn, &mask);
params.add_readonly_single_input(input1.as_span());
params.add_readonly_single_input(input2.as_span());
params.add_uninitialized_single_output(output.as_mutable_span());
ContextBuilder context;
fn.call(mask, params, context);
EXPECT_EQ(output[0], 14);
EXPECT_EQ(output[1], -1);
EXPECT_EQ(output[2], 36);
}
TEST_F(MultiFunctionTest, AddPrefixFunction)
{
AddPrefixFunction fn;
Array<std::string> strings = {
"Hello",
"World",
"This is a test",
"Another much longer string to trigger an allocation",
};
std::string prefix = "AB";
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 2, 3}, memory);
ParamsBuilder params(fn, &mask);
params.add_readonly_single_input(&prefix);
params.add_single_mutable(strings.as_mutable_span());
ContextBuilder context;
fn.call(mask, params, context);
EXPECT_EQ(strings[0], "ABHello");
EXPECT_EQ(strings[1], "World");
EXPECT_EQ(strings[2], "ABThis is a test");
EXPECT_EQ(strings[3], "ABAnother much longer string to trigger an allocation");
}
TEST_F(MultiFunctionTest, CreateRangeFunction)
{
CreateRangeFunction fn;
GVectorArray ranges(CPPType::get<int>(), 5);
GVectorArray_TypedMutableRef<int> ranges_ref{ranges};
Array<int> sizes = {3, 0, 6, 1, 4};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 1, 2, 3}, memory);
ParamsBuilder params(fn, &mask);
params.add_readonly_single_input(sizes.as_span());
params.add_vector_output(ranges);
ContextBuilder context;
fn.call(mask, params, context);
EXPECT_EQ(ranges[0].size(), 3);
EXPECT_EQ(ranges[1].size(), 0);
EXPECT_EQ(ranges[2].size(), 6);
EXPECT_EQ(ranges[3].size(), 1);
EXPECT_EQ(ranges[4].size(), 0);
EXPECT_EQ(ranges_ref[0][0], 0);
EXPECT_EQ(ranges_ref[0][1], 1);
EXPECT_EQ(ranges_ref[0][2], 2);
EXPECT_EQ(ranges_ref[2][0], 0);
EXPECT_EQ(ranges_ref[2][1], 1);
}
TEST_F(MultiFunctionTest, GenericAppendFunction)
{
GenericAppendFunction fn(CPPType::get<int32_t>());
GVectorArray vectors(CPPType::get<int32_t>(), 4);
GVectorArray_TypedMutableRef<int> vectors_ref{vectors};
vectors_ref.append(0, 1);
vectors_ref.append(0, 2);
vectors_ref.append(2, 6);
Array<int> values = {5, 7, 3, 1};
const IndexMask mask(IndexRange(vectors.size()));
ParamsBuilder params(fn, &mask);
params.add_vector_mutable(vectors);
params.add_readonly_single_input(values.as_span());
ContextBuilder context;
fn.call(mask, params, context);
EXPECT_EQ(vectors[0].size(), 3);
EXPECT_EQ(vectors[1].size(), 1);
EXPECT_EQ(vectors[2].size(), 2);
EXPECT_EQ(vectors[3].size(), 1);
EXPECT_EQ(vectors_ref[0][0], 1);
EXPECT_EQ(vectors_ref[0][1], 2);
EXPECT_EQ(vectors_ref[0][2], 5);
EXPECT_EQ(vectors_ref[1][0], 7);
EXPECT_EQ(vectors_ref[2][0], 6);
EXPECT_EQ(vectors_ref[2][1], 3);
EXPECT_EQ(vectors_ref[3][0], 1);
}
TEST_F(MultiFunctionTest, CustomMF_Constant)
{
CustomMF_Constant<int> fn{42};
Array<int> outputs(4, 0);
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 2, 3}, memory);
ParamsBuilder params(fn, &mask);
params.add_uninitialized_single_output(outputs.as_mutable_span());
ContextBuilder context;
fn.call(mask, params, context);
EXPECT_EQ(outputs[0], 42);
EXPECT_EQ(outputs[1], 0);
EXPECT_EQ(outputs[2], 42);
EXPECT_EQ(outputs[3], 42);
}
TEST_F(MultiFunctionTest, CustomMF_GenericConstant)
{
int value = 42;
CustomMF_GenericConstant fn{CPPType::get<int32_t>(), (const void *)&value, false};
Array<int> outputs(4, 0);
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({0, 1, 2}, memory);
ParamsBuilder params(fn, &mask);
params.add_uninitialized_single_output(outputs.as_mutable_span());
ContextBuilder context;
fn.call(mask, params, context);
EXPECT_EQ(outputs[0], 42);
EXPECT_EQ(outputs[1], 42);
EXPECT_EQ(outputs[2], 42);
EXPECT_EQ(outputs[3], 0);
}
TEST_F(MultiFunctionTest, CustomMF_GenericConstantArray)
{
std::array<int, 4> values = {3, 4, 5, 6};
CustomMF_GenericConstantArray fn{GSpan(Span(values))};
GVectorArray vector_array{CPPType::get<int32_t>(), 4};
GVectorArray_TypedMutableRef<int> vector_array_ref{vector_array};
IndexMaskMemory memory;
const IndexMask mask = IndexMask::from_indices<int>({1, 2, 3}, memory);
ParamsBuilder params(fn, &mask);
params.add_vector_output(vector_array);
ContextBuilder context;
fn.call(mask, params, context);
EXPECT_EQ(vector_array[0].size(), 0);
EXPECT_EQ(vector_array[1].size(), 4);
EXPECT_EQ(vector_array[2].size(), 4);
EXPECT_EQ(vector_array[3].size(), 4);
for (int i = 1; i < 4; i++) {
EXPECT_EQ(vector_array_ref[i][0], 3);
EXPECT_EQ(vector_array_ref[i][1], 4);
EXPECT_EQ(vector_array_ref[i][2], 5);
EXPECT_EQ(vector_array_ref[i][3], 6);
}
}
TEST_F(MultiFunctionTest, IgnoredOutputs)
{
OptionalOutputsFunction fn;
{
const IndexMask mask(10);
ParamsBuilder params(fn, &mask);
params.add_ignored_single_output("Out 1");
params.add_ignored_single_output("Out 2");
ContextBuilder context;
fn.call(mask, params, context);
}
{
Array<int> results_1(10);
Array<std::string> results_2(10, NoInitialization());
const IndexMask mask(10);
ParamsBuilder params(fn, &mask);
params.add_uninitialized_single_output(results_1.as_mutable_span(), "Out 1");
params.add_uninitialized_single_output(results_2.as_mutable_span(), "Out 2");
ContextBuilder context;
fn.call(mask, 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");
}
}
TEST_F(MultiFunctionTest, build_move_only)
{
auto adder = std::make_unique<int>(10);
const auto fn = mf::build::SI1_SO<int, int>(
"add", [adder = std::move(adder)](const int a) { return a + *adder; });
const IndexMask mask(2);
ParamsBuilder params(fn, &mask);
Array<int> inputs = {3, 5};
Array<int> outputs(2);
params.add_readonly_single_input(inputs.as_span());
params.add_uninitialized_single_output(outputs.as_mutable_span());
ContextBuilder context;
fn.call(mask, params, context);
EXPECT_EQ(outputs[0], 13);
EXPECT_EQ(outputs[1], 15);
}
} // namespace
} // namespace blender::fn::multi_function::tests

View File

@@ -0,0 +1,187 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "FN_multi_function.hh"
namespace blender::fn::multi_function::tests {
class AddPrefixFunction : public MultiFunction {
public:
AddPrefixFunction()
{
static const Signature signature = []() {
Signature signature;
SignatureBuilder builder{"Add Prefix", signature};
builder.single_input<std::string>("Prefix");
builder.single_mutable<std::string>("Strings");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
const VArray<std::string> &prefixes = params.readonly_single_input<std::string>(0, "Prefix");
MutableSpan<std::string> strings = params.single_mutable<std::string>(1, "Strings");
mask.foreach_index([&](const int64_t i) { strings[i] = prefixes[i] + strings[i]; });
}
};
class CreateRangeFunction : public MultiFunction {
public:
CreateRangeFunction()
{
static const Signature signature = []() {
Signature signature;
SignatureBuilder builder{"Create Range", signature};
builder.single_input<int>("Size");
builder.vector_output<int>("Range");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
const VArray<int> &sizes = params.readonly_single_input<int>(0, "Size");
GVectorArray &ranges = params.vector_output(1, "Range");
mask.foreach_index([&](const int64_t i) {
int size = sizes[i];
for (int j : IndexRange(size)) {
ranges.append(i, &j);
}
});
}
};
class GenericAppendFunction : public MultiFunction {
private:
Signature signature_;
public:
GenericAppendFunction(const CPPType &type)
{
SignatureBuilder builder{"Append", signature_};
builder.vector_mutable("Vector", type);
builder.single_input("Value", type);
this->set_signature(&signature_);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
GVectorArray &vectors = params.vector_mutable(0, "Vector");
const GVArray &values = params.readonly_single_input(1, "Value");
mask.foreach_index([&](const int64_t i) {
BUFFER_FOR_CPP_TYPE_VALUE(values.type(), buffer);
values.get(i, buffer);
vectors.append(i, buffer);
values.type().destruct(buffer);
});
}
};
class ConcatVectorsFunction : public MultiFunction {
public:
ConcatVectorsFunction()
{
static const Signature signature = []() {
Signature signature;
SignatureBuilder builder{"Concat Vectors", signature};
builder.vector_mutable<int>("A");
builder.vector_input<int>("B");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
GVectorArray &a = params.vector_mutable(0);
const GVVectorArray &b = params.readonly_vector_input(1);
a.extend(mask, b);
}
};
class AppendFunction : public MultiFunction {
public:
AppendFunction()
{
static const Signature signature = []() {
Signature signature;
SignatureBuilder builder{"Append", signature};
builder.vector_mutable<int>("Vector");
builder.single_input<int>("Value");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
GVectorArray_TypedMutableRef<int> vectors = params.vector_mutable<int>(0);
const VArray<int> &values = params.readonly_single_input<int>(1);
mask.foreach_index([&](const int64_t i) { vectors.append(i, values[i]); });
}
};
class SumVectorFunction : public MultiFunction {
public:
SumVectorFunction()
{
static const Signature signature = []() {
Signature signature;
SignatureBuilder builder{"Sum Vectors", signature};
builder.vector_input<int>("Vector");
builder.single_output<int>("Sum");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
const VVectorArray<int> &vectors = params.readonly_vector_input<int>(0);
MutableSpan<int> sums = params.uninitialized_single_output<int>(1);
mask.foreach_index([&](const int64_t i) {
int sum = 0;
for (int j : IndexRange(vectors.get_vector_size(i))) {
sum += vectors.get_vector_element(i, j);
}
sums[i] = sum;
});
}
};
class OptionalOutputsFunction : public MultiFunction {
public:
OptionalOutputsFunction()
{
static const Signature signature = []() {
Signature signature;
SignatureBuilder builder{"Optional Outputs", signature};
builder.single_output<int>("Out 1");
builder.single_output<std::string>("Out 2");
return signature;
}();
this->set_signature(&signature);
}
void call(const IndexMask &mask, Params params, Context /*context*/) const override
{
if (params.single_output_is_required(0, "Out 1")) {
MutableSpan<int> values = params.uninitialized_single_output<int>(0, "Out 1");
index_mask::masked_fill(values, 5, mask);
}
MutableSpan<std::string> values = params.uninitialized_single_output<std::string>(1, "Out 2");
mask.foreach_index(
[&](const int i) { new (&values[i]) std::string("hello, this is a long string"); });
}
};
} // namespace blender::fn::multi_function::tests