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,381 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector_set.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "COM_compile_state.hh"
#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_implicit_input_operation.hh"
#include "COM_input_descriptor.hh"
#include "COM_node_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::compositor {
CompileState::CompileState(const Context &context, const Schedule &schedule)
: context_(context), schedule_(schedule)
{
}
const Schedule &CompileState::get_schedule()
{
return schedule_;
}
void CompileState::map_node_to_node_operation(const bNode &node, NodeOperation *operations)
{
node_operations_.add_new(&node, operations);
}
void CompileState::map_node_to_pixel_operation(const bNode &node, PixelOperation *operations)
{
pixel_operations_.add_new(&node, operations);
}
Result &CompileState::get_result_from_output_socket(const bNodeSocket &output)
{
/* The output belongs to a node that was compiled into a standard node operation, so return a
* reference to the result from that operation using the output identifier. */
if (node_operations_.contains(&output.owner_node())) {
NodeOperation *operation = node_operations_.lookup(&output.owner_node());
return operation->get_result(output.identifier);
}
/* Otherwise, the output belongs to a node that was compiled into a pixel operation, so retrieve
* the internal identifier of that output and return a reference to the result from that
* operation using the retrieved identifier. */
PixelOperation *operation = pixel_operations_.lookup(&output.owner_node());
return operation->get_result(operation->get_output_identifier_from_output_socket(output));
}
void CompileState::add_node_to_pixel_compile_unit(const bNode &node)
{
pixel_compile_unit_.add_new(&node);
/* If this is the first node in the compile unit, then we should initialize the single value
* type, as well as the domain in case the node was not single value. */
const bool is_first_node_in_operation = pixel_compile_unit_.size() == 1;
if (is_first_node_in_operation) {
is_pixel_compile_unit_single_value_ = this->is_pixel_node_single_value(node);
/* If the node was not a single value, compute and initialize the domain. */
if (!is_pixel_compile_unit_single_value_) {
pixel_compile_unit_domain_ = this->compute_pixel_node_domain(node);
}
}
}
PixelCompileUnit &CompileState::get_pixel_compile_unit()
{
return pixel_compile_unit_;
}
bool CompileState::is_pixel_compile_unit_single_value()
{
return is_pixel_compile_unit_single_value_;
}
void CompileState::reset_pixel_compile_unit()
{
pixel_compile_unit_.clear();
pixel_compile_unit_domain_.reset();
}
bool CompileState::should_compile_pixel_compile_unit(const bNode &node)
{
/* If the pixel compile unit is empty, then it can't be compiled yet. */
if (pixel_compile_unit_.is_empty()) {
return false;
}
/* If the node is not a pixel node, then it can't be added to the pixel compile unit and the
* pixel compile unit is considered complete and should be compiled. */
if (!is_pixel_node(node)) {
return true;
}
/* If the compile unit is single value and the given node is not or vice versa, then it can't be
* added to the pixel compile unit and the pixel compile unit is considered complete and should
* be compiled. */
if (is_pixel_compile_unit_single_value_ != this->is_pixel_node_single_value(node)) {
return true;
}
/* For non single value compile units, if the computed domain of the node doesn't matches the
* domain of the pixel compile unit, then it can't be added to the pixel compile unit and the
* pixel compile unit is considered complete and should be compiled. */
if (!is_pixel_compile_unit_single_value_) {
if (pixel_compile_unit_domain_.value() != this->compute_pixel_node_domain(node)) {
return true;
}
}
/* Otherwise, the node is compatible and can be added to the compile unit and it shouldn't be
* compiled just yet. */
return false;
}
bool CompileState::is_pixel_node_single_value(const bNode &node)
{
/* If any of the outputs are single-only outputs, then the node is operating on single values. */
for (const bNodeSocket *output : node.output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
if (Result::is_single_value_only_type(get_node_socket_result_type(output))) {
return true;
}
}
/* If any of the inputs are single-only outputs, then the node is operating on single values. */
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
if (Result::is_single_value_only_type(get_node_socket_result_type(input))) {
return true;
}
}
/* The pixel node is single value when all of its inputs are single values. */
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
/* The input does not have an implicit input, so it is a single value. */
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
if (!input_descriptor.implicit_input.has_value()) {
continue;
}
const std::optional<Domain> domain = ImplicitInputOperation::get_domain(
context_, input_descriptor.implicit_input.value());
if (!domain.has_value()) {
/* The input has an implicit input, but it is a single value. */
continue;
}
/* Otherwise, it has an non-single-value implicit input. */
return false;
}
/* If the output belongs to a node that is part of the pixel compile unit and that compile unit
* is not single value, then the node is not single value. */
if (pixel_compile_unit_.contains(&output->owner_node())) {
if (is_pixel_compile_unit_single_value_) {
continue;
}
return false;
}
const Result &result = get_result_from_output_socket(*output);
if (!result.is_single_value()) {
return false;
}
}
return true;
}
Domain CompileState::compute_pixel_node_domain(const bNode &node)
{
/* Default to an identity domain in case no domain input was found, most likely because all
* inputs are single values. */
Domain node_domain = Domain::identity();
int current_domain_priority = std::numeric_limits<int>::max();
/* Go over the inputs and find the domain of the non single value input with the highest domain
* priority. */
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
/* The input does not have an implicit input, so it is a single that can't be a domain input
* and we skip it. */
if (!input_descriptor.implicit_input.has_value()) {
continue;
}
const std::optional<Domain> domain = ImplicitInputOperation::get_domain(
context_, input_descriptor.implicit_input.value());
if (!domain.has_value()) {
/* The input has an implicit input, but it is a single value that can't be a domain input
* and we skip it. */
continue;
}
/* Otherwise, the input has the domain of the implicit input, which is the domain of the
* compositing region. Notice that the lower the domain priority value is, the higher the
* priority is, hence the less than comparison. */
if (input_descriptor.domain_priority < current_domain_priority) {
node_domain = domain.value();
current_domain_priority = input_descriptor.domain_priority;
}
continue;
}
/* If the output belongs to a node that is part of the pixel compile unit, then the domain of
* the input is the domain of the compile unit itself. */
if (pixel_compile_unit_.contains(&output->owner_node())) {
/* Notice that the lower the domain priority value is, the higher the priority is, hence the
* less than comparison. */
if (input_descriptor.domain_priority < current_domain_priority) {
node_domain = pixel_compile_unit_domain_.value();
current_domain_priority = input_descriptor.domain_priority;
}
continue;
}
const Result &result = get_result_from_output_socket(*output);
/* A single value input can't be a domain input. */
if (result.is_single_value() || input_descriptor.expects_single_value) {
continue;
}
/* An input that skips operation domain realization can't be a domain input. */
if (input_descriptor.realization_mode != InputRealizationMode::OperationDomain) {
continue;
}
/* Notice that the lower the domain priority value is, the higher the priority is, hence the
* less than comparison. */
if (input_descriptor.domain_priority < current_domain_priority) {
node_domain = result.domain();
current_domain_priority = input_descriptor.domain_priority;
}
}
return node_domain;
}
bool CompileState::pixel_compile_unit_has_too_many_outputs(const bool are_node_previews_needed)
{
/* Only GPU and non-single units have output count limitations. */
if (!context_.use_gpu() || is_pixel_compile_unit_single_value_) {
return false;
}
int outputs_count = 0;
for (const bNode *node : pixel_compile_unit_) {
const bNodeSocket *preview_output = are_node_previews_needed ?
find_preview_output_socket(*node) :
nullptr;
for (const bNodeSocket *output : node->output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
/* If the output is used as the node preview, then an operation output will exist for it. */
const bool is_preview_output = output == preview_output;
/* If any of the nodes linked to the output are not part of the pixel compile unit but are
* part of the execution schedule, then an operation output will exist for it. */
const bool is_operation_output = is_output_linked_to_input_conditioned(
*output, [&](const bNodeSocket &input) {
return schedule_.nodes.contains(&input.owner_node()) &&
!schedule_.unneeded_inputs.contains(&input) &&
!pixel_compile_unit_.contains(&input.owner_node());
});
if (is_operation_output || is_preview_output) {
outputs_count += 1;
}
if (outputs_count > ShaderOperation::maximum_outputs_count) {
return true;
}
}
}
return false;
}
bool CompileState::pixel_compile_unit_has_too_many_inputs()
{
/* Only GPU and non-single units have input count limitations. */
if (!context_.use_gpu() || is_pixel_compile_unit_single_value_) {
return false;
}
Set<ImplicitInputType> referenced_implicit_inputs;
Set<const bNodeSocket *> referenced_output_sockets;
int inputs_count = 0;
for (const bNode *node : pixel_compile_unit_) {
for (const bNodeSocket *input : node->input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
if (!input_descriptor.implicit_input.has_value()) {
continue;
}
/* All implicit inputs of the same type share the same input, and this one was counted
* before, so no need to count it again. */
if (referenced_implicit_inputs.contains(input_descriptor.implicit_input.value())) {
continue;
}
inputs_count++;
if (inputs_count > ShaderOperation::maximum_inputs_count) {
return true;
}
referenced_implicit_inputs.add_new(input_descriptor.implicit_input.value());
continue;
}
/* This output is part of the pixel compile unit, so no input is declared for it. */
if (pixel_compile_unit_.contains(&output->owner_node())) {
continue;
}
/* All inputs linked to the same output share the same input, and this one was counted
* before, so no need to count it again. */
if (referenced_output_sockets.contains(output)) {
continue;
}
inputs_count++;
if (inputs_count > ShaderOperation::maximum_inputs_count) {
return true;
}
referenced_output_sockets.add_new(output);
}
}
return false;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,129 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_node_types.h"
#include "GPU_shader.hh"
#include "COM_context.hh"
#include "COM_render_context.hh"
#include "COM_static_cache_manager.hh"
namespace blender::compositor {
Context::Context(StaticCacheManager &cache_manager) : cache_manager_(cache_manager) {};
Result Context::get_pass(const Scene * /*scene*/, int /*view_layer*/, const char * /*name*/)
{
compositor::Result invalid_pass = this->create_result(compositor::ResultType::Color);
invalid_pass.allocate_invalid();
return invalid_pass;
}
const RenderData &Context::get_render_data() const
{
return this->get_scene().r;
}
StringRef Context::get_view_name() const
{
return "";
}
ResultPrecision Context::get_precision() const
{
return ResultPrecision::Full;
}
void Context::set_info_message(StringRef /*message*/) const {}
bool Context::treat_viewer_as_group_output() const
{
return false;
}
void Context::populate_meta_data_for_pass(const Scene * /*scene*/,
int /*view_layer_id*/,
const char * /*pass_name*/,
MetaData & /*meta_data*/) const
{
}
RenderContext *Context::render_context() const
{
return nullptr;
}
nodes::eval_log::NodesEvalLog *Context::nodes_evaluation_log() const
{
return nullptr;
}
void Context::evaluate_operation_post() const {}
bool Context::is_canceled() const
{
return false;
}
float Context::get_render_percentage() const
{
return get_render_data().size / 100.0f;
}
int Context::get_frame_number() const
{
return get_render_data().cfra;
}
float Context::get_time() const
{
const float frame_number = float(get_frame_number());
const float frame_rate = float(get_render_data().frs_sec) /
float(get_render_data().frs_sec_base);
return frame_number / frame_rate;
}
eCompositorDenoiseQaulity Context::get_denoise_quality() const
{
if (this->render_context()) {
return static_cast<eCompositorDenoiseQaulity>(
this->get_render_data().compositor_denoise_final_quality);
}
return static_cast<eCompositorDenoiseQaulity>(
this->get_render_data().compositor_denoise_preview_quality);
}
gpu::Shader *Context::get_shader(const char *info_name, ResultPrecision precision)
{
return cache_manager().cached_shaders.get(info_name, precision);
}
gpu::Shader *Context::get_shader(const char *info_name)
{
return get_shader(info_name, get_precision());
}
Result Context::create_result(ResultType type, ResultPrecision precision)
{
return Result(*this, type, precision);
}
Result Context::create_result(ResultType type)
{
return create_result(type, get_precision());
}
StaticCacheManager &Context::cache_manager()
{
return cache_manager_;
}
const Strip *Context::get_strip() const
{
return nullptr;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,112 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <fmt/format.h>
#include "BLI_generic_span.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_utildefines.h"
#include "GPU_shader.hh"
#include "IMB_colormanagement.hh"
#include "BKE_type_conversions.hh"
#include "COM_context.hh"
#include "COM_conversion_operation.hh"
#include "COM_input_descriptor.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
ConversionOperation::ConversionOperation(Context &context,
const ResultType input_type,
const ResultType expected_type)
: SimpleOperation(context)
{
this->declare_input_descriptor(InputDescriptor{input_type});
this->populate_result(expected_type);
}
void ConversionOperation::execute()
{
Result &result = this->get_result();
const Result &input = this->get_input();
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
if (!conversions.is_convertible(input.get_cpp_type(), result.get_cpp_type())) {
this->allocate_default_remaining_outputs();
return;
}
if (input.is_single_value()) {
result.allocate_single_value();
this->execute_single(input, result);
return;
}
result.allocate_texture(input.domain());
if (this->context().use_gpu()) {
const std::string shader_name = fmt::format("compositor_convert_{}_to_{}",
Result::type_name(this->get_input().type()),
Result::type_name(this->get_result().type()));
gpu::Shader *shader = this->context().get_shader(shader_name.c_str());
GPU_shader_bind(shader);
if (this->get_input().type() == ResultType::Color &&
ELEM(this->get_result().type(), ResultType::Float, ResultType::Int, ResultType::Bool))
{
float luminance_coefficients[3];
IMB_colormanagement_get_luminance_coefficients(luminance_coefficients);
GPU_shader_uniform_3fv(shader, "luminance_coefficients_u", luminance_coefficients);
}
input.bind_as_texture(shader, "input_tx");
result.bind_as_image(shader, "output_img");
compute_dispatch_threads_at_least(shader, input.domain().data_size);
input.unbind_as_texture();
result.unbind_as_image();
GPU_shader_unbind();
}
else {
this->execute_cpu(input, result);
}
}
SimpleOperation *ConversionOperation::construct_if_needed(Context &context,
const Result &input_result,
const InputDescriptor &input_descriptor)
{
if (input_descriptor.skip_type_conversion) {
return nullptr;
}
const ResultType result_type = input_result.type();
const ResultType expected_type = input_descriptor.type;
if (result_type != expected_type) {
return new ConversionOperation(context, result_type, expected_type);
}
return nullptr;
}
void ConversionOperation::execute_single(const Result &input, Result &output)
{
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
conversions.convert_to_initialized_n(
GSpan(input.single_value().type(), input.single_value().get(), 1),
GMutableSpan(output.single_value().type(), output.single_value().get(), 1));
output.update_single_value_data();
}
void ConversionOperation::execute_cpu(const Result &input, Result &output)
{
const bke::DataTypeConversions &conversions = bke::get_implicit_type_conversions();
conversions.convert_to_initialized_n(input.cpu_data(), output.cpu_data_for_write());
}
} // namespace blender::compositor

View File

@@ -0,0 +1,161 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_bounds.hh"
#include "BLI_bounds_types.hh"
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLT_translation.hh"
#include "GPU_texture.hh"
#include "COM_domain.hh"
namespace blender::compositor {
Domain::Domain(const int2 &size)
: data_size(size),
display_size(size),
data_offset(int2(0)),
transformation(float3x3::identity())
{
}
Domain::Domain(const int2 &size, const float3x3 &transformation)
: data_size(size), display_size(size), data_offset(int2(0)), transformation(transformation)
{
}
void Domain::transform(const float3x3 &input_transformation)
{
transformation = input_transformation * transformation;
}
Domain Domain::transposed() const
{
Domain domain = *this;
domain.data_size = int2(this->data_size.y, this->data_size.x);
domain.display_size = int2(this->display_size.y, this->display_size.x);
domain.data_offset = int2(this->data_offset.y, this->data_offset.x);
return domain;
}
Domain Domain::identity()
{
return Domain(int2(1), float3x3::identity());
}
bool Domain::is_equal(const Domain &a, const Domain &b, const float epsilon)
{
return a.data_size == b.data_size && a.display_size == b.display_size &&
a.data_offset == b.data_offset &&
math::is_equal(a.transformation, b.transformation, epsilon);
}
Domain Domain::realize_transformation(const bool realize_translation) const
{
/* If the domain is only infinitesimally rotated or scaled, only realize the translation if
* needed, otherwise, return as is. */
const float3x3 translation = math::from_location<float3x3>(this->transformation.location());
if (math::is_equal(float2x2(this->transformation), float2x2::identity(), 10e-6f)) {
Domain realized_domain = *this;
realized_domain.transformation = realize_translation ? float3x3::identity() : translation;
return realized_domain;
}
/* Eliminate the translation component of the transformation. Translation is ignored since it has
* no effect on the size of the domain and will be restored later if needed. */
const float3x3 transformation = float3x3(float2x2(this->transformation));
/* Translate the input such that it is centered in the virtual compositing space. */
const float2 center_translation = -float2(this->display_size) / 2.0f;
const float3x3 centered_transformation = math::translate(transformation, center_translation);
/* Compute display window after transformation. */
const Bounds<float2> display_window = {float2(0.0f), float2(this->display_size)};
const Bounds<float2> new_display_window = bounds::transform_bounds(centered_transformation,
display_window);
const Bounds<int2> new_integer_display_window = {int2(math::floor(new_display_window.min)),
int2(math::ceil(new_display_window.max))};
/* Compute data window after transformation. */
const Bounds<float2> data_window = {float2(this->data_offset),
float2(this->data_offset + this->data_size)};
const Bounds<float2> new_data_window = bounds::transform_bounds(centered_transformation,
data_window);
const Bounds<int2> new_integer_data_window = {int2(math::floor(new_data_window.min)),
int2(math::ceil(new_data_window.max))};
Domain realized_domain = *this;
realized_domain.display_size = math::max(int2(1), new_integer_display_window.size());
realized_domain.data_size = math::max(int2(1), new_integer_data_window.size());
realized_domain.data_offset = new_integer_data_window.min - new_integer_display_window.min;
realized_domain.transformation = realize_translation ? float3x3::identity() : translation;
return realized_domain;
}
bool operator==(const Domain &a, const Domain &b)
{
return a.data_size == b.data_size && a.display_size == b.display_size &&
a.data_offset == b.data_offset && a.transformation == b.transformation;
}
bool operator!=(const Domain &a, const Domain &b)
{
return !(a == b);
}
StringRefNull to_string(const Interpolation &interpolation)
{
switch (interpolation) {
case Interpolation::Nearest:
return N_("Nearest");
case Interpolation::Bilinear:
return N_("Bilinear");
case Interpolation::Bicubic:
return N_("Bicubic");
case Interpolation::Anisotropic:
return N_("Anisotropic");
}
BLI_assert_unreachable();
return "None";
}
StringRefNull to_string(const Extension &extension)
{
switch (extension) {
case Extension::Extend:
return N_("Extend");
case Extension::Repeat:
return N_("Repeat");
case Extension::Clip:
return N_("Clip");
}
BLI_assert_unreachable();
return "None";
}
GPUSamplerExtendMode map_extension_mode_to_extend_mode(const Extension &mode)
{
switch (mode) {
case compositor::Extension::Clip:
return GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER;
case compositor::Extension::Extend:
return GPU_SAMPLER_EXTEND_MODE_EXTEND;
case compositor::Extension::Repeat:
return GPU_SAMPLER_EXTEND_MODE_REPEAT;
}
BLI_assert_unreachable();
return GPU_SAMPLER_EXTEND_MODE_CLAMP_TO_BORDER;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,58 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "COM_context.hh"
#include "COM_group_input_node_operation.hh"
#include "COM_node_group_operation.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* A node operation representing a group input node that for each of its outputs gets the input
* from the node group operation it represents and shares its data with its own output with the
* same identifier. */
class GroupInputNodeOperation : public NodeOperation {
private:
/* The node group operation that this group input node belongs to. */
NodeGroupOperation &node_group_operation_;
public:
GroupInputNodeOperation(Context &context,
const bNode &node,
NodeGroupOperation &node_group_operation)
: NodeOperation(context, node), node_group_operation_(node_group_operation)
{
}
void execute() override
{
for (const bNodeSocket *output_socket : this->node().output_sockets()) {
if (!is_socket_available(output_socket)) {
continue;
}
Result &output_result = this->get_result(output_socket->identifier);
if (output_result.should_compute()) {
const Result &node_group_operation_input = node_group_operation_.get_input(
output_socket->identifier);
output_result.share_data(node_group_operation_input);
}
}
}
};
NodeOperation *get_group_input_node_operation(Context &context,
const bNode &node,
NodeGroupOperation &node_group_operation)
{
return new GroupInputNodeOperation(context, node, node_group_operation);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,142 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <memory>
#include "BLI_assert.h"
#include "BLI_vector.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "COM_group_node_operation.hh"
#include "COM_node_group_operation.hh"
#include "COM_node_operation.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* A node operation representing a group node. This is a thin wrapper around a NodeGroupOperation
* mapping its own inputs to the inputs of the node group operation and sharing its results with
* the results of the node group operation. */
class GroupNodeOperation : public NodeOperation {
private:
/* The node group outputs needed by the caller. */
const NodeGroupOutputTypes needed_outputs_;
/* The node instance key of the active group node. */
const bNodeInstanceKey active_node_group_instance_key_ = bke::NODE_INSTANCE_KEY_BASE;
public:
GroupNodeOperation(Context &context,
const bNode &node,
const NodeGroupOutputTypes needed_outputs,
const bNodeInstanceKey active_node_group_instance_key)
: NodeOperation(context, node),
needed_outputs_(needed_outputs),
active_node_group_instance_key_(active_node_group_instance_key)
{
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
InputDescriptor &descriptor = this->get_input_descriptor(input->identifier);
/* The structure type of the inputs of Group nodes are inferred, so we need to make sure this
* is not wrongly expecting single values. */
descriptor.expects_single_value = false;
/* Groups nodes should not force realization since it is defined by the user, and there is
* currently no way for the user to define that through the UI. */
descriptor.realization_mode = InputRealizationMode::None;
}
}
void execute() override
{
const bNodeTree *node_group = this->get_node_group();
if (!node_group) {
this->allocate_default_remaining_outputs();
return;
}
const bke::GroupNodeComputeContext compute_context(
&this->get_compute_context(), this->node().identifier, &this->node().owner_tree());
NodeGroupOperation operation(this->context(),
*node_group,
needed_outputs_,
active_node_group_instance_key_,
this->get_instance_key(),
compute_context);
this->set_reference_counts(operation);
Vector<std::unique_ptr<Result>> temporary_inputs = this->map_inputs(operation);
operation.evaluate();
this->write_outputs(operation);
}
/* Sets the reference counts of the node group operation according to the needed status of the
* outputs of the group node. */
void set_reference_counts(Operation &operation)
{
const bNodeTree *node_group = this->get_node_group();
node_group->ensure_interface_cache();
for (const bNodeTreeInterfaceSocket *output_socket : node_group->interface_outputs()) {
Result &node_group_result = operation.get_result(output_socket->identifier);
Result &group_node_result = this->get_result(output_socket->identifier);
node_group_result.set_reference_count(group_node_result.should_compute() ? 1 : 0);
}
}
/* Maps the input results of the node group operation to this group node's inputs through
* temporary results that share the data of the this group's inputs. */
Vector<std::unique_ptr<Result>> map_inputs(Operation &operation)
{
const bNodeTree *node_group = this->get_node_group();
Vector<std::unique_ptr<Result>> temporary_inputs;
node_group->ensure_interface_cache();
for (const bNodeTreeInterfaceSocket *input_socket : node_group->interface_inputs()) {
const Result &input_result = this->get_input(input_socket->identifier);
std::unique_ptr<Result> temporary_input = std::make_unique<Result>(
this->context().create_result(input_result.type(), input_result.precision()));
temporary_input->share_data(input_result);
temporary_inputs.append(std::move(temporary_input));
operation.map_input_to_result(input_socket->identifier, temporary_inputs.last().get());
}
return temporary_inputs;
}
/* Writes the output results of the node group operation to this group node operation by sharing
* its data and freeing the results. */
void write_outputs(Operation &operation)
{
const bNodeTree *node_group = this->get_node_group();
node_group->ensure_interface_cache();
for (const bNodeTreeInterfaceSocket *output_socket : node_group->interface_outputs()) {
Result &node_group_result = operation.get_result(output_socket->identifier);
Result &group_node_result = this->get_result(output_socket->identifier);
if (group_node_result.should_compute()) {
group_node_result.share_data(node_group_result);
node_group_result.release();
}
}
}
const bNodeTree *get_node_group()
{
BLI_assert(this->node().is_group());
return reinterpret_cast<const bNodeTree *>(this->node().id);
}
};
NodeOperation *get_group_node_operation(Context &context,
const bNode &node,
const NodeGroupOutputTypes &needed_outputs,
const bNodeInstanceKey active_node_group_instance_key)
{
return new GroupNodeOperation(context, node, needed_outputs, active_node_group_instance_key);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,70 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "COM_context.hh"
#include "COM_group_output_node_operation.hh"
#include "COM_node_group_operation.hh"
#include "COM_node_operation.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* A node operation representing a group output node that for each of its inputs gets the input
* and shares its data with the result of the node group operation it represents with the same
* identifier. */
class GroupOutputNodeOperation : public NodeOperation {
private:
NodeGroupOperation &node_group_operation_;
public:
GroupOutputNodeOperation(Context &context,
const bNode &node,
NodeGroupOperation &node_group_operation)
: NodeOperation(context, node), node_group_operation_(node_group_operation)
{
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
InputDescriptor &descriptor = this->get_input_descriptor(input->identifier);
/* The structure type of the inputs of Group Output nodes are inferred, so we need to
* make sure this is not wrongly expecting single values. */
descriptor.expects_single_value = false;
/* Groups Output nodes should not force realization since it is defined by the user, and
* there is currently no way for the user to define that through the UI. */
descriptor.realization_mode = InputRealizationMode::None;
}
}
void execute() override
{
for (const bNodeSocket *input_socket : this->node().input_sockets()) {
if (!is_socket_available(input_socket)) {
continue;
}
Result &node_group_operation_result = node_group_operation_.get_result(
input_socket->identifier);
if (node_group_operation_result.should_compute()) {
const Result &input_result = this->get_input(input_socket->identifier);
node_group_operation_result.share_data(input_result);
}
}
}
};
NodeOperation *get_group_output_node_operation(Context &context,
const bNode &node,
NodeGroupOperation &node_group_operation)
{
return new GroupOutputNodeOperation(context, node, node_group_operation);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,76 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <optional>
#include "BLI_assert.h"
#include "COM_domain.hh"
#include "COM_implicit_input_operation.hh"
#include "COM_input_descriptor.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
namespace blender::compositor {
const StringRef ImplicitInputOperation::output_identifier_ = StringRef("Output");
static ResultType get_implicit_input_result_type(const ImplicitInputType implicit_input)
{
switch (implicit_input) {
case ImplicitInputType::UniformImageCoordinates:
return ResultType::Float2;
case ImplicitInputType::SceneFrame:
return ResultType::Int;
}
BLI_assert_unreachable();
return ResultType::Float2;
}
ImplicitInputOperation::ImplicitInputOperation(Context &context,
const ImplicitInputType implicit_input)
: Operation(context), implicit_input_(implicit_input)
{
this->populate_result(output_identifier_, get_implicit_input_result_type(implicit_input));
}
void ImplicitInputOperation::execute()
{
Result &result = this->get_result();
switch (implicit_input_) {
case ImplicitInputType::UniformImageCoordinates: {
const int2 size = this->context().get_compositing_domain().data_size;
result.share_data(this->context().cache_manager().image_coordinates.get(
this->context(), size, CoordinatesType::Uniform));
break;
}
case ImplicitInputType::SceneFrame:
result.allocate_single_value();
result.set_single_value(this->context().get_frame_number());
break;
}
}
Result &ImplicitInputOperation::get_result()
{
return Operation::get_result(output_identifier_);
}
std::optional<Domain> ImplicitInputOperation::get_domain(const Context &context,
const ImplicitInputType implicit_input)
{
switch (implicit_input) {
case ImplicitInputType::UniformImageCoordinates:
return context.get_compositing_domain();
case ImplicitInputType::SceneFrame:
return std::nullopt;
}
BLI_assert_unreachable();
return std::nullopt;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,15 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_meta_data.hh"
namespace blender::compositor {
bool MetaData::is_cryptomatte_layer() const
{
return !this->cryptomatte.manifest.empty() || !this->cryptomatte.hash.empty() ||
!this->cryptomatte.conversion.empty();
}
} // namespace blender::compositor

View File

@@ -0,0 +1,534 @@
/* 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_euler.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.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 "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "BKE_type_conversions.hh"
#include "NOD_multi_function.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::compositor {
MultiFunctionProcedureOperation::MultiFunctionProcedureOperation(
Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule,
const bool is_single_value,
const ComputeContext &compute_context)
: PixelOperation(context, compile_unit, schedule, compute_context, is_single_value),
procedure_builder_(procedure_)
{
this->build_procedure();
procedure_executor_ = std::make_unique<mf::ProcedureExecutor>(procedure_);
}
void MultiFunctionProcedureOperation::execute()
{
const Domain domain = is_single_value_ ? Domain(int2(1)) : this->compute_domain();
const int64_t size = int64_t(domain.data_size.x) * domain.data_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) {
const Result &input = get_input(parameter_identifiers_[i]);
if (input.is_single_value()) {
parameter_builder.add_readonly_single_input(input.single_value());
}
else {
if (is_single_value_) {
/* The operation is operating on single values but an image is provided, so add a default
* single value as a fallback. */
parameter_builder.add_readonly_single_input(
GPointer(input.get_cpp_type(), input.get_cpp_type().default_value()));
}
else {
parameter_builder.add_readonly_single_input(input.cpu_data());
}
}
}
else {
Result &output = get_result(parameter_identifiers_[i]);
if (is_single_value_) {
output.allocate_single_value();
parameter_builder.add_uninitialized_single_output(
GMutableSpan(output.get_cpp_type(), output.single_value().get(), 1));
}
else {
output.allocate_texture(domain);
parameter_builder.add_uninitialized_single_output(output.cpu_data_for_write());
}
}
}
mf::ContextBuilder context_builder;
procedure_executor_->call_auto(mask, parameter_builder, context_builder);
/* In case of single value execution, update single value data. */
if (is_single_value_) {
for (int i = 0; i < procedure_.params().size(); i++) {
if (procedure_.params()[i].type == mf::ParamType::InterfaceType::Output) {
Result &output = get_result(parameter_identifiers_[i]);
output.update_single_value_data();
}
}
}
}
void MultiFunctionProcedureOperation::build_procedure()
{
for (const bNode *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, node->owner_tree());
});
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, multi_function);
/* 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_variables_.contains(item.value)) {
procedure_builder_.add_destruct(*item.value);
}
}
for (mf::Variable *variable : implicit_variables_) {
/* Variables that are used by the outputs should not be destructed. */
if (!output_variables_.contains(variable)) {
procedure_builder_.add_destruct(*variable);
}
}
for (mf::Variable *variable : implicit_input_to_variable_map_.values()) {
procedure_builder_.add_destruct(*variable);
}
mf::ReturnInstruction &return_instruction = procedure_builder_.add_return();
procedure_.prepare_for_execution();
mf::procedure_optimization::move_destructs_up(procedure_, return_instruction);
BLI_assert(procedure_.validate());
}
Vector<mf::Variable *> MultiFunctionProcedureOperation::get_input_variables(
const bNode &node, const mf::MultiFunction &multi_function)
{
int available_inputs_index = 0;
Vector<mf::Variable *> input_variables;
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const mf::ParamType parameter_type = multi_function.param_type(available_inputs_index);
available_inputs_index++;
if (schedule_.unneeded_inputs.contains(input)) {
input_variables.append(this->get_default_value_variable(parameter_type.data_type()));
continue;
}
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
if (!input_descriptor.implicit_input.has_value()) {
/* No implicit input, so get a constant variable that holds the socket value. */
input_variables.append(this->get_constant_input_variable(*input));
}
else {
input_variables.append(this->get_implicit_input_variable(*input));
}
}
else {
/* If the source node is part of the multi-function procedure operation, then the output has
* an existing variable for it. */
if (compile_unit_.contains(&output->owner_node())) {
input_variables.append(output_to_variable_map_.lookup(output));
}
else {
/* Otherwise, the source 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 to the expected parameter type if needed. */
input_variables.last() = this->convert_variable(input_variables.last(),
parameter_type.data_type());
}
return input_variables;
}
mf::Variable *MultiFunctionProcedureOperation::get_constant_input_variable(
const bNodeSocket &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_INT: {
const int value = input.default_value_typed<bNodeSocketValueInt>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<int32_t>>(value);
break;
}
case SOCK_BOOLEAN: {
const bool value = input.default_value_typed<bNodeSocketValueBoolean>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<bool>>(value);
break;
}
case SOCK_VECTOR: {
switch (input.default_value_typed<bNodeSocketValueVector>()->dimensions) {
case 2: {
const float2 value = float2(input.default_value_typed<bNodeSocketValueVector>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float2>>(value);
break;
}
case 3: {
const float3 value = float3(input.default_value_typed<bNodeSocketValueVector>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float3>>(value);
break;
}
case 4: {
const float4 value = float4(input.default_value_typed<bNodeSocketValueVector>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float4>>(value);
break;
}
default:
BLI_assert_unreachable();
break;
}
break;
}
case SOCK_INT_VECTOR: {
switch (input.default_value_typed<bNodeSocketValueIntVector>()->dimensions) {
case 2: {
const int2 value = int2(input.default_value_typed<bNodeSocketValueIntVector>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<int2>>(value);
break;
}
case 3: {
const int3 value = int3(input.default_value_typed<bNodeSocketValueIntVector>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<int3>>(value);
break;
}
default:
BLI_assert_unreachable();
break;
}
break;
}
case SOCK_RGBA: {
const Color value = Color(input.default_value_typed<bNodeSocketValueRGBA>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float4>>(value);
break;
}
case SOCK_MATRIX: {
const float4x4 value = float4x4::identity();
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float4x4>>(value);
break;
}
case SOCK_MENU: {
const int32_t value = input.default_value_typed<bNodeSocketValueMenu>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<nodes::MenuValue>>(
value);
break;
}
case SOCK_STRING: {
const std::string value = input.default_value_typed<bNodeSocketValueString>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<std::string>>(
value);
break;
}
case SOCK_ROTATION: {
const bNodeSocketValueRotation *rotation =
input.default_value_typed<bNodeSocketValueRotation>();
const math::EulerXYZ euler(float3(rotation->value_euler));
const math::Quaternion value = math::to_quaternion(euler);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<math::Quaternion>>(
value);
break;
}
case SOCK_OBJECT: {
Object *value = input.default_value_typed<bNodeSocketValueObject>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<Object *>>(value);
break;
}
case SOCK_IMAGE: {
Image *value = input.default_value_typed<bNodeSocketValueImage>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<Image *>>(value);
break;
}
case SOCK_FONT: {
VFont *value = input.default_value_typed<bNodeSocketValueFont>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<VFont *>>(value);
break;
}
case SOCK_SCENE: {
Scene *value = input.default_value_typed<bNodeSocketValueScene>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<Scene *>>(value);
break;
}
case SOCK_TEXT_ID: {
Text *value = input.default_value_typed<bNodeSocketValueText>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<Text *>>(value);
break;
}
case SOCK_MASK: {
Mask *value = input.default_value_typed<bNodeSocketValueMask>()->value;
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<Mask *>>(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_implicit_input_variable(
const bNodeSocket &input)
{
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(&input);
const ImplicitInputType implicit_input = input_descriptor.implicit_input.value();
/* An input was already declared for that implicit input, so no need to declare it again and we
* just return its variable. */
if (implicit_input_to_variable_map_.contains(implicit_input)) {
/* But first 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
* implicit input might be used in inputs inside the multi-function procedure operation which
* have different priorities. */
InputDescriptor &existing_input_descriptor = this->get_input_descriptor(
implicit_inputs_to_input_identifiers_map_.lookup(implicit_input));
existing_input_descriptor.domain_priority = math::min(
existing_input_descriptor.domain_priority, input_descriptor.domain_priority);
return implicit_input_to_variable_map_.lookup(implicit_input);
}
const int implicit_input_index = implicit_inputs_to_input_identifiers_map_.size();
const std::string input_identifier = "implicit_input" + std::to_string(implicit_input_index);
declare_input_descriptor(input_identifier, input_descriptor);
/* Map the implicit input to the identifier of the operation input that was declared for it. */
implicit_inputs_to_input_identifiers_map_.add_new(implicit_input, input_identifier);
mf::Variable &variable = procedure_builder_.add_input_parameter(
mf::DataType::ForSingle(Result::cpp_type(input_descriptor.type)), input_identifier);
parameter_identifiers_.append(input_identifier);
/* Map the implicit input to the variable that was created for it. */
implicit_input_to_variable_map_.add(implicit_input, &variable);
return &variable;
}
mf::Variable *MultiFunctionProcedureOperation::get_multi_function_input_variable(
const bNodeSocket &input_socket, const bNodeSocket &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. */
if (output_to_variable_map_.contains(&output_socket)) {
/* But first 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. */
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).domain_priority);
/* Increment the input's reference count. */
inputs_to_reference_counts_map_.lookup(input_identifier)++;
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);
input_descriptor.type = get_node_socket_result_type(&output_socket);
declare_input_descriptor(input_identifier, input_descriptor);
mf::Variable &variable = procedure_builder_.add_input_parameter(
mf::DataType::ForSingle(Result::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);
/* Map the identifier of the operation input to a reference count of 1, this will later be
* incremented if that same output was referenced again. */
inputs_to_reference_counts_map_.add_new(input_identifier, 1);
return &variable;
}
void MultiFunctionProcedureOperation::assign_output_variables(const bNode &node,
Vector<mf::Variable *> &variables)
{
const bool should_log_outputs = this->context().nodes_evaluation_log() && is_single_value_;
const bNodeSocket *preview_output = needs_node_previews_ && !is_single_value_ ?
find_preview_output_socket(node) :
nullptr;
int available_outputs_index = 0;
for (const bNodeSocket *output : node.output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
mf::Variable *output_variable = variables[available_outputs_index];
output_to_variable_map_.add_new(output, output_variable);
/* 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_input_conditioned(
*output, [&](const bNodeSocket &input) {
return schedule_.nodes.contains(&input.owner_node()) &&
!schedule_.unneeded_inputs.contains(&input) &&
!compile_unit_.contains(&input.owner_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 (should_log_outputs) {
logged_outputs_.add(output);
}
if (is_operation_output || is_preview_output || should_log_outputs) {
this->populate_operation_result(*output, output_variable);
}
available_outputs_index++;
}
}
void MultiFunctionProcedureOperation::populate_operation_result(const bNodeSocket &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);
const ResultType result_type = get_node_socket_result_type(&output_socket);
populate_result(output_identifier, result_type);
/* Map the output socket to the identifier of the newly populated result. */
output_sockets_to_output_identifiers_map_.add_new(&output_socket, output_identifier);
/* Implicitly convert the variable type to the expected result type if needed. */
const mf::DataType expected_type = mf::DataType::ForSingle(Result::cpp_type(result_type));
mf::Variable *converted_variable = this->convert_variable(variable, expected_type);
procedure_builder_.add_output_parameter(*converted_variable);
output_variables_.add_new(converted_variable);
parameter_identifiers_.append(output_identifier);
}
mf::Variable *MultiFunctionProcedureOperation::convert_variable(mf::Variable *variable,
const mf::DataType expected_type)
{
/* Conversion not needed. */
const mf::DataType variable_type = variable->data_type();
if (variable_type == expected_type) {
return variable;
}
const bke::DataTypeConversions &conversion_table = bke::get_implicit_type_conversions();
const mf::MultiFunction *function = conversion_table.get_conversion_multi_function(
variable_type, expected_type);
/* Conversion is not possible, return a default variable instead. */
if (!function) {
return this->get_default_value_variable(expected_type);
}
mf::Variable *converted_variable = procedure_builder_.add_call<1>(*function, {variable})[0];
implicit_variables_.append(converted_variable);
return converted_variable;
}
mf::Variable *MultiFunctionProcedureOperation::get_default_value_variable(const mf::DataType type)
{
const mf::MultiFunction &constant_function =
procedure_.construct_function<mf::CustomMF_GenericConstant>(
type.single_type(), type.single_type().default_value(), false);
mf::Variable *constant_variable = procedure_builder_.add_call<1>(constant_function)[0];
implicit_variables_.append(constant_variable);
return constant_variable;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,317 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_compute_context.hh"
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector_set.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "NOD_eval_log.hh"
#include "COM_compile_state.hh"
#include "COM_context.hh"
#include "COM_group_input_node_operation.hh"
#include "COM_group_node_operation.hh"
#include "COM_group_output_node_operation.hh"
#include "COM_implicit_input_operation.hh"
#include "COM_input_descriptor.hh"
#include "COM_multi_function_procedure_operation.hh"
#include "COM_node_group_operation.hh"
#include "COM_node_operation.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
#include "COM_scheduler.hh"
#include "COM_shader_operation.hh"
#include "COM_single_value_node_input_operation.hh"
#include "COM_undefined_node_operation.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
NodeGroupOperation::NodeGroupOperation(Context &context,
const bNodeTree &node_group,
const NodeGroupOutputTypes needed_outputs,
const bNodeInstanceKey active_node_group_instance_key,
const bNodeInstanceKey instance_key,
const ComputeContext &compute_context)
: Operation(context),
node_group_(node_group),
needed_output_types_(needed_outputs),
active_node_group_instance_key_(active_node_group_instance_key),
instance_key_(instance_key),
compute_context_(compute_context)
{
node_group.ensure_interface_cache();
for (const bNodeTreeInterfaceSocket *input : node_group.interface_inputs()) {
const InputDescriptor input_descriptor = input_descriptor_from_interface_input(node_group,
*input);
this->declare_input_descriptor(input->identifier, input_descriptor);
}
for (const bNodeTreeInterfaceSocket *output : node_group.interface_outputs()) {
this->populate_result(output->identifier, get_node_interface_socket_result_type(*output));
}
}
class ScopedNodeGroupTimer {
private:
const ComputeContext &compute_context_;
nodes::eval_log::NodesEvalLog *log_;
nodes::eval_log::TimePoint start_;
public:
ScopedNodeGroupTimer(const ComputeContext &compute_context, nodes::eval_log::NodesEvalLog *log)
: compute_context_(compute_context), log_(log)
{
start_ = nodes::eval_log::Clock::now();
}
~ScopedNodeGroupTimer()
{
if (!log_) {
return;
}
const nodes::eval_log::TimePoint end = nodes::eval_log::Clock::now();
nodes::eval_log::NodeTreeLogger &tree_logger = log_->get_local_tree_logger(compute_context_);
tree_logger.execution_time = end - start_;
}
};
void NodeGroupOperation::execute()
{
const ScopedNodeGroupTimer node_group_timer{compute_context_,
this->context().nodes_evaluation_log()};
const Schedule schedule = compute_schedule(this->context(),
node_group_,
*this,
needed_output_types_,
instance_key_,
active_node_group_instance_key_);
CompileState compile_state(this->context(), schedule);
for (const bNode *node : schedule.nodes) {
if (this->context().is_canceled()) {
this->cancel_evaluation();
break;
}
if (compile_state.should_compile_pixel_compile_unit(*node)) {
this->evaluate_pixel_compile_unit(compile_state);
}
if (is_pixel_node(*node)) {
compile_state.add_node_to_pixel_compile_unit(*node);
}
else {
this->evaluate_node(*node, compile_state);
}
}
/* Some of the needed outputs might not be allocated even after execution. This could happen for
* instance when no Group Output node exist or when the evaluation gets canceled before the
* output is written. */
this->allocate_default_remaining_outputs();
}
void NodeGroupOperation::evaluate_node(const bNode &node, CompileState &compile_state)
{
NodeOperation *operation = this->get_node_operation(node);
operation->set_instance_key(bke::node_instance_key(instance_key_, &node_group_, &node));
operation->set_compute_context(compute_context_);
/* Only compute previews if the node group is currently being viewed. */
operation->set_needs_node_previews(
bool(needed_output_types_ & NodeGroupOutputTypes::NodePreviews) &&
instance_key_ == active_node_group_instance_key_);
compile_state.map_node_to_node_operation(node, operation);
map_node_operation_inputs_to_their_results(node, operation, compile_state);
/* This has to be done after input mapping because the method may add Input Single Value
* Operations to the operations stream, which needs to be evaluated before the operation itself
* is evaluated. */
operations_stream_.append(std::unique_ptr<Operation>(operation));
operation->compute_results_reference_counts(compile_state.get_schedule());
operation->evaluate();
}
NodeOperation *NodeGroupOperation::get_node_operation(const bNode &node)
{
const char *disabled_hint = nullptr;
if (!node.typeinfo->poll(node.typeinfo, &node.owner_tree(), &disabled_hint)) {
return get_undefined_node_operation(this->context(), node);
}
if (node.is_group()) {
return get_group_node_operation(
this->context(), node, needed_output_types_, active_node_group_instance_key_);
}
if (node.is_group_output()) {
return get_group_output_node_operation(this->context(), node, *this);
}
if (node.is_group_input()) {
return get_group_input_node_operation(this->context(), node, *this);
}
return node.typeinfo->get_compositor_operation(this->context(), node);
}
void NodeGroupOperation::map_node_operation_inputs_to_their_results(const bNode &node,
NodeOperation *operation,
CompileState &compile_state)
{
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const bNodeSocket *output = get_output_linked_to_input(*input);
if (output && compile_state.get_schedule().nodes.contains(&output->owner_node()) &&
!compile_state.get_schedule().unneeded_inputs.contains(input))
{
/* The input is linked to a node that is part of the schedule. So map the input to the result
* we get from the output. */
Result &result = compile_state.get_result_from_output_socket(*output);
operation->map_input_to_result(input->identifier, &result);
continue;
}
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
if (!input_descriptor.implicit_input.has_value()) {
/* The input is unlinked with no implicit value. So map the input to the result of a newly
* created Input Single Value Operation. */
SingleValueNodeInputOperation *input_operation = new SingleValueNodeInputOperation(
this->context(), *input);
operations_stream_.append(std::unique_ptr<SingleValueNodeInputOperation>(input_operation));
input_operation->evaluate();
operation->map_input_to_result(input->identifier, &input_operation->get_result());
continue;
}
ImplicitInputOperation *input_operation = new ImplicitInputOperation(
this->context(), input_descriptor.implicit_input.value());
operations_stream_.append(std::unique_ptr<ImplicitInputOperation>(input_operation));
input_operation->evaluate();
operation->map_input_to_result(input->identifier, &input_operation->get_result());
}
}
/* Create one of the concrete subclasses of the PixelOperation based on the context and compile
* state. Deleting the operation is the caller's responsibility. */
static PixelOperation *create_pixel_operation(Context &context,
CompileState &compile_state,
const ComputeContext &compute_context)
{
const Schedule &schedule = compile_state.get_schedule();
PixelCompileUnit &compile_unit = compile_state.get_pixel_compile_unit();
/* Use multi-function procedure to execute the pixel compile unit for CPU contexts or if the
* compile unit is single value and would thus be more efficient to execute on the CPU. */
const bool is_single_value = compile_state.is_pixel_compile_unit_single_value();
if (!context.use_gpu() || is_single_value) {
return new MultiFunctionProcedureOperation(
context, compile_unit, schedule, is_single_value, compute_context);
}
return new ShaderOperation(context, compile_unit, schedule, compute_context);
}
void NodeGroupOperation::evaluate_pixel_compile_unit(CompileState &compile_state)
{
PixelCompileUnit &compile_unit = compile_state.get_pixel_compile_unit();
/* Pixel operations might have limitations on the number of outputs or inputs 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. */
const bool are_node_previews_needed = instance_key_ == active_node_group_instance_key_;
if (compile_state.pixel_compile_unit_has_too_many_outputs(are_node_previews_needed) ||
compile_state.pixel_compile_unit_has_too_many_inputs())
{
const int split_index = compile_unit.size() / 2;
const PixelCompileUnit start_compile_unit(compile_unit.as_span().take_front(split_index));
const PixelCompileUnit end_compile_unit(compile_unit.as_span().drop_front(split_index));
compile_state.get_pixel_compile_unit() = start_compile_unit;
this->evaluate_pixel_compile_unit(compile_state);
compile_state.get_pixel_compile_unit() = end_compile_unit;
this->evaluate_pixel_compile_unit(compile_state);
/* No need to continue, the above recursive calls will eventually exist the loop and do the
* actual compilation. */
return;
}
PixelOperation *operation = create_pixel_operation(
this->context(), compile_state, compute_context_);
/* Only compute previews if the node group is currently being viewed. */
operation->set_needs_node_previews(
bool(needed_output_types_ & NodeGroupOutputTypes::NodePreviews) &&
instance_key_ == active_node_group_instance_key_);
for (const bNode *node : compile_unit) {
compile_state.map_node_to_pixel_operation(*node, operation);
}
map_pixel_operation_inputs_to_their_results(operation, compile_state);
operations_stream_.append(std::unique_ptr<Operation>(operation));
operation->compute_results_reference_counts(compile_state.get_schedule());
operation->evaluate();
compile_state.reset_pixel_compile_unit();
}
void NodeGroupOperation::map_pixel_operation_inputs_to_their_results(PixelOperation *operation,
CompileState &compile_state)
{
for (const auto item : operation->get_inputs_to_linked_outputs_map().items()) {
const bNodeSocket &output = *item.value;
const StringRef input_identifier = item.key;
Result *input_result = &compile_state.get_result_from_output_socket(output);
operation->map_input_to_result(input_identifier, input_result);
/* Correct the reference count of the result in case multiple of the result's outgoing links
* corresponds to a single input in the pixel operation. See the description of the member
* inputs_to_reference_counts_map_ variable for more information. */
const int internal_reference_count = operation->get_internal_input_reference_count(
input_identifier);
input_result->decrement_reference_count(internal_reference_count - 1);
}
for (const auto item : operation->get_implicit_inputs_to_input_identifiers_map().items()) {
ImplicitInputOperation *input_operation = new ImplicitInputOperation(this->context(),
item.key);
operation->map_input_to_result(item.value, &input_operation->get_result());
operations_stream_.append(std::unique_ptr<ImplicitInputOperation>(input_operation));
input_operation->evaluate();
}
}
void NodeGroupOperation::cancel_evaluation()
{
for (const std::unique_ptr<Operation> &operation : operations_stream_) {
operation->free_results();
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,251 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_assert.h"
#include "BLI_string_ref.hh"
#include "BLI_vector_set.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "GPU_debug.hh"
#include "NOD_eval_log.hh"
#include "COM_algorithm_compute_preview.hh"
#include "COM_context.hh"
#include "COM_input_descriptor.hh"
#include "COM_node_operation.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
#include "COM_scheduler.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
NodeOperation::NodeOperation(Context &context, const bNode &node) : Operation(context), node_(node)
{
for (const bNodeSocket *output : this->node().output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
populate_result(output->identifier, get_node_socket_result_type(output));
}
for (const bNodeSocket *input : this->node().input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input);
declare_input_descriptor(input->identifier, input_descriptor);
}
}
class ScopedNodeTimer {
private:
const bNode &node_;
const ComputeContext &compute_context_;
nodes::eval_log::NodesEvalLog *log_;
nodes::eval_log::TimePoint start_;
public:
ScopedNodeTimer(const bNode &node,
const ComputeContext &compute_context,
nodes::eval_log::NodesEvalLog *log)
: node_(node), compute_context_(compute_context), log_(log)
{
start_ = nodes::eval_log::Clock::now();
}
~ScopedNodeTimer()
{
if (!log_) {
return;
}
const nodes::eval_log::TimePoint end = nodes::eval_log::Clock::now();
nodes::eval_log::NodeTreeLogger &tree_logger = log_->get_local_tree_logger(compute_context_);
tree_logger.node_execution_times.append(*tree_logger.allocator,
{node_.identifier, start_, end});
}
};
void NodeOperation::evaluate()
{
const ScopedNodeTimer node_timer{
this->node(), this->get_compute_context(), this->context().nodes_evaluation_log()};
if (this->context().use_gpu()) {
GPU_debug_group_begin(this->node().typeinfo->idname.c_str());
}
Operation::evaluate();
if (this->context().use_gpu()) {
GPU_debug_group_end();
}
}
void NodeOperation::compute_results_reference_counts(const Schedule &schedule)
{
for (const bNodeSocket *output : this->node().output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
const int reference_count = number_of_inputs_linked_to_output_conditioned(
*output, [&](const bNodeSocket &input) {
return schedule.nodes.contains(&input.owner_node()) &&
!schedule.unneeded_inputs.contains(&input);
});
this->get_result(output->identifier).set_reference_count(reference_count);
}
}
void NodeOperation::set_instance_key(const bNodeInstanceKey &instance_key)
{
instance_key_ = instance_key;
}
const bNodeInstanceKey &NodeOperation::get_instance_key() const
{
return instance_key_;
}
void NodeOperation::set_compute_context(const ComputeContext &compute_context)
{
compute_context_ = &compute_context;
}
const ComputeContext &NodeOperation::get_compute_context() const
{
return *compute_context_;
}
void NodeOperation::set_needs_node_previews(const bool needed)
{
needs_node_previews_ = needed;
}
static destruct_ptr<nodes::eval_log::ImageInfoLog> get_image_info_log(LinearAllocator<> *allocator,
const Result &result)
{
const Domain &domain = result.domain();
return allocator->construct<nodes::eval_log::ImageInfoLog>(
domain.data_size,
domain.display_size,
domain.data_offset,
domain.transformation,
to_string(domain.realization_options.interpolation),
to_string(domain.realization_options.extension_x),
to_string(domain.realization_options.extension_y),
to_string(result.precision()));
}
void NodeOperation::log_data()
{
nodes::eval_log::NodesEvalLog *log = this->context().nodes_evaluation_log();
if (!log) {
return;
}
nodes::eval_log::NodeTreeLogger &tree_logger = log->get_local_tree_logger(*compute_context_);
/* Log input values. */
for (const bNodeSocket *input_socket : this->node().input_sockets()) {
if (!is_socket_available(input_socket)) {
continue;
}
const InputDescriptor &input_descriptor = this->get_input_descriptor(input_socket->identifier);
if (!input_socket->is_logically_linked() && !input_descriptor.implicit_input.has_value()) {
continue;
}
const Result &input = this->get_input(input_socket->identifier);
if (input.is_single_value()) {
tree_logger.log_value(this->node(), *input_socket, input.single_value());
continue;
}
tree_logger.input_socket_values.append(*tree_logger.allocator,
{node_.identifier,
input_socket->index(),
get_image_info_log(tree_logger.allocator, input)});
}
/* Log output values. */
for (const bNodeSocket *output_socket : this->node().output_sockets()) {
if (!is_socket_available(output_socket)) {
continue;
}
const Result &result = this->get_result(output_socket->identifier);
if (!result.is_allocated()) {
continue;
}
if (result.is_single_value()) {
tree_logger.log_value(this->node(), *output_socket, result.single_value());
continue;
}
tree_logger.output_socket_values.append(*tree_logger.allocator,
{node_.identifier,
output_socket->index(),
get_image_info_log(tree_logger.allocator, result)});
}
/* Log node preview. */
if (needs_node_previews_ && is_node_preview_needed(this->node())) {
const Result *result = this->get_preview_result();
if (result && !result->is_single_value()) {
ImBuf *preview = compositor::compute_preview(this->context(), *result);
tree_logger.node_image_previews.append(*tree_logger.allocator, {node_.identifier, preview});
}
}
}
const bNode &NodeOperation::node() const
{
return node_;
}
Result *NodeOperation::get_preview_result()
{
/* Find the first linked output. */
for (const bNodeSocket *output : this->node().output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
Result &output_result = this->get_result(output->identifier);
if (output_result.should_compute()) {
return &output_result;
}
}
/* No linked outputs, but no inputs either, so nothing to preview. */
if (this->node().input_sockets().is_empty()) {
return nullptr;
}
/* Find the first allocated input. */
for (const bNodeSocket *input : this->node().input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
Result &input_result = this->get_input(input->identifier);
if (input_result.is_allocated()) {
return &input_result;
}
}
BLI_assert_unreachable();
return nullptr;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,179 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include <memory>
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "COM_context.hh"
#include "COM_conversion_operation.hh"
#include "COM_domain.hh"
#include "COM_input_descriptor.hh"
#include "COM_operation.hh"
#include "COM_realize_on_domain_operation.hh"
#include "COM_result.hh"
#include "COM_simple_operation.hh"
namespace blender::compositor {
Operation::Operation(Context &context) : context_(context) {}
Operation::~Operation() = default;
void Operation::evaluate()
{
this->evaluate_input_processors();
this->execute();
this->log_data();
this->release_inputs();
this->context().evaluate_operation_post();
}
Result &Operation::get_input(StringRef identifier) const
{
return *results_mapped_to_inputs_.lookup(identifier);
}
Result &Operation::get_result(StringRef identifier)
{
return results_.lookup(identifier);
}
void Operation::map_input_to_result(StringRef identifier, Result *result)
{
results_mapped_to_inputs_.add_new(identifier, result);
}
void Operation::free_results()
{
for (Result &result : results_.values()) {
result.free();
}
}
Domain Operation::compute_domain()
{
/* Default to an identity domain in case no domain input was found, most likely because all
* inputs are single values. */
Domain operation_domain = Domain::identity();
int current_domain_priority = std::numeric_limits<int>::max();
/* Go over the inputs and find the domain of the non single value input with the highest domain
* priority. */
for (StringRef identifier : input_descriptors_.keys()) {
const Result &result = get_input(identifier);
const InputDescriptor &descriptor = get_input_descriptor(identifier);
/* A single value input can't be a domain input. */
if (result.is_single_value() || descriptor.expects_single_value) {
continue;
}
/* An input that skips operation domain realization can't be a domain input. */
if (descriptor.realization_mode != InputRealizationMode::OperationDomain) {
continue;
}
/* Notice that the lower the domain priority value is, the higher the priority is, hence the
* less than comparison. */
if (descriptor.domain_priority < current_domain_priority) {
operation_domain = result.domain();
current_domain_priority = descriptor.domain_priority;
}
}
return operation_domain;
}
void Operation::evaluate_input_processors()
{
/* Each input processor type is added to all inputs entirely before the next type. This is done
* because the construction of the input processors may depend on the result of previous input
* processors for all inputs. For instance, the realize on domain input processor considers the
* value of all inputs, so previous input processors for all inputs needs to be added and
* evaluated first. */
for (const StringRef &identifier : results_mapped_to_inputs_.keys()) {
SimpleOperation *conversion = ConversionOperation::construct_if_needed(
this->context(), this->get_input(identifier), this->get_input_descriptor(identifier));
this->add_and_evaluate_input_processor(identifier, conversion);
}
for (const StringRef &identifier : results_mapped_to_inputs_.keys()) {
SimpleOperation *realize_on_domain = RealizeOnDomainOperation::construct_if_needed(
this->context(),
this->get_input(identifier),
this->get_input_descriptor(identifier),
this->compute_domain());
this->add_and_evaluate_input_processor(identifier, realize_on_domain);
}
}
void Operation::log_data() {};
void Operation::populate_result(StringRef identifier, const ResultType type)
{
results_.add_new(identifier, this->context().create_result(type));
}
void Operation::declare_input_descriptor(StringRef identifier, InputDescriptor descriptor)
{
input_descriptors_.add_new(identifier, descriptor);
}
InputDescriptor &Operation::get_input_descriptor(StringRef identifier)
{
return input_descriptors_.lookup(identifier);
}
void Operation::allocate_default_remaining_outputs()
{
for (Result &result : results_.values()) {
if (result.should_compute() && !result.is_allocated()) {
result.allocate_invalid();
}
}
}
Context &Operation::context() const
{
return context_;
}
void Operation::add_and_evaluate_input_processor(StringRef identifier, SimpleOperation *processor)
{
/* Allow null inputs to facilitate construct_if_needed pattern of addition. For instance, see the
* implementation of the evaluate_input_processors method. */
if (!processor) {
return;
}
ProcessorsVector &processors = input_processors_.lookup_or_add_default(identifier);
/* Get the result that should serve as the input for the processor. This is either the result
* mapped to the input or the result of the last processor depending on whether this is the first
* processor or not. */
Result &result = processors.is_empty() ? this->get_input(identifier) :
processors.last()->get_result();
/* Map the input result of the processor and add it to the processors vector. */
processor->map_input_to_result(&result);
processors.append(std::unique_ptr<SimpleOperation>(processor));
/* Switch the result mapped to the input to be the output result of the processor. */
results_mapped_to_inputs_.lookup(identifier) = &processor->get_result();
processor->evaluate();
}
void Operation::release_inputs()
{
for (Result *result : results_mapped_to_inputs_.values()) {
result->release();
}
}
} // namespace blender::compositor

View File

@@ -0,0 +1,218 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include <string>
#include "BLI_map.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector_set.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "NOD_eval_log.hh"
#include "COM_algorithm_compute_preview.hh"
#include "COM_context.hh"
#include "COM_operation.hh"
#include "COM_pixel_operation.hh"
#include "COM_result.hh"
#include "COM_scheduler.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
PixelOperation::PixelOperation(Context &context,
PixelCompileUnit &compile_unit,
const Schedule &schedule,
const ComputeContext &compute_context,
const bool is_single_value)
: Operation(context),
compile_unit_(compile_unit),
schedule_(schedule),
compute_context_(compute_context),
is_single_value_(is_single_value)
{
}
static destruct_ptr<nodes::eval_log::ImageInfoLog> get_image_info_log(
LinearAllocator<> *allocator, const Domain &domain, const ResultPrecision &precision)
{
return allocator->construct<nodes::eval_log::ImageInfoLog>(
domain.data_size,
domain.display_size,
domain.data_offset,
domain.transformation,
to_string(domain.realization_options.interpolation),
to_string(domain.realization_options.extension_x),
to_string(domain.realization_options.extension_y),
to_string(precision));
}
void PixelOperation::log_data()
{
nodes::eval_log::NodesEvalLog *log = this->context().nodes_evaluation_log();
if (!log) {
return;
}
nodes::eval_log::NodeTreeLogger &tree_logger = log->get_local_tree_logger(compute_context_);
if (is_single_value_) {
for (const bNodeSocket *output_socket : logged_outputs_) {
Result &result = this->get_result(
this->get_output_identifier_from_output_socket(*output_socket));
tree_logger.log_value(output_socket->owner_node(), *output_socket, result.single_value());
/* Logged results gets as an extra reference in pixel operations as can be seen in the
* compute_results_reference_counts method, so release it after logging. */
result.release();
}
return;
}
const Domain domain = this->compute_domain();
/* All inputs and outputs of pixel operations operate in the same domain, so the operation domain
* should be logged for all. The exception is inputs that are single values, in which case, their
* value is simply logged. */
for (const bNode *node : compile_unit_) {
/* Log output values. */
for (const bNodeSocket *output_socket : node->output_sockets()) {
if (!is_socket_available(output_socket)) {
continue;
}
if (!output_socket->is_logically_linked()) {
continue;
}
tree_logger.output_socket_values.append(
*tree_logger.allocator,
{node->identifier,
output_socket->index(),
get_image_info_log(tree_logger.allocator, domain, this->context().get_precision())});
}
/* Log input values. */
for (const bNodeSocket *input_socket : node->input_sockets()) {
if (!is_socket_available(input_socket)) {
continue;
}
/* The input has an implicit value. Get the input that corresponds to it, if it is a single
* value, log that single value, if not, we log the operation domain. */
const InputDescriptor input_descriptor = input_descriptor_from_input_socket(input_socket);
if (!input_socket->is_logically_linked() && input_descriptor.implicit_input.has_value()) {
const std::string &input_identifier = implicit_inputs_to_input_identifiers_map_.lookup(
input_descriptor.implicit_input.value());
const Result &input = this->get_input(input_identifier);
if (input.is_single_value()) {
tree_logger.log_value(*node, *input_socket, input.single_value());
continue;
}
tree_logger.input_socket_values.append(
*tree_logger.allocator,
{node->identifier,
input_socket->index(),
get_image_info_log(tree_logger.allocator, domain, this->context().get_precision())});
continue;
}
if (!input_socket->is_logically_linked()) {
continue;
}
/* The input is linked to a node that is inside the pixel operation, so skip it since it will
* inherit its value from an output that was logged above. */
const bNodeSocket &linked_output = *input_socket->logically_linked_sockets()[0];
if (compile_unit_.contains(&linked_output.owner_node())) {
continue;
}
/* Otherwise, it is linked to a node that is outside of the compile unit. If it is a single
* value, log that single value, if not, we log the operation domain. */
const std::string &input_identifier = outputs_to_declared_inputs_map_.lookup(&linked_output);
const Result &input = this->get_input(input_identifier);
if (input.is_single_value()) {
tree_logger.log_value(*node, *input_socket, input.single_value());
continue;
}
tree_logger.input_socket_values.append(
*tree_logger.allocator,
{node->identifier,
input_socket->index(),
get_image_info_log(tree_logger.allocator, domain, this->context().get_precision())});
}
}
for (const bNodeSocket *output : preview_outputs_) {
Result &result = this->get_result(get_output_identifier_from_output_socket(*output));
ImBuf *preview = compositor::compute_preview(context(), result);
tree_logger.node_image_previews.append(*tree_logger.allocator,
{output->owner_node().identifier, preview});
/* Preview results gets as an extra reference in pixel operations as can be seen in the
* compute_results_reference_counts method, so release it after computing preview. */
result.release();
}
}
StringRef PixelOperation::get_output_identifier_from_output_socket(
const bNodeSocket &output_socket)
{
return output_sockets_to_output_identifiers_map_.lookup(&output_socket);
}
Map<std::string, const bNodeSocket *> &PixelOperation::get_inputs_to_linked_outputs_map()
{
return inputs_to_linked_outputs_map_;
}
Map<ImplicitInputType, std::string> &PixelOperation::get_implicit_inputs_to_input_identifiers_map()
{
return implicit_inputs_to_input_identifiers_map_;
}
int PixelOperation::get_internal_input_reference_count(const StringRef &identifier)
{
return inputs_to_reference_counts_map_.lookup(identifier);
}
void PixelOperation::compute_results_reference_counts(const Schedule &schedule)
{
for (const auto item : output_sockets_to_output_identifiers_map_.items()) {
int reference_count = number_of_inputs_linked_to_output_conditioned(
*item.key, [&](const bNodeSocket &input) {
/* We only consider inputs that are not part of the pixel operations, because inputs
* that are part of the pixel operations are internal and do not deal with the result
* directly. */
return schedule.nodes.contains(&input.owner_node()) &&
!schedule.unneeded_inputs.contains(&input) &&
!compile_unit_.contains(&input.owner_node());
});
if (preview_outputs_.contains(item.key)) {
reference_count++;
}
if (logged_outputs_.contains(item.key)) {
reference_count++;
}
get_result(item.value).set_reference_count(reference_count);
}
}
void PixelOperation::set_needs_node_previews(const bool needed)
{
needs_node_previews_ = needed;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,328 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <limits>
#include "BLI_math_matrix.hh"
#include "BLI_math_matrix_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_utildefines.h"
#include "GPU_shader.hh"
#include "GPU_texture.hh"
#include "COM_context.hh"
#include "COM_domain.hh"
#include "COM_input_descriptor.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
#include "COM_realize_on_domain_operation.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* Realize On Domain Operation
*/
RealizeOnDomainOperation::RealizeOnDomainOperation(Context &context,
Domain target_domain,
ResultType type)
: SimpleOperation(context), target_domain_(target_domain)
{
InputDescriptor input_descriptor;
input_descriptor.type = type;
this->declare_input_descriptor(input_descriptor);
this->populate_result(type);
}
void RealizeOnDomainOperation::execute()
{
const Domain input_domain = this->get_input().domain();
const Domain output_domain = target_domain_;
/* Create a transformation matrix that transforms the pixels in the data window from the data
* space to the virtual compositing space. This is done by first adding the data offset to go
* from the data space to the display space, then subtracting the center of the display window to
* go from the display space to the virtual compositing space. See the corrective translation
* function for more information on its function. */
const float2 input_center = float2(input_domain.display_size) / 2.0f;
const float2 input_translation = float2(input_domain.data_offset) - input_center +
this->compute_corrective_translation();
const float3x3 input_data_to_virtual = math::translate(input_domain.transformation,
input_translation);
/* Same as above but for the output domain. */
const float2 output_center = float2(output_domain.display_size) / 2.0f;
const float2 output_translation = float2(output_domain.data_offset) - output_center;
const float3x3 output_data_to_virtual = math::translate(output_domain.transformation,
output_translation);
/* Create a transformation matrix from the output data space to the input data space */
const float3x3 virtual_to_input_data = math::invert(input_data_to_virtual);
const float3x3 output_data_to_input_data = virtual_to_input_data * output_data_to_virtual;
/* Create a transformation matrix from the output integer texel to the input normalized sampler
* coordinates. This is done by adding 0.5 to evaluate the output at the center if pixels and
* dividing by the input size to get normalized coordinates. */
const float3x3 output_texel_to_output_data = math::from_location<float3x3>(float2(0.5f));
const float3x3 input_data_to_input_sampler = math::from_scale<float3x3, 2>(
1.0f / float2(input_domain.data_size));
const float3x3 output_texel_to_input_sampler = input_data_to_input_sampler *
output_data_to_input_data *
output_texel_to_output_data;
if (this->context().use_gpu()) {
this->realize_on_domain_gpu(output_texel_to_input_sampler);
}
else {
this->realize_on_domain_cpu(output_texel_to_input_sampler);
}
}
float2 RealizeOnDomainOperation::compute_corrective_translation()
{
if (this->get_input().get_realization_options().interpolation == Interpolation::Nearest) {
/* Bias translations in case of nearest interpolation to avoids the round-to-even behavior of
* some GPUs at pixel boundaries. */
return float2(std::numeric_limits<float>::epsilon() * 10e3f);
}
/* Assuming no transformations, if the input size is odd and output size is even or vice versa,
* the centers of pixels of the input and output will be half a pixel away from each other due
* to the centering translation. Which introduce fuzzy result due to interpolation. So if one
* is odd and the other is even, detected by testing the low bit of the xor of the sizes, shift
* the input by 1/2 pixel so the pixels align. */
const int2 output_size = this->compute_domain().data_size;
const int2 input_size = this->get_input().domain().data_size;
return float2(((input_size[0] ^ output_size[0]) & 1) ? -0.5f : 0.0f,
((input_size[1] ^ output_size[1]) & 1) ? -0.5f : 0.0f);
}
void RealizeOnDomainOperation::realize_on_domain_gpu(const float3x3 &transformation)
{
gpu::Shader *shader = this->context().get_shader(this->get_realization_shader_name());
GPU_shader_bind(shader);
GPU_shader_uniform_mat3_as_mat4(shader, "transformation", transformation.ptr());
Result &input = this->get_input();
const RealizationOptions realization_options = input.get_realization_options();
if (!GPU_texture_has_integer_format(input)) {
/* The texture sampler should use bilinear interpolation for both the bilinear and bicubic
* cases, as the logic used by the bicubic realization shader expects textures to use bilinear
* interpolation. */
if (realization_options.interpolation == Interpolation::Anisotropic) {
GPU_texture_anisotropic_filter(input, true);
GPU_texture_mipmap_mode(input, true, true);
}
else {
GPU_texture_filter_mode(input, realization_options.interpolation != Interpolation::Nearest);
}
}
GPU_texture_extend_mode_x(input,
map_extension_mode_to_extend_mode(realization_options.extension_x));
GPU_texture_extend_mode_y(input,
map_extension_mode_to_extend_mode(realization_options.extension_y));
input.bind_as_texture(shader, "input_tx");
const Domain domain = this->compute_domain();
Result &output = this->get_result();
output.allocate_texture(domain);
output.bind_as_image(shader, "domain_img");
compute_dispatch_threads_at_least(shader, output.domain().data_size);
input.unbind_as_texture();
output.unbind_as_image();
GPU_shader_unbind();
}
const char *RealizeOnDomainOperation::get_realization_shader_name()
{
const Interpolation interpolation = get_input().get_realization_options().interpolation;
if (interpolation == Interpolation::Bicubic) {
switch (this->get_input().type()) {
case ResultType::Float:
return "compositor_realize_on_domain_bicubic_float";
case ResultType::Float2:
return "compositor_realize_on_domain_bicubic_float2";
case ResultType::Float3:
/* Float3 is internally stored in a float4 texture due to GPU module limitations. */
return "compositor_realize_on_domain_bicubic_float4";
case ResultType::Float4:
return "compositor_realize_on_domain_bicubic_float4";
case ResultType::Color:
return "compositor_realize_on_domain_bicubic_float4";
case ResultType::Int:
return "compositor_realize_on_domain_int";
case ResultType::Int2:
return "compositor_realize_on_domain_int2";
case ResultType::Int3:
/* Int3 is internally stored in a int4 texture due to GPU module limitations. */
return "compositor_realize_on_domain_int4";
case ResultType::Int4:
return "compositor_realize_on_domain_int4";
case ResultType::Bool:
return "compositor_realize_on_domain_bool";
case ResultType::Float4x4:
return "compositor_realize_on_domain_float4x4";
case ResultType::Menu:
return "compositor_realize_on_domain_menu";
case ResultType::Quaternion:
return "compositor_realize_on_domain_bicubic_float4";
case ResultType::String:
case ResultType::Object:
case ResultType::Image:
case ResultType::Font:
case ResultType::Scene:
case ResultType::Text:
case ResultType::Mask:
/* Single only types do not support GPU code path. */
BLI_assert(Result::is_single_value_only_type(this->get_input().type()));
BLI_assert_unreachable();
break;
}
}
else {
switch (this->get_input().type()) {
case ResultType::Float:
return "compositor_realize_on_domain_float";
case ResultType::Float2:
return "compositor_realize_on_domain_float2";
case ResultType::Float3:
/* Float3 is internally stored in a float4 texture due to GPU module limitations. */
return "compositor_realize_on_domain_float4";
case ResultType::Float4:
case ResultType::Color:
return (interpolation == Interpolation::Anisotropic) ?
"compositor_realize_on_domain_anisotropic_float4" :
"compositor_realize_on_domain_float4";
case ResultType::Int:
return "compositor_realize_on_domain_int";
case ResultType::Int2:
return "compositor_realize_on_domain_int2";
case ResultType::Int3:
/* Int3 is internally stored in a int4 texture due to GPU module limitations. */
return "compositor_realize_on_domain_int4";
case ResultType::Int4:
return "compositor_realize_on_domain_int4";
case ResultType::Bool:
return "compositor_realize_on_domain_bool";
case ResultType::Float4x4:
return "compositor_realize_on_domain_float4x4";
case ResultType::Menu:
return "compositor_realize_on_domain_menu";
case ResultType::Quaternion:
return "compositor_realize_on_domain_float4";
case ResultType::String:
case ResultType::Object:
case ResultType::Image:
case ResultType::Font:
case ResultType::Scene:
case ResultType::Text:
case ResultType::Mask:
/* Single only types do not support GPU code path. */
BLI_assert(Result::is_single_value_only_type(this->get_input().type()));
BLI_assert_unreachable();
break;
}
}
BLI_assert_unreachable();
return nullptr;
}
template<typename T>
static void realize_on_domain(const Result &input, Result &output, const float3x3 &transformation)
{
const RealizationOptions realization_options = input.get_realization_options();
const float2x2 jacobian(transformation);
parallel_for(output.domain().data_size, [&](const int2 texel) {
const float2 coordinates = math::transform_point(transformation, float2(texel));
T sample = input.sample<T>(coordinates,
realization_options.interpolation,
realization_options.extension_x,
realization_options.extension_y,
jacobian);
output.store_pixel(texel, sample);
});
}
void RealizeOnDomainOperation::realize_on_domain_cpu(const float3x3 &transformation)
{
Result &input = this->get_input();
Result &output = this->get_result();
const Domain domain = this->compute_domain();
output.allocate_texture(domain);
input.get_cpp_type()
.to_static_type<float,
float2,
float3,
float4,
Color,
int32_t,
int2,
int3,
int4,
bool,
float4x4,
nodes::MenuValue,
math::Quaternion>(
[&]<typename T>() { realize_on_domain<T>(input, output, transformation); });
}
Domain RealizeOnDomainOperation::compute_domain()
{
return target_domain_;
}
SimpleOperation *RealizeOnDomainOperation::construct_if_needed(
Context &context,
const Result &input_result,
const InputDescriptor &input_descriptor,
const Domain &operation_domain)
{
/* This input doesn't need realization, the operation is not needed. */
if (input_descriptor.realization_mode == InputRealizationMode::None) {
return nullptr;
}
/* The input expects a single value and if no single value is provided, it will be ignored and a
* default value will be used, so no need to realize it and the operation is not needed. */
if (input_descriptor.expects_single_value) {
return nullptr;
}
/* Input result is a single value and does not need realization, the operation is not needed. */
if (input_result.is_single_value()) {
return nullptr;
}
/* If we are realizing on the operation domain, then our target domain is the operation domain,
* otherwise, we are only realizing the transforms, then our target domain is the input's one. */
const bool use_operation_domain = input_descriptor.realization_mode ==
InputRealizationMode::OperationDomain;
const Domain target_domain = use_operation_domain ? operation_domain : input_result.domain();
const bool should_realize_translation = input_descriptor.realization_mode ==
InputRealizationMode::Transforms;
const Domain realized_target_domain = target_domain.realize_transformation(
should_realize_translation);
/* The input have an almost identical domain to the realized target domain, so no need to realize
* it and the operation is not needed. */
if (Domain::is_equal(input_result.domain(), realized_target_domain)) {
return nullptr;
}
return new RealizeOnDomainOperation(context, realized_target_domain, input_descriptor.type);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,206 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <memory>
#include <string>
#include "BLI_assert.h"
#include "BLI_listbase.h"
#include "BLI_map.hh"
#include "BLI_math_vector.h"
#include "BLI_math_vector_types.hh"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLI_utildefines.h"
#include "MEM_guardedalloc.h"
#include "IMB_imbuf.hh"
#include "IMB_imbuf_types.hh"
#include "DNA_scene_types.h"
#include "DNA_windowmanager_types.h"
#include "BKE_image.hh"
#include "BKE_image_save.hh"
#include "BKE_report.hh"
#include "BKE_scene.hh"
#include "RE_pipeline.h"
#include "COM_render_context.hh"
#include "COM_result.hh"
namespace blender::compositor {
/* ------------------------------------------------------------------------------------------------
* File Output
*/
FileOutput::FileOutput(const std::string &path,
const ImageFormatData &format,
int2 size,
bool save_as_render)
: path_(path), format_(format), save_as_render_(save_as_render)
{
render_result_ = MEM_new<RenderResult>("Temporary Render Result For File Output");
render_result_->rectx = size.x;
render_result_->recty = size.y;
/* NOTE: set dummy values which will won't be used unless overwritten.
* When `save_as_render` is set, this is overwritten by the scenes PPM setting.
* We *could* support setting the DPI in the file output node too. */
render_result_->ppm[0] = 0.0;
render_result_->ppm[1] = 0.0;
/* File outputs are always single layer, as images are actually stored in passes on that single
* layer. Create a single unnamed layer to add the passes to. A single unnamed layer is treated
* by the EXR writer as a special case where the channel names take the form:
* <pass-name>.<view-name>.<channel-id>
* Otherwise, the layer name would have preceded in the pass name in yet another section. */
RenderLayer *render_layer = MEM_new<RenderLayer>("Render Layer For File Output.");
BLI_addtail(&render_result_->layers, render_layer);
render_layer->name[0] = '\0';
/* File outputs do not support previews. */
format_.flag &= ~R_IMF_FLAG_PREVIEW_JPG;
}
FileOutput::~FileOutput()
{
RE_FreeRenderResult(render_result_);
}
void FileOutput::add_view(const char *view_name)
{
/* Empty views can only be added for EXR images. */
BLI_assert(ELEM(format_.imtype, R_IMF_IMTYPE_OPENEXR, R_IMF_IMTYPE_MULTILAYER));
RenderView *render_view = MEM_new<RenderView>("Render View For File Output.");
BLI_addtail(&render_result_->views, render_view);
STRNCPY_UTF8(render_view->name, view_name);
}
void FileOutput::add_view(const char *view_name, const Result &data)
{
RenderView *render_view = MEM_new<RenderView>("Render View For File Output.");
BLI_addtail(&render_result_->views, render_view);
STRNCPY_UTF8(render_view->name, view_name);
ImColorMode color_mode = ImColorMode::RGBA;
if (data.channels_count() == 1) {
color_mode = ImColorMode::BW;
}
else if (data.channels_count() == 3) {
color_mode = ImColorMode::RGB;
}
render_view->ibuf = IMB_allocImBuf(UNPACK2(data.domain().data_size), ImBufFlags::Zero);
render_view->ibuf->color_mode = color_mode;
if (data.sharing_info()) {
render_view->ibuf->channels = data.channels_count();
render_view->ibuf->float_buffer = ImBufFloatBuffer{
.data = static_cast<const float *>(data.cpu_data().data()),
.sharing_info = data.sharing_info(),
.colorspace = nullptr};
}
else if (data.cpu_data().data() != render_view->ibuf->float_data()) {
IMB_alloc_float_pixels(render_view->ibuf, data.channels_count(), false);
std::memcpy(
render_view->ibuf->float_data_for_write(), data.cpu_data().data(), data.size_in_bytes());
}
}
void FileOutput::add_pass(const char *pass_name,
const char *view_name,
const char *channels,
const Result &data)
{
/* Passes can only be added for EXR images. */
BLI_assert(ELEM(format_.imtype, R_IMF_IMTYPE_OPENEXR, R_IMF_IMTYPE_MULTILAYER));
RenderLayer *render_layer = static_cast<RenderLayer *>(render_result_->layers.first);
RenderPass *render_pass = MEM_new<RenderPass>("Render Pass For File Output.");
BLI_addtail(&render_layer->passes, render_pass);
STRNCPY(render_pass->name, pass_name);
STRNCPY(render_pass->view, view_name);
STRNCPY(render_pass->chan_id, channels);
render_pass->rectx = data.domain().data_size.x;
render_pass->recty = data.domain().data_size.y;
render_pass->channels = data.channels_count();
ImColorMode color_mode = ImColorMode::RGBA;
if (render_pass->channels == 1) {
color_mode = ImColorMode::BW;
}
else if (render_pass->channels == 3) {
color_mode = ImColorMode::RGB;
}
render_pass->ibuf = IMB_allocImBuf(UNPACK2(data.domain().data_size), ImBufFlags::Zero);
render_pass->ibuf->color_mode = color_mode;
if (data.sharing_info()) {
render_pass->ibuf->channels = data.channels_count();
render_pass->ibuf->float_buffer = ImBufFloatBuffer{
.data = static_cast<const float *>(data.cpu_data().data()),
.sharing_info = data.sharing_info(),
.colorspace = nullptr};
}
else if (data.cpu_data().data() != render_pass->ibuf->float_data()) {
IMB_alloc_float_pixels(render_pass->ibuf, data.channels_count(), false);
std::memcpy(
render_pass->ibuf->float_data_for_write(), data.cpu_data().data(), data.size_in_bytes());
}
copy_v2_v2_db(render_pass->ibuf->ppm, render_result_->ppm);
}
void FileOutput::add_meta_data(std::string key, std::string value)
{
meta_data_.add(key, value);
}
void FileOutput::save(Scene *scene)
{
ReportList reports;
BKE_reports_init(&reports, RPT_STORE);
/* Add scene stamp data as meta data as well as the custom meta data. */
BKE_render_result_stamp_info(scene, nullptr, render_result_, false);
for (const auto &field : meta_data_.items()) {
BKE_render_result_stamp_data(render_result_, field.key.c_str(), field.value.c_str());
}
/* NOTE: without this the file will be written without any density information.
* So always write this. */
if (save_as_render_ || true) {
BKE_scene_ppm_get(&scene->r, render_result_->ppm);
}
BKE_image_render_write(
&reports, render_result_, scene, true, path_.c_str(), &format_, save_as_render_);
BKE_reports_free(&reports);
}
/* ------------------------------------------------------------------------------------------------
* Render Context
*/
FileOutput &RenderContext::get_file_output(std::string path,
ImageFormatData format,
int2 size,
bool save_as_render)
{
return *file_outputs_.lookup_or_add_cb(
path, [&]() { return std::make_unique<FileOutput>(path, format, size, save_as_render); });
}
void RenderContext::save_file_outputs(Scene *scene)
{
for (std::unique_ptr<FileOutput> &file_output : file_outputs_.values()) {
file_output->save(scene);
}
}
} // namespace blender::compositor

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,615 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <algorithm>
#include "BLI_index_range.hh"
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_stack.hh"
#include "BLI_string_ref.hh"
#include "BLI_vector.hh"
#include "BLI_vector_set.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "NOD_geo_index_switch.hh"
#include "NOD_geo_menu_switch.hh"
#include "COM_context.hh"
#include "COM_scheduler.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
/* Checks if the node group has a File Output node in it or in one of its descendants. */
static bool has_file_output_recursive(const bNodeTree &node_group)
{
node_group.ensure_topology_cache();
for (const bNode *node : node_group.nodes_by_type("CompositorNodeOutputFile"_ustr)) {
if (!node->is_muted()) {
return true;
}
}
for (const bNode *group_node : node_group.group_nodes()) {
if (!group_node->is_muted() && group_node->id) {
if (has_file_output_recursive(*reinterpret_cast<const bNodeTree *>(group_node->id))) {
return true;
}
}
}
return false;
}
/* Checks if the node group with the given instance key has a Viewer node in it or in one of its
* descendants. Only nodes of node groups whose instance key match that of the given active node
* group instance key are considered active. */
static bool has_viewer_recursive(const bNodeTree &node_group,
const bNodeInstanceKey instance_key,
const bNodeInstanceKey active_node_group_instance_key)
{
node_group.ensure_topology_cache();
/* If this is the active node group, check if a viewer node exists. */
if (active_node_group_instance_key == instance_key) {
for (const bNode *node : node_group.nodes_by_type("CompositorNodeViewer"_ustr)) {
if (node->flag & NODE_DO_OUTPUT && !node->is_muted()) {
return true;
}
}
}
/* Otherwise, we have to check node groups recursively. */
for (const bNode *group_node : node_group.group_nodes()) {
if (group_node->is_muted() || !group_node->id) {
continue;
}
const bNodeTree &child_node_group = *reinterpret_cast<const bNodeTree *>(group_node->id);
const bNodeInstanceKey child_instance_key = bke::node_instance_key(
instance_key, &node_group, group_node);
if (has_viewer_recursive(child_node_group, child_instance_key, active_node_group_instance_key))
{
return true;
}
}
return false;
}
/* Add the output nodes whose result should be computed to the given stack. This includes File
* Output, Group Output, and Viewer nodes. This might also include group nodes that contain File
* Output or Viewer nodes. */
static void add_output_nodes(const Context &context,
const bNodeTree &node_group,
NodeGroupOperation &node_group_operation,
NodeGroupOutputTypes needed_outputs_types,
const bNodeInstanceKey instance_key,
const bNodeInstanceKey active_node_group_instance_key,
Stack<const bNode *> &node_stack)
{
node_group.ensure_topology_cache();
bool viewer_exists = false;
/* Add group nodes that contain File Output and Viewer nodes. */
for (const bNode *group_node : node_group.group_nodes()) {
if (group_node->is_muted() || !group_node->id) {
continue;
}
const bNodeTree &child_tree = *reinterpret_cast<const bNodeTree *>(group_node->id);
const bNodeInstanceKey child_instance_key = bke::node_instance_key(
instance_key, &node_group, group_node);
if (flag_is_set(needed_outputs_types, NodeGroupOutputTypes::ViewerNode) &&
has_viewer_recursive(child_tree, child_instance_key, active_node_group_instance_key))
{
node_stack.push(group_node);
viewer_exists = true;
continue;
}
if (flag_is_set(needed_outputs_types, NodeGroupOutputTypes::FileOutputNode) &&
has_file_output_recursive(child_tree))
{
node_stack.push(group_node);
}
}
/* Add Warning nodes. */
for (const bNode *node : node_group.nodes_by_type("GeometryNodeWarning"_ustr)) {
if (!node->is_muted()) {
node_stack.push(node);
}
}
/* Add File Output nodes. */
if (flag_is_set(needed_outputs_types, NodeGroupOutputTypes::FileOutputNode)) {
for (const bNode *node : node_group.nodes_by_type("CompositorNodeOutputFile"_ustr)) {
if (!node->is_muted()) {
node_stack.push(node);
}
}
}
/* Add Viewer node. Only add the node if the node group is active or is a root node group and no
* viewer node exists in descendants node groups. */
const bool is_active_node_group = active_node_group_instance_key == instance_key;
const bool is_root_node_group = instance_key == bke::NODE_INSTANCE_KEY_BASE;
const bool should_add_viewer = is_active_node_group || (is_root_node_group && !viewer_exists);
if (flag_is_set(needed_outputs_types, NodeGroupOutputTypes::ViewerNode) && should_add_viewer) {
for (const bNode *node : node_group.nodes_by_type("CompositorNodeViewer"_ustr)) {
if (node->flag & NODE_DO_OUTPUT && !node->is_muted()) {
node_stack.push(node);
viewer_exists = true;
break;
}
}
}
bool is_any_group_output_needed = false;
for (const bNodeTreeInterfaceSocket *output : node_group.interface_outputs()) {
if (node_group_operation.get_result(output->identifier).should_compute()) {
is_any_group_output_needed = true;
break;
}
}
/* None of the node groups outputs are needed, so no need to add the Group Output node. */
if (!is_any_group_output_needed) {
return;
}
/* Add Group Output node. None root node groups should always had a group output node. If the
* context is treating viewer nodes as group outputs, then the group output should be ignored
* even if needed. */
const bool context_ignores_output = context.treat_viewer_as_group_output() && viewer_exists;
if (!is_root_node_group ||
(flag_is_set(needed_outputs_types, NodeGroupOutputTypes::GroupOutputNode) &&
!context_ignores_output))
{
const bNode *output_node = node_group.group_output_node();
if (output_node && !output_node->is_muted()) {
node_stack.push(output_node);
}
}
}
/* Returns the value of input of the node with the given identifier in the given node group
* operation. If the value can not be determined statically, a nullopt is returned. The value is
* only known statically if the input is not connected or directly connected to a group input node
* with the same socket type. */
template<typename T, typename SocketT>
static std::optional<T> get_input_socket_value(const bNode &node,
const UString &identifier,
NodeGroupOperation &node_group_operation)
{
const bNodeSocket &input = *node.input_by_identifier(identifier);
if (!input.is_logically_linked()) {
return T(input.default_value_typed<SocketT>()->value);
}
const bNodeSocket *linked_output = input.logically_linked_sockets()[0];
if (!linked_output->owner_node().is_group_input()) {
return std::nullopt;
}
if (linked_output->type != input.type) {
return std::nullopt;
}
return node_group_operation.get_input(linked_output->identifier).get_single_value_default<T>();
}
/* Returns true if the given input of the given Switch node in the given node group operation is
* needed by the node. */
static bool is_switch_node_input_needed(const bNode &node,
const bNodeSocket &input,
NodeGroupOperation &node_group_operation)
{
const UString condition_identifier = "Switch"_ustr;
if (input.identifier_ustr() == condition_identifier) {
return true;
}
const std::optional<bool> condition = get_input_socket_value<bool, bNodeSocketValueBoolean>(
node, condition_identifier, node_group_operation);
if (!condition.has_value()) {
return true;
}
return (input.identifier_ustr() == "True"_ustr) == condition.value();
}
/* returns true if the given input of the given menu switch node in the given node group operation
* is needed by the node. */
static bool is_menu_switch_node_input_needed(const bNode &node,
const bNodeSocket &input,
NodeGroupOperation &node_group_operation)
{
const UString menu_identifier = "Menu"_ustr;
if (input.identifier_ustr() == menu_identifier) {
return true;
}
const std::optional<nodes::MenuValue> menu =
get_input_socket_value<nodes::MenuValue, bNodeSocketValueMenu>(
node, menu_identifier, node_group_operation);
if (!menu.has_value()) {
return true;
}
const NodeEnumItem menu_item = NodeEnumItem{nullptr, nullptr, menu.value().value};
const std::string identifier = nodes::MenuSwitchItemsAccessor::socket_identifier_for_item(
menu_item);
return input.identifier == identifier;
}
/* Returns true if the given input of the given Index node in the given node group operation is
* needed by the node. */
static bool is_index_switch_node_input_needed(const bNode &node,
const bNodeSocket &input,
NodeGroupOperation &node_group_operation)
{
const UString index_identifier = "Index"_ustr;
if (input.identifier_ustr() == index_identifier) {
return true;
}
const std::optional<int> index = get_input_socket_value<int, bNodeSocketValueInt>(
node, index_identifier, node_group_operation);
if (!index.has_value()) {
return true;
}
const NodeIndexSwitch &storage = *static_cast<const NodeIndexSwitch *>(node.storage);
if (!IndexRange(storage.items_num).contains(index.value())) {
return false;
}
const std::string identifier = nodes::IndexSwitchItemsAccessor::socket_identifier_for_item(
storage.items[index.value()]);
return input.identifier == identifier;
}
/* Returns true if the given input of the given node in the given node group operation is needed by
* the compositor. */
static bool is_input_needed(const bNode &node,
const bNodeSocket &input,
NodeGroupOperation &node_group_operation)
{
if (node.is_group_output()) {
return node_group_operation.get_result(input.identifier).should_compute();
}
if (node.is_type("GeometryNodeSwitch"_ustr)) {
return is_switch_node_input_needed(node, input, node_group_operation);
}
if (node.is_type("GeometryNodeMenuSwitch"_ustr)) {
return is_menu_switch_node_input_needed(node, input, node_group_operation);
}
if (node.is_type("GeometryNodeIndexSwitch"_ustr)) {
return is_index_switch_node_input_needed(node, input, node_group_operation);
}
return true;
}
/* A type representing a mapping that associates each node with a heuristic estimation of the
* number of intermediate buffers needed to compute it and all of its dependencies. See the
* compute_number_of_needed_buffers function for more information. */
using NeededBuffers = Map<const bNode *, int>;
/* Compute a heuristic estimation of the number of intermediate buffers needed to compute each node
* and all of its dependencies for all nodes that the given node depends on. The output is a map
* that maps each node with the number of intermediate buffers needed to compute it and all of its
* dependencies.
*
* Consider a node that takes n number of buffers as an input from a number of node dependencies,
* which we shall call the input nodes. The node also computes and outputs m number of buffers.
* In order for the node to compute its output, a number of intermediate buffers will be needed.
* Since the node takes n buffers and outputs m buffers, then the number of buffers directly
* needed by the node is (n + m). But each of the input buffers are computed by a node that, in
* turn, needs a number of buffers to compute its output. So the total number of buffers needed
* to compute the output of the node is max(n + m, d) where d is the number of buffers needed by
* the input node that needs the largest number of buffers. We only consider the input node that
* needs the largest number of buffers, because those buffers can be reused by any input node
* that needs a lesser number of buffers.
*
* Pixel nodes, however, are a special case because links between two pixel nodes inside the same
* pixel operation don't pass a buffer, but a single value in the pixel processor. So for pixel
* nodes, only inputs and outputs linked to nodes that are not pixel nodes should be considered.
* Note that this might not actually be true, because the compiler may decide to split a pixel
* operation into multiples ones that will pass buffers, but this is not something that can be
* known at scheduling-time. See the discussion in COM_compile_state.hh, COM_evaluator.hh, and
* COM_shader_operation.hh for more information. In the node tree shown below, node 4 will have
* exactly the same number of needed buffers by node 3, because its inputs and outputs are all
* internally linked in the pixel operation.
*
* Pixel Operation
* +------------------------------------------------------+
* .------------. | .------------. .------------. .------------. | .------------.
* | Node 1 | | | Node 3 | | Node 4 | | Node 5 | | | Node 6 |
* | |----|--| |--| |------| |--|--| |
* | | .-|--| | | | .---| | | | |
* '------------' | | '------------' '------------' | '------------' | '------------'
* | +----------------------------------|-------------------+
* .------------. | |
* | Node 2 | | |
* | |--'------------------------------------'
* | |
* '------------'
*
* Note that the computed output is not guaranteed to be accurate, and will not be in most cases.
* The computation is merely a heuristic estimation that works well in most cases. This is due to a
* number of reasons:
* - The node tree is actually a graph that allows output sharing, which is not something that was
* taken into consideration in this implementation because it is difficult to correctly consider.
* - Each node may allocate any number of internal buffers, which is not taken into account in this
* implementation because it rarely affects the output and is done by very few nodes.
* - The compiler may decide to compiler the schedule differently depending on runtime information
* which we can merely speculate at scheduling-time as described above. */
static NeededBuffers compute_number_of_needed_buffers(Stack<const bNode *> &output_nodes,
NodeGroupOperation &node_group_operation)
{
NeededBuffers needed_buffers;
/* A stack of nodes used to traverse the node group starting from the output nodes. */
Stack<const bNode *> node_stack = output_nodes;
/* Traverse the node group in a post order depth first manner and compute the number of needed
* buffers for each node. Post order traversal guarantee that all the node dependencies of each
* node are computed before it. This is done by pushing all the uncomputed node dependencies to
* the node stack first and only popping and computing the node when all its node dependencies
* were computed. */
while (!node_stack.is_empty()) {
/* Do not pop the node immediately, as it may turn out that we can't compute its number of
* needed buffers just yet because its dependencies weren't computed, it will be popped later
* when needed. */
const bNode &node = *node_stack.peek();
/* Go over the node dependencies connected to the inputs of the node and push them to the node
* stack if they were not computed already. */
Set<const bNode *> pushed_nodes;
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
if (!is_input_needed(node, *input, node_group_operation)) {
continue;
}
/* Get the output linked to the input. If it is null, that means the input is unlinked and
* has no dependency node. */
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
continue;
}
/* The node dependency was already computed or pushed before, so skip it. */
if (needed_buffers.contains(&output->owner_node()) ||
pushed_nodes.contains(&output->owner_node()))
{
continue;
}
/* The output node needs to be computed, push the node dependency to the node stack and
* indicate that it was pushed. */
node_stack.push(&output->owner_node());
pushed_nodes.add_new(&output->owner_node());
}
/* If any of the node dependencies were pushed, that means that not all of them were computed
* and consequently we can't compute the number of needed buffers for this node just yet. */
if (!pushed_nodes.is_empty()) {
continue;
}
/* We don't need to store the result of the pop because we already peeked at it before. */
node_stack.pop();
/* Compute the number of buffers that the node takes as an input as well as the number of
* buffers needed to compute the most demanding of the node dependencies. */
int number_of_input_buffers = 0;
int buffers_needed_by_dependencies = 0;
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
if (!is_input_needed(node, *input, node_group_operation)) {
continue;
}
/* Get the output linked to the input. If it is null, that means the input is unlinked.
* Unlinked inputs do not take a buffer, so skip those inputs. */
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
continue;
}
/* Since this input is linked, if the link is not between two pixel nodes, it means that the
* node takes a buffer through this input and so we increment the number of input buffers. */
if (!is_pixel_node(node) || !is_pixel_node(output->owner_node())) {
number_of_input_buffers++;
}
/* If the number of buffers needed by the node dependency is more than the total number of
* buffers needed by the dependencies, then update the latter to be the former. This is
* computing the "d" in the aforementioned equation "max(n + m, d)". */
const int buffers_needed_by_dependency = needed_buffers.lookup(&output->owner_node());
buffers_needed_by_dependencies = std::max(buffers_needed_by_dependency,
buffers_needed_by_dependencies);
}
/* Compute the number of buffers that will be computed/output by this node. */
int number_of_output_buffers = 0;
for (const bNodeSocket *output : node.output_sockets()) {
if (!is_socket_available(output)) {
continue;
}
/* The output is not linked, it outputs no buffer. */
if (!output->is_logically_linked()) {
continue;
}
/* If any of the links is not between two pixel nodes, it means that the node outputs
* a buffer through this output and so we increment the number of output buffers. */
if (!is_pixel_node(node) ||
is_output_linked_to_input_conditioned(*output, [&](const bNodeSocket &input) {
return !is_pixel_node(input.owner_node());
}))
{
number_of_output_buffers++;
}
}
/* Compute the heuristic estimation of the number of needed intermediate buffers to compute
* this node and all of its dependencies. This is computing the aforementioned equation
* "max(n + m, d)". */
const int total_buffers = std::max(number_of_input_buffers + number_of_output_buffers,
buffers_needed_by_dependencies);
needed_buffers.add(&node, total_buffers);
}
return needed_buffers;
}
/* There are multiple different possible orders of evaluating a node graph, each of which needs
* to allocate a number of intermediate buffers to store its intermediate results. It follows
* that we need to find the evaluation order which uses the least amount of intermediate buffers.
* For instance, consider a node that takes two input buffers A and B. Each of those buffers is
* computed through a number of nodes constituting a sub-graph whose root is the node that
* outputs that buffer. Suppose the number of intermediate buffers needed to compute A and B are
* N(A) and N(B) respectively and N(A) > N(B). Then evaluating the sub-graph computing A would be
* a better option than that of B, because had B was computed first, its outputs will need to be
* stored in extra buffers in addition to the buffers needed by A. The number of buffers needed by
* each node is estimated as described in the compute_number_of_needed_buffers function.
*
* This is a heuristic generalization of the Sethi-Ullman algorithm, a generalization that
* doesn't always guarantee an optimal evaluation order, as the optimal evaluation order is very
* difficult to compute, however, this method works well in most cases. Moreover it assumes that
* all buffers will have roughly the same size, which may not always be the case. */
Schedule compute_schedule(const Context &context,
const bNodeTree &node_group,
NodeGroupOperation &node_group_operation,
NodeGroupOutputTypes needed_outputs_types,
const bNodeInstanceKey instance_key,
const bNodeInstanceKey active_node_group_instance_key)
{
Schedule schedule;
/* Validate node group. */
node_group.ensure_topology_cache();
if (node_group.has_available_link_cycle()) {
context.set_info_message("Compositor node group has cyclic links.");
return schedule;
}
/* A stack of nodes used to traverse the node group starting from the output nodes. */
Stack<const bNode *> node_stack;
/* Add the output nodes whose result should be computed to the stack. */
add_output_nodes(context,
node_group,
node_group_operation,
needed_outputs_types,
instance_key,
active_node_group_instance_key,
node_stack);
/* No output nodes, the node group has no effect, return an empty schedule. */
if (node_stack.is_empty()) {
return schedule;
}
/* Compute the number of buffers needed by each node connected to the outputs. */
const NeededBuffers needed_buffers = compute_number_of_needed_buffers(node_stack,
node_group_operation);
/* Traverse the node group in a post order depth first manner, scheduling the nodes in an order
* informed by the number of buffers needed by each node. Post order traversal guarantee that all
* the node dependencies of each node are scheduled before it. This is done by pushing all the
* unscheduled node dependencies to the node stack first and only popping and scheduling the node
* when all its node dependencies were scheduled. */
while (!node_stack.is_empty()) {
/* Do not pop the node immediately, as it may turn out that we can't schedule it just yet
* because its dependencies weren't scheduled, it will be popped later when needed. */
const bNode &node = *node_stack.peek();
/* Compute the nodes directly connected to the node inputs sorted by their needed buffers such
* that the node with the lowest number of needed buffers comes first. Note that we actually
* want the node with the highest number of needed buffers to be schedule first, but since
* those are pushed to the traversal stack, we need to push them in reverse order. */
Vector<const bNode *> sorted_dependency_nodes;
for (const bNodeSocket *input : node.input_sockets()) {
if (!is_socket_available(input)) {
continue;
}
if (!is_input_needed(node, *input, node_group_operation)) {
schedule.unneeded_inputs.add(input);
continue;
}
/* Get the output linked to the input. If it is null, that means the input is unlinked and
* has no dependency node, so skip it. */
const bNodeSocket *output = get_output_linked_to_input(*input);
if (!output) {
continue;
}
/* The dependency node was added before, so skip it. The number of dependency nodes is very
* small, typically less than 3, so a linear search is okay. */
if (sorted_dependency_nodes.contains(&output->owner_node())) {
continue;
}
/* The dependency node was already schedule, so skip it. */
if (schedule.nodes.contains(&output->owner_node())) {
continue;
}
/* Sort in ascending order on insertion, the number of dependency nodes is very small,
* typically less than 3, so insertion sort is okay. */
int insertion_position = 0;
for (int i = 0; i < sorted_dependency_nodes.size(); i++) {
if (needed_buffers.lookup(&output->owner_node()) >
needed_buffers.lookup(sorted_dependency_nodes[i]))
{
insertion_position++;
}
else {
break;
}
}
sorted_dependency_nodes.insert(insertion_position, &output->owner_node());
}
/* Push the sorted dependency nodes to the node stack in order. */
for (const bNode *dependency_node : sorted_dependency_nodes) {
node_stack.push(dependency_node);
}
/* If there are no sorted dependency nodes, that means they were all already scheduled or that
* none exists in the first place, so we can pop and schedule the node now. */
if (sorted_dependency_nodes.is_empty()) {
/* The node might have already been scheduled, so we don't use add_new here and simply don't
* add it if it was already scheduled. */
schedule.nodes.add(node_stack.pop());
}
}
return schedule;
}
} // namespace blender::compositor

View File

@@ -0,0 +1,144 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_vector.h"
#include "BLI_string_ref.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "GPU_material.hh"
#include "COM_shader_node.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
ShaderNode::ShaderNode(const bNode &node) : node_(node)
{
this->populate_inputs();
this->populate_outputs();
}
void ShaderNode::compile(GPUMaterial *material)
{
node_.typeinfo->gpu_fn(
material, const_cast<bNode *>(&node_), nullptr, inputs_.data(), outputs_.data());
}
GPUNodeStack &ShaderNode::get_input(const StringRef identifier)
{
return GPU_node_get_input(node_, inputs_.data(), identifier);
}
GPUNodeStack &ShaderNode::get_output(const StringRef identifier)
{
return GPU_node_get_output(node_, outputs_.data(), identifier);
}
static GPUType gpu_type_from_socket(const bNodeSocket &socket)
{
switch (socket.type) {
case SOCK_FLOAT:
return GPU_FLOAT;
case SOCK_INT:
/* GPUMaterial doesn't support int, so it is passed as a float. */
return GPU_FLOAT;
case SOCK_BOOLEAN:
/* GPUMaterial doesn't support boolean, so it is passed as a float. */
return GPU_FLOAT;
case SOCK_VECTOR:
switch (socket.default_value_typed<bNodeSocketValueVector>()->dimensions) {
case 2:
return GPU_VEC2;
case 3:
return GPU_VEC3;
case 4:
return GPU_VEC4;
default:
BLI_assert_unreachable();
return GPU_NONE;
}
case SOCK_INT_VECTOR:
/* GPUMaterial doesn't support int[23], so it is passed as a float[23]. */
switch (socket.default_value_typed<bNodeSocketValueIntVector>()->dimensions) {
case 2:
return GPU_VEC2;
case 3:
return GPU_VEC3;
default:
BLI_assert_unreachable();
return GPU_NONE;
}
case SOCK_RGBA:
case SOCK_ROTATION:
return GPU_VEC4;
case SOCK_MATRIX:
return GPU_MAT4;
case SOCK_MENU:
/* GPUMaterial doesn't support int, so it is passed as a float. */
return GPU_FLOAT;
case SOCK_STRING:
case SOCK_OBJECT:
case SOCK_IMAGE:
case SOCK_FONT:
case SOCK_SCENE:
case SOCK_TEXT_ID:
case SOCK_MASK:
/* Single only types do not support GPU code path. */
BLI_assert(Result::is_single_value_only_type(get_node_socket_result_type(&socket)));
BLI_assert_unreachable();
return GPU_NONE;
default:
/* The GPU material compiler will skip unsupported sockets if GPU_NONE is provided. So this
* is an appropriate and a valid type for unsupported sockets. */
return GPU_NONE;
}
}
static void populate_gpu_node_stack(const bNodeSocket &socket, GPUNodeStack &stack)
{
/* Make sure this stack is not marked as the end of the stack array. */
stack.end = false;
/* This will be initialized later by the GPU material compiler or the compile method. */
stack.link = nullptr;
/* This will be initialized by the GPU material compiler if needed. */
zero_v4(stack.vec);
stack.sockettype = socket.type;
stack.type = gpu_type_from_socket(socket);
stack.hasinput = socket.is_logically_linked();
stack.hasoutput = socket.is_logically_linked();
}
void ShaderNode::populate_inputs()
{
/* Reserve a stack for each input in addition to an extra stack at the end to mark the end of the
* array, as this is what the GPU module functions expect. */
const int num_input_sockets = node_.input_sockets().size();
inputs_.resize(num_input_sockets + 1);
inputs_.last().end = true;
for (int i = 0; i < num_input_sockets; i++) {
populate_gpu_node_stack(node_.input_socket(i), inputs_[i]);
}
}
void ShaderNode::populate_outputs()
{
/* Reserve a stack for each output in addition to an extra stack at the end to mark the end of
* the array, as this is what the GPU module functions expect. */
const int num_output_sockets = node_.output_sockets().size();
outputs_.resize(num_output_sockets + 1);
outputs_.last().end = true;
for (int i = 0; i < num_output_sockets; i++) {
populate_gpu_node_stack(node_.output_socket(i), outputs_[i]);
}
}
} // namespace blender::compositor

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,50 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_input_descriptor.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
#include "COM_simple_operation.hh"
namespace blender::compositor {
const StringRef SimpleOperation::input_identifier_ = StringRef("Input");
const StringRef SimpleOperation::output_identifier_ = StringRef("Output");
Result &SimpleOperation::get_result()
{
return Operation::get_result(output_identifier_);
}
void SimpleOperation::map_input_to_result(Result *result)
{
Operation::map_input_to_result(input_identifier_, result);
}
void SimpleOperation::evaluate_input_processors() {}
Result &SimpleOperation::get_input()
{
return Operation::get_input(input_identifier_);
}
void SimpleOperation::populate_result(const ResultType type)
{
Operation::populate_result(output_identifier_, type);
/* The result of a simple operation is guaranteed to have a single user. */
get_result().set_reference_count(1);
}
void SimpleOperation::declare_input_descriptor(InputDescriptor descriptor)
{
Operation::declare_input_descriptor(input_identifier_, descriptor);
}
InputDescriptor &SimpleOperation::get_input_descriptor()
{
return Operation::get_input_descriptor(input_identifier_);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,167 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <string>
#include "BLI_assert.h"
#include "BLI_math_euler.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "COM_operation.hh"
#include "COM_result.hh"
#include "COM_single_value_node_input_operation.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
const StringRef SingleValueNodeInputOperation::output_identifier_ = StringRef("Output");
SingleValueNodeInputOperation::SingleValueNodeInputOperation(Context &context,
const bNodeSocket &input_socket)
: Operation(context), input_socket_(input_socket)
{
this->populate_result(get_node_socket_result_type(&input_socket));
}
void SingleValueNodeInputOperation::execute()
{
Result &result = this->get_result();
result.allocate_single_value();
switch (input_socket_.type) {
case SOCK_FLOAT: {
const float value = input_socket_.default_value_typed<bNodeSocketValueFloat>()->value;
result.set_single_value(value);
break;
}
case SOCK_INT: {
const int value = input_socket_.default_value_typed<bNodeSocketValueInt>()->value;
result.set_single_value(value);
break;
}
case SOCK_BOOLEAN: {
const bool value = input_socket_.default_value_typed<bNodeSocketValueBoolean>()->value;
result.set_single_value(value);
break;
}
case SOCK_VECTOR: {
switch (input_socket_.default_value_typed<bNodeSocketValueVector>()->dimensions) {
case 2: {
const float2 value = input_socket_.default_value_typed<bNodeSocketValueVector>()->value;
result.set_single_value(value);
break;
}
case 3: {
const float3 value = input_socket_.default_value_typed<bNodeSocketValueVector>()->value;
result.set_single_value(value);
break;
}
case 4: {
const float4 value = input_socket_.default_value_typed<bNodeSocketValueVector>()->value;
result.set_single_value(value);
break;
}
default:
BLI_assert_unreachable();
break;
}
break;
}
case SOCK_INT_VECTOR: {
switch (input_socket_.default_value_typed<bNodeSocketValueIntVector>()->dimensions) {
case 2: {
const int2 value = input_socket_.default_value_typed<bNodeSocketValueIntVector>()->value;
result.set_single_value(value);
break;
}
case 3: {
const int3 value = input_socket_.default_value_typed<bNodeSocketValueIntVector>()->value;
result.set_single_value(value);
break;
}
default:
BLI_assert_unreachable();
break;
}
break;
}
case SOCK_RGBA: {
const Color value = input_socket_.default_value_typed<bNodeSocketValueRGBA>()->value;
result.set_single_value(value);
break;
}
case SOCK_MATRIX: {
result.set_single_value(float4x4::identity());
break;
}
case SOCK_MENU: {
const int32_t value = input_socket_.default_value_typed<bNodeSocketValueMenu>()->value;
result.set_single_value(nodes::MenuValue(value));
break;
}
case SOCK_STRING: {
const std::string value = input_socket_.default_value_typed<bNodeSocketValueString>()->value;
result.set_single_value(value);
break;
}
case SOCK_ROTATION: {
const bNodeSocketValueRotation *rotation =
input_socket_.default_value_typed<bNodeSocketValueRotation>();
const math::EulerXYZ euler(float3(rotation->value_euler));
const math::Quaternion value = math::to_quaternion(euler);
result.set_single_value(value);
break;
}
case SOCK_OBJECT: {
Object *value = input_socket_.default_value_typed<bNodeSocketValueObject>()->value;
result.set_single_value(value);
break;
}
case SOCK_IMAGE: {
Image *value = input_socket_.default_value_typed<bNodeSocketValueImage>()->value;
result.set_single_value(value);
break;
}
case SOCK_FONT: {
VFont *value = input_socket_.default_value_typed<bNodeSocketValueFont>()->value;
result.set_single_value(value);
break;
}
case SOCK_SCENE: {
Scene *value = input_socket_.default_value_typed<bNodeSocketValueScene>()->value;
result.set_single_value(value);
break;
}
case SOCK_TEXT_ID: {
Text *value = input_socket_.default_value_typed<bNodeSocketValueText>()->value;
result.set_single_value(value);
break;
}
case SOCK_MASK: {
Mask *value = input_socket_.default_value_typed<bNodeSocketValueMask>()->value;
result.set_single_value(value);
break;
}
default:
BLI_assert_unreachable();
break;
}
}
Result &SingleValueNodeInputOperation::get_result()
{
return Operation::get_result(output_identifier_);
}
void SingleValueNodeInputOperation::populate_result(const ResultType type)
{
Operation::populate_result(output_identifier_, type);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,38 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "COM_static_cache_manager.hh"
namespace blender::compositor {
void StaticCacheManager::reset()
{
symmetric_blur_weights.reset();
symmetric_separable_blur_weights.reset();
morphological_distance_feather_weights.reset();
cached_masks.reset();
smaa_precomputed_textures.reset();
ocio_color_space_conversion_shaders.reset();
ocio_to_display_shaders.reset();
distortion_grids.reset();
keying_screens.reset();
cached_shaders.reset();
bokeh_kernels.reset();
cached_images.reset();
deriche_gaussian_coefficients.reset();
van_vliet_gaussian_coefficients.reset();
fog_glow_kernels.reset();
image_coordinates.reset();
string_images.reset();
}
void StaticCacheManager::free()
{
/* Resetting two times frees everything, because the first reset will declare everything as not
* needed, and the second reset will delete all such resources. */
this->reset();
this->reset();
}
} // namespace blender::compositor

View File

@@ -0,0 +1,33 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "COM_context.hh"
#include "COM_node_operation.hh"
#include "COM_result.hh"
#include "COM_undefined_node_operation.hh"
namespace blender::compositor {
/* A node operation that allocates all of its outputs as invalid. */
class UndefinedNodeOperation : public NodeOperation {
public:
using NodeOperation::NodeOperation;
void execute() override
{
this->allocate_default_remaining_outputs();
}
};
NodeOperation *get_undefined_node_operation(Context &context, const bNode &node)
{
return new UndefinedNodeOperation(context, node);
}
} // namespace blender::compositor

View File

@@ -0,0 +1,285 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include <optional>
#include "BLI_assert.h"
#include "BLI_math_vector.hh"
#include "BLI_math_vector_types.hh"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "NOD_node_declaration.hh"
#include "GPU_compute.hh"
#include "GPU_shader.hh"
#include "COM_result.hh"
#include "COM_utilities.hh"
namespace blender::compositor {
bool is_socket_available(const bNodeSocket *socket)
{
return socket->is_available() && StringRef(socket->idname) != "NodeSocketVirtual";
}
const bNodeSocket *get_output_linked_to_input(const bNodeSocket &input)
{
if (!input.is_logically_linked()) {
return nullptr;
}
return input.logically_linked_sockets()[0];
}
ResultType socket_data_type_to_result_type(const eNodeSocketDatatype data_type,
const std::optional<int> dimensions)
{
switch (data_type) {
case SOCK_FLOAT:
return ResultType::Float;
case SOCK_INT:
return ResultType::Int;
case SOCK_BOOLEAN:
return ResultType::Bool;
case SOCK_VECTOR:
switch (dimensions.value_or(3)) {
case 2:
return ResultType::Float2;
case 3:
return ResultType::Float3;
case 4:
return ResultType::Float4;
default:
BLI_assert_unreachable();
return ResultType::Float;
}
case SOCK_INT_VECTOR:
switch (dimensions.value_or(2)) {
case 2:
return ResultType::Int2;
case 3:
return ResultType::Int3;
default:
BLI_assert_unreachable();
return ResultType::Float;
}
case SOCK_RGBA:
return ResultType::Color;
case SOCK_MATRIX:
return ResultType::Float4x4;
case SOCK_MENU:
return ResultType::Menu;
case SOCK_STRING:
return ResultType::String;
case SOCK_ROTATION:
return ResultType::Quaternion;
case SOCK_OBJECT:
return ResultType::Object;
case SOCK_IMAGE:
return ResultType::Image;
case SOCK_FONT:
return ResultType::Font;
case SOCK_SCENE:
return ResultType::Scene;
case SOCK_TEXT_ID:
return ResultType::Text;
case SOCK_MASK:
return ResultType::Mask;
default:
BLI_assert_unreachable();
return ResultType::Float;
}
}
ResultType get_node_socket_result_type(const bNodeSocket *socket)
{
/* Gracefully handle undefined sockets, falling back to a float. */
if (socket->typeinfo == &bke::NodeSocketTypeUndefined) {
return ResultType::Float;
}
const eNodeSocketDatatype socket_type = static_cast<eNodeSocketDatatype>(socket->type);
if (socket_type == SOCK_VECTOR) {
return socket_data_type_to_result_type(
socket_type, socket->default_value_typed<bNodeSocketValueVector>()->dimensions);
}
if (socket_type == SOCK_INT_VECTOR) {
return socket_data_type_to_result_type(
socket_type, socket->default_value_typed<bNodeSocketValueIntVector>()->dimensions);
}
return socket_data_type_to_result_type(socket_type);
}
ResultType get_node_interface_socket_result_type(const bNodeTreeInterfaceSocket &socket)
{
/* Gracefully handle undefined interface sockets, falling back to a float. */
if (socket.socket_typeinfo() == &bke::NodeSocketTypeUndefined) {
return ResultType::Float;
}
const eNodeSocketDatatype socket_type = socket.socket_typeinfo()->type;
if (socket_type == SOCK_VECTOR) {
return socket_data_type_to_result_type(
socket_type, static_cast<bNodeSocketValueVector *>(socket.socket_data)->dimensions);
}
if (socket_type == SOCK_INT_VECTOR) {
return socket_data_type_to_result_type(
socket_type, static_cast<bNodeSocketValueIntVector *>(socket.socket_data)->dimensions);
}
return socket_data_type_to_result_type(socket_type);
}
bool is_output_linked_to_input_conditioned(const bNodeSocket &output,
FunctionRef<bool(const bNodeSocket &)> condition)
{
for (const bNodeSocket *input : output.logically_linked_sockets()) {
if (condition(*input)) {
return true;
}
}
return false;
}
int number_of_inputs_linked_to_output_conditioned(const bNodeSocket &output,
FunctionRef<bool(const bNodeSocket &)> condition)
{
if (!output.is_logically_linked()) {
return 0;
}
int count = 0;
for (const bNodeSocket *input : output.logically_linked_sockets()) {
if (condition(*input)) {
count++;
}
}
return count;
}
bool is_pixel_node(const bNode &node)
{
return node.typeinfo->build_multi_function;
}
static std::optional<ImplicitInputType> get_implicit_input(
const NodeDefaultInputType node_default_input_type)
{
switch (node_default_input_type) {
case NodeDefaultInputType::NODE_DEFAULT_INPUT_VALUE:
return std::nullopt;
case NodeDefaultInputType::NODE_DEFAULT_INPUT_UNIFORM_IMAGE_COORDINATES:
return ImplicitInputType::UniformImageCoordinates;
case NodeDefaultInputType::NODE_DEFAULT_INPUT_SCENE_FRAME:
return ImplicitInputType::SceneFrame;
case NodeDefaultInputType::NODE_DEFAULT_INPUT_INDEX_FIELD:
case NodeDefaultInputType::NODE_DEFAULT_INPUT_ID_INDEX_FIELD:
case NodeDefaultInputType::NODE_DEFAULT_INPUT_NORMAL_FIELD:
case NodeDefaultInputType::NODE_DEFAULT_INPUT_POSITION_FIELD:
case NodeDefaultInputType::NODE_DEFAULT_INPUT_INSTANCE_TRANSFORM_FIELD:
case NodeDefaultInputType::NODE_DEFAULT_INPUT_HANDLE_LEFT_FIELD:
case NodeDefaultInputType::NODE_DEFAULT_INPUT_HANDLE_RIGHT_FIELD:
case NodeDefaultInputType::NODE_DEFAULT_INPUT_SELF_OBJECT:
break;
}
BLI_assert_unreachable();
return std::nullopt;
}
static int get_domain_priority(const bNodeSocket *input,
const nodes::SocketDeclaration *socket_declaration)
{
/* Negative priority means no priority is set and we fall back to the index, that is, we
* prioritize inputs according to their order. */
if (socket_declaration->compositor_domain_priority() < 0) {
return input->index();
}
return socket_declaration->compositor_domain_priority();
}
InputDescriptor input_descriptor_from_input_socket(const bNodeSocket *socket)
{
InputDescriptor input_descriptor;
input_descriptor.type = get_node_socket_result_type(socket);
/* Default to the index of the input as its domain priority in case the node does not have a
* declaration. */
input_descriptor.domain_priority = socket->index();
/* Not every node has a declaration, in which case we assume the default values for the rest of
* the properties. */
const nodes::NodeDeclaration *node_declaration = socket->owner_node().declaration();
if (!node_declaration) {
return input_descriptor;
}
const nodes::SocketDeclaration *socket_declaration = node_declaration->inputs[socket->index()];
input_descriptor.domain_priority = get_domain_priority(socket, socket_declaration);
input_descriptor.expects_single_value = socket_declaration->structure_type ==
nodes::StructureType::Single;
input_descriptor.realization_mode = static_cast<InputRealizationMode>(
socket_declaration->compositor_realization_mode());
input_descriptor.implicit_input = get_implicit_input(socket_declaration->default_input_type);
return input_descriptor;
}
InputDescriptor input_descriptor_from_interface_input(const bNodeTree &node_group,
const bNodeTreeInterfaceSocket &socket)
{
InputDescriptor input_descriptor;
input_descriptor.type = get_node_interface_socket_result_type(socket);
input_descriptor.domain_priority = node_group.interface_input_index(socket);
input_descriptor.expects_single_value = socket.structure_type ==
NodeSocketInterfaceStructureType::Single;
input_descriptor.realization_mode = InputRealizationMode::None;
input_descriptor.implicit_input = get_implicit_input(socket.default_input);
return input_descriptor;
}
void compute_dispatch_threads_at_least(gpu::Shader *shader, int2 threads_range, int2 local_size)
{
/* If the threads range is divisible by the local size, dispatch the number of needed groups,
* which is their division. If it is not divisible, then dispatch an extra group to cover the
* remaining invocations, which means the actual threads range of the dispatch will be a bit
* larger than the given one. */
const int2 groups_to_dispatch = math::divide_ceil(threads_range, local_size);
GPU_compute_dispatch(shader, groups_to_dispatch.x, groups_to_dispatch.y, 1);
}
bool is_node_preview_needed(const bNode &node)
{
if (!(node.flag & NODE_PREVIEW)) {
return false;
}
if (node.flag & NODE_COLLAPSED) {
return false;
}
return true;
}
const bNodeSocket *find_preview_output_socket(const bNode &node)
{
if (!is_node_preview_needed(node)) {
return nullptr;
}
for (const bNodeSocket *output : node.output_sockets()) {
if (is_socket_available(output) && output->is_logically_linked()) {
return output;
}
}
return nullptr;
}
} // namespace blender::compositor