sky_ecs 0.1.3

High-performance typed chunk-based ECS for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
use super::{component_type, EntityId, World};
use core::marker::PhantomData;
use core::ptr::NonNull;
use core::slice;

const MISSING_COLUMN_START: usize = usize::MAX;

#[derive(Clone, Copy)]
struct DataColumnStart {
    start: usize,
}

impl DataColumnStart {
    const MISSING: Self = Self {
        start: MISSING_COLUMN_START,
    };

    #[inline(always)]
    fn contains_component(self) -> bool {
        self.start != MISSING_COLUMN_START
    }
}

struct ComponentColumn<T> {
    ptr: NonNull<T>,
    len: usize,
}

/// A read-only component accessor bound to one [`World`].
///
/// Create an accessor with [`World::accessor`] when a hot path repeatedly
/// looks up the same component type by [`EntityId`]. Construction resolves the
/// component column once for every archetype and stores typed views of the
/// matching chunk columns. Individual calls to [`get`](Self::get) therefore do
/// not need to search archetype metadata again.
///
/// The accessor borrows the world for its entire lifetime. This keeps its
/// cached chunk views valid and prevents structural changes while it is in
/// use. For occasional lookups, prefer [`World::get`].
#[must_use = "component accessors do nothing until get is called"]
pub struct ComponentAccessor<'w, T> {
    world: &'w World,
    data_columns: Box<[DataColumnStart]>,
    columns: Box<[&'w [T]]>,
}

impl<'w, T: 'static> ComponentAccessor<'w, T> {
    pub(crate) fn new(world: &'w World) -> Self {
        let component = component_type::<T>();
        let mut data_columns = vec![DataColumnStart::MISSING; world.data.len()];
        let mut columns = Vec::new();

        if let Some(postings) = world.component_posting(&component) {
            columns.reserve(
                (0..postings.len())
                    .filter_map(|index| postings.entry(index))
                    .map(|entry| world.data[entry.data_index()].chunks.len())
                    .sum(),
            );

            for posting_index in 0..postings.len() {
                let entry = postings
                    .entry(posting_index)
                    .expect("posting index must stay in bounds");
                let data = &world.data[entry.data_index()];
                let start = columns.len();
                columns.extend(data.chunks.iter().map(|chunk| unsafe {
                    // The posting entry proves that this column stores T.
                    // Chunk storage remains fixed for 'w because the accessor holds
                    // a shared World borrow, and entity_count is the initialized
                    // prefix of every component column.
                    slice::from_raw_parts(
                        chunk.column_ptr(entry.column_index() as usize).cast::<T>(),
                        chunk.entity_count,
                    )
                }));
                data_columns[entry.data_index()] = DataColumnStart { start };
            }
        }

        Self {
            world,
            data_columns: data_columns.into_boxed_slice(),
            columns: columns.into_boxed_slice(),
        }
    }

    /// Returns component `T` on `entity`.
    ///
    /// Returns `None` when the entity is dead, its generation is stale, or its
    /// archetype does not contain `T`.
    #[inline(always)]
    pub fn get(&self, entity: EntityId) -> Option<&'w T> {
        let location = self.world.entity_location(entity)?;

        unsafe {
            // A live EntityRecord always names existing archetype storage, a Chunk, and
            // initialized entity row. The immutable World borrow prevents all
            // structural operations that could invalidate those indices. The
            // optional entry is None exactly when that storage lacks T.
            let data_column = *self.data_columns.get_unchecked(location.data_index);
            if !data_column.contains_component() {
                return None;
            }
            let column: &'w [T] = self
                .columns
                .get_unchecked(data_column.start + location.chunk_index);
            Some(column.get_unchecked(location.entity_index))
        }
    }
}

/// An exclusive component accessor bound to one [`World`].
///
/// Create one with [`World::accessor_mut`] when a hot path repeatedly updates
/// the same component type by [`EntityId`]. The accessor exclusively borrows
/// the world, and every reference returned by [`get_mut`](Self::get_mut) is
/// tied to the corresponding mutable borrow of the accessor. This prevents
/// overlapping mutable component references through the safe API.
#[must_use = "component accessors do nothing until get_mut is called"]
pub struct ComponentAccessorMut<'w, T> {
    world: NonNull<World>,
    data_columns: Box<[DataColumnStart]>,
    columns: Box<[ComponentColumn<T>]>,
    marker: PhantomData<&'w mut World>,
}

impl<'w, T: 'static> ComponentAccessorMut<'w, T> {
    pub(crate) fn new(world: &'w mut World) -> Self {
        let world_ptr = NonNull::from(&mut *world);
        let component = component_type::<T>();
        let mut data_columns = vec![DataColumnStart::MISSING; world.data.len()];
        let mut columns = Vec::new();

        if let Some(postings) = world.component_posting(&component) {
            columns.reserve(
                (0..postings.len())
                    .filter_map(|index| postings.entry(index))
                    .map(|entry| world.data[entry.data_index()].chunks.len())
                    .sum(),
            );

            for posting_index in 0..postings.len() {
                let entry = postings
                    .entry(posting_index)
                    .expect("posting index must stay in bounds");
                let data = &world.data[entry.data_index()];
                let start = columns.len();
                columns.extend(data.chunks.iter().map(|chunk| ComponentColumn {
                    ptr: unsafe {
                        // The posting entry proves this column stores T, and Chunk
                        // always owns a non-null aligned backing block.
                        NonNull::new_unchecked(
                            chunk.column_ptr(entry.column_index() as usize).cast::<T>(),
                        )
                    },
                    len: chunk.entity_count,
                }));
                data_columns[entry.data_index()] = DataColumnStart { start };
            }
        }

        Self {
            world: world_ptr,
            data_columns: data_columns.into_boxed_slice(),
            columns: columns.into_boxed_slice(),
            marker: PhantomData,
        }
    }

    /// Returns component `T` on `entity` with exclusive access.
    ///
    /// Returns `None` when the entity is dead, its generation is stale, or its
    /// archetype does not contain `T`.
    #[inline(always)]
    pub fn get_mut(&mut self, entity: EntityId) -> Option<&mut T> {
        let location = unsafe {
            // `marker` retains the exclusive World borrow for 'w, so the World
            // remains alive and no safe structural operation can run.
            self.world.as_ref().entity_location(entity)?
        };

        unsafe {
            // A live EntityRecord names existing archetype storage, a Chunk, and an initialized
            // row. The exclusive World borrow keeps those locations stable.
            // The returned reference is tied to `&mut self`, preventing another
            // accessor call until that reference is no longer used.
            let data_column = *self.data_columns.get_unchecked(location.data_index);
            if !data_column.contains_component() {
                return None;
            }
            let column = self
                .columns
                .get_unchecked(data_column.start + location.chunk_index);
            debug_assert!(location.entity_index < column.len);
            Some(&mut *column.ptr.as_ptr().add(location.entity_index))
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::atomic::{AtomicUsize, Ordering};
    use std::sync::Arc;

    #[derive(Clone, Copy, Debug, PartialEq)]
    struct Position(u32);

    #[derive(Clone, Copy, Debug, PartialEq)]
    struct Velocity(u32);

    #[derive(Clone, Copy, Debug, PartialEq)]
    struct Marker;

    #[allow(dead_code)]
    #[derive(Clone, Copy)]
    struct Large([u8; 4 * 1024]);

    #[test]
    fn reads_components_across_archetypes_and_reports_missing_components() {
        let mut world = World::new();
        let position_only = world.spawn((Position(1),));
        let both = world.spawn((Position(2), Velocity(3)));
        let velocity_only = world.spawn((Velocity(4),));

        let positions = world.accessor::<Position>();

        assert_eq!(positions.get(position_only), Some(&Position(1)));
        assert_eq!(positions.get(both), Some(&Position(2)));
        assert_eq!(positions.get(velocity_only), None);
    }

    #[test]
    fn stores_matching_chunk_columns_in_one_flat_table() {
        let mut world = World::new();
        world.spawn((Position(1),));
        world.spawn((Position(2), Velocity(3)));
        world.spawn((Velocity(4),));

        let positions = world.accessor::<Position>();
        let expected_columns = positions
            .world
            .data
            .iter()
            .filter(|data| {
                data.archetype
                    .query_component_index(&component_type::<Position>())
                    .is_some()
            })
            .map(|data| data.chunks.len())
            .sum::<usize>();

        assert_eq!(positions.data_columns.len(), positions.world.data.len());
        assert_eq!(positions.columns.len(), expected_columns);
        assert_eq!(
            positions
                .data_columns
                .iter()
                .filter(|range| range.contains_component())
                .count(),
            2
        );
    }

    #[test]
    fn rejects_invalid_and_stale_entity_ids() {
        let mut world = World::new();
        let stale = world.spawn((Position(1),));
        assert!(world.despawn(stale));
        let current = world.spawn((Position(2),));
        assert_eq!(stale.index(), current.index());

        let positions = world.accessor::<Position>();

        assert_eq!(positions.get(stale), None);
        assert_eq!(positions.get(EntityId::new(u32::MAX, 0)), None);
        assert_eq!(positions.get(current), Some(&Position(2)));
    }

    #[test]
    fn reads_small_and_large_chunks() {
        let mut world = World::new();
        let entities: Vec<_> = (0..160)
            .map(|index| world.spawn((Large([index as u8; 4 * 1024]), Position(index))))
            .collect();

        let positions = world.accessor::<Position>();

        for (index, entity) in entities.into_iter().enumerate() {
            assert_eq!(positions.get(entity), Some(&Position(index as u32)));
        }
    }

    #[test]
    fn reads_zero_sized_components() {
        let mut world = World::new();
        let entity = world.spawn((Marker, Position(1)));

        let markers = world.accessor::<Marker>();

        assert_eq!(markers.get(entity), Some(&Marker));
    }

    struct Droppable {
        drops: Arc<AtomicUsize>,
    }

    impl Drop for Droppable {
        fn drop(&mut self) {
            self.drops.fetch_add(1, Ordering::Relaxed);
        }
    }

    #[test]
    fn reads_non_copy_components_without_affecting_drop_semantics() {
        let drops = Arc::new(AtomicUsize::new(0));
        {
            let mut world = World::new();
            let entity = world.spawn((Droppable {
                drops: Arc::clone(&drops),
            },));

            {
                let components = world.accessor::<Droppable>();
                assert!(Arc::ptr_eq(&components.get(entity).unwrap().drops, &drops));
                assert_eq!(drops.load(Ordering::Relaxed), 0);
            }
        }

        assert_eq!(drops.load(Ordering::Relaxed), 1);
    }

    #[test]
    fn mutably_updates_components_across_archetypes() {
        let mut world = World::new();
        let position_only = world.spawn((Position(1),));
        let both = world.spawn((Position(2), Velocity(3)));
        let velocity_only = world.spawn((Velocity(4),));

        {
            let mut positions = world.accessor_mut::<Position>();
            positions.get_mut(position_only).unwrap().0 += 10;
            positions.get_mut(both).unwrap().0 += 20;
            assert!(positions.get_mut(velocity_only).is_none());
        }

        assert_eq!(world.get::<Position>(position_only), Some(&Position(11)));
        assert_eq!(world.get::<Position>(both), Some(&Position(22)));
    }

    #[test]
    fn mutable_accessor_rejects_invalid_and_stale_entity_ids() {
        let mut world = World::new();
        let stale = world.spawn((Position(1),));
        assert!(world.despawn(stale));
        let current = world.spawn((Position(2),));

        let mut positions = world.accessor_mut::<Position>();

        assert!(positions.get_mut(stale).is_none());
        assert!(positions.get_mut(EntityId::new(u32::MAX, 0)).is_none());
        positions.get_mut(current).unwrap().0 = 5;
        assert_eq!(
            positions.get_mut(current).map(|position| position.0),
            Some(5)
        );
    }

    #[test]
    fn mutable_accessor_handles_multiple_chunk_layouts() {
        let mut world = World::new();
        let entities: Vec<_> = (0..160)
            .map(|index| world.spawn((Large([index as u8; 4 * 1024]), Position(index))))
            .collect();

        {
            let mut positions = world.accessor_mut::<Position>();
            for entity in entities.iter().copied() {
                positions.get_mut(entity).unwrap().0 += 1;
            }
        }

        for (index, entity) in entities.into_iter().enumerate() {
            assert_eq!(
                world.get::<Position>(entity),
                Some(&Position(index as u32 + 1))
            );
        }
    }

    #[test]
    fn mutable_accessor_updates_non_copy_components() {
        let mut world = World::new();
        let entity = world.spawn((String::from("Sky"),));

        {
            let mut strings = world.accessor_mut::<String>();
            strings.get_mut(entity).unwrap().push_str(" ECS");
        }

        assert_eq!(
            world.get::<String>(entity).map(String::as_str),
            Some("Sky ECS")
        );
    }
}