Gaia-ECS v1.0.0
A simple and powerful entity component system
Loading...
Searching...
No Matches
observer.inl
1#include "gaia/config/config.h"
2
3#include <cinttypes>
4
5#include "gaia/ecs/chunk_iterator.h"
6#include "gaia/ecs/id.h"
7#include "gaia/ecs/observer.h"
8
9#if GAIA_OBSERVERS_ENABLED
10namespace gaia {
11 namespace ecs {
12 inline void world_finish_write(World& world, Entity term, Entity entity);
13
14 static void observer_finish_iter_writes(Iter& it) {
15 auto* pChunk = const_cast<Chunk*>(it.chunk());
16 if (pChunk == nullptr)
17 return;
18
19 for (auto compIdx: it.touched_comp_indices())
20 pChunk->finish_write(compIdx, it.row_begin(), it.row_end());
21
22 auto terms = it.touched_terms();
23 if (terms.empty())
24 return;
25
26 auto& world = *it.world();
27 const auto entities = it.entity_rows();
28 GAIA_EACH(terms) {
29 const auto term = terms[i];
30 if (!world_component_uses_sparse_storage(world, term)) {
31 const auto compIdx = core::get_index(it.chunk()->ids_view(), term);
32 if (compIdx != BadIndex) {
33 pChunk->finish_write(compIdx, it.row_begin(), it.row_end());
34 continue;
35 }
36 }
37
38 GAIA_FOR_(entities.size(), j) {
39 world_finish_write(world, term, entities[j]);
40 }
41 }
42 }
43
44 inline void ObserverRuntimeData::exec(Iter& iter, EntitySpan targets, ObserverEvent reportedEvent) {
45 const auto& queryInfo = query.fetch();
46 #if GAIA_ASSERT_ENABLED
47 iter.set_query_access(&queryInfo.ctx().data);
48 #endif
49
50 #if GAIA_PROFILER_CPU
51 const auto name = entity_name(*queryInfo.world(), entity);
52 const char* pScopeName = !name.empty() ? name.data() : sc_observer_query_func_str;
53 GAIA_PROF_SCOPE2(pScopeName);
54 #endif
55
56 auto* pWorld = iter.world();
57 #if GAIA_OBSERVERS_ENABLED && GAIA_ASSERT_ENABLED
58 pWorld->observer_callback_enter();
59 #endif
60 const auto queryIdCnt = (uint32_t)plan.termCount;
61 iter.event(reportedEvent);
62 const auto& termIds = queryTermIds;
63 const auto terms = queryInfo.ctx().data.terms_view();
64 const QueryTerm* termsByField[MAX_ITEMS_IN_QUERY]{};
65 for (const auto& term: terms)
66 termsByField[term.fieldIndex] = &term;
67
68 const Archetype* pCachedArchetype = nullptr;
69 uint8_t cachedIndices[ChunkHeader::MAX_COMPONENTS];
70 GAIA_FOR(ChunkHeader::MAX_COMPONENTS) {
71 cachedIndices[i] = 0xFF;
72 }
73
74 for (auto e: targets) {
75 const auto& ec = pWorld->fetch(e);
76 if (pCachedArchetype != ec.pArchetype) {
77 pCachedArchetype = ec.pArchetype;
78 GAIA_FOR(ChunkHeader::MAX_COMPONENTS) {
79 cachedIndices[i] = 0xFF;
80 }
81
82 auto indicesView = queryInfo.try_indices_mapping_view(ec.pArchetype);
83 if (!indicesView.empty()) {
84 GAIA_FOR(queryIdCnt) {
85 cachedIndices[i] = indicesView[i];
86 }
87 } else {
88 GAIA_FOR(queryIdCnt) {
89 const auto* pTerm = termsByField[i];
90 if (pTerm == nullptr || !query_term_maps_to_current_archetype(*pTerm))
91 continue;
92
93 const auto queryId = termIds[i];
94 auto compIdx = world_component_index_comp_idx(*pWorld, *ec.pArchetype, queryId);
95 if (compIdx == BadIndex || compIdx == ComponentIndexBad)
96 compIdx = core::get_index(ec.pArchetype->ids_view(), queryId);
97 cachedIndices[i] =
98 (compIdx != BadIndex && compIdx != ComponentIndexBad && compIdx < ec.pArchetype->ids_view().size())
99 ? (uint8_t)compIdx
100 : (uint8_t)0xFF;
101 }
102 }
103 }
104
105 iter.set_archetype(ec.pArchetype);
106 iter.set_chunk(ec.pChunk, ec.row, (uint16_t)(ec.row + 1));
107 iter.set_comp_indices(cachedIndices);
108 iter.set_term_ids(termIds.data());
109 iter.set_write_im(false);
110 on_each_func(iter);
111 observer_finish_iter_writes(iter);
112 iter.clear_touched_writes();
113 }
114 #if GAIA_OBSERVERS_ENABLED && GAIA_ASSERT_ENABLED
115 pWorld->observer_callback_leave();
116 #endif
117 }
118
119 class ObserverBuilder {
120 World& m_world;
121 Entity m_entity;
122
123 void validate() {
124 GAIA_ASSERT(m_world.valid(m_entity));
125 }
126
127 Observer_& data() {
128 auto ss = m_world.acc_mut(m_entity);
129 auto& sys = ss.smut<Observer_>();
130 return sys;
131 }
132
133 const Observer_& data() const {
134 auto ss = m_world.acc(m_entity);
135 const auto& sys = ss.get<Observer_>();
136 return sys;
137 }
138
139 ObserverRuntimeData& runtime_data() {
140 return m_world.observers().data(m_entity);
141 }
142
143 const ObserverRuntimeData& runtime_data() const {
144 return m_world.observers().data(m_entity);
145 }
146
147 static void
148 cache_term_desc(ObserverRuntimeData& data, QueryOpKind op, Entity term, const QueryTermOptions& options) {
149 GAIA_ASSERT(data.plan.termCount < MAX_ITEMS_IN_QUERY);
150 if (data.plan.termCount < MAX_ITEMS_IN_QUERY) {
151 data.queryTermIds[data.plan.termCount] = term;
152 data.queryTermOps[data.plan.termCount] = op;
153 data.queryTermMatchKinds[data.plan.termCount] = options.matchKind;
154 }
155 }
156
157 bool has_default_match_options(const QueryTermOptions& options) const {
158 // Access mode (read/write) does not change membership, only access semantics.
159 // Source/traversal options can change membership and must stay on generic matcher.
160 return options.entSrc == EntityBad && options.entTrav == EntityBad;
161 }
162
163 bool is_complex_pair_term(Entity term) const {
164 GAIA_ASSERT(term.pair());
165
166 // Wildcards, Is-relations and variable-like endpoints can have dynamic semantics.
167 // Keep these on the generic matcher.
168 if (is_wildcard(term))
169 return true;
170 if (term.id() == Is.id())
171 return true;
172 if (is_variable(term.id()) || is_variable(term.gen()))
173 return true;
174
175 return false;
176 }
177
178 bool is_fast_path_eligible_term(Entity term, const QueryTermOptions& options) const {
179 // Pair/traversal/source terms can carry non-trivial semantics (e.g. IsA-like expressions).
180 // Also exclude wildcard-style terms (All), which are not fixed direct term matches.
181 // Keep these on the generic matcher for correctness.
182 if (term == EntityBad)
183 return false;
184
185 if (!has_default_match_options(options))
186 return false;
187
188 if (term == All)
189 return false;
190
191 if (!term.pair())
192 return true;
193
194 // Pair fast-path only supports fixed direct pairs.
195 if (is_complex_pair_term(term))
196 return false;
197
198 return true;
199 }
200
201 bool requires_diff_dispatch(Entity term, const QueryTermOptions& options) const {
202 if (options.entSrc != EntityBad || options.entTrav != EntityBad)
203 return true;
204
205 if (term == EntityBad || term == All)
206 return true;
207
208 if (is_variable((EntityId)term.id()))
209 return true;
210
211 if (term.pair()) {
212 if (is_wildcard(term))
213 return true;
214 if (is_variable((EntityId)term.id()) || is_variable((EntityId)term.gen()))
215 return true;
216 }
217
218 return false;
219 }
220
221 void register_diff_term_index(
222 ObserverRuntimeData& data, QueryOpKind op, Entity term, const QueryTermOptions& options) {
223 if (options.entTrav != EntityBad) {
224 bool hasRelation = false;
225 GAIA_FOR(data.plan.diff.traversalRelationCount) {
226 if (data.plan.diff.traversalRelations[i] == options.entTrav) {
227 hasRelation = true;
228 break;
229 }
230 }
231
232 if (!hasRelation) {
233 GAIA_ASSERT(data.plan.diff.traversalRelationCount < MAX_ITEMS_IN_QUERY);
234 if (data.plan.diff.traversalRelationCount < MAX_ITEMS_IN_QUERY)
235 data.plan.diff.traversalRelations[data.plan.diff.traversalRelationCount++] = options.entTrav;
236 }
237 }
238 update_diff_target_narrow_plan(data, op, term, options);
239 data.plan.refresh_exec_kind();
240 m_world.observers().add_diff_observer_term(m_world, m_entity, op, term, options);
241 }
242
243 void register_diff_term(ObserverRuntimeData& data, QueryOpKind op, Entity term, const QueryTermOptions& options) {
244 if (data.plan.monitorsQuery) {
245 const bool compoundNegative = data.plan.hasNegativeTerm && data.plan.termCount > 1;
246 if (requires_diff_dispatch(term, options) || compoundNegative)
247 data.plan.diff.enabled = true;
248 register_diff_term_index(data, op, term, options);
249 return;
250 }
251
252 const bool compoundNegative = data.plan.hasNegativeTerm && data.plan.termCount > 1;
253 if (data.plan.diff.enabled) {
254 register_diff_term_index(data, op, term, options);
255 return;
256 }
257
258 if (!requires_diff_dispatch(term, options) && !compoundNegative)
259 return;
260
261 data.plan.diff.enabled = true;
262 data.plan.refresh_exec_kind();
263
264 // A compound negative query starts using diff dispatch only after its second
265 // term is known. Register every query term so either transition direction
266 // can find the observer.
267 GAIA_FOR(data.plan.termCount) {
268 QueryTermOptions queryOptions{};
269 queryOptions.matchKind = data.queryTermMatchKinds[i];
270 if (i + 1 == data.plan.termCount)
271 queryOptions = options;
272 register_diff_term_index(data, data.queryTermOps[i], data.queryTermIds[i], queryOptions);
273 }
274 }
275
276 void update_diff_target_narrow_plan(
277 ObserverRuntimeData& data, QueryOpKind op, Entity term, const QueryTermOptions& options) {
278 using DispatchKind = ObserverPlan::DiffPlan::DispatchKind;
279 auto& diff = data.plan.diff;
280 if (diff.dispatchKind == DispatchKind::GlobalFallback)
281 return;
282
283 const auto mark_unsupported = [&] {
284 diff.dispatchKind = DispatchKind::GlobalFallback;
285 diff.bindingVar = EntityBad;
286 diff.bindingRelation = EntityBad;
287 diff.traversalRelation = EntityBad;
288 diff.travKind = QueryTravKind::None;
289 diff.travDepth = QueryTermOptions::TravDepthUnlimited;
290 diff.traversalTriggerTermCount = 0;
291 };
292
293 if (options.entSrc != EntityBad || options.entTrav != EntityBad) {
294 if (options.entSrc == EntityBad || options.entTrav == EntityBad || op != QueryOpKind::All ||
295 !query_trav_has(options.travKind, QueryTravKind::Up) ||
296 query_trav_has(options.travKind, QueryTravKind::Down)) {
297 mark_unsupported();
298 return;
299 }
300
301 if (diff.bindingVar == EntityBad)
302 diff.bindingVar = options.entSrc;
303 else if (diff.bindingVar != options.entSrc) {
304 mark_unsupported();
305 return;
306 }
307
308 if (diff.traversalRelation == EntityBad) {
309 diff.traversalRelation = options.entTrav;
310 diff.travKind = options.travKind;
311 diff.travDepth = options.travDepth;
312 } else if (
313 diff.traversalRelation != options.entTrav || diff.travKind != options.travKind ||
314 diff.travDepth != options.travDepth) {
315 mark_unsupported();
316 return;
317 }
318
319 bool hasTerm = false;
320 GAIA_FOR(diff.traversalTriggerTermCount) {
321 if (diff.traversalTriggerTerms[i] == term) {
322 hasTerm = true;
323 break;
324 }
325 }
326 if (!hasTerm) {
327 if (diff.traversalTriggerTermCount >= MAX_ITEMS_IN_QUERY) {
328 mark_unsupported();
329 return;
330 }
331 diff.traversalTriggerTerms[diff.traversalTriggerTermCount++] = term;
332 }
333
334 if (diff.dispatchKind == DispatchKind::LocalTargets)
335 diff.dispatchKind = DispatchKind::PropagatedTraversal;
336 return;
337 }
338
339 if (term.pair() && op == QueryOpKind::All && !is_wildcard(term) && !is_variable((EntityId)term.id()) &&
340 is_variable((EntityId)term.gen())) {
341 const auto bindingVar = entity_from_id(m_world, term.gen());
342 const auto bindingRelation = entity_from_id(m_world, term.id());
343 if (!m_world.valid(bindingRelation)) {
344 mark_unsupported();
345 return;
346 }
347
348 if (diff.bindingVar == EntityBad)
349 diff.bindingVar = bindingVar;
350 else if (diff.bindingVar != bindingVar) {
351 mark_unsupported();
352 return;
353 }
354
355 if (diff.bindingRelation == EntityBad)
356 diff.bindingRelation = bindingRelation;
357 else if (diff.bindingRelation != bindingRelation) {
358 mark_unsupported();
359 return;
360 }
361
362 return;
363 }
364
365 // Fixed local terms can use the entities supplied by the structural mutation.
366 // Semantic Is terms can affect inheriting entities, so keep those on the
367 // conservative global path.
368 if (!requires_diff_dispatch(term, options) &&
369 (!term.pair() || term.id() != Is.id() || options.matchKind == QueryMatchKind::Direct))
370 return;
371
372 mark_unsupported();
373 }
374
375 void reg_term(ObserverRuntimeData& data, QueryOpKind op, Entity term, const QueryTermOptions& options) {
376 cache_term_desc(data, op, term, options);
377 data.plan.add_term_desc(op, is_fast_path_eligible_term(term, options));
378 register_diff_term(data, op, term, options);
379 m_world.observers().add(m_world, term, m_entity, op, options.matchKind);
380 }
381
382 void rebuild_indices(ObserverRuntimeData& data) {
383 if (data.plan.termCount == 0)
384 return;
385
386 // Terms are registered as they are added to the builder. Rebuild those indexes
387 // when the mode is selected later so fluent-call order does not change behavior.
388 const auto terms = data.query.fetch().ctx().data.terms_view();
389 for (const auto& term: terms) {
390 QueryTermOptions options{};
391 options.entSrc = term.src;
392 options.entTrav = term.entTrav;
393 options.travKind = term.travKind;
394 options.travDepth = term.travDepth;
395 options.matchKind = term.matchKind;
396
397 m_world.observers().add(m_world, term.id, m_entity, term.op, term.matchKind);
398 if (data.plan.uses_diff_dispatch())
399 m_world.observers().add_diff_observer_term(m_world, m_entity, term.op, term.id, options);
400 }
401 }
402
403 public:
404 ObserverBuilder(World& world, Entity entity): m_world(world), m_entity(entity) {}
405
406 //------------------------------------------------
407
412 ObserverBuilder& event(ObserverEvent event) {
413 validate();
414 auto& observer = data();
415 if (!observer.monitorsQuery && observer.event == event)
416 return *this;
417
418 auto& runtime = runtime_data();
419 if (runtime.plan.termCount != 0)
420 m_world.observers().remove_observer_indices(m_world, m_entity);
421
422 observer.event = event;
423 observer.monitorsQuery = false;
424 runtime.plan.monitorsQuery = false;
425 runtime.plan.refresh_exec_kind();
426 rebuild_indices(runtime);
427 return *this;
428 }
429
434 ObserverBuilder& monitor() {
435 validate();
436 auto& observer = data();
437 if (observer.monitorsQuery)
438 return *this;
439
440 auto& runtime = runtime_data();
441 if (runtime.plan.termCount != 0)
442 m_world.observers().remove_observer_indices(m_world, m_entity);
443
444 observer.monitorsQuery = true;
445 runtime.plan.monitorsQuery = true;
446 runtime.plan.refresh_exec_kind();
447 rebuild_indices(runtime);
448 return *this;
449 }
450
454 ObserverBuilder& kind(QueryCacheKind kind) {
455 validate();
456 runtime_data().query.kind(kind);
457 return *this;
458 }
459
463 ObserverBuilder& scope(QueryCacheScope scope) {
464 validate();
465 runtime_data().query.scope(scope);
466 return *this;
467 }
468
469 //------------------------------------------------
470
471 ObserverBuilder& add(QueryInput item) {
472 validate();
473 auto& data = runtime_data();
474 data.query.add(item);
475
476 QueryTermOptions options{};
477 options.entSrc = item.entSrc;
478 options.entTrav = item.entTrav;
479 options.travKind = item.travKind;
480 options.travDepth = item.travDepth;
481 options.access = item.access;
482 options.matchKind = item.matchKind;
483
484 cache_term_desc(data, item.op, item.id, options);
485 data.plan.add_term_desc(item.op, is_fast_path_eligible_term(item.id, options));
486 register_diff_term(data, item.op, item.id, options);
487 m_world.observers().add(m_world, item.id, m_entity, item.op, item.matchKind);
488 return *this;
489 }
490
491 //------------------------------------------------
492
493 ObserverBuilder& is(Entity entity, const QueryTermOptions& options = {}) {
494 return all(Pair(Is, entity), options);
495 }
496
497 //------------------------------------------------
498
499 ObserverBuilder& in(Entity entity, QueryTermOptions options = {}) {
500 options.in();
501 return all(Pair(Is, entity), options);
502 }
503
504 //------------------------------------------------
505
506 ObserverBuilder& all(Entity entity, const QueryTermOptions& options = {}) {
507 validate();
508 auto& data = runtime_data();
509 data.query.all(entity, options);
510 reg_term(data, QueryOpKind::All, entity, options);
511 return *this;
512 }
513
514 ObserverBuilder& any(Entity entity, const QueryTermOptions& options = {}) {
515 validate();
516 auto& data = runtime_data();
517 data.query.any(entity, options);
518 reg_term(data, QueryOpKind::Any, entity, options);
519 return *this;
520 }
521
522 ObserverBuilder& or_(Entity entity, const QueryTermOptions& options = {}) {
523 validate();
524 auto& data = runtime_data();
525 data.query.or_(entity, options);
526 reg_term(data, QueryOpKind::Or, entity, options);
527 return *this;
528 }
529
530 ObserverBuilder& no(Entity entity, const QueryTermOptions& options = {}) {
531 validate();
532 auto& data = runtime_data();
533 data.query.no(entity, options);
534 reg_term(data, QueryOpKind::Not, entity, options);
535 return *this;
536 }
537
538 ObserverBuilder& match_prefab() {
539 validate();
540 runtime_data().query.match_prefab();
541 return *this;
542 }
543
544 template <typename T>
545 ObserverBuilder& all(const QueryTermOptions& options);
546
547 template <typename T>
548 ObserverBuilder& any(const QueryTermOptions& options);
549
550 template <typename T>
551 ObserverBuilder& or_(const QueryTermOptions& options);
552
553 template <typename T>
554 ObserverBuilder& no(const QueryTermOptions& options);
555
556 //------------------------------------------------
557
558 template <typename T>
559 ObserverBuilder& all();
560
561 template <typename T>
562 ObserverBuilder& any();
563
564 template <typename T>
565 ObserverBuilder& or_();
566
567 template <typename T>
568 ObserverBuilder& no();
569
570 //------------------------------------------------
571
574 ObserverBuilder& depth_order(Entity relation = ChildOf) {
575 validate();
576 runtime_data().query.depth_order(relation);
577 return *this;
578 }
579
582 template <typename Rel>
583 ObserverBuilder& depth_order();
584
585 //------------------------------------------------
586
587 ObserverBuilder& name(const char* name, uint32_t len = 0) {
588 m_world.name(m_entity, name, len);
589 return *this;
590 }
591
592 ObserverBuilder& name_raw(const char* name, uint32_t len = 0) {
593 m_world.name_raw(m_entity, name, len);
594 return *this;
595 }
596
597 //------------------------------------------------
598
599 template <typename Func, std::enable_if_t<std::is_invocable_v<Func, Iter&>, int> = 0>
600 ObserverBuilder& on_each(Func func) {
601 validate();
602
603 auto& ctx = runtime_data();
604 ctx.on_each_func = [func](Iter& it) {
605 func(it);
606 };
607
608 return (ObserverBuilder&)*this;
609 }
610
611 template <typename Func, std::enable_if_t<!std::is_invocable_v<Func, Iter&>, int> = 0>
612 ObserverBuilder& on_each(Func func);
613
614 GAIA_NODISCARD Entity entity() const {
615 return m_entity;
616 }
617
618 void exec(Iter& iter, EntitySpan targets) {
619 auto& ctx = runtime_data();
620 ctx.exec(iter, targets, data().event);
621 }
622 };
623
624 } // namespace ecs
625} // namespace gaia
626
627 #include "gaia/ecs/observer_typed.inl"
628
629#endif
GAIA_NODISCARD constexpr auto id() const noexcept
Entity index in the entity array.
Definition id.h:295