Compositor: Support node integer sockets

This patch adds support for using integer sockets in compositor nodes.
This involves updating the Result class, node tree compiler, implicit
conversion operation, multi-function procedure operation, shader
operation, and some operations that supports multiple types.

Shader operation internally treats integers as floats, doing conversion
to and from int when reading and writing. That's because the GPUMaterial
compiler doesn't support integers. This is also the same workaround used
by the shader system. Though the GPU module are eyeing adding support
for integers, so we will update the code once they do that.

Domain realization is not yet supported for integer types, but this is
an internal limitation so far, as we do not plan to add nodes that
outputs integers soon. We are not yet sure how realization should happen
with regards to interpolation and we do not have base functions to
sample integer images, that's why I decided to delay its implementation
when it is actually needed.

Pull Request: https://projects.blender.org/blender/blender/pulls/132599
This commit is contained in:
Omar Emara 2025-01-06 10:09:26 +01:00 committed by Omar Emara
parent 77041084cd
commit b3623feab2
19 changed files with 569 additions and 79 deletions

View file

@ -33,6 +33,7 @@ enum class ResultType : uint8_t {
* can encode two 2D vectors, one 3D vector with the last component ignored, or other dimensional
* data. */
Float,
Int,
Vector,
Color,
@ -142,6 +143,7 @@ class Result {
float4 color_value_ = float4(0.0f);
float2 float2_value_;
float3 float3_value_;
int int_value_;
int2 int2_value_;
};
/* The domain of the result. This only matters if the result was a texture. See the discussion in
@ -364,6 +366,10 @@ class Result {
/* Returns a reference to the allocate integer data. */
int *integer_texture() const;
/* Returns a reference to the allocated CPU data. The returned data is untyped, use the
* float_texture() or the integer_texture() methods for typed data. */
void *data() const;
/* Gets the single value stored in the result. Assumes the result stores a value of the given
* template type. */
template<typename T> T get_single_value() const;
@ -501,6 +507,7 @@ inline int64_t Result::channels_count() const
{
switch (type_) {
case ResultType::Float:
case ResultType::Int:
return 1;
case ResultType::Float2:
case ResultType::Int2:
@ -526,6 +533,21 @@ inline int *Result::integer_texture() const
return integer_texture_;
}
inline void *Result::data() const
{
switch (storage_type_) {
case ResultStorageType::FloatCPU:
return this->float_texture();
case ResultStorageType::IntegerCPU:
return this->integer_texture();
case ResultStorageType::GPU:
break;
}
BLI_assert_unreachable();
return nullptr;
}
template<typename T> inline T Result::get_single_value() const
{
BLI_assert(this->is_single_value());
@ -535,6 +557,10 @@ template<typename T> inline T Result::get_single_value() const
BLI_assert(type_ == ResultType::Float);
return float_value_;
}
else if constexpr (std::is_same_v<T, int>) {
BLI_assert(type_ == ResultType::Int);
return int_value_;
}
else if constexpr (std::is_same_v<T, float2>) {
BLI_assert(type_ == ResultType::Float2);
return float2_value_;
@ -574,6 +600,10 @@ template<typename T> inline void Result::set_single_value(const T &value)
BLI_assert(type_ == ResultType::Float);
float_value_ = value;
}
else if constexpr (std::is_same_v<T, int>) {
BLI_assert(type_ == ResultType::Int);
int_value_ = value;
}
else if constexpr (std::is_same_v<T, float2>) {
BLI_assert(type_ == ResultType::Float2);
float2_value_ = value;
@ -987,7 +1017,7 @@ template<typename T> constexpr int Result::get_type_channels_count()
template<typename T> constexpr bool Result::is_supported_type()
{
return is_same_any_v<T, float, float2, float3, float4, int2>;
return is_same_any_v<T, float, int, float2, float3, float4, int2>;
}
template<typename T> inline int64_t Result::get_pixel_index(const int2 &texel) const
@ -1059,6 +1089,9 @@ inline void Result::copy_pixel(float *target, const float *source, const int cha
inline void Result::copy_pixel(int *target, const int *source, const int channels_count)
{
switch (channels_count) {
case 1:
*target = *source;
break;
case 2:
copy_v2_v2_int(target, source);
break;
@ -1084,6 +1117,7 @@ inline void Result::copy_pixel(float *target, const float *source) const
case ResultType::Color:
copy_v4_v4(target, source);
break;
case ResultType::Int:
case ResultType::Int2:
BLI_assert_unreachable();
break;

View file

@ -34,6 +34,7 @@ static const char *get_realization_shader(Result &input,
return "compositor_realize_on_domain_bicubic_vector";
case ResultType::Float:
return "compositor_realize_on_domain_bicubic_float";
case ResultType::Int:
case ResultType::Int2:
case ResultType::Float2:
case ResultType::Float3:
@ -49,6 +50,7 @@ static const char *get_realization_shader(Result &input,
return "compositor_realize_on_domain_vector";
case ResultType::Float:
return "compositor_realize_on_domain_float";
case ResultType::Int:
case ResultType::Int2:
case ResultType::Float2:
case ResultType::Float3:

View file

@ -1426,6 +1426,7 @@ static float3 get_luminance_coefficients(ResultType type)
case ResultType::Float3:
/* GPU module does not support float3 outputs. */
break;
case ResultType::Int:
case ResultType::Int2:
/* SMAA does not support integer types. */
break;
@ -1596,6 +1597,7 @@ static const char *get_blend_shader_name(ResultType type)
case ResultType::Float3:
/* GPU module does not support float3 outputs. */
break;
case ResultType::Int:
case ResultType::Int2:
/* SMAA does not support integer types. */
break;
@ -1675,6 +1677,9 @@ static void compute_single_value(Result &input, Result &output)
case ResultType::Float3:
output.set_single_value(input.get_single_value<float3>());
break;
case ResultType::Int:
output.set_single_value(input.get_single_value<int>());
break;
case ResultType::Int2:
output.set_single_value(input.get_single_value<int2>());
break;

View file

@ -84,6 +84,7 @@ static const char *get_blur_shader(const ResultType type)
case ResultType::Float2:
case ResultType::Float3:
case ResultType::Int2:
case ResultType::Int:
/* Not supported. */
break;
}
@ -176,6 +177,7 @@ static Result horizontal_pass_cpu(Context &context,
case ResultType::Float2:
case ResultType::Float3:
case ResultType::Int2:
case ResultType::Int:
/* Not supported. */
BLI_assert_unreachable();
break;
@ -263,6 +265,7 @@ static void vertical_pass_cpu(Context &context,
case ResultType::Float2:
case ResultType::Float3:
case ResultType::Int2:
case ResultType::Int:
/* Not supported. */
BLI_assert_unreachable();
break;

View file

@ -71,6 +71,8 @@ const char *ConversionOperation::get_conversion_shader_name()
switch (this->get_input().type()) {
case ResultType::Float:
switch (this->get_result().type()) {
case ResultType::Int:
return "compositor_convert_float_to_int";
case ResultType::Vector:
return "compositor_convert_float_to_vector";
case ResultType::Color:
@ -85,10 +87,30 @@ const char *ConversionOperation::get_conversion_shader_name()
break;
}
break;
case ResultType::Int:
switch (this->get_result().type()) {
case ResultType::Float:
return "compositor_convert_int_to_float";
case ResultType::Vector:
return "compositor_convert_int_to_vector";
case ResultType::Color:
return "compositor_convert_int_to_color";
case ResultType::Int:
/* Same type, no conversion needed. */
break;
case ResultType::Float2:
case ResultType::Float3:
case ResultType::Int2:
/* Types are not user facing, so we needn't implement them. */
break;
}
break;
case ResultType::Vector:
switch (this->get_result().type()) {
case ResultType::Float:
return "compositor_convert_vector_to_float";
case ResultType::Int:
return "compositor_convert_vector_to_int";
case ResultType::Color:
return "compositor_convert_vector_to_color";
case ResultType::Vector:
@ -105,6 +127,8 @@ const char *ConversionOperation::get_conversion_shader_name()
switch (this->get_result().type()) {
case ResultType::Float:
return "compositor_convert_color_to_float";
case ResultType::Int:
return "compositor_convert_color_to_int";
case ResultType::Vector:
return "compositor_convert_color_to_vector";
case ResultType::Color:
@ -133,6 +157,9 @@ void ConversionOperation::execute_single(const Result &input, Result &output)
switch (this->get_input().type()) {
case ResultType::Float:
switch (this->get_result().type()) {
case ResultType::Int:
output.set_single_value(float_to_int(input.get_single_value<float>()));
return;
case ResultType::Vector:
output.set_single_value(float_to_vector(input.get_single_value<float>()));
return;
@ -149,11 +176,35 @@ void ConversionOperation::execute_single(const Result &input, Result &output)
break;
}
break;
case ResultType::Int:
switch (this->get_result().type()) {
case ResultType::Float:
output.set_single_value(int_to_float(input.get_single_value<int>()));
return;
case ResultType::Vector:
output.set_single_value(int_to_vector(input.get_single_value<int>()));
return;
case ResultType::Color:
output.set_single_value(int_to_color(input.get_single_value<int>()));
return;
case ResultType::Int:
/* Same type, no conversion needed. */
break;
case ResultType::Float2:
case ResultType::Float3:
case ResultType::Int2:
/* Types are not user facing, so we needn't implement them. */
break;
}
break;
case ResultType::Vector:
switch (this->get_result().type()) {
case ResultType::Float:
output.set_single_value(vector_to_float(input.get_single_value<float4>()));
return;
case ResultType::Int:
output.set_single_value(vector_to_int(input.get_single_value<float4>()));
return;
case ResultType::Color:
output.set_single_value(vector_to_color(input.get_single_value<float4>()));
return;
@ -172,6 +223,9 @@ void ConversionOperation::execute_single(const Result &input, Result &output)
case ResultType::Float:
output.set_single_value(color_to_float(input.get_single_value<float4>()));
return;
case ResultType::Int:
output.set_single_value(color_to_int(input.get_single_value<float4>()));
return;
case ResultType::Vector:
output.set_single_value(color_to_vector(input.get_single_value<float4>()));
return;
@ -200,6 +254,11 @@ void ConversionOperation::execute_cpu(const Result &input, Result &output)
switch (this->get_input().type()) {
case ResultType::Float:
switch (this->get_result().type()) {
case ResultType::Int:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, float_to_int(input.load_pixel<float>(texel)));
});
return;
case ResultType::Vector:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, float_to_vector(input.load_pixel<float>(texel)));
@ -220,6 +279,33 @@ void ConversionOperation::execute_cpu(const Result &input, Result &output)
break;
}
break;
case ResultType::Int:
switch (this->get_result().type()) {
case ResultType::Float:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, int_to_float(input.load_pixel<int>(texel)));
});
return;
case ResultType::Vector:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, int_to_vector(input.load_pixel<int>(texel)));
});
return;
case ResultType::Color:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, int_to_color(input.load_pixel<int>(texel)));
});
return;
case ResultType::Int:
/* Same type, no conversion needed. */
break;
case ResultType::Float2:
case ResultType::Float3:
case ResultType::Int2:
/* Types are not user facing, so we needn't implement them. */
break;
}
break;
case ResultType::Vector:
switch (this->get_result().type()) {
case ResultType::Float:
@ -227,6 +313,11 @@ void ConversionOperation::execute_cpu(const Result &input, Result &output)
output.store_pixel(texel, vector_to_float(input.load_pixel<float4>(texel)));
});
return;
case ResultType::Int:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, vector_to_int(input.load_pixel<float4>(texel)));
});
return;
case ResultType::Color:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, vector_to_color(input.load_pixel<float4>(texel)));
@ -249,6 +340,11 @@ void ConversionOperation::execute_cpu(const Result &input, Result &output)
output.store_pixel(texel, color_to_float(input.load_pixel<float4>(texel)));
});
return;
case ResultType::Int:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, color_to_int(input.load_pixel<float4>(texel)));
});
return;
case ResultType::Vector:
parallel_for(input.domain().size, [&](const int2 texel) {
output.store_pixel(texel, color_to_vector(input.load_pixel<float4>(texel)));

View file

@ -39,6 +39,9 @@ void InputSingleValueOperation::execute()
case ResultType::Float:
result.set_single_value(bsocket->default_value_typed<bNodeSocketValueFloat>()->value);
break;
case ResultType::Int:
result.set_single_value(bsocket->default_value_typed<bNodeSocketValueInt>()->value);
break;
case ResultType::Vector:
result.set_single_value(
float4(float3(bsocket->default_value_typed<bNodeSocketValueVector>()->value), 0.0f));

View file

@ -58,6 +58,8 @@ static const CPPType &get_cpp_type(ResultType type)
switch (type) {
case ResultType::Float:
return CPPType::get<float>();
case ResultType::Int:
return CPPType::get<int>();
case ResultType::Vector:
case ResultType::Color:
return CPPType::get<float4>();
@ -80,6 +82,9 @@ static void add_single_value_parameter(mf::ParamsBuilder &parameter_builder, con
case ResultType::Float:
parameter_builder.add_readonly_single_input_value(input.get_single_value<float>());
return;
case ResultType::Int:
parameter_builder.add_readonly_single_input_value(input.get_single_value<int>());
return;
case ResultType::Color:
parameter_builder.add_readonly_single_input_value(input.get_single_value<float4>());
return;
@ -111,14 +116,14 @@ void MultiFunctionProcedureOperation::execute()
add_single_value_parameter(parameter_builder, input);
}
else {
const GSpan span{get_cpp_type(input.type()), input.float_texture(), size};
const GSpan span{get_cpp_type(input.type()), input.data(), size};
parameter_builder.add_readonly_single_input(span);
}
}
else {
Result &result = get_result(parameter_identifiers_[i]);
result.allocate_texture(domain);
const GMutableSpan span{get_cpp_type(result.type()), result.float_texture(), size};
const GMutableSpan span{get_cpp_type(result.type()), result.data(), size};
parameter_builder.add_uninitialized_single_output(span);
}
}
@ -217,6 +222,11 @@ mf::Variable *MultiFunctionProcedureOperation::get_constant_input_variable(DInpu
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<int>>(value);
break;
}
case SOCK_VECTOR: {
const float3 value = float3(input->default_value_typed<bNodeSocketValueVector>()->value);
constant_function = &procedure_.construct_function<mf::CustomMF_Constant<float4>>(
@ -295,22 +305,39 @@ mf::Variable *MultiFunctionProcedureOperation::get_multi_function_input_variable
static mf::MultiFunction *get_conversion_function(const ResultType variable_type,
const ResultType expected_type)
{
static auto float_to_int_function = mf::build::SI1_SO<float, int>(
"Float To Int", float_to_int, mf::build::exec_presets::AllSpanOrSingle());
static auto float_to_vector_function = mf::build::SI1_SO<float, float4>(
"Float To Vector", float_to_vector, mf::build::exec_presets::AllSpanOrSingle());
static auto float_to_color_function = mf::build::SI1_SO<float, float4>(
"Float To Color", float_to_color, mf::build::exec_presets::AllSpanOrSingle());
static auto int_to_float_function = mf::build::SI1_SO<int, float>(
"Int To Float", int_to_float, mf::build::exec_presets::AllSpanOrSingle());
static auto int_to_vector_function = mf::build::SI1_SO<int, float4>(
"Int To Vector", int_to_vector, mf::build::exec_presets::AllSpanOrSingle());
static auto int_to_color_function = mf::build::SI1_SO<int, float4>(
"Int To Color", int_to_color, mf::build::exec_presets::AllSpanOrSingle());
static auto vector_to_float_function = mf::build::SI1_SO<float4, float>(
"Vector To Float", vector_to_float, mf::build::exec_presets::AllSpanOrSingle());
static auto vector_to_int_function = mf::build::SI1_SO<float4, int>(
"Vector To Int", vector_to_int, mf::build::exec_presets::AllSpanOrSingle());
static auto vector_to_color_function = mf::build::SI1_SO<float4, float4>(
"Vector To Color", vector_to_color, mf::build::exec_presets::AllSpanOrSingle());
static auto color_to_float_function = mf::build::SI1_SO<float4, float>(
"Color To Float", color_to_float, mf::build::exec_presets::AllSpanOrSingle());
static auto color_to_int_function = mf::build::SI1_SO<float4, int>(
"Color To Int", color_to_int, mf::build::exec_presets::AllSpanOrSingle());
static auto color_to_vector_function = mf::build::SI1_SO<float4, float4>(
"Color To Vector", color_to_vector, mf::build::exec_presets::AllSpanOrSingle());
switch (variable_type) {
case ResultType::Float:
switch (expected_type) {
case ResultType::Int:
return &float_to_int_function;
case ResultType::Vector:
return &float_to_vector_function;
case ResultType::Color:
@ -325,10 +352,30 @@ static mf::MultiFunction *get_conversion_function(const ResultType variable_type
break;
}
break;
case ResultType::Int:
switch (expected_type) {
case ResultType::Float:
return &int_to_float_function;
case ResultType::Vector:
return &int_to_vector_function;
case ResultType::Color:
return &int_to_color_function;
case ResultType::Int:
/* Same type, no conversion needed. */
return nullptr;
case ResultType::Float2:
case ResultType::Float3:
case ResultType::Int2:
/* Types are not user facing, so we needn't implement them. */
break;
}
break;
case ResultType::Vector:
switch (expected_type) {
case ResultType::Float:
return &vector_to_float_function;
case ResultType::Int:
return &vector_to_int_function;
case ResultType::Color:
return &vector_to_color_function;
case ResultType::Vector:
@ -345,6 +392,8 @@ static mf::MultiFunction *get_conversion_function(const ResultType variable_type
switch (expected_type) {
case ResultType::Float:
return &color_to_float_function;
case ResultType::Int:
return &color_to_int_function;
case ResultType::Vector:
return &color_to_vector_function;
case ResultType::Color:

View file

@ -29,12 +29,12 @@ void ReduceToSingleValueOperation::execute()
{
Result &input = get_input();
float *pixel = nullptr;
void *pixel = nullptr;
bool need_to_free_pixel = false;
if (context().use_gpu()) {
/* Make sure any prior writes to the texture are reflected before downloading it. */
GPU_memory_barrier(GPU_BARRIER_TEXTURE_UPDATE);
pixel = static_cast<float *>(GPU_texture_read(input, GPU_DATA_FLOAT, 0));
pixel = GPU_texture_read(input, GPU_DATA_FLOAT, 0);
need_to_free_pixel = true;
}
else {
@ -45,13 +45,16 @@ void ReduceToSingleValueOperation::execute()
result.allocate_single_value();
switch (result.type()) {
case ResultType::Color:
result.set_single_value(float4(pixel));
result.set_single_value(float4(static_cast<float *>(pixel)));
break;
case ResultType::Vector:
result.set_single_value(float4(pixel));
result.set_single_value(float4(static_cast<float *>(pixel)));
break;
case ResultType::Float:
result.set_single_value(*pixel);
result.set_single_value(*static_cast<float *>(pixel));
break;
case ResultType::Int:
result.set_single_value(*static_cast<int *>(pixel));
break;
case ResultType::Float2:
case ResultType::Float3:

View file

@ -45,6 +45,8 @@ eGPUTextureFormat Result::gpu_texture_format(ResultType type, ResultPrecision pr
return GPU_RG16F;
case ResultType::Float3:
return GPU_RGB16F;
case ResultType::Int:
return GPU_R16I;
case ResultType::Int2:
return GPU_RG16I;
}
@ -60,6 +62,8 @@ eGPUTextureFormat Result::gpu_texture_format(ResultType type, ResultPrecision pr
return GPU_RG32F;
case ResultType::Float3:
return GPU_RGB32F;
case ResultType::Int:
return GPU_R32I;
case ResultType::Int2:
return GPU_RG32I;
}
@ -80,6 +84,7 @@ eGPUTextureFormat Result::gpu_texture_format(eGPUTextureFormat format, ResultPre
case GPU_RG16F:
case GPU_RGB16F:
case GPU_RGBA16F:
case GPU_R16I:
case GPU_RG16I:
return format;
@ -91,6 +96,8 @@ eGPUTextureFormat Result::gpu_texture_format(eGPUTextureFormat format, ResultPre
return GPU_RGB16F;
case GPU_RGBA32F:
return GPU_RGBA16F;
case GPU_R32I:
return GPU_R16I;
case GPU_RG32I:
return GPU_RG16I;
default:
@ -104,6 +111,7 @@ eGPUTextureFormat Result::gpu_texture_format(eGPUTextureFormat format, ResultPre
case GPU_RG32F:
case GPU_RGB32F:
case GPU_RGBA32F:
case GPU_R32I:
case GPU_RG32I:
return format;
@ -115,6 +123,8 @@ eGPUTextureFormat Result::gpu_texture_format(eGPUTextureFormat format, ResultPre
return GPU_RGB32F;
case GPU_RGBA16F:
return GPU_RGBA32F;
case GPU_R16I:
return GPU_R32I;
case GPU_RG16I:
return GPU_RG32I;
default:
@ -134,12 +144,14 @@ ResultPrecision Result::precision(eGPUTextureFormat format)
case GPU_RG16F:
case GPU_RGB16F:
case GPU_RGBA16F:
case GPU_R16I:
case GPU_RG16I:
return ResultPrecision::Half;
case GPU_R32F:
case GPU_RG32F:
case GPU_RGB32F:
case GPU_RGBA32F:
case GPU_R32I:
case GPU_RG32I:
return ResultPrecision::Full;
default:
@ -165,6 +177,9 @@ ResultType Result::type(eGPUTextureFormat format)
case GPU_RGBA16F:
case GPU_RGBA32F:
return ResultType::Color;
case GPU_R16I:
case GPU_R32I:
return ResultType::Int;
case GPU_RG16I:
case GPU_RG32I:
return ResultType::Int2;
@ -249,6 +264,9 @@ void Result::allocate_invalid()
case ResultType::Float3:
set_single_value(float3(0.0f));
break;
case ResultType::Int:
set_single_value(0);
break;
case ResultType::Int2:
set_single_value(int2(0));
break;
@ -553,6 +571,7 @@ void Result::allocate_data(int2 size, bool from_pool)
int64_t(size.x) * int64_t(size.y), this->channels_count() * sizeof(float), __func__));
storage_type_ = ResultStorageType::FloatCPU;
break;
case ResultType::Int:
case ResultType::Int2:
integer_texture_ = static_cast<int *>(MEM_malloc_arrayN(
int64_t(size.x) * int64_t(size.y), this->channels_count() * sizeof(int), __func__));

View file

@ -14,6 +14,7 @@
#include "COM_shader_node.hh"
#include "COM_utilities.hh"
#include "COM_utilities_type_conversion.hh"
namespace blender::compositor {
@ -69,6 +70,9 @@ static eGPUType gpu_type_from_socket_type(eNodeSocketDatatype type)
switch (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_VECTOR:
return GPU_VEC3;
case SOCK_RGBA:
@ -84,63 +88,100 @@ static eGPUType gpu_type_from_socket_type(eNodeSocketDatatype type)
* conversion if needed. */
static void gpu_stack_vector_from_socket(GPUNodeStack &stack, const bNodeSocket *socket)
{
switch (socket->type) {
const eNodeSocketDatatype input_type = static_cast<eNodeSocketDatatype>(socket->type);
const eNodeSocketDatatype expected_type = static_cast<eNodeSocketDatatype>(stack.sockettype);
switch (input_type) {
case SOCK_FLOAT: {
const float value = socket->default_value_typed<bNodeSocketValueFloat>()->value;
switch (stack.sockettype) {
switch (expected_type) {
case SOCK_FLOAT:
stack.vec[0] = value;
return;
case SOCK_INT:
/* GPUMaterial doesn't support int, so it is passed as a float. */
stack.vec[0] = float(float_to_int(value));
return;
case SOCK_VECTOR:
copy_v3_fl(stack.vec, value);
copy_v4_v4(stack.vec, float_to_vector(value));
return;
case SOCK_RGBA:
copy_v4_fl(stack.vec, value);
stack.vec[3] = 1.0f;
copy_v4_v4(stack.vec, float_to_color(value));
return;
default:
BLI_assert_unreachable();
return;
break;
}
break;
}
case SOCK_INT: {
const int value = socket->default_value_typed<bNodeSocketValueInt>()->value;
switch (expected_type) {
case SOCK_FLOAT:
stack.vec[0] = int_to_float(value);
return;
case SOCK_INT:
/* GPUMaterial doesn't support int, so it is passed as a float. */
stack.vec[0] = float(value);
return;
case SOCK_VECTOR:
copy_v4_v4(stack.vec, int_to_vector(value));
return;
case SOCK_RGBA:
copy_v4_v4(stack.vec, int_to_color(value));
return;
default:
break;
}
break;
}
case SOCK_VECTOR: {
const float *value = socket->default_value_typed<bNodeSocketValueVector>()->value;
switch (stack.sockettype) {
const float4 value = float4(
float3(socket->default_value_typed<bNodeSocketValueVector>()->value), 0.0f);
switch (expected_type) {
case SOCK_FLOAT:
stack.vec[0] = (value[0] + value[1] + value[2]) / 3.0f;
stack.vec[0] = vector_to_float(value);
return;
case SOCK_INT:
/* GPUMaterial doesn't support int, so it is passed as a float. */
stack.vec[0] = float(vector_to_int(value));
return;
case SOCK_VECTOR:
copy_v3_v3(stack.vec, value);
return;
case SOCK_RGBA:
copy_v3_v3(stack.vec, value);
stack.vec[3] = 1.0f;
copy_v4_v4(stack.vec, vector_to_color(value));
return;
default:
BLI_assert_unreachable();
return;
break;
}
break;
}
case SOCK_RGBA: {
const float *value = socket->default_value_typed<bNodeSocketValueRGBA>()->value;
switch (stack.sockettype) {
const float4 value = socket->default_value_typed<bNodeSocketValueRGBA>()->value;
switch (expected_type) {
case SOCK_FLOAT:
stack.vec[0] = (value[0] + value[1] + value[2]) / 3.0f;
stack.vec[0] = color_to_float(value);
return;
case SOCK_INT:
/* GPUMaterial doesn't support int, so it is passed as a float. */
stack.vec[0] = float(color_to_int(value));
return;
case SOCK_VECTOR:
copy_v3_v3(stack.vec, value);
copy_v4_v4(stack.vec, color_to_vector(value));
return;
case SOCK_RGBA:
copy_v4_v4(stack.vec, value);
return;
default:
BLI_assert_unreachable();
return;
break;
}
break;
}
default:
BLI_assert_unreachable();
break;
}
BLI_assert_unreachable();
}
static void populate_gpu_node_stack(DSocket socket, GPUNodeStack &stack)

View file

@ -204,6 +204,9 @@ static const char *get_set_function_name(ResultType type)
switch (type) {
case ResultType::Float:
return "set_value";
case ResultType::Int:
/* GPUMaterial doesn't support int, so it is passed as a float. */
return "set_value";
case ResultType::Vector:
return "set_rgb";
case ResultType::Color:
@ -286,6 +289,8 @@ static const char *get_store_function_name(ResultType type)
switch (type) {
case ResultType::Float:
return "node_compositor_store_output_float";
case ResultType::Int:
return "node_compositor_store_output_int";
case ResultType::Vector:
return "node_compositor_store_output_vector";
case ResultType::Color:
@ -376,13 +381,17 @@ void ShaderOperation::generate_code(void *thunk,
shader_create_info.compute_source_generated += "}\n";
}
/* Texture storers in the shader always take a vec4 as an argument, so encode each type in a vec4
* appropriately. */
/* Texture storers in the shader always take a [i]vec4 as an argument, so encode each type in an
* [i]vec4 appropriately. */
static const char *glsl_store_expression_from_result_type(ResultType type)
{
switch (type) {
case ResultType::Float:
return "vec4(value)";
case ResultType::Int:
/* GPUMaterial doesn't support int, so it is passed as a float, and we need to convert it
* back to int before writing it. */
return "ivec4(int(value))";
case ResultType::Vector:
return "vec4(vector, 0.0)";
case ResultType::Color:
@ -398,18 +407,41 @@ static const char *glsl_store_expression_from_result_type(ResultType type)
return nullptr;
}
static ImageType gpu_image_type_from_result_type(const ResultType type)
{
switch (type) {
case ResultType::Float:
case ResultType::Vector:
case ResultType::Color:
return ImageType::FLOAT_2D;
case ResultType::Int:
return ImageType::INT_2D;
case ResultType::Float2:
case ResultType::Float3:
case ResultType::Int2:
/* Those types are internal and needn't be handled by operations. */
break;
}
BLI_assert_unreachable();
return ImageType::FLOAT_2D;
}
void ShaderOperation::generate_code_for_outputs(ShaderCreateInfo &shader_create_info)
{
const std::string store_float_function_header = "void store_float(const uint id, float value)";
/* GPUMaterial doesn't support int, so it is passed as a float. */
const std::string store_int_function_header = "void store_int(const uint id, float value)";
const std::string store_vector_function_header = "void store_vector(const uint id, vec3 vector)";
const std::string store_color_function_header = "void store_color(const uint id, vec4 color)";
/* The store functions are used by the node_compositor_store_output_[float|vector|color]
/* The store functions are used by the node_compositor_store_output_[float|int|vector|color]
* functions but are only defined later as part of the compute source, so they need to be forward
* declared.
* NOTE(Metal): Metal does not require forward declarations. */
if (GPU_backend_get_type() != GPU_BACKEND_METAL) {
shader_create_info.typedef_source_generated += store_float_function_header + ";\n";
shader_create_info.typedef_source_generated += store_int_function_header + ";\n";
shader_create_info.typedef_source_generated += store_vector_function_header + ";\n";
shader_create_info.typedef_source_generated += store_color_function_header + ";\n";
}
@ -418,10 +450,12 @@ void ShaderOperation::generate_code_for_outputs(ShaderCreateInfo &shader_create_
* opening the function with a curly bracket followed by opening a switch statement in each of
* the functions. */
std::stringstream store_float_function;
std::stringstream store_int_function;
std::stringstream store_vector_function;
std::stringstream store_color_function;
const std::string store_function_start = "\n{\n switch (id) {\n";
store_float_function << store_float_function_header << store_function_start;
store_int_function << store_int_function_header << store_function_start;
store_vector_function << store_vector_function_header << store_function_start;
store_color_function << store_color_function_header << store_function_start;
@ -432,7 +466,7 @@ void ShaderOperation::generate_code_for_outputs(ShaderCreateInfo &shader_create_
shader_create_info.image(0,
result.get_gpu_texture_format(),
Qualifier::WRITE,
ImageType::FLOAT_2D,
gpu_image_type_from_result_type(result.type()),
output_identifier,
Frequency::PASS);
@ -449,6 +483,9 @@ void ShaderOperation::generate_code_for_outputs(ShaderCreateInfo &shader_create_
case ResultType::Float:
store_float_function << case_code.str();
break;
case ResultType::Int:
store_int_function << case_code.str();
break;
case ResultType::Vector:
store_vector_function << case_code.str();
break;
@ -467,10 +504,12 @@ void ShaderOperation::generate_code_for_outputs(ShaderCreateInfo &shader_create_
/* Close the previously opened switch statement as well as the function itself. */
const std::string store_function_end = " }\n}\n\n";
store_float_function << store_function_end;
store_int_function << store_function_end;
store_vector_function << store_function_end;
store_color_function << store_function_end;
shader_create_info.compute_source_generated += store_float_function.str() +
store_int_function.str() +
store_vector_function.str() +
store_color_function.str();
}
@ -480,6 +519,9 @@ static const char *glsl_type_from_result_type(ResultType type)
switch (type) {
case ResultType::Float:
return "float";
case ResultType::Int:
/* GPUMaterial doesn't support int, so it is passed as a float. */
return "float";
case ResultType::Vector:
return "vec3";
case ResultType::Color:
@ -495,12 +537,13 @@ static const char *glsl_type_from_result_type(ResultType type)
return nullptr;
}
/* Texture loaders in the shader always return a vec4, so a swizzle is needed to retrieve the
/* Texture loaders in the shader always return an [i]vec4, so a swizzle is needed to retrieve the
* actual value for each type. */
static const char *glsl_swizzle_from_result_type(ResultType type)
{
switch (type) {
case ResultType::Float:
case ResultType::Int:
return "x";
case ResultType::Vector:
return "xyz";
@ -529,7 +572,11 @@ void ShaderOperation::generate_code_for_inputs(GPUMaterial *material,
/* Add a texture sampler for each of the inputs with the same name as the attribute. */
LISTBASE_FOREACH (GPUMaterialAttribute *, attribute, &attributes) {
shader_create_info.sampler(0, ImageType::FLOAT_2D, attribute->name, Frequency::PASS);
const InputDescriptor &input_descriptor = get_input_descriptor(attribute->name);
shader_create_info.sampler(0,
gpu_image_type_from_result_type(input_descriptor.type),
attribute->name,
Frequency::PASS);
}
/* Declare a struct called var_attrs that includes an appropriately typed member for each of the
@ -551,14 +598,16 @@ void ShaderOperation::generate_code_for_inputs(GPUMaterial *material,
shader_create_info.typedef_source("gpu_shader_compositor_texture_utilities.glsl");
/* Initialize each member of the previously declared struct by loading its corresponding texture
* with an appropriate swizzle for its type. */
* with an appropriate swizzle and cast for its type. */
std::stringstream initialize_attributes;
LISTBASE_FOREACH (GPUMaterialAttribute *, attribute, &attributes) {
const InputDescriptor &input_descriptor = get_input_descriptor(attribute->name);
const std::string swizzle = glsl_swizzle_from_result_type(input_descriptor.type);
initialize_attributes << "var_attrs.v" << attribute->id << " = "
const std::string type = glsl_type_from_result_type(input_descriptor.type);
initialize_attributes << "var_attrs.v" << attribute->id << " = " << type << "("
<< "texture_load(" << attribute->name
<< ", ivec2(gl_GlobalInvocationID.xy))." << swizzle << ";\n";
<< ", ivec2(gl_GlobalInvocationID.xy))." << swizzle << ")"
<< ";\n";
}
initialize_attributes << "\n";

View file

@ -58,6 +58,8 @@ ResultType get_node_socket_result_type(const bNodeSocket *socket)
switch (socket->type) {
case SOCK_FLOAT:
return ResultType::Float;
case SOCK_INT:
return ResultType::Int;
case SOCK_VECTOR:
return ResultType::Vector;
case SOCK_RGBA:

View file

@ -7,6 +7,5 @@
void main()
{
ivec2 texel = ivec2(gl_GlobalInvocationID.xy);
vec4 value = texture_load(input_tx, texel);
imageStore(output_img, texel, CONVERT_EXPRESSION(value));
imageStore(output_img, texel, CONVERT_EXPRESSION(texture_load(input_tx, texel)));
}

View file

@ -6,76 +6,126 @@
GPU_SHADER_CREATE_INFO(compositor_convert_shared)
LOCAL_GROUP_SIZE(16, 16)
SAMPLER(0, FLOAT_2D, input_tx)
TYPEDEF_SOURCE("gpu_shader_compositor_type_conversion.glsl")
COMPUTE_SOURCE("compositor_convert.glsl")
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_float_to_float)
GPU_SHADER_CREATE_INFO(compositor_convert_float_shared)
ADDITIONAL_INFO(compositor_convert_shared)
IMAGE(0, GPU_R16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "value")
SAMPLER(0, FLOAT_2D, input_tx)
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_int_shared)
ADDITIONAL_INFO(compositor_convert_shared)
SAMPLER(0, INT_2D, input_tx)
GPU_SHADER_CREATE_END()
/* --------------------------------------------------------------------
* Float to other.
*/
GPU_SHADER_CREATE_INFO(compositor_convert_float_to_int)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_R16I, WRITE, INT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "ivec4(float_to_int(value.x), ivec3(0))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_float_to_vector)
ADDITIONAL_INFO(compositor_convert_shared)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_RGBA16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(vec3_from_float(value.x), 1.0)")
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(float_to_vector(value.x))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_float_to_color)
ADDITIONAL_INFO(compositor_convert_shared)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_RGBA16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4_from_float(value.x)")
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(float_to_color(value.x))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_color_to_float)
ADDITIONAL_INFO(compositor_convert_shared)
/* --------------------------------------------------------------------
* Int to other.
*/
GPU_SHADER_CREATE_INFO(compositor_convert_int_to_float)
ADDITIONAL_INFO(compositor_convert_int_shared)
IMAGE(0, GPU_R16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(float_from_vec4(value), vec3(0.0))")
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(int_to_float(value.x), vec3(0.0))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_color_to_vector)
ADDITIONAL_INFO(compositor_convert_shared)
GPU_SHADER_CREATE_INFO(compositor_convert_int_to_vector)
ADDITIONAL_INFO(compositor_convert_int_shared)
IMAGE(0, GPU_RGBA16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "value")
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(int_to_vector(value.x))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_color_to_color)
ADDITIONAL_INFO(compositor_convert_shared)
GPU_SHADER_CREATE_INFO(compositor_convert_int_to_color)
ADDITIONAL_INFO(compositor_convert_int_shared)
IMAGE(0, GPU_RGBA16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "value")
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(int_to_color(value.x))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
/* --------------------------------------------------------------------
* Vector to other.
*/
GPU_SHADER_CREATE_INFO(compositor_convert_vector_to_float)
ADDITIONAL_INFO(compositor_convert_shared)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_R16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(float_from_vec3(value.xyz), vec3(0.0))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_vector_to_vector)
ADDITIONAL_INFO(compositor_convert_shared)
IMAGE(0, GPU_RGBA16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "value")
GPU_SHADER_CREATE_INFO(compositor_convert_vector_to_int)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_R16I, WRITE, INT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "ivec4(vector_to_int(value), ivec3(0.0))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_vector_to_color)
ADDITIONAL_INFO(compositor_convert_shared)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_RGBA16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4_from_vec3(value.xyz)")
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(vector_to_color(value))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
/* --------------------------------------------------------------------
* Color to other.
*/
GPU_SHADER_CREATE_INFO(compositor_convert_color_to_float)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_R16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(color_to_float(value), vec3(0.0))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_color_to_int)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_R16I, WRITE, INT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "ivec4(color_to_int(value), ivec3(0))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
GPU_SHADER_CREATE_INFO(compositor_convert_color_to_vector)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_RGBA16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(color_to_vector(value))")
DO_STATIC_COMPILATION()
GPU_SHADER_CREATE_END()
/* --------------------------------------------------------------------
* Color to channel.
*/
GPU_SHADER_CREATE_INFO(compositor_convert_color_to_alpha)
ADDITIONAL_INFO(compositor_convert_shared)
ADDITIONAL_INFO(compositor_convert_float_shared)
IMAGE(0, GPU_R16F, WRITE, FLOAT_2D, output_img)
DEFINE_VALUE("CONVERT_EXPRESSION(value)", "vec4(value.a)")
DO_STATIC_COMPILATION()

View file

@ -17,6 +17,13 @@ void node_compositor_store_output_float(const float id, float value, out float o
out_value = value;
}
/* GPUMaterial doesn't support int, so it is passed as a float. */
void node_compositor_store_output_int(const float id, float value, out float out_value)
{
store_int(floatBitsToUint(id), value);
out_value = value;
}
void node_compositor_store_output_vector(const float id, vec3 vector, out vec3 out_vector)
{
store_vector(floatBitsToUint(id), vector);

View file

@ -2,6 +2,88 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/* --------------------------------------------------------------------
* Float to other.
*/
int float_to_int(float value)
{
return int(value);
}
vec4 float_to_vector(float value)
{
return vec4(vec3(value), 1.0);
}
vec4 float_to_color(float value)
{
return vec4(vec3(value), 1.0);
}
/* --------------------------------------------------------------------
* Int to other.
*/
float int_to_float(int value)
{
return float(value);
}
vec4 int_to_vector(int value)
{
return float_to_vector(int_to_float(value));
}
vec4 int_to_color(int value)
{
return float_to_color(int_to_float(value));
}
/* --------------------------------------------------------------------
* Vector to other.
*/
float vector_to_float(vec4 value)
{
return dot(value.xyz, vec3(1.0)) / 3.0;
}
int vector_to_int(vec4 value)
{
return float_to_int(vector_to_float(value));
}
vec4 vector_to_color(vec4 value)
{
return vec4(value.xyz, 1.0);
}
/* --------------------------------------------------------------------
* Color to other.
*/
float color_to_float(vec4 value)
{
return dot(value.rgb, vec3(1.0)) / 3.0;
}
int color_to_int(vec4 value)
{
return float_to_int(color_to_float(value));
}
vec4 color_to_vector(vec4 value)
{
return value;
}
/* --------------------------------------------------------------------
* GPUMatrial-specific implicit conversion functions.
*
* Those should have the same interface and names as the macros in gpu_shader_codegen_lib.glsl
* since the GPUMaterial compiler inserts those hard coded names. */
float float_from_vec4(vec4 vector)
{
return dot(vector.rgb, vec3(1.0)) / 3.0;

View file

@ -9,34 +9,80 @@
namespace blender::compositor {
inline float4 float_to_vector(const float &input)
/* --------------------------------------------------------------------
* Float to other.
*/
inline int float_to_int(const float &value)
{
return float4(float3(input), 1.0f);
return int(value);
}
inline float4 float_to_color(const float &input)
inline float4 float_to_vector(const float &value)
{
return float4(float3(input), 1.0f);
return float4(float3(value), 1.0f);
}
inline float vector_to_float(const float4 &input)
inline float4 float_to_color(const float &value)
{
return math::reduce_add(input.xyz()) / 3.0f;
return float4(float3(value), 1.0f);
}
inline float4 vector_to_color(const float4 &input)
/* --------------------------------------------------------------------
* Int to other.
*/
inline float int_to_float(const int &value)
{
return float4(input.xyz(), 1.0f);
return float(value);
}
inline float color_to_float(const float4 &input)
inline float4 int_to_vector(const int &value)
{
return math::reduce_add(input.xyz()) / 3.0f;
return float_to_vector(int_to_float(value));
}
inline float4 color_to_vector(const float4 &input)
inline float4 int_to_color(const int &value)
{
return input;
return float_to_color(int_to_float(value));
}
/* --------------------------------------------------------------------
* Vector to other.
*/
inline float vector_to_float(const float4 &value)
{
return math::reduce_add(value.xyz()) / 3.0f;
}
inline int vector_to_int(const float4 &value)
{
return float_to_int(vector_to_float(value));
}
inline float4 vector_to_color(const float4 &value)
{
return float4(value.xyz(), 1.0f);
}
/* --------------------------------------------------------------------
* Vector to other.
*/
inline float color_to_float(const float4 &value)
{
return math::reduce_add(value.xyz()) / 3.0f;
}
inline int color_to_int(const float4 &value)
{
return float_to_int(color_to_float(value));
}
inline float4 color_to_vector(const float4 &value)
{
return value;
}
} // namespace blender::compositor

View file

@ -143,7 +143,7 @@ static bool composite_node_tree_socket_type_valid(blender::bke::bNodeTreeType *
blender::bke::bNodeSocketType *socket_type)
{
return blender::bke::node_is_static_socket_type(socket_type) &&
ELEM(socket_type->type, SOCK_FLOAT, SOCK_VECTOR, SOCK_RGBA);
ELEM(socket_type->type, SOCK_FLOAT, SOCK_INT, SOCK_VECTOR, SOCK_RGBA);
}
blender::bke::bNodeTreeType *ntreeType_Composite;

View file

@ -446,7 +446,7 @@ class CompositorNodeGroupInterfaceTest(AbstractNodeGroupInterfaceTest, NodeGroup
self.do_test_socket_type("NodeSocketFloat")
self.do_test_invalid_socket_type("NodeSocketGeometry")
self.do_test_invalid_socket_type("NodeSocketImage")
self.do_test_invalid_socket_type("NodeSocketInt")
self.do_test_socket_type("NodeSocketInt")
self.do_test_invalid_socket_type("NodeSocketMaterial")
self.do_test_invalid_socket_type("NodeSocketObject")
self.do_test_invalid_socket_type("NodeSocketRotation")