mirror of
https://github.com/minetest/minetest.git
synced 2024-07-02 14:10:33 +02:00
Some cleanup, add brief explanation
This commit is contained in:
parent
fcfabbe1e6
commit
8954059561
@ -10,9 +10,52 @@
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
using Idx = uint16_t;
|
||||
/*
|
||||
This implements a dynamic forest of static k-d-trees.
|
||||
|
||||
// TODO docs and explanation
|
||||
A k-d-tree is a k-dimensional binary search tree.
|
||||
On the i-th level of the tree, you split by the (i mod k)-th coordinate.
|
||||
|
||||
Building a balanced k-d-tree for n points is done in O(n log n) time:
|
||||
Points are stored in a matrix, identified by indices.
|
||||
These indices are presorted by all k axes.
|
||||
To split, you simply pick the pivot index in the appropriate index array,
|
||||
and mark all points left to it by index in a bitset.
|
||||
This lets you then split the indices sorted by the other axes,
|
||||
while preserving the sorted order.
|
||||
|
||||
This however only gives us a static spatial index.
|
||||
To make it dynamic, we keep a "forest" of k-d-trees of sizes of successive powers of two.
|
||||
When we insert a new tree, we check whether there already is a k-d-tree of the same size.
|
||||
If this is the case, we merge with that tree, giving us a tree of twice the size,
|
||||
and so on, until we find a free size.
|
||||
|
||||
This means our "forest" corresponds to a bit pattern,
|
||||
where a set bit means a non-empty tree.
|
||||
Inserting a point is equivalent to incrementing this bit pattern.
|
||||
|
||||
To handle deletions, we simply mark the appropriate point as deleted using another bitset.
|
||||
When more than half the points have been deleted,
|
||||
we shrink the structure by removing all deleted points.
|
||||
This is equivalent to shifting down the "bit pattern" by one.
|
||||
|
||||
There are plenty variations that could be explored:
|
||||
|
||||
* Keeping a small amount of points in a small pool to make updates faster -
|
||||
avoid building and querying small k-d-trees.
|
||||
This might be useful if the overhead for small sizes hurts performance.
|
||||
* Keeping fewer trees to make queries faster, at the expense of updates.
|
||||
* More eagerly removing entries marked as deleted (for example, on merge).
|
||||
* Replacing the array-backed structure with a structure of dynamically allocated nodes.
|
||||
This would make it possible to "let trees get out of shape".
|
||||
* Shrinking the structure currently sorts the live points by all axes,
|
||||
not leveraging the existing presorting of the subsets.
|
||||
Cleverly done filtering followed by sorted merges should enable linear time.
|
||||
*/
|
||||
|
||||
using Idx = uint16_t; // TODO unify with Id
|
||||
|
||||
// TODO more doc comments
|
||||
|
||||
// TODO profile and tweak knobs
|
||||
|
||||
@ -24,9 +67,8 @@ class Points {
|
||||
using Point = std::array<Component, Dim>;
|
||||
//! Empty
|
||||
Points() : n(0), coords(nullptr) {}
|
||||
//! Leaves coords uninitialized!
|
||||
// TODO we want make_unique_for_overwrite here...
|
||||
Points(Idx n) : n(n), coords(std::make_unique<Component[]>(Dim * n)) {}
|
||||
//! Allocating constructor; leaves coords uninitialized!
|
||||
Points(Idx n) : n(n), coords(new Component[Dim * n]) {}
|
||||
//! Copying constructor
|
||||
Points(Idx n, const std::array<Component const *, Dim> &coords) : Points(n) {
|
||||
for (uint8_t d = 0; d < Dim; ++d)
|
||||
@ -49,11 +91,16 @@ class Points {
|
||||
for (uint8_t d = 0; d < Dim; ++d)
|
||||
begin(d)[i] = point[d];
|
||||
}
|
||||
// HACK interior mutability...
|
||||
Component *begin(uint8_t d) const {
|
||||
Component *begin(uint8_t d) {
|
||||
return coords.get() + d * n;
|
||||
}
|
||||
Component *end(uint8_t d) const {
|
||||
Component *end(uint8_t d) {
|
||||
return begin(d) + n;
|
||||
}
|
||||
const Component *begin(uint8_t d) const {
|
||||
return coords.get() + d * n;
|
||||
}
|
||||
const Component *end(uint8_t d) const {
|
||||
return begin(d) + n;
|
||||
}
|
||||
private:
|
||||
@ -69,10 +116,10 @@ class SortedIndices {
|
||||
|
||||
//! uninitialized indices
|
||||
static SortedIndices newUninitialized(Idx n) {
|
||||
return SortedIndices(n); // HACK can't be arsed to fix rn Points<Dim, Idx>(n));
|
||||
return SortedIndices(Points<Dim, Idx>(n));
|
||||
}
|
||||
|
||||
// Identity permutation on all axes
|
||||
//! Identity permutation on all axes
|
||||
SortedIndices(Idx n) : indices(n) {
|
||||
for (uint8_t d = 0; d < Dim; ++d)
|
||||
for (Idx i = 0; i < n; ++i)
|
||||
@ -130,14 +177,20 @@ class SortedIndices {
|
||||
return SplitResult{std::move(left), std::move(right), *mid};
|
||||
}
|
||||
|
||||
Idx *begin(uint8_t d) const {
|
||||
Idx *begin(uint8_t d) {
|
||||
return indices.begin(d);
|
||||
}
|
||||
|
||||
Idx *end(uint8_t d) const {
|
||||
Idx *end(uint8_t d) {
|
||||
return indices.end(d);
|
||||
}
|
||||
const Idx *begin(uint8_t d) const {
|
||||
return indices.begin(d);
|
||||
}
|
||||
const Idx *end(uint8_t d) const {
|
||||
return indices.end(d);
|
||||
}
|
||||
private:
|
||||
SortedIndices(Points<Dim, Idx> &&indices) : indices(std::move(indices)) {}
|
||||
Points<Dim, Idx> indices;
|
||||
};
|
||||
|
||||
@ -147,7 +200,6 @@ class SortedPoints {
|
||||
SortedPoints() : points(), indices() {}
|
||||
|
||||
//! Single point
|
||||
// TODO remove this
|
||||
SortedPoints(const std::array<Component, Dim> &point) : points(1), indices(1) {
|
||||
points.setPoint(0, point);
|
||||
}
|
||||
@ -200,8 +252,7 @@ class SortedPoints {
|
||||
// but that is irrelevant
|
||||
return points.size();
|
||||
}
|
||||
|
||||
// HACK private:
|
||||
|
||||
Points<Dim, Component> points;
|
||||
SortedIndices<Dim> indices;
|
||||
};
|
||||
@ -220,7 +271,6 @@ class KdTree {
|
||||
{}
|
||||
|
||||
//! Build a tree containing just a single point
|
||||
// TODO this will probably be obsolete soon (TM)
|
||||
KdTree(const Point &point, const Id &id)
|
||||
: items(point)
|
||||
, ids(std::make_unique<Id[]>(1))
|
||||
@ -250,8 +300,10 @@ class KdTree {
|
||||
ids = std::make_unique<Id[]>(cap());
|
||||
std::copy(a.ids.get(), a.ids.get() + a.cap(), ids.get());
|
||||
std::copy(b.ids.get(), b.ids.get() + b.cap(), ids.get() + a.cap());
|
||||
// Note: Initialize `deleted` *before* calling `init`,
|
||||
// since `init` abuses the `deleted` marks as left/right marks.
|
||||
deleted = std::vector<bool>(cap());
|
||||
init(0, 0, items.indices); // this does le dirty dirty hack so call it BEFORE we deal with deleted
|
||||
init(0, 0, items.indices);
|
||||
std::copy(a.deleted.begin(), a.deleted.end(), deleted.begin());
|
||||
std::copy(b.deleted.begin(), b.deleted.end(), deleted.begin() + a.items.size());
|
||||
}
|
||||
@ -290,7 +342,7 @@ class KdTree {
|
||||
|
||||
private:
|
||||
void init(Idx root, uint8_t axis, const SortedIndices<Dim> &sorted) {
|
||||
// HACK abuse deleted marks as left/right marks
|
||||
// Temporarily abuse "deleted" marks as left/right marks
|
||||
const auto split = sorted.split(axis, deleted);
|
||||
tree[root] = split.pivot;
|
||||
const auto next_axis = (axis + 1) % Dim;
|
||||
@ -324,7 +376,7 @@ class KdTree {
|
||||
for (uint8_t d = 0; d < Dim; ++d)
|
||||
if (point[d] < min[d] || point[d] > max[d])
|
||||
return;
|
||||
cb(items.points.getPoint(ptid), ids[ptid]);
|
||||
cb(point, ids[ptid]);
|
||||
}
|
||||
}
|
||||
SortedPoints<Dim, Component> items;
|
||||
@ -336,28 +388,22 @@ class KdTree {
|
||||
std::vector<bool> deleted;
|
||||
};
|
||||
|
||||
// TODO abstract dynamic spatial index superclass
|
||||
template<uint8_t Dim, class Component, class Id>
|
||||
class DynamicKdTrees {
|
||||
using Tree = KdTree<Dim, Component, Id>;
|
||||
public:
|
||||
using Point = typename Tree::Point;
|
||||
void insert(const std::array<Component, Dim> &point, const Id id) {
|
||||
void insert(const std::array<Component, Dim> &point, Id id) {
|
||||
Tree tree(point, id);
|
||||
for (uint8_t tree_idx = 0;; ++tree_idx) {
|
||||
if (tree_idx >= trees.size()) {
|
||||
tree.foreach([&](Idx in_tree_idx, auto _, Id id) {
|
||||
del_entries[id] = {tree_idx, in_tree_idx};
|
||||
});
|
||||
if (tree_idx == trees.size()) {
|
||||
trees.push_back(std::move(tree));
|
||||
updateDelEntries(tree_idx);
|
||||
break;
|
||||
}
|
||||
if (trees[tree_idx].empty()) {
|
||||
// TODO deduplicate
|
||||
tree.foreach([&](Idx in_tree_idx, auto _, Id id) {
|
||||
del_entries[id] = {tree_idx, in_tree_idx};
|
||||
});
|
||||
trees[tree_idx] = std::move(tree);
|
||||
updateDelEntries(tree_idx);
|
||||
break;
|
||||
}
|
||||
tree = Tree(tree, trees[tree_idx]);
|
||||
@ -366,12 +412,13 @@ class DynamicKdTrees {
|
||||
++n_entries;
|
||||
}
|
||||
void remove(Id id) {
|
||||
const auto del_entry = del_entries.at(id);
|
||||
trees.at(del_entry.treeIdx).remove(del_entry.inTree);
|
||||
del_entries.erase(id); // TODO use iterator right away...
|
||||
const auto it = del_entries.find(id);
|
||||
assert(it != del_entries.end());
|
||||
trees.at(it->second.tree_idx).remove(it->second.in_tree);
|
||||
del_entries.erase(it);
|
||||
++deleted;
|
||||
if (deleted > n_entries/2) // we want to shift out the one!
|
||||
compactify();
|
||||
if (deleted > n_entries/2) // "shift out" the last tree
|
||||
shrink_to_half();
|
||||
}
|
||||
void update(const Point &newPos, Id id) {
|
||||
remove(id);
|
||||
@ -384,14 +431,20 @@ class DynamicKdTrees {
|
||||
tree.rangeQuery(min, max, cb);
|
||||
}
|
||||
private:
|
||||
void compactify() {
|
||||
void updateDelEntries(uint8_t tree_idx) {
|
||||
trees[tree_idx].foreach([&](Idx in_tree_idx, auto _, Id id) {
|
||||
del_entries[id] = {tree_idx, in_tree_idx};
|
||||
});
|
||||
}
|
||||
// Shrink to half the size, equivalent to shifting down the "bit pattern".
|
||||
void shrink_to_half() {
|
||||
assert(n_entries >= deleted);
|
||||
n_entries -= deleted; // note: this should be exactly n_entries/2
|
||||
assert(n_entries - deleted == (n_entries >> 1));
|
||||
n_entries -= deleted;
|
||||
deleted = 0;
|
||||
// reset map, freeing memory (instead of clearing)
|
||||
// Reset map, freeing memory (instead of clearing)
|
||||
del_entries = std::unordered_map<Id, DelEntry>();
|
||||
|
||||
|
||||
// Collect all live points and corresponding IDs.
|
||||
const auto live_ids = std::make_unique<Id[]>(n_entries);
|
||||
Points<Dim, Component> live_points(n_entries);
|
||||
@ -413,29 +466,25 @@ class DynamicKdTrees {
|
||||
Idx n = 1;
|
||||
for (uint8_t d = 0; d < Dim; ++d)
|
||||
point_ptrs[d] = live_points.begin(d);
|
||||
for (uint8_t treeIdx = 0; treeIdx < trees.size() - 1; ++treeIdx, n *= 2) {
|
||||
for (uint8_t tree_idx = 0; tree_idx < trees.size() - 1; ++tree_idx, n *= 2) {
|
||||
Tree tree;
|
||||
if (!trees[treeIdx+1].empty()) {
|
||||
// TODO maybe optimize from log² -> log?
|
||||
// This could be achieved by doing a sorted merge of live points, then doing a radix sort.
|
||||
if (!trees[tree_idx+1].empty()) {
|
||||
tree = std::move(Tree(n, id_ptr, point_ptrs));
|
||||
id_ptr += n;
|
||||
for (uint8_t d = 0; d < Dim; ++d)
|
||||
point_ptrs[d] += n;
|
||||
// TODO dedupe
|
||||
tree.foreach([&](Idx objIdx, auto _, Id id) {
|
||||
del_entries[id] = {treeIdx, objIdx};
|
||||
});
|
||||
}
|
||||
trees[treeIdx] = std::move(tree);
|
||||
trees[tree_idx] = std::move(tree);
|
||||
updateDelEntries(tree_idx);
|
||||
}
|
||||
trees.pop_back(); // "shift out" tree with the most elements
|
||||
}
|
||||
// could use an array (rather than a vector) here since we've got a good bound on the size ahead of time but meh
|
||||
// This could even use an array instead of a vector,
|
||||
// since the number of trees is guaranteed to be logarithmic in the max of Idx
|
||||
std::vector<Tree> trees;
|
||||
struct DelEntry {
|
||||
uint8_t treeIdx;
|
||||
Idx inTree;
|
||||
uint8_t tree_idx;
|
||||
Idx in_tree;
|
||||
};
|
||||
std::unordered_map<Id, DelEntry> del_entries;
|
||||
Idx n_entries = 0;
|
||||
|
Loading…
Reference in New Issue
Block a user