Compositor: Implement Multi-Function Procedure Operation #126988

Merged
Omar Emara merged 19 commits from OmarEmaraDev/blender:multi-function-procedure-compositor-operation into main 2024-10-15 06:52:04 +02:00
19 changed files with 802 additions and 92 deletions

View File

@ -50,6 +50,7 @@ BLI_CPP_TYPE_MAKE(bool, CPPTypeFlags::BasicType)
BLI_CPP_TYPE_MAKE(float, CPPTypeFlags::BasicType)
BLI_CPP_TYPE_MAKE(blender::float2, CPPTypeFlags::BasicType)
BLI_CPP_TYPE_MAKE(blender::float3, CPPTypeFlags::BasicType)
BLI_CPP_TYPE_MAKE(blender::float4, CPPTypeFlags::BasicType)
BLI_CPP_TYPE_MAKE(blender::float4x4, CPPTypeFlags::BasicType)
BLI_CPP_TYPE_MAKE(int8_t, CPPTypeFlags::BasicType)

View File

@ -9,6 +9,7 @@ set(INC
../../blenkernel
../../blentranslation
../../draw
../../functions
../../gpu
../../imbuf
../../makesrna
@ -29,6 +30,7 @@ set(SRC
intern/evaluator.cc
intern/input_single_value_operation.cc
intern/meta_data.cc
intern/multi_function_procedure_operation.cc
intern/node_operation.cc
intern/operation.cc
intern/pixel_operation.cc
@ -53,6 +55,7 @@ set(SRC
COM_input_descriptor.hh
COM_input_single_value_operation.hh
COM_meta_data.hh
COM_multi_function_procedure_operation.hh
COM_node_operation.hh
COM_operation.hh
COM_pixel_operation.hh
@ -146,6 +149,7 @@ set(LIB
bf_render
PRIVATE bf::blenlib
bf_blenkernel
bf_functions
)
set(GLSL_SRC

View File

@ -0,0 +1,110 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include <memory>
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "FN_multi_function_procedure.hh"
#include "FN_multi_function_procedure_builder.hh"
#include "FN_multi_function_procedure_executor.hh"
#include "NOD_derived_node_tree.hh"
#include "NOD_multi_function.hh"
#include "COM_context.hh"
#include "COM_operation.hh"
#include "COM_pixel_operation.hh"
#include "COM_scheduler.hh"
namespace blender::realtime_compositor {
using namespace nodes::derived_node_tree_types;
/* ------------------------------------------------------------------------------------------------
* Multi-Function Procedure Operation
*
* A pixel operation that evaluates a multi-function procedure built from the pixel compile unit
* using the multi-function procedure builder, see FN_multi_function_procedure_builder.hh for more
* information. Also see the PixelOperation class for more information on pixel operations. */
class MultiFunctionProcedureOperation : public PixelOperation {
private:
/* The multi-function procedure, its builder, and executor that are backing the operation. This
* is created and compiled during construction. */
mf::Procedure procedure_;
mf::ProcedureBuilder procedure_builder_;
std::unique_ptr<mf::ProcedureExecutor> procedure_executor_;
/* A map that associates each node in the compile unit with an instance of its multi-function
* builder. */
Map<DNode, std::unique_ptr<nodes::NodeMultiFunctionBuilder>> node_multi_functions_;
/* A map that associates the output sockets of each node to the variables that were created for
* them. */
Map<DOutputSocket, mf::Variable *> output_to_variable_map_;
/* A vector that stores the intermediate variables that were implicitly created for the procedure
* but are not associated with a node output. Those variables are for such multi-functions like
* constant inputs and implicit conversion. */
Vector<mf::Variable *> implicit_variables_;
/* A vector that stores the identifiers of the parameters of the multi-function procedure in
* order. The parameters include both inputs and outputs. This is used to retrieve the input and
* output results for each of the parameters in the procedure. Note that parameters have no
* identifiers and are identified solely by their order. */
Vector<std::string> parameter_identifiers_;
public:
/* Build a multi-function procedure as well as an executor for it from the given pixel compile
* unit and execution schedule. */
MultiFunctionProcedureOperation(Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule);
/* Calls the multi-function procedure executor on the domain of the operator passing in the
* inputs and outputs as parameters. */
void execute() override;
private:
/* Builds the procedure by going over the nodes in the compile unit, calling their
* multi-functions and creating any necessary inputs or outputs to the operation/procedure. */
void build_procedure();
/* Get the variables corresponding to the inputs of the given node. The variables can be those
* that were returned by a previous call to a multi-function, those that were generated as
* constants for unlinked inputs, or those that were added as inputs to the operation/procedure
* itself. */
Vector<mf::Variable *> get_input_variables(DNode node);
/* Returns a constant variable that was created by calling a constant function carrying the value
* of the given input socket. */
mf::Variable *get_constant_input_variable(DInputSocket input);
/* Given an input in a node that is part of the compile unit that is connected to an output that
* is in a non that is not part of the compile unit. Declare an input to the operation/procedure
* for that output if not done already and return a variable that represent that input. */
mf::Variable *get_multi_function_input_variable(DInputSocket input_socket,
DOutputSocket output_socket);
/* Implicitly convert the type of the given variable that is passed from the given output socket
* to the given input socket if needed. This is done by adding an implicit conversion function
* whose output variable will be returned. If no conversion is needed, the given variable is
* returned as is. */
mf::Variable *do_variable_implicit_conversion(DInputSocket input_socket,
DOutputSocket output_socket,
mf::Variable *variable);
/* Given the variables that were returned by calling the multi-function for the given node,
* assign the variables to their corresponding outputs. And if an output is connected to a node
* outside of the compile unit or is used as the preview of the node, declare an output to the
* operation/procedure for it. */
void assign_output_variables(DNode node, Vector<mf::Variable *> &variables);
/* Populate an output to the operator/procedure for the given output socket whose value is stored
* in the given variable. */
void populate_operation_result(DOutputSocket output_socket, mf::Variable *variable);
};
} // namespace blender::realtime_compositor

View File

@ -74,6 +74,9 @@ class PixelOperation : public Operation {
/* A map that associates the identifier of each input of the operation with the output socket it
* is linked to. This is needed to help the compiler establish links between operations. */
Map<std::string, DOutputSocket> inputs_to_linked_outputs_map_;
/* A map that associates the output socket of a node that is not part of the pixel operation to
* the identifier of the input of the operation that was declared for it. */
Map<DOutputSocket, std::string> outputs_to_declared_inputs_map_;
/* A map that associates the output socket that provides the result of an output of the operation
* with the identifier of that output. This is needed to help the compiler establish links
* between operations. */
@ -85,6 +88,17 @@ class PixelOperation : public Operation {
public:
PixelOperation(Context &context, PixelCompileUnit &compile_unit, const Schedule &schedule);
/* Create one of the concrete subclasses based on the context. Deleting the operation is the
* caller's responsibility. */
static PixelOperation *create_operation(Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule);
/* Returns the maximum number of outputs that the PixelOperation can have. Pixel compile units
* need to be split into smaller units if the numbers of outputs they have is more than the
* number returned by this method. */
static int maximum_number_of_outputs(Context &context);
/* Compute a node preview for all nodes in the pixel operations if the node requires a preview.
*
* Previews are computed from results that are populated for outputs that are used to compute

View File

@ -26,10 +26,6 @@ namespace blender::realtime_compositor {
using namespace nodes::derived_node_tree_types;
/* A type representing a contiguous subset of the node execution schedule that will be compiled
* into a Pixel Operation. */
using PixelCompileUnit = VectorSet<DNode>;
/* ------------------------------------------------------------------------------------------------
* Shader Operation
*
@ -70,9 +66,6 @@ class ShaderOperation : public PixelOperation {
* the attribute that was created for it. This is used to share the same attribute with all
* inputs that are linked to the same output socket. */
Map<DOutputSocket, GPUNodeLink *> output_to_material_attribute_map_;
/* A map that associates the output socket of a node that is not part of the shader operation to
* the identifier of the input of the operation that was declared for it. */
Map<DOutputSocket, std::string> outputs_to_declared_inputs_map_;
public:
/* Construct and compile a GPU material from the given shader compile unit and execution schedule

View File

@ -163,7 +163,7 @@ void Evaluator::compile_and_evaluate_pixel_compile_unit(CompileState &compile_st
{
PixelCompileUnit &compile_unit = compile_state.get_pixel_compile_unit();
/* GPUs have hardware limitations on the number of output images shaders can have, so we might
/* Pixel operations might have limitations on the number of outputs they can have, so we might
* have to split the compile unit into smaller units to workaround this limitation. In practice,
* splitting will almost always never happen due to the scheduling strategy we use, so the base
* case remains fast. */
@ -172,9 +172,7 @@ void Evaluator::compile_and_evaluate_pixel_compile_unit(CompileState &compile_st
const DNode node = compile_unit[i];
number_of_outputs += compile_state.compute_pixel_node_operation_outputs_count(node);
/* The GPU module currently only supports up to 8 output images in shaders, but once this
* limitation is lifted, we can replace that with GPU_max_images(). */
if (number_of_outputs <= 8) {
if (number_of_outputs <= PixelOperation::maximum_number_of_outputs(context_)) {
continue;
}
@ -202,7 +200,7 @@ void Evaluator::compile_and_evaluate_pixel_compile_unit(CompileState &compile_st
}
const Schedule &schedule = compile_state.get_schedule();
PixelOperation *operation = new ShaderOperation(context_, compile_unit, schedule);
PixelOperation *operation = PixelOperation::create_operation(context_, compile_unit, schedule);
for (DNode node : compile_unit) {
compile_state.map_node_to_pixel_operation(node, operation);

View File

@ -0,0 +1,399 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <memory>
#include <string>
#include "BLI_assert.h"
#include "BLI_cpp_type.hh"
#include "BLI_generic_span.hh"
#include "BLI_index_mask.hh"
#include "BLI_map.hh"
#include "BLI_math_base.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "FN_multi_function.hh"
#include "FN_multi_function_builder.hh"
#include "FN_multi_function_context.hh"
#include "FN_multi_function_data_type.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"
#include "DNA_node_types.h"
#include "NOD_derived_node_tree.hh"
#include "NOD_multi_function.hh"
#include "NOD_node_declaration.hh"
#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_input_descriptor.hh"
#include "COM_multi_function_procedure_operation.hh"
#include "COM_pixel_operation.hh"
#include "COM_result.hh"
#include "COM_scheduler.hh"
#include "COM_utilities.hh"
namespace blender::realtime_compositor {
using namespace nodes::derived_node_tree_types;
MultiFunctionProcedureOperation::MultiFunctionProcedureOperation(Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule)
: PixelOperation(context, compile_unit, schedule), procedure_builder_(procedure_)
{
this->build_procedure();
procedure_executor_ = std::make_unique<mf::ProcedureExecutor>(procedure_);
}
static const CPPType &get_cpp_type(ResultType type)
{
switch (type) {
case ResultType::Float:
return CPPType::get<float>();
case ResultType::Vector:
case ResultType::Color:
return CPPType::get<float4>();
default:
/* Other types are internal and needn't be handled by operations. */
break;
}
BLI_assert_unreachable();
return CPPType::get<float>();
}
/* Adds the single value parameter of the given input to the given parameter_builder. */
static void add_single_value_parameter(mf::ParamsBuilder &parameter_builder, const Result &input)
{
BLI_assert(input.is_single_value());
switch (input.type()) {
case ResultType::Float:
parameter_builder.add_readonly_single_input_value(input.get_float_value());
return;
case ResultType::Color:
parameter_builder.add_readonly_single_input_value(input.get_color_value());
return;
case ResultType::Vector:
parameter_builder.add_readonly_single_input_value(input.get_vector_value());
return;
default:
/* Other types are internal and needn't be handled by operations. */
BLI_assert_unreachable();
break;
}
}
void MultiFunctionProcedureOperation::execute()
{
const Domain domain = compute_domain();
const int64_t size = int64_t(domain.size.x) * domain.size.y;
const IndexMask mask = IndexMask(size);
mf::ParamsBuilder parameter_builder{*procedure_executor_, &mask};
/* For each of the parameters, either add an input or an output depending on its interface type,
* allocating the outputs when needed. */
for (int i = 0; i < procedure_.params().size(); i++) {
if (procedure_.params()[i].type == mf::ParamType::InterfaceType::Input) {
Result &input = get_input(parameter_identifiers_[i]);
if (input.is_single_value()) {
add_single_value_parameter(parameter_builder, input);
}
else {
const GSpan span{get_cpp_type(input.type()), input.float_texture(), size};
parameter_builder.add_readonly_single_input(span);
}
}
else {
Result &result = get_result(parameter_identifiers_[i]);
result.allocate_texture(domain);
const GMutableSpan span{get_cpp_type(result.type()), result.float_texture(), size};
parameter_builder.add_uninitialized_single_output(span);
}
}
mf::ContextBuilder context_builder;
procedure_executor_->call_auto(mask, parameter_builder, context_builder);
}
void MultiFunctionProcedureOperation::build_procedure()
{
for (DNode node : compile_unit_) {
/* Get the multi-function of the node. */
auto &multi_function_builder = *node_multi_functions_.lookup_or_add_cb(node, [&]() {
return std::make_unique<nodes::NodeMultiFunctionBuilder>(*node.bnode(),
node.context()->btree());
});
node->typeinfo->build_multi_function(multi_function_builder);
const mf::MultiFunction &multi_function = multi_function_builder.function();
/* Get the variables of the inputs of the node, creating inputs to the operation/procedure if
* needed. */
Vector<mf::Variable *> input_variables = this->get_input_variables(node);
/* Call the node multi-function, getting the variables for its outputs. */
Vector<mf::Variable *> output_variables = procedure_builder_.add_call(multi_function,
input_variables);
/* Assign the output variables to the node's respective outputs, creating outputs for the
* operation/procedure if needed. */
this->assign_output_variables(node, output_variables);
}
/* Add destructor calls for the variables. */
for (const auto &item : output_to_variable_map_.items()) {
/* Variables that are used by the outputs should not be destructed. */
if (!output_sockets_to_output_identifiers_map_.contains(item.key)) {
procedure_builder_.add_destruct(*item.value);
}
}
for (mf::Variable *variable : implicit_variables_) {
procedure_builder_.add_destruct(*variable);
}
mf::ReturnInstruction &return_instruction = procedure_builder_.add_return();
mf::procedure_optimization::move_destructs_up(procedure_, return_instruction);
BLI_assert(procedure_.validate());
}
Vector<mf::Variable *> MultiFunctionProcedureOperation::get_input_variables(DNode node)
{
Vector<mf::Variable *> input_variables;
for (int i = 0; i < node->input_sockets().size(); i++) {
const DInputSocket input{node.context(), node->input_sockets()[i]};
if (!input->is_available()) {
continue;
}
/* Get the output linked to the input. If it is null, that means the input is unlinked and we
* generate a constant variable for it. */
const DOutputSocket output = get_output_linked_to_input(input);
if (!output) {
input_variables.append(this->get_constant_input_variable(input));
continue;
}
/* If the origin node is part of the multi-function procedure operation, then the output has an
* existing variable for it. */
if (compile_unit_.contains(output.node())) {
input_variables.append(output_to_variable_map_.lookup(output));
}
else {
/* Otherwise, the origin node is not part of the multi-function procedure operation, and a
* variable that represents an input to the multi-function procedure operation is used. */
input_variables.append(this->get_multi_function_input_variable(input, output));
}
/* Implicitly convert the variable type if needed by adding a call to an implicit conversion
* function. */
input_variables.last() = this->do_variable_implicit_conversion(
input, output, input_variables.last());
}
return input_variables;
}
mf::Variable *MultiFunctionProcedureOperation::get_constant_input_variable(DInputSocket input)
{
const mf::MultiFunction *constant_function = nullptr;
switch (input->type) {
case SOCK_FLOAT: {
const float value = input->default_value_typed<bNodeSocketValueFloat>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float>>(value);
break;
}
case SOCK_VECTOR: {
const float3 value = float3(input->default_value_typed<bNodeSocketValueVector>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float4>>(
float4(value, 0.0f));
break;
}
case SOCK_RGBA: {
const float4 value = float4(input->default_value_typed<bNodeSocketValueRGBA>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float4>>(value);
break;
}
default:
BLI_assert_unreachable();
break;
}
mf::Variable *constant_variable = procedure_builder_.add_call<1>(*constant_function)[0];
implicit_variables_.append(constant_variable);
return constant_variable;
}
mf::Variable *MultiFunctionProcedureOperation::get_multi_function_input_variable(
DInputSocket input_socket, DOutputSocket output_socket)
{
/* An input was already declared for that same output socket, so no need to declare it again and
* we just return its variable. But we update the domain priority of the input descriptor to be
* the higher priority of the existing descriptor and the descriptor of the new input socket.
* That's because the same output might be connected to multiple inputs inside the multi-function
* procedure operation which have different priorities. */
OmarEmaraDev marked this conversation as resolved Outdated

typo (proprieties)

typo (`proprieties`)
if (output_to_variable_map_.contains(output_socket)) {
const std::string input_identifier = outputs_to_declared_inputs_map_.lookup(output_socket);
InputDescriptor &input_descriptor = this->get_input_descriptor(input_identifier);
input_descriptor.domain_priority = math::min(
input_descriptor.domain_priority,
input_descriptor_from_input_socket(input_socket.bsocket()).domain_priority);
return output_to_variable_map_.lookup(output_socket);
}
const int input_index = inputs_to_linked_outputs_map_.size();
const std::string input_identifier = "input" + std::to_string(input_index);
/* Declare the input descriptor for this input and prefer to declare its type to be the same as
* the type of the output socket because doing type conversion in the multi-function procedure is
* cheaper. */
InputDescriptor input_descriptor = input_descriptor_from_input_socket(input_socket.bsocket());
input_descriptor.type = get_node_socket_result_type(output_socket.bsocket());
declare_input_descriptor(input_identifier, input_descriptor);
mf::Variable &variable = procedure_builder_.add_input_parameter(
mf::DataType::ForSingle(get_cpp_type(input_descriptor.type)), input_identifier);
parameter_identifiers_.append(input_identifier);
/* Map the output socket to the variable that was created for it. */
output_to_variable_map_.add(output_socket, &variable);
/* Map the identifier of the operation input to the output socket it is linked to. */
inputs_to_linked_outputs_map_.add_new(input_identifier, output_socket);
/* Map the output socket to the identifier of the operation input that was declared for it. */
outputs_to_declared_inputs_map_.add_new(output_socket, input_identifier);
return &variable;
}
/* Returns a multi-function that implicitly converts from the given variable type to the given
* expected type. nullptr will be returned if no conversion is needed. */
static mf::MultiFunction *get_conversion_function(const ResultType variable_type,
const ResultType expected_type)
{
/* No conversion needed. */
if (expected_type == variable_type) {
return nullptr;
}
if (variable_type == ResultType::Float && expected_type == ResultType::Vector) {
static auto float_to_vector_function = mf::build::SI1_SO<float, float4>(
"Float To Vector",
[](const float &input) -> float4 { return float4(float3(input), 1.0f); },
mf::build::exec_presets::AllSpanOrSingle());
return &float_to_vector_function;
}
if (variable_type == ResultType::Float && expected_type == ResultType::Color) {
static auto float_to_color_function = mf::build::SI1_SO<float, float4>(
"Float To Color",
[](const float &input) -> float4 { return float4(float3(input), 1.0f); },
OmarEmaraDev marked this conversation as resolved

To make compositor node tree totally compatible with other backends we will have to delete this at some point and just use BKE_type_conversions.hh. Same for float4 instead of ColorGeometry4f.

To make compositor node tree totally compatible with other backends we will have to delete this at some point and just use `BKE_type_conversions.hh`. Same for `float4` instead of `ColorGeometry4f`.
Review

Yes, that's the plan. Though our conversion functions and types are different at the moment, so we will have to sort that out first.

Yes, that's the plan. Though our conversion functions and types are different at the moment, so we will have to sort that out first.
mf::build::exec_presets::AllSpanOrSingle());
return &float_to_color_function;
}
if (variable_type == ResultType::Vector && expected_type == ResultType::Float) {
static auto vector_to_float_function = mf::build::SI1_SO<float4, float>(
"Vector To Float",
[](const float4 &input) -> float { return (input.x + input.y + input.z) / 3.0f; },
mf::build::exec_presets::AllSpanOrSingle());
return &vector_to_float_function;
}
if (variable_type == ResultType::Vector && expected_type == ResultType::Color) {
static auto vector_to_color_function = mf::build::SI1_SO<float4, float4>(
"Vector To Color",
[](const float4 &input) -> float4 { return float4(input.xyz(), 1.0f); },
mf::build::exec_presets::AllSpanOrSingle());
return &vector_to_color_function;
}
if (variable_type == ResultType::Color && expected_type == ResultType::Float) {
static auto color_to_float_function = mf::build::SI1_SO<float4, float>(
"Color To Float",
[](const float4 &input) -> float { return (input.x + input.y + input.z) / 3.0f; },
mf::build::exec_presets::AllSpanOrSingle());
return &color_to_float_function;
}
if (variable_type == ResultType::Color && expected_type == ResultType::Vector) {
/* No conversion needed. */
return nullptr;
}
BLI_assert_unreachable();
return nullptr;
}
mf::Variable *MultiFunctionProcedureOperation::do_variable_implicit_conversion(
DInputSocket input_socket, DOutputSocket output_socket, mf::Variable *variable)
{
const ResultType expected_type = get_node_socket_result_type(input_socket.bsocket());
const ResultType variable_type = get_node_socket_result_type(output_socket.bsocket());
const mf::MultiFunction *function = get_conversion_function(variable_type, expected_type);
if (!function) {
return variable;
}
mf::Variable *converted_variable = procedure_builder_.add_call<1>(*function, {variable})[0];
implicit_variables_.append(converted_variable);
return converted_variable;
}
void MultiFunctionProcedureOperation::assign_output_variables(DNode node,
Vector<mf::Variable *> &variables)
{
const DOutputSocket preview_output = find_preview_output_socket(node);
for (int i = 0; i < node->output_sockets().size(); i++) {
const DOutputSocket output{node.context(), node->output_sockets()[i]};
output_to_variable_map_.add_new(output, variables[i]);
/* If any of the nodes linked to the output are not part of the multi-function procedure
* operation but are part of the execution schedule, then an output result needs to be
* populated for it. */
const bool is_operation_output = is_output_linked_to_node_conditioned(output, [&](DNode node) {
return schedule_.contains(node) && !compile_unit_.contains(node);
});
/* If the output is used as the node preview, then an output result needs to be populated for
* it, and we additionally keep track of that output to later compute the previews from. */
const bool is_preview_output = output == preview_output;
if (is_preview_output) {
preview_outputs_.add(output);
}
if (is_operation_output || is_preview_output) {
this->populate_operation_result(output, variables[i]);
}
}
}
void MultiFunctionProcedureOperation::populate_operation_result(DOutputSocket output_socket,
mf::Variable *variable)
{
const uint output_id = output_sockets_to_output_identifiers_map_.size();
const std::string output_identifier = "output" + std::to_string(output_id);
OmarEmaraDev marked this conversation as resolved Outdated

const std::string

`const std::string`
const ResultType result_type = get_node_socket_result_type(output_socket.bsocket());
const Result result = context().create_result(result_type);
populate_result(output_identifier, result);
/* Map the output socket to the identifier of the newly populated result. */
output_sockets_to_output_identifiers_map_.add_new(output_socket, output_identifier);
procedure_builder_.add_output_parameter(*variable);
parameter_identifiers_.append(output_identifier);
}
} // namespace blender::realtime_compositor

View File

@ -2,6 +2,7 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include <string>
#include "BLI_map.hh"
@ -11,10 +12,12 @@
#include "COM_algorithm_compute_preview.hh"
#include "COM_context.hh"
#include "COM_multi_function_procedure_operation.hh"
#include "COM_operation.hh"
#include "COM_pixel_operation.hh"
#include "COM_result.hh"
#include "COM_scheduler.hh"
#include "COM_shader_operation.hh"
#include "COM_utilities.hh"
namespace blender::realtime_compositor {
@ -28,6 +31,28 @@ PixelOperation::PixelOperation(Context &context,
{
}
PixelOperation *PixelOperation::create_operation(Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule)
{
if (context.use_gpu()) {
return new ShaderOperation(context, compile_unit, schedule);
}
return new MultiFunctionProcedureOperation(context, compile_unit, schedule);
}
int PixelOperation::maximum_number_of_outputs(Context &context)
{
if (context.use_gpu()) {
/* The GPU module currently only supports up to 8 output images in shaders, but once this
* limitation is lifted, we can replace that with GPU_max_images(). */
return 8;
}
return std::numeric_limits<int>::max();
}
void PixelOperation::compute_preview()
{
for (const DOutputSocket &output : preview_outputs_) {

View File

@ -25,7 +25,6 @@
#include "NOD_node_declaration.hh"
#include "COM_context.hh"
#include "COM_operation.hh"
#include "COM_pixel_operation.hh"
#include "COM_result.hh"
#include "COM_scheduler.hh"
@ -184,7 +183,7 @@ void ShaderOperation::link_node_input_external(DInputSocket input_socket,
* But we update the domain priority of the input descriptor to be the higher priority of the
* existing descriptor and the descriptor of the new input socket. That's because the same
* output might be connected to multiple inputs inside the shader operation which have
* different proprieties. */
* different priorities. */
const std::string input_identifier = outputs_to_declared_inputs_map_.lookup(output_socket);
InputDescriptor &input_descriptor = this->get_input_descriptor(input_identifier);
input_descriptor.domain_priority = math::min(

View File

@ -540,6 +540,19 @@ inline auto build_multi_function_with_n_inputs_one_output(const char *name,
return CustomMF(name, call_fn, param_tags);
}
template<typename Out1, typename Out2, typename... In, typename ElementFn, typename ExecPreset>
inline auto build_multi_function_with_n_inputs_two_outputs(const char *name,
const ElementFn element_fn,
const ExecPreset exec_preset,
TypeSequence<In...> /*in_types*/)
{
constexpr auto param_tags = TypeSequence<ParamTag<ParamCategory::SingleInput, In>...,
ParamTag<ParamCategory::SingleOutput, Out1>,
ParamTag<ParamCategory::SingleOutput, Out2>>();
auto call_fn = build_multi_function_call_from_element_fn(element_fn, exec_preset, param_tags);
return CustomMF(name, call_fn, param_tags);
}
} // namespace detail
/** Build multi-function with 1 single-input and 1 single-output parameter. */
@ -647,6 +660,20 @@ inline auto SM(const char *name,
return detail::CustomMF(name, call_fn, param_tags);
}
/** Build multi-function with 1 single-input and 2 single-output parameter. */
template<typename In1,
typename Out1,
typename Out2,
typename ElementFn,
typename ExecPreset = exec_presets::Materialized>
inline auto SI1_SO2(const char *name,
const ElementFn element_fn,
const ExecPreset exec_preset = exec_presets::Materialized())
{
return detail::build_multi_function_with_n_inputs_two_outputs<Out1, Out2>(
name, element_fn, exec_preset, TypeSequence<In1>());
}
} // namespace blender::fn::multi_function::build
namespace blender::fn::multi_function {

View File

@ -13,8 +13,12 @@
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
namespace blender::nodes {
void node_math_build_multi_function(NodeMultiFunctionBuilder &builder);
struct FloatMathOperationInfo {
StringRefNull title_case_name;
StringRefNull shader_name;

View File

@ -40,8 +40,17 @@ class NodeMultiFunctionBuilder : NonCopyable, NonMovable {
*/
template<typename T, typename... Args> void construct_and_set_matching_fn(Args &&...args);
/**
* Similar to #construct_and_set_matching_fn, but can be used when the type name of the
* multi-function is not known (e.g. when using `mf::build::SI1_SO`).
*
* \param create_multi_function: A function that returns the multi-function by value.
*/
template<typename Fn> void construct_and_set_matching_fn_cb(Fn &&create_multi_function);
OmarEmaraDev marked this conversation as resolved

I clearly do not see any usage of this thing in the PR.

I clearly do not see any usage of this thing in the PR.
const bNode &node();
const bNodeTree &tree();
const mf::MultiFunction &function();
};
/**
@ -82,6 +91,11 @@ inline const bNodeTree &NodeMultiFunctionBuilder::tree()
return tree_;
}
inline const mf::MultiFunction &NodeMultiFunctionBuilder::function()
{
return *built_fn_;
}
inline void NodeMultiFunctionBuilder::set_matching_fn(const mf::MultiFunction *fn)
{
built_fn_ = fn;
@ -99,6 +113,15 @@ inline void NodeMultiFunctionBuilder::construct_and_set_matching_fn(Args &&...ar
built_fn_ = &*owned_built_fn_;
}
template<typename Fn>
inline void NodeMultiFunctionBuilder::construct_and_set_matching_fn_cb(Fn &&create_multi_function)
{
using T = decltype(create_multi_function());
T *allocated_function = new T(create_multi_function());
owned_built_fn_ = std::shared_ptr<T>(allocated_function);
built_fn_ = &*owned_built_fn_;
}
/** \} */
/* -------------------------------------------------------------------- */

View File

@ -129,6 +129,7 @@ set(LIB
PRIVATE bf::blenlib
PRIVATE bf::depsgraph
PRIVATE bf::dna
bf_functions
PRIVATE bf::intern::guardedalloc
PRIVATE bf::extern::fmtlib
bf_realtime_compositor

View File

@ -6,6 +6,14 @@
* \ingroup cmpnodes
*/
#include <limits>
#include "BLI_math_color.h"
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
OmarEmaraDev marked this conversation as resolved
Review

Make sure the include is in all updated node files, unity builds might hide missing includes.

Make sure the include is in all updated node files, unity builds might hide missing includes.
#include "UI_interface.hh"
#include "UI_resources.hh"
@ -71,6 +79,66 @@ static ShaderNode *get_compositor_shader_node(DNode node)
return new BrightContrastShaderNode(node);
}
/* The algorithm is by Werner D. Streidt, extracted of OpenCV demhist.c:
* http://visca.com/ffactory/archives/5-99/msg00021.html */
template<bool UsePremultiply>
static float4 brightness_and_contrast(const float4 &color,
const float brightness,
const float contrast)
{
float scaled_brightness = brightness / 100.0f;
float delta = contrast / 200.0f;
float multiplier, offset;
if (contrast > 0.0f) {
multiplier = 1.0f - delta * 2.0f;
multiplier = 1.0f / math::max(multiplier, std::numeric_limits<float>::epsilon());
offset = multiplier * (scaled_brightness - delta);
}
else {
delta *= -1.0f;
multiplier = math::max(1.0f - delta * 2.0f, 0.0f);
offset = multiplier * scaled_brightness + delta;
}
float4 input_color = color;
if constexpr (UsePremultiply) {
premul_to_straight_v4(input_color);
}
float4 result = float4(input_color.xyz() * multiplier + offset, input_color.w);
if constexpr (UsePremultiply) {
straight_to_premul_v4(result);
}
return result;
}
static void node_build_multi_function(blender::nodes::NodeMultiFunctionBuilder &builder)
{
static auto premultiply_used_function = mf::build::SI3_SO<float4, float, float, float4>(
"Bright And Contrast Use Premultiply",
[](const float4 &color, const float brightness, const float contrast) -> float4 {
return brightness_and_contrast<true>(color, brightness, contrast);
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
static auto no_premultiply_function = mf::build::SI3_SO<float4, float, float, float4>(
"Bright And Contrast No Premultiply",
[](const float4 &color, const float brightness, const float contrast) -> float4 {
return brightness_and_contrast<false>(color, brightness, contrast);
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
const bool use_premultiply = builder.node().custom1;
if (use_premultiply) {
builder.set_matching_fn(premultiply_used_function);
}
else {
builder.set_matching_fn(no_premultiply_function);
}
}
} // namespace blender::nodes::node_composite_brightness_cc
void register_node_type_cmp_brightcontrast()
@ -84,6 +152,7 @@ void register_node_type_cmp_brightcontrast()
ntype.draw_buttons = file_ns::node_composit_buts_brightcontrast;
ntype.initfunc = file_ns::node_composit_init_brightcontrast;
ntype.get_compositor_shader_node = file_ns::get_compositor_shader_node;
ntype.build_multi_function = file_ns::node_build_multi_function;
blender::bke::node_register_type(&ntype);
}

View File

@ -6,6 +6,13 @@
* \ingroup cmpnodes
*/
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "NOD_multi_function.hh"
#include "FN_multi_function_builder.hh"
#include "GPU_material.hh"
#include "COM_shader_node.hh"
@ -50,6 +57,17 @@ static ShaderNode *get_compositor_shader_node(DNode node)
return new GammaShaderNode(node);
}
static void node_build_multi_function(blender::nodes::NodeMultiFunctionBuilder &builder)
{
static auto gamma_function = mf::build::SI2_SO<float4, float, float4>(
"Gamma",
[](const float4 &color, const float gamma) -> float4 {
return float4(math::safe_pow(color.xyz(), gamma), color.w);
},
mf::build::exec_presets::SomeSpanOrSingle<0>());
builder.set_matching_fn(gamma_function);
}
} // namespace blender::nodes::node_composite_gamma_cc
void register_node_type_cmp_gamma()
@ -61,6 +79,7 @@ void register_node_type_cmp_gamma()
cmp_node_type_base(&ntype, CMP_NODE_GAMMA, "Gamma", NODE_CLASS_OP_COLOR);
ntype.declare = file_ns::cmp_node_gamma_declare;
ntype.get_compositor_shader_node = file_ns::get_compositor_shader_node;
ntype.build_multi_function = file_ns::node_build_multi_function;
blender::bke::node_register_type(&ntype);
}

View File

@ -11,6 +11,7 @@
#include "COM_shader_node.hh"
#include "NOD_math_functions.hh"
#include "NOD_multi_function.hh"
#include "NOD_socket_search_link.hh"
#include "RNA_enum_types.hh"
@ -129,6 +130,7 @@ void register_node_type_cmp_math()
ntype.labelfunc = node_math_label;
ntype.updatefunc = node_math_update;
ntype.get_compositor_shader_node = file_ns::get_compositor_shader_node;
ntype.build_multi_function = blender::nodes::node_math_build_multi_function;
ntype.gather_link_search_ops = file_ns::node_gather_link_searches;
blender::bke::node_register_type(&ntype);

View File

@ -6,6 +6,10 @@
* \ingroup cmpnodes
*/
#include "FN_multi_function_builder.hh"
#include "NOD_multi_function.hh"
#include "GPU_material.hh"
#include "COM_shader_node.hh"
@ -66,6 +70,23 @@ static ShaderNode *get_compositor_shader_node(DNode node)
return new NormalShaderNode(node);
}
static void node_build_multi_function(blender::nodes::NodeMultiFunctionBuilder &builder)
{
const bNodeSocket &normal_output = builder.node().output_by_identifier("Normal");
const float3 node_normal = normal_output.default_value_typed<bNodeSocketValueVector>()->value;
const float3 normalized_node_normal = math::normalize(node_normal);
builder.construct_and_set_matching_fn_cb([=]() {
return mf::build::SI1_SO2<float4, float4, float>(
"Normal And Dot",
[=](const float4 &normal, float4 &output_normal, float &dot) -> void {
output_normal = float4(normalized_node_normal, 0.0f);
OmarEmaraDev marked this conversation as resolved

Looks like mf::MultiFunction case.. At least, not sure if this is correct / worth it to combine kinda simple computation of two values ​​in one callback without explicit output.

Looks like `mf::MultiFunction` case.. At least, not sure if this is correct / worth it to combine kinda simple computation of two values ​​in one callback without explicit output.
Review

I think it's perfectly fine to do that right now to avoid all the boilerplate. We could potentially investigate optimizations that avoid the computation of unnecessary outputs if necessary.

I think it's perfectly fine to do that right now to avoid all the boilerplate. We could potentially investigate optimizations that avoid the computation of unnecessary outputs if necessary.

Not sure about this, but does this mean that lambda is instantiated for single/field of outputs as well as for input? So there is 6 instances instead of just 2\

Not sure about this, but does this mean that lambda is instantiated for single/field of outputs as well as for input? So there is 6 instances instead of just 2\
Review

Right now it's only instantiated for different kinds of inputs. But it's not impossible to also instantiate for different kinds of outputs if that ever becomes necessary.

Right now it's only instantiated for different kinds of inputs. But it's not impossible to also instantiate for different kinds of outputs if that ever becomes necessary.
dot = -math::dot(normal.xyz(), normalized_node_normal);
},
mf::build::exec_presets::AllSpanOrSingle());
});
}
} // namespace blender::nodes::node_composite_normal_cc
void register_node_type_cmp_normal()
@ -77,6 +98,7 @@ void register_node_type_cmp_normal()
cmp_node_type_base(&ntype, CMP_NODE_NORMAL, "Normal", NODE_CLASS_OP_VECTOR);
ntype.declare = file_ns::cmp_node_normal_declare;
ntype.get_compositor_shader_node = file_ns::get_compositor_shader_node;
ntype.build_multi_function = file_ns::node_build_multi_function;
blender::bke::node_register_type(&ntype);
}

View File

@ -6,6 +6,83 @@
namespace blender::nodes {
static const mf::MultiFunction *get_base_multi_function(const bNode &node)
{
const int mode = node.custom1;
const mf::MultiFunction *base_fn = nullptr;
try_dispatch_float_math_fl_to_fl(
mode, [&](auto devi_fn, auto function, const FloatMathOperationInfo &info) {
static auto fn = mf::build::SI1_SO<float, float>(
info.title_case_name.c_str(), function, devi_fn);
base_fn = &fn;
});
if (base_fn != nullptr) {
return base_fn;
}
try_dispatch_float_math_fl_fl_to_fl(
mode, [&](auto devi_fn, auto function, const FloatMathOperationInfo &info) {
static auto fn = mf::build::SI2_SO<float, float, float>(
info.title_case_name.c_str(), function, devi_fn);
base_fn = &fn;
});
if (base_fn != nullptr) {
return base_fn;
}
try_dispatch_float_math_fl_fl_fl_to_fl(
mode, [&](auto devi_fn, auto function, const FloatMathOperationInfo &info) {
static auto fn = mf::build::SI3_SO<float, float, float, float>(
info.title_case_name.c_str(), function, devi_fn);
base_fn = &fn;
});
if (base_fn != nullptr) {
return base_fn;
}
return nullptr;
}
class ClampWrapperFunction : public mf::MultiFunction {
private:
const mf::MultiFunction &fn_;
public:
ClampWrapperFunction(const mf::MultiFunction &fn) : fn_(fn)
{
this->set_signature(&fn.signature());
}
void call(const IndexMask &mask, mf::Params params, mf::Context context) const override
{
fn_.call(mask, params, context);
/* Assumes the output parameter is the last one. */
const int output_param_index = this->param_amount() - 1;
/* This has actually been initialized in the call above. */
MutableSpan<float> results = params.uninitialized_single_output<float>(output_param_index);
mask.foreach_index_optimized<int>([&](const int i) {
float &value = results[i];
CLAMP(value, 0.0f, 1.0f);
});
}
};
void node_math_build_multi_function(NodeMultiFunctionBuilder &builder)
{
const mf::MultiFunction *base_function = get_base_multi_function(builder.node());
const bool clamp_output = builder.node().custom2 != 0;
if (clamp_output) {
builder.construct_and_set_matching_fn<ClampWrapperFunction>(*base_function);
}
else {
builder.set_matching_fn(base_function);
}
}
const FloatMathOperationInfo *get_float_math_operation_info(const int operation)
{

View File

@ -103,83 +103,6 @@ static int gpu_shader_math(GPUMaterial *mat,
return 0;
}
static const mf::MultiFunction *get_base_multi_function(const bNode &node)
{
const int mode = node.custom1;
const mf::MultiFunction *base_fn = nullptr;
try_dispatch_float_math_fl_to_fl(
mode, [&](auto devi_fn, auto function, const FloatMathOperationInfo &info) {
static auto fn = mf::build::SI1_SO<float, float>(
info.title_case_name.c_str(), function, devi_fn);
base_fn = &fn;
});
if (base_fn != nullptr) {