Geometry Nodes: improve internal bundle api

This extends the API for bundles in two main ways:
* Adds support for working with paths to reference nested bundles directly.
* Adds support for typed add/lookup, also taking into account implicit conversions.

Some unit tests have been added as well.

Pull Request: https://projects.blender.org/blender/blender/pulls/142942
This commit is contained in:
Jacques Lucke
2025-07-23 11:23:32 +02:00
parent 665aff0797
commit 2e48d33101
9 changed files with 487 additions and 49 deletions

View File

@@ -4883,6 +4883,25 @@ std::optional<eNodeSocketDatatype> geo_nodes_base_cpp_type_to_socket_type(const
if (type.is<nodes::ClosurePtr>()) {
return SOCK_CLOSURE;
}
if (type.is<GeometrySet>()) {
return SOCK_GEOMETRY;
}
if (type.is<Material *>()) {
return SOCK_MATERIAL;
}
if (type.is<Tex *>()) {
return SOCK_TEXTURE;
}
if (type.is<Object *>()) {
return SOCK_OBJECT;
}
if (type.is<Collection *>()) {
return SOCK_COLLECTION;
}
if (type.is<Image *>()) {
return SOCK_IMAGE;
}
return std::nullopt;
}

View File

@@ -240,6 +240,7 @@ if(WITH_GTESTS)
)
set(TEST_SRC
intern/node_iterator_tests.cc
intern/geometry_nodes_bundle_tests.cc
)
set(TEST_LIB
bf_nodes

View File

@@ -24,6 +24,7 @@
#include "NOD_derived_node_tree.hh"
#include "NOD_geometry_nodes_lazy_function.hh"
#include "NOD_geometry_nodes_values.hh"
namespace blender::nodes {
@@ -96,22 +97,6 @@ class GeoNodeExecParams {
{
}
template<typename T>
static constexpr bool is_field_base_type_v = is_same_any_v<T,
float,
int,
bool,
ColorGeometry4f,
float3,
std::string,
math::Quaternion,
float4x4>;
template<typename T>
static constexpr bool stored_as_SocketValueVariant_v =
is_field_base_type_v<T> || fn::is_field_v<T> || bke::is_VolumeGrid_v<T> ||
is_same_any_v<T, GField, bke::GVolumeGrid, nodes::BundlePtr, nodes::ClosurePtr>;
/**
* Get the input value for the input socket with the given identifier.
*
@@ -122,7 +107,7 @@ class GeoNodeExecParams {
if constexpr (std::is_enum_v<T>) {
return T(this->extract_input<int>(identifier));
}
else if constexpr (stored_as_SocketValueVariant_v<T>) {
else if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v<T>) {
SocketValueVariant value_variant = this->extract_input<SocketValueVariant>(identifier);
return value_variant.extract<T>();
}
@@ -154,7 +139,7 @@ class GeoNodeExecParams {
if constexpr (std::is_enum_v<T>) {
return T(this->get_input<int>(identifier));
}
else if constexpr (stored_as_SocketValueVariant_v<T>) {
else if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v<T>) {
auto value_variant = this->get_input<SocketValueVariant>(identifier);
return value_variant.extract<T>();
}
@@ -191,7 +176,7 @@ class GeoNodeExecParams {
template<typename T> void set_output(StringRef identifier, T &&value)
{
using StoredT = std::decay_t<T>;
if constexpr (stored_as_SocketValueVariant_v<StoredT>) {
if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v<StoredT>) {
this->set_output(identifier, SocketValueVariant::From(std::forward<T>(value)));
}
else {

View File

@@ -6,7 +6,9 @@
#include "BKE_node.hh"
#include "BKE_node_socket_value.hh"
#include "NOD_geometry_nodes_bundle_fwd.hh"
#include "NOD_geometry_nodes_values.hh"
#include "NOD_socket_interface_key.hh"
#include "DNA_node_types.h"
@@ -16,6 +18,8 @@ namespace blender::nodes {
/**
* A bundle is a map containing keys and their corresponding values. Values are stored as the type
* they have in Geometry Nodes (#bNodeSocketType::geometry_nodes_cpp_type).
*
* The API also supports working with paths in nested bundles like `root/child/data`.
*/
class Bundle : public ImplicitSharingMixin {
public:
@@ -33,6 +37,12 @@ class Bundle : public ImplicitSharingMixin {
struct Item {
const bke::bNodeSocketType *type;
const void *value;
/**
* Attempts to cast the stored value to the given type. This may do implicit conversions.
*/
template<typename T> std::optional<T> as(const bke::bNodeSocketType &socket_type) const;
template<typename T> std::optional<T> as() const;
};
Bundle();
@@ -42,25 +52,183 @@ class Bundle : public ImplicitSharingMixin {
Bundle &operator=(Bundle &&other) noexcept;
~Bundle();
static BundlePtr create()
{
return BundlePtr(MEM_new<Bundle>(__func__));
}
static BundlePtr create();
void add_new(SocketInterfaceKey key, const bke::bNodeSocketType &type, const void *value);
bool add(const SocketInterfaceKey &key, const bke::bNodeSocketType &type, const void *value);
bool add(SocketInterfaceKey &&key, const bke::bNodeSocketType &type, const void *value);
void add_new(SocketInterfaceKey key, const bke::bNodeSocketType &type, const void *value);
void add_override(const SocketInterfaceKey &key,
const bke::bNodeSocketType &type,
const void *value);
bool add_path(StringRef path, const bke::bNodeSocketType &type, const void *value);
void add_path_new(StringRef path, const bke::bNodeSocketType &type, const void *value);
void add_path_override(StringRef path, const bke::bNodeSocketType &type, const void *value);
template<typename T> void add(const SocketInterfaceKey &key, T value);
template<typename T> void add_override(const SocketInterfaceKey &key, T value);
template<typename T> void add_path(StringRef path, T value);
template<typename T> void add_path_override(StringRef path, T value);
bool remove(const SocketInterfaceKey &key);
bool contains(const SocketInterfaceKey &key) const;
bool contains_path(StringRef path) const;
std::optional<Item> lookup(const SocketInterfaceKey &key) const;
std::optional<Item> lookup_path(Span<StringRef> path) const;
std::optional<Item> lookup_path(StringRef path) const;
template<typename T> std::optional<T> lookup(const SocketInterfaceKey &key) const;
template<typename T> std::optional<T> lookup_path(StringRef path) const;
Span<StoredItem> items() const
{
return items_;
}
bool is_empty() const;
int64_t size() const;
Span<StoredItem> items() const;
BundlePtr copy() const;
void delete_self() override;
};
template<typename T>
inline std::optional<T> Bundle::Item::as(const bke::bNodeSocketType &socket_type) const
{
if (!this->value || !this->type) {
return std::nullopt;
}
const void *converted_value = this->value;
BUFFER_FOR_CPP_TYPE_VALUE(*socket_type.geometry_nodes_cpp_type, buffer);
if (this->type != &socket_type) {
if (!implicitly_convert_socket_value(*this->type, this->value, socket_type, buffer)) {
return std::nullopt;
}
converted_value = buffer;
}
if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v<T>) {
const auto &value_variant = *static_cast<const bke::SocketValueVariant *>(converted_value);
return value_variant.get<T>();
}
return *static_cast<const T *>(converted_value);
}
template<typename T> constexpr bool is_valid_static_bundle_item_type()
{
if (geo_nodes_is_field_base_type_v<T>) {
return true;
}
if constexpr (fn::is_field_v<T>) {
return geo_nodes_is_field_base_type_v<typename T::base_type>;
}
if constexpr (is_same_any_v<T, BundlePtr, ClosurePtr>) {
return true;
}
return !geo_nodes_type_stored_as_SocketValueVariant_v<T>;
}
template<typename T> inline const bke::bNodeSocketType *socket_type_info_by_static_type()
{
if constexpr (fn::is_field_v<T>) {
if constexpr (geo_nodes_is_field_base_type_v<typename T::base_type>) {
const std::optional<eNodeSocketDatatype> socket_type =
bke::geo_nodes_base_cpp_type_to_socket_type(CPPType::get<typename T::base_type>());
BLI_assert(socket_type);
const bke::bNodeSocketType *socket_type_info = bke::node_socket_type_find_static(
*socket_type);
BLI_assert(socket_type_info);
return socket_type_info;
}
}
const std::optional<eNodeSocketDatatype> socket_type =
bke::geo_nodes_base_cpp_type_to_socket_type(CPPType::get<T>());
if (!socket_type) {
return nullptr;
}
return bke::node_socket_type_find_static(*socket_type);
}
template<typename T> inline std::optional<T> Bundle::Item::as() const
{
static_assert(is_valid_static_bundle_item_type<T>());
if (const bke::bNodeSocketType *socket_type = socket_type_info_by_static_type<T>()) {
return this->as<T>(*socket_type);
}
/* Can't lookup this type directly currently. */
BLI_assert_unreachable();
return std::nullopt;
}
template<typename T> inline std::optional<T> Bundle::lookup(const SocketInterfaceKey &key) const
{
const std::optional<Item> item = this->lookup(key);
if (!item) {
return std::nullopt;
}
return item->as<T>();
}
template<typename T> inline std::optional<T> Bundle::lookup_path(const StringRef path) const
{
const std::optional<Item> item = this->lookup_path(path);
if (!item) {
return std::nullopt;
}
return item->as<T>();
}
template<typename T, typename Fn> inline void to_stored_type(T &&value, Fn &&fn)
{
using DecayT = std::decay_t<T>;
static_assert(is_valid_static_bundle_item_type<DecayT>());
const bke::bNodeSocketType *socket_type = socket_type_info_by_static_type<DecayT>();
BLI_assert(socket_type);
if constexpr (geo_nodes_type_stored_as_SocketValueVariant_v<DecayT>) {
auto value_variant = bke::SocketValueVariant::From(std::forward<T>(value));
fn(*socket_type, &value_variant);
}
else {
fn(*socket_type, &value);
}
}
template<typename T> inline void Bundle::add(const SocketInterfaceKey &key, T value)
{
to_stored_type(value, [&](const bke::bNodeSocketType &type, const void *value) {
this->add(key, type, value);
});
}
template<typename T> inline void Bundle::add_path(const StringRef path, T value)
{
to_stored_type(value, [&](const bke::bNodeSocketType &type, const void *value) {
this->add_path(path, type, value);
});
}
template<typename T> inline void Bundle::add_override(const SocketInterfaceKey &key, T value)
{
to_stored_type(value, [&](const bke::bNodeSocketType &type, const void *value) {
this->add_override(key, type, value);
});
}
template<typename T> inline void Bundle::add_path_override(const StringRef path, T value)
{
to_stored_type(value, [&](const bke::bNodeSocketType &type, const void *value) {
this->add_path_override(path, type, value);
});
}
inline Span<Bundle::StoredItem> Bundle::items() const
{
return items_;
}
inline bool Bundle::is_empty() const
{
return items_.is_empty();
}
inline int64_t Bundle::size() const
{
return items_.size();
}
} // namespace blender::nodes

View File

@@ -629,23 +629,6 @@ std::string zone_wrapper_output_name(const ZoneBuildInfo &zone_info,
const Span<lf::Output> outputs,
const int lf_socket_i);
/**
* Performs implicit conversion between socket types. Returns false if the conversion is not
* possible. In that case, r_to_value is left uninitialized.
*/
[[nodiscard]] bool implicitly_convert_socket_value(const bke::bNodeSocketType &from_type,
const void *from_value,
const bke::bNodeSocketType &to_type,
void *r_to_value);
/**
* Builds a lazy-function that can convert between socket types. Returns null if the conversion is
* never possible.
*/
const LazyFunction *build_implicit_conversion_lazy_function(const bke::bNodeSocketType &from_type,
const bke::bNodeSocketType &to_type,
ResourceScope &scope);
/**
* Report an error from a multi-function evaluation within a Geometry Nodes evaluation.
*

View File

@@ -0,0 +1,61 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#pragma once
#include "BKE_node.hh"
#include "BKE_volume_grid_fwd.hh"
#include "BLI_color.hh"
#include "BLI_math_quaternion_types.hh"
#include "BLI_math_vector_types.hh"
#include "BLI_memory_utils.hh"
#include "FN_field.hh"
#include "FN_lazy_function.hh"
#include "NOD_geometry_nodes_bundle_fwd.hh"
#include "NOD_geometry_nodes_closure_fwd.hh"
namespace blender::nodes {
/** True if a static type can also exist as field in Geometry Nodes. */
template<typename T>
static constexpr bool geo_nodes_is_field_base_type_v = is_same_any_v<T,
float,
int,
bool,
ColorGeometry4f,
float3,
std::string,
math::Quaternion,
float4x4>;
/** True if Geometry Nodes sockets can store values of the given type and the type is stored
* embedded in a #SocketValueVariant. */
template<typename T>
static constexpr bool geo_nodes_type_stored_as_SocketValueVariant_v =
std::is_enum_v<T> || geo_nodes_is_field_base_type_v<T> || fn::is_field_v<T> ||
bke::is_VolumeGrid_v<T> ||
is_same_any_v<T, fn::GField, bke::GVolumeGrid, nodes::BundlePtr, nodes::ClosurePtr>;
/**
* Performs implicit conversion between socket types. Returns false if the conversion is not
* possible. In that case, r_to_value is left uninitialized.
*/
[[nodiscard]] bool implicitly_convert_socket_value(const bke::bNodeSocketType &from_type,
const void *from_value,
const bke::bNodeSocketType &to_type,
void *r_to_value);
/**
* Builds a lazy-function that can convert between socket types. Returns null if the conversion is
* never possible.
*/
const fn::lazy_function::LazyFunction *build_implicit_conversion_lazy_function(
const bke::bNodeSocketType &from_type,
const bke::bNodeSocketType &to_type,
ResourceScope &scope);
} // namespace blender::nodes

View File

@@ -2,6 +2,7 @@
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BKE_node_socket_value.hh"
#include "BLI_cpp_type.hh"
#include "BKE_node_runtime.hh"
@@ -104,6 +105,11 @@ Bundle::Bundle(Bundle &&other) noexcept
{
}
BundlePtr Bundle::create()
{
return BundlePtr(MEM_new<Bundle>(__func__));
}
Bundle &Bundle::operator=(const Bundle &other)
{
if (this == &other) {
@@ -135,6 +141,14 @@ void Bundle::add_new(SocketInterfaceKey key, const bke::bNodeSocketType &type, c
buffers_.append(buffer);
}
void Bundle::add_override(const SocketInterfaceKey &key,
const bke::bNodeSocketType &type,
const void *value)
{
this->remove(key);
this->add_new(key, type, value);
}
bool Bundle::add(const SocketInterfaceKey &key,
const bke::bNodeSocketType &type,
const void *value)
@@ -146,15 +160,56 @@ bool Bundle::add(const SocketInterfaceKey &key,
return true;
}
bool Bundle::add(SocketInterfaceKey &&key, const bke::bNodeSocketType &type, const void *value)
void Bundle::add_path_override(const StringRef path,
const bke::bNodeSocketType &type,
const void *value)
{
if (this->contains(key)) {
BLI_assert(!path.is_empty());
BLI_assert(!path.endswith("/"));
BLI_assert(this->is_mutable());
const int sep = path.find_first_of('/');
if (sep == StringRef::not_found) {
this->remove(SocketInterfaceKey{path});
this->add_new(SocketInterfaceKey{path}, type, value);
return;
}
const StringRef first_part = path.substr(0, sep);
BundlePtr child_bundle;
const std::optional<Bundle::Item> item = this->lookup(SocketInterfaceKey{first_part});
if (item && item->type->type == SOCK_BUNDLE) {
child_bundle = static_cast<const bke::SocketValueVariant *>(item->value)->get<BundlePtr>();
}
if (!child_bundle) {
child_bundle = Bundle::create();
}
this->remove(SocketInterfaceKey{first_part});
if (!child_bundle->is_mutable()) {
child_bundle = child_bundle->copy();
}
child_bundle->tag_ensured_mutable();
const_cast<Bundle &>(*child_bundle).add_path_override(path.substr(sep + 1), type, value);
bke::SocketValueVariant child_bundle_value = bke::SocketValueVariant::From(
std::move(child_bundle));
this->add(SocketInterfaceKey{first_part},
*bke::node_socket_type_find_static(SOCK_BUNDLE),
&child_bundle_value);
}
bool Bundle::add_path(StringRef path, const bke::bNodeSocketType &type, const void *value)
{
if (this->contains_path(path)) {
return false;
}
this->add_new(std::move(key), type, value);
this->add_path_new(path, type, value);
return true;
}
void Bundle::add_path_new(StringRef path, const bke::bNodeSocketType &type, const void *value)
{
BLI_assert(!this->contains_path(path));
this->add_path_override(path, type, value);
}
std::optional<Bundle::Item> Bundle::lookup(const SocketInterfaceKey &key) const
{
for (const StoredItem &item : items_) {
@@ -165,6 +220,60 @@ std::optional<Bundle::Item> Bundle::lookup(const SocketInterfaceKey &key) const
return std::nullopt;
}
std::optional<Bundle::Item> Bundle::lookup_path(const Span<StringRef> path) const
{
BLI_assert(!path.is_empty());
const StringRef first_elem = path[0];
const std::optional<Bundle::Item> item = this->lookup(SocketInterfaceKey(first_elem));
if (!item) {
return std::nullopt;
}
if (path.size() == 1) {
return item;
}
if (item->type->type != SOCK_BUNDLE) {
return std::nullopt;
}
const BundlePtr child_bundle =
static_cast<const bke::SocketValueVariant *>(item->value)->get<BundlePtr>();
if (!child_bundle) {
return std::nullopt;
}
return child_bundle->lookup_path(path.drop_front(1));
}
static Vector<StringRef> split_path(const StringRef path)
{
Vector<StringRef> path_elems;
StringRef remaining = path;
while (!remaining.is_empty()) {
const int sep = remaining.find_first_of('/');
if (sep == StringRef::not_found) {
path_elems.append(remaining);
break;
}
path_elems.append(remaining.substr(0, sep));
remaining = remaining.substr(sep + 1);
}
return path_elems;
}
std::optional<Bundle::Item> Bundle::lookup_path(const StringRef path) const
{
const Vector<StringRef> path_elems = split_path(path);
return this->lookup_path(path_elems);
}
BundlePtr Bundle::copy() const
{
BundlePtr copy_ptr = Bundle::create();
Bundle &copy = const_cast<Bundle &>(*copy_ptr);
for (const StoredItem &item : items_) {
copy.add_new(item.key, *item.type, item.value);
}
return copy_ptr;
}
bool Bundle::remove(const SocketInterfaceKey &key)
{
const int removed_num = items_.remove_if([&key](StoredItem &item) {
@@ -187,6 +296,11 @@ bool Bundle::contains(const SocketInterfaceKey &key) const
return false;
}
bool Bundle::contains_path(const StringRef path) const
{
return this->lookup_path(path).has_value();
}
void Bundle::delete_self()
{
MEM_delete(this);

View File

@@ -0,0 +1,106 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
#include "CLG_log.h"
#include "BKE_appdir.hh"
#include "BKE_context.hh"
#include "BKE_global.hh"
#include "BKE_idtype.hh"
#include "BKE_main.hh"
#include "BKE_material.hh"
#include "BKE_node.hh"
#include "BKE_scene.hh"
#include "IMB_imbuf.hh"
#include "RNA_define.hh"
#include "NOD_geometry_nodes_bundle.hh"
namespace blender::nodes::tests {
class BundleTest : public ::testing::Test {
protected:
static void SetUpTestSuite()
{
CLG_init();
BKE_idtype_init();
RNA_init();
blender::bke::node_system_init();
BKE_appdir_init();
IMB_init();
BKE_materials_init();
}
static void TearDownTestSuite()
{
BKE_materials_exit();
bke::node_system_exit();
RNA_exit();
BKE_appdir_exit();
IMB_exit();
CLG_exit();
}
};
TEST_F(BundleTest, DefaultBundle)
{
BundlePtr bundle = Bundle::create();
EXPECT_TRUE(bundle);
EXPECT_TRUE(bundle->is_empty());
}
TEST_F(BundleTest, AddItems)
{
BundlePtr bundle_ptr = Bundle::create();
Bundle &bundle = const_cast<Bundle &>(*bundle_ptr);
bundle.add(SocketInterfaceKey{"a"}, 3);
EXPECT_EQ(bundle.size(), 1);
EXPECT_TRUE(bundle.contains(SocketInterfaceKey{"a"}));
EXPECT_EQ(bundle.lookup<int>(SocketInterfaceKey{"a"}), 3);
}
TEST_F(BundleTest, AddLookupPath)
{
BundlePtr bundle_ptr = Bundle::create();
Bundle &bundle = const_cast<Bundle &>(*bundle_ptr);
bundle.add_path("a/b/c", 3);
bundle.add_path("a/b/d", 4);
EXPECT_EQ(bundle.size(), 1);
EXPECT_EQ((*bundle.lookup_path<BundlePtr>("a"))->size(), 1);
EXPECT_EQ((*bundle.lookup_path<BundlePtr>("a/b"))->size(), 2);
EXPECT_EQ(bundle.lookup_path<int>("a/b/c"), 3);
EXPECT_EQ(bundle.lookup_path<int>("a/b/d"), 4);
EXPECT_EQ(bundle.lookup_path<BundlePtr>("a/b/c"), std::nullopt);
EXPECT_EQ(bundle.lookup_path<BundlePtr>("a/b/x"), std::nullopt);
}
TEST_F(BundleTest, LookupConversion)
{
BundlePtr bundle_ptr = Bundle::create();
Bundle &bundle = const_cast<Bundle &>(*bundle_ptr);
bundle.add_path("a/b", -3.4f);
EXPECT_EQ(bundle.lookup_path<float>("a/b"), -3.4f);
EXPECT_EQ(bundle.lookup_path<int>("a/b"), -3);
EXPECT_EQ(bundle.lookup_path<bool>("a/b"), false);
EXPECT_EQ(bundle.lookup_path<float3>("a/b"), float3(-3.4f));
EXPECT_EQ(bundle.lookup_path<std::string>("a/b"), std::nullopt);
}
TEST_F(BundleTest, AddOverride)
{
BundlePtr bundle_ptr = Bundle::create();
Bundle &bundle = const_cast<Bundle &>(*bundle_ptr);
bundle.add_path("a/b", 4);
EXPECT_EQ(bundle.lookup_path<int>("a/b"), 4);
bundle.add_path_override("a/b", 10);
EXPECT_EQ(bundle.lookup_path<int>("a/b"), 10);
bundle.add_path("a/b", 15);
EXPECT_EQ(bundle.lookup_path<int>("a/b"), 10);
}
} // namespace blender::nodes::tests

View File

@@ -15,6 +15,7 @@
#include "NOD_geo_closure.hh"
#include "NOD_geometry_nodes_closure.hh"
#include "NOD_geometry_nodes_values.hh"
#include "DEG_depsgraph_query.hh"