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