Add Chromium-only Blender WebEngine parity work
This commit is contained in:
82
blender-5.2.0/source/blender/functions/CMakeLists.txt
Normal file
82
blender-5.2.0/source/blender/functions/CMakeLists.txt
Normal 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()
|
||||
695
blender-5.2.0/source/blender/functions/FN_field.hh
Normal file
695
blender-5.2.0/source/blender/functions/FN_field.hh
Normal 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
|
||||
217
blender-5.2.0/source/blender/functions/FN_field_evaluation.hh
Normal file
217
blender-5.2.0/source/blender/functions/FN_field_evaluation.hh
Normal 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
|
||||
11
blender-5.2.0/source/blender/functions/FN_init.hh
Normal file
11
blender-5.2.0/source/blender/functions/FN_init.hh
Normal 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();
|
||||
|
||||
}
|
||||
479
blender-5.2.0/source/blender/functions/FN_lazy_function.hh
Normal file
479
blender-5.2.0/source/blender/functions/FN_lazy_function.hh
Normal 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 ¶ms, const Context &context) const;
|
||||
|
||||
/**
|
||||
* Utility to check that the guarantee by #Input::usage is followed.
|
||||
*/
|
||||
bool always_used_inputs_available(const Params ¶ms) 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 ¶ms, 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 ¶ms, 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
|
||||
@@ -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
|
||||
554
blender-5.2.0/source/blender/functions/FN_lazy_function_graph.hh
Normal file
554
blender-5.2.0/source/blender/functions/FN_lazy_function_graph.hh
Normal 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
|
||||
@@ -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 ¶ms,
|
||||
const Context &context) const;
|
||||
|
||||
virtual void log_after_node_execute(const FunctionNode &node,
|
||||
const Params ¶ms,
|
||||
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 ¶ms,
|
||||
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 ¶ms, const Context &context) const override;
|
||||
};
|
||||
|
||||
} // namespace blender::fn::lazy_function
|
||||
@@ -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
|
||||
152
blender-5.2.0/source/blender/functions/FN_multi_function.hh
Normal file
152
blender-5.2.0/source/blender/functions/FN_multi_function.hh
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¶m_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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¶m_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 ¶m_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
|
||||
38
blender-5.2.0/source/blender/functions/FN_user_data.hh
Normal file
38
blender-5.2.0/source/blender/functions/FN_user_data.hh
Normal 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
|
||||
567
blender-5.2.0/source/blender/functions/intern/field.cc
Normal file
567
blender-5.2.0/source/blender/functions/intern/field.cc
Normal 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>(¤t.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 ¬_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
|
||||
@@ -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 ©_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
|
||||
@@ -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 ¶ms) 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 ¶ms, 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 ¶ms,
|
||||
const Context &context) const
|
||||
{
|
||||
UNUSED_VARS(node, params, context);
|
||||
}
|
||||
|
||||
void GraphExecutorLogger::log_after_node_execute(const FunctionNode &node,
|
||||
const Params ¶ms,
|
||||
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
|
||||
File diff suppressed because it is too large
Load Diff
178
blender-5.2.0/source/blender/functions/intern/multi_function.cc
Normal file
178
blender-5.2.0/source/blender/functions/intern/multi_function.cc
Normal 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 ¶m_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
|
||||
@@ -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
|
||||
@@ -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 = ®istry::lookup("float ** 2"_ustr);
|
||||
pow_3 = ®istry::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 = ®istry::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 = ÷_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(÷nd);
|
||||
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
|
||||
@@ -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
|
||||
@@ -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 ¶m : 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 ¶m : 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 ¶m : 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 ¶m : 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 ¶m = 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 ¶m : 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 ¶m = 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
|
||||
@@ -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
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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 ®istry = 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 ®istry = 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
|
||||
14
blender-5.2.0/source/blender/functions/intern/user_data.cc
Normal file
14
blender-5.2.0/source/blender/functions/intern/user_data.cc
Normal 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
|
||||
308
blender-5.2.0/source/blender/functions/tests/FN_field_test.cc
Normal file
308
blender-5.2.0/source/blender/functions/tests/FN_field_test.cc
Normal 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
|
||||
@@ -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 ¶ms, 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 ¶ms, 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 ¶ms, 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user