Fx2D
A C++20 2D rigid-body physics engine
Loading...
Searching...
No Matches
Registry.h
Go to the documentation of this file.
1#pragma once
2
3#include "Fx2D/Execution.h"
4#include <algorithm>
5#include <cstdint>
6#include <functional>
7#include <iostream>
8#include <limits>
9#include <memory>
10#include <string>
11#include <unordered_map>
12#include <unordered_set>
13#include <vector>
14
15#include "Fx2D/AABBTree.h"
16
17template<typename T>
19 protected:
20 std::vector<std::shared_ptr<T>> m_items_vec; // packed storage for cache-friendly iteration
21 std::unordered_map<std::string, size_t> m_name_map; // name -> packed index in m_items_vec
22 size_t m_size_limit = std::numeric_limits<size_t>::max(); // Hard ceiling imposed by the derived
23 // registry
24 size_t m_max_size = std::numeric_limits<size_t>::max();
25
26 bool _add(const std::shared_ptr<T>& item) {
27 // The base registry only manages packed storage and name lookup.
28 if (!item) {
29 std::cerr << "FxNamedRegistry: Cannot add null item.\n";
30 return false;
31 }
32 if (m_items_vec.size() >= m_max_size) {
33 std::cerr << "FxNamedRegistry: Items limit exceeded.\n";
34 return false;
35 }
36
37 const std::string& name = item->get_name();
38 if (m_name_map.find(name) != m_name_map.end()) {
39 std::cerr << "FxNamedRegistry: Item '" << name << "' already exists.\n";
40 return false;
41 }
42
43 // New items are always appended so indices stay contiguous.
44 m_items_vec.push_back(item);
45 m_name_map.emplace(name, m_items_vec.size() - 1);
46 return true;
47 }
48
49 bool _remove(const std::string& name) {
50 // Removal is swap-pop so the packed vector stays dense.
51 auto it = m_name_map.find(name);
52 if (it == m_name_map.end()) {
53 std::cerr << "FxNamedRegistry: Item '" << name << "' not found.\n";
54 return false;
55 }
56
57 size_t idx = it->second;
58 size_t last = m_items_vec.size() - 1;
59 if (idx != last) {
60 // Move the last live item into the hole and repair its name -> index mapping.
61 m_items_vec[idx] = std::move(m_items_vec[last]);
62 const std::string& moved_name = m_items_vec[idx]->get_name();
63 m_name_map[moved_name] = idx;
64 }
65 // Pop the duplicated tail slot after the optional move above.
66 m_items_vec.pop_back();
67 m_name_map.erase(it);
68 return true;
69 }
70
71 public:
72 FxNamedRegistry() = default;
73 explicit FxNamedRegistry(size_t max_size) : m_max_size(max_size) {}
74
75 void reserve(size_t n) {
76 // Reserve both containers together so packed indices remain valid after insertion.
77 if (n > m_max_size) n = m_max_size;
78 m_items_vec.reserve(n);
79 m_name_map.reserve(n);
80 }
81
82 // Packed vector size is the canonical item count.
83 size_t size() const noexcept { return m_items_vec.size(); }
84 bool empty() const noexcept { return m_items_vec.empty(); }
85 void set_max_size(size_t n) {
86 if (n > m_size_limit) {
87 std::cerr << "FxNamedRegistry: clamping max_size " << n << " to limit.\n";
89 } else {
90 m_max_size = n;
91 }
92 }
93
94 virtual bool add(const std::shared_ptr<T>& item) { return _add(item); }
95 virtual bool remove(const std::string& name) { return _remove(name); }
96
97 std::shared_ptr<T> get(const std::string& name) const {
98 // Returning a shared_ptr extends the item's lifetime for the caller.
99 auto it = m_name_map.find(name);
100 if (it == m_name_map.end()) {
101 std::cerr << "FxNamedRegistry: Item '" << name << "' not found.\n";
102 return nullptr;
103 }
104 return m_items_vec[it->second];
105 }
106
107 T* get_rawptr(const std::string& name) const noexcept {
108 // Raw access avoids refcount churn when the caller only needs a borrowed pointer.
109 auto it = m_name_map.find(name);
110 return (it == m_name_map.end()) ? nullptr : m_items_vec[it->second].get();
111 }
112
113 // base if free, else base_1, base_2, ... until a free name is found.
114 std::string make_unique_name(const std::string& base) const {
115 if (m_name_map.find(base) == m_name_map.end()) return base;
116 for (int i = 1;; ++i) {
117 std::string candidate = base + "_" + std::to_string(i);
118 if (m_name_map.find(candidate) == m_name_map.end()) return candidate;
119 }
120 }
121
122 const std::vector<std::shared_ptr<T>>& items() const noexcept { return m_items_vec; }
123 std::vector<std::shared_ptr<T>>& items() noexcept { return m_items_vec; }
124
125 void clear() {
126 // Derived registries can override this when they have side caches to reset as well.
127 m_items_vec.clear();
128 m_name_map.clear();
129 }
130
132 // Rehash(0) lets the unordered_map release extra buckets after compaction.
133 m_items_vec.shrink_to_fit();
134 m_name_map.rehash(0);
135 }
136
137 private:
138 // Scratch for the raw-pointer snapshot below, kept as a member so its capacity survives.
139 std::vector<T*> m_raw_items;
140 bool m_raw_items_in_use = false;
141
142 void fill_raw(std::vector<T*>& out) const {
143 out.clear();
144 out.reserve(m_items_vec.size());
145 for (const auto& item : m_items_vec) {
146 out.push_back(item.get());
147 }
148 }
149
150 // Borrows the shared scratch buffer for one sweep, falling back to a local vector if a
151 // sweep is already using it. A fresh vector per sweep was tens of allocations per step; the
152 // fallback keeps that safe when a callback starts a second sweep of the same registry.
153 class RawSweep {
154 public:
155 explicit RawSweep(FxNamedRegistry& registry) : m_registry(registry) {
156 if (m_registry.m_raw_items_in_use) {
157 m_registry.fill_raw(m_local);
158 m_items = &m_local;
159 return;
160 }
161 m_registry.m_raw_items_in_use = true;
162 m_owns_shared = true;
163 m_registry.fill_raw(m_registry.m_raw_items);
164 m_items = &m_registry.m_raw_items;
165 }
166 ~RawSweep() {
167 if (m_owns_shared) m_registry.m_raw_items_in_use = false;
168 }
169 RawSweep(const RawSweep&) = delete;
170 RawSweep& operator=(const RawSweep&) = delete;
171
172 std::vector<T*>& items() const { return *m_items; }
173
174 private:
175 FxNamedRegistry& m_registry;
176 std::vector<T*> m_local;
177 std::vector<T*>* m_items = nullptr;
178 bool m_owns_shared = false;
179 };
180
181 public:
182 template<typename ExecPolicy, typename Func>
183 void for_each(ExecPolicy&& policy, Func&& func) {
184 // Snapshot raw pointers first so parallel algorithms iterate a simple contiguous array,
185 // and so the callee cannot invalidate the range it is walking.
186 RawSweep sweep(*this);
187 std::vector<T*>& raw = sweep.items();
188
189#if FX2D_HAS_EXECUTION_POLICIES
190 std::for_each(std::forward<ExecPolicy>(policy), raw.begin(), raw.end(),
191 std::forward<Func>(func));
192#else
193 (void)policy;
194 std::for_each(raw.begin(), raw.end(), std::forward<Func>(func));
195#endif
196 }
197
198 template<typename ExecPolicy, typename Func>
199 void transform(ExecPolicy&& policy, Func&& func,
200 std::vector<std::invoke_result_t<Func, std::shared_ptr<T>>>& results) {
201 // Pre-size the output so std::transform can write by index in one pass.
202 results.resize(m_items_vec.size());
203
204 // Same snapshot as for_each, for the same reasons.
205 RawSweep sweep(*this);
206 std::vector<T*>& raw = sweep.items();
207
208#if FX2D_HAS_EXECUTION_POLICIES
209 std::transform(std::forward<ExecPolicy>(policy), raw.begin(), raw.end(), results.begin(),
210 std::forward<Func>(func));
211#else
212 (void)policy;
213 std::transform(raw.begin(), raw.end(), results.begin(), std::forward<Func>(func));
214#endif
215 }
216};
217
218class FxEntity;
219
220// Specialized registry for entities that handles collision pair exclusion.
221class FxEntityRegistry : public FxNamedRegistry<FxEntity> {
222 private:
223 std::unordered_set<uint64_t> m_no_collision_pairs; // excluded collision pairs
224 uint32_t m_next_entity_id = 0; // entity ID counter
225 mutable FxAABBTree m_aabb_tree; // dynamic AABB tree
226 // entity_id -> packed index. Ids are dense and issued in order from zero, so a vector is
227 // both smaller and faster than the hash map this used to be, and the broad phase resolves
228 // two of these per pair. -1 marks an id that has been removed.
229 std::vector<int32_t> m_id_to_index;
230
231 // Scratch for the tree pair query, reused across substeps.
232 mutable std::vector<std::pair<int32_t, int32_t>> m_tree_pairs;
233
234 void remove_entity_from_tree(FxEntity& entity) const {
235 // Keep the tree free of stale leaves when an entity disappears or loses collision geometry.
236 const int32_t node = entity.broad_phase_node();
237 if (node < 0) return;
238 m_aabb_tree.remove(node);
239 entity.set_broad_phase_node(-1);
240 }
241
242 int32_t index_of_id(uint32_t entity_id) const {
243 if (entity_id >= m_id_to_index.size()) return -1;
244 return m_id_to_index[entity_id];
245 }
246
247 void erase_collision_pairs_for(uint32_t entity_id) {
248 // Collision exclusions are keyed by entity id, so purge every pair that references the
249 // removed entity.
250 for (auto it = m_no_collision_pairs.begin(); it != m_no_collision_pairs.end();) {
251 uint32_t a = static_cast<uint32_t>(*it >> 32);
252 uint32_t b = static_cast<uint32_t>(*it & 0xffffffffULL);
253 if (a == entity_id || b == entity_id) {
254 it = m_no_collision_pairs.erase(it);
255 } else {
256 ++it;
257 }
258 }
259 }
260
261 public:
262 FxEntityRegistry() = default;
263 explicit FxEntityRegistry(size_t max_size) : FxNamedRegistry<FxEntity>() {
264 m_size_limit = std::numeric_limits<uint32_t>::max();
265 set_max_size(max_size);
266 // A lower load factor keeps collision-pair lookups closer to O(1) under load.
267 m_no_collision_pairs.max_load_factor(0.7f);
268 }
269
270 bool add(const std::shared_ptr<FxEntity>& entity) override {
271 if (m_next_entity_id == std::numeric_limits<uint32_t>::max()) {
272 std::cerr << "FxEntityRegistry: Entity ID limit exceeded.\n";
273 return false;
274 }
275
276 // Entity ids stay stable even if packed indices move after swap-pop removal.
277 entity->set_entity_id(m_next_entity_id);
278 bool success = _add(entity);
279 if (success) {
280 // Broad-phase queries translate tree entity ids back to packed indices through this
281 // map.
282 const int32_t packed = static_cast<int32_t>(m_items_vec.size() - 1);
283 if (m_next_entity_id >= m_id_to_index.size()) {
284 m_id_to_index.resize(static_cast<size_t>(m_next_entity_id) + 1, -1);
285 }
286 m_id_to_index[m_next_entity_id] = packed;
287 entity->set_packed_index(packed);
288 m_next_entity_id++;
289 }
290 return success;
291 }
292
293 bool remove(const std::string& name) override {
294 auto it = m_name_map.find(name);
295 if (it == m_name_map.end()) {
296 std::cerr << "FxEntityRegistry: Item '" << name << "' not found.\n";
297 return false;
298 }
299
300 size_t idx = it->second;
301 size_t last = m_items_vec.size() - 1;
302 uint32_t removed_id = m_items_vec[idx]->get_entity_id();
303 bool moved_last = (idx != last);
304 uint32_t moved_id = moved_last ? m_items_vec[last]->get_entity_id() : 0;
305
306 // Clean broad-phase and collision-filter state before the packed storage changes underneath
307 // us.
308 remove_entity_from_tree(*m_items_vec[idx]);
309 erase_collision_pairs_for(removed_id);
310 m_items_vec[idx]->set_packed_index(-1);
311 m_id_to_index[removed_id] = -1;
312
313 bool success = _remove(name);
314 if (success && moved_last) {
315 // Swap-pop removal can move the last entity into idx, so refresh its cached packed
316 // index.
317 m_id_to_index[moved_id] = static_cast<int32_t>(idx);
318 m_items_vec[idx]->set_packed_index(static_cast<int32_t>(idx));
319 }
320 return success;
321 }
322
323 void clear() {
324 // Entities outlive the registry that held them, so their cached indices have to be
325 // invalidated here rather than left pointing into storage that no longer exists.
326 for (const auto& e : m_items_vec) {
327 if (e) {
328 e->set_broad_phase_node(-1);
329 e->set_packed_index(-1);
330 }
331 }
333 m_no_collision_pairs.clear();
334 m_id_to_index.clear();
335 // Rebuild the tree pool from scratch so old node indices cannot leak across clear().
336 m_aabb_tree = FxAABBTree{};
337 m_next_entity_id = 0;
338 }
339
340 void enable_collision(const std::string& entity1_name, const std::string& entity2_name) {
341 auto e1 = get_rawptr(entity1_name);
342 auto e2 = get_rawptr(entity2_name);
343 if (e1 && e2 && e1 != e2) {
344 uint64_t pair_id = pack_id_pair(e1->get_entity_id(), e2->get_entity_id());
345 m_no_collision_pairs.erase(pair_id);
346 }
347 }
348
349 void disable_collision(const std::string& entity1_name, const std::string& entity2_name) {
350 auto e1 = get_rawptr(entity1_name);
351 auto e2 = get_rawptr(entity2_name);
352 if (e1 && e2 && e1 != e2) {
353 uint64_t pair_id = pack_id_pair(e1->get_entity_id(), e2->get_entity_id());
354 m_no_collision_pairs.insert(pair_id);
355 }
356 }
357
358 // Sync the tree from current entity state, then collect overlapping pairs. Convenience
359 // overload; FxScene uses the out-parameter form so the pair buffer is allocated once.
360 std::vector<std::pair<size_t, size_t>> get_broad_phase_pairs(float sweep_dt = 0.0f) const {
361 std::vector<std::pair<size_t, size_t>> pairs;
362 get_broad_phase_pairs(pairs, sweep_dt);
363 return pairs;
364 }
365
366 // sweep_all_movers extends every mover's box along its velocity, not just CCD bodies, so
367 // the list stays valid for the whole of sweep_dt. skip_sleeping_pairs must be false for a
368 // list reused across a step, or a body woken partway through loses its pairs.
369 void get_broad_phase_pairs(std::vector<std::pair<size_t, size_t>>& pairs, float sweep_dt = 0.0f,
370 bool sweep_all_movers = false,
371 bool skip_sleeping_pairs = true) const {
372 sync_broad_phase(sweep_dt, sweep_all_movers);
373 collect_broad_phase_pairs(pairs, skip_sleeping_pairs);
374 }
375
376 // Bring every proxy up to date and report whether any leaf was inserted, removed or
377 // reinserted. That return value is what lets FxScene skip the pair walk: an unchanged tree
378 // would rebuild the identical list. Syncing is a linear sweep; the walk is a descent.
379 bool sync_broad_phase(float sweep_dt = 0.0f, bool sweep_all_movers = false) const {
380 bool tree_changed = false;
381 for (size_t i = 0; i < m_items_vec.size(); ++i) {
382 const auto& e = m_items_vec[i];
383 const int32_t node = e->broad_phase_node();
384 const bool in_tree = (node >= 0);
385
386 if (!e->enabled || !e->collision_geometry()) {
387 // Disabled or geometry-less entities should never leave a broad-phase proxy behind.
388 if (in_tree) {
389 remove_entity_from_tree(*e);
390 tree_changed = true;
391 }
392 continue;
393 }
394
395 const auto& bb = e->bounding_box();
396 FxAABB tight{bb[0], bb[1], bb[2], bb[3]};
397 // An axis-aligned edge or chain is zero-thickness on one axis, which is_valid()
398 // rejects, so the entity would never enter the tree and never collide. Give the
399 // proxy a hair of thickness; the shape's own AABB is left alone.
400 constexpr float kMinProxyExtent = 1e-3f;
401 if (tight.maxX - tight.minX < kMinProxyExtent) {
402 tight.minX -= kMinProxyExtent;
403 tight.maxX += kMinProxyExtent;
404 }
405 if (tight.maxY - tight.minY < kMinProxyExtent) {
406 tight.minY -= kMinProxyExtent;
407 tight.maxY += kMinProxyExtent;
408 }
409 if (!tight.is_valid()) continue;
410
411 // Sweeping every mover was measured to flood the tree with false pairs when many
412 // bodies fall together, so only CCD bodies extend their box by default -- they are
413 // the ones whose speculative contacts need to see a partner before they reach it.
414 FxAABB query_aabb = tight;
415 if ((sweep_all_movers || e->enable_ccd) && sweep_dt > 0.0f) {
416 float dx = e->velocity.x() * sweep_dt;
417 float dy = e->velocity.y() * sweep_dt;
418 if (dx != 0.0f || dy != 0.0f) {
419 FxAABB swept{tight.minX + dx, tight.minY + dy, tight.maxX + dx,
420 tight.maxY + dy};
421 query_aabb = FxAABB::combine(tight, swept);
422 }
423 }
424
425 if (!in_tree) {
426 // New or re-enabled entities lazily create their tree leaf on demand.
427 e->set_broad_phase_node(
428 m_aabb_tree.insert(static_cast<int32_t>(e->get_entity_id()), query_aabb));
429 tree_changed = true;
430 } else if (m_aabb_tree.update(node, query_aabb)) {
431 tree_changed = true;
432 }
433 }
434 return tree_changed;
435 }
436
437 // Walk the synced tree for overlapping proxies and translate them into packed indices,
438 // applying the registry-level filters. Assumes sync_broad_phase has already run.
439 void collect_broad_phase_pairs(std::vector<std::pair<size_t, size_t>>& pairs,
440 bool skip_sleeping_pairs = true) const {
441 // The scratch buffer is a member so its capacity carries across calls; query_pairs
442 // clears it before filling.
443 m_aabb_tree.query_pairs(m_tree_pairs);
444
445 pairs.clear();
446 pairs.reserve(m_tree_pairs.size());
447 for (const auto& [eid_a, eid_b] : m_tree_pairs) {
448 const int32_t ia = index_of_id(static_cast<uint32_t>(eid_a));
449 const int32_t ib = index_of_id(static_cast<uint32_t>(eid_b));
450 if (ia < 0 || ib < 0) continue;
451
452 size_t i = static_cast<size_t>(ia);
453 size_t j = static_cast<size_t>(ib);
454
455 if (skip_sleeping_pairs && m_items_vec[i]->is_sleeping() &&
456 m_items_vec[j]->is_sleeping() && !m_items_vec[i]->enable_ccd &&
457 !m_items_vec[j]->enable_ccd)
458 continue;
459 if (!is_collision_pair(static_cast<uint32_t>(eid_a), static_cast<uint32_t>(eid_b)))
460 continue;
461 // One integer per body replaces O(N^2) pair exclusions for intra-group filtering.
462 if (m_items_vec[i]->collision_group != 0 &&
463 m_items_vec[i]->collision_group == m_items_vec[j]->collision_group &&
464 m_items_vec[i]->collision_group < 0)
465 continue;
466 if (!m_items_vec[i]->collision_geometry() || !m_items_vec[j]->collision_geometry())
467 continue;
468
469 if (i > j) std::swap(i, j);
470 pairs.emplace_back(i, j);
471 }
472 }
473
474 private:
475 static uint64_t pack_id_pair(uint32_t a, uint32_t b) {
476 if (a > b) std::swap(a, b);
477 return static_cast<uint64_t>(a) << 32 | static_cast<uint64_t>(b);
478 }
479
480 bool is_collision_pair(uint32_t entity1_id, uint32_t entity2_id) const {
481 uint64_t pair_id = pack_id_pair(entity1_id, entity2_id);
482 return m_no_collision_pairs.find(pair_id) == m_no_collision_pairs.end();
483 }
484};
Dynamic broad-phase tree used to find candidate pairs.
Definition AABBTree.h:26
void query_pairs(std::vector< std::pair< int32_t, int32_t > > &out) const
Definition AABBTree.h:62
bool update(int32_t node_idx, const FxAABB &new_tight)
Definition AABBTree.h:48
int32_t insert(int32_t entity_id, const FxAABB &tight)
Definition AABBTree.h:31
void remove(int32_t node_idx)
Definition AABBTree.h:41
void collect_broad_phase_pairs(std::vector< std::pair< size_t, size_t > > &pairs, bool skip_sleeping_pairs=true) const
Definition Registry.h:439
bool add(const std::shared_ptr< FxEntity > &entity) override
Definition Registry.h:270
bool sync_broad_phase(float sweep_dt=0.0f, bool sweep_all_movers=false) const
Definition Registry.h:379
FxEntityRegistry()=default
FxEntityRegistry(size_t max_size)
Definition Registry.h:263
std::vector< std::pair< size_t, size_t > > get_broad_phase_pairs(float sweep_dt=0.0f) const
Definition Registry.h:360
bool remove(const std::string &name) override
Definition Registry.h:293
void get_broad_phase_pairs(std::vector< std::pair< size_t, size_t > > &pairs, float sweep_dt=0.0f, bool sweep_all_movers=false, bool skip_sleeping_pairs=true) const
Definition Registry.h:369
void enable_collision(const std::string &entity1_name, const std::string &entity2_name)
Definition Registry.h:340
void disable_collision(const std::string &entity1_name, const std::string &entity2_name)
Definition Registry.h:349
A named rigid body with state, material properties, geometry, and forces.
Definition Entity.h:49
int32_t broad_phase_node() const
Definition Entity.h:146
void set_broad_phase_node(int32_t node)
Definition Entity.h:147
virtual bool add(const std::shared_ptr< T > &item)
Definition Registry.h:94
bool _remove(const std::string &name)
Definition Registry.h:49
std::string make_unique_name(const std::string &base) const
Definition Registry.h:114
size_t m_max_size
Definition Registry.h:24
size_t m_size_limit
Definition Registry.h:22
size_t size() const noexcept
Definition Registry.h:83
const std::vector< std::shared_ptr< T > > & items() const noexcept
Definition Registry.h:122
std::shared_ptr< T > get(const std::string &name) const
Definition Registry.h:97
void shrink_to_fit()
Definition Registry.h:131
void set_max_size(size_t n)
Definition Registry.h:85
std::unordered_map< std::string, size_t > m_name_map
Definition Registry.h:21
std::vector< std::shared_ptr< T > > m_items_vec
Definition Registry.h:20
bool _add(const std::shared_ptr< T > &item)
Definition Registry.h:26
void reserve(size_t n)
Definition Registry.h:75
virtual bool remove(const std::string &name)
Definition Registry.h:95
T * get_rawptr(const std::string &name) const noexcept
Definition Registry.h:107
std::vector< std::shared_ptr< T > > & items() noexcept
Definition Registry.h:123
FxNamedRegistry()=default
bool empty() const noexcept
Definition Registry.h:84
void for_each(ExecPolicy &&policy, Func &&func)
Definition Registry.h:183
void transform(ExecPolicy &&policy, Func &&func, std::vector< std::invoke_result_t< Func, std::shared_ptr< T > > > &results)
Definition Registry.h:199
FxNamedRegistry(size_t max_size)
Definition Registry.h:73
Axis-aligned bounding box used by broad-phase operations.
Definition Geometry.h:20
float minX
Definition Geometry.h:21
static FxAABB combine(const FxAABB &a, const FxAABB &b)
Definition Geometry.h:25