Skip to main content

concinnity_core/ecs/
context.rs

1//! `PipelineContext`: what a system sees of the world during one tick.
2//!
3//! Renderer-free. It exposes the typed component storage, the type-keyed
4//! resource and event surfaces, the compiled-payload store, the per-frame
5//! profiler, and the frame scratch. A `World` constructs one each
6//! tick and hands it to every `System::step`.
7//!
8//! Every accessor reports what it touched to [`crate::ecs::access_check`] in
9//! debug builds, so a system reaching outside its declared [`Access`] is caught
10//! at the point of the read rather than as a data race later.
11//!
12//! [`Access`]: crate::ecs::Access
13
14use alloc::vec::Vec;
15
16use crate::ecs::{
17    ColumnTicks, ComponentSlot, ComponentStorage, Entity, EventStore, Events, FrameContext,
18    PayloadLocator, PayloadStore, Resources, Tick, access_check,
19};
20use crate::gfx::profile::FrameProfile;
21use crate::result::CnResult;
22
23// Debug-only touch reporters for the accessors below, so each accessor carries
24// one line. Compiled out of release builds.
25#[cfg(debug_assertions)]
26fn note_read<C: ComponentSlot>() {
27    access_check::touch(access_check::Touch::ComponentRead {
28        id: C::DISCRIMINANT,
29        type_name: core::any::type_name::<C>(),
30    });
31}
32
33#[cfg(debug_assertions)]
34fn note_write<C: ComponentSlot>() {
35    access_check::touch(access_check::Touch::ComponentWrite {
36        id: C::DISCRIMINANT,
37        type_name: core::any::type_name::<C>(),
38    });
39}
40
41#[cfg(debug_assertions)]
42fn note_structural(op: &'static str) {
43    access_check::touch(access_check::Touch::Structural { op });
44}
45
46#[cfg(debug_assertions)]
47fn note_resource<T: 'static>(write: bool) {
48    access_check::touch(access_check::Touch::Resource {
49        type_id: core::any::TypeId::of::<T>(),
50        type_name: core::any::type_name::<T>(),
51        write,
52    });
53}
54
55/// A system's view of the world for the duration of one `step`: the five things
56/// it borrows, and the accessors that reach them.
57pub struct PipelineContext<'a> {
58    /// Per-type component storage. Systems should not access this directly;
59    /// use `query`, `query_mut`, `drain`, or `push` instead.
60    pub components: &'a mut ComponentStorage,
61    /// Compiled-payload store. Systems use `read_payload` to fetch binary data,
62    /// then call `release_blob` when done with it. A trait object so the ECS
63    /// mechanism names no concrete blob store; the runtime passes `BlobData`.
64    pub blob: &'a mut dyn PayloadStore,
65    /// Per-frame profiling data. `World::step` records each system's CPU step
66    /// time here; `GraphicsSystem` writes the backend `RenderStats` after its
67    /// draw call, and `StatHud` reads it back to drive the on-screen HUD.
68    pub profile: &'a mut FrameProfile,
69    /// Type-keyed engine singletons (e.g. the per-frame FrameInput snapshot),
70    /// accessed via `resource` / `resource_mut` / `insert_resource`. Events live
71    /// here too, inside a single `EventStore` resource that owns one `Events<E>`
72    /// queue per event type, reached via `events` / `events_mut`.
73    pub resources: &'a mut Resources,
74    /// Frame-scoped facilities: scratch that the frame loop reclaims wholesale.
75    /// One field rather than several so what a frame offers can grow without
76    /// touching every system and every construction site again.
77    pub frame: FrameContext<'a>,
78}
79
80impl<'a> PipelineContext<'a> {
81    /// Immutable iteration over all components of type C.
82    pub fn query<C: ComponentSlot>(&self) -> core::slice::Iter<'_, C> {
83        #[cfg(debug_assertions)]
84        note_read::<C>();
85        C::slot(self.components).iter()
86    }
87
88    /// Iterate all components of type C paired with their owning Entity.
89    pub fn query_with_entity<C: ComponentSlot>(&self) -> impl Iterator<Item = (Entity, &C)> {
90        #[cfg(debug_assertions)]
91        note_read::<C>();
92        C::slot(self.components).iter_with_entities()
93    }
94
95    /// Mutable iteration over all components of type C.
96    pub fn query_mut<C: ComponentSlot>(&mut self) -> core::slice::IterMut<'_, C> {
97        #[cfg(debug_assertions)]
98        note_write::<C>();
99        self.components.values_mut::<C>().iter_mut()
100    }
101
102    /// Mutable iteration over all components of type C paired with their owning
103    /// Entity (the mutable counterpart of `query_with_entity`), so a system can
104    /// update each component and still know which entity owns it without first
105    /// materializing the entity set into a Vec.
106    pub fn query_mut_with_entity<C: ComponentSlot>(
107        &mut self,
108    ) -> impl Iterator<Item = (Entity, &mut C)> {
109        #[cfg(debug_assertions)]
110        note_write::<C>();
111        self.components.values_mut_with_entities::<C>()
112    }
113
114    /// The change tick of component type C's column (bumped on any insert /
115    /// remove / mutable access of a C). Comparing two reads across frames detects
116    /// whether any C changed without scanning the column, so a per-frame pass can
117    /// skip its work when nothing touched C since it last ran.
118    pub fn changed_tick<C: ComponentSlot>(&self) -> Tick {
119        #[cfg(debug_assertions)]
120        note_read::<C>();
121        self.components.changed_tick::<C>()
122    }
123
124    /// Every tick stamp of C's column. `changed` answers "did any C move"; a
125    /// pass that wants to re-examine only the components that moved also needs
126    /// `bulk` (a whole-column write, after which every row must be assumed
127    /// written) and `structural` (a row added or removed, after which row
128    /// positions and membership have moved).
129    pub fn column_ticks<C: ComponentSlot>(&self) -> ColumnTicks {
130        #[cfg(debug_assertions)]
131        note_read::<C>();
132        self.components.column_ticks::<C>()
133    }
134
135    /// Components of type C written since `since`, paired with their owning
136    /// entity: the dirty set a per-frame pass walks instead of the whole column.
137    /// Reports only rows a targeted `get_mut` touched, so it is meaningful only
138    /// while C's `bulk` and `structural` ticks have not moved since `since`.
139    pub fn changed_rows<C: ComponentSlot>(
140        &self,
141        since: Tick,
142    ) -> impl Iterator<Item = (Entity, &C)> {
143        #[cfg(debug_assertions)]
144        note_read::<C>();
145        self.components.changed_rows::<C>(since)
146    }
147
148    /// Mutable slice of all components of type C. Unlike `query_mut` this
149    /// exposes the backing storage as a slice, which a system can hand to the
150    /// job pool for parallel per-component work.
151    pub fn query_slice_mut<C: ComponentSlot>(&mut self) -> &mut [C] {
152        #[cfg(debug_assertions)]
153        note_write::<C>();
154        self.components.values_mut::<C>()
155    }
156
157    /// Remove and return all components of type C, despawning each removed
158    /// row's Entity so the indices recycle.
159    pub fn drain<C: ComponentSlot>(&mut self) -> Vec<C> {
160        #[cfg(debug_assertions)]
161        note_structural("drain");
162        self.components.drain::<C>()
163    }
164
165    /// Push a runtime-produced component into the matching typed column,
166    /// minting a fresh Entity for it. Preferred over reaching into
167    /// `self.components` directly.
168    pub fn push<C: ComponentSlot>(&mut self, c: C) {
169        #[cfg(debug_assertions)]
170        note_structural("push");
171        self.components.push_typed(c);
172    }
173
174    /// Add a component to an existing entity, so an entity can own more than one
175    /// component. The entity must be alive and must not already have C. Allowed
176    /// dead because the caller is in the client crate (the load-time Prop
177    /// decomposition); core itself has no systems.
178    pub fn insert<C: ComponentSlot>(&mut self, entity: Entity, c: C) {
179        #[cfg(debug_assertions)]
180        note_structural("insert");
181        self.components.insert_typed(entity, c);
182    }
183
184    /// Remove a component from an entity, returning it if present. The entity
185    /// keeps its other components. Allowed dead for the same cross-crate reason
186    /// as `insert` (the client toggles the Held tag on pickup/drop).
187    pub fn remove<C: ComponentSlot>(&mut self, entity: Entity) -> Option<C> {
188        #[cfg(debug_assertions)]
189        note_structural("remove");
190        self.components.remove_typed::<C>(entity)
191    }
192
193    /// Remove an entity entirely: swap-remove its row from every component
194    /// column and recycle its id (a stale handle to it then reads as dead). A
195    /// no-op on an already-dead or unknown entity. Allowed dead for the same
196    /// cross-crate reason as `insert` (the client despawns entities at runtime
197    /// from the GraphicsSystem).
198    pub fn despawn(&mut self, entity: Entity) {
199        #[cfg(debug_assertions)]
200        note_structural("despawn");
201        self.components.despawn(entity);
202    }
203
204    /// Whether an entity is still live (not despawned, matching generation).
205    /// Allowed dead for the same cross-crate reason as `insert` (the client
206    /// reaps a despawned entity's physics body in PhysicsSystem).
207    pub fn is_alive(&self, entity: Entity) -> bool {
208        self.components.is_alive(entity)
209    }
210
211    /// Borrow one entity's component C read-only. Allowed dead for the same
212    /// cross-crate reason as `insert` (the client reads Transform / Held by
213    /// entity in the physics, camera, and audio systems).
214    pub fn get<C: ComponentSlot>(&self, entity: Entity) -> Option<&C> {
215        #[cfg(debug_assertions)]
216        note_read::<C>();
217        self.components.get::<C>(entity)
218    }
219
220    /// Every entity carrying the component with this tag. Serves the queries a
221    /// Behavior declares by component name, which no type parameter can express.
222    pub fn entities_with_tag(&self, tag: u8) -> &[Entity] {
223        #[cfg(debug_assertions)]
224        access_check::touch(access_check::Touch::ComponentRead {
225            id: tag,
226            type_name: "<by tag>",
227        });
228        self.components.entities_with_tag(tag)
229    }
230
231    /// Read-only join over two component types: iterate the first type's rows
232    /// and yield both refs for every entity that also has the second. Allowed
233    /// dead for the same cross-crate reason as `insert`.
234    pub fn join2<A: ComponentSlot, B: ComponentSlot>(
235        &self,
236    ) -> impl Iterator<Item = (Entity, &A, &B)> {
237        #[cfg(debug_assertions)]
238        {
239            note_read::<A>();
240            note_read::<B>();
241        }
242        self.components.join2::<A, B>()
243    }
244
245    /// Mutably borrow one entity's component C (a propagation pass writing a
246    /// single entity's value). Allowed dead for the same cross-crate reason as
247    /// `insert`.
248    pub fn get_mut<C: ComponentSlot>(&mut self, entity: Entity) -> Option<&mut C> {
249        #[cfg(debug_assertions)]
250        note_write::<C>();
251        self.components.get_mut::<C>(entity)
252    }
253
254    /// Borrow the singleton resource of type T, if present.
255    pub fn resource<T: core::any::Any>(&self) -> Option<&T> {
256        #[cfg(debug_assertions)]
257        note_resource::<T>(false);
258        self.resources.get::<T>()
259    }
260
261    /// Mutably borrow the singleton resource of type T, if present.
262    pub fn resource_mut<T: core::any::Any>(&mut self) -> Option<&mut T> {
263        #[cfg(debug_assertions)]
264        note_resource::<T>(true);
265        self.resources.get_mut::<T>()
266    }
267
268    /// Install (or replace) the singleton resource of type T, returning the
269    /// previous instance if one was present.
270    pub fn insert_resource<T: core::any::Any + Send>(&mut self, value: T) -> Option<T> {
271        #[cfg(debug_assertions)]
272        note_resource::<T>(true);
273        self.resources.insert(value)
274    }
275
276    /// Withdraw the singleton resource of type T, if present.
277    pub fn remove_resource<T: core::any::Any>(&mut self) -> Option<T> {
278        #[cfg(debug_assertions)]
279        note_resource::<T>(true);
280        self.resources.remove::<T>()
281    }
282
283    /// Take the singleton resource value of type T, leaving `T::default()`
284    /// parked in its slot so a take/republish cycle reuses the allocation.
285    /// `None` when the type was never inserted.
286    pub fn take_resource<T: core::any::Any + Send + Default>(&mut self) -> Option<T> {
287        #[cfg(debug_assertions)]
288        note_resource::<T>(true);
289        self.resources.take::<T>()
290    }
291
292    /// Borrow the event queue for event type E, if any events of that type have
293    /// been registered.
294    pub fn events<E: 'static>(&self) -> Option<&Events<E>> {
295        #[cfg(debug_assertions)]
296        note_resource::<E>(false);
297        self.resources.get::<EventStore>()?.get::<E>()
298    }
299
300    /// Mutably borrow the event queue for event type E, creating an empty one on
301    /// first access so writers and readers never miss it. All queues live in the
302    /// `EventStore` resource, which the frame driver rotates wholesale.
303    pub fn events_mut<E: Send + 'static>(&mut self) -> &mut Events<E> {
304        #[cfg(debug_assertions)]
305        note_resource::<E>(true);
306        if !self.resources.contains::<EventStore>() {
307            self.resources.insert(EventStore::new());
308        }
309        self.resources
310            .get_mut::<EventStore>()
311            .expect("EventStore was just inserted")
312            .get_mut_or_create::<E>()
313    }
314
315    /// Read the compiled payload bytes for a locator.
316    ///
317    /// Takes `&mut self` because an overflow blob is read from disk lazily on
318    /// first access. Returns an error if the blob was released, the locator is
319    /// out of range, or the on-demand load fails.
320    pub fn read_payload(&mut self, locator: &PayloadLocator) -> Result<&[u8], CnResult> {
321        #[cfg(debug_assertions)]
322        access_check::touch(access_check::Touch::Blob { op: "read_payload" });
323        self.blob.read(locator)
324    }
325
326    /// Release the in-memory payload for an entire blob once all systems
327    /// that need it have finished (e.g. after GPU upload).
328    ///
329    /// See `PayloadStore::release` for semantics.
330    pub fn release_blob(&mut self, blob_index: u32) {
331        #[cfg(debug_assertions)]
332        access_check::touch(access_check::Touch::Blob { op: "release_blob" });
333        self.blob.release(blob_index);
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340    use crate::ecs::{AssetKind, BlobAssetDef, ComponentAsset, ComponentTag, EventCursor};
341    use alloc::vec;
342
343    // A payload store holding nothing: every read errors, releases are no-ops.
344    // Lets the ECS tests exercise the context's payload forwarding without
345    // depending on the concrete `BlobData` (which lives host-side).
346    struct EmptyStore;
347
348    impl PayloadStore for EmptyStore {
349        fn read(&mut self, _locator: &PayloadLocator) -> Result<&[u8], CnResult> {
350            Err(CnResult::FileIo)
351        }
352        fn release(&mut self, _blob_index: u32) {}
353        fn disk_backed(&self) -> bool {
354            false
355        }
356    }
357
358    // A standalone PipelineContext over empty storage, for exercising the
359    // resource and event surfaces without a running world.
360    fn parts() -> (
361        ComponentStorage,
362        EmptyStore,
363        FrameProfile,
364        Resources,
365        concinnity_memory::Arena,
366    ) {
367        (
368            ComponentStorage::default(),
369            EmptyStore,
370            FrameProfile::default(),
371            Resources::new(),
372            concinnity_memory::Arena::with_capacity(64 * 1024),
373        )
374    }
375
376    #[test]
377    fn resources_round_trip_through_context() {
378        let (mut c, mut b, mut p, mut r, scratch) = parts();
379        let mut ctx = PipelineContext {
380            components: &mut c,
381            blob: &mut b,
382            profile: &mut p,
383            resources: &mut r,
384            frame: FrameContext::new(&scratch),
385        };
386        assert!(ctx.resource::<u32>().is_none());
387        assert_eq!(ctx.insert_resource(7u32), None);
388        assert_eq!(ctx.resource::<u32>(), Some(&7));
389        *ctx.resource_mut::<u32>().unwrap() = 9;
390        assert_eq!(ctx.resource::<u32>(), Some(&9));
391        // Re-inserting returns the previous value.
392        assert_eq!(ctx.insert_resource(1u32), Some(9));
393    }
394
395    #[test]
396    fn events_round_trip_through_context() {
397        let (mut c, mut b, mut p, mut r, scratch) = parts();
398        let mut ctx = PipelineContext {
399            components: &mut c,
400            blob: &mut b,
401            profile: &mut p,
402            resources: &mut r,
403            frame: FrameContext::new(&scratch),
404        };
405        assert!(ctx.events::<u32>().is_none());
406        ctx.events_mut::<u32>().send(1);
407        ctx.events_mut::<u32>().send(2);
408
409        let mut cursor = EventCursor::default();
410        let seen: Vec<u32> = ctx
411            .events::<u32>()
412            .unwrap()
413            .read(&mut cursor)
414            .copied()
415            .collect();
416        assert_eq!(seen, vec![1, 2]);
417        // The same cursor sees nothing new on a second read.
418        assert_eq!(ctx.events::<u32>().unwrap().read(&mut cursor).count(), 0);
419    }
420
421    // A blob record loads through `from_baked`: the bytes are the serialized
422    // runtime component, name injection follows, and an unknown tag is
423    // rejected.
424    #[test]
425    fn baked_records_load_through_from_baked() {
426        use crate::components::PointLight;
427        let light = PointLight {
428            intensity: 3.5,
429            range: 12.0,
430            ..Default::default()
431        };
432        let baked = BlobAssetDef {
433            name: None,
434            kind: AssetKind::Component,
435            discriminant: ComponentTag::PointLight as u8,
436            args_bytes: postcard::to_allocvec(&light).unwrap(),
437            payload: None,
438        };
439        let from_baked = ComponentAsset::from_baked(&baked).unwrap();
440        let ComponentAsset::PointLight(b) = &from_baked else {
441            panic!("expected PointLight");
442        };
443        assert_eq!(b.intensity, 3.5);
444        assert_eq!(b.range, 12.0);
445        // An unknown tag is rejected.
446        let mut bad = baked;
447        bad.discriminant = 255;
448        assert_eq!(
449            ComponentAsset::from_baked(&bad).unwrap_err(),
450            CnResult::AssetInvalidType
451        );
452    }
453
454    #[test]
455    fn storage_push_dispatches_into_the_typed_column() {
456        let mut storage = ComponentStorage::default();
457        storage.push(crate::components::Transform::default().into());
458        let census = storage.component_census();
459        // Transform's tag is its position in the component list.
460        assert_eq!(census, vec![(ComponentTag::Transform as u8, 1)]);
461    }
462
463    #[test]
464    fn context_component_ops_cover_the_entity_lifecycle() {
465        use crate::components::{GlobalTransform, Transform};
466        let (mut c, mut b, mut p, mut r, scratch) = parts();
467        let mut ctx = PipelineContext {
468            components: &mut c,
469            blob: &mut b,
470            profile: &mut p,
471            resources: &mut r,
472            frame: FrameContext::new(&scratch),
473        };
474
475        ctx.push(Transform::default());
476        let e = ctx.components.push_typed(Transform::default());
477        assert!(ctx.is_alive(e));
478        assert_eq!(ctx.query::<Transform>().count(), 2);
479        assert_eq!(ctx.query_with_entity::<Transform>().count(), 2);
480
481        // Mutate through each of the mutable access paths.
482        for t in ctx.query_mut::<Transform>() {
483            t.position[0] = 1.0;
484        }
485        ctx.query_slice_mut::<Transform>()[0].position[1] = 2.0;
486        ctx.get_mut::<Transform>(e).unwrap().position[2] = 3.0;
487        assert_eq!(ctx.get::<Transform>(e).unwrap().position, [1.0, 0.0, 3.0]);
488
489        // A second component on the same entity, then remove it again.
490        ctx.insert(e, GlobalTransform::default());
491        assert_eq!(ctx.join2::<Transform, GlobalTransform>().count(), 1);
492        assert!(ctx.remove::<GlobalTransform>(e).is_some());
493        assert!(ctx.remove::<GlobalTransform>(e).is_none());
494
495        // Despawn kills the entity and its remaining components.
496        ctx.despawn(e);
497        assert!(!ctx.is_alive(e));
498        assert_eq!(ctx.query::<Transform>().count(), 1);
499        assert!(ctx.get::<Transform>(e).is_none());
500
501        // Drain empties the column and returns the survivors.
502        let drained = ctx.drain::<Transform>();
503        assert_eq!(drained.len(), 1);
504        assert_eq!(ctx.query::<Transform>().count(), 0);
505    }
506
507    #[test]
508    fn read_payload_and_release_forward_to_the_store() {
509        let (mut c, mut b, mut p, mut r, scratch) = parts();
510        let mut ctx = PipelineContext {
511            components: &mut c,
512            blob: &mut b,
513            profile: &mut p,
514            resources: &mut r,
515            frame: FrameContext::new(&scratch),
516        };
517        let loc = PayloadLocator {
518            blob_index: 0,
519            offset: 0,
520            len: 4,
521        };
522        // read_payload forwards the store's error verbatim.
523        assert_eq!(ctx.read_payload(&loc).unwrap_err(), CnResult::FileIo);
524        // release_blob forwards without panicking.
525        ctx.release_blob(0);
526    }
527}