As described in #122398, implement read and write support for a new attribute storage system. Currently this is only implemented to support forward compatibility; the format used at runtime isn't changed at all. That can be done one step at a time during the 4.5 and 5.0 development cycles. A new experimental option for testing tells Blender to always save with the new format. The main benefit of the new structure is that it matches the attribute system design, it allows for future attribute storage optimization, and each attribute is an allocated struct, which will give pointer stability for the Python API. The next step is to connect the attribute API and the RNA API to AttributeStorage for the simplest geometry type, point clouds. Pull Request: https://projects.blender.org/blender/blender/pulls/133874
540 lines
18 KiB
C++
540 lines
18 KiB
C++
/* 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<void(Attribute &)> fn)
|
|
{
|
|
for (const std::unique_ptr<Attribute> &attribute : this->runtime->attributes) {
|
|
fn(*attribute);
|
|
}
|
|
}
|
|
void AttributeStorage::foreach(FunctionRef<void(const Attribute &)> fn) const
|
|
{
|
|
for (const std::unique_ptr<Attribute> &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<ArrayDataImplicitSharing>(__func__, data, size, type);
|
|
}
|
|
|
|
AttrStorageType Attribute::storage_type() const
|
|
{
|
|
if (std::get_if<Attribute::ArrayData>(&data_)) {
|
|
return AttrStorageType::Array;
|
|
}
|
|
if (std::get_if<Attribute::SingleData>(&data_)) {
|
|
return AttrStorageType::Single;
|
|
}
|
|
BLI_assert_unreachable();
|
|
return AttrStorageType::Array;
|
|
}
|
|
|
|
Attribute::DataVariant &Attribute::data_for_write()
|
|
{
|
|
if (auto *data = std::get_if<Attribute::ArrayData>(&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<Attribute::SingleData>(&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<AttributeStorageRuntime>(__func__);
|
|
}
|
|
|
|
AttributeStorage::AttributeStorage(const AttributeStorage &other)
|
|
{
|
|
this->dna_attributes = nullptr;
|
|
this->dna_attributes_num = 0;
|
|
this->runtime = MEM_new<AttributeStorageRuntime>(__func__);
|
|
this->runtime->attributes.reserve(other.runtime->attributes.size());
|
|
other.foreach([&](const Attribute &attribute) {
|
|
this->runtime->attributes.add_new(std::make_unique<Attribute>(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<AttributeStorageRuntime>(__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<blender::bke::Attribute> *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<blender::bke::Attribute> *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<Attribute> ptr = std::make_unique<Attribute>();
|
|
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<int8_t **>(data));
|
|
return;
|
|
case int8_t(AttrType::Int8):
|
|
BLO_read_int8_array(&reader, size, reinterpret_cast<int8_t **>(data));
|
|
return;
|
|
case int8_t(AttrType::Int16_2D):
|
|
BLO_read_int16_array(&reader, size * 2, reinterpret_cast<int16_t **>(data));
|
|
return;
|
|
case int8_t(AttrType::Int32):
|
|
BLO_read_int32_array(&reader, size, reinterpret_cast<int32_t **>(data));
|
|
return;
|
|
case int8_t(AttrType::Int32_2D):
|
|
BLO_read_int32_array(&reader, size * 2, reinterpret_cast<int32_t **>(data));
|
|
return;
|
|
case int8_t(AttrType::Float):
|
|
BLO_read_float_array(&reader, size, reinterpret_cast<float **>(data));
|
|
return;
|
|
case int8_t(AttrType::Float2):
|
|
BLO_read_float_array(&reader, size * 2, reinterpret_cast<float **>(data));
|
|
return;
|
|
case int8_t(AttrType::Float3):
|
|
BLO_read_float3_array(&reader, size, reinterpret_cast<float **>(data));
|
|
return;
|
|
case int8_t(AttrType::Float4x4):
|
|
BLO_read_float_array(&reader, size * 16, reinterpret_cast<float **>(data));
|
|
return;
|
|
case int8_t(AttrType::ColorByte):
|
|
BLO_read_uint8_array(&reader, size * 4, reinterpret_cast<uint8_t **>(data));
|
|
return;
|
|
case int8_t(AttrType::ColorFloat):
|
|
BLO_read_float_array(&reader, size * 4, reinterpret_cast<float **>(data));
|
|
return;
|
|
case int8_t(AttrType::Quaternion):
|
|
BLO_read_float_array(&reader, size * 4, reinterpret_cast<float **>(data));
|
|
return;
|
|
case int8_t(AttrType::String):
|
|
BLO_read_struct_array(
|
|
&reader, MStringProperty, size, reinterpret_cast<MStringProperty **>(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<ArrayDataImplicitSharing>(func, *data, size, cpp_type);
|
|
});
|
|
}
|
|
|
|
static std::optional<Attribute::DataVariant> 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<AttrDomain> 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<AttributeStorageRuntime>(__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<AttrDomain> domain = read_attr_domain(dna_attr.domain);
|
|
if (!domain) {
|
|
continue;
|
|
}
|
|
|
|
std::optional<Attribute::DataVariant> data = read_attr_data(
|
|
reader, dna_attr.storage_type, dna_attr.data_type, dna_attr);
|
|
if (!data) {
|
|
continue;
|
|
}
|
|
|
|
std::unique_ptr<Attribute> attribute = std::make_unique<Attribute>();
|
|
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<const int8_t *>(data));
|
|
break;
|
|
case AttrType::Int8:
|
|
BLO_write_int8_array(&writer, size, static_cast<const int8_t *>(data));
|
|
break;
|
|
case AttrType::Int16_2D:
|
|
BLO_write_int16_array(&writer, size * 2, static_cast<const int16_t *>(data));
|
|
break;
|
|
case AttrType::Int32:
|
|
BLO_write_int32_array(&writer, size, static_cast<const int32_t *>(data));
|
|
break;
|
|
case AttrType::Int32_2D:
|
|
BLO_write_int32_array(&writer, size * 2, static_cast<const int32_t *>(data));
|
|
break;
|
|
case AttrType::Float:
|
|
BLO_write_float_array(&writer, size, static_cast<const float *>(data));
|
|
break;
|
|
case AttrType::Float2:
|
|
BLO_write_float_array(&writer, size * 2, static_cast<const float *>(data));
|
|
break;
|
|
case AttrType::Float3:
|
|
BLO_write_float3_array(&writer, size, static_cast<const float *>(data));
|
|
break;
|
|
case AttrType::Float4x4:
|
|
BLO_write_float_array(&writer, size * 16, static_cast<const float *>(data));
|
|
break;
|
|
case AttrType::ColorByte:
|
|
BLO_write_uint8_array(&writer, size * 4, static_cast<const uint8_t *>(data));
|
|
break;
|
|
case AttrType::ColorFloat:
|
|
BLO_write_float_array(&writer, size * 4, static_cast<const float *>(data));
|
|
break;
|
|
case AttrType::Quaternion:
|
|
BLO_write_float_array(&writer, size * 4, static_cast<const float *>(data));
|
|
break;
|
|
case AttrType::String:
|
|
BLO_write_struct_array(
|
|
&writer, MStringProperty, size, static_cast<const MStringProperty *>(data));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void attribute_storage_blend_write_prepare(
|
|
AttributeStorage &data,
|
|
const Map<AttrDomain, Vector<CustomDataLayer, 16> *> &layers_to_write,
|
|
AttributeStorage::BlendWriteData &write_data)
|
|
{
|
|
Set<std::string, 16> all_names_written;
|
|
for (Vector<CustomDataLayer, 16> *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<Attribute::ArrayData>(&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<Attribute::ArrayData>(&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<Attribute::SingleData>(&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
|