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
126 template <typename T>
127 void add(Entity entity) {
128 verify_comp<T>();
129 core::lock_scope lock(m_acc);
130
131 // Make sure the component is registered
132 const auto& item = comp_cache_add<T>(m_world);
133
134 push_op({OpType::ADD_COMPONENT, 0, entity, item.entity});
135 }
136
140 void add(Entity entity, Entity other) {
141 core::lock_scope lock(m_acc);
142
143 push_op({OpType::ADD_COMPONENT, 0, entity, other});
144 }
145
152 template <typename T>
153 void add(Entity entity, T&& value) {
154 verify_comp<T>();
155 core::lock_scope lock(m_acc);
156
157 // Make sure the component is registered
158 const auto& item = comp_cache_add<T>(m_world);
159
160 const auto pos = m_data.tell();
161 auto serializer = ser::make_serializer(m_data);
162 item.save(serializer, &value, 0, 1, 1);
163 push_op({OpType::ADD_COMPONENT_DATA, pos, entity, item.entity});
164 }
165
172 template <typename T>
173 void set(Entity entity, T&& value) {
174 verify_comp<T>();
175 core::lock_scope lock(m_acc);
176
177 // Make sure the component is registered
178 const auto& item = comp_cache(m_world).template get<T>();
179
180 const auto pos = m_data.tell();
181 auto serializer = ser::make_serializer(m_data);
182 item.save(serializer, &value, 0, 1, 1);
183 push_op({OpType::SET_COMPONENT, pos, entity, item.entity});
184 }
185
188 void del(Entity entity) {
189 core::lock_scope lock(m_acc);
190
191 push_op({OpType::DEL_ENTITY, 0, entity, EntityBad});
192 }
193
199 template <typename T>
200 void del(Entity entity) {
201 verify_comp<T>();
202 core::lock_scope lock(m_acc);
203
204 // Make sure the component is registered
205 const auto& item = comp_cache(m_world).template get<T>();
206
207 push_op({OpType::DEL_COMPONENT, 0, entity, item.entity});
208 }
209
213 void del(Entity entity, Entity object) {
214 core::lock_scope lock(m_acc);
215
216 push_op({OpType::DEL_COMPONENT, 0, entity, object});
217 }
218
219 private:
221 GAIA_NODISCARD bool is_rel(OpType t) const {
222 return (uint32_t)t >= (uint32_t)OpType::ADD_COMPONENT;
223 }
224
226 GAIA_NODISCARD bool is_tmp(Entity e) const {
227 return e.data.tmp != 0;
228 }
229
231 GAIA_NODISCARD Entity resolve(Entity e) const {
232 if (!is_tmp(e))
233 return e;
234
235 const auto ti = e.id();
236 if (ti < m_temp2real.size())
237 return m_temp2real[ti];
238
239 return EntityBad;
240 }
241
244 GAIA_NODISCARD bool is_canceled_temp(uint32_t idx) const {
245 const uint32_t base = idx * 3;
246 if (base + 2 >= m_tmpFlags.size())
247 return false;
248
249 const bool destroy = m_tmpFlags.test(base);
250 const bool usedOther = m_tmpFlags.test(base + 2);
251
252 // If deleted in this batch and not referenced by others, cancel entirely
253 return destroy && !usedOther;
254 }
255
257 GAIA_NODISCARD Entity add_temp(EntityKind kind) {
258 m_temp2real.push_back(EntityBad);
259 Entity tmp(m_nextTemp++, 0, true, false, kind);
260 // Use the unused flag to mark temporary entities
261 tmp.data.tmp = 1;
262 return tmp;
263 }
264
265 GAIA_NODISCARD bool less_target(Entity a, Entity b) const {
266 const bool ta = is_tmp(a);
267 const bool tb = is_tmp(b);
268
269 // Real entities always come first in order. Temps come last.
270 if (ta != tb)
271 return !ta && tb;
272
273 // Within same domain, normal numeric order.
274 return a.id() < b.id();
275 }
276
278 void check_sort(const Op& op) {
279 if (is_tmp(op.target)) {
280 if (m_haveTemp) {
281 // Only compare temp indices against previous temp target
282 const uint32_t prev = m_lastTempTarget.id();
283 const uint32_t curr = op.target.id();
284 if (curr < prev)
285 m_needsSort = true;
286 }
287 m_lastTempTarget = op.target;
288 m_haveTemp = true;
289 } else {
290 if (m_haveReal) {
291 // Only compare real IDs against previous real target
292 if (op.target.id() < m_lastRealTarget.id())
293 m_needsSort = true;
294 }
295 m_lastRealTarget = op.target;
296 m_haveReal = true;
297 }
298 }
299
301 void push_op(Op&& op) {
302 check_sort(op);
303 m_ops.push_back(GAIA_MOV(op));
304 }
305
307 void clear() {
308 m_ops.clear();
309 m_temp2real.clear();
310 m_tmpFlags.reset();
311 m_nextTemp = 0;
312 m_data.reset();
313
314 m_needsSort = false;
315 m_haveReal = false;
316 m_haveTemp = false;
317 m_lastRealTarget = EntityBad;
318 m_lastTempTarget = EntityBad;
319 }
320
321 public:
323 void commit() {
324 core::lock_scope lock(m_acc);
325
326 if (m_ops.empty())
327 return;
328
329 GAIA_PROF_SCOPE(cmdbuf::commit);
330
331 // Build flags + allocate entities
332 if (m_nextTemp > 0) {
333 GAIA_PROF_SCOPE(cmdbuf::alloc);
334
335 // Bit layout for each temporary entity:
336 // bit 0 -> marked for destruction (DEL_ENTITY recorded)
337 // bit 1 -> used as a relation target (appeared as target in component ops)
338 // bit 2 -> used as a relation source / dependency (appeared as other in component ops)
339 m_tmpFlags.resize(m_nextTemp * 3);
340
341 // Pre-map surviving temps for ADD/CPY (avoid allocating canceled temps)
342 if (m_temp2real.size() < m_nextTemp) {
343 const auto from = m_temp2real.size();
344 m_temp2real.resize(m_nextTemp);
345 GAIA_FOR2(from, m_nextTemp) m_temp2real[i] = EntityBad;
346 }
347
348 // Build all flags
349 for (const Op& o: m_ops) {
350 // Set flag bits
351 if (is_tmp(o.target)) {
352 const uint32_t ti = o.target.id();
353
354 if (o.type == OpType::DEL_ENTITY)
355 m_tmpFlags.set((ti * 3) + 0, true);
356 else if (is_rel(o.type))
357 m_tmpFlags.set((ti * 3) + 1, true);
358 }
359
360 if (is_tmp(o.other) && o.other.id() < m_tmpFlags.size())
361 m_tmpFlags.set((o.other.id() * 3) + 2, true);
362 }
363
364 // Allocate real entities for the surviving temporaries
365 for (const Op& o: m_ops) {
366 if (!is_tmp(o.target))
367 continue;
368
369 const uint32_t ti = o.target.id();
370 if (is_canceled_temp(ti))
371 continue;
372
373 if (o.type == OpType::ADD_ENTITY) {
374 if (m_temp2real[ti] == EntityBad)
375 m_temp2real[ti] = m_world.add(o.target.kind());
376 } else if (o.type == OpType::CPY_ENTITY) {
377 if (m_temp2real[ti] == EntityBad) {
378 const Entity src = resolve(o.other);
379 if (src != EntityBad)
380 m_temp2real[ti] = m_world.copy(src);
381 }
382 }
383 }
384 }
385
386 // Sort by (target, other), reduce last-wins, apply relations.
387 // DEL_COMPONENT last per target.
388 if (m_needsSort) {
389 GAIA_PROF_SCOPE(cmdbuf::sort);
390
391 m_needsSort = false;
392 core::sort(m_ops.begin(), m_ops.end(), [](const Op& a, const Op& b) {
393 if (a.target != b.target)
394 return a.target < b.target;
395 if (a.other != b.other)
396 return a.other < b.other;
397 return false;
398 });
399 }
400
401 // Replay batched operations
402 Entity lastKey = EntityBad;
403 Entity lastResolved = EntityBad;
404 auto resolve_cached = [&](Entity e) {
405 if (e == lastKey)
406 return lastResolved;
407 lastKey = e;
408 return lastResolved = resolve(e);
409 };
410
411 {
412 GAIA_PROF_SCOPE(cmdbuf::merges);
413 for (uint32_t p = 0; p < m_ops.size();) {
414 GAIA_PROF_SCOPE(cmdbuf::merge);
415
416 const Entity tgtKey = m_ops[p].target;
417
418 const bool tgtIsTemp = is_tmp(tgtKey);
419 const uint32_t ti = tgtIsTemp ? tgtKey.id() : 0u;
420 const Entity tgtReal =
421 tgtIsTemp ? (ti < m_temp2real.size() ? m_temp2real[ti] : EntityBad) : resolve_cached(tgtKey);
422
423 // Range for this target
424 uint32_t q = p;
425 bool hasDelEntity = false;
426 while (q < m_ops.size() && m_ops[q].target == tgtKey) {
427 if (m_ops[q].type == OpType::DEL_ENTITY)
428 hasDelEntity = true;
429 ++q;
430 }
431
432 // Skip canceled or non-existent temporary entities
433 if (tgtReal == EntityBad) {
434 p = q;
435 continue;
436 }
437 if (tgtIsTemp && is_canceled_temp(ti)) {
438 p = q;
439 continue;
440 }
441
442 enum : uint8_t { F_ADD = 1 << 0, F_ADD_DATA = 1 << 1, F_SET = 1 << 2, F_DEL = 1 << 3 };
443
444 // Emit relation groups.
445 // Inside [p..q) range (same target), process groups by 'other'.
446 // We perform per-component reduction.
447 for (uint32_t i = p; i < q;) {
448 const Entity othKey = m_ops[i].other;
449 const Entity othReal = resolve_cached(othKey);
450
451 // Group ops with same (target, other)
452 uint32_t j = i + 1;
453 while (j < q && m_ops[j].other == othKey)
454 ++j;
455
456 if (tgtReal != EntityBad) {
457 const uint32_t groupSize = j - i;
458 // Fast path - single op
459 if (groupSize == 1) {
460 const Op& op = m_ops[i];
461 switch (op.type) {
462 case OpType::DEL_COMPONENT:
463 World::EntityBuilder(m_world, tgtReal).del(othReal);
464 break;
465 case OpType::ADD_COMPONENT:
466 World::EntityBuilder(m_world, tgtReal).add(othReal);
467 break;
468 case OpType::ADD_COMPONENT_DATA:
469 World::EntityBuilder(m_world, tgtReal).add(othReal);
470 GAIA_FALLTHROUGH;
471 case OpType::SET_COMPONENT: {
472 const auto& ec = m_world.m_recs.entities[tgtReal.id()];
473 const auto row = tgtReal.kind() == EntityKind::EK_Uni ? 0U : ec.row;
474 const auto compIdx = ec.pChunk->comp_idx(othReal);
475 auto* pComponentData = (void*)ec.pChunk->comp_ptr_mut(compIdx, 0);
476
477 // Component data
478 auto serializer = ser::make_serializer(m_data);
479 serializer.seek(op.off);
480 const auto& item = m_world.comp_cache().get(othReal);
481 item.load(serializer, pComponentData, row, row + 1, ec.pChunk->capacity());
482 } break;
483 default:
484 break;
485 }
486 }
487 // Slow path: merge multiple ops
488 else {
489 uint8_t mask = 0;
490 uint32_t dataPos = 0;
491
492 for (uint32_t k = i; k < j; ++k) {
493 const Op& op = m_ops[k];
494 switch (op.type) {
495 case OpType::ADD_COMPONENT:
496 mask |= F_ADD;
497 break;
498 case OpType::ADD_COMPONENT_DATA:
499 mask |= F_ADD_DATA;
500 dataPos = op.off;
501 break;
502 case OpType::SET_COMPONENT:
503 mask |= F_SET;
504 dataPos = op.off;
505 break;
506 case OpType::DEL_COMPONENT:
507 mask |= F_DEL;
508 break;
509 default:
510 break;
511 }
512 }
513
514 const bool hasAdd = mask & F_ADD;
515 const bool hasAddData = mask & F_ADD_DATA;
516 const bool hasSet = mask & F_SET;
517 const bool hasDel = mask & F_DEL;
518
519 // 1) ADD(+DATA) + DEL = no-op
520 if (hasDel && (hasAdd || hasAddData)) {
521 }
522 // 2) DEL only
523 else if (hasDel) {
524 World::EntityBuilder(m_world, tgtReal).del(othReal);
525 }
526 // 3) ADD_WITH_DATA or ADD+SET = ADD_WITH_DATA
527 else if (hasAddData || (hasAdd && hasSet)) {
528 World::EntityBuilder(m_world, tgtReal).add(othReal);
529
530 const auto& ec = m_world.m_recs.entities[tgtReal.id()];
531 const auto row = tgtReal.kind() == EntityKind::EK_Uni ? 0U : ec.row;
532 const auto compIdx = ec.pChunk->comp_idx(othReal);
533 auto* pComponentData = (void*)ec.pChunk->comp_ptr_mut(compIdx, 0);
534
535 // Component data
536 auto serializer = ser::make_serializer(m_data);
537 serializer.seek(dataPos);
538 const auto& item = m_world.comp_cache().get(othReal);
539 item.load(serializer, pComponentData, row, row + 1, ec.pChunk->capacity());
540 }
541 // 4) ADD only
542 else if (hasAdd) {
543 World::EntityBuilder(m_world, tgtReal).add(othReal);
544 }
545 // 5) SET only
546 else if (hasSet) {
547 const auto& ec = m_world.m_recs.entities[tgtReal.id()];
548 const auto row = tgtReal.kind() == EntityKind::EK_Uni ? 0U : ec.row;
549 const auto compIdx = ec.pChunk->comp_idx(othReal);
550 auto* pComponentData = (void*)ec.pChunk->comp_ptr_mut(compIdx, 0);
551
552 // Component data
553 auto serializer = ser::make_serializer(m_data);
554 serializer.seek(dataPos);
555 const auto& item = m_world.comp_cache().get(othReal);
556 item.load(serializer, pComponentData, row, row + 1, ec.pChunk->capacity());
557 }
558 }
559 }
560
561 // Advance to next component group
562 i = j;
563 }
564
565 // Safely delete entity only if it was actually created
566 if (hasDelEntity)
567 m_world.del(tgtReal);
568
569 // Advance to next target group
570 p = q;
571 }
572 }
573
574 clear();
575 }
576 };
577 } // namespace detail
578
579 using CommandBufferST = detail::CommandBuffer<AccessContextST>;
580 using CommandBufferMT = detail::CommandBuffer<AccessContextMT>;
581
582 inline CommandBufferST* cmd_buffer_st_create(World& world) {
583 return new CommandBufferST(world);
584 }
585 inline void cmd_buffer_destroy(CommandBufferST& cmdBuffer) {
586 delete &cmdBuffer;
587 }
588 inline void cmd_buffer_commit(CommandBufferST& cmdBuffer) {
589 cmdBuffer.commit();
590 }
591
592 inline CommandBufferMT* cmd_buffer_mt_create(World& world) {
593 return new CommandBufferMT(world);
594 }
595 inline void cmd_buffer_destroy(CommandBufferMT& cmdBuffer) {
596 delete &cmdBuffer;
597 }
598 inline void cmd_buffer_commit(CommandBufferMT& cmdBuffer) {
599 cmdBuffer.commit();
600 }
601 } // namespace ecs
602} // namespace gaia
Array of elements of type.
Definition darray_ext_impl.h:28
Definition dbitset.h:15
void reset()
Unsets all bits.
Definition dbitset.h:347
GAIA_NODISCARD constexpr uint32_t size() const
Returns the number of bits the dbitset holds.
Definition dbitset.h:419
GAIA_NODISCARD bool test(uint32_t pos) const
Returns the value of the bit at the position.
Definition dbitset.h:359
void set()
Sets all bits.
Definition dbitset.h:265
GAIA_NODISCARD const ComponentCacheItem & get(detail::ComponentDescId compDescId) const noexcept
Returns the component cache item given the compDescId.
Definition component_cache.h:373
Definition world.h:88
GAIA_NODISCARD const ComponentCache & comp_cache() const
Returns read-only access to the world component cache.
Definition world.h:4036
void del(Entity entity)
Removes an entity along with all data associated with it.
Definition world.h:6048
GAIA_NODISCARD Entity copy(Entity srcEntity)
Creates a new entity by cloning an already existing one. Does not trigger observers.
Definition world.h:4780
GAIA_NODISCARD Entity add(EntityKind kind=EntityKind::EK_Gen)
Creates a new empty entity.
Definition world.h:4410
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:140
void add(Entity entity)
Requests a component T to be added to entity.
Definition command_buffer.h:127
void del(Entity entity)
Requests an existing entity to be removed.
Definition command_buffer.h:188
void add(Entity entity, T &&value)
Requests a component T to be added to entity. Also sets its value.
Definition command_buffer.h:153
void commit()
Commits all queued changes.
Definition command_buffer.h:323
GAIA_NODISCARD Entity add(EntityKind kind=EntityKind::EK_Gen)
Requests a new entity to be created.
Definition command_buffer.h:104
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:200
void set(Entity entity, T &&value)
Requests component data to be set to given values for a given entity.
Definition command_buffer.h:173
void del(Entity entity, Entity object)
Requests removal of entity object from entity entity.
Definition command_buffer.h:213
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
Definition utility.h:123
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:144
IdentifierData tmp
0-real entity, 1-temporary entity
Definition id.h:263
Definition id.h:241
Definition world.h:2870
EntityBuilder & add(Entity entity)
Prepares an archetype movement by following the "add" edge of the current archetype.
Definition world.h:3157
EntityBuilder & del(Entity entity)
Prepares an archetype movement by following the "del" edge of the current archetype.
Definition world.h:3226