khora_data/ecs/page.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16 any::{Any, TypeId},
17 collections::HashMap,
18};
19
20use crate::ecs::EntityId;
21
22/// An internal helper trait to perform vector operations on a type-erased `Box<dyn Any>`.
23///
24/// This allows us to call methods like `swap_remove` on component columns without
25/// needing to know their concrete `Vec<T>` type at compile time.
26pub trait AnyVec {
27 /// Casts the trait object to `&dyn Any`.
28 fn as_any(&self) -> &dyn Any;
29
30 /// Casts the trait object to `&mut dyn Any`.
31 fn as_any_mut(&mut self) -> &mut dyn Any;
32
33 /// Performs a `swap_remove` on the underlying `Vec`, removing the element at `index`.
34 fn swap_remove_any(&mut self, index: usize);
35}
36
37// We implement this trait for any `Vec<T>` where T is `'static`.
38impl<T: 'static> AnyVec for Vec<T> {
39 fn as_any(&self) -> &dyn Any {
40 self
41 }
42
43 fn as_any_mut(&mut self) -> &mut dyn Any {
44 self
45 }
46
47 fn swap_remove_any(&mut self, index: usize) {
48 self.swap_remove(index);
49 }
50}
51
52/// A logical address pointing to an entity's component data within a specific `ComponentPage`.
53///
54/// This struct is the core of the relational aspect of our ECS. It decouples an entity's
55/// identity from the physical storage of its data by acting as a coordinate.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct PageIndex {
58 /// The unique identifier of the `ComponentPage` that stores the component data.
59 pub page_id: u32,
60 /// The index of the row within the page where this entity's components are stored.
61 pub row_index: u32,
62}
63
64/// A page of memory that stores the component data for multiple entities
65/// in a Structure of Arrays (SoA) layout.
66///
67/// A `ComponentPage` is specialized for a single semantic domain (e.g., physics).
68/// It contains multiple columns, where each column is a `Vec<T>` for a specific
69/// component type `T`. This SoA layout is the key to our high iteration performance,
70/// as it guarantees contiguous data access for native queries.
71pub struct ComponentPage {
72 /// A map from a component's `TypeId` to its actual storage column.
73 /// The `Box<dyn AnyVec>` is a type-erased `Vec<T>` that knows how to
74 /// perform basic vector operations like `swap_remove`.
75 pub(crate) columns: HashMap<TypeId, Box<dyn AnyVec>>,
76
77 /// A list of the `EntityId`s that own the data in each row of this page.
78 /// The entity at `entities[i]` corresponds to the components at `columns[...][i]`.
79 /// This is crucial for reverse lookups, especially during entity despawning.
80 pub(crate) entities: Vec<EntityId>,
81
82 /// The sorted list of `TypeId`s for the components stored in this page.
83 /// This acts as the page's "signature" for matching with bundles. It is
84 /// kept sorted to ensure that the signature is canonical.
85 pub(crate) type_ids: Vec<TypeId>,
86}
87
88impl ComponentPage {
89 /// Adds an entity to this page's entity list.
90 ///
91 /// This method is called by `World::spawn` and is a crucial part of maintaining
92 /// the invariant that the number of rows in the component columns is always
93 /// equal to the number of entities tracked by the page.
94 pub(crate) fn add_entity(&mut self, entity_id: EntityId) {
95 self.entities.push(entity_id);
96 }
97
98 /// Performs a `swap_remove` on a specific row across all component columns
99 /// and the entity list.
100 ///
101 /// This is the core of an O(1) despawn operation. It removes the data for the entity
102 /// at `row_index` by swapping it with the last element in each column and in the
103 /// entity list.
104 ///
105 /// It's the caller's (`World::despawn`) responsibility to update the metadata of
106 /// the entity that was moved from the last row.
107 pub(crate) fn swap_remove_row(&mut self, row_index: u32) {
108 // 1. Remove the corresponding entity ID from the list. `swap_remove` on a Vec
109 // returns the element that was at that index, but we don't need it here.
110 self.entities.swap_remove(row_index as usize);
111
112 // 2. Iterate through all component columns and perform the same swap_remove
113 // on each one, using our `AnyVec` trait.
114 for column in self.columns.values_mut() {
115 column.swap_remove_any(row_index as usize);
116 }
117 }
118
119 /// Returns the number of rows of data (and entities) this page currently stores.
120 #[allow(dead_code)]
121 pub(crate) fn row_count(&self) -> usize {
122 self.entities.len()
123 }
124}