concinnity_core/ecs/world.rs
1//! A world: its data, the systems built to run over it, and their schedule.
2//!
3//! The data half is components, resources, events, the compiled-payload store,
4//! the frame profile, and the frame scratch -- exactly the five things a
5//! [`PipelineContext`] borrows, owned in one place. Over it run the systems a
6//! host's [`SystemTable`] gates in, in table order, under a schedule derived
7//! from what each declares it touches.
8//!
9//! Building and running one needs no operating system: the two ties a step
10//! would otherwise have are seams instead -- the [`Clock`] resource for the
11//! per-system profile micros, and the debug-build access validator's hooks in
12//! [`access_check`](crate::ecs::access_check).
13
14use crate::memory::{Arena, MemTag};
15use alloc::boxed::Box;
16use alloc::vec::Vec;
17
18use crate::ecs::asset_id::{AssetId, MintedIds};
19use crate::ecs::waves::{self, ExecSchedule};
20use crate::ecs::{
21 BuiltSystem, Clock, ComponentAsset, ComponentId, ComponentSlot, ComponentStorage, Entity,
22 EnvironmentMapHandle, EventStore, Events, FrameContext, MaterialHandle, MeshHandle, NoPayloads,
23 PayloadStore, PipelineContext, Resources, RuntimeComponent, StepResult, SystemEntry,
24 SystemTable,
25};
26use crate::gfx::profile::FrameProfile;
27use crate::result::CnResult;
28
29// The per-frame scratch reserve. An engine constant rather than an authored
30// field: a schema field would be blob churn for a knob nobody should have to
31// set, and the frame loop reports any frame that outgrows it.
32//
33// A frame's draw scales with the runtime requests it drains: 2,000 visibility
34// requests in one frame measured 24 KiB, so this holds on the order of 87,000.
35const FRAME_SCRATCH_BYTES: usize = 1 << 20;
36
37/// What one frame's scratch reserve cost and whether it held. A non-zero
38/// `overflows` means some frame fell back to the heap, so `peak` understates
39/// what the frame actually wanted.
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
41pub struct ScratchStats {
42 /// The reserve's size in bytes.
43 pub capacity: usize,
44 /// The most bytes any frame took from it.
45 pub peak: usize,
46 /// Requests the reserve declined, sending the caller to the heap.
47 pub overflows: u64,
48}
49
50/// A world: its component storage, its resources, the compiled payloads it
51/// loads from, and the systems that run over all three.
52///
53/// Constructing one and filling it with components needs no systems, so this is
54/// the whole world for any caller that only builds or inspects content.
55/// [`start`](World::start) is what gives it systems, from the table the caller
56/// hands it.
57pub struct World {
58 components: ComponentStorage,
59 // Compiled payloads, behind the store seam rather than a concrete type, so
60 // a world names no blob file format and no filesystem.
61 blob: Box<dyn PayloadStore + Send>,
62 profile: FrameProfile,
63 // Type-keyed engine singletons (e.g. the per-frame FrameInput snapshot
64 // GraphicsSystem publishes) and the event queues.
65 resources: Resources,
66 // Per-frame scratch, reset at the top of every step. Owned here because
67 // `reset` needs `&mut`, which is what proves no system still holds an
68 // allocation from the frame just finished.
69 scratch: Arena,
70 // Requests the scratch reserve could not satisfy, over the world's whole
71 // life. The arena's own counter is cleared each frame once reported, so
72 // this is what survives to say the reserve wants raising.
73 scratch_overflows: u64,
74 // The systems built for this world, in table order.
75 systems: Vec<BuiltSystem>,
76 // The table `start` built them from, kept for the schedule rebuild a
77 // finished system triggers.
78 entries: &'static [SystemEntry],
79 // Set once the systems have been built, so a second `start()` on the same
80 // world does not append them twice.
81 systems_built: bool,
82 // The executable schedule over the built systems: declared ordering edges
83 // validated + conflict waves from each system's declared access. Built at
84 // the end of `start()` (after init, when data-dependent declarations are
85 // final) and rebuilt when a `Done` system leaves the set.
86 schedule: Option<ExecSchedule>,
87}
88
89// A world must stay movable to the simulation thread; a !Send member in any
90// system, component, or resource breaks the pipelined driver's thread handoff.
91const _: () = {
92 const fn require_send<T: Send>() {}
93 require_send::<World>()
94};
95
96impl core::fmt::Debug for World {
97 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98 f.debug_struct("World")
99 .field("components", &self.components.len())
100 .field("systems", &self.systems.len())
101 .finish()
102 }
103}
104
105impl Default for World {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111// The next minted id, drawn from the world's shared counter so ids handed out
112// before start and by the completion pass never collide.
113fn mint_id(ctx: &mut PipelineContext) -> AssetId {
114 if ctx.resource::<MintedIds>().is_none() {
115 ctx.insert_resource(MintedIds::default());
116 }
117 ctx.resource_mut::<MintedIds>()
118 .expect("the counter was just ensured")
119 .next_id()
120}
121
122impl World {
123 /// An empty world, for contexts that have no compiled payloads (e.g. unit
124 /// tests, or worlds built entirely from runtime-only components).
125 pub fn new() -> Self {
126 Self::from_payloads(Box::new(NoPayloads))
127 }
128
129 /// A world backed by a compiled payload store.
130 pub fn from_payloads(blob: Box<dyn PayloadStore + Send>) -> Self {
131 Self {
132 components: ComponentStorage::default(),
133 blob,
134 profile: FrameProfile::default(),
135 resources: Resources::new(),
136 scratch: Arena::tagged(FRAME_SCRATCH_BYTES, MemTag::Scratch),
137 scratch_overflows: 0,
138 systems: Vec::new(),
139 entries: &[],
140 systems_built: false,
141 schedule: None,
142 }
143 }
144
145 /// Pre-size the component columns from the blob manifest's per-type record
146 /// counts, so the bulk `add` loop that follows never reallocates mid-push.
147 pub fn reserve_components(&mut self, counts: &[(u8, u32)]) {
148 for &(discriminant, count) in counts {
149 self.components
150 .reserve(ComponentId::new(discriminant), count as usize);
151 }
152 }
153
154 /// Add a component loaded from a blob def, returning its minted entity so
155 /// the loaders can index it by name.
156 pub fn add(&mut self, component: ComponentAsset) -> Entity {
157 self.components.push(component)
158 }
159
160 /// Add one component to the world.
161 ///
162 /// Only a [`RuntimeComponent`] can be added: a build-only asset is consumed
163 /// by the cook and never reaches a world.
164 pub fn add_component<C: RuntimeComponent>(&mut self, c: C) {
165 self.components.push(c.into());
166 }
167
168 /// Add a mesh with its baked geometry `payload` and return the handle a
169 /// [`Prop`](crate::components::Prop) references it by.
170 ///
171 /// The world names the mesh itself (from the minted range) and holds the
172 /// payload directly, so no compiled blob is involved; handles count up in
173 /// call order, after any the build assigned.
174 pub fn add_mesh(
175 &mut self,
176 mut mesh: crate::components::ProceduralMesh,
177 payload: Vec<u8>,
178 ) -> MeshHandle {
179 let mut ctx = self.context();
180 mesh.asset_id = mint_id(&mut ctx);
181 let handle = crate::resource::append_mesh(&mut ctx, mesh.asset_id, payload);
182 ctx.push(mesh);
183 handle
184 }
185
186 /// Add a material and return the handle a
187 /// [`Prop`](crate::components::Prop) references it by. The value's fields
188 /// are clamped into their valid ranges on the way in, the same way the
189 /// cook clamps an authored material.
190 pub fn add_material(&mut self, material: crate::components::Material) -> MaterialHandle {
191 crate::resource::append_material(&mut self.context(), material)
192 }
193
194 /// Add a baked image-based-lighting `payload` (see
195 /// [`bake::payload::environment_map`](crate::bake::payload::environment_map))
196 /// and return its handle. The renderer lights with the map at handle 0.
197 pub fn add_environment_map(&mut self, payload: Vec<u8>) -> EnvironmentMapHandle {
198 crate::resource::append_environment_map(&mut self.context(), payload)
199 }
200
201 /// Remove and drop every component of type C.
202 pub fn remove_all<C: ComponentSlot>(&mut self) {
203 let _ = self.components.drain::<C>();
204 }
205
206 /// Whether the world holds neither components nor systems.
207 pub fn is_empty(&self) -> bool {
208 self.components.is_empty() && self.systems.is_empty()
209 }
210
211 /// Components across every typed column.
212 pub fn component_count(&self) -> usize {
213 self.components.len()
214 }
215
216 /// Iterate every stored component of a given type. Mirrors
217 /// `PipelineContext::query`; useful in tests that hold a `World` directly.
218 pub fn query<C: ComponentSlot>(&self) -> core::slice::Iter<'_, C> {
219 C::slot(&self.components).iter()
220 }
221
222 /// Mutable iteration over all components of type C. Mirror of
223 /// `PipelineContext::query_mut` for code holding a `World` directly rather
224 /// than a per-system `PipelineContext`.
225 pub fn query_mut<C: ComponentSlot>(&mut self) -> core::slice::IterMut<'_, C> {
226 self.components.values_mut::<C>().iter_mut()
227 }
228
229 /// Push a runtime-produced component into the matching typed slot,
230 /// returning its minted entity. Mirror of `PipelineContext::push`.
231 pub fn push<C: ComponentSlot>(&mut self, c: C) -> Entity {
232 self.components.push_typed(c)
233 }
234
235 /// Borrow one entity's component, for code holding a `World` directly.
236 /// Mirror of `PipelineContext::get`.
237 pub fn get<C: ComponentSlot>(&self, entity: Entity) -> Option<&C> {
238 self.components.get::<C>(entity)
239 }
240
241 /// Mutably borrow one entity's component. Mirror of
242 /// `PipelineContext::get_mut`.
243 pub fn get_mut<C: ComponentSlot>(&mut self, entity: Entity) -> Option<&mut C> {
244 self.components.get_mut::<C>(entity)
245 }
246
247 /// Add a component to an existing entity. Mirror of
248 /// `PipelineContext::insert`.
249 pub fn insert<C: ComponentSlot>(&mut self, entity: Entity, c: C) {
250 self.components.insert_typed(entity, c);
251 }
252
253 /// Overwrite an existing component with a rebuilt one, keeping the entity
254 /// and its other components. `false` when the entity holds no component of
255 /// that type. An editing tool that rebuilds one component from changed
256 /// authoring data writes it back through here rather than reloading the
257 /// world around it.
258 pub fn replace_component(&mut self, entity: Entity, asset: ComponentAsset) -> bool {
259 self.components.replace(entity, asset)
260 }
261
262 /// Whether an entity is still live. Mirror of `PipelineContext::is_alive`;
263 /// guards name-index resolves against entities despawned by the start-time
264 /// drains (Window, GraphicsConfig, Scene, ...).
265 pub fn is_alive(&self, entity: Entity) -> bool {
266 self.components.is_alive(entity)
267 }
268
269 /// Despawn an entity (all its components, recycling its id). Stands in for
270 /// the GraphicsSystem-mediated despawn in system tests that need an entity
271 /// gone before a later system step (e.g. physics-body reaping).
272 pub fn despawn(&mut self, entity: Entity) {
273 self.components.despawn(entity);
274 }
275
276 /// Read-only join over two component types, for code holding a `World`
277 /// directly (the decomposition round-trip tests). Mirror of
278 /// `PipelineContext::join2`.
279 pub fn join2<A: ComponentSlot, B: ComponentSlot>(
280 &self,
281 ) -> impl Iterator<Item = (Entity, &A, &B)> {
282 self.components.join2::<A, B>()
283 }
284
285 /// How many components of each type the world holds, one entry per
286 /// populated type.
287 pub fn component_census(&self) -> Vec<(u8, u32)> {
288 self.components.component_census()
289 }
290
291 /// Borrow the event queue for event type E, if any have been sent. Mirror of
292 /// `PipelineContext::events`, for code holding a `World` directly (tests).
293 pub fn events<E: 'static>(&self) -> Option<&Events<E>> {
294 self.resources.get::<EventStore>()?.get::<E>()
295 }
296
297 /// Mutably borrow (creating if absent) the event queue for event type E.
298 /// Mirror of `PipelineContext::events_mut`, for code holding a `World`
299 /// directly: tests, and the editor's debug-driven command injection.
300 pub fn events_mut<E: Send + 'static>(&mut self) -> &mut Events<E> {
301 self.event_store().get_mut_or_create::<E>()
302 }
303
304 /// Seed (or replace) a singleton resource that persists across steps.
305 pub fn insert_resource<T: core::any::Any + Send>(&mut self, value: T) {
306 self.resources.insert(value);
307 }
308
309 /// Borrow a published singleton resource.
310 pub fn resource<T: core::any::Any>(&self) -> Option<&T> {
311 self.resources.get::<T>()
312 }
313
314 /// Mutably borrow a published singleton resource.
315 pub fn resource_mut<T: core::any::Any>(&mut self) -> Option<&mut T> {
316 self.resources.get_mut::<T>()
317 }
318
319 /// Withdraw a published singleton resource. Presence-keyed protocols turn
320 /// off by removing their resource, so the reading system pays nothing
321 /// beyond noticing the absence.
322 pub fn remove_resource<T: core::any::Any>(&mut self) -> Option<T> {
323 self.resources.remove::<T>()
324 }
325
326 /// Per-frame profiling data: system CPU timings and render-backend stats
327 /// from the most recently completed frame.
328 pub fn profile(&self) -> &FrameProfile {
329 &self.profile
330 }
331
332 /// Mutable view of the frame profile, for the frame loop that rotates its
333 /// buffers and stamps the frame's totals around each step.
334 pub fn profile_mut(&mut self) -> &mut FrameProfile {
335 &mut self.profile
336 }
337
338 /// What the frame scratch cost and whether it was big enough, for the
339 /// `memory` query and the Health panel. `peak` is what sizes the reserve.
340 pub fn scratch_stats(&self) -> ScratchStats {
341 ScratchStats {
342 capacity: self.scratch.capacity(),
343 peak: self.scratch.peak(),
344 overflows: self.scratch_overflows,
345 }
346 }
347
348 /// The systems' view of this world for one tick. The caller holds the
349 /// returned context for the whole tick, so the borrow of `self` is what
350 /// keeps the world's data still while systems run over it.
351 pub fn context(&mut self) -> PipelineContext<'_> {
352 self.systems_and_context().1
353 }
354
355 /// The `EventStore` resource, created on first use. Every queue
356 /// `events_mut` ever handed out (here or on a `PipelineContext`) lives in
357 /// this one resource, so no per-type rotation list can fall out of sync.
358 pub fn event_store(&mut self) -> &mut EventStore {
359 if !self.resources.contains::<EventStore>() {
360 self.resources.insert(EventStore::new());
361 }
362 self.resources
363 .get_mut::<EventStore>()
364 .expect("EventStore was just inserted")
365 }
366
367 /// Advance every event queue once, before systems run, so each queue's
368 /// two-frame retention holds for readers that run after the writer.
369 pub fn update_events(&mut self) {
370 if let Some(store) = self.resources.get_mut::<EventStore>() {
371 store.update_all();
372 }
373 }
374
375 /// Hand the whole frame's scratch back. `&mut self` is the proof that no
376 /// allocation from the last frame survives.
377 pub fn reset_scratch(&mut self) {
378 self.scratch.reset();
379 }
380
381 /// Release every resident compiled payload, returning the bytes freed. Run
382 /// once every system has inited and cached what it keeps.
383 pub fn release_payloads(&mut self) -> usize {
384 self.blob.release_all_resident()
385 }
386
387 /// The world's systems, in schedule order.
388 pub fn systems(&self) -> &[BuiltSystem] {
389 &self.systems
390 }
391
392 /// Mutable view of the active systems. Lets a caller holding the world
393 /// downcast one system out of the boxed set and drive it from outside the
394 /// per-system step (the `cn debug` hot-reload drive).
395 pub fn systems_mut(&mut self) -> &mut [BuiltSystem] {
396 &mut self.systems
397 }
398
399 /// Disjoint mutable borrows of the system list and the resource map, for a
400 /// caller that drives a system against something parked in a resource (the
401 /// `cn debug` hot-reload drive reaches the render backend that way).
402 pub fn systems_and_resources(&mut self) -> (&mut [BuiltSystem], &mut Resources) {
403 (&mut self.systems, &mut self.resources)
404 }
405
406 /// Systems built for this world.
407 pub fn system_count(&self) -> usize {
408 self.systems.len()
409 }
410
411 /// The system names `table` would build for this world's current content,
412 /// in run order. Runs the same gates [`start`](World::start) runs, so
413 /// tooling that reports a world's schedule cannot drift from the runtime;
414 /// the probe constructs and discards each gated system, which is why
415 /// constructors must stay cheap and side-effect-free. It reads the world
416 /// as it stands: before `start` the table's `complete_world` pass has not
417 /// run, so a system only an injected default turns on is not listed yet,
418 /// and after `start` has drained the gating components it reports the
419 /// systems a rebuild of the CURRENT content would get, not the built set.
420 pub fn system_manifest(&self, table: &SystemTable) -> Vec<&'static str> {
421 table
422 .entries
423 .iter()
424 .filter(|entry| (entry.gate)(self).is_some())
425 .map(|entry| entry.name)
426 .collect()
427 }
428
429 // Disjoint borrows of the system list and the tick's context over the data
430 // half. Splitting the two is what lets a system step against the world it
431 // lives in.
432 fn systems_and_context(&mut self) -> (&mut Vec<BuiltSystem>, PipelineContext<'_>) {
433 (
434 &mut self.systems,
435 PipelineContext {
436 components: &mut self.components,
437 blob: &mut *self.blob,
438 profile: &mut self.profile,
439 resources: &mut self.resources,
440 frame: FrameContext::new(&self.scratch),
441 },
442 )
443 }
444
445 /// Build the systems `table` gates in for this world's content and run
446 /// their `init`.
447 pub fn start(&mut self, table: &SystemTable) -> Result<(), CnResult> {
448 // The host's completion pass, before the gates read the world: an
449 // injected component brings its own system into the schedule. Guarded
450 // by the same once-per-world flag as the build below, so a second
451 // `start` neither re-injects nor re-gates.
452 if !self.systems_built
453 && let Some(complete) = table.complete_world
454 {
455 let mut ctx = self.context();
456 complete(&mut ctx)?;
457 }
458 self.build_systems(table);
459 let (systems, mut ctx) = self.systems_and_context();
460 // The host's load-time pass, before systems init: the engine gives each
461 // loaded placement its per-instance components here.
462 if let Some(before_init) = table.before_init {
463 before_init(&mut ctx);
464 }
465 for system in systems.iter_mut() {
466 system.init(&mut ctx);
467 }
468 // Every system has inited and cached the payloads it keeps; nothing
469 // reads compiled payloads at runtime. Free every blob section still
470 // resident: the shipped runtime's blob 0, the audio / SDF / terrain
471 // blobs the GraphicsSystem init sweep held back for their later
472 // consumers, and every blob in a world with no GraphicsSystem to run
473 // that sweep at all.
474 self.release_payloads();
475 // Access declarations are final once every system has inited, so this
476 // is the earliest the edges can be validated and the waves derived.
477 let schedule = waves::build(&self.systems, self.entries);
478 // Pre-create the event queues declared systems can touch, so their
479 // `events_mut` never grows the store's map mid-tick.
480 if let Some(prepare_events) = table.prepare_events
481 && !schedule.is_empty()
482 {
483 for i in 0..schedule.len() {
484 let access = schedule.access(i);
485 prepare_events(self.event_store(), access);
486 }
487 }
488 self.schedule = Some(schedule);
489 Ok(())
490 }
491
492 // Construct the systems the table gates in, in table order, just before
493 // `init`. Each entry is present only when its gating content is, and is
494 // built from it by the entry's gate. Runs at most once per world (guarded
495 // by `systems_built`) so a system whose gating components survive `init` is
496 // not built twice.
497 fn build_systems(&mut self, table: &SystemTable) {
498 if self.systems_built {
499 return;
500 }
501 self.systems_built = true;
502 self.entries = table.entries;
503 for entry in table.entries {
504 if let Some(system) = (entry.gate)(self) {
505 self.systems.push(BuiltSystem::new(entry.name, system));
506 }
507 }
508 }
509
510 /// Tick -- systems run in order, Done systems are removed.
511 /// Returns Done when no systems remain, Stop on hard halt.
512 pub fn step(&mut self) -> StepResult {
513 // Dev builds sample the tracked heap around the frame and each system
514 // step, so per-frame allocation churn is visible in the profile. The
515 // counters are process-wide: a delta includes concurrent threads
516 // (streaming workers, the pipelined render half), so per-system
517 // attribution is approximate while the frame total is exact churn.
518 #[cfg(debug_assertions)]
519 let frame_alloc_start = crate::memory::alloc_count();
520 // Rotate the profiler's system-timing buffers so the frame that just
521 // finished becomes the readable snapshot for this frame's readers.
522 self.profile.begin_frame();
523 // Advance every event queue once per frame, before systems run, so each
524 // queue's two-frame retention holds for readers that run after the
525 // writer.
526 self.update_events();
527 // Hand the whole frame's scratch back before anything runs.
528 self.reset_scratch();
529 // The host's monotonic clock, read once per tick. A world running
530 // without one records zero micros per system.
531 let clock = self.resources.get::<Clock>().map(|c| c.0);
532 let (systems, mut ctx) = self.systems_and_context();
533 let mut i = 0;
534 let mut removed_any = false;
535 while i < systems.len() {
536 let name = systems[i].name();
537 let started = clock.map_or(0, |now| now());
538 #[cfg(debug_assertions)]
539 let alloc_start = crate::memory::alloc_count();
540 #[cfg(debug_assertions)]
541 crate::ecs::access_check::set_active(Some((systems[i].access(), name)));
542 let result = systems[i].step(&mut ctx);
543 #[cfg(debug_assertions)]
544 crate::ecs::access_check::set_active(None);
545 let micros = clock.map_or(0, |now| {
546 now().saturating_sub(started).min(u32::MAX as u64) as u32
547 });
548 ctx.profile.record_system(name, micros);
549 #[cfg(debug_assertions)]
550 if let (Some(start), Some(end)) = (alloc_start, crate::memory::alloc_count()) {
551 ctx.profile.record_system_allocs(
552 name,
553 end.saturating_sub(start).min(u32::MAX as u64) as u32,
554 );
555 }
556 match result {
557 StepResult::Stop => return StepResult::Stop,
558 StepResult::Done => {
559 systems.remove(i);
560 removed_any = true;
561 }
562 StepResult::Continue => {
563 i += 1;
564 }
565 }
566 }
567 if removed_any && self.schedule.is_some() {
568 self.schedule = Some(waves::build(&self.systems, self.entries));
569 }
570 self.take_scratch_overflows();
571 #[cfg(debug_assertions)]
572 if let (Some(start), Some(end)) = (frame_alloc_start, crate::memory::alloc_count()) {
573 self.profile
574 .set_frame_allocs(end.saturating_sub(start).min(u32::MAX as u64) as u32);
575 }
576 if self.systems.is_empty() {
577 StepResult::Done
578 } else {
579 StepResult::Continue
580 }
581 }
582
583 /// Fold the frame's declined scratch requests into the world's running
584 /// total, returning what this frame declined. A frame that outgrew the
585 /// reserve fell back to the heap and still rendered, so nothing breaks; the
586 /// count is what a host surfaces to say the reserve is undersized.
587 pub fn take_scratch_overflows(&mut self) -> u32 {
588 let overflows = self.scratch.overflows();
589 if overflows > 0 {
590 self.scratch.clear_overflows();
591 self.scratch_overflows = self.scratch_overflows.saturating_add(overflows as u64);
592 }
593 overflows
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use crate::components::TextLabel;
601
602 #[test]
603 fn a_new_world_is_empty() {
604 let world = World::new();
605 assert!(world.is_empty());
606 assert_eq!(world.component_count(), 0);
607 }
608
609 #[test]
610 fn components_are_queryable_after_add() {
611 let mut world = World::new();
612 world.add_component(TextLabel {
613 content: "hello".into(),
614 ..Default::default()
615 });
616 assert!(!world.is_empty());
617 assert_eq!(world.component_count(), 1);
618 assert_eq!(world.query::<TextLabel>().count(), 1);
619 assert_eq!(world.query::<TextLabel>().next().unwrap().content, "hello");
620 }
621
622 #[test]
623 fn reserve_components_leaves_the_world_empty() {
624 let mut world = World::new();
625 world.reserve_components(&[(TextLabel::DISCRIMINANT, 8)]);
626 assert!(world.is_empty());
627 assert_eq!(world.query::<TextLabel>().count(), 0);
628 }
629
630 #[test]
631 fn a_pushed_component_is_reachable_by_its_entity() {
632 let mut world = World::new();
633 let entity = world.push(TextLabel {
634 content: "one".into(),
635 ..Default::default()
636 });
637 assert!(world.is_alive(entity));
638 assert_eq!(world.get::<TextLabel>(entity).unwrap().content, "one");
639 world.get_mut::<TextLabel>(entity).unwrap().content = "two".into();
640 assert_eq!(world.get::<TextLabel>(entity).unwrap().content, "two");
641 world.despawn(entity);
642 assert!(!world.is_alive(entity));
643 }
644
645 #[test]
646 fn remove_all_drains_one_column() {
647 let mut world = World::new();
648 world.add_component(TextLabel::default());
649 world.add_component(TextLabel::default());
650 assert_eq!(world.component_count(), 2);
651 world.remove_all::<TextLabel>();
652 assert!(world.is_empty());
653 }
654
655 #[test]
656 fn the_census_counts_each_populated_type() {
657 let mut world = World::new();
658 world.add_component(TextLabel::default());
659 world.add_component(TextLabel::default());
660 let census = world.component_census();
661 assert_eq!(census, alloc::vec![(TextLabel::DISCRIMINANT, 2)]);
662 }
663
664 #[test]
665 fn resources_round_trip() {
666 let mut world = World::new();
667 assert!(world.resource::<u32>().is_none());
668 world.insert_resource(7u32);
669 assert_eq!(world.resource::<u32>(), Some(&7));
670 *world.resource_mut::<u32>().unwrap() = 9;
671 assert_eq!(world.remove_resource::<u32>(), Some(9));
672 assert!(world.resource::<u32>().is_none());
673 }
674
675 #[test]
676 fn events_are_readable_after_send() {
677 let mut world = World::new();
678 assert!(world.events::<u8>().is_none());
679 world.events_mut::<u8>().send(3);
680 assert_eq!(
681 world.events::<u8>().expect("queue was just created").len(),
682 1
683 );
684 }
685
686 // Two frames' worth of rotation: the queue's retention must outlive one
687 // update so a reader running after the writer still sees the send.
688 #[test]
689 fn update_events_retains_a_send_for_one_frame() {
690 let mut world = World::new();
691 world.events_mut::<u8>().send(3);
692 world.update_events();
693 assert_eq!(world.events::<u8>().unwrap().len(), 1);
694 world.update_events();
695 assert_eq!(world.events::<u8>().unwrap().len(), 0);
696 }
697
698 #[test]
699 fn the_context_sees_the_worlds_components() {
700 let mut world = World::new();
701 world.add_component(TextLabel {
702 content: "ctx".into(),
703 ..Default::default()
704 });
705 let ctx = world.context();
706 assert_eq!(ctx.query::<TextLabel>().next().unwrap().content, "ctx");
707 }
708
709 // The reserve is whole at rest, and a world that never allocated from it
710 // has declined nothing.
711 #[test]
712 fn a_quiet_world_reports_no_scratch_overflow() {
713 let mut world = World::new();
714 assert_eq!(world.take_scratch_overflows(), 0);
715 let stats = world.scratch_stats();
716 assert_eq!(stats.capacity, FRAME_SCRATCH_BYTES);
717 assert_eq!(stats.overflows, 0);
718 }
719
720 #[test]
721 fn an_empty_payload_store_frees_nothing() {
722 let mut world = World::new();
723 assert_eq!(world.release_payloads(), 0);
724 }
725}