Gaia-ECS v1.0.0
A simple and powerful entity component system
Loading...
Searching...
No Matches
query_cache.h
1#pragma once
2#include "gaia/config/config.h"
3
4#include "gaia/cnt/darray.h"
5#include "gaia/cnt/map.h"
6#include "gaia/cnt/paged_storage.h"
7#include "gaia/core/utility.h"
8#include "gaia/ecs/component.h"
9#include "gaia/ecs/id.h"
10#include "gaia/ecs/query_common.h"
11#include "gaia/ecs/query_info.h"
12
14namespace gaia {
15 namespace cnt {
16 template <>
17 struct to_page_storage_id<ecs::QueryInfo> {
21 static page_storage_id get(const ecs::QueryInfo& item) noexcept {
22 return item.idx;
23 }
24 };
25 } // namespace cnt
26
27 namespace ecs {
28 class QueryLookupKey {
29 QueryLookupHash m_hash;
30 const QueryCtx* m_pCtx;
31
32 public:
33 static constexpr bool IsDirectHashKey = true;
34
35 QueryLookupKey(): m_hash({0}), m_pCtx(nullptr) {}
36 explicit QueryLookupKey(QueryLookupHash hash, const QueryCtx* pCtx): m_hash(hash), m_pCtx(pCtx) {}
37
40 size_t hash() const {
41 return (size_t)m_hash.hash;
42 }
43
44 bool operator==(const QueryLookupKey& other) const {
45 // Hash doesn't match we don't have a match.
46 // Hash collisions are expected to be very unlikely so optimize for this case.
47 if GAIA_LIKELY (m_hash != other.m_hash)
48 return false;
49
50 if (m_pCtx == other.m_pCtx)
51 return true;
52
53 const auto lhsReal = m_pCtx->q.handle.id() != QueryIdBad;
54 const auto rhsReal = other.m_pCtx->q.handle.id() != QueryIdBad;
55
56 // Two persisted cached-query keys with different stable context pointers cannot represent
57 // the same query.
58 if (lhsReal && rhsReal)
59 return false;
60
61 // At least one side is a temporary lookup key. Fall back to structural comparison so cached
62 // queries deduplicate correctly regardless of comparison direction.
63 return QueryCtx::equals_no_handle_assumption(*m_pCtx, *other.m_pCtx);
64 }
65 };
66
67 class QueryCache {
68 public:
70 enum class ChangeKind : uint8_t {
71 // Query membership may have changed due to structural world changes.
72 Structural,
73 // Only dynamic/source-driven results may have changed.
74 DynamicResult,
75 // Full query cache invalidation.
76 All,
77 };
78
79 private:
84 QueryInfo& register_query_info(QueryHandle handle, QueryInfo& info) {
85 // Add the entity->query pair
86 add_entity_to_query_pairs(info.ctx().data.ids_view(), handle);
87 add_rel_to_query_pairs(info.ctx(), handle);
88 add_sort_to_query_pairs(info.ctx(), handle);
89 add_sorted_query(info.ctx(), handle);
90 add_create_to_query_pairs(info.ctx(), handle);
91
92 return info;
93 }
94
97 struct CreateQueryCandidate {
98 QueryHandle handle;
99 Entity matchedSelector = EntityBad;
100 };
101
104 enum class CreateSelectorKind : uint8_t {
105 Other,
106 ExactPair,
107 RelWildcardPair,
108 TgtWildcardPair,
109 AnyPairWildcard,
110 Count
111 };
112
114 struct TrackedArchetypes {
115 cnt::darray<const Archetype*> archetypes;
116 uint32_t syncedRevision = 0;
117 };
118
120 cnt::map<QueryLookupKey, QueryInfo*> m_pCache;
122 cnt::paged_ilist<QueryInfo, QueryHandle> m_queryArr;
123
125 cnt::map<EntityLookupKey, cnt::darray<QueryHandle>> m_entityToQuery;
127 cnt::map<EntityLookupKey, cnt::darray<QueryHandle>> m_relationToQuery;
129 cnt::map<EntityLookupKey, cnt::darray<QueryHandle>> m_sortEntityToQuery;
131 cnt::darray<QueryHandle> m_sortedQueries;
133 cnt::map<EntityLookupKey, cnt::darray<QueryHandle>> m_entityToCreateQuery;
135 cnt::map<ArchetypeIdLookupKey, cnt::darray<QueryHandle>> m_archetypeToQuery;
137 cnt::map<QueryHandleLookupKey, TrackedArchetypes> m_queryToArchetype;
139 cnt::darray<CreateQueryCandidate> m_createQueryHandleScratch;
141 cnt::darray<uint32_t> m_createQueryHandleStampById;
142 uint32_t m_createQueryHandleStamp = 1;
144 uint32_t m_createQuerySelectorCnt[(size_t)CreateSelectorKind::Count] = {};
145
146 public:
147 QueryCache() {
148 m_queryArr.reserve(256);
149 }
150
151 ~QueryCache() = default;
152
153 QueryCache(QueryCache&&) = delete;
154 QueryCache(const QueryCache&) = delete;
155 QueryCache& operator=(QueryCache&&) = delete;
156 QueryCache& operator=(const QueryCache&) = delete;
157
161 GAIA_NODISCARD bool valid(QueryHandle handle) const {
162 if (handle.id() == QueryIdBad)
163 return false;
164
165 if (!m_queryArr.has(handle.id()))
166 return false;
167
168 const auto& h = m_queryArr[handle.id()];
169 return h.idx == handle.id() && h.gen == handle.gen();
170 }
171
173 void clear() {
174 m_pCache.clear();
175 m_queryArr.clear();
176 m_entityToQuery.clear();
177 m_relationToQuery.clear();
178 m_sortEntityToQuery.clear();
179 m_sortedQueries.clear();
180 m_entityToCreateQuery.clear();
181 m_archetypeToQuery.clear();
182 m_queryToArchetype.clear();
183 m_createQueryHandleScratch.clear();
184 m_createQueryHandleStampById.clear();
185 m_createQueryHandleStamp = 1;
186 for (auto& cnt: m_createQuerySelectorCnt)
187 cnt = 0;
188 }
189
192 void clear_archetype_tracking() {
193 m_archetypeToQuery.clear();
194 m_queryToArchetype.clear();
195 }
196
200 QueryInfo* try_get(QueryHandle handle) {
201 if (!valid(handle))
202 return nullptr;
203
204 auto& info = m_queryArr[handle.id()];
205 GAIA_ASSERT(info.idx == handle.id());
206 GAIA_ASSERT(info.gen == handle.gen());
207 return &info;
208 }
209
213 const QueryInfo* try_get(QueryHandle handle) const {
214 if (!valid(handle))
215 return nullptr;
216
217 const auto& info = m_queryArr[handle.id()];
218 GAIA_ASSERT(info.idx == handle.id());
219 GAIA_ASSERT(info.gen == handle.gen());
220 return &info;
221 }
222
223#if GAIA_ECS_TEST_HOOKS
225 GAIA_NODISCARD bool verify_archetype_tracking() const {
226 for (const auto& pair: m_queryToArchetype) {
227 const auto handle = pair.first.handle();
228 const auto* pInfo = try_get(handle);
229 if (pInfo == nullptr || pInfo->refs() == 0)
230 return false;
231
232 const auto& tracked = pair.second.archetypes;
233 for (uint32_t i = 0; i < tracked.size(); ++i) {
234 const auto* pArchetype = tracked[i];
235 if (pArchetype == nullptr)
236 return false;
237
238 for (uint32_t j = i + 1; j < tracked.size(); ++j) {
239 if (tracked[j] == pArchetype)
240 return false;
241 }
242
243 if (!archetype_span_contains(pInfo->cache_archetype_view(), pArchetype))
244 return false;
245
246 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
247 const auto reverseIt = m_archetypeToQuery.find(archetypeKey);
248 if (reverseIt == m_archetypeToQuery.end() || !core::has(reverseIt->second, handle))
249 return false;
250 }
251 }
252
253 for (const auto& pair: m_archetypeToQuery) {
254 const auto& handles = pair.second;
255 for (uint32_t i = 0; i < handles.size(); ++i) {
256 const auto handle = handles[i];
257 const auto* pInfo = try_get(handle);
258 if (pInfo == nullptr || pInfo->refs() == 0)
259 return false;
260
261 for (uint32_t j = i + 1; j < handles.size(); ++j) {
262 if (handles[j] == handle)
263 return false;
264 }
265
266 const auto trackedIt = m_queryToArchetype.find(QueryHandleLookupKey(handle));
267 if (trackedIt == m_queryToArchetype.end())
268 return false;
269
270 if (!tracked_archetypes_contain(trackedIt->second.archetypes, pair.first))
271 return false;
272 }
273 }
274
275 return true;
276 }
277
280 GAIA_NODISCARD uint32_t test_query_count() const {
281 return (uint32_t)m_queryArr.item_count();
282 }
283#endif
284
288 QueryInfo& get(QueryHandle handle) {
289 GAIA_ASSERT(valid(handle));
290
291 auto& info = m_queryArr[handle.id()];
292 GAIA_ASSERT(info.idx == handle.id());
293 GAIA_ASSERT(info.gen == handle.gen());
294 return info;
295 }
296
302 QueryInfo&
303 add(QueryCtx&& ctx, //
304 const EntityToArchetypeMap& entityToArchetypeMap, //
305 std::span<const Archetype*> allArchetypes) {
306 GAIA_ASSERT(ctx.hashLookup.hash != 0);
307
308 // First check if the query cache record exists
309 auto ret = m_pCache.try_emplace(QueryLookupKey(ctx.hashLookup, &ctx), nullptr);
310 if (!ret.second) {
311 auto* pInfo = ret.first->second;
312 GAIA_ASSERT(pInfo != nullptr);
313 pInfo->add_ref();
314 return *pInfo;
315 }
316
317 // No record exists, let us create a new one
318 QueryInfoCreationCtx creationCtx{};
319 creationCtx.pQueryCtx = &ctx;
320 creationCtx.pEntityToArchetypeMap = &entityToArchetypeMap;
321 creationCtx.allArchetypes = allArchetypes;
322 auto handle = m_queryArr.alloc(&creationCtx);
323
324 // We are moving the rvalue to "ctx". As a result, the pointer stored in m_pCache.emplace above is no longer
325 // going to be valid. Therefore we swap the map key with a one with a valid pointer.
326 auto& info = get(handle);
327 info.add_ref();
328 ret.first->second = &info;
329 auto new_p = robin_hood::pair(std::make_pair(QueryLookupKey(ctx.hashLookup, &info.ctx()), &info));
330 ret.first->swap(new_p);
331
332 return register_query_info(handle, info);
333 }
334
340 QueryInfo& add_local(
341 QueryCtx&& ctx, //
342 const EntityToArchetypeMap& entityToArchetypeMap, //
343 std::span<const Archetype*> allArchetypes) {
344 QueryInfoCreationCtx creationCtx{};
345 creationCtx.pQueryCtx = &ctx;
346 creationCtx.pEntityToArchetypeMap = &entityToArchetypeMap;
347 creationCtx.allArchetypes = allArchetypes;
348 auto handle = m_queryArr.alloc(&creationCtx);
349
350 auto& info = get(handle);
351 info.add_ref();
352 return register_query_info(handle, info);
353 }
354
358 bool del(QueryHandle handle) {
359 auto* pInfo = try_get(handle);
360 if (pInfo == nullptr)
361 return false;
362
363 pInfo->dec_ref();
364 if (pInfo->refs() != 0)
365 return false;
366
367 unregister_query_archetypes(handle);
368
369 // If this was the last reference to the query, we can safely remove it
370 auto it = m_pCache.find(QueryLookupKey(pInfo->ctx().hashLookup, &pInfo->ctx()));
371 if (it != m_pCache.end())
372 m_pCache.erase(it);
373
374 // Remove the entity->query pair
375 del_entity_to_query_pairs(pInfo->ctx().data.ids_view(), handle);
376 del_rel_to_query_pairs(pInfo->ctx(), handle);
377 del_sort_to_query_pairs(pInfo->ctx(), handle);
378 del_sorted_query(pInfo->ctx(), handle);
379 del_create_to_query_pairs(pInfo->ctx(), handle);
380 m_queryArr.free(handle);
381
382 return true;
383 }
384
386 auto begin() {
387 return m_queryArr.begin();
388 }
389
391 auto end() {
392 return m_queryArr.end();
393 }
394
397 GAIA_NODISCARD bool has_relation_query_dependencies() const {
398 return !m_relationToQuery.empty();
399 }
400
408 void invalidate_queries_for_entity(EntityLookupKey entityKey, ChangeKind changeKind) {
409 auto it = m_entityToQuery.find(entityKey);
410 if (it == m_entityToQuery.end())
411 return;
412
413 const auto& handles = it->second;
414 for (const auto& handle: handles) {
415 auto& info = get(handle);
416 // World mutations invalidate cached results, but they do not change query shape.
417 // Recomputing QueryCtx metadata here only adds overhead to the invalidation path.
418 info.invalidate(select_invalidation_kind(info, changeKind));
419 }
420 }
421
425 void invalidate_queries_for_rel(Entity relation, ChangeKind changeKind) {
426 auto it = m_relationToQuery.find(EntityLookupKey(relation));
427 if (it == m_relationToQuery.end())
428 return;
429
430 for (const auto handle: it->second) {
431 auto& info = get(handle);
432 // Relation changes affect dynamic freshness, not the query definition itself.
433 info.invalidate(select_invalidation_kind(info, changeKind));
434 }
435 }
436
439 GAIA_NODISCARD bool has_sorted_queries() const {
440 return !m_sortedQueries.empty();
441 }
442
446 GAIA_NODISCARD bool has_sorted_queries_for_entity(Entity entity) const {
447 const auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
448 return it != m_sortEntityToQuery.end() && !it->second.empty();
449 }
450
453 void invalidate_sorted_queries_for_entity(Entity entity) {
454 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
455 if (it == m_sortEntityToQuery.end())
456 return;
457
458 for (const auto handle: it->second) {
459 auto* pInfo = try_get(handle);
460 if (pInfo == nullptr || pInfo->refs() == 0)
461 continue;
462
463 pInfo->invalidate_sort();
464 }
465 }
466
468 void invalidate_sorted_queries() {
469 for (const auto handle: m_sortedQueries) {
470 auto* pInfo = try_get(handle);
471 if (pInfo == nullptr || pInfo->refs() == 0)
472 continue;
473
474 pInfo->invalidate_sort();
475 }
476 }
477
480 void sync_archetype_cache(QueryInfo& queryInfo) {
481 const auto handle = QueryInfo::handle(queryInfo);
482 if (!valid(handle))
483 return;
484
485 const auto archetypes = queryInfo.cache_archetype_view();
486 const auto key = QueryHandleLookupKey(handle);
487 auto it = m_queryToArchetype.find(key);
488 if (it != m_queryToArchetype.end() && it->second.syncedRevision == queryInfo.result_cache_rev())
489 return;
490
491 unregister_query_archetypes(handle);
492
493 if (archetypes.empty())
494 return;
495
496 auto [trackedIt, inserted] = m_queryToArchetype.try_emplace(key);
497 auto& tracked = trackedIt->second.archetypes;
498 if (!inserted)
499 tracked.clear();
500
501 tracked.reserve((uint32_t)archetypes.size());
502 for (const auto* pArchetype: archetypes) {
503 tracked.push_back(pArchetype);
504 add_archetype_query_pair(pArchetype, handle);
505 }
506 trackedIt->second.syncedRevision = queryInfo.result_cache_rev();
507 }
508
511 void remove_archetype_from_queries(Archetype* pArchetype) {
512 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
513 auto it = m_archetypeToQuery.find(archetypeKey);
514 if (it == m_archetypeToQuery.end())
515 return;
516
517 const auto handles = it->second;
518 for (const auto handle: handles) {
519 auto* pInfo = try_get(handle);
520 if (pInfo != nullptr && pInfo->refs() != 0)
521 pInfo->remove(pArchetype);
522
523 auto trackedIt = m_queryToArchetype.find(QueryHandleLookupKey(handle));
524 if (trackedIt == m_queryToArchetype.end())
525 continue;
526
527 auto& tracked = trackedIt->second.archetypes;
528 core::swap_erase(tracked, core::get_index(tracked, pArchetype));
529 if (tracked.empty())
530 m_queryToArchetype.erase(trackedIt);
531 }
532
533 m_archetypeToQuery.erase(it);
534 }
535
538 void register_archetype_with_queries(const Archetype* pArchetype) {
539 if (m_entityToCreateQuery.empty()) {
540 (void)pArchetype;
541 return;
542 }
543
544 auto& handles = prepare_create_query_handles();
545 const bool needsExactPairSelectors = has_create_selector_kind(CreateSelectorKind::ExactPair);
546 const bool needsRelWildcardSelectors = has_create_selector_kind(CreateSelectorKind::RelWildcardPair);
547 const bool needsTgtWildcardSelectors = has_create_selector_kind(CreateSelectorKind::TgtWildcardPair);
548 const bool needsAnyPairWildcardSelectors = has_create_selector_kind(CreateSelectorKind::AnyPairWildcard);
549 bool hasAnyPair = false;
550 cnt::darray_ext<Entity, 16> pairWildcardRelations;
551 for (const auto entity: pArchetype->ids_view()) {
552 if (!entity.pair()) {
553 add_create_query_handles(entity, handles);
554 continue;
555 }
556
557 hasAnyPair = true;
558 if (needsExactPairSelectors)
559 add_create_query_handles(entity, handles);
560
561 // Pair ids retain the relation/target ids plus their kind bits. That is enough to
562 // rebuild wildcard pair lookup keys without touching the world record storage.
563 const auto relKind = entity.entity() ? EntityKind::EK_Uni : EntityKind::EK_Gen;
564 const auto rel = Entity((EntityId)entity.id(), 0, false, false, relKind);
565 const auto tgt = Entity((EntityId)entity.gen(), 0, false, false, entity.kind());
566 if (needsTgtWildcardSelectors)
567 add_create_query_handles(Pair(All, tgt), handles);
568 if (needsRelWildcardSelectors && !core::has(pairWildcardRelations, rel)) {
569 pairWildcardRelations.push_back(rel);
570 add_create_query_handles(Pair(rel, All), handles);
571 }
572 }
573
574 if (hasAnyPair && needsAnyPairWildcardSelectors)
575 add_create_query_handles(Pair(All, All), handles);
576
577 for (const auto& candidate: handles) {
578 auto* pInfo = try_get(candidate.handle);
579 if (pInfo == nullptr || pInfo->refs() == 0)
580 continue;
581
582 if (!pInfo->register_archetype(*pArchetype, candidate.matchedSelector, true))
583 continue;
584
585 register_query_archetype(candidate.handle, pArchetype, pInfo->result_cache_rev());
586 }
587 }
588
589 private:
590#if GAIA_ECS_TEST_HOOKS
595 GAIA_NODISCARD static bool
596 archetype_span_contains(std::span<const Archetype*> archetypes, const Archetype* pArchetype) {
597 for (const auto* pCachedArchetype: archetypes) {
598 if (pCachedArchetype == pArchetype)
599 return true;
600 }
601 return false;
602 }
603
608 GAIA_NODISCARD static bool tracked_archetypes_contain(
609 const cnt::darray<const Archetype*>& archetypes, const ArchetypeIdLookupKey& archetypeKey) {
610 for (const auto* pArchetype: archetypes) {
611 if (pArchetype == nullptr)
612 return false;
613
614 if (ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash()) == archetypeKey)
615 return true;
616 }
617 return false;
618 }
619#endif
620
622 static CreateSelectorKind classify_create_selector(Entity entity) {
623 if (!entity.pair())
624 return CreateSelectorKind::Other;
625 if (is_wildcard(entity.id()))
626 return is_wildcard(entity.gen()) ? CreateSelectorKind::AnyPairWildcard : CreateSelectorKind::TgtWildcardPair;
627 if (is_wildcard(entity.gen()))
628 return CreateSelectorKind::RelWildcardPair;
629 return CreateSelectorKind::ExactPair;
630 }
631
635 GAIA_NODISCARD static constexpr uint32_t selector_kind_idx(CreateSelectorKind kind) {
636 return (uint32_t)kind;
637 }
638
642 GAIA_NODISCARD bool has_create_selector_kind(CreateSelectorKind kind) const {
643 return m_createQuerySelectorCnt[selector_kind_idx(kind)] != 0;
644 }
645
647 void track_create_selector(Entity entity) {
648 const auto kind = classify_create_selector(entity);
649 ++m_createQuerySelectorCnt[selector_kind_idx(kind)];
650 }
651
653 void untrack_create_selector(Entity entity) {
654 const auto kind = classify_create_selector(entity);
655 auto& cnt = m_createQuerySelectorCnt[selector_kind_idx(kind)];
656 GAIA_ASSERT(cnt != 0);
657 --cnt;
658 }
659
664 static QueryInfo::InvalidationKind select_invalidation_kind(const QueryInfo& info, ChangeKind changeKind) {
665 switch (changeKind) {
666 case ChangeKind::DynamicResult:
667 return QueryInfo::InvalidationKind::Result;
668 case ChangeKind::All:
669 return QueryInfo::InvalidationKind::All;
670 case ChangeKind::Structural:
671 // Structural changes invalidate seed caches for structural queries.
672 // Dynamic queries reuse structural compilation state and only need their
673 // final result refreshed on the next read.
674 return (info.ctx().data.deps.has_dep_flag(QueryCtx::DependencyHasSourceTerms) ||
675 info.ctx().data.deps.has_dep_flag(QueryCtx::DependencyHasVariableTerms))
676 ? QueryInfo::InvalidationKind::Result
677 : QueryInfo::InvalidationKind::Seed;
678 }
679
680 GAIA_ASSERT(false);
681 return QueryInfo::InvalidationKind::All;
682 }
683
687 void add_entity_query_pair(Entity entity, QueryHandle handle) {
688 EntityLookupKey entityKey(entity);
689 const auto it = m_entityToQuery.find(entityKey);
690 if (it == m_entityToQuery.end()) {
691 m_entityToQuery.try_emplace(entityKey, cnt::darray<QueryHandle>{handle});
692 return;
693 }
694
695 auto& handles = it->second;
696 if (!core::has(handles, handle))
697 handles.push_back(handle);
698 }
699
703 void del_entity_query_pair(Entity entity, QueryHandle handle) {
704 auto it = m_entityToQuery.find(EntityLookupKey(entity));
705 if (it == m_entityToQuery.end())
706 return;
707
708 auto& handles = it->second;
709 const auto idx = core::get_index_unsafe(handles, handle);
710 core::swap_erase_unsafe(handles, idx);
711
712 // Remove the mapping if there are no more matches
713 if (handles.empty())
714 m_entityToQuery.erase(it);
715 }
716
720 void add_entity_to_query_pairs(EntitySpan entities, QueryHandle handle) {
721 for (auto entity: entities) {
722 add_entity_query_pair(entity, handle);
723 }
724 }
725
729 void del_entity_to_query_pairs(EntitySpan entities, QueryHandle handle) {
730 for (auto entity: entities) {
731 del_entity_query_pair(entity, handle);
732 }
733 }
734
738 void add_create_to_query_pair(Entity entity, QueryHandle handle) {
739 EntityLookupKey entityKey(entity);
740 const auto it = m_entityToCreateQuery.find(entityKey);
741 if (it == m_entityToCreateQuery.end()) {
742 m_entityToCreateQuery.try_emplace(entityKey, cnt::darray<QueryHandle>{handle});
743 track_create_selector(entity);
744 return;
745 }
746
747 auto& handles = it->second;
748 if (!core::has(handles, handle)) {
749 handles.push_back(handle);
750 track_create_selector(entity);
751 }
752 }
753
757 void add_sort_to_query_pair(Entity entity, QueryHandle handle) {
758 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
759 if (it == m_sortEntityToQuery.end()) {
760 m_sortEntityToQuery.try_emplace(EntityLookupKey(entity), cnt::darray<QueryHandle>{handle});
761 return;
762 }
763
764 auto& handles = it->second;
765 if (!core::has(handles, handle))
766 handles.push_back(handle);
767 }
768
772 void del_sort_to_query_pair(Entity entity, QueryHandle handle) {
773 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
774 if (it == m_sortEntityToQuery.end())
775 return;
776
777 auto& handles = it->second;
778 core::swap_erase(handles, core::get_index(handles, handle));
779 if (handles.empty())
780 m_sortEntityToQuery.erase(it);
781 }
782
786 void add_sort_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
787 if (ctx.data.sortByFunc == nullptr || ctx.data.sortBy == EntityBad)
788 return;
789
790 add_sort_to_query_pair(ctx.data.sortBy, handle);
791 }
792
796 void del_sort_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
797 if (ctx.data.sortByFunc == nullptr || ctx.data.sortBy == EntityBad)
798 return;
799
800 del_sort_to_query_pair(ctx.data.sortBy, handle);
801 }
802
806 void add_sorted_query(const QueryCtx& ctx, QueryHandle handle) {
807 if (ctx.data.sortByFunc == nullptr)
808 return;
809
810 m_sortedQueries.push_back(handle);
811 }
812
816 void del_sorted_query(const QueryCtx& ctx, QueryHandle handle) {
817 if (ctx.data.sortByFunc == nullptr)
818 return;
819
820 const auto idx = core::get_index(m_sortedQueries, handle);
821 GAIA_ASSERT(idx != BadIndex);
822 if (idx != BadIndex)
823 core::swap_erase(m_sortedQueries, idx);
824 }
825
829 void del_create_to_query_pair(Entity entity, QueryHandle handle) {
830 auto it = m_entityToCreateQuery.find(EntityLookupKey(entity));
831 if (it == m_entityToCreateQuery.end())
832 return;
833
834 auto& handles = it->second;
835 core::swap_erase(handles, core::get_index(handles, handle));
836 untrack_create_selector(entity);
837 if (handles.empty())
838 m_entityToCreateQuery.erase(it);
839 }
840
844 void add_create_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
845 if (ctx.data.cachePolicy != QueryCtx::CachePolicy::Immediate)
846 return;
847
848 // Only structural queries with positive selector dependencies are tracked here.
849 // Dependency metadata is refreshed together with cache policy classification so
850 // create-time propagation can consume it without re-deriving query shape here.
851 for (const auto entity: ctx.data.deps.create_selectors_view())
852 add_create_to_query_pair(entity, handle);
853 }
854
858 void del_create_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
859 if (ctx.data.cachePolicy != QueryCtx::CachePolicy::Immediate)
860 return;
861
862 for (const auto entity: ctx.data.deps.create_selectors_view())
863 del_create_to_query_pair(entity, handle);
864 }
865
869 void add_create_query_handles(Entity selector, cnt::darray<CreateQueryCandidate>& handles) {
870 const auto it = m_entityToCreateQuery.find(EntityLookupKey(selector));
871 if (it == m_entityToCreateQuery.end())
872 return;
873
874 for (const auto handle: it->second) {
875 if (mark_create_query_handle(handle))
876 handles.push_back(CreateQueryCandidate{handle, selector});
877 }
878 }
879
882 GAIA_NODISCARD cnt::darray<CreateQueryCandidate>& prepare_create_query_handles() {
883 m_createQueryHandleScratch.clear();
884
885 // Archetype creation can fan out through many positive selector ids. Use a monotonic stamp table
886 // keyed by query-handle id so duplicate candidates do not devolve into repeated linear scans.
887 ++m_createQueryHandleStamp;
888 if (m_createQueryHandleStamp == 0) {
889 m_createQueryHandleStampById = {};
890 m_createQueryHandleStamp = 1;
891 }
892
893 return m_createQueryHandleScratch;
894 }
895
899 GAIA_NODISCARD bool mark_create_query_handle(QueryHandle handle) {
900 const auto handleId = (uint32_t)handle.id();
901 if (handleId >= m_createQueryHandleStampById.size())
902 m_createQueryHandleStampById.resize(handleId + 1);
903
904 auto& stamp = m_createQueryHandleStampById[handleId];
905 if (stamp == m_createQueryHandleStamp)
906 return false;
907
908 stamp = m_createQueryHandleStamp;
909 return true;
910 }
911
915 void add_archetype_query_pair(const Archetype* pArchetype, QueryHandle handle) {
916 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
917 const auto it = m_archetypeToQuery.find(archetypeKey);
918 if (it == m_archetypeToQuery.end()) {
919 m_archetypeToQuery.try_emplace(archetypeKey, cnt::darray<QueryHandle>{handle});
920 return;
921 }
922
923 auto& handles = it->second;
924 // Callers only register a <query, archetype> edge after they proved that edge is new.
925 GAIA_ASSERT(!core::has(handles, handle));
926 handles.push_back(handle);
927 }
928
932 void del_archetype_query_pair(const Archetype* pArchetype, QueryHandle handle) {
933 auto it = m_archetypeToQuery.find(ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash()));
934 if (it == m_archetypeToQuery.end())
935 return;
936
937 auto& handles = it->second;
938 const auto idx = core::get_index(handles, handle);
939 GAIA_ASSERT(idx != BadIndex);
940 core::swap_erase(handles, idx);
941 if (handles.empty())
942 m_archetypeToQuery.erase(it);
943 }
944
947 void unregister_query_archetypes(QueryHandle handle) {
948 auto it = m_queryToArchetype.find(QueryHandleLookupKey(handle));
949 if (it == m_queryToArchetype.end())
950 return;
951
952 const auto& tracked = it->second.archetypes;
953 for (const auto* pArchetype: tracked)
954 del_archetype_query_pair(pArchetype, handle);
955
956 m_queryToArchetype.erase(it);
957 }
958
963 void register_query_archetype(QueryHandle handle, const Archetype* pArchetype, uint32_t syncedRevision) {
964 auto [trackedIt, inserted] = m_queryToArchetype.try_emplace(QueryHandleLookupKey(handle));
965 auto& tracked = trackedIt->second.archetypes;
966
967 // Newly-created archetypes and sync_archetype_cache() both route through a deduplicated edge set,
968 // so reverse-index registration can append directly instead of re-scanning tracked archetypes.
969 GAIA_ASSERT(inserted || !core::has(tracked, pArchetype));
970 tracked.push_back(pArchetype);
971 trackedIt->second.syncedRevision = syncedRevision;
972 add_archetype_query_pair(pArchetype, handle);
973 }
974
978 void add_rel_query_pair(Entity relation, QueryHandle handle) {
979 const auto key = EntityLookupKey(relation);
980 const auto it = m_relationToQuery.find(key);
981 if (it == m_relationToQuery.end()) {
982 m_relationToQuery.try_emplace(key, cnt::darray<QueryHandle>{handle});
983 return;
984 }
985
986 auto& handles = it->second;
987 if (!core::has(handles, handle))
988 handles.push_back(handle);
989 }
990
994 void del_rel_query_pair(Entity relation, QueryHandle handle) {
995 auto it = m_relationToQuery.find(EntityLookupKey(relation));
996 if (it == m_relationToQuery.end())
997 return;
998
999 auto& handles = it->second;
1000 core::swap_erase(handles, core::get_index(handles, handle));
1001 if (handles.empty())
1002 m_relationToQuery.erase(it);
1003 }
1004
1008 void add_rel_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
1009 for (const auto relation: ctx.data.deps.relations_view())
1010 add_rel_query_pair(relation, handle);
1011 }
1012
1016 void del_rel_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
1017 for (const auto relation: ctx.data.deps.relations_view())
1018 del_rel_query_pair(relation, handle);
1019 }
1020 };
1021 } // namespace ecs
1022} // namespace gaia
static page_storage_id get(const T &item) noexcept
Applies the default conversion for an item.
Definition paged_storage.h:47