1#[macro_export]
30macro_rules! define_component_storage {
31 (
32 storage: $storage:ident,
33 slot: $slot:ident,
34 $( $field:ident => $ty:path, $disc:expr ),+ $(,)?
35 ) => {
36 #[expect(non_snake_case, reason = "field columns take the caller's idents")]
42 #[derive(Default, Debug)]
43 pub struct $storage {
44 $(
45 pub $field: $crate::ecs::Column<$ty>,
47 )+
48 entities: $crate::ecs::Entities,
49 change_tick: $crate::ecs::AtomicTick,
50 join: $crate::ecs::JoinIndex,
51 }
52
53 impl $storage {
54 pub fn push_typed<C: $slot>(&mut self, c: C) -> $crate::ecs::Entity {
57 let entity = self.entities.alloc();
58 let tick = self.change_tick.bump();
59 let col = C::slot_mut(self);
60 col.push(entity, c, tick);
61 let row = (col.len() - 1) as u32;
62 self.join.set(entity, $crate::ecs::ComponentId::new(C::DISCRIMINANT), row);
63 entity
64 }
65
66 pub fn reserve(&mut self, component: $crate::ecs::ComponentId, additional: usize) {
69 $(
70 if component == $crate::ecs::ComponentId::new($disc) {
71 self.$field.reserve(additional);
72 return;
73 }
74 )+
75 }
76
77 pub fn spawn(&mut self) -> $crate::ecs::Entity {
80 self.entities.alloc()
81 }
82
83 pub fn is_alive(&self, entity: $crate::ecs::Entity) -> bool {
85 self.entities.is_alive(entity)
86 }
87
88 pub fn insert_typed<C: $slot>(&mut self, entity: $crate::ecs::Entity, c: C) {
93 let id = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
94 debug_assert!(
95 self.entities.is_alive(entity),
96 "insert_typed on a despawned entity",
97 );
98 debug_assert!(
99 self.join.row(entity, id).is_none(),
100 "insert_typed: entity already has this component",
101 );
102 let tick = self.change_tick.bump();
103 let col = C::slot_mut(self);
104 col.push(entity, c, tick);
105 let row = (col.len() - 1) as u32;
106 self.join.set(entity, id, row);
107 }
108
109 pub fn remove_typed<C: $slot>(&mut self, entity: $crate::ecs::Entity) -> Option<C> {
114 let id = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
115 let row = self.join.row(entity, id)? as usize;
116 let tick = self.change_tick.bump();
117 let col = C::slot_mut(self);
118 let last = col.len() - 1;
119 let moved = if row != last { Some(col.entities()[last]) } else { None };
120 let value = col.swap_remove(row, tick);
121 self.join.clear(entity, id);
122 if let Some(moved) = moved {
123 self.join.set(moved, id, row as u32);
124 }
125 Some(value)
126 }
127
128 pub fn despawn(&mut self, entity: $crate::ecs::Entity) {
132 if !self.entities.is_alive(entity) {
133 return;
134 }
135 let tick = self.change_tick.bump();
136 $(
137 {
138 let id = $crate::ecs::ComponentId::new(<$ty as $slot>::DISCRIMINANT);
139 if let Some(row) = self.join.row(entity, id) {
140 let row = row as usize;
141 let col = &mut self.$field;
142 let last = col.len() - 1;
143 let moved =
144 if row != last { Some(col.entities()[last]) } else { None };
145 col.swap_remove(row, tick);
146 if let Some(moved) = moved {
147 self.join.set(moved, id, row as u32);
148 }
149 }
150 }
151 )+
152 self.join.clear_entity(entity);
153 self.entities.despawn(entity);
154 }
155
156 pub fn drain<C: $slot>(&mut self) -> ::alloc::vec::Vec<C> {
163 let id = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
164 let owners = C::slot(self).entities().to_vec();
165 let tick = self.change_tick.bump();
166 let drained = C::slot_mut(self).drain(tick);
167 for entity in owners {
168 self.join.clear(entity, id);
169 if self.join.mask(entity).is_empty() {
170 self.entities.despawn(entity);
171 }
172 }
173 drained
174 }
175
176 pub fn values_mut<C: $slot>(&mut self) -> &mut [C] {
179 let tick = self.change_tick.bump();
180 C::slot_mut(self).values_mut(tick)
181 }
182
183 pub fn values_mut_with_entities<C: $slot>(
187 &mut self,
188 ) -> impl Iterator<Item = ($crate::ecs::Entity, &mut C)> {
189 let tick = self.change_tick.bump();
190 C::slot_mut(self).iter_mut_with_entities(tick)
191 }
192
193 pub fn changed_tick<C: $slot>(&self) -> $crate::ecs::Tick {
197 C::slot(self).changed_tick()
198 }
199
200 pub fn column_ticks<C: $slot>(&self) -> $crate::ecs::ColumnTicks {
205 C::slot(self).ticks()
206 }
207
208 pub fn changed_rows<C: $slot>(
215 &self,
216 since: $crate::ecs::Tick,
217 ) -> impl Iterator<Item = ($crate::ecs::Entity, &C)> {
218 C::slot(self).changed_rows(since.clamp_to(self.change_tick.get()))
219 }
220
221 pub fn get<C: $slot>(&self, entity: $crate::ecs::Entity) -> Option<&C> {
223 let row = self.join.row(entity, $crate::ecs::ComponentId::new(C::DISCRIMINANT))?;
224 C::slot(self).get(row as usize)
225 }
226
227 pub fn get_mut<C: $slot>(&mut self, entity: $crate::ecs::Entity) -> Option<&mut C> {
231 let id = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
232 let row = self.join.row(entity, id)? as usize;
233 let tick = self.change_tick.bump();
234 C::slot_mut(self).value_mut(row, tick)
235 }
236
237 pub fn join2<'s, A: $slot, B: $slot>(
243 &'s self,
244 ) -> impl Iterator<Item = ($crate::ecs::Entity, &'s A, &'s B)> + 's {
245 let bid = $crate::ecs::ComponentId::new(B::DISCRIMINANT);
246 let bcol = B::slot(self);
247 A::slot(self)
248 .iter_with_entities()
249 .filter_map(move |(entity, a)| {
250 let brow = self.join.row(entity, bid)? as usize;
251 let b = bcol.get(brow);
252 debug_assert!(
253 b.is_some(),
254 "join2: stale JoinIndex row for an entity's component",
255 );
256 Some((entity, a, b?))
257 })
258 }
259
260 pub fn join3<'s, A: $slot, B: $slot, C: $slot>(
262 &'s self,
263 ) -> impl Iterator<Item = ($crate::ecs::Entity, &'s A, &'s B, &'s C)> + 's {
264 let bid = $crate::ecs::ComponentId::new(B::DISCRIMINANT);
265 let cid = $crate::ecs::ComponentId::new(C::DISCRIMINANT);
266 let bcol = B::slot(self);
267 let ccol = C::slot(self);
268 A::slot(self)
269 .iter_with_entities()
270 .filter_map(move |(entity, a)| {
271 let brow = self.join.row(entity, bid)? as usize;
272 let crow = self.join.row(entity, cid)? as usize;
273 let b = bcol.get(brow);
274 let c = ccol.get(crow);
275 debug_assert!(
276 b.is_some() && c.is_some(),
277 "join3: stale JoinIndex row for an entity's component",
278 );
279 Some((entity, a, b?, c?))
280 })
281 }
282
283 pub fn len(&self) -> usize {
285 0 $( + self.$field.len() )+
286 }
287
288 pub fn is_empty(&self) -> bool {
290 true $( && self.$field.is_empty() )+
291 }
292 }
293
294 pub trait $slot: Sized + 'static {
301 const DISCRIMINANT: u8;
303 fn slot(s: &$storage) -> &$crate::ecs::Column<Self>;
305 fn slot_mut(s: &mut $storage) -> &mut $crate::ecs::Column<Self>;
307 }
308
309 $(
310 impl $slot for $ty {
311 const DISCRIMINANT: u8 = $disc;
312 fn slot(s: &$storage) -> &$crate::ecs::Column<Self> { &s.$field }
313 fn slot_mut(s: &mut $storage) -> &mut $crate::ecs::Column<Self> { &mut s.$field }
314 }
315 const _: () = assert!(
319 $disc <= $crate::ecs::ComponentId::MAX,
320 "component discriminant exceeds the 127-bit ComponentMask ceiling",
321 );
322 )+
323 };
324}
325
326#[cfg(test)]
327mod tests {
328 #![expect(
334 dead_code,
335 unreachable_pub,
336 reason = "TestStorage is module-private, so the generated pub items are unreachable and dead_code fires on whichever ones these tests skip"
337 )]
338
339 use std::vec;
340 use std::vec::Vec;
341 #[derive(Default, Debug, PartialEq, Clone, Copy)]
344 #[expect(
345 unreachable_pub,
346 reason = "pub so the generated pub columns do not expose a more-private type"
347 )]
348 pub struct Position(u32);
349
350 #[derive(Default, Debug, PartialEq, Clone, Copy)]
351 #[expect(
352 unreachable_pub,
353 reason = "pub so the generated pub columns do not expose a more-private type"
354 )]
355 pub struct Velocity(i32);
356
357 #[derive(Default, Debug, PartialEq, Clone, Copy)]
358 #[expect(
359 unreachable_pub,
360 reason = "pub so the generated pub columns do not expose a more-private type"
361 )]
362 pub struct Tag;
363
364 define_component_storage! {
365 storage: TestStorage,
366 slot: TestSlot,
367 Position => Position, 1,
368 Velocity => Velocity, 2,
369 Tag => Tag, 3,
370 }
371
372 #[test]
375 fn reserve_presizes_the_addressed_column() {
376 let mut s = TestStorage::default();
377 s.reserve(crate::ecs::ComponentId::new(1), 64);
378 assert!(s.Position.capacity() >= 64);
379 assert_eq!(s.Velocity.capacity(), 0, "other columns untouched");
380 s.reserve(crate::ecs::ComponentId::new(99), 64); s.push_typed(Position(1));
382 assert!(s.Position.capacity() >= 64);
383 assert_eq!(s.len(), 1);
384 }
385
386 #[test]
387 fn push_count_mutate_drain() {
388 let mut s = TestStorage::default();
389 assert!(s.is_empty());
390 assert_eq!(s.len(), 0);
391
392 s.push_typed(Position(1));
393 s.push_typed(Position(2));
394 s.push_typed(Velocity(-3));
395 assert!(!s.is_empty());
396 assert_eq!(s.len(), 3);
397
398 for p in s.values_mut::<Position>() {
400 p.0 += 10;
401 }
402
403 assert_eq!(s.drain::<Position>(), vec![Position(11), Position(12)]);
405 assert_eq!(s.len(), 1);
406 assert_eq!(s.drain::<Velocity>(), vec![Velocity(-3)]);
407 assert!(s.is_empty());
408 }
409
410 #[test]
411 fn columns_carry_row_aligned_entities() {
412 let mut s = TestStorage::default();
413 let a = s.push_typed(Position(7));
414 let b = s.push_typed(Position(8));
415 let entities = <Position as TestSlot>::slot(&s).entities();
417 assert_eq!(entities, &[a, b]);
418 assert_ne!(a, b);
419 }
420
421 #[test]
422 fn insert_puts_two_components_on_one_entity() {
423 let mut s = TestStorage::default();
424 let e = s.push_typed(Position(5));
426 s.insert_typed(e, Velocity(-2));
429 s.insert_typed(e, Tag);
430
431 let joined: Vec<_> = s.join2::<Position, Velocity>().collect();
432 assert_eq!(joined.len(), 1);
433 assert_eq!(joined[0], (e, &Position(5), &Velocity(-2)));
434
435 let joined3: Vec<_> = s.join3::<Position, Velocity, Tag>().collect();
436 assert_eq!(joined3.len(), 1);
437 assert_eq!(joined3[0], (e, &Position(5), &Velocity(-2), &Tag));
438 }
439
440 #[test]
441 fn join2_only_matches_entities_with_both() {
442 let mut s = TestStorage::default();
443 let a = s.push_typed(Position(1));
444 s.insert_typed(a, Velocity(10));
445 let _b = s.push_typed(Position(2));
447 let c = s.push_typed(Position(3));
448 s.insert_typed(c, Velocity(30));
449
450 let mut joined: Vec<_> = s
451 .join2::<Position, Velocity>()
452 .map(|(e, p, v)| (e, *p, *v))
453 .collect();
454 joined.sort_by_key(|(e, _, _)| e.index());
455 assert_eq!(
456 joined,
457 vec![
458 (a, Position(1), Velocity(10)),
459 (c, Position(3), Velocity(30))
460 ]
461 );
462 }
463
464 #[test]
465 fn remove_typed_patches_the_moved_tail_row() {
466 let mut s = TestStorage::default();
467 let a = s.push_typed(Velocity(1));
470 let b = s.push_typed(Velocity(2));
471 let c = s.push_typed(Velocity(3));
472
473 let removed = s.remove_typed::<Velocity>(b);
474 assert_eq!(removed, Some(Velocity(2)));
475 let joined: std::collections::HashMap<_, _> = s
478 .join2::<Velocity, Velocity>() .map(|(e, v, _)| (e, *v))
480 .collect();
481 assert_eq!(joined.get(&a), Some(&Velocity(1)));
482 assert_eq!(joined.get(&c), Some(&Velocity(3)));
483 assert_eq!(joined.get(&b), None);
484 assert_eq!(s.len(), 2);
485 }
486
487 #[test]
488 fn remove_typed_returns_none_when_absent() {
489 let mut s = TestStorage::default();
490 let e = s.push_typed(Position(1));
491 assert_eq!(s.remove_typed::<Velocity>(e), None);
493 let bare = s.spawn();
495 assert_eq!(s.remove_typed::<Position>(bare), None);
496 assert_eq!(s.len(), 1);
497 }
498
499 #[test]
500 fn remove_typed_last_row_takes_the_no_move_branch() {
501 let mut s = TestStorage::default();
502 let a = s.push_typed(Velocity(1));
503 let b = s.push_typed(Velocity(2));
504 let c = s.push_typed(Velocity(3));
505 assert_eq!(s.remove_typed::<Velocity>(c), Some(Velocity(3)));
507 let joined: std::collections::HashMap<_, _> = s
508 .join2::<Velocity, Velocity>()
509 .map(|(e, v, _)| (e, *v))
510 .collect();
511 assert_eq!(joined.get(&a), Some(&Velocity(1)));
512 assert_eq!(joined.get(&b), Some(&Velocity(2)));
513 assert_eq!(joined.get(&c), None);
514 assert_eq!(s.len(), 2);
515 }
516
517 #[test]
518 fn remove_one_component_keeps_siblings_on_a_multi_component_entity() {
519 let mut s = TestStorage::default();
520 let a = s.push_typed(Position(1));
523 s.insert_typed(a, Velocity(10));
524 s.insert_typed(a, Tag);
525 let b = s.push_typed(Position(2));
526 s.insert_typed(b, Velocity(20));
527
528 assert_eq!(s.remove_typed::<Velocity>(a), Some(Velocity(10)));
529 assert!(s.is_alive(a));
530 let pos_tag: Vec<_> = s
532 .join2::<Position, Tag>()
533 .map(|(e, p, _)| (e, *p))
534 .collect();
535 assert_eq!(pos_tag, vec![(a, Position(1))]);
536 let pos_vel: Vec<_> = s
537 .join2::<Position, Velocity>()
538 .map(|(e, p, v)| (e, *p, *v))
539 .collect();
540 assert_eq!(pos_vel, vec![(b, Position(2), Velocity(20))]);
541 }
542
543 #[test]
544 fn remove_then_reinsert_same_component_on_live_entity() {
545 let mut s = TestStorage::default();
546 let a = s.push_typed(Position(1));
547 let b = s.push_typed(Position(2));
548 let _c = s.push_typed(Position(3));
549 s.insert_typed(b, Velocity(20));
550
551 assert_eq!(s.remove_typed::<Velocity>(b), Some(Velocity(20)));
554 assert!(s.is_alive(b));
555 s.insert_typed(b, Velocity(21));
556
557 let joined: std::collections::HashMap<_, _> = s
558 .join2::<Position, Velocity>()
559 .map(|(e, p, v)| (e, (*p, *v)))
560 .collect();
561 assert_eq!(joined.get(&b), Some(&(Position(2), Velocity(21))));
562 assert_eq!(joined.get(&a), None);
563 }
564
565 #[test]
566 fn drain_one_type_keeps_shared_entities_and_their_other_components() {
567 let mut s = TestStorage::default();
568 let shared = s.push_typed(Position(1));
570 s.insert_typed(shared, Velocity(99));
571 let solo = s.push_typed(Position(2));
572
573 let drained = s.drain::<Position>();
574 assert_eq!(drained.len(), 2);
575 assert!(!s.is_alive(solo));
577 assert!(s.is_alive(shared));
580 let vels: Vec<_> = s
581 .join2::<Velocity, Velocity>()
582 .map(|(e, v, _)| (e, *v))
583 .collect();
584 assert_eq!(vels, vec![(shared, Velocity(99))]);
585 assert_eq!(s.len(), 1);
586 assert_eq!(s.drain::<Velocity>(), vec![Velocity(99)]);
588 assert!(!s.is_alive(shared));
589 assert!(s.is_empty());
590 }
591
592 #[test]
593 fn despawn_removes_all_components_and_patches_tails() {
594 let mut s = TestStorage::default();
595 let e1 = s.push_typed(Position(1));
598 s.insert_typed(e1, Velocity(11));
599 s.insert_typed(e1, Tag);
600 let e2 = s.push_typed(Position(2));
601 s.insert_typed(e2, Velocity(22));
602
603 s.despawn(e1);
604 assert!(!s.is_alive(e1));
605 assert!(s.is_alive(e2));
606
607 let joined: Vec<_> = s.join2::<Position, Velocity>().collect();
609 assert_eq!(joined, vec![(e2, &Position(2), &Velocity(22))]);
610 assert_eq!(<Position as TestSlot>::slot(&s).len(), 1);
612 assert_eq!(<Velocity as TestSlot>::slot(&s).len(), 1);
613 assert_eq!(<Tag as TestSlot>::slot(&s).len(), 0);
614 }
615
616 #[test]
617 fn despawn_is_a_noop_on_a_stale_handle() {
618 let mut s = TestStorage::default();
619 let e = s.push_typed(Position(1));
620 s.despawn(e);
621 s.despawn(e);
623 assert_eq!(s.len(), 0);
624 }
625
626 #[test]
627 fn get_and_get_mut_address_one_entity() {
628 let mut s = TestStorage::default();
629 let a = s.push_typed(Position(1));
630 let b = s.push_typed(Position(2));
631 s.insert_typed(a, Velocity(10));
632
633 assert_eq!(s.get::<Position>(a), Some(&Position(1)));
634 assert_eq!(s.get::<Position>(b), Some(&Position(2)));
635 assert_eq!(s.get::<Velocity>(a), Some(&Velocity(10)));
636 assert_eq!(s.get::<Velocity>(b), None);
638
639 if let Some(p) = s.get_mut::<Position>(b) {
640 p.0 = 99;
641 }
642 assert_eq!(s.get::<Position>(b), Some(&Position(99)));
643 assert_eq!(s.get::<Position>(a), Some(&Position(1)));
645 }
646
647 #[test]
651 fn changed_rows_reports_only_the_row_get_mut_touched() {
652 let mut s = TestStorage::default();
653 let _a = s.push_typed(Position(1));
654 let _b = s.push_typed(Position(2));
655 let c = s.push_typed(Position(3));
656 let before = s.column_ticks::<Position>();
657
658 s.get_mut::<Position>(c).unwrap().0 = 30;
659
660 let seen: Vec<(crate::ecs::Entity, u32)> = s
661 .changed_rows::<Position>(before.changed)
662 .map(|(e, p)| (e, p.0))
663 .collect();
664 assert_eq!(seen, vec![(c, 30)]);
665
666 let after = s.column_ticks::<Position>();
667 assert!(after.changed.is_newer_than(before.changed));
668 assert_eq!(
669 after.bulk, before.bulk,
670 "a targeted write is not a bulk one"
671 );
672 assert_eq!(after.structural, before.structural, "nor a structural one");
673 }
674
675 #[test]
679 fn a_bulk_write_moves_only_the_bulk_stamp() {
680 let mut s = TestStorage::default();
681 s.push_typed(Position(1));
682 s.push_typed(Position(2));
683 let before = s.column_ticks::<Position>();
684
685 for p in s.values_mut::<Position>() {
686 p.0 += 10;
687 }
688
689 let after = s.column_ticks::<Position>();
690 assert!(after.bulk.is_newer_than(before.bulk));
691 assert_eq!(after.structural, before.structural);
692 assert_eq!(
693 s.changed_rows::<Position>(before.changed).count(),
694 0,
695 "per-row stamps cannot describe a bulk write",
696 );
697 }
698
699 #[test]
703 fn push_and_remove_move_the_structural_stamp() {
704 let mut s = TestStorage::default();
705 let a = s.push_typed(Position(1));
706 let before = s.column_ticks::<Position>();
707
708 s.get_mut::<Position>(a).unwrap().0 = 5;
709 assert_eq!(s.column_ticks::<Position>().structural, before.structural);
710
711 let b = s.push_typed(Position(2));
712 let grown = s.column_ticks::<Position>();
713 assert!(grown.structural.is_newer_than(before.structural));
714
715 s.remove_typed::<Position>(b);
716 assert!(
717 s.column_ticks::<Position>()
718 .structural
719 .is_newer_than(grown.structural)
720 );
721 }
722
723 #[test]
728 fn changed_rows_pulls_a_stale_since_forward() {
729 let mut s = TestStorage::default();
730 let a = s.push_typed(Position(1));
731 let b = s.push_typed(Position(2));
732
733 let stale = crate::ecs::Tick(2_000_000_000);
736 assert!(
737 !crate::ecs::Tick(1).is_newer_than(stale),
738 "unclamped, the comparison aliases and drops these rows",
739 );
740
741 let seen: Vec<crate::ecs::Entity> =
742 s.changed_rows::<Position>(stale).map(|(e, _)| e).collect();
743 assert_eq!(
744 seen,
745 vec![a, b],
746 "the clamp makes a stale window over-report"
747 );
748 }
749
750 #[test]
751 fn spawn_makes_a_bare_entity_for_later_inserts() {
752 let mut s = TestStorage::default();
753 let e = s.spawn();
754 assert!(s.is_alive(e));
755 assert_eq!(s.len(), 0);
756 s.insert_typed(e, Position(9));
757 s.insert_typed(e, Velocity(-9));
758 let joined: Vec<_> = s.join2::<Position, Velocity>().collect();
759 assert_eq!(joined, vec![(e, &Position(9), &Velocity(-9))]);
760 }
761
762 #[test]
763 fn recycled_entity_index_does_not_report_stale_components() {
764 let mut s = TestStorage::default();
765 let a = s.push_typed(Position(1));
766 s.insert_typed(a, Velocity(1));
767 s.despawn(a);
768 let b = s.spawn();
771 assert_eq!(a.index(), b.index());
772 s.insert_typed(b, Position(2));
773 let joined: Vec<_> = s.join2::<Position, Velocity>().collect();
774 assert!(
775 joined.is_empty(),
776 "b has no Velocity; stale join must not match"
777 );
778 let positions: Vec<_> = s
779 .join2::<Position, Position>()
780 .map(|(e, p, _)| (e, *p))
781 .collect();
782 assert_eq!(positions, vec![(b, Position(2))]);
783 }
784}