Gaia-ECS v0.9.3
A simple and powerful entity component system
Loading...
Searching...
No Matches
command_buffer.h
1#pragma once
2#include "gaia/config/config.h"
3
4#include <cstdint>
5#include <type_traits>
6
7#include "gaia/cnt/darray_ext.h"
8#include "gaia/cnt/dbitset.h"
9#include "gaia/ecs/archetype.h"
10#include "gaia/ecs/command_buffer_fwd.h"
11#include "gaia/ecs/common.h"
12#include "gaia/ecs/component.h"
13#include "gaia/ecs/component_cache.h"
14#include "gaia/ecs/component_cache_item.h"
15#include "gaia/ecs/id.h"
16#include "gaia/ecs/world.h"
17#include "gaia/ser/ser_buffer_binary.h"
18
19namespace gaia {
20 namespace ecs {
22 void lock() {}
23 void unlock() {}
24 };
25
27 mt::SpinLock m_lock;
28
29 void lock() {
30 m_lock.lock();
31 }
32
33 void unlock() {
34 m_lock.unlock();
35 }
36 };
37
38 namespace detail {
44 template <typename AccessContext>
45 class CommandBuffer final {
46 enum class OpType : uint8_t {
47 NONE = 0,
48 ADD_ENTITY,
49 CPY_ENTITY,
50 DEL_ENTITY,
51 ADD_COMPONENT,
52 ADD_COMPONENT_DATA,
53 SET_COMPONENT,
54 DEL_COMPONENT,
55 };
56
57 struct Op {
59 OpType type;
61 uint32_t off;
63 Entity target;
65 Entity other;
66 };
67
69 ecs::World& m_world;
77 uint32_t m_nextTemp = 0;
79 bool m_needsSort = false;
80
81 bool m_haveReal = false;
82 bool m_haveTemp = false;
83 Entity m_lastRealTarget = EntityBad;
84 Entity m_lastTempTarget = EntityBad;
85
87 ser::bin_stream m_data;
89 AccessContext m_acc;
90
91 public:
92 explicit CommandBuffer(World& world): m_world(world) {}
93 ~CommandBuffer() = default;
94
95 CommandBuffer(CommandBuffer&&) = delete;
96 CommandBuffer(const CommandBuffer&) = delete;
97 CommandBuffer& operator=(CommandBuffer&&) = delete;
98 CommandBuffer& operator=(const CommandBuffer&) = delete;
99
104 GAIA_NODISCARD Entity add(EntityKind kind = EntityKind::EK_Gen) {
105 core::lock_scope lock(m_acc);
106
107 Entity temp = add_temp(kind);
108 push_op({OpType::ADD_ENTITY, 0, temp, EntityBad});
109 return temp;
110 }
111
115 GAIA_NODISCARD Entity copy(Entity entityFrom) {
116 core::lock_scope lock(m_acc);
117
118 Entity temp = add_temp(entityFrom.kind());
119 push_op({OpType::CPY_ENTITY, 0, temp, entityFrom});
120 return temp;
121 }
122
128 template <typename T>
129 void add(Entity entity) {
130 verify_comp<T>();
131 core::lock_scope lock(m_acc);
132
133 // Make sure the component is registered
134 const auto& item = comp_cache_add<T>(m_world);
135
136 push_op({OpType::ADD_COMPONENT, 0, entity, item.entity});
137 }
138
142 void add(Entity entity, Entity other) {
143 core::lock_scope lock(m_acc);
144
145 push_op({OpType::ADD_COMPONENT, 0, entity, other});
146 }
147
151 void add(Entity entity, const Pair& pair) {
152 core::lock_scope lock(m_acc);
153
154 push_op({OpType::ADD_COMPONENT, 0, entity, (Entity)pair});
155 }
156
164 template <typename T, std::enable_if_t<!is_pair<std::remove_cv_t<std::remove_reference_t<T>>>::value, int> = 0>
165 void add(Entity entity, T&& value) {
166 verify_comp<T>();
167 core::lock_scope lock(m_acc);
168
169 // Make sure the component is registered
170 const auto& item = comp_cache_add<T>(m_world);
171
172 const auto pos = m_data.tell();
173 auto serializer = ser::make_serializer(m_data);
174 item.save(serializer, &value, 0, 1, 1);
175 push_op({OpType::ADD_COMPONENT_DATA, pos, entity, item.entity});
176 }
177
184 template <typename T>
185 void set(Entity entity, T&& value) {
186 verify_comp<T>();
187 core::lock_scope lock(m_acc);
188
189 // Make sure the component is registered
190 const auto& item = comp_cache(m_world).template get<T>();
191
192 const auto pos = m_data.tell();
193 auto serializer = ser::make_serializer(m_data);
194 item.save(serializer, &value, 0, 1, 1);
195 push_op({OpType::SET_COMPONENT, pos, entity, item.entity});
196 }
197
200 void del(Entity entity) {
201 core::lock_scope lock(m_acc);
202
203 push_op({OpType::DEL_ENTITY, 0, entity, EntityBad});
204 }
205
211 template <typename T>
212 void del(Entity entity) {
213 verify_comp<T>();
214 core::lock_scope lock(m_acc);
215
216 // Make sure the component is registered
217 const auto& item = comp_cache(m_world).template get<T>();
218
219 push_op({OpType::DEL_COMPONENT, 0, entity, item.entity});
220 }
221
225 void del(Entity entity, Entity object) {
226 core::lock_scope lock(m_acc);
227
228 push_op({OpType::DEL_COMPONENT, 0, entity, object});
229 }
230
234 void del(Entity entity, const Pair& pair) {
235 core::lock_scope lock(m_acc);
236
237 push_op({OpType::DEL_COMPONENT, 0, entity, (Entity)pair});
238 }
239
240 private:
242 GAIA_NODISCARD bool is_rel(OpType t) const {
243 return (uint32_t)t >= (uint32_t)OpType::ADD_COMPONENT;
244 }
245
247 GAIA_NODISCARD bool is_tmp(Entity e) const {
248 return e.data.tmp != 0;
249 }
250
252 GAIA_NODISCARD Entity resolve(Entity e) const {
253 if (!is_tmp(e))
254 return e;
255
256 const auto ti = e.id();
257 if (ti < m_temp2real.size())
258 return m_temp2real[ti];
259
260 return EntityBad;
261 }
262
264 GAIA_NODISCARD Pair decode_pair(Entity pair) const {
265 GAIA_ASSERT(pair.pair());
266 return Pair(m_world.get(pair.id()), m_world.get(pair.gen()));
267 }
268
270 void replay_add(Entity target, Entity object) {
271 if (object.pair())
272 World::EntityBuilder(m_world, target).add(decode_pair(object));
273 else
274 World::EntityBuilder(m_world, target).add(object);
275 }
276
278 void replay_del(Entity target, Entity object) {
279 if (object.pair())
280 World::EntityBuilder(m_world, target).del(decode_pair(object));
281 else
282 World::EntityBuilder(m_world, target).del(object);
283 }
284
287 GAIA_NODISCARD bool is_canceled_temp(uint32_t idx) const {
288 const uint32_t base = idx * 3;
289 if (base + 2 >= m_tmpFlags.size())
290 return false;
291
292 const bool destroy = m_tmpFlags.test(base);
293 const bool usedOther = m_tmpFlags.test(base + 2);
294
295 // If deleted in this batch and not referenced by others, cancel entirely
296 return destroy && !usedOther;
297 }
298
300 GAIA_NODISCARD Entity add_temp(EntityKind kind) {
301 m_temp2real.push_back(EntityBad);
302 Entity tmp(m_nextTemp++, 0, true, false, kind);
303 // Use the unused flag to mark temporary entities
304 tmp.data.tmp = 1;
305 return tmp;
306 }
307
308 GAIA_NODISCARD bool less_target(Entity a, Entity b) const {
309 const bool ta = is_tmp(a);
310 const bool tb = is_tmp(b);
311
312 // Real entities always come first in order. Temps come last.
313 if (ta != tb)
314 return !ta && tb;
315
316 // Within same domain, normal numeric order.
317 return a.id() < b.id();
318 }
319
321 void check_sort(const Op& op) {
322 if (is_tmp(op.target)) {
323 if (m_haveTemp) {
324 // Only compare temp indices against previous temp target
325 const uint32_t prev = m_lastTempTarget.id();
326 const uint32_t curr = op.target.id();
327 if (curr < prev)
328 m_needsSort = true;
329 }
330 m_lastTempTarget = op.target;
331 m_haveTemp = true;
332 } else {
333 if (m_haveReal) {
334 // Only compare real IDs against previous real target
335 if (op.target.id() < m_lastRealTarget.id())
336 m_needsSort = true;
337 }
338 m_lastRealTarget = op.target;
339 m_haveReal = true;
340 }
341 }
342
344 void push_op(Op&& op) {
345 check_sort(op);
346 m_ops.push_back(GAIA_MOV(op));
347 }
348
350 void clear() {
351 m_ops.clear();
352 m_temp2real.clear();
353 m_tmpFlags.reset();
354 m_nextTemp = 0;
355 m_data.reset();
356
357 m_needsSort = false;
358 m_haveReal = false;
359 m_haveTemp = false;
360 m_lastRealTarget = EntityBad;
361 m_lastTempTarget = EntityBad;
362 }
363
364 public:
366 void commit() {
367 core::lock_scope lock(m_acc);
368
369 if (m_ops.empty())
370 return;
371
372 GAIA_PROF_SCOPE(cmdbuf::commit);
373
374 // Build flags + allocate entities
375 if (m_nextTemp > 0) {
376 GAIA_PROF_SCOPE(cmdbuf::alloc);
377
378 // Bit layout for each temporary entity:
379 // bit 0 -> marked for destruction (DEL_ENTITY recorded)
380 // bit 1 -> used as a relation target (appeared as target in component ops)
381 // bit 2 -> used as a relation source / dependency (appeared as other in component ops)
382 m_tmpFlags.resize(m_nextTemp * 3);
383
384 // Pre-map surviving temps for ADD/CPY (avoid allocating canceled temps)
385 if (m_temp2real.size() < m_nextTemp) {
386 const auto from = m_temp2real.size();
387 m_temp2real.resize(m_nextTemp);
388 GAIA_FOR2(from, m_nextTemp) m_temp2real[i] = EntityBad;
389 }
390
391 // Build all flags
392 for (const Op& o: m_ops) {
393 // Set flag bits
394 if (is_tmp(o.target)) {
395 const uint32_t ti = o.target.id();
396
397 if (o.type == OpType::DEL_ENTITY)
398 m_tmpFlags.set((ti * 3) + 0, true);
399 else if (is_rel(o.type))
400 m_tmpFlags.set((ti * 3) + 1, true);
401 }
402
403 if (is_tmp(o.other) && o.other.id() < m_tmpFlags.size())
404 m_tmpFlags.set((o.other.id() * 3) + 2, true);
405 }
406
407 // Allocate real entities for the surviving temporaries
408 for (const Op& o: m_ops) {
409 if (!is_tmp(o.target))
410 continue;
411
412 const uint32_t ti = o.target.id();
413 if (is_canceled_temp(ti))
414 continue;
415
416 if (o.type == OpType::ADD_ENTITY) {
417 if (m_temp2real[ti] == EntityBad)
418 m_temp2real[ti] = m_world.add(o.target.kind());
419 } else if (o.type == OpType::CPY_ENTITY) {
420 if (m_temp2real[ti] == EntityBad) {
421 const Entity src = resolve(o.other);
422 if (src != EntityBad)
423 m_temp2real[ti] = m_world.copy(src);
424 }
425 }
426 }
427 }
428
429 // Sort by (target, other), reduce last-wins, apply relations.
430 // DEL_COMPONENT last per target.
431 if (m_needsSort) {
432 GAIA_PROF_SCOPE(cmdbuf::sort);
433
434 m_needsSort = false;
435 core::sort(m_ops.begin(), m_ops.end(), [](const Op& a, const Op& b) {
436 if (a.target != b.target)
437 return a.target < b.target;
438 if (a.other != b.other)
439 return a.other < b.other;
440 return false;
441 });
442 }
443
444 // Replay batched operations
445 Entity lastKey = EntityBad;
446 Entity lastResolved = EntityBad;
447 auto resolve_cached = [&](Entity e) {
448 if (e == lastKey)
449 return lastResolved;
450 lastKey = e;
451 return lastResolved = resolve(e);
452 };
453
454 {
455 GAIA_PROF_SCOPE(cmdbuf::merges);
456 for (uint32_t p = 0; p < m_ops.size();) {
457 GAIA_PROF_SCOPE(cmdbuf::merge);
458
459 const Entity tgtKey = m_ops[p].target;
460
461 const bool tgtIsTemp = is_tmp(tgtKey);
462 const uint32_t ti = tgtIsTemp ? tgtKey.id() : 0u;
463 const Entity tgtReal =
464 tgtIsTemp ? (ti < m_temp2real.size() ? m_temp2real[ti] : EntityBad) : resolve_cached(tgtKey);
465
466 // Range for this target
467 uint32_t q = p;
468 bool hasDelEntity = false;
469 while (q < m_ops.size() && m_ops[q].target == tgtKey) {
470 if (m_ops[q].type == OpType::DEL_ENTITY)
471 hasDelEntity = true;
472 ++q;
473 }
474
475 // Skip canceled or non-existent temporary entities
476 if (tgtReal == EntityBad) {
477 p = q;
478 continue;
479 }
480 if (tgtIsTemp && is_canceled_temp(ti)) {
481 p = q;
482 continue;
483 }
484
485 enum : uint8_t { F_ADD = 1 << 0, F_ADD_DATA = 1 << 1, F_SET = 1 << 2, F_DEL = 1 << 3 };
486
487 // Emit relation groups.
488 // Inside [p..q) range (same target), process groups by 'other'.
489 // We perform per-component reduction.
490 for (uint32_t i = p; i < q;) {
491 const Entity othKey = m_ops[i].other;
492 const Entity othReal = resolve_cached(othKey);
493
494 // Group ops with same (target, other)
495 uint32_t j = i + 1;
496 while (j < q && m_ops[j].other == othKey)
497 ++j;
498
499 if (tgtReal != EntityBad) {
500 const uint32_t groupSize = j - i;
501 // Fast path - single op
502 if (groupSize == 1) {
503 const Op& op = m_ops[i];
504 switch (op.type) {
505 case OpType::DEL_COMPONENT:
506 replay_del(tgtReal, othReal);
507 break;
508 case OpType::ADD_COMPONENT:
509 replay_add(tgtReal, othReal);
510 break;
511 case OpType::ADD_COMPONENT_DATA:
512 replay_add(tgtReal, othReal);
513 GAIA_FALLTHROUGH;
514 case OpType::SET_COMPONENT: {
515 const auto& ec = m_world.m_recs.entities[tgtReal.id()];
516 const auto row = tgtReal.kind() == EntityKind::EK_Uni ? 0U : ec.row;
517 const auto compIdx = ec.pChunk->comp_idx(othReal);
518 auto* pComponentData = (void*)ec.pChunk->comp_ptr_mut(compIdx, 0);
519
520 // Component data
521 auto serializer = ser::make_serializer(m_data);
522 serializer.seek(op.off);
523 const auto& item = m_world.comp_cache().get(othReal);
524 item.load(serializer, pComponentData, row, row + 1, ec.pChunk->capacity());
525 } break;
526 default:
527 break;
528 }
529 }
530 // Slow path: merge multiple ops
531 else {
532 uint8_t mask = 0;
533 uint32_t dataPos = 0;
534
535 for (uint32_t k = i; k < j; ++k) {
536 const Op& op = m_ops[k];
537 switch (op.type) {
538 case OpType::ADD_COMPONENT:
539 mask |= F_ADD;
540 break;
541 case OpType::ADD_COMPONENT_DATA:
542 mask |= F_ADD_DATA;
543 dataPos = op.off;
544 break;
545 case OpType::SET_COMPONENT:
546 mask |= F_SET;
547 dataPos = op.off;
548 break;
549 case OpType::DEL_COMPONENT:
550 mask |= F_DEL;
551 break;
552 default:
553 break;
554 }
555 }
556
557 const bool hasAdd = mask & F_ADD;
558 const bool hasAddData = mask & F_ADD_DATA;
559 const bool hasSet = mask & F_SET;
560 const bool hasDel = mask & F_DEL;
561
562 // 1) ADD(+DATA) + DEL = no-op
563 if (hasDel && (hasAdd || hasAddData)) {
564 }
565 // 2) DEL only
566 else if (hasDel) {
567 replay_del(tgtReal, othReal);
568 }
569 // 3) ADD_WITH_DATA or ADD+SET = ADD_WITH_DATA
570 else if (hasAddData || (hasAdd && hasSet)) {
571 replay_add(tgtReal, othReal);
572
573 const auto& ec = m_world.m_recs.entities[tgtReal.id()];
574 const auto row = tgtReal.kind() == EntityKind::EK_Uni ? 0U : ec.row;
575 const auto compIdx = ec.pChunk->comp_idx(othReal);
576 auto* pComponentData = (void*)ec.pChunk->comp_ptr_mut(compIdx, 0);
577
578 // Component data
579 auto serializer = ser::make_serializer(m_data);
580 serializer.seek(dataPos);
581 const auto& item = m_world.comp_cache().get(othReal);
582 item.load(serializer, pComponentData, row, row + 1, ec.pChunk->capacity());
583 }
584 // 4) ADD only
585 else if (hasAdd) {
586 replay_add(tgtReal, othReal);
587 }
588 // 5) SET only
589 else if (hasSet) {
590 const auto& ec = m_world.m_recs.entities[tgtReal.id()];
591 const auto row = tgtReal.kind() == EntityKind::EK_Uni ? 0U : ec.row;
592 const auto compIdx = ec.pChunk->comp_idx(othReal);
593 auto* pComponentData = (void*)ec.pChunk->comp_ptr_mut(compIdx, 0);
594
595 // Component data
596 auto serializer = ser::make_serializer(m_data);
597 serializer.seek(dataPos);
598 const auto& item = m_world.comp_cache().get(othReal);
599 item.load(serializer, pComponentData, row, row + 1, ec.pChunk->capacity());
600 }
601 }
602 }
603
604 // Advance to next component group
605 i = j;
606 }
607
608 // Safely delete entity only if it was actually created
609 if (hasDelEntity)
610 m_world.del(tgtReal);
611
612 // Advance to next target group
613 p = q;
614 }
615 }
616
617 clear();
618 }
619 };
620 } // namespace detail
621
622 using CommandBufferST = detail::CommandBuffer<AccessContextST>;
623 using CommandBufferMT = detail::CommandBuffer<AccessContextMT>;
624
625 inline CommandBufferST* cmd_buffer_st_create(World& world) {
626 return new CommandBufferST(world);
627 }
628 inline void cmd_buffer_destroy(CommandBufferST& cmdBuffer) {
629 delete &cmdBuffer;
630 }
631 inline void cmd_buffer_commit(CommandBufferST& cmdBuffer) {
632 cmdBuffer.commit();
633 }
634
635 inline CommandBufferMT* cmd_buffer_mt_create(World& world) {
636 return new CommandBufferMT(world);
637 }
638 inline void cmd_buffer_destroy(CommandBufferMT& cmdBuffer) {
639 delete &cmdBuffer;
640 }
641 inline void cmd_buffer_commit(CommandBufferMT& cmdBuffer) {
642 cmdBuffer.commit();
643 }
644 } // namespace ecs
645} // namespace gaia
Array with variable size of elements of type.
Definition darray_impl.h:25
GAIA_NODISCARD const ComponentCacheItem & get(Entity entity) const noexcept
Returns the component cache item.
Definition component_cache.h:408
Definition world.h:174
GAIA_NODISCARD const ComponentCache & comp_cache() const
Returns read-only access to the world component cache.
Definition world.h:2807
void del(Entity entity)
Removes an entity along with all data associated with it.
Definition world.h:4864
GAIA_NODISCARD Entity get(EntityId id) const
Returns the entity located at the index id.
Definition world.h:3212
GAIA_NODISCARD Entity copy(Entity srcEntity)
Creates a new entity by cloning an already existing one. Does not trigger observers.
Definition world.h:3545
GAIA_NODISCARD Entity add(EntityKind kind=EntityKind::EK_Gen)
Creates a new empty entity.
Definition world.h:3260
Buffer for deferred execution of some operations on entities.
Definition command_buffer.h:45
void add(Entity entity, Entity other)
Requests an entity other to be added to entity entity.
Definition command_buffer.h:142
void add(Entity entity)
Requests a component T to be added to entity.
Definition command_buffer.h:129
void del(Entity entity)
Requests an existing entity to be removed.
Definition command_buffer.h:200
void add(Entity entity, const Pair &pair)
Requests a relationship pair to be added to entity entity.
Definition command_buffer.h:151
void commit()
Commits all queued changes.
Definition command_buffer.h:366
GAIA_NODISCARD Entity add(EntityKind kind=EntityKind::EK_Gen)
Requests a new entity to be created.
Definition command_buffer.h:104
void add(Entity entity, T &&value)
Requests a component T to be added to entity. Also sets its value.
Definition command_buffer.h:165
GAIA_NODISCARD Entity copy(Entity entityFrom)
Requests a new entity to be created by cloning an already existing entity.
Definition command_buffer.h:115
void del(Entity entity)
Requests removal of component T from entity.
Definition command_buffer.h:212
void set(Entity entity, T &&value)
Requests component data to be set to given values for a given entity.
Definition command_buffer.h:185
void del(Entity entity, Entity object)
Requests removal of entity object from entity entity.
Definition command_buffer.h:225
void del(Entity entity, const Pair &pair)
Requests removal of a relationship pair from entity entity.
Definition command_buffer.h:234
Wrapper for two Entities forming a relationship pair.
Definition id.h:529
Wrapper for two types forming a relationship pair. Depending on what types are used to form a pair it...
Definition id.h:224
Definition spinlock.h:8
Default in-memory binary backend used by ECS world/runtime serialization. Provides aligned raw read/w...
Definition ser_binary.h:12
uint32_t tell() const
Returns current stream cursor position in bytes.
Definition ser_binary.h:48
void reset()
Clears buffered data and resets stream position.
Definition ser_binary.h:43
Checks if endianess was detected correctly at compile-time.
Definition bitset.h:9
RAII helper that calls lock() on construction and unlock() on destruction.
Definition utility.h:186
Definition command_buffer.h:26
Definition command_buffer.h:21
cnt::paged_ilist< EntityContainer, Entity > entities
Implicit list of entities. Used for look-ups only when searching for entities in chunks + data valida...
Definition entity_container.h:145
IdentifierData tmp
0-real entity, 1-temporary entity
Definition id.h:269
Definition id.h:247