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;
124 uint64_t m_epoch = 0;
125
127 cnt::map<EntityLookupKey, cnt::darray<QueryHandle>> m_entityToQuery;
129 cnt::map<EntityLookupKey, cnt::darray<QueryHandle>> m_relationToQuery;
131 cnt::map<EntityLookupKey, cnt::darray<QueryHandle>> m_sortEntityToQuery;
133 cnt::darray<QueryHandle> m_sortedQueries;
135 cnt::map<EntityLookupKey, cnt::darray<QueryHandle>> m_entityToCreateQuery;
137 cnt::map<ArchetypeIdLookupKey, cnt::darray<QueryHandle>> m_archetypeToQuery;
139 cnt::map<QueryHandleLookupKey, TrackedArchetypes> m_queryToArchetype;
141 cnt::darray<CreateQueryCandidate> m_createQueryHandleScratch;
143 cnt::darray<uint32_t> m_createQueryHandleStampById;
144 uint32_t m_createQueryHandleStamp = 1;
146 uint32_t m_createQuerySelectorCnt[(size_t)CreateSelectorKind::Count] = {};
147
148 public:
149 QueryCache() {
150 m_queryArr.reserve(256);
151 }
152
153 ~QueryCache() = default;
154
155 QueryCache(QueryCache&&) = delete;
156 QueryCache(const QueryCache&) = delete;
157 QueryCache& operator=(QueryCache&&) = delete;
158 QueryCache& operator=(const QueryCache&) = delete;
159
163 GAIA_NODISCARD bool valid(QueryHandle handle) const {
164 if (handle.id() == QueryIdBad)
165 return false;
166
167 if (!m_queryArr.has(handle.id()))
168 return false;
169
170 const auto& h = m_queryArr[handle.id()];
171 return h.idx == handle.id() && h.gen == handle.gen();
172 }
173
175 void clear() {
176 ++m_epoch;
177 m_pCache.clear();
178 m_queryArr.clear();
179 m_entityToQuery.clear();
180 m_relationToQuery.clear();
181 m_sortEntityToQuery.clear();
182 m_sortedQueries.clear();
183 m_entityToCreateQuery.clear();
184 m_archetypeToQuery.clear();
185 m_queryToArchetype.clear();
186 m_createQueryHandleScratch.clear();
187 m_createQueryHandleStampById.clear();
188 m_createQueryHandleStamp = 1;
189 for (auto& cnt: m_createQuerySelectorCnt)
190 cnt = 0;
191 }
192
195 GAIA_NODISCARD uint64_t epoch() const {
196 return m_epoch;
197 }
198
201 void clear_archetype_tracking() {
202 m_archetypeToQuery.clear();
203 m_queryToArchetype.clear();
204 }
205
209 QueryInfo* try_get(QueryHandle handle) {
210 if (!valid(handle))
211 return nullptr;
212
213 auto& info = m_queryArr[handle.id()];
214 GAIA_ASSERT(info.idx == handle.id());
215 GAIA_ASSERT(info.gen == handle.gen());
216 return &info;
217 }
218
222 const QueryInfo* try_get(QueryHandle handle) const {
223 if (!valid(handle))
224 return nullptr;
225
226 const auto& info = m_queryArr[handle.id()];
227 GAIA_ASSERT(info.idx == handle.id());
228 GAIA_ASSERT(info.gen == handle.gen());
229 return &info;
230 }
231
232#if GAIA_ECS_TEST_HOOKS
234 GAIA_NODISCARD bool verify_archetype_tracking() const {
235 for (const auto& pair: m_queryToArchetype) {
236 const auto handle = pair.first.handle();
237 const auto* pInfo = try_get(handle);
238 if (pInfo == nullptr || pInfo->refs() == 0)
239 return false;
240
241 const auto& tracked = pair.second.archetypes;
242 for (uint32_t i = 0; i < tracked.size(); ++i) {
243 const auto* pArchetype = tracked[i];
244 if (pArchetype == nullptr)
245 return false;
246
247 for (uint32_t j = i + 1; j < tracked.size(); ++j) {
248 if (tracked[j] == pArchetype)
249 return false;
250 }
251
252 if (!archetype_span_contains(pInfo->cache_archetype_view(), pArchetype))
253 return false;
254
255 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
256 const auto reverseIt = m_archetypeToQuery.find(archetypeKey);
257 if (reverseIt == m_archetypeToQuery.end() || !core::has(reverseIt->second, handle))
258 return false;
259 }
260 }
261
262 for (const auto& pair: m_archetypeToQuery) {
263 const auto& handles = pair.second;
264 for (uint32_t i = 0; i < handles.size(); ++i) {
265 const auto handle = handles[i];
266 const auto* pInfo = try_get(handle);
267 if (pInfo == nullptr || pInfo->refs() == 0)
268 return false;
269
270 for (uint32_t j = i + 1; j < handles.size(); ++j) {
271 if (handles[j] == handle)
272 return false;
273 }
274
275 const auto trackedIt = m_queryToArchetype.find(QueryHandleLookupKey(handle));
276 if (trackedIt == m_queryToArchetype.end())
277 return false;
278
279 if (!tracked_archetypes_contain(trackedIt->second.archetypes, pair.first))
280 return false;
281 }
282 }
283
284 return true;
285 }
286
289 GAIA_NODISCARD uint32_t test_query_count() const {
290 return (uint32_t)m_queryArr.item_count();
291 }
292#endif
293
297 QueryInfo& get(QueryHandle handle) {
298 GAIA_ASSERT(valid(handle));
299
300 auto& info = m_queryArr[handle.id()];
301 GAIA_ASSERT(info.idx == handle.id());
302 GAIA_ASSERT(info.gen == handle.gen());
303 return info;
304 }
305
311 QueryInfo&
312 add(QueryCtx&& ctx, //
313 const EntityToArchetypeMap& entityToArchetypeMap, //
314 std::span<const Archetype*> allArchetypes) {
315 GAIA_ASSERT(ctx.hashLookup.hash != 0);
316
317 // First check if the query cache record exists
318 auto ret = m_pCache.try_emplace(QueryLookupKey(ctx.hashLookup, &ctx), nullptr);
319 if (!ret.second) {
320 auto* pInfo = ret.first->second;
321 GAIA_ASSERT(pInfo != nullptr);
322 pInfo->add_ref();
323 return *pInfo;
324 }
325
326 // No record exists, let us create a new one
327 QueryInfoCreationCtx creationCtx{};
328 creationCtx.pQueryCtx = &ctx;
329 creationCtx.pEntityToArchetypeMap = &entityToArchetypeMap;
330 creationCtx.allArchetypes = allArchetypes;
331 auto handle = m_queryArr.alloc(&creationCtx);
332
333 // We are moving the rvalue to "ctx". As a result, the pointer stored in m_pCache.emplace above is no longer
334 // going to be valid. Therefore we swap the map key with a one with a valid pointer.
335 auto& info = get(handle);
336 info.add_ref();
337 ret.first->second = &info;
338 auto new_p = robin_hood::pair(std::make_pair(QueryLookupKey(ctx.hashLookup, &info.ctx()), &info));
339 ret.first->swap(new_p);
340
341 return register_query_info(handle, info);
342 }
343
349 QueryInfo& add_local(
350 QueryCtx&& ctx, //
351 const EntityToArchetypeMap& entityToArchetypeMap, //
352 std::span<const Archetype*> allArchetypes) {
353 QueryInfoCreationCtx creationCtx{};
354 creationCtx.pQueryCtx = &ctx;
355 creationCtx.pEntityToArchetypeMap = &entityToArchetypeMap;
356 creationCtx.allArchetypes = allArchetypes;
357 auto handle = m_queryArr.alloc(&creationCtx);
358
359 auto& info = get(handle);
360 info.add_ref();
361 return register_query_info(handle, info);
362 }
363
367 bool del(QueryHandle handle) {
368 auto* pInfo = try_get(handle);
369 if (pInfo == nullptr)
370 return false;
371
372 pInfo->dec_ref();
373 if (pInfo->refs() != 0)
374 return false;
375
376 unregister_query_archetypes(handle);
377
378 // If this was the last reference to the query, we can safely remove it
379 auto it = m_pCache.find(QueryLookupKey(pInfo->ctx().hashLookup, &pInfo->ctx()));
380 if (it != m_pCache.end())
381 m_pCache.erase(it);
382
383 // Remove the entity->query pair
384 del_entity_to_query_pairs(pInfo->ctx().data.ids_view(), handle);
385 del_rel_to_query_pairs(pInfo->ctx(), handle);
386 del_sort_to_query_pairs(pInfo->ctx(), handle);
387 del_sorted_query(pInfo->ctx(), handle);
388 del_create_to_query_pairs(pInfo->ctx(), handle);
389 m_queryArr.free(handle);
390
391 return true;
392 }
393
395 auto begin() {
396 return m_queryArr.begin();
397 }
398
400 auto end() {
401 return m_queryArr.end();
402 }
403
406 GAIA_NODISCARD bool has_relation_query_dependencies() const {
407 return !m_relationToQuery.empty();
408 }
409
417 void invalidate_queries_for_entity(EntityLookupKey entityKey, ChangeKind changeKind) {
418 auto it = m_entityToQuery.find(entityKey);
419 if (it == m_entityToQuery.end())
420 return;
421
422 const auto& handles = it->second;
423 for (const auto& handle: handles) {
424 auto& info = get(handle);
425 // World mutations invalidate cached results, but they do not change query shape.
426 // Recomputing QueryCtx metadata here only adds overhead to the invalidation path.
427 info.invalidate(select_invalidation_kind(info, changeKind));
428 }
429 }
430
434 void invalidate_queries_for_rel(Entity relation, ChangeKind changeKind) {
435 auto it = m_relationToQuery.find(EntityLookupKey(relation));
436 if (it == m_relationToQuery.end())
437 return;
438
439 for (const auto handle: it->second) {
440 auto& info = get(handle);
441 // Relation changes affect dynamic freshness, not the query definition itself.
442 info.invalidate(select_invalidation_kind(info, changeKind));
443 }
444 }
445
448 GAIA_NODISCARD bool has_sorted_queries() const {
449 return !m_sortedQueries.empty();
450 }
451
455 GAIA_NODISCARD bool has_sorted_queries_for_entity(Entity entity) const {
456 const auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
457 return it != m_sortEntityToQuery.end() && !it->second.empty();
458 }
459
462 void invalidate_sorted_queries_for_entity(Entity entity) {
463 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
464 if (it == m_sortEntityToQuery.end())
465 return;
466
467 for (const auto handle: it->second) {
468 auto* pInfo = try_get(handle);
469 if (pInfo == nullptr || pInfo->refs() == 0)
470 continue;
471
472 pInfo->invalidate_sort();
473 }
474 }
475
477 void invalidate_sorted_queries() {
478 for (const auto handle: m_sortedQueries) {
479 auto* pInfo = try_get(handle);
480 if (pInfo == nullptr || pInfo->refs() == 0)
481 continue;
482
483 pInfo->invalidate_sort();
484 }
485 }
486
489 void sync_archetype_cache(QueryInfo& queryInfo) {
490 const auto handle = QueryInfo::handle(queryInfo);
491 if (!valid(handle))
492 return;
493
494 const auto archetypes = queryInfo.cache_archetype_view();
495 const auto key = QueryHandleLookupKey(handle);
496 auto it = m_queryToArchetype.find(key);
497 if (it != m_queryToArchetype.end() && it->second.syncedRevision == queryInfo.result_cache_rev())
498 return;
499
500 unregister_query_archetypes(handle);
501
502 if (archetypes.empty())
503 return;
504
505 auto [trackedIt, inserted] = m_queryToArchetype.try_emplace(key);
506 auto& tracked = trackedIt->second.archetypes;
507 if (!inserted)
508 tracked.clear();
509
510 tracked.reserve((uint32_t)archetypes.size());
511 for (const auto* pArchetype: archetypes) {
512 tracked.push_back(pArchetype);
513 add_archetype_query_pair(pArchetype, handle);
514 }
515 trackedIt->second.syncedRevision = queryInfo.result_cache_rev();
516 }
517
520 void remove_archetype_from_queries(Archetype* pArchetype) {
521 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
522 auto it = m_archetypeToQuery.find(archetypeKey);
523 if (it == m_archetypeToQuery.end())
524 return;
525
526 const auto handles = it->second;
527 for (const auto handle: handles) {
528 auto* pInfo = try_get(handle);
529 if (pInfo != nullptr && pInfo->refs() != 0)
530 pInfo->remove(pArchetype);
531
532 auto trackedIt = m_queryToArchetype.find(QueryHandleLookupKey(handle));
533 if (trackedIt == m_queryToArchetype.end())
534 continue;
535
536 auto& tracked = trackedIt->second.archetypes;
537 core::swap_erase(tracked, core::get_index(tracked, pArchetype));
538 if (tracked.empty())
539 m_queryToArchetype.erase(trackedIt);
540 }
541
542 m_archetypeToQuery.erase(it);
543 }
544
547 void register_archetype_with_queries(const Archetype* pArchetype) {
548 if (m_entityToCreateQuery.empty()) {
549 (void)pArchetype;
550 return;
551 }
552
553 auto& handles = prepare_create_query_handles();
554 const bool needsExactPairSelectors = has_create_selector_kind(CreateSelectorKind::ExactPair);
555 const bool needsRelWildcardSelectors = has_create_selector_kind(CreateSelectorKind::RelWildcardPair);
556 const bool needsTgtWildcardSelectors = has_create_selector_kind(CreateSelectorKind::TgtWildcardPair);
557 const bool needsAnyPairWildcardSelectors = has_create_selector_kind(CreateSelectorKind::AnyPairWildcard);
558 bool hasAnyPair = false;
559 cnt::darray_ext<Entity, 16> pairWildcardRelations;
560 for (const auto entity: pArchetype->ids_view()) {
561 if (!entity.pair()) {
562 add_create_query_handles(entity, handles);
563 continue;
564 }
565
566 hasAnyPair = true;
567 if (needsExactPairSelectors)
568 add_create_query_handles(entity, handles);
569
570 // Pair ids retain the relation and target ids. That is enough to
571 // rebuild wildcard pair lookup keys without touching the world record storage.
572 const auto rel = Entity((EntityId)entity.id(), 0, false, false);
573 const auto tgt = Entity((EntityId)entity.gen(), 0, false, false);
574 if (needsTgtWildcardSelectors)
575 add_create_query_handles(Pair(All, tgt), handles);
576 if (needsRelWildcardSelectors && !core::has(pairWildcardRelations, rel)) {
577 pairWildcardRelations.push_back(rel);
578 add_create_query_handles(Pair(rel, All), handles);
579 }
580 }
581
582 if (hasAnyPair && needsAnyPairWildcardSelectors)
583 add_create_query_handles(Pair(All, All), handles);
584
585 for (const auto& candidate: handles) {
586 auto* pInfo = try_get(candidate.handle);
587 if (pInfo == nullptr || pInfo->refs() == 0)
588 continue;
589
590 if (!pInfo->register_archetype(*pArchetype, candidate.matchedSelector, true))
591 continue;
592
593 register_query_archetype(candidate.handle, pArchetype, pInfo->result_cache_rev());
594 }
595 }
596
597 private:
598#if GAIA_ECS_TEST_HOOKS
603 GAIA_NODISCARD static bool
604 archetype_span_contains(std::span<const Archetype*> archetypes, const Archetype* pArchetype) {
605 for (const auto* pCachedArchetype: archetypes) {
606 if (pCachedArchetype == pArchetype)
607 return true;
608 }
609 return false;
610 }
611
616 GAIA_NODISCARD static bool tracked_archetypes_contain(
617 const cnt::darray<const Archetype*>& archetypes, const ArchetypeIdLookupKey& archetypeKey) {
618 for (const auto* pArchetype: archetypes) {
619 if (pArchetype == nullptr)
620 return false;
621
622 if (ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash()) == archetypeKey)
623 return true;
624 }
625 return false;
626 }
627#endif
628
630 static CreateSelectorKind classify_create_selector(Entity entity) {
631 if (!entity.pair())
632 return CreateSelectorKind::Other;
633 if (is_wildcard(entity.id()))
634 return is_wildcard(entity.gen()) ? CreateSelectorKind::AnyPairWildcard : CreateSelectorKind::TgtWildcardPair;
635 if (is_wildcard(entity.gen()))
636 return CreateSelectorKind::RelWildcardPair;
637 return CreateSelectorKind::ExactPair;
638 }
639
643 GAIA_NODISCARD static constexpr uint32_t selector_kind_idx(CreateSelectorKind kind) {
644 return (uint32_t)kind;
645 }
646
650 GAIA_NODISCARD bool has_create_selector_kind(CreateSelectorKind kind) const {
651 return m_createQuerySelectorCnt[selector_kind_idx(kind)] != 0;
652 }
653
655 void track_create_selector(Entity entity) {
656 const auto kind = classify_create_selector(entity);
657 ++m_createQuerySelectorCnt[selector_kind_idx(kind)];
658 }
659
661 void untrack_create_selector(Entity entity) {
662 const auto kind = classify_create_selector(entity);
663 auto& cnt = m_createQuerySelectorCnt[selector_kind_idx(kind)];
664 GAIA_ASSERT(cnt != 0);
665 --cnt;
666 }
667
672 static QueryInfo::InvalidationKind select_invalidation_kind(const QueryInfo& info, ChangeKind changeKind) {
673 switch (changeKind) {
674 case ChangeKind::DynamicResult:
675 return QueryInfo::InvalidationKind::Result;
676 case ChangeKind::All:
677 return QueryInfo::InvalidationKind::All;
678 case ChangeKind::Structural:
679 // Structural changes invalidate seed caches for structural queries.
680 // Dynamic queries reuse structural compilation state and only need their
681 // final result refreshed on the next read.
682 return (info.ctx().data.deps.has_dep_flag(QueryCtx::DependencyHasSourceTerms) ||
683 info.ctx().data.deps.has_dep_flag(QueryCtx::DependencyHasVariableTerms))
684 ? QueryInfo::InvalidationKind::Result
685 : QueryInfo::InvalidationKind::Seed;
686 }
687
688 GAIA_ASSERT(false);
689 return QueryInfo::InvalidationKind::All;
690 }
691
695 void add_entity_query_pair(Entity entity, QueryHandle handle) {
696 EntityLookupKey entityKey(entity);
697 const auto it = m_entityToQuery.find(entityKey);
698 if (it == m_entityToQuery.end()) {
699 m_entityToQuery.try_emplace(entityKey, cnt::darray<QueryHandle>{handle});
700 return;
701 }
702
703 auto& handles = it->second;
704 if (!core::has(handles, handle))
705 handles.push_back(handle);
706 }
707
711 void del_entity_query_pair(Entity entity, QueryHandle handle) {
712 auto it = m_entityToQuery.find(EntityLookupKey(entity));
713 if (it == m_entityToQuery.end())
714 return;
715
716 auto& handles = it->second;
717 const auto idx = core::get_index_unsafe(handles, handle);
718 core::swap_erase_unsafe(handles, idx);
719
720 // Remove the mapping if there are no more matches
721 if (handles.empty())
722 m_entityToQuery.erase(it);
723 }
724
728 void add_entity_to_query_pairs(EntitySpan entities, QueryHandle handle) {
729 for (auto entity: entities) {
730 add_entity_query_pair(entity, handle);
731 }
732 }
733
737 void del_entity_to_query_pairs(EntitySpan entities, QueryHandle handle) {
738 for (auto entity: entities) {
739 del_entity_query_pair(entity, handle);
740 }
741 }
742
746 void add_create_to_query_pair(Entity entity, QueryHandle handle) {
747 EntityLookupKey entityKey(entity);
748 const auto it = m_entityToCreateQuery.find(entityKey);
749 if (it == m_entityToCreateQuery.end()) {
750 m_entityToCreateQuery.try_emplace(entityKey, cnt::darray<QueryHandle>{handle});
751 track_create_selector(entity);
752 return;
753 }
754
755 auto& handles = it->second;
756 if (!core::has(handles, handle)) {
757 handles.push_back(handle);
758 track_create_selector(entity);
759 }
760 }
761
765 void add_sort_to_query_pair(Entity entity, QueryHandle handle) {
766 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
767 if (it == m_sortEntityToQuery.end()) {
768 m_sortEntityToQuery.try_emplace(EntityLookupKey(entity), cnt::darray<QueryHandle>{handle});
769 return;
770 }
771
772 auto& handles = it->second;
773 if (!core::has(handles, handle))
774 handles.push_back(handle);
775 }
776
780 void del_sort_to_query_pair(Entity entity, QueryHandle handle) {
781 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
782 if (it == m_sortEntityToQuery.end())
783 return;
784
785 auto& handles = it->second;
786 core::swap_erase(handles, core::get_index(handles, handle));
787 if (handles.empty())
788 m_sortEntityToQuery.erase(it);
789 }
790
794 void add_sort_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
795 if (ctx.data.sortByFunc == nullptr || ctx.data.sortBy == EntityBad)
796 return;
797
798 add_sort_to_query_pair(ctx.data.sortBy, handle);
799 }
800
804 void del_sort_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
805 if (ctx.data.sortByFunc == nullptr || ctx.data.sortBy == EntityBad)
806 return;
807
808 del_sort_to_query_pair(ctx.data.sortBy, handle);
809 }
810
814 void add_sorted_query(const QueryCtx& ctx, QueryHandle handle) {
815 if (ctx.data.sortByFunc == nullptr)
816 return;
817
818 m_sortedQueries.push_back(handle);
819 }
820
824 void del_sorted_query(const QueryCtx& ctx, QueryHandle handle) {
825 if (ctx.data.sortByFunc == nullptr)
826 return;
827
828 const auto idx = core::get_index(m_sortedQueries, handle);
829 GAIA_ASSERT(idx != BadIndex);
830 if (idx != BadIndex)
831 core::swap_erase(m_sortedQueries, idx);
832 }
833
837 void del_create_to_query_pair(Entity entity, QueryHandle handle) {
838 auto it = m_entityToCreateQuery.find(EntityLookupKey(entity));
839 if (it == m_entityToCreateQuery.end())
840 return;
841
842 auto& handles = it->second;
843 core::swap_erase(handles, core::get_index(handles, handle));
844 untrack_create_selector(entity);
845 if (handles.empty())
846 m_entityToCreateQuery.erase(it);
847 }
848
852 void add_create_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
853 if (ctx.data.cachePolicy != QueryCtx::CachePolicy::Immediate)
854 return;
855
856 // Only structural queries with positive selector dependencies are tracked here.
857 // Dependency metadata is refreshed together with cache policy classification so
858 // create-time propagation can consume it without re-deriving query shape here.
859 for (const auto entity: ctx.data.deps.create_selectors_view())
860 add_create_to_query_pair(entity, handle);
861 }
862
866 void del_create_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
867 if (ctx.data.cachePolicy != QueryCtx::CachePolicy::Immediate)
868 return;
869
870 for (const auto entity: ctx.data.deps.create_selectors_view())
871 del_create_to_query_pair(entity, handle);
872 }
873
877 void add_create_query_handles(Entity selector, cnt::darray<CreateQueryCandidate>& handles) {
878 const auto it = m_entityToCreateQuery.find(EntityLookupKey(selector));
879 if (it == m_entityToCreateQuery.end())
880 return;
881
882 for (const auto handle: it->second) {
883 if (mark_create_query_handle(handle))
884 handles.push_back(CreateQueryCandidate{handle, selector});
885 }
886 }
887
890 GAIA_NODISCARD cnt::darray<CreateQueryCandidate>& prepare_create_query_handles() {
891 m_createQueryHandleScratch.clear();
892
893 // Archetype creation can fan out through many positive selector ids. Use a monotonic stamp table
894 // keyed by query-handle id so duplicate candidates do not devolve into repeated linear scans.
895 ++m_createQueryHandleStamp;
896 if (m_createQueryHandleStamp == 0) {
897 m_createQueryHandleStampById = {};
898 m_createQueryHandleStamp = 1;
899 }
900
901 return m_createQueryHandleScratch;
902 }
903
907 GAIA_NODISCARD bool mark_create_query_handle(QueryHandle handle) {
908 const auto handleId = (uint32_t)handle.id();
909 // darray::resize() leaves trivial types uninitialized. Zero-fill new slots explicitly,
910 // otherwise recycled heap memory may already hold the current stamp and the query gets skipped.
911 if (handleId >= m_createQueryHandleStampById.size())
912 m_createQueryHandleStampById.resize(handleId + 1, 0U, cnt::ResizePolicy::Scale);
913
914 auto& stamp = m_createQueryHandleStampById[handleId];
915 if (stamp == m_createQueryHandleStamp)
916 return false;
917
918 stamp = m_createQueryHandleStamp;
919 return true;
920 }
921
925 void add_archetype_query_pair(const Archetype* pArchetype, QueryHandle handle) {
926 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
927 const auto it = m_archetypeToQuery.find(archetypeKey);
928 if (it == m_archetypeToQuery.end()) {
929 m_archetypeToQuery.try_emplace(archetypeKey, cnt::darray<QueryHandle>{handle});
930 return;
931 }
932
933 auto& handles = it->second;
934 // Callers only register a <query, archetype> edge after they proved that edge is new.
935 GAIA_ASSERT(!core::has(handles, handle));
936 handles.push_back(handle);
937 }
938
942 void del_archetype_query_pair(const Archetype* pArchetype, QueryHandle handle) {
943 auto it = m_archetypeToQuery.find(ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash()));
944 if (it == m_archetypeToQuery.end())
945 return;
946
947 auto& handles = it->second;
948 const auto idx = core::get_index(handles, handle);
949 GAIA_ASSERT(idx != BadIndex);
950 core::swap_erase(handles, idx);
951 if (handles.empty())
952 m_archetypeToQuery.erase(it);
953 }
954
957 void unregister_query_archetypes(QueryHandle handle) {
958 auto it = m_queryToArchetype.find(QueryHandleLookupKey(handle));
959 if (it == m_queryToArchetype.end())
960 return;
961
962 const auto& tracked = it->second.archetypes;
963 for (const auto* pArchetype: tracked)
964 del_archetype_query_pair(pArchetype, handle);
965
966 m_queryToArchetype.erase(it);
967 }
968
973 void register_query_archetype(QueryHandle handle, const Archetype* pArchetype, uint32_t syncedRevision) {
974 auto [trackedIt, inserted] = m_queryToArchetype.try_emplace(QueryHandleLookupKey(handle));
975 auto& tracked = trackedIt->second.archetypes;
976
977 // Newly-created archetypes and sync_archetype_cache() both route through a deduplicated edge set,
978 // so reverse-index registration can append directly instead of re-scanning tracked archetypes.
979 GAIA_ASSERT(inserted || !core::has(tracked, pArchetype));
980 tracked.push_back(pArchetype);
981 trackedIt->second.syncedRevision = syncedRevision;
982 add_archetype_query_pair(pArchetype, handle);
983 }
984
988 void add_rel_query_pair(Entity relation, QueryHandle handle) {
989 const auto key = EntityLookupKey(relation);
990 const auto it = m_relationToQuery.find(key);
991 if (it == m_relationToQuery.end()) {
992 m_relationToQuery.try_emplace(key, cnt::darray<QueryHandle>{handle});
993 return;
994 }
995
996 auto& handles = it->second;
997 if (!core::has(handles, handle))
998 handles.push_back(handle);
999 }
1000
1004 void del_rel_query_pair(Entity relation, QueryHandle handle) {
1005 auto it = m_relationToQuery.find(EntityLookupKey(relation));
1006 if (it == m_relationToQuery.end())
1007 return;
1008
1009 auto& handles = it->second;
1010 core::swap_erase(handles, core::get_index(handles, handle));
1011 if (handles.empty())
1012 m_relationToQuery.erase(it);
1013 }
1014
1018 void add_rel_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
1019 for (const auto relation: ctx.data.deps.relations_view())
1020 add_rel_query_pair(relation, handle);
1021 }
1022
1026 void del_rel_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
1027 for (const auto relation: ctx.data.deps.relations_view())
1028 del_rel_query_pair(relation, handle);
1029 }
1030 };
1031 } // namespace ecs
1032} // namespace gaia
static page_storage_id get(const T &item) noexcept
Applies the default conversion for an item.
Definition paged_storage.h:47