diff --git a/source/blender/blenkernel/intern/pbvh_pixels.cc b/source/blender/blenkernel/intern/pbvh_pixels.cc index 00c83fd532c..c90b2e0870a 100644 --- a/source/blender/blenkernel/intern/pbvh_pixels.cc +++ b/source/blender/blenkernel/intern/pbvh_pixels.cc @@ -128,12 +128,8 @@ struct UVPrimitiveLookup { uint64_t uv_island_index = 0; for (uv_islands::UVIsland &uv_island : uv_islands.islands) { - for (VectorList::UsedVector &uv_primitives : - uv_island.uv_primitives) - { - for (uv_islands::UVPrimitive &uv_primitive : uv_primitives) { - lookup[uv_primitive.primitive_i].append_as(Entry(&uv_primitive, uv_island_index)); - } + for (uv_islands::UVPrimitive &uv_primitive : uv_island.uv_primitives) { + lookup[uv_primitive.primitive_i].append_as(Entry(&uv_primitive, uv_island_index)); } uv_island_index++; } diff --git a/source/blender/blenkernel/intern/pbvh_uv_islands.cc b/source/blender/blenkernel/intern/pbvh_uv_islands.cc index 9f7ec096eef..1a83d346245 100644 --- a/source/blender/blenkernel/intern/pbvh_uv_islands.cc +++ b/source/blender/blenkernel/intern/pbvh_uv_islands.cc @@ -386,11 +386,9 @@ void UVIsland::append(const UVPrimitive &primitive) bool UVIsland::has_shared_edge(const UVPrimitive &primitive) const { - for (const VectorList::UsedVector &prims : uv_primitives) { - for (const UVPrimitive &prim : prims) { - if (prim.has_shared_edge(primitive)) { - return true; - } + for (const UVPrimitive &prim : uv_primitives) { + if (prim.has_shared_edge(primitive)) { + return true; } } return false; @@ -398,11 +396,9 @@ bool UVIsland::has_shared_edge(const UVPrimitive &primitive) const bool UVIsland::has_shared_edge(const MeshData &mesh_data, const int primitive_i) const { - for (const VectorList::UsedVector &primitives : uv_primitives) { - for (const UVPrimitive &prim : primitives) { - if (prim.has_shared_edge(mesh_data, primitive_i)) { - return true; - } + for (const UVPrimitive &prim : uv_primitives) { + if (prim.has_shared_edge(mesh_data, primitive_i)) { + return true; } } return false; @@ -410,11 +406,9 @@ bool UVIsland::has_shared_edge(const MeshData &mesh_data, const int primitive_i) void UVIsland::extend_border(const UVPrimitive &primitive) { - for (const VectorList::UsedVector &primitives : uv_primitives) { - for (const UVPrimitive &prim : primitives) { - if (prim.has_shared_edge(primitive)) { - this->append(primitive); - } + for (const UVPrimitive &prim : uv_primitives) { + if (prim.has_shared_edge(primitive)) { + this->append(primitive); } } } @@ -446,12 +440,10 @@ void UVIsland::extract_borders() { /* Lookup all borders of the island. */ Vector edges; - for (VectorList::UsedVector &prims : uv_primitives) { - for (UVPrimitive &prim : prims) { - for (UVEdge *edge : prim.edges) { - if (edge->is_border_edge()) { - edges.append(UVBorderEdge(edge, &prim)); - } + for (UVPrimitive &prim : uv_primitives) { + for (UVEdge *edge : prim.edges) { + if (edge->is_border_edge()) { + edges.append(UVBorderEdge(edge, &prim)); } } } @@ -1029,11 +1021,9 @@ static void extend_at_vert(const MeshData &mesh_data, /* Marks vertices that can be extended. Only vertices that are part of a border can be extended. */ static void reset_extendability_flags(UVIsland &island) { - for (VectorList::UsedVector &uv_vertices : island.uv_vertices) { - for (UVVertex &uv_vertex : uv_vertices) { - uv_vertex.flags.is_border = false; - uv_vertex.flags.is_extended = false; - } + for (UVVertex &uv_vertex : island.uv_vertices) { + uv_vertex.flags.is_border = false; + uv_vertex.flags.is_extended = false; } for (const UVBorder &border : island.borders) { for (const UVBorderEdge &border_edge : border.edges) { @@ -1090,32 +1080,28 @@ void UVIsland::print_debug(const MeshData &mesh_data) const ss << "uvisland_edges = []\n"; ss << "uvisland_faces = [\n"; - for (const VectorList::UsedVector &uvprimitives : uv_primitives) { - for (const UVPrimitive &uvprimitive : uvprimitives) { - ss << " [" << uvprimitive.edges[0]->vertices[0]->vertex << ", " - << uvprimitive.edges[0]->vertices[1]->vertex << ", " - << uvprimitive - .get_other_uv_vertex(uvprimitive.edges[0]->vertices[0], - uvprimitive.edges[0]->vertices[1]) - ->vertex - << "],\n"; - } + for (const UVPrimitive &uvprimitive : uv_primitives) { + ss << " [" << uvprimitive.edges[0]->vertices[0]->vertex << ", " + << uvprimitive.edges[0]->vertices[1]->vertex << ", " + << uvprimitive + .get_other_uv_vertex(uvprimitive.edges[0]->vertices[0], + uvprimitive.edges[0]->vertices[1]) + ->vertex + << "],\n"; } ss << "]\n"; ss << "uvisland_uvs = [\n"; - for (const VectorList::UsedVector &uvprimitives : uv_primitives) { - for (const UVPrimitive &uvprimitive : uvprimitives) { - float2 uv = uvprimitive.edges[0]->vertices[0]->uv; - ss << " " << uv.x << ", " << uv.y << ",\n"; - uv = uvprimitive.edges[0]->vertices[1]->uv; - ss << " " << uv.x << ", " << uv.y << ",\n"; - uv = uvprimitive - .get_other_uv_vertex(uvprimitive.edges[0]->vertices[0], - uvprimitive.edges[0]->vertices[1]) - ->uv; - ss << " " << uv.x << ", " << uv.y << ",\n"; - } + for (const UVPrimitive &uvprimitive : uv_primitives) { + float2 uv = uvprimitive.edges[0]->vertices[0]->uv; + ss << " " << uv.x << ", " << uv.y << ",\n"; + uv = uvprimitive.edges[0]->vertices[1]->uv; + ss << " " << uv.x << ", " << uv.y << ",\n"; + uv = uvprimitive + .get_other_uv_vertex(uvprimitive.edges[0]->vertices[0], + uvprimitive.edges[0]->vertices[1]) + ->uv; + ss << " " << uv.x << ", " << uv.y << ",\n"; } ss << "]\n"; @@ -1518,39 +1504,37 @@ static void add_uv_island(const MeshData &mesh_data, const UVIsland &uv_island, int16_t island_index) { - for (const VectorList::UsedVector &uv_primitives : uv_island.uv_primitives) { - for (const UVPrimitive &uv_primitive : uv_primitives) { - const int3 &tri = mesh_data.corner_tris[uv_primitive.primitive_i]; + for (const UVPrimitive &uv_primitive : uv_island.uv_primitives) { + const int3 &tri = mesh_data.corner_tris[uv_primitive.primitive_i]; - rctf uv_bounds = primitive_uv_bounds(tri, mesh_data.uv_map); - rcti buffer_bounds; - buffer_bounds.xmin = max_ii( - floor((uv_bounds.xmin - tile.udim_offset.x) * tile.mask_resolution.x), 0); - buffer_bounds.xmax = min_ii( - ceil((uv_bounds.xmax - tile.udim_offset.x) * tile.mask_resolution.x), - tile.mask_resolution.x - 1); - buffer_bounds.ymin = max_ii( - floor((uv_bounds.ymin - tile.udim_offset.y) * tile.mask_resolution.y), 0); - buffer_bounds.ymax = min_ii( - ceil((uv_bounds.ymax - tile.udim_offset.y) * tile.mask_resolution.y), - tile.mask_resolution.y - 1); + rctf uv_bounds = primitive_uv_bounds(tri, mesh_data.uv_map); + rcti buffer_bounds; + buffer_bounds.xmin = max_ii( + floor((uv_bounds.xmin - tile.udim_offset.x) * tile.mask_resolution.x), 0); + buffer_bounds.xmax = min_ii( + ceil((uv_bounds.xmax - tile.udim_offset.x) * tile.mask_resolution.x), + tile.mask_resolution.x - 1); + buffer_bounds.ymin = max_ii( + floor((uv_bounds.ymin - tile.udim_offset.y) * tile.mask_resolution.y), 0); + buffer_bounds.ymax = min_ii( + ceil((uv_bounds.ymax - tile.udim_offset.y) * tile.mask_resolution.y), + tile.mask_resolution.y - 1); - for (int y = buffer_bounds.ymin; y < buffer_bounds.ymax + 1; y++) { - for (int x = buffer_bounds.xmin; x < buffer_bounds.xmax + 1; x++) { - float2 uv(float(x) / tile.mask_resolution.x, float(y) / tile.mask_resolution.y); - float3 weights; - barycentric_weights_v2(mesh_data.uv_map[tri[0]], - mesh_data.uv_map[tri[1]], - mesh_data.uv_map[tri[2]], - uv + tile.udim_offset, - weights); - if (!barycentric_inside_triangle_v2(weights)) { - continue; - } - - uint64_t offset = tile.mask_resolution.x * y + x; - tile.mask[offset] = island_index; + for (int y = buffer_bounds.ymin; y < buffer_bounds.ymax + 1; y++) { + for (int x = buffer_bounds.xmin; x < buffer_bounds.xmax + 1; x++) { + float2 uv(float(x) / tile.mask_resolution.x, float(y) / tile.mask_resolution.y); + float3 weights; + barycentric_weights_v2(mesh_data.uv_map[tri[0]], + mesh_data.uv_map[tri[1]], + mesh_data.uv_map[tri[2]], + uv + tile.udim_offset, + weights); + if (!barycentric_inside_triangle_v2(weights)) { + continue; } + + uint64_t offset = tile.mask_resolution.x * y + x; + tile.mask[offset] = island_index; } } } diff --git a/source/blender/blenlib/BLI_vector_list.hh b/source/blender/blenlib/BLI_vector_list.hh index aa70e96cb0a..c5bd4e11a81 100644 --- a/source/blender/blenlib/BLI_vector_list.hh +++ b/source/blender/blenlib/BLI_vector_list.hh @@ -9,7 +9,10 @@ #pragma once #include +#include +#include "BLI_math_bits.h" +#include "BLI_utildefines.h" #include "BLI_vector.hh" namespace blender { @@ -28,83 +31,190 @@ namespace blender { * * When a VectorList reserved memory is full it will allocate memory for the new items, breaking * the sequential access. Within each allocated memory block the elements are ordered sequentially. + * + * Indexing has some overhead compared to a Vector or an Array, but it still has constant time + * access. */ -template -class VectorList { - public: - using UsedVector = Vector; +template class VectorList { + using SelfT = VectorList; + using VectorT = Vector; - private: - /** - * Contains the individual vectors. There must always be at least one vector - */ - Vector vectors_; + static_assert(is_power_of_2(CapacityStart)); + static_assert(is_power_of_2(CapacityMax)); + static_assert(CapacityStart <= CapacityMax); + + /* Contains the individual vectors. There must always be at least one vector. */ + Vector vectors_; + /* Number of vectors in use. */ + int64_t used_vectors_ = 0; + /* Total element count accross all vectors_. */ + int64_t size_ = 0; public: VectorList() { this->append_vector(); + used_vectors_ = 1; } + VectorList(VectorList &&other) noexcept + { + vectors_ = std::move(other.vectors_); + used_vectors_ = other.used_vectors_; + size_ = other.size_; + other.clear_and_shrink(); + } + + VectorList &operator=(VectorList &&other) + { + return move_assign_container(*this, std::move(other)); + } + + /* Insert a new element at the end of the VectorList. */ void append(const T &value) { this->append_as(value); } + /* Insert a new element at the end of the VectorList. */ void append(T &&value) { this->append_as(std::move(value)); } + /* This is similar to `std::vector::emplace_back`. */ template void append_as(ForwardT &&value) { - UsedVector &vector = this->ensure_space_for_one(); + VectorT &vector = this->ensure_space_for_one(); vector.append_unchecked_as(std::forward(value)); + size_++; } - UsedVector *begin() + /** + * Return a reference to the first element in the VectorList. + * This invokes undefined behavior when the VectorList is empty. + */ + T &first() { - return vectors_.begin(); - } - - UsedVector *end() - { - return vectors_.end(); - } - - const UsedVector *begin() const - { - return vectors_.begin(); - } - - const UsedVector *end() const - { - return vectors_.end(); + BLI_assert(size() > 0); + return vectors_.first().first(); } + /** + * Return a reference to the last element in the VectorList. + * This invokes undefined behavior when the VectorList is empty. + */ T &last() { - return vectors_.last().last(); + BLI_assert(size() > 0); + return vectors_[used_vectors_ - 1].last(); } + /* Return how many values are currently stored in the VectorList. */ int64_t size() const { - int64_t result = 0; - for (const UsedVector &vector : *this) { - result += vector.size(); + return size_; + } + + /** + * Returns true when the VectorList contains no elements, otherwise false. + * + * This is the same as std::vector::empty. + */ + bool is_empty() const + { + return size_ == 0; + } + + /* Afterwards the VectorList has 0 elements, but will still have memory to be refilled again. */ + void clear() + { + for (VectorT &vector : vectors_) { + vector.clear(); } - return result; + used_vectors_ = 1; + size_ = 0; + } + + /* Afterwards the VectorList has 0 elements and the Vectors allocated memory will be freed. */ + void clear_and_shrink() + { + vectors_.clear(); + this->append_vector(); + used_vectors_ = 1; + size_ = 0; + } + + /** + * Get the value at the given index. + * This invokes undefined behavior when the index is out of bounds. + */ + const T &operator[](int64_t index) const + { + std::pair index_pair = this->global_index_to_index_pair(index); + return vectors_[index_pair.first][index_pair.second]; + } + + /** + * Get the value at the given index. + * This invokes undefined behavior when the index is out of bounds. + */ + T &operator[](int64_t index) + { + std::pair index_pair = this->global_index_to_index_pair(index); + return vectors_[index_pair.first][index_pair.second]; } private: - UsedVector &ensure_space_for_one() + /** + * Convert a global index into a Vector index and Element index pair. + * We use the fact that vector sizes increase geometrically to compute this in constant time. + * https://en.wikipedia.org/wiki/Geometric_progression + */ + std::pair global_index_to_index_pair(int64_t index) const { - UsedVector &vector = vectors_.last(); - if (LIKELY(!vector.is_at_capacity())) { - return vector; + BLI_assert(index >= 0); + BLI_assert(index < this->size()); + + auto log2 = [](int64_t value) -> int64_t { + return 31 - bitscan_reverse_uint(uint32_t(value)); + }; + auto geometric_sum = [](int64_t index) -> int64_t { + return CapacityStart * ((2 << index) - 1); + }; + auto index_from_sum = [log2](int64_t sum) -> int64_t { + return log2((sum / CapacityStart) + 1); + }; + + static const int64_t start_log2 = log2(CapacityStart); + static const int64_t end_log2 = log2(CapacityMax); + /* The number of vectors until CapacityMax size is reached. */ + static const int64_t geometric_steps = end_log2 - start_log2 + 1; + /* The number of elements until CapacityMax size is reached. */ + static const int64_t geometric_total = geometric_sum(geometric_steps - 1); + + int64_t index_a, index_b; + if (index < geometric_total) { + index_a = index_from_sum(index); + index_b = index_a > 0 ? index - geometric_sum(index_a - 1) : index; } - this->append_vector(); - return vectors_.last(); + else { + int64_t linear_start = index - geometric_total; + index_a = geometric_steps + linear_start / CapacityMax; + index_b = linear_start % CapacityMax; + } + return {index_a, index_b}; + } + + VectorT &ensure_space_for_one() + { + if (vectors_[used_vectors_ - 1].is_at_capacity()) { + if (used_vectors_ == vectors_.size()) { + this->append_vector(); + } + used_vectors_++; + } + return vectors_[used_vectors_ - 1]; } void append_vector() @@ -119,7 +229,74 @@ class VectorList { if (vectors_.is_empty()) { return CapacityStart; } - return std::min(vectors_.last().capacity() * 2, CapacitySoftLimit); + return std::min(vectors_.last().capacity() * 2, CapacityMax); + } + + template struct Iterator { + IterableT &vector_list; + int64_t index_a = 0; + int64_t index_b = 0; + + Iterator(IterableT &vector_list, int64_t index_a = 0, int64_t index_b = 0) + : vector_list(vector_list), index_a(index_a), index_b(index_b) + { + } + + ElemT &operator*() const + { + return vector_list.vectors_[index_a][index_b]; + } + + Iterator &operator++() + { + if (vector_list.vectors_[index_a].size() == index_b + 1) { + if (index_a + 1 == vector_list.vectors_.size()) { + /* Reached the end. */ + index_b++; + } + else { + index_a++; + index_b = 0; + } + } + else { + index_b++; + } + return *this; + } + + bool operator==(const Iterator &other) const + { + BLI_assert(&other.vector_list == &vector_list); + return other.index_a == index_a && other.index_b == index_b; + } + + bool operator!=(const Iterator &other) const + { + return !(other == *this); + } + }; + + using MutIterator = Iterator; + using ConstIterator = Iterator; + + public: + MutIterator begin() + { + return MutIterator(*this, 0, 0); + } + MutIterator end() + { + return MutIterator(*this, used_vectors_ - 1, vectors_[used_vectors_ - 1].size()); + } + + ConstIterator begin() const + { + return ConstIterator(*this, 0, 0); + } + ConstIterator end() const + { + return ConstIterator(*this, used_vectors_ - 1, vectors_[used_vectors_ - 1].size()); } }; diff --git a/source/blender/blenlib/CMakeLists.txt b/source/blender/blenlib/CMakeLists.txt index fb41d245c6d..ecb089815c3 100644 --- a/source/blender/blenlib/CMakeLists.txt +++ b/source/blender/blenlib/CMakeLists.txt @@ -603,6 +603,7 @@ if(WITH_GTESTS) tests/BLI_unique_sorted_indices_test.cc tests/BLI_utildefines_test.cc tests/BLI_uuid_test.cc + tests/BLI_vector_list_test.cc tests/BLI_vector_set_test.cc tests/BLI_vector_test.cc tests/BLI_virtual_array_test.cc diff --git a/source/blender/blenlib/tests/BLI_vector_list_test.cc b/source/blender/blenlib/tests/BLI_vector_list_test.cc new file mode 100644 index 00000000000..bb1c8d9601d --- /dev/null +++ b/source/blender/blenlib/tests/BLI_vector_list_test.cc @@ -0,0 +1,250 @@ +/* SPDX-FileCopyrightText: 2023 Blender Authors + * + * SPDX-License-Identifier: Apache-2.0 */ + +#include "BLI_exception_safety_test_utils.hh" +#include "BLI_vector_list.hh" +#include "testing/testing.h" + +#include "BLI_strict_flags.h" /* IWYU pragma: keep. Keep last. */ + +namespace blender::tests { + +TEST(vectorlist, DefaultConstructor) +{ + VectorList vec; + EXPECT_EQ(vec.size(), 0); +} + +TEST(vectorlist, MoveConstructor) +{ + VectorList vec1; + vec1.append(1); + vec1.append(2); + vec1.append(3); + vec1.append(4); + VectorList vec2(std::move(vec1)); + + EXPECT_EQ(vec1.size(), 0); /* NOLINT: bugprone-use-after-move */ + EXPECT_EQ(vec2.size(), 4); + EXPECT_EQ(vec2[0], 1); + EXPECT_EQ(vec2[1], 2); + EXPECT_EQ(vec2[2], 3); + EXPECT_EQ(vec2[3], 4); +} + +TEST(vectorlist, MoveOperator) +{ + VectorList vec1; + vec1.append(1); + vec1.append(2); + vec1.append(3); + vec1.append(4); + VectorList vec2; + vec2 = std::move(vec1); + + EXPECT_EQ(vec1.size(), 0); /* NOLINT: bugprone-use-after-move */ + EXPECT_EQ(vec2.size(), 4); + EXPECT_EQ(vec2[0], 1); + EXPECT_EQ(vec2[1], 2); + EXPECT_EQ(vec2[2], 3); + EXPECT_EQ(vec2[3], 4); +} + +TEST(vectorlist, Append) +{ + VectorList vec; + vec.append(3); + vec.append(6); + vec.append(7); + EXPECT_EQ(vec.size(), 3); + EXPECT_EQ(vec[0], 3); + EXPECT_EQ(vec[1], 6); + EXPECT_EQ(vec[2], 7); +} + +TEST(vectorlist, Iterator) +{ + VectorList vec; + vec.append(1); + vec.append(4); + vec.append(9); + vec.append(16); + int i = 1; + for (int value : vec) { + EXPECT_EQ(value, i * i); + i++; + } +} + +TEST(vectorlist, ConstIterator) +{ + VectorList vec; + vec.append(1); + vec.append(4); + vec.append(9); + vec.append(16); + const VectorList &const_ref = vec; + int i = 1; + for (int value : const_ref) { + EXPECT_EQ(value, i * i); + i++; + } +} + +TEST(vectorlist, LimitIterator) +{ + VectorList vec; + for (int64_t i : IndexRange(1024)) { + vec.append(int(i)); + } + int i = 0; + for (int value : vec) { + EXPECT_EQ(value, i); + i++; + } +} + +TEST(vectorlist, LimitIndexing) +{ + VectorList vec; + for (int64_t i : IndexRange(1024)) { + vec.append(int(i)); + } + for (int64_t i : IndexRange(1024)) { + EXPECT_EQ(vec[i], i); + } +} + +TEST(vectorlist, ConstLimitIndexing) +{ + VectorList vec; + for (int64_t i : IndexRange(1024)) { + vec.append(int(i)); + } + const VectorList &const_ref = vec; + for (int64_t i : IndexRange(1024)) { + EXPECT_EQ(const_ref[i], i); + } +} + +static VectorList return_by_value_helper() +{ + VectorList vec; + vec.append(3); + vec.append(5); + vec.append(1); + return vec; +} + +TEST(vectorlist, ReturnByValue) +{ + VectorList vec = return_by_value_helper(); + EXPECT_EQ(vec.size(), 3); + EXPECT_EQ(vec[0], 3); + EXPECT_EQ(vec[1], 5); + EXPECT_EQ(vec[2], 1); +} + +TEST(vectorlist, IsEmpty) +{ + VectorList vec; + EXPECT_TRUE(vec.is_empty()); + vec.append(1); + EXPECT_FALSE(vec.is_empty()); + vec.clear(); + EXPECT_TRUE(vec.is_empty()); +} + +TEST(vectorlist, First) +{ + VectorList vec; + vec.append(3); + vec.append(5); + vec.append(7); + EXPECT_EQ(vec.first(), 3); +} + +TEST(vectorlist, Last) +{ + VectorList vec; + vec.append(3); + vec.append(5); + vec.append(7); + EXPECT_EQ(vec.last(), 7); +} + +class TypeConstructMock { + public: + bool default_constructed = false; + bool copy_constructed = false; + bool move_constructed = false; + bool copy_assigned = false; + bool move_assigned = false; + + TypeConstructMock() : default_constructed(true) {} + + TypeConstructMock(const TypeConstructMock & /*other*/) : copy_constructed(true) {} + + TypeConstructMock(TypeConstructMock && /*other*/) noexcept : move_constructed(true) {} + + TypeConstructMock &operator=(const TypeConstructMock &other) + { + if (this == &other) { + return *this; + } + + copy_assigned = true; + return *this; + } + + TypeConstructMock &operator=(TypeConstructMock &&other) noexcept + { + if (this == &other) { + return *this; + } + + move_assigned = true; + return *this; + } +}; + +TEST(vectorlist, AppendCallsCopyConstructor) +{ + VectorList vec; + TypeConstructMock value; + vec.append(value); + EXPECT_TRUE(vec[0].copy_constructed); +} + +TEST(vectorlist, AppendCallsMoveConstructor) +{ + VectorList vec; + vec.append(TypeConstructMock()); + EXPECT_TRUE(vec[0].move_constructed); +} + +TEST(vectorlist, OveralignedValues) +{ + VectorList> vec; + for (int i = 0; i < 100; i++) { + vec.append({}); + EXPECT_EQ(uintptr_t(&vec.last()) % 512, 0); + } +} + +TEST(vectorlist, AppendExceptions) +{ + VectorList vec; + vec.append({}); + vec.append({}); + ExceptionThrower *ptr1 = &vec.last(); + ExceptionThrower value; + value.throw_during_copy = true; + EXPECT_ANY_THROW({ vec.append(value); }); + EXPECT_EQ(vec.size(), 2); + ExceptionThrower *ptr2 = &vec.last(); + EXPECT_EQ(ptr1, ptr2); +} + +} // namespace blender::tests