Skip to main content

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    fmt,
19};
20
21use bincode::{Decode, Encode};
22use khora_core::ecs::entity::EntityId;
23
24/// Upper bound on the byte payload accepted by [`AnyVec::set_from_bytes`] for a
25/// single component column.
26///
27/// Deserialization feeds this length straight into a `Vec` reservation, so an
28/// untrusted scene/pack file could otherwise request an arbitrarily large
29/// allocation and abort the process with an OOM. Real component columns are
30/// small (transforms, handles, a few scalars per entity); even a million
31/// entities of a 256-byte component is 256 MiB, so this ceiling comfortably
32/// covers legitimate scenes while rejecting hostile inputs before any
33/// allocation happens.
34pub const MAX_COLUMN_PAYLOAD_BYTES: usize = 256 * 1024 * 1024;
35
36/// Error returned when raw column bytes fail validation in
37/// [`AnyVec::set_from_bytes`].
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub enum SetFromBytesError {
40    /// The byte length is not an exact multiple of the column's element size,
41    /// so it cannot describe a whole number of elements.
42    MisalignedLength {
43        /// Length of the supplied byte buffer.
44        len: usize,
45        /// Size of a single element in the column.
46        elem_size: usize,
47    },
48    /// The byte length exceeds [`MAX_COLUMN_PAYLOAD_BYTES`].
49    PayloadTooLarge {
50        /// Length of the supplied byte buffer.
51        len: usize,
52        /// The maximum accepted payload size.
53        max: usize,
54    },
55}
56
57impl fmt::Display for SetFromBytesError {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            SetFromBytesError::MisalignedLength { len, elem_size } => write!(
61                f,
62                "column byte length {len} is not a multiple of element size {elem_size}"
63            ),
64            SetFromBytesError::PayloadTooLarge { len, max } => write!(
65                f,
66                "column byte length {len} exceeds the maximum payload size {max}"
67            ),
68        }
69    }
70}
71
72impl std::error::Error for SetFromBytesError {}
73
74/// An internal helper trait to perform vector operations on a type-erased `Box<dyn Any>`.
75///
76/// This allows us to call methods like `swap_remove` on component columns without
77/// needing to know their concrete `Vec<T>` type at compile time.
78pub trait AnyVec: Any + Send + Sync {
79    /// Casts the trait object to `&dyn Any`.
80    fn as_any(&self) -> &dyn Any;
81
82    /// Casts the trait object to `&mut dyn Any`.
83    fn as_any_mut(&mut self) -> &mut dyn Any;
84
85    /// Performs a `swap_remove` on the underlying column, removing the element at `index`.
86    fn swap_remove_any(&mut self, index: usize);
87
88    /// Serialises the column to an owned byte buffer.
89    ///
90    /// Returns an *owned* `Vec<u8>` (not a borrowed slice) so that columns whose
91    /// data is not a single contiguous buffer — e.g. a field-split SoA column —
92    /// can materialise their bytes. The format is the column's own concern; the
93    /// only contract is that [`set_from_bytes`](AnyVec::set_from_bytes) on the
94    /// same column type reverses it.
95    fn to_bytes(&self) -> Vec<u8>;
96
97    /// Replaces the column contents from bytes previously produced by
98    /// [`to_bytes`](AnyVec::to_bytes) **on the same column type**.
99    ///
100    /// The length is validated against the column's element size and against
101    /// [`MAX_COLUMN_PAYLOAD_BYTES`] before any allocation, so untrusted input
102    /// cannot force an out-of-memory abort; a malformed length returns
103    /// [`SetFromBytesError`] and leaves the column unchanged.
104    ///
105    /// # Safety
106    /// The caller must guarantee the bytes match this column's element *type*,
107    /// element size, and alignment. Length validation alone cannot detect a
108    /// type mismatch, so feeding bytes produced by a different column type is
109    /// still undefined behaviour.
110    unsafe fn set_from_bytes(&mut self, bytes: &[u8]) -> Result<(), SetFromBytesError>;
111}
112
113// We implement this trait for any `Vec<T>` where T is `'static`.
114impl<T: 'static + Send + Sync> AnyVec for Vec<T> {
115    fn as_any(&self) -> &dyn Any {
116        self
117    }
118
119    fn as_any_mut(&mut self) -> &mut dyn Any {
120        self
121    }
122
123    fn swap_remove_any(&mut self, index: usize) {
124        self.swap_remove(index);
125    }
126
127    fn to_bytes(&self) -> Vec<u8> {
128        // SAFETY: reads `len * size_of::<T>()` initialised bytes from the Vec's
129        // buffer and copies them into an owned `Vec<u8>`.
130        unsafe {
131            std::slice::from_raw_parts(
132                self.as_ptr() as *const u8,
133                self.len() * std::mem::size_of::<T>(),
134            )
135            .to_vec()
136        }
137    }
138
139    unsafe fn set_from_bytes(&mut self, bytes: &[u8]) -> Result<(), SetFromBytesError> {
140        let elem_size = std::mem::size_of::<T>();
141        if elem_size == 0 {
142            return Ok(()); // Correctly handle Zero-Sized Types.
143        }
144
145        // Validate the untrusted length *before* reserving, so a hostile scene
146        // file cannot trigger a huge allocation or a partial copy.
147        if !bytes.len().is_multiple_of(elem_size) {
148            return Err(SetFromBytesError::MisalignedLength {
149                len: bytes.len(),
150                elem_size,
151            });
152        }
153        if bytes.len() > MAX_COLUMN_PAYLOAD_BYTES {
154            return Err(SetFromBytesError::PayloadTooLarge {
155                len: bytes.len(),
156                max: MAX_COLUMN_PAYLOAD_BYTES,
157            });
158        }
159
160        // Calculate the new length and resize the Vec accordingly.
161        let new_len = bytes.len() / elem_size;
162        self.clear();
163        self.reserve(new_len);
164
165        // SAFETY: `reserve(new_len)` guaranteed capacity for `new_len` elements,
166        // i.e. `new_len * elem_size == bytes.len()` initialised bytes (the length
167        // was validated as an exact multiple above). The source and destination
168        // are distinct, non-overlapping allocations, so `copy_nonoverlapping` of
169        // exactly `bytes.len()` bytes is in-bounds; `set_len(new_len)` then marks
170        // those bytes initialised. Element *type* correctness is the documented
171        // obligation of the caller.
172        let ptr = self.as_mut_ptr() as *mut u8;
173        std::ptr::copy_nonoverlapping(bytes.as_ptr(), ptr, bytes.len());
174        self.set_len(new_len);
175        Ok(())
176    }
177}
178
179/// A logical address pointing to an entity's component data within a specific `ComponentPage`.
180///
181/// This struct is the core of the relational aspect of our ECS. It decouples an entity's
182/// identity from the physical storage of its data by acting as a coordinate.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
184pub struct PageIndex {
185    /// The unique identifier of the `ComponentPage` that stores the component data.
186    pub page_id: u32,
187    /// The index of the row within the page where this entity's components are stored.
188    pub row_index: u32,
189}
190
191/// A serializable representation of a single `ComponentPage`.
192#[derive(Encode, Decode)]
193pub(crate) struct SerializedPage {
194    /// The unique identifiers of this page.
195    pub(crate) type_names: Vec<String>,
196    /// The list of entities whose component data is stored in this page.
197    pub(crate) entities: Vec<EntityId>,
198    /// The actual serialized component data columns. Each column is a byte vector
199    /// representing the serialized `Vec<T>` for a specific component
200    pub(crate) columns: HashMap<String, Vec<u8>>,
201}
202
203/// A page of memory that stores the component data for multiple entities
204/// in a Structure of Arrays (SoA) layout.
205///
206/// A `ComponentPage` is specialized for a single semantic domain (e.g., physics).
207/// It contains multiple columns, where each column is a `Vec<T>` for a specific
208/// component type `T`. This SoA layout is the key to our high iteration performance,
209/// as it guarantees contiguous data access for native queries.
210pub struct ComponentPage {
211    /// A map from a component's `TypeId` to its actual storage column.
212    /// The `Box<dyn AnyVec>` is a type-erased `Vec<T>` that knows how to
213    /// perform basic vector operations like `swap_remove`.
214    pub(crate) columns: HashMap<TypeId, Box<dyn AnyVec>>,
215
216    /// A list of the `EntityId`s that own the data in each row of this page.
217    /// The entity at `entities[i]` corresponds to the components at `columns[...][i]`.
218    /// This is crucial for reverse lookups, especially during entity despawning.
219    pub(crate) entities: Vec<EntityId>,
220
221    /// The sorted list of `TypeId`s for the components stored in this page.
222    /// This acts as the page's "signature" for matching with bundles. It is
223    /// kept sorted to ensure that the signature is canonical.
224    pub(crate) type_ids: Vec<TypeId>,
225}
226
227impl ComponentPage {
228    /// Adds an entity to this page's entity list.
229    ///
230    /// This method is called by `World::spawn` and is a crucial part of maintaining
231    /// the invariant that the number of rows in the component columns is always
232    /// equal to the number of entities tracked by the page.
233    pub(crate) fn add_entity(&mut self, entity_id: EntityId) {
234        self.entities.push(entity_id);
235    }
236
237    /// Performs a `swap_remove` on a specific row across all component columns
238    /// and the entity list.
239    ///
240    /// This is the core of an O(1) despawn operation. It removes the data for the entity
241    /// at `row_index` by swapping it with the last element in each column and in the
242    /// entity list.
243    ///
244    /// It's the caller's (`World::despawn`) responsibility to update the metadata of
245    /// the entity that was moved from the last row.
246    pub(crate) fn swap_remove_row(&mut self, row_index: u32) {
247        // 1. Remove the corresponding entity ID from the list. `swap_remove` on a Vec
248        // returns the element that was at that index, but we don't need it here.
249        self.entities.swap_remove(row_index as usize);
250
251        // 2. Iterate through all component columns and perform the same swap_remove
252        // on each one, using our `AnyVec` trait.
253        for column in self.columns.values_mut() {
254            column.swap_remove_any(row_index as usize);
255        }
256    }
257
258    /// Returns the number of rows of data (and entities) this page currently stores.
259    #[allow(dead_code)]
260    pub(crate) fn row_count(&self) -> usize {
261        self.entities.len()
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn set_from_bytes_valid_roundtrip() {
271        let original: Vec<u32> = vec![1, 2, 3, 4];
272        let bytes = original.to_bytes();
273        let mut restored: Vec<u32> = Vec::new();
274        // SAFETY: `bytes` was produced by `to_bytes` on a `Vec<u32>`, matching
275        // the element type, size, and alignment of `restored`.
276        unsafe { restored.set_from_bytes(&bytes) }.expect("valid bytes must round-trip");
277        assert_eq!(restored, original);
278    }
279
280    #[test]
281    fn set_from_bytes_misaligned_length_errs() {
282        // 5 bytes is not a multiple of size_of::<u32>() == 4.
283        let bytes = [0u8; 5];
284        let mut col: Vec<u32> = vec![42];
285        // SAFETY: the call validates length before touching memory; we only
286        // assert it rejects a misaligned buffer.
287        let result = unsafe { col.set_from_bytes(&bytes) };
288        assert!(matches!(
289            result,
290            Err(SetFromBytesError::MisalignedLength {
291                len: 5,
292                elem_size: 4
293            })
294        ));
295        // The column must be left untouched on error.
296        assert_eq!(col, vec![42]);
297    }
298
299    #[test]
300    fn set_from_bytes_oversized_payload_errs() {
301        // A length that is a valid multiple of the element size but exceeds the
302        // ceiling. We build a fake slice header without allocating the bytes.
303        let elem_size = std::mem::size_of::<u8>();
304        let oversized_len = MAX_COLUMN_PAYLOAD_BYTES + elem_size;
305        // SAFETY: we never read through this pointer — `set_from_bytes`
306        // validates the length and returns `Err` before any access. The dangling
307        // pointer is only used to construct a slice whose length triggers the
308        // ceiling check.
309        let fake = unsafe {
310            std::slice::from_raw_parts(std::ptr::NonNull::<u8>::dangling().as_ptr(), oversized_len)
311        };
312        let mut col: Vec<u8> = Vec::new();
313        // SAFETY: as above — the oversized length short-circuits with an error
314        // before the slice contents are ever touched.
315        let result = unsafe { col.set_from_bytes(fake) };
316        assert!(matches!(
317            result,
318            Err(SetFromBytesError::PayloadTooLarge { .. })
319        ));
320    }
321
322    #[test]
323    fn set_from_bytes_zero_sized_type_is_noop() {
324        let mut col: Vec<()> = Vec::new();
325        // SAFETY: ZST columns carry no byte payload; the call returns early.
326        let result = unsafe { col.set_from_bytes(&[]) };
327        assert!(result.is_ok());
328    }
329}