diff --git a/scripts/startup/bl_ui/space_userpref.py b/scripts/startup/bl_ui/space_userpref.py index eddfadc7e75..0aca339ac9f 100644 --- a/scripts/startup/bl_ui/space_userpref.py +++ b/scripts/startup/bl_ui/space_userpref.py @@ -2872,6 +2872,7 @@ class USERPREF_PT_experimental_prototypes(ExperimentalPanel, Panel): ({"property": "use_new_curves_tools"}, ("blender/blender/issues/68981", "#68981")), ({"property": "use_sculpt_texture_paint"}, ("blender/blender/issues/96225", "#96225")), ({"property": "write_large_blend_file_blocks"}, ("/blender/blender/issues/129309", "#129309")), + ({"property": "use_attribute_storage_write"}, ("/blender/blender/issues/122398", "#122398")), ), ) diff --git a/source/blender/CMakeLists.txt b/source/blender/CMakeLists.txt index 00798c38a73..07d2615f651 100644 --- a/source/blender/CMakeLists.txt +++ b/source/blender/CMakeLists.txt @@ -9,6 +9,7 @@ set(SRC_DNA_INC ${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_anim_types.h ${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_armature_types.h ${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_asset_types.h + ${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_attribute_types.h ${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_boid_types.h ${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_brush_enums.h ${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_brush_types.h diff --git a/source/blender/blenkernel/BKE_attribute.hh b/source/blender/blenkernel/BKE_attribute.hh index 084ff8343bd..36b4683ee6d 100644 --- a/source/blender/blenkernel/BKE_attribute.hh +++ b/source/blender/blenkernel/BKE_attribute.hh @@ -32,6 +32,33 @@ class GField; namespace blender::bke { +/** Some storage types are only relevant for certain attribute types. */ +enum class AttrStorageType : int8_t { + /** #AttributeDataArray. */ + Array, + /** A single value for the whole attribute. */ + Single, +}; + +enum class AttrType : int16_t { + Bool, + Int8, + Int16_2D, + Int32, + Int32_2D, + Float, + Float2, + Float3, + Float4x4, + ColorByte, + ColorFloat, + Quaternion, + String, +}; + +const CPPType &attribute_type_to_cpp_type(AttrType type); +AttrType cpp_type_to_attribute_type(const CPPType &type); + enum class AttrDomain : int8_t { /* Used to choose automatically based on other data. */ Auto = -1, diff --git a/source/blender/blenkernel/BKE_attribute_legacy_convert.hh b/source/blender/blenkernel/BKE_attribute_legacy_convert.hh new file mode 100644 index 00000000000..31789475f01 --- /dev/null +++ b/source/blender/blenkernel/BKE_attribute_legacy_convert.hh @@ -0,0 +1,65 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "DNA_attribute_types.h" + +#include "BKE_attribute.hh" +#include "BKE_attribute_storage.hh" + +struct CustomData; +namespace blender::bke { +class CurvesGeometry; +} +struct PointCloud; +struct GreasePencil; +struct Mesh; + +namespace blender::bke { + +/** + * Convert a custom data type to an attribute type. May return `std::nullopt` if the custom data + * type isn't used at runtime, is not a generic type that can be stored as an attribute, or is only + * used for #BMesh. + */ +std::optional custom_data_type_to_attr_type(eCustomDataType data_type); + +/** + * Convert an attribute type to a legacy custom data type. + */ +std::optional attr_type_to_custom_data_type(AttrType attr_type); + +/** + * Move attributes from the #AttributeStorage to the mesh's #CustomData structs. Used for forward + * compatibility: converting newer files written with #AttributeStorage while #CustomData is still + * used at runtime. + */ +void mesh_convert_storage_to_customdata(Mesh &mesh); + +/** + * Move generic attributes from #CustomData to #AttributeStorage (not including non-generic layer + * types). Use for versioning old files when the newer #AttributeStorage format is used at runtime. + */ +AttributeStorage mesh_convert_customdata_to_storage(const Mesh &mesh); + +/** See #mesh_convert_storage_to_customdata. */ +void curves_convert_storage_to_customdata(CurvesGeometry &curves); + +/** See #mesh_convert_customdata_to_storage. */ +AttributeStorage curves_convert_customdata_to_storage(const CurvesGeometry &curves); + +/** See #mesh_convert_storage_to_customdata. */ +void pointcloud_convert_storage_to_customdata(PointCloud &pointcloud); + +/** See #mesh_convert_customdata_to_storage. */ +AttributeStorage pointcloud_convert_customdata_to_storage(const PointCloud &pointcloud); + +/** See #mesh_convert_storage_to_customdata. */ +void grease_pencil_convert_storage_to_customdata(GreasePencil &grease_pencil); + +/** See #mesh_convert_customdata_to_storage. */ +AttributeStorage grease_pencil_convert_customdata_to_storage(const GreasePencil &grease_pencil); + +} // namespace blender::bke diff --git a/source/blender/blenkernel/BKE_attribute_storage.hh b/source/blender/blenkernel/BKE_attribute_storage.hh new file mode 100644 index 00000000000..7c60ffabef1 --- /dev/null +++ b/source/blender/blenkernel/BKE_attribute_storage.hh @@ -0,0 +1,213 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include + +#include "BLI_function_ref.hh" +#include "BLI_implicit_sharing_ptr.hh" +#include "BLI_string_ref.hh" +#include "BLI_vector_set.hh" + +#include "DNA_attribute_types.h" + +struct BlendDataReader; +struct BlendWriter; +namespace blender { +class ResourceScope; +} + +namespace blender::bke { + +enum class AttrDomain : int8_t; +enum class AttrType : int16_t; +enum class AttrStorageType : int8_t; + +/** Data and metadata for a single geometry attribute. */ +class Attribute { + public: + /** + * Data for an attribute stored as a full contiguous array with a data type exactly matching the + * attribute's type. The array referenced must match the size of the domain and the data type. + */ + struct ArrayData { + /* NOTE: Since the shared data pointed to by `sharing_info` knows how to free itself, it often + * stores the size and type itself. It may be possible to make use of that fact to avoid + * storing it here, or even vice versa. */ + void *data; + /* The number of elements in the array. */ + int64_t size; + ImplicitSharingPtr<> sharing_info; + }; + /** Data for an attribute stored as a single value for the entire domain. */ + struct SingleData { + /* NOTE: For simplicity and to avoid a bit of redundancy, the domain size isn't stored here. + * It's not necessary to manage a single value. */ + void *value; + ImplicitSharingPtr<> sharing_info; + }; + using DataVariant = std::variant; + friend AttributeStorage; + + private: + /** + * Because it's used as the custom ID for the attributes vector set, the name cannot be changed + * without removing and adding the attribute. + */ + std::string name_; + AttrDomain domain_; + AttrType type_; + + DataVariant data_; + + public: + /** + * Unique name across all domains. + * \note Compared to #CustomData, which doesn't enforce uniqueness, across domains on its own, + * this is enforced by asserts when adding attributes. See #unique_name_calc() (which is also + * called during the conversion process). + */ + StringRefNull name() const; + + /** Which part of a geometry the attribute corresponds to. */ + AttrDomain domain() const; + + /** + * The data type exposed to the user. Depending on the storage type, the actual internal values + * may not be the same type. + */ + AttrType data_type() const; + + /** + * The method used to store the data. This gives flexibility to optimize the internal storage + * even though conceptually the attribute is an array of values. + */ + AttrStorageType storage_type() const; + + /** + * Low level access to the data stored for the attribute. The variant's type will correspond to + * the storage type. + */ + const DataVariant &data() const; + + /** + * The same as #data(), but if the attribute data is shared initially, it will be unshared and + * made mutable. + * + * \warning Does not yet support attributes stored as a single value (#AttrStorageType::Single). + */ + DataVariant &data_for_write(); +}; + +class AttributeStorageRuntime { + friend AttributeStorage; + struct AttributeNameGetter { + StringRef operator()(const std::unique_ptr &value) const + { + return value->name(); + } + }; + /** + * For quick access, the attributes are stored in a vector set, keyed by their name. Attributes + * can still be reordered by rebuilding the vector set from scratch. Each attribute is allocated + * to give pointer stability across additions and removals. + */ + CustomIDVectorSet, AttributeNameGetter> attributes; +}; + +class AttributeStorage : public ::AttributeStorage { + public: + AttributeStorage(); + AttributeStorage(const AttributeStorage &other); + AttributeStorage(AttributeStorage &&other); + AttributeStorage &operator=(const AttributeStorage &other); + AttributeStorage &operator=(AttributeStorage &&other); + ~AttributeStorage(); + + /** + * Iterate over all attributes, with the order defined by the order of insertion. It is not safe + * to add or remove attributes while iterating. + */ + void foreach(FunctionRef fn); + void foreach(FunctionRef fn) const; + + /** + * Try to find the attribute with a givin name. The non-const overload does not make the + * attribute data itself mutable. + */ + Attribute *lookup(StringRef name); + const Attribute *lookup(StringRef name) const; + + /** + * Attempt to remove the attribute with the given name, returning `true` if successful. Should + * not be called while iterating over attributes. + */ + bool remove(StringRef name); + + /** + * Add an attribute with the given name, which must not already be used by an existing attribute + * or this will invoke undefined behavior. + */ + Attribute &add(std::string name, + bke::AttrDomain domain, + bke::AttrType data_type, + Attribute::DataVariant data); + + /** Return a possibly changed version of the input name that is unique within existing names. */ + std::string unique_name_calc(StringRef name); + + /** + * Read data owned by the #AttributeStorage struct. This works by converting the DNA-specific + * types stored in the files to the runtime data structures. + */ + void blend_read(BlendDataReader &reader); + /** + * Temporary data used to write a #AttributeStorage struct embedded in another struct. See + * #attribute_storage_blend_write_prepare for more information. + */ + struct BlendWriteData { + ResourceScope &scope; + Vector<::Attribute, 16> attributes; + }; + /** + * Write the prepared data and the data stored in the DNA fields in + * the #AttributeStorage struct. + */ + void blend_write(BlendWriter &writer, const BlendWriteData &write_data); +}; + +/** The C++ wrapper needs to be the same size as the DNA struct. */ +static_assert(sizeof(AttributeStorage) == sizeof(::AttributeStorage)); + +inline StringRefNull Attribute::name() const +{ + return name_; +} + +inline AttrDomain Attribute::domain() const +{ + return domain_; +} + +inline AttrType Attribute::data_type() const +{ + return type_; +} + +inline const Attribute::DataVariant &Attribute::data() const +{ + return data_; +} + +} // namespace blender::bke + +inline blender::bke::AttributeStorage &AttributeStorage::wrap() +{ + return *reinterpret_cast(this); +} +inline const blender::bke::AttributeStorage &AttributeStorage::wrap() const +{ + return *reinterpret_cast(this); +} diff --git a/source/blender/blenkernel/BKE_attribute_storage_blend_write.hh b/source/blender/blenkernel/BKE_attribute_storage_blend_write.hh new file mode 100644 index 00000000000..731aa44419c --- /dev/null +++ b/source/blender/blenkernel/BKE_attribute_storage_blend_write.hh @@ -0,0 +1,26 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include "BKE_attribute_storage.hh" + +#include "BLI_map.hh" + +#include "DNA_customdata_types.h" + +namespace blender::bke { + +/** + * Prepare an #AttributeStorage struct embedded in another struct to be written. This is necessary + * because the #AttributeStorage implementation doesn't use the DNA structs at runtime, they are + * created just for the writing process. Creating them mutates the struct, which must be done + * before writing the struct that embeds it. + */ +void attribute_storage_blend_write_prepare( + AttributeStorage &data, + const Map *> &layers_to_write, + AttributeStorage::BlendWriteData &write_data); + +} // namespace blender::bke diff --git a/source/blender/blenkernel/BKE_curves.hh b/source/blender/blenkernel/BKE_curves.hh index 3228c5d5a0e..9f8073c179c 100644 --- a/source/blender/blenkernel/BKE_curves.hh +++ b/source/blender/blenkernel/BKE_curves.hh @@ -22,6 +22,7 @@ #include "BLI_virtual_array_fwd.hh" #include "BKE_attribute_math.hh" +#include "BKE_attribute_storage.hh" #include "BKE_curves.h" struct BlendDataReader; @@ -495,16 +496,16 @@ class CurvesGeometry : public ::CurvesGeometry { * Helper struct for `CurvesGeometry::blend_write_*` functions. */ struct BlendWriteData { - /* The point custom data layers to be written. */ Vector point_layers; - /* The curve custom data layers to be written. */ Vector curve_layers; + AttributeStorage::BlendWriteData attribute_data; + explicit BlendWriteData(ResourceScope &scope) : attribute_data{scope} {} }; /** * This function needs to be called before `blend_write` and before the `CurvesGeometry` struct - * is written because it can mutate the `CustomData` struct. + * is written because it can mutate the `CustomData` and `AttributeStorage` structs. */ - BlendWriteData blend_write_prepare(); + void blend_write_prepare(BlendWriteData &write_data); void blend_write(BlendWriter &writer, ID &id, const BlendWriteData &write_data); }; diff --git a/source/blender/blenkernel/BKE_customdata.hh b/source/blender/blenkernel/BKE_customdata.hh index acdb1b9459b..39fc449f04c 100644 --- a/source/blender/blenkernel/BKE_customdata.hh +++ b/source/blender/blenkernel/BKE_customdata.hh @@ -13,12 +13,12 @@ #include "BLI_implicit_sharing.h" #include "BLI_memory_counter_fwd.hh" -#include "BLI_set.hh" #include "BLI_span.hh" #include "BLI_string_ref.hh" #include "BLI_sys_types.h" #include "BLI_vector.hh" +#include "BKE_attribute_storage.hh" #include "BKE_volume_enums.hh" #include "DNA_customdata_types.h" @@ -32,6 +32,10 @@ struct CustomDataTransferLayerMap; struct ID; struct MeshPairRemap; +namespace blender::bke { +enum class AttrDomain : int8_t; +} + /* These names are used as prefixes for UV layer names to find the associated boolean * layers. They should never be longer than 2 chars, as #MAX_CUSTOMDATA_LAYER_NAME * has 4 extra bytes above what can be used for the base layer name, and these @@ -731,14 +735,18 @@ void CustomData_data_transfer(const MeshPairRemap *me_remap, * * \param data: The custom-data to tweak for .blend file writing (modified in place). * \param layers_to_write: A reduced set of layers to be written to file. + * \param write_data: #AttributeStorage data to write, to support the option for writing the new + * format even when it isn't used at runtime. * * \warning This function invalidates the custom data struct by changing the layer counts and the * #layers pointer, and by invalidating the type map. It expects to work on a shallow copy of * the struct. */ void CustomData_blend_write_prepare(CustomData &data, + blender::bke::AttrDomain domain, + int domain_size, blender::Vector &layers_to_write, - const blender::Set &skip_names = {}); + blender::bke::AttributeStorage::BlendWriteData &write_data); /** * \param layers_to_write: Layers created by #CustomData_blend_write_prepare. diff --git a/source/blender/blenkernel/CMakeLists.txt b/source/blender/blenkernel/CMakeLists.txt index 67e5651ab11..8df7d0fe1c3 100644 --- a/source/blender/blenkernel/CMakeLists.txt +++ b/source/blender/blenkernel/CMakeLists.txt @@ -55,6 +55,8 @@ set(SRC intern/asset_weak_reference.cc intern/attribute.cc intern/attribute_access.cc + intern/attribute_legacy_convert.cc + intern/attribute_storage.cc intern/attribute_math.cc intern/autoexec.cc intern/bake_data_block_map.cc @@ -333,7 +335,10 @@ set(SRC BKE_attribute.hh BKE_attribute_filter.hh BKE_attribute_filters.hh + BKE_attribute_legacy_convert.hh BKE_attribute_math.hh + BKE_attribute_storage.hh + BKE_attribute_storage_blend_write.hh BKE_autoexec.hh BKE_bake_data_block_id.hh BKE_bake_data_block_map.hh @@ -818,6 +823,7 @@ add_dependencies(bf_blenkernel bf_rna) if(WITH_GTESTS) set(TEST_SRC intern/action_test.cc + intern/attribute_storage_test.cc intern/armature_test.cc intern/asset_metadata_test.cc intern/bpath_test.cc diff --git a/source/blender/blenkernel/intern/attribute_access.cc b/source/blender/blenkernel/intern/attribute_access.cc index fa82cc0e100..6a1c61d4780 100644 --- a/source/blender/blenkernel/intern/attribute_access.cc +++ b/source/blender/blenkernel/intern/attribute_access.cc @@ -34,6 +34,85 @@ namespace blender::bke { +const CPPType &attribute_type_to_cpp_type(const AttrType type) +{ + switch (type) { + case AttrType::Bool: + return CPPType::get(); + case AttrType::Int8: + return CPPType::get(); + case AttrType::Int16_2D: + return CPPType::get(); + case AttrType::Int32: + return CPPType::get(); + case AttrType::Int32_2D: + return CPPType::get(); + case AttrType::Float: + return CPPType::get(); + case AttrType::Float2: + return CPPType::get(); + case AttrType::Float3: + return CPPType::get(); + case AttrType::Float4x4: + return CPPType::get(); + case AttrType::ColorByte: + return CPPType::get(); + case AttrType::ColorFloat: + return CPPType::get(); + case AttrType::Quaternion: + return CPPType::get(); + case AttrType::String: + return CPPType::get(); + } + BLI_assert_unreachable(); + return CPPType::get(); +} + +AttrType cpp_type_to_attribute_type(const CPPType &type) +{ + if (type.is()) { + return AttrType::Float; + } + if (type.is()) { + return AttrType::Float2; + } + if (type.is()) { + return AttrType::Float3; + } + if (type.is()) { + return AttrType::Int32; + } + if (type.is()) { + return AttrType::Int32_2D; + } + if (type.is()) { + return AttrType::ColorFloat; + } + if (type.is()) { + return AttrType::Bool; + } + if (type.is()) { + return AttrType::Int8; + } + if (type.is()) { + return AttrType::ColorByte; + } + if (type.is()) { + return AttrType::Quaternion; + } + if (type.is()) { + return AttrType::Float4x4; + } + if (type.is()) { + return AttrType::Int16_2D; + } + if (type.is()) { + return AttrType::String; + } + BLI_assert_unreachable(); + return AttrType::Bool; +} + const blender::CPPType *custom_data_type_to_cpp_type(const eCustomDataType type) { switch (type) { diff --git a/source/blender/blenkernel/intern/attribute_legacy_convert.cc b/source/blender/blenkernel/intern/attribute_legacy_convert.cc new file mode 100644 index 00000000000..8628c62099a --- /dev/null +++ b/source/blender/blenkernel/intern/attribute_legacy_convert.cc @@ -0,0 +1,273 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#define DNA_DEPRECATED_ALLOW + +#include + +#include "DNA_grease_pencil_types.h" +#include "DNA_mesh_types.h" +#include "DNA_pointcloud_types.h" + +#include "BKE_attribute.hh" +#include "BKE_curves.hh" +#include "BKE_customdata.hh" + +#include "BKE_attribute_legacy_convert.hh" + +namespace blender::bke { + +std::optional custom_data_type_to_attr_type(const eCustomDataType data_type) +{ + switch (data_type) { + case CD_AUTO_FROM_NAME: + /* This type is not used for actual #CustomData layers. */ + BLI_assert_unreachable(); + return std::nullopt; + case CD_MVERT: + case CD_MSTICKY: + case CD_MEDGE: + case CD_FACEMAP: + case CD_MTEXPOLY: + case CD_MLOOPUV: + case CD_MPOLY: + case CD_MLOOP: + case CD_BWEIGHT: + case CD_CREASE: + case CD_PAINT_MASK: + case CD_CUSTOMLOOPNORMAL: + case CD_SCULPT_FACE_SETS: + case CD_NUMTYPES: + /* These types are only used for versioning old files. */ + return std::nullopt; + /* These types are only used for #BMesh. */ + case CD_SHAPEKEY: + case CD_SHAPE_KEYINDEX: + case CD_BM_ELEM_PYPTR: + return std::nullopt; + case CD_MDEFORMVERT: + case CD_MFACE: + case CD_MTFACE: + case CD_MCOL: + case CD_ORIGINDEX: + case CD_NORMAL: + case CD_ORIGSPACE: + case CD_ORCO: + case CD_TANGENT: + case CD_MDISPS: + case CD_CLOTH_ORCO: + case CD_ORIGSPACE_MLOOP: + case CD_GRID_PAINT_MASK: + case CD_MVERT_SKIN: + case CD_FREESTYLE_EDGE: + case CD_FREESTYLE_FACE: + case CD_MLOOPTANGENT: + case CD_TESSLOOPNORMAL: + /* These types are not generic. They will either be moved to some generic data type or + * #AttributeStorage will be extended to be able to support a similar format. */ + return std::nullopt; + case CD_PROP_FLOAT: + return AttrType::Float; + case CD_PROP_INT32: + return AttrType::Int32; + case CD_PROP_BYTE_COLOR: + return AttrType::ColorByte; + case CD_PROP_FLOAT4X4: + return AttrType::Float4x4; + case CD_PROP_INT16_2D: + return AttrType::Int16_2D; + case CD_PROP_INT8: + return AttrType::Int8; + case CD_PROP_INT32_2D: + return AttrType::Int32_2D; + case CD_PROP_COLOR: + return AttrType::ColorFloat; + case CD_PROP_FLOAT3: + return AttrType::Float3; + case CD_PROP_FLOAT2: + return AttrType::Float2; + case CD_PROP_BOOL: + return AttrType::Bool; + case CD_PROP_STRING: + return AttrType::String; + case CD_PROP_QUATERNION: + return AttrType::Quaternion; + } + return std::nullopt; +} + +struct CustomDataAndSize { + const CustomData &data; + int size; +}; + +static AttributeStorage attribute_legacy_convert_customdata_to_storage( + const Map &domains) +{ + AttributeStorage storage{}; + struct AttributeToAdd { + std::string name; + AttrDomain domain; + AttrType type; + void *array_data; + int array_size; + const ImplicitSharingInfo *sharing_info; + }; + Vector attributes_to_add; + for (const auto &item : domains.items()) { + const AttrDomain domain = item.key; + const CustomData &custom_data = item.value.data; + const int domain_size = item.value.size; + for (const CustomDataLayer &layer : MutableSpan(custom_data.layers, custom_data.totlayer)) { + const std::optional attr_type = custom_data_type_to_attr_type( + eCustomDataType(layer.type)); + if (!attr_type) { + continue; + } + attributes_to_add.append( + {layer.name, domain, *attr_type, layer.data, domain_size, layer.sharing_info}); + layer.sharing_info->add_user(); + } + } + + for (AttributeToAdd &attribute : attributes_to_add) { + bke::Attribute::ArrayData array_data; + array_data.data = attribute.array_data; + array_data.size = attribute.array_size; + array_data.sharing_info = ImplicitSharingPtr<>(attribute.sharing_info); + storage.add(storage.unique_name_calc(attribute.name), + attribute.domain, + attribute.type, + std::move(array_data)); + } + + return storage; +} + +std::optional attr_type_to_custom_data_type(const AttrType attr_type) +{ + switch (attr_type) { + case AttrType::Bool: + return CD_PROP_BOOL; + case AttrType::Int8: + return CD_PROP_INT8; + case AttrType::Int16_2D: + return CD_PROP_INT16_2D; + case AttrType::Int32: + return CD_PROP_INT32; + case AttrType::Int32_2D: + return CD_PROP_INT32_2D; + case AttrType::Float: + return CD_PROP_FLOAT; + case AttrType::Float2: + return CD_PROP_FLOAT2; + case AttrType::Float3: + return CD_PROP_FLOAT3; + case AttrType::Float4x4: + return CD_PROP_FLOAT4X4; + case AttrType::ColorByte: + return CD_PROP_BYTE_COLOR; + case AttrType::ColorFloat: + return CD_PROP_COLOR; + case AttrType::Quaternion: + return CD_PROP_QUATERNION; + case AttrType::String: + return CD_PROP_STRING; + } + return std::nullopt; +} + +struct CustomDataAndSizeMutable { + CustomData &data; + int size; +}; + +static void convert_storage_to_customdata( + AttributeStorage &storage, + const Map &custom_data_domains) +{ + /* Name uniqueness is handled by the #CustomData API. */ + storage.foreach([&](const Attribute &attribute) { + const std::optional data_type = attr_type_to_custom_data_type( + attribute.data_type()); + if (!data_type) { + return; + } + CustomData &custom_data = custom_data_domains.lookup(attribute.domain()).data; + const int domain_size = custom_data_domains.lookup(attribute.domain()).size; + if (const auto *array_data = std::get_if(&attribute.data())) { + BLI_assert(array_data->size == domain_size); + CustomData_add_layer_named_with_data(&custom_data, + *data_type, + array_data->data, + array_data->size, + attribute.name(), + array_data->sharing_info.get()); + } + else if (const auto *single_data = std::get_if(&attribute.data())) { + const CPPType &cpp_type = *custom_data_type_to_cpp_type(*data_type); + auto *value = new ImplicitSharedValue>(cpp_type, domain_size); + cpp_type.fill_construct_n(single_data->value, value->data.data(), domain_size); + CustomData_add_layer_named_with_data( + &custom_data, *data_type, value->data.data(), domain_size, attribute.name(), value); + } + }); + storage = {}; +} + +void mesh_convert_storage_to_customdata(Mesh &mesh) +{ + convert_storage_to_customdata(mesh.attribute_storage.wrap(), + {{AttrDomain::Point, {mesh.vert_data, mesh.verts_num}}, + {AttrDomain::Edge, {mesh.edge_data, mesh.edges_num}}, + {AttrDomain::Face, {mesh.face_data, mesh.faces_num}}, + {AttrDomain::Corner, {mesh.corner_data, mesh.corners_num}}}); +} +AttributeStorage mesh_convert_customdata_to_storage(const Mesh &mesh) +{ + return bke::attribute_legacy_convert_customdata_to_storage( + {{AttrDomain::Point, {mesh.vert_data, mesh.verts_num}}, + {AttrDomain::Edge, {mesh.edge_data, mesh.edges_num}}, + {AttrDomain::Face, {mesh.face_data, mesh.faces_num}}, + {AttrDomain::Corner, {mesh.corner_data, mesh.corners_num}}}); +} + +void curves_convert_storage_to_customdata(CurvesGeometry &curves) +{ + convert_storage_to_customdata(curves.attribute_storage.wrap(), + {{AttrDomain::Point, {curves.point_data, curves.points_num()}}, + {AttrDomain::Curve, {curves.curve_data, curves.curves_num()}}}); +} +AttributeStorage curves_convert_customdata_to_storage(const CurvesGeometry &curves) +{ + return attribute_legacy_convert_customdata_to_storage( + {{AttrDomain::Point, {curves.point_data, curves.points_num()}}, + {AttrDomain::Curve, {curves.curve_data, curves.curves_num()}}}); +} + +void pointcloud_convert_storage_to_customdata(PointCloud &pointcloud) +{ + convert_storage_to_customdata(pointcloud.attribute_storage.wrap(), + {{AttrDomain::Point, {pointcloud.pdata, pointcloud.totpoint}}}); +} + +AttributeStorage pointcloud_convert_customdata_to_storage(const PointCloud &pointcloud) +{ + return attribute_legacy_convert_customdata_to_storage( + {{AttrDomain::Point, {pointcloud.pdata, pointcloud.totpoint}}}); +} + +void grease_pencil_convert_storage_to_customdata(GreasePencil &grease_pencil) +{ + convert_storage_to_customdata( + grease_pencil.attribute_storage.wrap(), + {{AttrDomain::Layer, {grease_pencil.layers_data, int(grease_pencil.layers().size())}}}); +} +AttributeStorage grease_pencil_convert_customdata_to_storage(const GreasePencil &grease_pencil) +{ + return attribute_legacy_convert_customdata_to_storage( + {{AttrDomain::Layer, {grease_pencil.layers_data, int(grease_pencil.layers().size())}}}); +} + +} // namespace blender::bke diff --git a/source/blender/blenkernel/intern/attribute_storage.cc b/source/blender/blenkernel/intern/attribute_storage.cc new file mode 100644 index 00000000000..f74cfecec05 --- /dev/null +++ b/source/blender/blenkernel/intern/attribute_storage.cc @@ -0,0 +1,539 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "CLG_log.h" + +#include "BLI_assert.h" +#include "BLI_implicit_sharing.hh" +#include "BLI_resource_scope.hh" +#include "BLI_string_utils.hh" +#include "BLI_vector_set.hh" + +#include "BLO_read_write.hh" + +#include "DNA_attribute_types.h" +#include "DNA_meshdata_types.h" + +#include "BKE_attribute.hh" +#include "BKE_attribute_legacy_convert.hh" +#include "BKE_attribute_storage.hh" +#include "BKE_attribute_storage_blend_write.hh" + +static CLG_LogRef LOG = {"bke.attribute_storage"}; + +namespace blender::bke { + +class ArrayDataImplicitSharing : public ImplicitSharingInfo { + private: + void *data_; + int64_t size_; + const CPPType &type_; + /* This struct could also store caches about the array data, like the min and max values. */ + + public: + ArrayDataImplicitSharing(void *data, const int64_t size, const CPPType &type) + : ImplicitSharingInfo(), data_(data), size_(size), type_(type) + { + } + + private: + void delete_self_with_data() override + { + if (data_ != nullptr) { + type_.destruct_n(data_, size_); + MEM_freeN(data_); + } + MEM_delete(this); + } + + void delete_data_only() override + { + type_.destruct_n(data_, size_); + MEM_freeN(data_); + data_ = nullptr; + size_ = 0; + } +}; + +void AttributeStorage::foreach(FunctionRef fn) +{ + for (const std::unique_ptr &attribute : this->runtime->attributes) { + fn(*attribute); + } +} +void AttributeStorage::foreach(FunctionRef fn) const +{ + for (const std::unique_ptr &attribute : this->runtime->attributes) { + fn(*attribute); + } +} + +static ImplicitSharingInfo *create_sharing_info_for_array(void *data, + const int64_t size, + const CPPType &type) +{ + return MEM_new(__func__, data, size, type); +} + +AttrStorageType Attribute::storage_type() const +{ + if (std::get_if(&data_)) { + return AttrStorageType::Array; + } + if (std::get_if(&data_)) { + return AttrStorageType::Single; + } + BLI_assert_unreachable(); + return AttrStorageType::Array; +} + +Attribute::DataVariant &Attribute::data_for_write() +{ + if (auto *data = std::get_if(&data_)) { + if (data->sharing_info->is_mutable()) { + data->sharing_info->tag_ensured_mutable(); + return data_; + } + + const CPPType &cpp_type = attribute_type_to_cpp_type(type_); + void *new_data = MEM_malloc_arrayN_aligned( + data->size, cpp_type.size, cpp_type.alignment, __func__); + cpp_type.copy_construct_n(data->data, new_data, data->size); + + data->data = new_data; + data->sharing_info = ImplicitSharingPtr<>( + create_sharing_info_for_array(data->data, data->size, cpp_type)); + } + else if (std::get_if(&data_)) { + /* Not yet implemented because #SingleData isn't used at runtime yet. */ + BLI_assert_unreachable(); + } + return data_; +} + +AttributeStorage::AttributeStorage() +{ + this->dna_attributes = nullptr; + this->dna_attributes_num = 0; + this->runtime = MEM_new(__func__); +} + +AttributeStorage::AttributeStorage(const AttributeStorage &other) +{ + this->dna_attributes = nullptr; + this->dna_attributes_num = 0; + this->runtime = MEM_new(__func__); + this->runtime->attributes.reserve(other.runtime->attributes.size()); + other.foreach([&](const Attribute &attribute) { + this->runtime->attributes.add_new(std::make_unique(attribute)); + }); +} + +AttributeStorage &AttributeStorage::operator=(const AttributeStorage &other) +{ + if (this == &other) { + return *this; + } + std::destroy_at(this); + new (this) AttributeStorage(other); + return *this; +} + +AttributeStorage::AttributeStorage(AttributeStorage &&other) +{ + this->dna_attributes = nullptr; + this->dna_attributes_num = 0; + this->runtime = MEM_new(__func__, std::move(*other.runtime)); +} + +AttributeStorage &AttributeStorage::operator=(AttributeStorage &&other) +{ + if (this == &other) { + return *this; + } + std::destroy_at(this); + new (this) AttributeStorage(std::move(other)); + return *this; +} + +AttributeStorage::~AttributeStorage() +{ + MEM_delete(this->runtime); +} + +const Attribute *AttributeStorage::lookup(const StringRef name) const +{ + const std::unique_ptr *attribute = + this->runtime->attributes.lookup_key_ptr_as(name); + if (!attribute) { + return nullptr; + } + return attribute->get(); +} + +Attribute *AttributeStorage::lookup(const StringRef name) +{ + const std::unique_ptr *attribute = + this->runtime->attributes.lookup_key_ptr_as(name); + if (!attribute) { + return nullptr; + } + return attribute->get(); +} + +Attribute &AttributeStorage::add(std::string name, + const AttrDomain domain, + const AttrType data_type, + Attribute::DataVariant data) +{ + BLI_assert(!this->lookup(name)); + std::unique_ptr ptr = std::make_unique(); + Attribute &attribute = *ptr; + attribute.name_ = std::move(name); + attribute.domain_ = domain; + attribute.type_ = data_type; + attribute.data_ = std::move(data); + this->runtime->attributes.add_new(std::move(ptr)); + return attribute; +} + +bool AttributeStorage::remove(const StringRef name) +{ + return this->runtime->attributes.remove_as(name); +} + +std::string AttributeStorage::unique_name_calc(const StringRef name) +{ + return BLI_uniquename_cb( + [&](const StringRef check_name) { return this->lookup(check_name) != nullptr; }, '.', name); +} + +static void read_array_data(BlendDataReader &reader, + const int8_t dna_attr_type, + const int64_t size, + void **data) +{ + switch (dna_attr_type) { + case int8_t(AttrType::Bool): + static_assert(sizeof(bool) == sizeof(int8_t)); + BLO_read_int8_array(&reader, size, reinterpret_cast(data)); + return; + case int8_t(AttrType::Int8): + BLO_read_int8_array(&reader, size, reinterpret_cast(data)); + return; + case int8_t(AttrType::Int16_2D): + BLO_read_int16_array(&reader, size * 2, reinterpret_cast(data)); + return; + case int8_t(AttrType::Int32): + BLO_read_int32_array(&reader, size, reinterpret_cast(data)); + return; + case int8_t(AttrType::Int32_2D): + BLO_read_int32_array(&reader, size * 2, reinterpret_cast(data)); + return; + case int8_t(AttrType::Float): + BLO_read_float_array(&reader, size, reinterpret_cast(data)); + return; + case int8_t(AttrType::Float2): + BLO_read_float_array(&reader, size * 2, reinterpret_cast(data)); + return; + case int8_t(AttrType::Float3): + BLO_read_float3_array(&reader, size, reinterpret_cast(data)); + return; + case int8_t(AttrType::Float4x4): + BLO_read_float_array(&reader, size * 16, reinterpret_cast(data)); + return; + case int8_t(AttrType::ColorByte): + BLO_read_uint8_array(&reader, size * 4, reinterpret_cast(data)); + return; + case int8_t(AttrType::ColorFloat): + BLO_read_float_array(&reader, size * 4, reinterpret_cast(data)); + return; + case int8_t(AttrType::Quaternion): + BLO_read_float_array(&reader, size * 4, reinterpret_cast(data)); + return; + case int8_t(AttrType::String): + BLO_read_struct_array( + &reader, MStringProperty, size, reinterpret_cast(data)); + return; + default: + *data = nullptr; + return; + } +} + +static void read_shared_array(BlendDataReader &reader, + const int8_t dna_attr_type, + const int64_t size, + void **data, + const ImplicitSharingInfo **sharing_info) +{ + const char *func = __func__; + *sharing_info = BLO_read_shared(&reader, &data, [&]() -> const ImplicitSharingInfo * { + read_array_data(reader, dna_attr_type, size, data); + if (*data == nullptr) { + return nullptr; + } + const CPPType &cpp_type = attribute_type_to_cpp_type(AttrType(dna_attr_type)); + return MEM_new(func, *data, size, cpp_type); + }); +} + +static std::optional read_attr_data(BlendDataReader &reader, + const int8_t dna_storage_type, + const int8_t dna_attr_type, + ::Attribute &dna_attr) +{ + switch (dna_storage_type) { + case int8_t(AttrStorageType::Array): { + BLO_read_struct(&reader, AttributeArray, &dna_attr.data); + auto &data = *static_cast<::AttributeArray *>(dna_attr.data); + read_shared_array(reader, dna_attr_type, data.size, &data.data, &data.sharing_info); + if (!data.data) { + return std::nullopt; + } + return Attribute::ArrayData{data.data, data.size, ImplicitSharingPtr<>(data.sharing_info)}; + } + case int8_t(AttrStorageType::Single): { + BLO_read_struct(&reader, AttributeSingle, &dna_attr.data); + auto &data = *static_cast<::AttributeSingle *>(dna_attr.data); + read_shared_array(reader, dna_attr_type, 1, &data.data, &data.sharing_info); + if (!data.data) { + return std::nullopt; + } + return Attribute::SingleData{data.data, ImplicitSharingPtr<>(data.sharing_info)}; + } + default: + return std::nullopt; + } +} + +static std::optional read_attr_domain(const int8_t dna_domain) +{ + switch (dna_domain) { + case int8_t(AttrDomain::Point): + case int8_t(AttrDomain::Edge): + case int8_t(AttrDomain::Face): + case int8_t(AttrDomain::Corner): + case int8_t(AttrDomain::Curve): + case int8_t(AttrDomain::Instance): + case int8_t(AttrDomain::Layer): + return AttrDomain(dna_domain); + default: + return std::nullopt; + } +} + +void AttributeStorage::blend_read(BlendDataReader &reader) +{ + this->runtime = MEM_new(__func__); + this->runtime->attributes.reserve(this->dna_attributes_num); + + BLO_read_struct_array(&reader, ::Attribute, this->dna_attributes_num, &this->dna_attributes); + for (const int i : IndexRange(this->dna_attributes_num)) { + ::Attribute &dna_attr = this->dna_attributes[i]; + BLO_read_string(&reader, &dna_attr.name); + + const std::optional domain = read_attr_domain(dna_attr.domain); + if (!domain) { + continue; + } + + std::optional data = read_attr_data( + reader, dna_attr.storage_type, dna_attr.data_type, dna_attr); + if (!data) { + continue; + } + + std::unique_ptr attribute = std::make_unique(); + attribute->name_ = dna_attr.name; + attribute->domain_ = *domain; + attribute->type_ = AttrType(dna_attr.data_type); + attribute->data_ = std::move(*data); + + if (!this->runtime->attributes.add(std::move(attribute))) { + CLOG_ERROR(&LOG, "Ignoring attribute with duplicate name: \"%s\"", dna_attr.name); + } + + MEM_SAFE_FREE(dna_attr.name); + MEM_SAFE_FREE(dna_attr.data); + } + + /* These fields are not used at runtime. */ + MEM_SAFE_FREE(this->dna_attributes); + this->dna_attributes_num = 0; +} + +static void write_array_data(BlendWriter &writer, + const AttrType data_type, + const void *data, + const int64_t size) +{ + switch (data_type) { + case AttrType::Bool: + static_assert(sizeof(bool) == sizeof(int8_t)); + BLO_write_int8_array(&writer, size, static_cast(data)); + break; + case AttrType::Int8: + BLO_write_int8_array(&writer, size, static_cast(data)); + break; + case AttrType::Int16_2D: + BLO_write_int16_array(&writer, size * 2, static_cast(data)); + break; + case AttrType::Int32: + BLO_write_int32_array(&writer, size, static_cast(data)); + break; + case AttrType::Int32_2D: + BLO_write_int32_array(&writer, size * 2, static_cast(data)); + break; + case AttrType::Float: + BLO_write_float_array(&writer, size, static_cast(data)); + break; + case AttrType::Float2: + BLO_write_float_array(&writer, size * 2, static_cast(data)); + break; + case AttrType::Float3: + BLO_write_float3_array(&writer, size, static_cast(data)); + break; + case AttrType::Float4x4: + BLO_write_float_array(&writer, size * 16, static_cast(data)); + break; + case AttrType::ColorByte: + BLO_write_uint8_array(&writer, size * 4, static_cast(data)); + break; + case AttrType::ColorFloat: + BLO_write_float_array(&writer, size * 4, static_cast(data)); + break; + case AttrType::Quaternion: + BLO_write_float_array(&writer, size * 4, static_cast(data)); + break; + case AttrType::String: + BLO_write_struct_array( + &writer, MStringProperty, size, static_cast(data)); + break; + } +} + +void attribute_storage_blend_write_prepare( + AttributeStorage &data, + const Map *> &layers_to_write, + AttributeStorage::BlendWriteData &write_data) +{ + Set all_names_written; + for (Vector *const layers : layers_to_write.values()) { + for (const CustomDataLayer &layer : *layers) { + all_names_written.add(layer.name); + } + } + data.foreach([&](Attribute &attr) { + if (!U.experimental.use_attribute_storage_write) { + /* In version 4.5, all attribute data is written in the #CustomData format (at least when the + * debug option is not enabled), so the #Attribute needs to be converted to a + * #CustomDataLayer in the proper list. This is only relevant when #AttributeStorage is + * actually used at runtime. + * + * When removing this option to always write the new format in 5.0, #BLENDER_FILE_MIN_VERSION + * must be increased. */ + if (const std::optional data_type = attr_type_to_custom_data_type(attr.data_type())) { + if (const auto *array_data = std::get_if(&attr.data())) { + CustomDataLayer layer{}; + layer.type = *data_type; + layer.data = array_data->data; + layer.sharing_info = array_data->sharing_info.get(); + + /* Because the #Attribute::name_ `std::string` has no length limit (unlike + * #CustomDataLayer::name), we have to manually make the name unique in case it exceeds + * the limit. */ + BLI_uniquename_cb( + [&](const StringRefNull name) { return all_names_written.contains(name); }, + attr.name().c_str(), + '.', + layer.name, + MAX_CUSTOMDATA_LAYER_NAME); + all_names_written.add(layer.name); + + layers_to_write.lookup(attr.domain())->append(layer); + } + } + return; + } + + /* Names within an AttributeStorage are unique. */ + all_names_written.add(attr.name()); + ::Attribute attribute_dna{}; + attribute_dna.name = attr.name().c_str(); + attribute_dna.data_type = int16_t(attr.data_type()); + attribute_dna.domain = int8_t(attr.domain()); + attribute_dna.storage_type = int8_t(attr.storage_type()); + + /* The idea is to use a separate DNA struct for each #AttrStorageType. They each need to have a + * unique address (while writing a specific ID anyway) in order to be identified when + * reading the file, so we add them to the resource scope which outlives this function call. + * Using a #ResourceScope is a simple way to get pointer stability when adding every new data + * struct without the cost of many small allocations or unnecessary overhead of storing a full + * array for every storage type. */ + + if (const auto *data = std::get_if(&attr.data())) { + auto &array_dna = write_data.scope.construct<::AttributeArray>(); + array_dna.data = data->data; + array_dna.sharing_info = data->sharing_info.get(); + array_dna.size = data->size; + attribute_dna.data = &array_dna; + } + else if (const auto *data = std::get_if(&attr.data())) { + auto &single_dna = write_data.scope.construct<::AttributeSingle>(); + single_dna.data = data->value; + single_dna.sharing_info = data->sharing_info.get(); + attribute_dna.data = &single_dna; + } + + write_data.attributes.append(attribute_dna); + }); +} + +static void write_shared_array(BlendWriter &writer, + const AttrType data_type, + const void *data, + const int64_t size, + const ImplicitSharingInfo &sharing_info) +{ + const CPPType &cpp_type = attribute_type_to_cpp_type(data_type); + BLO_write_shared(&writer, data, cpp_type.size * size, &sharing_info, [&]() { + write_array_data(writer, data_type, data, size); + }); +} + +void AttributeStorage::blend_write(BlendWriter &writer, + const AttributeStorage::BlendWriteData &write_data) +{ + /* Use string argument to avoid confusion with the C++ class with the same name. */ + BLO_write_struct_array_by_name( + &writer, "Attribute", write_data.attributes.size(), write_data.attributes.data()); + for (const ::Attribute &attr_dna : write_data.attributes) { + BLO_write_string(&writer, attr_dna.name); + switch (AttrStorageType(attr_dna.storage_type)) { + case AttrStorageType::Single: { + ::AttributeSingle *single_dna = static_cast<::AttributeSingle *>(attr_dna.data); + BLO_write_struct(&writer, AttributeSingle, single_dna); + write_shared_array( + writer, AttrType(attr_dna.data_type), single_dna->data, 1, *single_dna->sharing_info); + break; + } + case AttrStorageType::Array: { + ::AttributeArray *array_dna = static_cast<::AttributeArray *>(attr_dna.data); + BLO_write_struct(&writer, AttributeArray, array_dna); + write_shared_array(writer, + AttrType(attr_dna.data_type), + array_dna->data, + array_dna->size, + *array_dna->sharing_info); + break; + } + } + } + + this->dna_attributes = nullptr; + this->dna_attributes_num = 0; +} + +} // namespace blender::bke diff --git a/source/blender/blenkernel/intern/attribute_storage_test.cc b/source/blender/blenkernel/intern/attribute_storage_test.cc new file mode 100644 index 00000000000..c98dba00a01 --- /dev/null +++ b/source/blender/blenkernel/intern/attribute_storage_test.cc @@ -0,0 +1,187 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "testing/testing.h" + +#include "BKE_attribute.hh" +#include "BKE_attribute_storage.hh" + +namespace blender::bke::tests { + +TEST(attribute_storage, Empty) +{ + AttributeStorage storage; + int count = 0; + storage.foreach([&](const Attribute & /*attribute*/) { count++; }); + EXPECT_EQ(count, 0); +} + +TEST(attribute_storage, Single) +{ + AttributeStorage storage; + + auto *sharing_info = new ImplicitSharedValue>(Span{1.5f, 1.2f, 1.1f, 1.0f}); + Attribute::ArrayData data{}; + data.sharing_info = ImplicitSharingPtr<>(sharing_info); + data.data = sharing_info->data.data(); + data.size = 4; + storage.add("foo", AttrDomain::Corner, AttrType::Float, std::move(data)); + + EXPECT_TRUE(storage.lookup("foo")); + EXPECT_EQ(storage.lookup("foo")->domain(), AttrDomain::Corner); + EXPECT_EQ(storage.lookup("foo")->data_type(), AttrType::Float); + { + const auto &data = std::get(storage.lookup("foo")->data()); + EXPECT_EQ(data.data, sharing_info->data.data()); + } + + int count = 0; + storage.foreach([&](const Attribute & /*attribute*/) { count++; }); + EXPECT_EQ(count, 1); +} + +TEST(attribute_storage, GetForWrite) +{ + AttributeStorage storage; + + auto *sharing_info = new ImplicitSharedValue>(Span{1.5f, 1.2f, 1.1f, 1.0f}); + Attribute::ArrayData data{}; + data.sharing_info = ImplicitSharingPtr<>(sharing_info); + data.data = sharing_info->data.data(); + data.size = 4; + storage.add("foo", AttrDomain::Corner, AttrType::Float, std::move(data)); + { + const auto &data = std::get(storage.lookup("foo")->data_for_write()); + EXPECT_EQ(data.data, sharing_info->data.data()); + } + { + sharing_info->add_user(); + const auto &data = std::get(storage.lookup("foo")->data_for_write()); + EXPECT_NE(data.data, sharing_info->data.data()); + const float *data_ptr = static_cast(data.data); + EXPECT_EQ(data_ptr[0], 1.5f); + EXPECT_EQ(data_ptr[1], 1.2f); + EXPECT_EQ(data_ptr[2], 1.1f); + EXPECT_EQ(data_ptr[3], 1.0f); + sharing_info->remove_user_and_delete_if_last(); + } + { + const auto &data = std::get(storage.lookup("foo")->data_for_write()); + const float *data_ptr = static_cast(data.data); + EXPECT_EQ(data_ptr[0], 1.5f); + EXPECT_EQ(data_ptr[1], 1.2f); + EXPECT_EQ(data_ptr[2], 1.1f); + EXPECT_EQ(data_ptr[3], 1.0f); + } +} + +TEST(attribute_storage, MultipleShared) +{ + AttributeStorage storage; + + auto *sharing_info = new ImplicitSharedValue>(Span{1.5f, 1.2f, 1.1f, 1.0f}); + Attribute::ArrayData data{}; + data.sharing_info = ImplicitSharingPtr<>(sharing_info); + data.data = sharing_info->data.data(); + data.size = 4; + storage.add("we", AttrDomain::Corner, AttrType::Float, data); + storage.add("need", AttrDomain::Point, AttrType::Float, data); + storage.add("more", AttrDomain::Face, AttrType::Float, data); + storage.add("data", AttrDomain::Edge, AttrType::Float, data); + + /* The same data is shared among 4 attributes (as well as the original `data`). */ + EXPECT_EQ(sharing_info->strong_users(), 5); + storage.add("final!", AttrDomain::Edge, AttrType::Float, std::move(data)); + EXPECT_EQ(sharing_info->strong_users(), 5); + + { + const auto &data = std::get(storage.lookup("more")->data_for_write()); + const float *data_ptr = static_cast(data.data); + EXPECT_EQ(data_ptr[0], 1.5f); + EXPECT_EQ(data_ptr[1], 1.2f); + EXPECT_EQ(data_ptr[2], 1.1f); + EXPECT_EQ(data_ptr[3], 1.0f); + } + + int count = 0; + storage.foreach([&](const Attribute & /*attribute*/) { count++; }); + EXPECT_EQ(count, 5); +} + +TEST(attribute_storage, CopyConstruct) +{ + AttributeStorage storage; + + auto *sharing_info = new ImplicitSharedValue>(Span{1.5f, 1.2f, 1.1f, 1.0f}); + Attribute::ArrayData data{}; + data.sharing_info = ImplicitSharingPtr<>(sharing_info); + data.data = sharing_info->data.data(); + data.size = 4; + storage.add("foo", AttrDomain::Corner, AttrType::Float, std::move(data)); + + AttributeStorage copy{storage}; + + EXPECT_TRUE(copy.lookup("foo")); + EXPECT_EQ(copy.lookup("foo")->domain(), AttrDomain::Corner); + EXPECT_EQ(copy.lookup("foo")->data_type(), AttrType::Float); + { + const auto &data = std::get(copy.lookup("foo")->data()); + /* The data is shared, so it should be the same as the original. */ + EXPECT_EQ(data.data, sharing_info->data.data()); + } +} + +TEST(attribute_storage, MoveConstruct) +{ + AttributeStorage storage; + + auto *sharing_info = new ImplicitSharedValue>(Span{1.5f, 1.2f, 1.1f, 1.0f}); + Attribute::ArrayData data{}; + data.sharing_info = ImplicitSharingPtr<>(sharing_info); + data.data = sharing_info->data.data(); + data.size = 4; + storage.add("foo", AttrDomain::Corner, AttrType::Float, std::move(data)); + + AttributeStorage copy{std::move(storage)}; + + EXPECT_TRUE(copy.lookup("foo")); + EXPECT_EQ(copy.lookup("foo")->domain(), AttrDomain::Corner); + EXPECT_EQ(copy.lookup("foo")->data_type(), AttrType::Float); + { + const auto &data = std::get(copy.lookup("foo")->data()); + /* The data is shared, so it should be the same as the original. */ + EXPECT_EQ(data.data, sharing_info->data.data()); + } +} + +TEST(attribute_storage, UniqueNames) +{ + AttributeStorage storage; + + auto create_array_data = []() { + auto *sharing_info = new ImplicitSharedValue>( + Span{1.5f, 1.2f, 1.1f, 1.0f}); + Attribute::ArrayData data{}; + data.sharing_info = ImplicitSharingPtr<>(sharing_info); + data.data = sharing_info->data.data(); + data.size = 4; + return data; + }; + + storage.add("foo", AttrDomain::Corner, AttrType::Float, create_array_data()); + storage.add("foo_2", AttrDomain::Face, AttrType::Float, create_array_data()); + storage.add("foo_3", AttrDomain::Point, AttrType::Float, create_array_data()); + storage.add( + storage.unique_name_calc("foo"), AttrDomain::Edge, AttrType::Float, create_array_data()); + storage.add( + storage.unique_name_calc("foo"), AttrDomain::Corner, AttrType::Float, create_array_data()); + storage.add( + storage.unique_name_calc("foo_2"), AttrDomain::Point, AttrType::Float, create_array_data()); + + int count = 0; + storage.foreach([&](const Attribute & /*attribute*/) { count++; }); + EXPECT_EQ(count, 6); +} + +} // namespace blender::bke::tests diff --git a/source/blender/blenkernel/intern/curves.cc b/source/blender/blenkernel/intern/curves.cc index 16f1d36e75f..d2b37f3efd1 100644 --- a/source/blender/blenkernel/intern/curves.cc +++ b/source/blender/blenkernel/intern/curves.cc @@ -19,13 +19,16 @@ #include "BLI_index_range.hh" #include "BLI_math_matrix.hh" #include "BLI_rand.hh" +#include "BLI_resource_scope.hh" #include "BLI_span.hh" #include "BLI_string.h" #include "BLI_utildefines.h" #include "BLI_vector.hh" #include "BKE_anim_data.hh" +#include "BKE_attribute_legacy_convert.hh" #include "BKE_curves.hh" +#include "BKE_customdata.hh" #include "BKE_geometry_fields.hh" #include "BKE_geometry_set.hh" #include "BKE_idtype.hh" @@ -108,8 +111,9 @@ static void curves_blend_write(BlendWriter *writer, ID *id, const void *id_addre /* Only for forward compatibility. */ curves->attributes_active_index_legacy = curves->geometry.attributes_active_index; - blender::bke::CurvesGeometry::BlendWriteData write_data = - curves->geometry.wrap().blend_write_prepare(); + blender::ResourceScope scope; + blender::bke::CurvesGeometry::BlendWriteData write_data(scope); + curves->geometry.wrap().blend_write_prepare(write_data); /* Write LibData */ BLO_write_id_struct(writer, Curves, id_address, &curves->id); diff --git a/source/blender/blenkernel/intern/curves_geometry.cc b/source/blender/blenkernel/intern/curves_geometry.cc index 58119a416ea..ad8a6f36b5f 100644 --- a/source/blender/blenkernel/intern/curves_geometry.cc +++ b/source/blender/blenkernel/intern/curves_geometry.cc @@ -26,7 +26,10 @@ #include "DNA_material_types.h" #include "BKE_attribute.hh" +#include "BKE_attribute_legacy_convert.hh" #include "BKE_attribute_math.hh" +#include "BKE_attribute_storage.hh" +#include "BKE_attribute_storage_blend_write.hh" #include "BKE_bake_data_block_id.hh" #include "BKE_curves.hh" #include "BKE_curves_utils.hh" @@ -65,6 +68,7 @@ CurvesGeometry::CurvesGeometry(const int point_num, const int curve_num) this->curve_num = curve_num; CustomData_reset(&this->point_data); CustomData_reset(&this->curve_data); + new (&this->attribute_storage.wrap()) blender::bke::AttributeStorage(); BLI_listbase_clear(&this->vertex_group_names); this->attributes_for_write().add( @@ -108,6 +112,8 @@ CurvesGeometry::CurvesGeometry(const CurvesGeometry &other) CustomData_init_from(&other.point_data, &this->point_data, CD_MASK_ALL, other.point_num); CustomData_init_from(&other.curve_data, &this->curve_data, CD_MASK_ALL, other.curve_num); + new (&this->attribute_storage.wrap()) AttributeStorage(other.attribute_storage.wrap()); + this->point_num = other.point_num; this->curve_num = other.curve_num; @@ -167,6 +173,9 @@ CurvesGeometry::CurvesGeometry(CurvesGeometry &&other) this->curve_data = other.curve_data; CustomData_reset(&other.curve_data); + new (&this->attribute_storage.wrap()) + AttributeStorage(std::move(other.attribute_storage.wrap())); + this->point_num = other.point_num; other.point_num = 0; @@ -200,6 +209,7 @@ CurvesGeometry::~CurvesGeometry() { CustomData_free(&this->point_data); CustomData_free(&this->curve_data); + this->attribute_storage.wrap().~AttributeStorage(); BLI_freelistN(&this->vertex_group_names); if (this->runtime) { implicit_sharing::free_shared_data(&this->curve_offsets, @@ -1844,6 +1854,7 @@ void CurvesGeometry::blend_read(BlendDataReader &reader) CustomData_blend_read(&reader, &this->point_data, this->point_num); CustomData_blend_read(&reader, &this->curve_data, this->curve_num); + this->attribute_storage.wrap().blend_read(reader); if (this->curve_offsets) { this->runtime->curve_offsets_sharing_info = BLO_read_shared( @@ -1853,6 +1864,9 @@ void CurvesGeometry::blend_read(BlendDataReader &reader) }); } + /* Forward compatibility. To be removed when runtime format changes. */ + curves_convert_storage_to_customdata(*this); + BLO_read_struct_list(&reader, bDeformGroup, &this->vertex_group_names); if (this->custom_knot_num) { @@ -1867,12 +1881,24 @@ void CurvesGeometry::blend_read(BlendDataReader &reader) this->update_curve_types(); } -CurvesGeometry::BlendWriteData CurvesGeometry::blend_write_prepare() +void CurvesGeometry::blend_write_prepare(CurvesGeometry::BlendWriteData &write_data) { - CurvesGeometry::BlendWriteData write_data; - CustomData_blend_write_prepare(this->point_data, write_data.point_layers); - CustomData_blend_write_prepare(this->curve_data, write_data.curve_layers); - return write_data; + attribute_storage_blend_write_prepare(this->attribute_storage.wrap(), + {{AttrDomain::Point, &write_data.point_layers}, + {AttrDomain::Curve, &write_data.curve_layers}}, + write_data.attribute_data); + CustomData_blend_write_prepare(this->point_data, + AttrDomain::Point, + this->points_num(), + write_data.point_layers, + write_data.attribute_data); + CustomData_blend_write_prepare(this->curve_data, + AttrDomain::Curve, + this->curves_num(), + write_data.curve_layers, + write_data.attribute_data); + this->attribute_storage.dna_attributes = write_data.attribute_data.attributes.data(); + this->attribute_storage.dna_attributes_num = write_data.attribute_data.attributes.size(); } void CurvesGeometry::blend_write(BlendWriter &writer, @@ -1883,6 +1909,7 @@ void CurvesGeometry::blend_write(BlendWriter &writer, &writer, &this->point_data, write_data.point_layers, this->point_num, CD_MASK_ALL, &id); CustomData_blend_write( &writer, &this->curve_data, write_data.curve_layers, this->curve_num, CD_MASK_ALL, &id); + this->attribute_storage.wrap().blend_write(writer, write_data.attribute_data); if (this->curve_offsets) { BLO_write_shared( diff --git a/source/blender/blenkernel/intern/customdata.cc b/source/blender/blenkernel/intern/customdata.cc index 2caf204f226..a853a659f04 100644 --- a/source/blender/blenkernel/intern/customdata.cc +++ b/source/blender/blenkernel/intern/customdata.cc @@ -29,6 +29,7 @@ #include "BLI_memory_counter.hh" #include "BLI_mempool.h" #include "BLI_path_utils.hh" +#include "BLI_resource_scope.hh" #include "BLI_set.hh" #include "BLI_span.hh" #include "BLI_string.h" @@ -44,7 +45,9 @@ #include "BLT_translation.hh" #include "BKE_anonymous_attribute_id.hh" +#include "BKE_attribute_legacy_convert.hh" #include "BKE_attribute_math.hh" +#include "BKE_attribute_storage.hh" #include "BKE_customdata.hh" #include "BKE_customdata_file.h" #include "BKE_deform.hh" @@ -5113,18 +5116,47 @@ static void get_type_file_write_info(const eCustomDataType type, } void CustomData_blend_write_prepare(CustomData &data, + const blender::bke::AttrDomain domain, + const int domain_size, Vector &layers_to_write, - const Set &skip_names) + blender::bke::AttributeStorage::BlendWriteData &write_data) { + using namespace blender::bke; for (const CustomDataLayer &layer : Span(data.layers, data.totlayer)) { if (layer.flag & CD_FLAG_NOCOPY) { continue; } - if (blender::bke::attribute_name_is_anonymous(layer.name)) { + const StringRef name = layer.name; + if (attribute_name_is_anonymous(name)) { continue; } - if (skip_names.contains(layer.name)) { - continue; + if (U.experimental.use_attribute_storage_write) { + /* When this experimental option is turned on, we always write the data in the new + * #AttributeStorage format, even though it's not yet used at runtime. This is meant for + * testing the forward compatibility capabilities meant to be shipped in version 4.5, in + * other words, the ability to read #AttributeStorage and convert it to #CustomData. This + * block should be removed when the new format is used at runtime. */ + const eCustomDataType data_type = eCustomDataType(layer.type); + if (const std::optional type = custom_data_type_to_attr_type(data_type)) { + ::Attribute attribute_dna{}; + attribute_dna.name = layer.name; + attribute_dna.data_type = int16_t(*type); + attribute_dna.domain = int8_t(domain); + attribute_dna.storage_type = int8_t(AttrStorageType::Array); + + /* Do not increase the user count; #::AttributeArray does not act as an owner of the + * attribute data, since it's only used temporarily for writing files. Changing the user + * count would be okay too, but it's unnecessary because none of this data should be + * modified while it's being written anyway. */ + auto &array_dna = write_data.scope.construct<::AttributeArray>(); + array_dna.data = layer.data; + array_dna.sharing_info = layer.sharing_info; + array_dna.size = domain_size; + attribute_dna.data = &array_dna; + + write_data.attributes.append(attribute_dna); + continue; + } } layers_to_write.append(layer); } diff --git a/source/blender/blenkernel/intern/grease_pencil.cc b/source/blender/blenkernel/intern/grease_pencil.cc index 7c421cbaeca..2851ca0a201 100644 --- a/source/blender/blenkernel/intern/grease_pencil.cc +++ b/source/blender/blenkernel/intern/grease_pencil.cc @@ -13,6 +13,9 @@ #include "BKE_anim_data.hh" #include "BKE_animsys.h" #include "BKE_asset_edit.hh" +#include "BKE_attribute_legacy_convert.hh" +#include "BKE_attribute_storage.hh" +#include "BKE_attribute_storage_blend_write.hh" #include "BKE_bake_data_block_id.hh" #include "BKE_curves.hh" #include "BKE_customdata.hh" @@ -44,6 +47,7 @@ #include "BLI_memarena.h" #include "BLI_memory_utils.hh" #include "BLI_polyfill_2d.h" +#include "BLI_resource_scope.hh" #include "BLI_span.hh" #include "BLI_stack.hh" #include "BLI_string.h" @@ -83,7 +87,9 @@ static const char *ATTR_POSITION = "position"; /* Forward declarations. */ static void read_drawing_array(GreasePencil &grease_pencil, BlendDataReader *reader); -static void write_drawing_array(GreasePencil &grease_pencil, BlendWriter *writer); +static void write_drawing_array(GreasePencil &grease_pencil, + blender::ResourceScope &scope, + BlendWriter *writer); static void free_drawing_array(GreasePencil &grease_pencil); static void read_layer_tree(GreasePencil &grease_pencil, BlendDataReader *reader); @@ -102,6 +108,7 @@ static void grease_pencil_init_data(ID *id) grease_pencil->set_active_node(nullptr); CustomData_reset(&grease_pencil->layers_data); + new (&grease_pencil->attribute_storage.wrap()) blender::bke::AttributeStorage(); grease_pencil->runtime = MEM_new(__func__); } @@ -197,6 +204,8 @@ static void grease_pencil_copy_data(Main * /*bmain*/, &grease_pencil_dst->layers_data, CD_MASK_ALL, grease_pencil_dst->layers().size()); + new (&grease_pencil_dst->attribute_storage.wrap()) + blender::bke::AttributeStorage(grease_pencil_src->attribute_storage.wrap()); BKE_defgroup_copy_list(&grease_pencil_dst->vertex_group_names, &grease_pencil_src->vertex_group_names); @@ -220,6 +229,7 @@ static void grease_pencil_free_data(ID *id) MEM_SAFE_FREE(grease_pencil->material_array); CustomData_free(&grease_pencil->layers_data); + grease_pencil->attribute_storage.wrap().~AttributeStorage(); free_drawing_array(*grease_pencil); MEM_delete(&grease_pencil->root_group()); @@ -254,10 +264,24 @@ static void grease_pencil_foreach_id(ID *id, LibraryForeachIDData *data) static void grease_pencil_blend_write(BlendWriter *writer, ID *id, const void *id_address) { + using namespace blender; + using namespace blender::bke; GreasePencil *grease_pencil = reinterpret_cast(id); + blender::ResourceScope scope; + blender::Vector layers_data_layers; - CustomData_blend_write_prepare(grease_pencil->layers_data, layers_data_layers); + blender::bke::AttributeStorage::BlendWriteData attribute_data{scope}; + attribute_storage_blend_write_prepare(grease_pencil->attribute_storage.wrap(), + {{AttrDomain::Layer, &layers_data_layers}}, + attribute_data); + CustomData_blend_write_prepare(grease_pencil->layers_data, + AttrDomain::Layer, + grease_pencil->layers().size(), + layers_data_layers, + attribute_data); + grease_pencil->attribute_storage.dna_attributes = attribute_data.attributes.data(); + grease_pencil->attribute_storage.dna_attributes_num = attribute_data.attributes.size(); /* Write LibData */ BLO_write_id_struct(writer, GreasePencil, id_address, &grease_pencil->id); @@ -269,9 +293,10 @@ static void grease_pencil_blend_write(BlendWriter *writer, ID *id, const void *i grease_pencil->layers().size(), CD_MASK_ALL, id); + grease_pencil->attribute_storage.wrap().blend_write(*writer, attribute_data); /* Write drawings. */ - write_drawing_array(*grease_pencil, writer); + write_drawing_array(*grease_pencil, scope, writer); /* Write layer tree. */ write_layer_tree(*grease_pencil, writer); @@ -293,6 +318,10 @@ static void grease_pencil_blend_read_data(BlendDataReader *reader, ID *id) read_layer_tree(*grease_pencil, reader); CustomData_blend_read(reader, &grease_pencil->layers_data, grease_pencil->layers().size()); + grease_pencil->attribute_storage.wrap().blend_read(*reader); + + /* Forward compatibility. To be removed when runtime format changes. */ + blender::bke::grease_pencil_convert_storage_to_customdata(*grease_pencil); /* Read materials. */ BLO_read_pointer_array(reader, @@ -4212,7 +4241,9 @@ static void read_drawing_array(GreasePencil &grease_pencil, BlendDataReader *rea } } -static void write_drawing_array(GreasePencil &grease_pencil, BlendWriter *writer) +static void write_drawing_array(GreasePencil &grease_pencil, + blender::ResourceScope &scope, + BlendWriter *writer) { using namespace blender; BLO_write_pointer_array(writer, grease_pencil.drawing_array_num, grease_pencil.drawing_array); @@ -4221,10 +4252,13 @@ static void write_drawing_array(GreasePencil &grease_pencil, BlendWriter *writer switch (GreasePencilDrawingType(drawing_base->type)) { case GP_DRAWING: { GreasePencilDrawing *drawing = reinterpret_cast(drawing_base); - bke::CurvesGeometry::BlendWriteData write_data = - drawing->wrap().strokes_for_write().blend_write_prepare(); + bke::CurvesGeometry &curves = drawing->wrap().strokes_for_write(); + + bke::CurvesGeometry::BlendWriteData write_data(scope); + curves.blend_write_prepare(write_data); + BLO_write_struct(writer, GreasePencilDrawing, drawing); - drawing->wrap().strokes_for_write().blend_write(*writer, grease_pencil.id, write_data); + curves.blend_write(*writer, grease_pencil.id, write_data); break; } case GP_DRAWING_REFERENCE: { diff --git a/source/blender/blenkernel/intern/mesh.cc b/source/blender/blenkernel/intern/mesh.cc index 1c0cf39d098..9bc14b79ddd 100644 --- a/source/blender/blenkernel/intern/mesh.cc +++ b/source/blender/blenkernel/intern/mesh.cc @@ -29,6 +29,7 @@ #include "BLI_math_matrix.hh" #include "BLI_math_vector.hh" #include "BLI_memory_counter.hh" +#include "BLI_resource_scope.hh" #include "BLI_set.hh" #include "BLI_span.hh" #include "BLI_string.h" @@ -43,6 +44,9 @@ #include "BKE_anim_data.hh" #include "BKE_anonymous_attribute_id.hh" #include "BKE_attribute.hh" +#include "BKE_attribute_legacy_convert.hh" +#include "BKE_attribute_storage.hh" +#include "BKE_attribute_storage_blend_write.hh" #include "BKE_bake_data_block_id.hh" #include "BKE_bpath.hh" #include "BKE_deform.hh" @@ -93,6 +97,7 @@ static void mesh_init_data(ID *id) CustomData_reset(&mesh->face_data); CustomData_reset(&mesh->corner_data); + new (&mesh->attribute_storage.wrap()) blender::bke::AttributeStorage(); mesh->runtime = new blender::bke::MeshRuntime(); mesh->face_sets_color_seed = BLI_hash_int(BLI_time_now_seconds_i() & UINT_MAX); @@ -205,6 +210,8 @@ static void mesh_copy_data(Main *bmain, &mesh_src->corner_data, &mesh_dst->corner_data, mask.lmask, mesh_dst->corners_num); CustomData_init_from( &mesh_src->face_data, &mesh_dst->face_data, mask.pmask, mesh_dst->faces_num); + new (&mesh_dst->attribute_storage.wrap()) + blender::bke::AttributeStorage(mesh_src->attribute_storage.wrap()); blender::implicit_sharing::copy_shared_pointer(mesh_src->face_offset_indices, mesh_src->runtime->face_offsets_sharing_info, &mesh_dst->face_offset_indices, @@ -241,6 +248,7 @@ static void mesh_free_data(ID *id) BLI_freelistN(&mesh->vertex_group_names); MEM_SAFE_FREE(mesh->active_color_attribute); MEM_SAFE_FREE(mesh->default_color_attribute); + mesh->attribute_storage.wrap().~AttributeStorage(); if (mesh->face_offset_indices) { blender::implicit_sharing::free_shared_data(&mesh->face_offset_indices, &mesh->runtime->face_offsets_sharing_info); @@ -330,10 +338,12 @@ static void mesh_blend_write(BlendWriter *writer, ID *id, const void *id_address Mesh *mesh = reinterpret_cast(id); const bool is_undo = BLO_write_is_undo(writer); + ResourceScope scope; Vector vert_layers; Vector edge_layers; Vector loop_layers; Vector face_layers; + bke::AttributeStorage::BlendWriteData attribute_data{scope}; /* Cache only - don't write. */ mesh->mface = nullptr; @@ -356,10 +366,22 @@ static void mesh_blend_write(BlendWriter *writer, ID *id, const void *id_address mesh->face_offset_indices = nullptr; } else { - CustomData_blend_write_prepare(mesh->vert_data, vert_layers, {}); - CustomData_blend_write_prepare(mesh->edge_data, edge_layers, {}); - CustomData_blend_write_prepare(mesh->corner_data, loop_layers, {}); - CustomData_blend_write_prepare(mesh->face_data, face_layers, {}); + attribute_storage_blend_write_prepare(mesh->attribute_storage.wrap(), + {{AttrDomain::Point, &vert_layers}, + {AttrDomain::Edge, &edge_layers}, + {AttrDomain::Face, &face_layers}, + {AttrDomain::Corner, &loop_layers}}, + attribute_data); + CustomData_blend_write_prepare( + mesh->vert_data, AttrDomain::Point, mesh->verts_num, vert_layers, attribute_data); + CustomData_blend_write_prepare( + mesh->edge_data, AttrDomain::Edge, mesh->edges_num, edge_layers, attribute_data); + CustomData_blend_write_prepare( + mesh->face_data, AttrDomain::Face, mesh->faces_num, face_layers, attribute_data); + CustomData_blend_write_prepare( + mesh->corner_data, AttrDomain::Corner, mesh->corners_num, loop_layers, attribute_data); + mesh->attribute_storage.dna_attributes = attribute_data.attributes.data(); + mesh->attribute_storage.dna_attributes_num = attribute_data.attributes.size(); if (!is_undo) { /* Write forward compatible format. To be removed in 5.0. */ rename_seam_layer_to_old_name( @@ -394,6 +416,8 @@ static void mesh_blend_write(BlendWriter *writer, ID *id, const void *id_address CustomData_blend_write( writer, &mesh->face_data, face_layers, mesh->faces_num, CD_MASK_MESH.pmask, &mesh->id); + mesh->attribute_storage.wrap().blend_write(*writer, attribute_data); + if (mesh->face_offset_indices) { BLO_write_shared( writer, @@ -432,6 +456,7 @@ static void mesh_blend_read_data(BlendDataReader *reader, ID *id) CustomData_blend_read(reader, &mesh->fdata_legacy, mesh->totface_legacy); CustomData_blend_read(reader, &mesh->corner_data, mesh->corners_num); CustomData_blend_read(reader, &mesh->face_data, mesh->faces_num); + mesh->attribute_storage.wrap().blend_read(*reader); if (mesh->deform_verts().is_empty()) { /* Vertex group data was also an owning pointer in old Blender versions. * Don't read them again if they were read as part of #CustomData. */ @@ -440,6 +465,9 @@ static void mesh_blend_read_data(BlendDataReader *reader, ID *id) BLO_read_string(reader, &mesh->active_color_attribute); BLO_read_string(reader, &mesh->default_color_attribute); + /* Forward compatibility. To be removed when runtime format changes. */ + blender::bke::mesh_convert_storage_to_customdata(*mesh); + mesh->texspace_flag &= ~ME_TEXSPACE_FLAG_AUTO_EVALUATED; mesh->runtime = new blender::bke::MeshRuntime(); @@ -629,6 +657,7 @@ static void mesh_clear_geometry(Mesh &mesh) CustomData_free(&mesh.fdata_legacy); CustomData_free(&mesh.corner_data); CustomData_free(&mesh.face_data); + mesh.attribute_storage.wrap() = blender::bke::AttributeStorage(); if (mesh.face_offset_indices) { blender::implicit_sharing::free_shared_data(&mesh.face_offset_indices, &mesh.runtime->face_offsets_sharing_info); diff --git a/source/blender/blenkernel/intern/pointcloud.cc b/source/blender/blenkernel/intern/pointcloud.cc index 67d7c8cf365..95e133528c9 100644 --- a/source/blender/blenkernel/intern/pointcloud.cc +++ b/source/blender/blenkernel/intern/pointcloud.cc @@ -18,11 +18,15 @@ #include "BLI_bounds.hh" #include "BLI_index_range.hh" #include "BLI_rand.h" +#include "BLI_resource_scope.hh" #include "BLI_span.hh" #include "BLI_utildefines.h" #include "BLI_vector.hh" #include "BKE_anim_data.hh" +#include "BKE_attribute_legacy_convert.hh" +#include "BKE_attribute_storage.hh" +#include "BKE_attribute_storage_blend_write.hh" #include "BKE_bake_data_block_id.hh" #include "BKE_customdata.hh" #include "BKE_geometry_set.hh" @@ -63,6 +67,7 @@ static void pointcloud_init_data(ID *id) MEMCPY_STRUCT_AFTER(pointcloud, DNA_struct_default_get(PointCloud), id); + new (&pointcloud->attribute_storage.wrap()) blender::bke::AttributeStorage(); pointcloud->runtime = new blender::bke::PointCloudRuntime(); CustomData_reset(&pointcloud->pdata); @@ -82,6 +87,8 @@ static void pointcloud_copy_data(Main * /*bmain*/, CustomData_init_from( &pointcloud_src->pdata, &pointcloud_dst->pdata, CD_MASK_ALL, pointcloud_dst->totpoint); + new (&pointcloud_dst->attribute_storage.wrap()) + blender::bke::AttributeStorage(pointcloud_src->attribute_storage.wrap()); pointcloud_dst->runtime = new blender::bke::PointCloudRuntime(); pointcloud_dst->runtime->bounds_cache = pointcloud_src->runtime->bounds_cache; @@ -103,6 +110,7 @@ static void pointcloud_free_data(ID *id) BKE_animdata_free(&pointcloud->id, false); BKE_pointcloud_batch_cache_free(pointcloud); CustomData_free(&pointcloud->pdata); + pointcloud->attribute_storage.wrap().~AttributeStorage(); MEM_SAFE_FREE(pointcloud->mat); delete pointcloud->runtime; } @@ -117,10 +125,19 @@ static void pointcloud_foreach_id(ID *id, LibraryForeachIDData *data) static void pointcloud_blend_write(BlendWriter *writer, ID *id, const void *id_address) { + using namespace blender; + using namespace blender::bke; PointCloud *pointcloud = (PointCloud *)id; + ResourceScope scope; Vector point_layers; - CustomData_blend_write_prepare(pointcloud->pdata, point_layers); + bke::AttributeStorage::BlendWriteData attribute_data{scope}; + attribute_storage_blend_write_prepare( + pointcloud->attribute_storage.wrap(), {{AttrDomain::Point, &point_layers}}, attribute_data); + CustomData_blend_write_prepare( + pointcloud->pdata, AttrDomain::Point, pointcloud->totpoint, point_layers, attribute_data); + pointcloud->attribute_storage.dna_attributes = attribute_data.attributes.data(); + pointcloud->attribute_storage.dna_attributes_num = attribute_data.attributes.size(); /* Write LibData */ BLO_write_id_struct(writer, PointCloud, id_address, &pointcloud->id); @@ -133,6 +150,7 @@ static void pointcloud_blend_write(BlendWriter *writer, ID *id, const void *id_a pointcloud->totpoint, CD_MASK_ALL, &pointcloud->id); + pointcloud->attribute_storage.wrap().blend_write(*writer, attribute_data); BLO_write_pointer_array(writer, pointcloud->totcol, pointcloud->mat); } @@ -143,6 +161,10 @@ static void pointcloud_blend_read_data(BlendDataReader *reader, ID *id) /* Geometry */ CustomData_blend_read(reader, &pointcloud->pdata, pointcloud->totpoint); + pointcloud->attribute_storage.wrap().blend_read(*reader); + + /* Forward compatibility. To be removed when runtime format changes. */ + blender::bke::pointcloud_convert_storage_to_customdata(*pointcloud); /* Materials */ BLO_read_pointer_array(reader, pointcloud->totcol, (void **)&pointcloud->mat); diff --git a/source/blender/makesdna/DNA_attribute_types.h b/source/blender/makesdna/DNA_attribute_types.h new file mode 100644 index 00000000000..2f236239d12 --- /dev/null +++ b/source/blender/makesdna/DNA_attribute_types.h @@ -0,0 +1,69 @@ +/* SPDX-FileCopyrightText: 2025 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#pragma once + +#include + +#include "BLI_implicit_sharing.h" + +#ifdef __cplusplus + +namespace blender::bke { +class AttributeStorage; +class AttributeStorageRuntime; +} // namespace blender::bke + +using AttributeStorageRuntimeHandle = blender::bke::AttributeStorageRuntime; +#else +typedef struct AttributeStorageRuntimeHandle AttributeStorageRuntimeHandle; +#endif + +/** DNA data for bke::Attribute::ArrayData. */ +struct AttributeArray { + void *data; + const ImplicitSharingInfoHandle *sharing_info; + /* The number of elements in the array. */ + int64_t size; +}; + +/** DNA data for bke::Attribute::SingleData. */ +struct AttributeSingle { + void *data; + const ImplicitSharingInfoHandle *sharing_info; +}; + +/** DNA data for bke::Attribute. */ +struct Attribute { + const char *name; + /* bke::AttrType. */ + int16_t data_type; + /* bke::AttrDomain. */ + int8_t domain; + /* bke::AttrStorageType */ + int8_t storage_type; + char _pad[4]; + + /** Type depends on storage type. */ + void *data; +}; + +/** + * The DNA type for storing attribute values. Logic at runtime is handled by the runtime struct. + * The DNA data here is created just for file reading and writing. + */ +struct AttributeStorage { + /* Array only used in files, otherwise #AttributeStorageRuntime::attributes is used. */ + struct Attribute *dna_attributes; + int dna_attributes_num; + + char _pad[4]; + + AttributeStorageRuntimeHandle *runtime; + +#ifdef __cplusplus + blender::bke::AttributeStorage &wrap(); + const blender::bke::AttributeStorage &wrap() const; +#endif +}; diff --git a/source/blender/makesdna/DNA_curves_types.h b/source/blender/makesdna/DNA_curves_types.h index 10d49a6fe5c..bce010e9d10 100644 --- a/source/blender/makesdna/DNA_curves_types.h +++ b/source/blender/makesdna/DNA_curves_types.h @@ -9,6 +9,7 @@ #pragma once #include "DNA_ID.h" +#include "DNA_attribute_types.h" #include "DNA_customdata_types.h" #include "DNA_listBase.h" @@ -116,6 +117,12 @@ typedef struct CurvesGeometry { */ int *curve_offsets; + /** + * Curve and point domain attributes. Currently unused at runtime, but used for forward + * compatibility when reading files (see #122398). + */ + struct AttributeStorage attribute_storage; + /** * All attributes stored on control points (#AttrDomain::Point). * This might not contain a layer for positions if there are no points. diff --git a/source/blender/makesdna/DNA_grease_pencil_types.h b/source/blender/makesdna/DNA_grease_pencil_types.h index afc4f3417d5..79e9e33084a 100644 --- a/source/blender/makesdna/DNA_grease_pencil_types.h +++ b/source/blender/makesdna/DNA_grease_pencil_types.h @@ -9,6 +9,7 @@ #pragma once #include "DNA_ID.h" +#include "DNA_attribute_types.h" #include "DNA_curve_types.h" #include "DNA_curves_types.h" #include "DNA_listBase.h" @@ -466,6 +467,13 @@ typedef struct GreasePencil { * All attributes stored on the grease pencil layers (#AttrDomain::Layer). */ CustomData layers_data; + + /** + * Layer domain attributes. Currently unused at runtime, but used for forward + * compatibility when reading files (see #122398). + */ + struct AttributeStorage attribute_storage; + /** * The index of the active attribute in the UI. * diff --git a/source/blender/makesdna/DNA_mesh_types.h b/source/blender/makesdna/DNA_mesh_types.h index 08ea531f764..e7c5900ef67 100644 --- a/source/blender/makesdna/DNA_mesh_types.h +++ b/source/blender/makesdna/DNA_mesh_types.h @@ -9,6 +9,7 @@ #pragma once #include "DNA_ID.h" +#include "DNA_attribute_types.h" #include "DNA_customdata_types.h" #include "DNA_defs.h" #include "DNA_session_uid_types.h" @@ -93,6 +94,12 @@ typedef struct Mesh { */ int *face_offset_indices; + /** + * Vertex, edge, face, and corner generic attributes. Currently unused at runtime, but used for + * forward compatibility when reading files (see #122398). + */ + struct AttributeStorage attribute_storage; + CustomData vert_data; CustomData edge_data; CustomData face_data; diff --git a/source/blender/makesdna/DNA_pointcloud_types.h b/source/blender/makesdna/DNA_pointcloud_types.h index 1dd481d8b98..66a096f8eb3 100644 --- a/source/blender/makesdna/DNA_pointcloud_types.h +++ b/source/blender/makesdna/DNA_pointcloud_types.h @@ -9,6 +9,7 @@ #pragma once #include "DNA_ID.h" +#include "DNA_attribute_types.h" #include "DNA_customdata_types.h" #ifdef __cplusplus @@ -50,6 +51,12 @@ typedef struct PointCloud { /* Geometry */ int totpoint; + /** + * Storage for generic attributes. Currently unused at runtime, but used for forward + * compatibility when reading files (see #122398). + */ + struct AttributeStorage attribute_storage; + /* Custom Data */ struct CustomData pdata; /** Set to -1 when none is active. */ diff --git a/source/blender/makesdna/DNA_userdef_types.h b/source/blender/makesdna/DNA_userdef_types.h index 76b3265985d..fcc68942d17 100644 --- a/source/blender/makesdna/DNA_userdef_types.h +++ b/source/blender/makesdna/DNA_userdef_types.h @@ -759,6 +759,7 @@ typedef struct UserDef_Experimental { char use_extensions_debug; char use_recompute_usercount_on_save_debug; char write_large_blend_file_blocks; + char use_attribute_storage_write; char SANITIZE_AFTER_HERE; /* The following options are automatically sanitized (set to 0) * when the release cycle is not alpha. */ @@ -769,7 +770,7 @@ typedef struct UserDef_Experimental { char use_new_volume_nodes; char use_shader_node_previews; char use_bundle_and_closure_nodes; - char _pad[5]; + char _pad[4]; } UserDef_Experimental; #define USER_EXPERIMENTAL_TEST(userdef, member) \ diff --git a/source/blender/makesrna/intern/rna_userdef.cc b/source/blender/makesrna/intern/rna_userdef.cc index d72fec46e19..b27e6e3114a 100644 --- a/source/blender/makesrna/intern/rna_userdef.cc +++ b/source/blender/makesrna/intern/rna_userdef.cc @@ -7635,6 +7635,12 @@ static void rna_def_userdef_experimental(BlenderRNA *brna) "Recompute all ID usercounts before saving to a blendfile. Allows to " "work around invalid usercount handling in code that may lead to loss " "of data due to wrongly detected unused data-blocks"); + + prop = RNA_def_property(srna, "use_attribute_storage_write", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_ui_text(prop, + "Write New Attribute Storage Format", + "Instead of writing with the older \"CustomData\" format for forward " + "compatibility, use the new \"AttributeStorage\" format"); } static void rna_def_userdef_addon_collection(BlenderRNA *brna, PropertyRNA *cprop)