Core: VectorList improvements

- Add a custom Iterator, so it can be iterated as a 1D list.
- Add missing functions like `first`, `is_empty`, `clear`, and
  subscript operator.
- Add a `size_` member variable for faster `size` calls.
- Add compile-time asserts to ensure the Capacity sizes are valid.
- Add unit tests.

See #138947 for the motivation behind this.

Pull Request: https://projects.blender.org/blender/blender/pulls/139102
This commit is contained in:
Miguel Pozo
2025-05-23 15:34:26 +02:00
parent a44c515844
commit 4f00a470cd
5 changed files with 531 additions and 123 deletions

View File

@@ -128,12 +128,8 @@ struct UVPrimitiveLookup {
uint64_t uv_island_index = 0;
for (uv_islands::UVIsland &uv_island : uv_islands.islands) {
for (VectorList<uv_islands::UVPrimitive>::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++;
}

View File

@@ -386,11 +386,9 @@ void UVIsland::append(const UVPrimitive &primitive)
bool UVIsland::has_shared_edge(const UVPrimitive &primitive) const
{
for (const VectorList<UVPrimitive>::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<UVPrimitive>::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<UVPrimitive>::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<UVBorderEdge> edges;
for (VectorList<UVPrimitive>::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<UVVertex>::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<UVPrimitive>::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<UVPrimitive>::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<UVPrimitive>::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;
}
}
}

View File

@@ -9,7 +9,10 @@
#pragma once
#include <algorithm>
#include <cmath>
#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<typename T, int64_t CapacityStart = 32, int64_t CapacitySoftLimit = 4096>
class VectorList {
public:
using UsedVector = Vector<T, 0>;
template<typename T, int64_t CapacityStart = 32, int64_t CapacityMax = 4096> class VectorList {
using SelfT = VectorList<T, CapacityStart, CapacityMax>;
using VectorT = Vector<T, 0>;
private:
/**
* Contains the individual vectors. There must always be at least one vector
*/
Vector<UsedVector> 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<VectorT> 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<typename ForwardT> 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<ForwardT>(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<int64_t, int64_t> 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<int64_t, int64_t> 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<int64_t, int64_t> 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<typename IterableT, typename ElemT> 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<SelfT, T>;
using ConstIterator = Iterator<const SelfT, const T>;
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());
}
};

View File

@@ -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

View File

@@ -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<int> vec;
EXPECT_EQ(vec.size(), 0);
}
TEST(vectorlist, MoveConstructor)
{
VectorList<int> vec1;
vec1.append(1);
vec1.append(2);
vec1.append(3);
vec1.append(4);
VectorList<int> 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<int> vec1;
vec1.append(1);
vec1.append(2);
vec1.append(3);
vec1.append(4);
VectorList<int> 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<int> 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<int> 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<int> vec;
vec.append(1);
vec.append(4);
vec.append(9);
vec.append(16);
const VectorList<int> &const_ref = vec;
int i = 1;
for (int value : const_ref) {
EXPECT_EQ(value, i * i);
i++;
}
}
TEST(vectorlist, LimitIterator)
{
VectorList<int, 8, 128> 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<int, 8, 128> 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<int, 8, 128> vec;
for (int64_t i : IndexRange(1024)) {
vec.append(int(i));
}
const VectorList<int, 8, 128> &const_ref = vec;
for (int64_t i : IndexRange(1024)) {
EXPECT_EQ(const_ref[i], i);
}
}
static VectorList<int> return_by_value_helper()
{
VectorList<int> vec;
vec.append(3);
vec.append(5);
vec.append(1);
return vec;
}
TEST(vectorlist, ReturnByValue)
{
VectorList<int> 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<int> 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<int> vec;
vec.append(3);
vec.append(5);
vec.append(7);
EXPECT_EQ(vec.first(), 3);
}
TEST(vectorlist, Last)
{
VectorList<int> 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<TypeConstructMock> vec;
TypeConstructMock value;
vec.append(value);
EXPECT_TRUE(vec[0].copy_constructed);
}
TEST(vectorlist, AppendCallsMoveConstructor)
{
VectorList<TypeConstructMock> vec;
vec.append(TypeConstructMock());
EXPECT_TRUE(vec[0].move_constructed);
}
TEST(vectorlist, OveralignedValues)
{
VectorList<AlignedBuffer<1, 512>> vec;
for (int i = 0; i < 100; i++) {
vec.append({});
EXPECT_EQ(uintptr_t(&vec.last()) % 512, 0);
}
}
TEST(vectorlist, AppendExceptions)
{
VectorList<ExceptionThrower> 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