Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
292 changes: 140 additions & 152 deletions src/ipc/broad_phase/hash_grid.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <tbb/parallel_sort.h>

#include <algorithm> // std::min/max
#include <type_traits>

#define IPC_TOOLKIT_HASH_GRID_USE_SORT_UNIQUE // else use unordered_set

Expand Down Expand Up @@ -103,112 +104,160 @@ void HashGrid::insert_box(
}
}

template <typename Candidate>
void HashGrid::detect_candidates(
const std::vector<HashItem>& items0,
const std::vector<HashItem>& items1,
const AABBs& boxes0,
const AABBs& boxes1,
const std::function<bool(size_t, size_t)>& can_collide,
std::vector<Candidate>& candidates) const
{
// Entries with the same key means they share a cell (that cell index
// hashes to the same key) and should be flagged for low-level intersection
// testing. We loop over the entire sorted set of (key,value) pairs
// creating Candidate entries for pairs with the same key

// 1. Soft merge of items (assuming items are sorted)
size_t num_items = items0.size() + items1.size();
std::vector<long> merged_item_indices;
merged_item_indices.reserve(num_items);
namespace {
// Join sorted cell runs in bounded batches, then enumerate pairs inside
// each common cell. Large cells retain a 2D parallel split.
template <typename Candidate, bool SameItems>
void detect_cell_pairs(
const std::vector<HashItem>& items0,
const std::vector<HashItem>& items1,
const AABBs& boxes0,
const AABBs& boxes1,
const std::function<bool(size_t, size_t)>& can_collide,
std::vector<Candidate>& candidates)
{
long i = 0, j = 0;
while (i < items0.size() && j < items1.size()) {
if (items0[i] < items1[j]) {
merged_item_indices.push_back(-(i++) - 1);
} else {
merged_item_indices.push_back(j++);
}
}
while (i < items0.size()) {
merged_item_indices.push_back(-(i++) - 1);
}
while (j < items1.size()) {
merged_item_indices.push_back(j++);
}
}
assert(merged_item_indices.size() == num_items);

const auto get_item = [&](long i) -> const HashItem& {
return i < 0 ? items0[-(i + 1)] : items1[i];
};
struct CellRange {
long begin, end;
};
struct CellPair {
CellRange first, second;
};
using Cell = std::conditional_t<SameItems, CellRange, CellPair>;

// Bound temporary storage independently of the number of occupied
// cells. Same-type relations need only one range per cell.
constexpr size_t max_cells_per_batch = 65536;
const size_t max_cells = SameItems
? items0.size() / 2
: std::min(items0.size(), items1.size());
std::vector<Cell> cells;
cells.reserve(std::min(max_cells_per_batch, max_cells));

// 2. Enumerate hash collisions
#ifdef IPC_TOOLKIT_HASH_GRID_USE_SORT_UNIQUE
tbb::enumerable_thread_specific<std::vector<Candidate>> storage;
tbb::enumerable_thread_specific<std::vector<Candidate>> storage;
#else
tbb::enumerable_thread_specific<unordered_set<Candidate>> storage;
tbb::enumerable_thread_specific<unordered_set<Candidate>> storage;
#endif

tbb::parallel_for(
tbb::blocked_range2d<long>(0L, num_items - 1, 0L, num_items),
[&](const tbb::blocked_range2d<long>& r) {
const auto emit = [&](long begin0, long end0, long begin1, long end1) {
auto& local_candidates = storage.local();

// i < j
long i_end = std::min(r.rows().end(), r.cols().end());
for (long i = r.rows().begin(); i < i_end; i++) {
const long idx0 = merged_item_indices[i];
const HashItem& item0 = get_item(idx0);

// i < r.cols().end() → i + 1 <= r.cols().end()
long j_begin = std::max(i + 1, r.cols().begin());
for (long j = j_begin; j < r.cols().end(); j++) {
const long idx1 = merged_item_indices[j];
const HashItem& item1 = get_item(idx1);

if (item0.key != item1.key) {
break; // This avoids a brute force comparison
}

long id0 = item0.id, id1 = item1.id;
if (idx0 >= 0 && idx1 < 0) {
std::swap(id0, id1);
} else if (idx0 >= 0 || idx1 < 0) {
continue;
}
assert(id0 < boxes0.size() && id1 < boxes1.size());

if (!can_collide(id0, id1)) {
if constexpr (SameItems) {
end0 = std::min(end0, end1);
}
for (long a = begin0; a < end0; ++a) {
const auto id0 = items0[a].id;
const AABB& box0 = boxes0[id0];
const long first = SameItems ? std::max(a + 1, begin1) : begin1;
for (long b = first; b < end1; ++b) {
const auto id1 = items1[b].id;
if (!can_collide(id0, id1)
|| !box0.intersects(boxes1[id1])) {
continue;
}

if (boxes0[id0].intersects(boxes1[id1])) {
#ifdef IPC_TOOLKIT_HASH_GRID_USE_SORT_UNIQUE
local_candidates.emplace_back(id0, id1);
local_candidates.emplace_back(id0, id1);
#else
local_candidates.emplace(id0, id1);
local_candidates.emplace(id0, id1);
#endif
}
}
};
const auto process_cell = [&](long begin0, long end0, long begin1,
long end1) {
if (end0 - begin0 <= 64 && end1 - begin1 <= 64) {
emit(begin0, end0, begin1, end1);
} else {
tbb::parallel_for(
tbb::blocked_range2d<long>(
begin0, end0, 64, begin1, end1, 64),
[&](const auto& r) {
emit(
r.rows().begin(), r.rows().end(), r.cols().begin(),
r.cols().end());
});
}
};
const auto process_batch = [&]() {
tbb::parallel_for(
tbb::blocked_range<size_t>(0, cells.size()),
[&](const auto& range) {
for (size_t k = range.begin(); k < range.end(); ++k) {
const auto& c = cells[k];
if constexpr (SameItems) {
process_cell(c.begin, c.end, c.begin, c.end);
} else {
process_cell(
c.first.begin, c.first.end, c.second.begin,
c.second.end);
}
}
});
cells.clear();
};

const auto run_end = [](const auto& items, long begin) {
long end = begin + 1;
while (end < items.size() && items[end].key == items[begin].key) {
++end;
}
return end;
};
long i = 0, j = 0;
while (i < items0.size() && j < items1.size()) {
if constexpr (SameItems) {
const long ie = run_end(items0, i);
if (ie - i > 1) {
cells.push_back({ i, ie });
}
i = j = ie;
} else {
if (items0[i].key < items1[j].key) {
i = run_end(items0, i);
continue;
}
if (items1[j].key < items0[i].key) {
j = run_end(items1, j);
continue;
}
const long ie = run_end(items0, i);
const long je = run_end(items1, j);
cells.push_back({ { i, ie }, { j, je } });
i = ie;
j = je;
}
});

if (cells.size() == max_cells_per_batch) {
process_batch();
}
}
if (!cells.empty()) {
process_batch();
}
#ifdef IPC_TOOLKIT_HASH_GRID_USE_SORT_UNIQUE
merge_thread_local_vectors(storage, candidates);

// Remove the duplicate candidates
tbb::parallel_sort(candidates.begin(), candidates.end());
auto new_end = std::unique(candidates.begin(), candidates.end());
candidates.erase(new_end, candidates.end());
merge_thread_local_vectors(storage, candidates);
tbb::parallel_sort(candidates.begin(), candidates.end());
candidates.erase(
std::unique(candidates.begin(), candidates.end()),
candidates.end());
#else
unordered_set<Candidate> candidates_set;
merge_thread_local_unordered_sets(storage, candidates_set);

candidates.reserve(candidates_set.size());
candidates.insert(
candidates.end(), candidates_set.begin(), candidates_set.end());
unordered_set<Candidate> candidates_set;
merge_thread_local_unordered_sets(storage, candidates_set);
candidates.reserve(candidates_set.size());
candidates.insert(
candidates.end(), candidates_set.begin(), candidates_set.end());
#endif
}
} // namespace

template <typename Candidate>
void HashGrid::detect_candidates(
const std::vector<HashItem>& items0,
const std::vector<HashItem>& items1,
const AABBs& boxes0,
const AABBs& boxes1,
const std::function<bool(size_t, size_t)>& can_collide,
std::vector<Candidate>& candidates) const
{
detect_cell_pairs<Candidate, false>(
items0, items1, boxes0, boxes1, can_collide, candidates);
}

template <typename Candidate>
Expand All @@ -218,69 +267,8 @@ void HashGrid::detect_candidates(
const std::function<bool(size_t, size_t)>& can_collide,
std::vector<Candidate>& candidates) const
{
// Entries with the same key means they share a cell (that cell index
// hashes to the same key) and should be flagged for low-level
// intersection testing. So we loop over the entire sorted set of
// (key,value) pairs creating Candidate entries for pairs with the same key

#ifdef IPC_TOOLKIT_HASH_GRID_USE_SORT_UNIQUE
tbb::enumerable_thread_specific<std::vector<Candidate>> storage;
#else
tbb::enumerable_thread_specific<unordered_set<Candidate>> storage;
#endif

tbb::parallel_for(
tbb::blocked_range2d<long>(0L, items.size() - 1, 0L, items.size()),
[&](const tbb::blocked_range2d<long>& r) {
auto& local_candidates = storage.local();

// i < j
long i_end = std::min(r.rows().end(), r.cols().end());
for (long i = r.rows().begin(); i < i_end; i++) {
const HashItem& item0 = items[i];
const AABB& box0 = boxes[item0.id];

// i < r.cols().end() → i + 1 <= r.cols().end()
long j_begin = std::max(i + 1, r.cols().begin());
assert(j_begin > i);
for (long j = j_begin; j < r.cols().end(); j++) {
const HashItem& item1 = items[j];

if (item0.key != item1.key) {
break; // This avoids a brute force comparison
}

if (!can_collide(item0.id, item1.id)) {
continue;
}

const AABB& box1 = boxes[item1.id];
if (box0.intersects(box1)) {
#ifdef IPC_TOOLKIT_HASH_GRID_USE_SORT_UNIQUE
local_candidates.emplace_back(item0.id, item1.id);
#else
local_candidates.emplace(item0.id, item1.id);
#endif
}
}
}
});

#ifdef IPC_TOOLKIT_HASH_GRID_USE_SORT_UNIQUE
merge_thread_local_vectors(storage, candidates);

// Remove the duplicate candidates
tbb::parallel_sort(candidates.begin(), candidates.end());
auto new_end = std::unique(candidates.begin(), candidates.end());
candidates.erase(new_end, candidates.end());
#else
unordered_set<Candidate> candidates_set;
merge_thread_local_unordered_sets(storage, candidates_set);

candidates.reserve(candidates_set.size());
candidates.insert(
candidates.end(), candidates_set.begin(), candidates_set.end());
#endif
detect_cell_pairs<Candidate, true>(
items, items, boxes, boxes, can_collide, candidates);
}

void HashGrid::detect_vertex_vertex_candidates(
Expand Down
52 changes: 52 additions & 0 deletions tests/src/tests/broad_phase/benchmark_broad_phase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,60 @@
#include <ipc/broad_phase/spatial_hash.hpp>
#include <ipc/broad_phase/brute_force.hpp>

#include <tbb/global_control.h>

using namespace ipc;

namespace {
class HashGridTraversalBenchmark : public HashGrid {
public:
void prepare(size_t count, bool sparse)
{
clear();
dim = 3;
vertex_boxes.resize(count);
vertex_items.reserve(count);
// Prebuilt sorted cell runs isolate traversal from grid construction.
for (size_t i = 0; i < count; ++i) {
const long cell = sparse ? static_cast<long>(i / 2) : 0;
const double x =
sparse ? static_cast<double>(cell) + (i % 2 ? .75 : .25) : 0.;
auto& box = vertex_boxes[i];
box.min = Eigen::Array3d(x, 0., 0.);
box.max = box.min;
box.vertex_ids = { { static_cast<index_t>(i), -1, -1 } };
vertex_items.emplace_back(cell, static_cast<long>(i));
}
if (!sparse) {
can_vertices_collide = [](size_t, size_t) { return false; };
}
}
};
} // namespace

TEST_CASE(
"Benchmark HashGrid traversal on sparse and reject-heavy cells",
"[!benchmark][broad_phase][hash_grid][traversal]")
{
const int threads = GENERATE(1, 16);
const bool sparse = GENERATE(false, true);
const tbb::global_control limit(
tbb::global_control::max_allowed_parallelism, threads);
HashGridTraversalBenchmark grid;
grid.prepare(sparse ? 1048578 : 4096, sparse);
std::vector<VertexVertexCandidate> candidates;
BENCHMARK(
fmt::format(
"HashGrid {} ({} threads)", sparse ? "sparse" : "reject-all",
threads))
{
grid.detect_vertex_vertex_candidates(candidates);
return candidates.size();
};
grid.detect_vertex_vertex_candidates(candidates);
CHECK(candidates.empty());
}

TEST_CASE("Benchmark broad phase on simple scenes", "[!benchmark][broad_phase]")
{
Eigen::MatrixXd V0, V1;
Expand Down
Loading
Loading