Files
test/source/blender/functions/intern/field.cc
Jacques Lucke 2cfcb8b0b8 BLI: refactor IndexMask for better performance and memory usage
Goals of this refactor:
* Reduce memory consumption of `IndexMask`. The old `IndexMask` uses an
  `int64_t` for each index which is more than necessary in pretty much all
  practical cases currently. Using `int32_t` might still become limiting
  in the future in case we use this to index e.g. byte buffers larger than
  a few gigabytes. We also don't want to template `IndexMask`, because
  that would cause a split in the "ecosystem", or everything would have to
  be implemented twice or templated.
* Allow for more multi-threading. The old `IndexMask` contains a single
  array. This is generally good but has the problem that it is hard to fill
  from multiple-threads when the final size is not known from the beginning.
  This is commonly the case when e.g. converting an array of bool to an
  index mask. Currently, this kind of code only runs on a single thread.
* Allow for efficient set operations like join, intersect and difference.
  It should be possible to multi-thread those operations.
* It should be possible to iterate over an `IndexMask` very efficiently.
  The most important part of that is to avoid all memory access when iterating
  over continuous ranges. For some core nodes (e.g. math nodes), we generate
  optimized code for the cases of irregular index masks and simple index ranges.

To achieve these goals, a few compromises had to made:
* Slicing of the mask (at specific indices) and random element access is
  `O(log #indices)` now, but with a low constant factor. It should be possible
  to split a mask into n approximately equally sized parts in `O(n)` though,
  making the time per split `O(1)`.
* Using range-based for loops does not work well when iterating over a nested
  data structure like the new `IndexMask`. Therefor, `foreach_*` functions with
  callbacks have to be used. To avoid extra code complexity at the call site,
  the `foreach_*` methods support multi-threading out of the box.

The new data structure splits an `IndexMask` into an arbitrary number of ordered
`IndexMaskSegment`. Each segment can contain at most `2^14 = 16384` indices. The
indices within a segment are stored as `int16_t`. Each segment has an additional
`int64_t` offset which allows storing arbitrary `int64_t` indices. This approach
has the main benefits that segments can be processed/constructed individually on
multiple threads without a serial bottleneck. Also it reduces the memory
requirements significantly.

For more details see comments in `BLI_index_mask.hh`.

I did a few tests to verify that the data structure generally improves
performance and does not cause regressions:
* Our field evaluation benchmarks take about as much as before. This is to be
  expected because we already made sure that e.g. add node evaluation is
  vectorized. The important thing here is to check that changes to the way we
  iterate over the indices still allows for auto-vectorization.
* Memory usage by a mask is about 1/4 of what it was before in the average case.
  That's mainly caused by the switch from `int64_t` to `int16_t` for indices.
  In the worst case, the memory requirements can be larger when there are many
  indices that are very far away. However, when they are far away from each other,
  that indicates that there aren't many indices in total. In common cases, memory
  usage can be way lower than 1/4 of before, because sub-ranges use static memory.
* For some more specific numbers I benchmarked `IndexMask::from_bools` in
  `index_mask_from_selection` on 10.000.000 elements at various probabilities for
  `true` at every index:
  ```
  Probability      Old        New
  0              4.6 ms     0.8 ms
  0.001          5.1 ms     1.3 ms
  0.2            8.4 ms     1.8 ms
  0.5           15.3 ms     3.0 ms
  0.8           20.1 ms     3.0 ms
  0.999         25.1 ms     1.7 ms
  1             13.5 ms     1.1 ms
  ```

Pull Request: https://projects.blender.org/blender/blender/pulls/104629
2023-05-24 18:11:41 +02:00

829 lines
29 KiB
C++

/* 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.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 {
/**
* 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<GFieldRef, GFieldRef> 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<std::reference_wrapper<const FieldInput>> deduplicated_field_inputs;
};
/**
* Collects some information from the field tree that is required by later steps.
*/
static FieldTreeInfo preprocess_field_tree(Span<GFieldRef> entry_fields)
{
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()) {
GFieldRef field = fields_to_check.pop();
const FieldNode &field_node = field.node();
switch (field_node.node_type()) {
case FieldNodeType::Input: {
const FieldInput &field_input = static_cast<const FieldInput &>(field_node);
field_tree_info.deduplicated_field_inputs.add(field_input);
break;
}
case FieldNodeType::Operation: {
const FieldOperation &operation = static_cast<const FieldOperation &>(field_node);
for (const GFieldRef operation_input : operation.inputs()) {
field_tree_info.field_users.add(operation_input, field);
if (handled_fields.add(operation_input)) {
fields_to_check.push(operation_input);
}
}
break;
}
case FieldNodeType::Constant: {
/* Nothing to do. */
break;
}
}
}
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<std::reference_wrapper<const FieldInput>> field_inputs)
{
Vector<GVArray> field_context_inputs;
for (const FieldInput &field_input : field_inputs) {
GVArray varray = context.get_varray_for_input(field_input, mask, scope);
if (!varray) {
const CPPType &type = field_input.cpp_type();
varray = GVArray::ForSingleDefault(type, mask.min_array_size());
}
field_context_inputs.append(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<GFieldRef> find_varying_fields(const FieldTreeInfo &field_tree_info,
Span<GVArray> field_context_inputs)
{
Set<GFieldRef> found_fields;
Stack<GFieldRef> 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 i : field_context_inputs.index_range()) {
const GVArray &varray = field_context_inputs[i];
if (varray.is_single()) {
continue;
}
const FieldInput &field_input = field_tree_info.deduplicated_field_inputs[i];
const GFieldRef field_input_field{field_input, 0};
const Span<GFieldRef> users = field_tree_info.field_users.lookup(field_input_field);
for (const GFieldRef &field : users) {
if (found_fields.add(field)) {
fields_to_check.push(field);
}
}
}
while (!fields_to_check.is_empty()) {
GFieldRef field = fields_to_check.pop();
const Span<GFieldRef> users = field_tree_info.field_users.lookup(field);
for (GFieldRef field : users) {
if (found_fields.add(field)) {
fields_to_check.push(field);
}
}
}
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)
{
mf::ProcedureBuilder builder{procedure};
/* Every input, intermediate and output field corresponds to a variable in the procedure. */
Map<GFieldRef, mf::Variable *> variable_by_field;
/* Start by adding the field inputs as parameters to the procedure. */
for (const FieldInput &field_input : field_tree_info.deduplicated_field_inputs) {
mf::Variable &variable = builder.add_input_parameter(
mf::DataType::ForSingle(field_input.cpp_type()), field_input.debug_name());
variable_by_field.add_new({field_input, 0}, &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 does never depend 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;
if (variable_by_field.contains(field)) {
/* The field has been handled already. */
fields_to_check.pop();
continue;
}
const FieldNode &field_node = field.node();
switch (field_node.node_type()) {
case FieldNodeType::Input: {
/* Field inputs should already be handled above. */
break;
}
case FieldNodeType::Operation: {
const FieldOperation &operation_node = static_cast<const FieldOperation &>(field.node());
const Span<GField> operation_inputs = operation_node.inputs();
if (field_with_index.current_input_index < operation_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({operation_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 = operation_node.multi_function();
Vector<mf::Variable *> 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 = operation_inputs[param_input_index];
variables[param_index] = variable_by_field.lookup(input_field);
param_input_index++;
}
else if (interface_type == mf::ParamType::Output) {
const GFieldRef output_field{operation_node, param_output_index};
const bool output_is_ignored =
field_tree_info.field_users.lookup(output_field).is_empty() &&
!output_fields.contains(output_field);
if (output_is_ignored) {
/* Ignored outputs don't need a variable. */
variables[param_index] = nullptr;
}
else {
/* Create a new variable for used outputs. */
mf::Variable &new_variable = procedure.new_variable(param_type.data_type());
variables[param_index] = &new_variable;
variable_by_field.add_new(output_field, &new_variable);
}
param_output_index++;
}
else {
BLI_assert_unreachable();
}
}
builder.add_call_with_all_variables(multi_function, variables);
}
break;
}
case FieldNodeType::Constant: {
const FieldConstant &constant_node = static_cast<const FieldConstant &>(field_node);
const mf::MultiFunction &fn = procedure.construct_function<mf::CustomMF_GenericConstant>(
constant_node.type(), constant_node.value().get(), false);
mf::Variable &new_variable = *builder.add_call<1>(fn)[0];
variable_by_field.add_new(field, &new_variable);
break;
}
}
}
}
/* Add output parameters to the procedure. */
Set<mf::Variable *> already_output_variables;
for (const GFieldRef &field : output_fields) {
mf::Variable *variable = variable_by_field.lookup(field);
if (!already_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];
}
builder.add_output_parameter(*variable);
}
/* Remove the variables that should not be destructed from the map. */
for (const GFieldRef &field : output_fields) {
variable_by_field.remove(field);
}
/* Add destructor calls for the remaining variables. */
for (mf::Variable *variable : variable_by_field.values()) {
builder.add_destruct(*variable);
}
mf::ReturnInstruction &return_instr = builder.add_return();
mf::procedure_optimization::move_destructs_up(procedure, return_instr);
// 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)
{
Vector<GVArray> r_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();
r_varrays[i] = GVArray::ForEmpty(type);
}
return r_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_field_inputs);
/* Finish fields that don't need any processing directly. */
for (const int out_index : fields_to_evaluate.index_range()) {
const GFieldRef &field = fields_to_evaluate[out_index];
const FieldNode &field_node = field.node();
switch (field_node.node_type()) {
case FieldNodeType::Input: {
const FieldInput &field_input = static_cast<const FieldInput &>(field.node());
const int field_input_index = field_tree_info.deduplicated_field_inputs.index_of(
field_input);
const GVArray &varray = field_context_inputs[field_input_index];
r_varrays[out_index] = varray;
break;
}
case FieldNodeType::Constant: {
const FieldConstant &field_constant = static_cast<const FieldConstant &>(field.node());
r_varrays[out_index] = GVArray::ForSingleRef(
field_constant.type(), mask.min_array_size(), field_constant.value().get());
break;
}
case FieldNodeType::Operation: {
break;
}
}
}
Set<GFieldRef> varying_fields = find_varying_fields(field_tree_info, field_context_inputs);
/* Separate 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 i : fields_to_evaluate.index_range()) {
if (r_varrays[i]) {
/* Already done. */
continue;
}
GFieldRef field = fields_to_evaluate[i];
if (varying_fields.contains(field)) {
varying_fields_to_evaluate.append(field);
varying_field_indices.append(i);
}
else {
constant_fields_to_evaluate.append(field);
constant_field_indices.append(i);
}
}
/* 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.linear_allocator().allocate(type.size() * array_size, type.alignment());
if (!type.is_trivially_destructible()) {
/* Destruct values in the end. */
scope.add_destruct_call(
[buffer, mask, &type]() { type.destruct_indices(buffer, mask); });
}
r_varrays[out_index] = GVArray::ForSpan({type, buffer, array_size});
}
else {
/* Write the result into the existing span. */
buffer = dst_varray.get_internal_span().data();
r_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.linear_allocator().allocate(type.size(), type.alignment());
if (!type.is_trivially_destructible()) {
/* Destruct value in the end. */
scope.add_destruct_call([buffer, &type]() { type.destruct(buffer); });
}
/* 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];
r_varrays[out_index] = GVArray::ForSingleRef(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 = r_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()) {
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);
}
});
});
}
r_varrays[out_index] = dst_varray;
}
}
return r_varrays;
}
void evaluate_constant_field(const GField &field, void *r_value)
{
if (field.node().depends_on_input()) {
const CPPType &type = field.cpp_type();
type.value_initialize(r_value);
return;
}
ResourceScope scope;
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.node().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 = make_constant_field(type, buffer);
type.destruct(buffer);
return new_field;
}
Field<bool> invert_boolean_field(const Field<bool> &field)
{
static auto not_fn = mf::build::SI1_SO<bool, bool>(
"Not", [](bool a) { return !a; }, mf::build::exec_presets::AllSpanOrSingle());
auto not_op = FieldOperation::Create(not_fn, {field});
return Field<bool>(not_op);
}
GField make_constant_field(const CPPType &type, const void *value)
{
auto constant_node = std::make_shared<FieldConstant>(type, value);
return GField{std::move(constant_node)};
}
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")
{
category_ = Category::Generated;
}
GVArray IndexFieldInput::get_index_varray(const IndexMask &mask)
{
auto index_func = [](int i) { return i; };
return VArray<int>::ForFunc(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);
}
uint64_t IndexFieldInput::hash() const
{
/* Some random constant hash. */
return 128736487678;
}
bool IndexFieldInput::is_equal_to(const fn::FieldNode &other) const
{
return dynamic_cast<const IndexFieldInput *>(&other) != nullptr;
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #FieldNode
* \{ */
/* Avoid generating the destructor in every translation unit. */
FieldNode::~FieldNode() = default;
void FieldNode::for_each_field_input_recursive(FunctionRef<void(const FieldInput &)> fn) const
{
if (field_inputs_) {
for (const FieldInput &field_input : field_inputs_->deduplicated_nodes) {
fn(field_input);
if (&field_input != this) {
field_input.for_each_field_input_recursive(fn);
}
}
}
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #FieldOperation
* \{ */
FieldOperation::FieldOperation(std::shared_ptr<const mf::MultiFunction> function,
Vector<GField> inputs)
: FieldOperation(*function, std::move(inputs))
{
owned_function_ = std::move(function);
}
/* Avoid generating the destructor in every translation unit. */
FieldOperation::~FieldOperation() = default;
/**
* Returns the field inputs used by all the provided fields.
* This tries to reuse an existing #FieldInputs whenever possible to avoid copying it.
*/
static std::shared_ptr<const FieldInputs> combine_field_inputs(Span<GField> fields)
{
/* The #FieldInputs that we try to reuse if possible. */
const std::shared_ptr<const FieldInputs> *field_inputs_candidate = nullptr;
for (const GField &field : fields) {
const std::shared_ptr<const FieldInputs> &field_inputs = field.node().field_inputs();
/* Only try to reuse non-empty #FieldInputs. */
if (field_inputs && !field_inputs->nodes.is_empty()) {
if (field_inputs_candidate == nullptr) {
field_inputs_candidate = &field_inputs;
}
else if ((*field_inputs_candidate)->nodes.size() < field_inputs->nodes.size()) {
/* Always try to reuse the #FieldInputs that has the most nodes already. */
field_inputs_candidate = &field_inputs;
}
}
}
if (field_inputs_candidate == nullptr) {
/* None of the field depends on an input. */
return {};
}
/* Check if all inputs are in the candidate. */
Vector<const FieldInput *> inputs_not_in_candidate;
for (const GField &field : fields) {
const std::shared_ptr<const FieldInputs> &field_inputs = field.node().field_inputs();
if (!field_inputs) {
continue;
}
if (&field_inputs == field_inputs_candidate) {
continue;
}
for (const FieldInput *field_input : field_inputs->nodes) {
if (!(*field_inputs_candidate)->nodes.contains(field_input)) {
inputs_not_in_candidate.append(field_input);
}
}
}
if (inputs_not_in_candidate.is_empty()) {
/* The existing #FieldInputs can be reused, because no other field has additional inputs. */
return *field_inputs_candidate;
}
/* Create new #FieldInputs that contains all of the inputs that the fields depend on. */
std::shared_ptr<FieldInputs> new_field_inputs = std::make_shared<FieldInputs>(
**field_inputs_candidate);
for (const FieldInput *field_input : inputs_not_in_candidate) {
new_field_inputs->nodes.add(field_input);
new_field_inputs->deduplicated_nodes.add(*field_input);
}
return new_field_inputs;
}
FieldOperation::FieldOperation(const mf::MultiFunction &function, Vector<GField> inputs)
: FieldNode(FieldNodeType::Operation), function_(&function), inputs_(std::move(inputs))
{
field_inputs_ = combine_field_inputs(inputs_);
}
/** \} */
/* -------------------------------------------------------------------- */
/** \name #FieldInput
* \{ */
FieldInput::FieldInput(const CPPType &type, std::string debug_name)
: FieldNode(FieldNodeType::Input), type_(&type), debug_name_(std::move(debug_name))
{
std::shared_ptr<FieldInputs> field_inputs = std::make_shared<FieldInputs>();
field_inputs->nodes.add_new(this);
field_inputs->deduplicated_nodes.add_new(*this);
field_inputs_ = std::move(field_inputs);
}
/* Avoid generating the destructor in every translation unit. */
FieldInput::~FieldInput() = default;
/** \} */
/* -------------------------------------------------------------------- */
/** \name #FieldConstant
* \{ */
FieldConstant::FieldConstant(const CPPType &type, const void *value)
: FieldNode(FieldNodeType::Constant), type_(type)
{
value_ = MEM_mallocN_aligned(type.size(), type.alignment(), __func__);
type.copy_construct(value, value_);
}
FieldConstant::~FieldConstant()
{
type_.destruct(value_);
MEM_freeN(value_);
}
const CPPType &FieldConstant::output_cpp_type(int output_index) const
{
BLI_assert(output_index == 0);
UNUSED_VARS_NDEBUG(output_index);
return type_;
}
const CPPType &FieldConstant::type() const
{
return type_;
}
GPointer FieldConstant::value() const
{
return {type_, value_};
}
/** \} */
/* -------------------------------------------------------------------- */
/** \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.construct<IndexMaskMemory>());
}
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::ForSpan(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,
IndexMask full_mask,
ResourceScope &scope)
{
if (selection_field) {
VArray<bool> selection =
evaluate_fields(scope, {selection_field}, full_mask, context)[0].typed<bool>();
return index_mask_from_selection(full_mask, selection, scope);
}
return full_mask;
}
void FieldEvaluator::evaluate()
{
BLI_assert_msg(!is_evaluated_, "Cannot evaluate fields twice.");
selection_mask_ = evaluate_selection(selection_field_, context_, mask_, scope_);
Array<GFieldRef> fields(fields_to_evaluate_.size());
for (const int i : fields_to_evaluate_.index_range()) {
fields[i] = fields_to_evaluate_[i];
}
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()
{
BLI_assert(is_evaluated_);
return selection_mask_;
}
/** \} */
} // namespace blender::fn