Add Chromium-only Blender WebEngine parity work

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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