Gaia-ECS v0.9.3
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
13namespace gaia {
14 namespace cnt {
15 template <>
16 struct to_page_storage_id<ecs::QueryInfo> {
17 static page_storage_id get(const ecs::QueryInfo& item) noexcept {
18 return item.idx;
19 }
20 };
21 } // namespace cnt
22
23 namespace ecs {
25 QueryLookupHash m_hash;
26 const QueryCtx* m_pCtx;
27
28 public:
29 static constexpr bool IsDirectHashKey = true;
30
31 QueryLookupKey(): m_hash({0}), m_pCtx(nullptr) {}
32 explicit QueryLookupKey(QueryLookupHash hash, const QueryCtx* pCtx): m_hash(hash), m_pCtx(pCtx) {}
33
34 size_t hash() const {
35 return (size_t)m_hash.hash;
36 }
37
38 bool operator==(const QueryLookupKey& other) const {
39 // Hash doesn't match we don't have a match.
40 // Hash collisions are expected to be very unlikely so optimize for this case.
41 if GAIA_LIKELY (m_hash != other.m_hash)
42 return false;
43
44 if (m_pCtx == other.m_pCtx)
45 return true;
46
47 const auto lhsReal = m_pCtx->q.handle.id() != QueryIdBad;
48 const auto rhsReal = other.m_pCtx->q.handle.id() != QueryIdBad;
49
50 // Two persisted cached-query keys with different stable context pointers cannot represent
51 // the same query.
52 if (lhsReal && rhsReal)
53 return false;
54
55 // At least one side is a temporary lookup key. Fall back to structural comparison so cached
56 // queries deduplicate correctly regardless of comparison direction.
57 return QueryCtx::equals_no_handle_assumption(*m_pCtx, *other.m_pCtx);
58 }
59 };
60
61 class QueryCache {
62 public:
63 enum class ChangeKind : uint8_t {
64 // Query membership may have changed due to structural world changes.
65 Structural,
66 // Only dynamic/source-driven results may have changed.
67 DynamicResult,
68 // Full query cache invalidation.
69 All,
70 };
71
72 private:
73 QueryInfo& register_query_info(QueryHandle handle, QueryInfo& info) {
74 // Add the entity->query pair
75 add_entity_to_query_pairs(info.ctx().data.ids_view(), handle);
76 add_rel_to_query_pairs(info.ctx(), handle);
77 add_sort_to_query_pairs(info.ctx(), handle);
78 add_sorted_query(info.ctx(), handle);
79 add_create_to_query_pairs(info.ctx(), handle);
80
81 return info;
82 }
83
86 struct CreateQueryCandidate {
87 QueryHandle handle;
88 Entity matchedSelector = EntityBad;
89 };
90
93 enum class CreateSelectorKind : uint8_t {
94 Other,
95 ExactPair,
96 RelWildcardPair,
97 TgtWildcardPair,
98 AnyPairWildcard,
99 Count
100 };
101
102 struct TrackedArchetypes {
104 uint32_t syncedRevision = 0;
105 };
106
110
118 cnt::darray<QueryHandle> m_sortedQueries;
126 cnt::darray<CreateQueryCandidate> m_createQueryHandleScratch;
128 cnt::darray<uint32_t> m_createQueryHandleStampById;
129 uint32_t m_createQueryHandleStamp = 1;
131 uint32_t m_createQuerySelectorCnt[(size_t)CreateSelectorKind::Count] = {};
132
133 public:
134 QueryCache() {
135 m_queryArr.reserve(256);
136 }
137
138 ~QueryCache() = default;
139
140 QueryCache(QueryCache&&) = delete;
141 QueryCache(const QueryCache&) = delete;
142 QueryCache& operator=(QueryCache&&) = delete;
143 QueryCache& operator=(const QueryCache&) = delete;
144
145 GAIA_NODISCARD bool valid(QueryHandle handle) const {
146 if (handle.id() == QueryIdBad)
147 return false;
148
149 if (!m_queryArr.has(handle.id()))
150 return false;
151
152 const auto& h = m_queryArr[handle.id()];
153 return h.idx == handle.id() && h.gen == handle.gen();
154 }
155
156 void clear() {
157 m_pCache.clear();
158 m_queryArr.clear();
159 m_entityToQuery.clear();
160 m_relationToQuery.clear();
161 m_sortEntityToQuery.clear();
162 m_sortedQueries.clear();
163 m_entityToCreateQuery.clear();
164 m_archetypeToQuery.clear();
165 m_queryToArchetype.clear();
166 m_createQueryHandleScratch.clear();
167 m_createQueryHandleStampById.clear();
168 m_createQueryHandleStamp = 1;
169 for (auto& cnt: m_createQuerySelectorCnt)
170 cnt = 0;
171 }
172
176 m_archetypeToQuery.clear();
177 m_queryToArchetype.clear();
178 }
179
184 if (!valid(handle))
185 return nullptr;
186
187 auto& info = m_queryArr[handle.id()];
188 GAIA_ASSERT(info.idx == handle.id());
189 GAIA_ASSERT(info.gen == handle.gen());
190 return &info;
191 }
192
196 const QueryInfo* try_get(QueryHandle handle) const {
197 if (!valid(handle))
198 return nullptr;
199
200 const auto& info = m_queryArr[handle.id()];
201 GAIA_ASSERT(info.idx == handle.id());
202 GAIA_ASSERT(info.gen == handle.gen());
203 return &info;
204 }
205
206#if GAIA_ECS_TEST_HOOKS
208 GAIA_NODISCARD bool verify_archetype_tracking() const {
209 for (const auto& pair: m_queryToArchetype) {
210 const auto handle = pair.first.handle();
211 const auto* pInfo = try_get(handle);
212 if (pInfo == nullptr || pInfo->refs() == 0)
213 return false;
214
215 const auto& tracked = pair.second.archetypes;
216 for (uint32_t i = 0; i < tracked.size(); ++i) {
217 const auto* pArchetype = tracked[i];
218 if (pArchetype == nullptr)
219 return false;
220
221 for (uint32_t j = i + 1; j < tracked.size(); ++j) {
222 if (tracked[j] == pArchetype)
223 return false;
224 }
225
226 if (!archetype_span_contains(pInfo->cache_archetype_view(), pArchetype))
227 return false;
228
229 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
230 const auto reverseIt = m_archetypeToQuery.find(archetypeKey);
231 if (reverseIt == m_archetypeToQuery.end() || !core::has(reverseIt->second, handle))
232 return false;
233 }
234 }
235
236 for (const auto& pair: m_archetypeToQuery) {
237 const auto& handles = pair.second;
238 for (uint32_t i = 0; i < handles.size(); ++i) {
239 const auto handle = handles[i];
240 const auto* pInfo = try_get(handle);
241 if (pInfo == nullptr || pInfo->refs() == 0)
242 return false;
243
244 for (uint32_t j = i + 1; j < handles.size(); ++j) {
245 if (handles[j] == handle)
246 return false;
247 }
248
249 const auto trackedIt = m_queryToArchetype.find(QueryHandleLookupKey(handle));
250 if (trackedIt == m_queryToArchetype.end())
251 return false;
252
253 if (!tracked_archetypes_contain(trackedIt->second.archetypes, pair.first))
254 return false;
255 }
256 }
257
258 return true;
259 }
260
263 GAIA_NODISCARD uint32_t test_query_count() const {
264 return (uint32_t)m_queryArr.item_count();
265 }
266#endif
267
272 GAIA_ASSERT(valid(handle));
273
274 auto& info = m_queryArr[handle.id()];
275 GAIA_ASSERT(info.idx == handle.id());
276 GAIA_ASSERT(info.gen == handle.gen());
277 return info;
278 }
279
285 QueryInfo&
286 add(QueryCtx&& ctx, //
287 const EntityToArchetypeMap& entityToArchetypeMap, //
288 std::span<const Archetype*> allArchetypes) {
289 GAIA_ASSERT(ctx.hashLookup.hash != 0);
290
291 // First check if the query cache record exists
292 auto ret = m_pCache.try_emplace(QueryLookupKey(ctx.hashLookup, &ctx), nullptr);
293 if (!ret.second) {
294 auto* pInfo = ret.first->second;
295 GAIA_ASSERT(pInfo != nullptr);
296 pInfo->add_ref();
297 return *pInfo;
298 }
299
300 // No record exists, let us create a new one
301 QueryInfoCreationCtx creationCtx{};
302 creationCtx.pQueryCtx = &ctx;
303 creationCtx.pEntityToArchetypeMap = &entityToArchetypeMap;
304 creationCtx.allArchetypes = allArchetypes;
305 auto handle = m_queryArr.alloc(&creationCtx);
306
307 // We are moving the rvalue to "ctx". As a result, the pointer stored in m_pCache.emplace above is no longer
308 // going to be valid. Therefore we swap the map key with a one with a valid pointer.
309 auto& info = get(handle);
310 info.add_ref();
311 ret.first->second = &info;
312 auto new_p = robin_hood::pair(std::make_pair(QueryLookupKey(ctx.hashLookup, &info.ctx()), &info));
313 ret.first->swap(new_p);
314
315 return register_query_info(handle, info);
316 }
317
320 QueryCtx&& ctx, //
321 const EntityToArchetypeMap& entityToArchetypeMap, //
322 std::span<const Archetype*> allArchetypes) {
323 QueryInfoCreationCtx creationCtx{};
324 creationCtx.pQueryCtx = &ctx;
325 creationCtx.pEntityToArchetypeMap = &entityToArchetypeMap;
326 creationCtx.allArchetypes = allArchetypes;
327 auto handle = m_queryArr.alloc(&creationCtx);
328
329 auto& info = get(handle);
330 info.add_ref();
331 return register_query_info(handle, info);
332 }
333
337 bool del(QueryHandle handle) {
338 auto* pInfo = try_get(handle);
339 if (pInfo == nullptr)
340 return false;
341
342 pInfo->dec_ref();
343 if (pInfo->refs() != 0)
344 return false;
345
346 unregister_query_archetypes(handle);
347
348 // If this was the last reference to the query, we can safely remove it
349 auto it = m_pCache.find(QueryLookupKey(pInfo->ctx().hashLookup, &pInfo->ctx()));
350 if (it != m_pCache.end())
351 m_pCache.erase(it);
352
353 // Remove the entity->query pair
354 del_entity_to_query_pairs(pInfo->ctx().data.ids_view(), handle);
355 del_rel_to_query_pairs(pInfo->ctx(), handle);
356 del_sort_to_query_pairs(pInfo->ctx(), handle);
357 del_sorted_query(pInfo->ctx(), handle);
358 del_create_to_query_pairs(pInfo->ctx(), handle);
359 m_queryArr.free(handle);
360
361 return true;
362 }
363
364 auto begin() {
365 return m_queryArr.begin();
366 }
367
368 auto end() {
369 return m_queryArr.end();
370 }
371
379 void invalidate_queries_for_entity(EntityLookupKey entityKey, ChangeKind changeKind) {
380 auto it = m_entityToQuery.find(entityKey);
381 if (it == m_entityToQuery.end())
382 return;
383
384 const auto& handles = it->second;
385 for (const auto& handle: handles) {
386 auto& info = get(handle);
387 // World mutations invalidate cached results, but they do not change query shape.
388 // Recomputing QueryCtx metadata here only adds overhead to the invalidation path.
389 info.invalidate(select_invalidation_kind(info, changeKind));
390 }
391 }
392
393 void invalidate_queries_for_rel(Entity relation, ChangeKind changeKind) {
394 auto it = m_relationToQuery.find(EntityLookupKey(relation));
395 if (it == m_relationToQuery.end())
396 return;
397
398 for (const auto handle: it->second) {
399 auto& info = get(handle);
400 // Relation changes affect dynamic freshness, not the query definition itself.
401 info.invalidate(select_invalidation_kind(info, changeKind));
402 }
403 }
404
405 void invalidate_sorted_queries_for_entity(Entity entity) {
406 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
407 if (it == m_sortEntityToQuery.end())
408 return;
409
410 for (const auto handle: it->second) {
411 auto* pInfo = try_get(handle);
412 if (pInfo == nullptr || pInfo->refs() == 0)
413 continue;
414
415 pInfo->invalidate_sort();
416 }
417 }
418
421 for (const auto handle: m_sortedQueries) {
422 auto* pInfo = try_get(handle);
423 if (pInfo == nullptr || pInfo->refs() == 0)
424 continue;
425
426 pInfo->invalidate_sort();
427 }
428 }
429
430 void sync_archetype_cache(QueryInfo& queryInfo) {
431 const auto handle = QueryInfo::handle(queryInfo);
432 if (!valid(handle))
433 return;
434
435 const auto archetypes = queryInfo.cache_archetype_view();
436 const auto key = QueryHandleLookupKey(handle);
437 auto it = m_queryToArchetype.find(key);
438 if (it != m_queryToArchetype.end() && it->second.syncedRevision == queryInfo.result_cache_rev())
439 return;
440
441 unregister_query_archetypes(handle);
442
443 if (archetypes.empty())
444 return;
445
446 auto [trackedIt, inserted] = m_queryToArchetype.try_emplace(key);
447 auto& tracked = trackedIt->second.archetypes;
448 if (!inserted)
449 tracked.clear();
450
451 tracked.reserve((uint32_t)archetypes.size());
452 for (const auto* pArchetype: archetypes) {
453 tracked.push_back(pArchetype);
454 add_archetype_query_pair(pArchetype, handle);
455 }
456 trackedIt->second.syncedRevision = queryInfo.result_cache_rev();
457 }
458
459 void remove_archetype_from_queries(Archetype* pArchetype) {
460 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
461 auto it = m_archetypeToQuery.find(archetypeKey);
462 if (it == m_archetypeToQuery.end())
463 return;
464
465 const auto handles = it->second;
466 for (const auto handle: handles) {
467 auto* pInfo = try_get(handle);
468 if (pInfo != nullptr && pInfo->refs() != 0)
469 pInfo->remove(pArchetype);
470
471 auto trackedIt = m_queryToArchetype.find(QueryHandleLookupKey(handle));
472 if (trackedIt == m_queryToArchetype.end())
473 continue;
474
475 auto& tracked = trackedIt->second.archetypes;
476 core::swap_erase(tracked, core::get_index(tracked, pArchetype));
477 if (tracked.empty())
478 m_queryToArchetype.erase(trackedIt);
479 }
480
481 m_archetypeToQuery.erase(it);
482 }
483
484 void register_archetype_with_queries(const Archetype* pArchetype) {
485 auto& handles = prepare_create_query_handles();
486 const bool needsExactPairSelectors = has_create_selector_kind(CreateSelectorKind::ExactPair);
487 const bool needsRelWildcardSelectors = has_create_selector_kind(CreateSelectorKind::RelWildcardPair);
488 const bool needsTgtWildcardSelectors = has_create_selector_kind(CreateSelectorKind::TgtWildcardPair);
489 const bool needsAnyPairWildcardSelectors = has_create_selector_kind(CreateSelectorKind::AnyPairWildcard);
490 bool hasAnyPair = false;
491 cnt::darray_ext<Entity, 16> pairWildcardRelations;
492 for (const auto entity: pArchetype->ids_view()) {
493 if (!entity.pair()) {
494 add_create_query_handles(entity, handles);
495 continue;
496 }
497
498 hasAnyPair = true;
499 if (needsExactPairSelectors)
500 add_create_query_handles(entity, handles);
501
502 // Pair ids retain the relation/target ids plus their kind bits. That is enough to
503 // rebuild wildcard pair lookup keys without touching the world record storage.
504 const auto relKind = entity.entity() ? EntityKind::EK_Uni : EntityKind::EK_Gen;
505 const auto rel = Entity((EntityId)entity.id(), 0, false, false, relKind);
506 const auto tgt = Entity((EntityId)entity.gen(), 0, false, false, entity.kind());
507 if (needsTgtWildcardSelectors)
508 add_create_query_handles(Pair(All, tgt), handles);
509 if (needsRelWildcardSelectors && !core::has(pairWildcardRelations, rel)) {
510 pairWildcardRelations.push_back(rel);
511 add_create_query_handles(Pair(rel, All), handles);
512 }
513 }
514
515 if (hasAnyPair && needsAnyPairWildcardSelectors)
516 add_create_query_handles(Pair(All, All), handles);
517
518 for (const auto& candidate: handles) {
519 auto* pInfo = try_get(candidate.handle);
520 if (pInfo == nullptr || pInfo->refs() == 0)
521 continue;
522
523 if (!pInfo->register_archetype(*pArchetype, candidate.matchedSelector, true))
524 continue;
525
526 register_query_archetype(candidate.handle, pArchetype, pInfo->result_cache_rev());
527 }
528 }
529
530 private:
531#if GAIA_ECS_TEST_HOOKS
532 GAIA_NODISCARD static bool
533 archetype_span_contains(std::span<const Archetype*> archetypes, const Archetype* pArchetype) {
534 for (const auto* pCachedArchetype: archetypes) {
535 if (pCachedArchetype == pArchetype)
536 return true;
537 }
538 return false;
539 }
540
541 GAIA_NODISCARD static bool tracked_archetypes_contain(
542 const cnt::darray<const Archetype*>& archetypes, const ArchetypeIdLookupKey& archetypeKey) {
543 for (const auto* pArchetype: archetypes) {
544 if (pArchetype == nullptr)
545 return false;
546
547 if (ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash()) == archetypeKey)
548 return true;
549 }
550 return false;
551 }
552#endif
553
555 static CreateSelectorKind classify_create_selector(Entity entity) {
556 if (!entity.pair())
557 return CreateSelectorKind::Other;
558 if (is_wildcard(entity.id()))
559 return is_wildcard(entity.gen()) ? CreateSelectorKind::AnyPairWildcard : CreateSelectorKind::TgtWildcardPair;
560 if (is_wildcard(entity.gen()))
561 return CreateSelectorKind::RelWildcardPair;
562 return CreateSelectorKind::ExactPair;
563 }
564
565 GAIA_NODISCARD static constexpr uint32_t selector_kind_idx(CreateSelectorKind kind) {
566 return (uint32_t)kind;
567 }
568
569 GAIA_NODISCARD bool has_create_selector_kind(CreateSelectorKind kind) const {
570 return m_createQuerySelectorCnt[selector_kind_idx(kind)] != 0;
571 }
572
574 void track_create_selector(Entity entity) {
575 const auto kind = classify_create_selector(entity);
576 ++m_createQuerySelectorCnt[selector_kind_idx(kind)];
577 }
578
580 void untrack_create_selector(Entity entity) {
581 const auto kind = classify_create_selector(entity);
582 auto& cnt = m_createQuerySelectorCnt[selector_kind_idx(kind)];
583 GAIA_ASSERT(cnt != 0);
584 --cnt;
585 }
586
587 static QueryInfo::InvalidationKind select_invalidation_kind(const QueryInfo& info, ChangeKind changeKind) {
588 switch (changeKind) {
589 case ChangeKind::DynamicResult:
591 case ChangeKind::All:
593 case ChangeKind::Structural:
594 // Structural changes invalidate seed caches for structural queries.
595 // Dynamic queries reuse structural compilation state and only need their
596 // final result refreshed on the next read.
597 return (info.ctx().data.deps.has_dep_flag(QueryCtx::DependencyHasSourceTerms) ||
598 info.ctx().data.deps.has_dep_flag(QueryCtx::DependencyHasVariableTerms))
601 }
602
603 GAIA_ASSERT(false);
605 }
606
610 void add_entity_query_pair(Entity entity, QueryHandle handle) {
611 EntityLookupKey entityKey(entity);
612 const auto it = m_entityToQuery.find(entityKey);
613 if (it == m_entityToQuery.end()) {
614 m_entityToQuery.try_emplace(entityKey, cnt::darray<QueryHandle>{handle});
615 return;
616 }
617
618 auto& handles = it->second;
619 if (!core::has(handles, handle))
620 handles.push_back(handle);
621 }
622
626 void del_entity_query_pair(Entity entity, QueryHandle handle) {
627 auto it = m_entityToQuery.find(EntityLookupKey(entity));
628 if (it == m_entityToQuery.end())
629 return;
630
631 auto& handles = it->second;
632 const auto idx = core::get_index_unsafe(handles, handle);
633 core::swap_erase_unsafe(handles, idx);
634
635 // Remove the mapping if there are no more matches
636 if (handles.empty())
637 m_entityToQuery.erase(it);
638 }
639
643 void add_entity_to_query_pairs(EntitySpan entities, QueryHandle handle) {
644 for (auto entity: entities) {
645 add_entity_query_pair(entity, handle);
646 }
647 }
648
652 void del_entity_to_query_pairs(EntitySpan entities, QueryHandle handle) {
653 for (auto entity: entities) {
654 del_entity_query_pair(entity, handle);
655 }
656 }
657
658 void add_create_to_query_pair(Entity entity, QueryHandle handle) {
659 EntityLookupKey entityKey(entity);
660 const auto it = m_entityToCreateQuery.find(entityKey);
661 if (it == m_entityToCreateQuery.end()) {
662 m_entityToCreateQuery.try_emplace(entityKey, cnt::darray<QueryHandle>{handle});
663 track_create_selector(entity);
664 return;
665 }
666
667 auto& handles = it->second;
668 if (!core::has(handles, handle)) {
669 handles.push_back(handle);
670 track_create_selector(entity);
671 }
672 }
673
674 void add_sort_to_query_pair(Entity entity, QueryHandle handle) {
675 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
676 if (it == m_sortEntityToQuery.end()) {
677 m_sortEntityToQuery.try_emplace(EntityLookupKey(entity), cnt::darray<QueryHandle>{handle});
678 return;
679 }
680
681 auto& handles = it->second;
682 if (!core::has(handles, handle))
683 handles.push_back(handle);
684 }
685
686 void del_sort_to_query_pair(Entity entity, QueryHandle handle) {
687 auto it = m_sortEntityToQuery.find(EntityLookupKey(entity));
688 if (it == m_sortEntityToQuery.end())
689 return;
690
691 auto& handles = it->second;
692 core::swap_erase(handles, core::get_index(handles, handle));
693 if (handles.empty())
694 m_sortEntityToQuery.erase(it);
695 }
696
697 void add_sort_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
698 if (ctx.data.sortByFunc == nullptr || ctx.data.sortBy == EntityBad)
699 return;
700
701 add_sort_to_query_pair(ctx.data.sortBy, handle);
702 }
703
704 void del_sort_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
705 if (ctx.data.sortByFunc == nullptr || ctx.data.sortBy == EntityBad)
706 return;
707
708 del_sort_to_query_pair(ctx.data.sortBy, handle);
709 }
710
711 void add_sorted_query(const QueryCtx& ctx, QueryHandle handle) {
712 if (ctx.data.sortByFunc == nullptr)
713 return;
714
715 m_sortedQueries.push_back(handle);
716 }
717
718 void del_sorted_query(const QueryCtx& ctx, QueryHandle handle) {
719 if (ctx.data.sortByFunc == nullptr)
720 return;
721
722 const auto idx = core::get_index(m_sortedQueries, handle);
723 GAIA_ASSERT(idx != BadIndex);
724 if (idx != BadIndex)
725 core::swap_erase(m_sortedQueries, idx);
726 }
727
728 void del_create_to_query_pair(Entity entity, QueryHandle handle) {
729 auto it = m_entityToCreateQuery.find(EntityLookupKey(entity));
730 if (it == m_entityToCreateQuery.end())
731 return;
732
733 auto& handles = it->second;
734 core::swap_erase(handles, core::get_index(handles, handle));
735 untrack_create_selector(entity);
736 if (handles.empty())
737 m_entityToCreateQuery.erase(it);
738 }
739
740 void add_create_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
741 if (ctx.data.cachePolicy != QueryCtx::CachePolicy::Immediate)
742 return;
743
744 // Only structural queries with positive selector dependencies are tracked here.
745 // Dependency metadata is refreshed together with cache policy classification so
746 // create-time propagation can consume it without re-deriving query shape here.
747 for (const auto entity: ctx.data.deps.create_selectors_view())
748 add_create_to_query_pair(entity, handle);
749 }
750
751 void del_create_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
752 if (ctx.data.cachePolicy != QueryCtx::CachePolicy::Immediate)
753 return;
754
755 for (const auto entity: ctx.data.deps.create_selectors_view())
756 del_create_to_query_pair(entity, handle);
757 }
758
759 void add_create_query_handles(Entity selector, cnt::darray<CreateQueryCandidate>& handles) {
760 const auto it = m_entityToCreateQuery.find(EntityLookupKey(selector));
761 if (it == m_entityToCreateQuery.end())
762 return;
763
764 for (const auto handle: it->second) {
765 if (mark_create_query_handle(handle))
766 handles.push_back(CreateQueryCandidate{handle, selector});
767 }
768 }
769
770 cnt::darray<CreateQueryCandidate>& prepare_create_query_handles() {
771 m_createQueryHandleScratch.clear();
772
773 // Archetype creation can fan out through many positive selector ids. Use a monotonic stamp table
774 // keyed by query-handle id so duplicate candidates do not devolve into repeated linear scans.
775 ++m_createQueryHandleStamp;
776 if (m_createQueryHandleStamp == 0) {
777 m_createQueryHandleStampById = {};
778 m_createQueryHandleStamp = 1;
779 }
780
781 return m_createQueryHandleScratch;
782 }
783
784 GAIA_NODISCARD bool mark_create_query_handle(QueryHandle handle) {
785 const auto handleId = (uint32_t)handle.id();
786 if (handleId >= m_createQueryHandleStampById.size())
787 m_createQueryHandleStampById.resize(handleId + 1);
788
789 auto& stamp = m_createQueryHandleStampById[handleId];
790 if (stamp == m_createQueryHandleStamp)
791 return false;
792
793 stamp = m_createQueryHandleStamp;
794 return true;
795 }
796
797 void add_archetype_query_pair(const Archetype* pArchetype, QueryHandle handle) {
798 const auto archetypeKey = ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash());
799 const auto it = m_archetypeToQuery.find(archetypeKey);
800 if (it == m_archetypeToQuery.end()) {
801 m_archetypeToQuery.try_emplace(archetypeKey, cnt::darray<QueryHandle>{handle});
802 return;
803 }
804
805 auto& handles = it->second;
806 // Callers only register a <query, archetype> edge after they proved that edge is new.
807 GAIA_ASSERT(!core::has(handles, handle));
808 handles.push_back(handle);
809 }
810
811 void del_archetype_query_pair(const Archetype* pArchetype, QueryHandle handle) {
812 auto it = m_archetypeToQuery.find(ArchetypeIdLookupKey(pArchetype->id(), pArchetype->id_hash()));
813 if (it == m_archetypeToQuery.end())
814 return;
815
816 auto& handles = it->second;
817 const auto idx = core::get_index(handles, handle);
818 GAIA_ASSERT(idx != BadIndex);
819 core::swap_erase(handles, idx);
820 if (handles.empty())
821 m_archetypeToQuery.erase(it);
822 }
823
824 void unregister_query_archetypes(QueryHandle handle) {
825 auto it = m_queryToArchetype.find(QueryHandleLookupKey(handle));
826 if (it == m_queryToArchetype.end())
827 return;
828
829 const auto& tracked = it->second.archetypes;
830 for (const auto* pArchetype: tracked)
831 del_archetype_query_pair(pArchetype, handle);
832
833 m_queryToArchetype.erase(it);
834 }
835
836 void register_query_archetype(QueryHandle handle, const Archetype* pArchetype, uint32_t syncedRevision) {
837 auto [trackedIt, inserted] = m_queryToArchetype.try_emplace(QueryHandleLookupKey(handle));
838 auto& tracked = trackedIt->second.archetypes;
839
840 // Newly-created archetypes and sync_archetype_cache() both route through a deduplicated edge set,
841 // so reverse-index registration can append directly instead of re-scanning tracked archetypes.
842 GAIA_ASSERT(inserted || !core::has(tracked, pArchetype));
843 tracked.push_back(pArchetype);
844 trackedIt->second.syncedRevision = syncedRevision;
845 add_archetype_query_pair(pArchetype, handle);
846 }
847
848 void add_rel_query_pair(Entity relation, QueryHandle handle) {
849 const auto key = EntityLookupKey(relation);
850 const auto it = m_relationToQuery.find(key);
851 if (it == m_relationToQuery.end()) {
852 m_relationToQuery.try_emplace(key, cnt::darray<QueryHandle>{handle});
853 return;
854 }
855
856 auto& handles = it->second;
857 if (!core::has(handles, handle))
858 handles.push_back(handle);
859 }
860
861 void del_rel_query_pair(Entity relation, QueryHandle handle) {
862 auto it = m_relationToQuery.find(EntityLookupKey(relation));
863 if (it == m_relationToQuery.end())
864 return;
865
866 auto& handles = it->second;
867 core::swap_erase(handles, core::get_index(handles, handle));
868 if (handles.empty())
869 m_relationToQuery.erase(it);
870 }
871
872 void add_rel_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
873 for (const auto relation: ctx.data.deps.relations_view())
874 add_rel_query_pair(relation, handle);
875 }
876
877 void del_rel_to_query_pairs(const QueryCtx& ctx, QueryHandle handle) {
878 for (const auto relation: ctx.data.deps.relations_view())
879 del_rel_query_pair(relation, handle);
880 }
881 };
882 } // namespace ecs
883} // namespace gaia
Array with variable size of elements of type.
Definition darray_impl.h:25
iterator erase(iterator pos) noexcept
Removes the element at pos.
Definition darray_impl.h:331
Definition span_impl.h:99
Definition query_cache.h:61
void clear_archetype_tracking()
Clears only the reverse indices that keep raw archetype pointers alive. Used during world shutdown be...
Definition query_cache.h:175
QueryInfo & add(QueryCtx &&ctx, const EntityToArchetypeMap &entityToArchetypeMap, std::span< const Archetype * > allArchetypes)
Registers the provided query lookup context ctx. If it already exists it is returned.
Definition query_cache.h:286
QueryInfo & get(QueryHandle handle)
Returns a QueryInfo object associated with handle.
Definition query_cache.h:271
bool del(QueryHandle handle)
Deletes an existing QueryInfo object given the provided query handle.
Definition query_cache.h:337
QueryInfo * try_get(QueryHandle handle)
Returns a QueryInfo object associated with handle.
Definition query_cache.h:183
QueryInfo & add_local(QueryCtx &&ctx, const EntityToArchetypeMap &entityToArchetypeMap, std::span< const Archetype * > allArchetypes)
Registers a cached query without deduplicating against the shared lookup map.
Definition query_cache.h:319
const QueryInfo * try_get(QueryHandle handle) const
Returns a QueryInfo object associated with handle.
Definition query_cache.h:196
void invalidate_sorted_queries()
Invalidates all cached sorted queries after chunk row order changes.
Definition query_cache.h:420
void invalidate_queries_for_entity(EntityLookupKey entityKey, ChangeKind changeKind)
Invalidates all cached queries that work with the given entity This covers the following kinds of que...
Definition query_cache.h:379
Definition query_info.h:100
GAIA_NODISCARD std::span< const Archetype * > cache_archetype_view() const
Returns the cached result archetypes as a span.
Definition query_info.h:2307
GAIA_NODISCARD uint32_t result_cache_rev() const
Returns the result membership revision used by reverse-index cache users.
Definition query_info.h:979
InvalidationKind
Definition query_info.h:146
@ Result
Only the final result cache is stale. Structural seed matches remain valid and can be reused.
@ All
Full invalidation of all query cache state.
@ Seed
Structural seed matches are stale. This also implies the final result cache must be rebuilt.
uint32_t idx
Allocated items: index in the query slot list. Deleted items: index of the next deleted item in the s...
Definition query_info.h:104
void add_ref()
Adds one external reference to this query slot.
Definition query_info.h:833
uint32_t gen
Generation ID of the query slot.
Definition query_info.h:106
GAIA_NODISCARD QueryCtx & ctx()
Returns the mutable compiled query context.
Definition query_info.h:2092
static GAIA_NODISCARD QueryHandle handle(const QueryInfo &info)
Builds a stable query handle from query slot metadata.
Definition query_info.h:943
void invalidate(InvalidationKind kind=InvalidationKind::All)
Marks cached query results stale.
Definition query_info.h:862
Definition query_cache.h:24
Wrapper for two types forming a relationship pair. Depending on what types are used to form a pair it...
Definition id.h:224
Checks if endianess was detected correctly at compile-time.
Definition bitset.h:9
constexpr uint32_t BadIndex
Sentinel index value returned by helpers when a lookup fails.
Definition utility.h:20
Definition paged_storage.h:37
Hashmap lookup structure used for Entity.
Definition id.h:468
Definition id.h:247
Definition query_common.h:564
QueryIdentity q
Query identity.
Definition query_common.h:572
Hashmap lookup structure used for Entity.
Definition query_common.h:193
Definition query_common.h:144
QueryHandle handle
Query id.
Definition query_common.h:552
Definition query_info.h:94
Definition robin_hood.h:418