khora_data/ecs/world.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
15//! The heart of the CRPECS: the `World` struct.
16
17use std::{
18 any::TypeId,
19 collections::{HashMap, HashSet},
20};
21
22use bincode::config;
23use khora_core::{
24 asset::Material,
25 ecs::entity::EntityId,
26 renderer::api::scene::{GpuMaterial, GpuMesh, Mesh},
27};
28
29use crate::ecs::{
30 components::HandleComponent,
31 entity_store::EntityStore,
32 page::{ComponentPage, PageIndex},
33 planner::QueryPlanner,
34 query::{Query, WorldQuery},
35 registry::ComponentRegistry,
36 serialization::SceneMemoryLayout,
37 storage::StorageManager,
38 Component, ComponentBundle, DomainBitset, LayoutPolicy, MaterialRef, MeshRef, QueryMut,
39 QueryPlan, SemanticDomain, SerializedPage, TypeRegistry,
40};
41
42/// Errors that can occur when adding a component to an entity.
43#[derive(Debug, PartialEq, Eq)]
44pub enum AddComponentError {
45 /// The specified entity does not exist or is not alive.
46 EntityNotFound,
47 /// The specified component type is not registered in the ECS.
48 ComponentNotRegistered,
49 /// The entity already has a component of the specified type.
50 ComponentAlreadyExists,
51}
52
53/// Errors that can occur when removing a single component from an entity.
54#[derive(Debug, PartialEq, Eq)]
55pub enum RemoveComponentError {
56 /// The specified entity does not exist or is not alive.
57 EntityNotFound,
58 /// The specified component type is not registered in the ECS.
59 ComponentNotRegistered,
60 /// The entity does not currently have a component of the specified type.
61 ComponentNotPresent,
62}
63
64/// Errors that can occur while reconstructing a `World` from a raw archetype
65/// memory snapshot in [`World::deserialize_archetype`].
66#[derive(Debug)]
67pub enum DeserializeArchetypeError {
68 /// The outer bincode payload could not be decoded.
69 Decode(bincode::error::DecodeError),
70 /// A serialized component type name is not present in the type registry of
71 /// this `World`, so its column cannot be reconstructed.
72 UnknownComponent(String),
73 /// A column's raw bytes failed validation (misaligned or oversized length).
74 InvalidColumn(super::page::SetFromBytesError),
75}
76
77impl std::fmt::Display for DeserializeArchetypeError {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 match self {
80 DeserializeArchetypeError::Decode(e) => write!(f, "archetype decode failed: {e}"),
81 DeserializeArchetypeError::UnknownComponent(name) => {
82 write!(f, "unknown serialized component type: {name}")
83 }
84 DeserializeArchetypeError::InvalidColumn(e) => {
85 write!(f, "invalid component column: {e}")
86 }
87 }
88 }
89}
90
91impl std::error::Error for DeserializeArchetypeError {}
92
93impl From<bincode::error::DecodeError> for DeserializeArchetypeError {
94 fn from(e: bincode::error::DecodeError) -> Self {
95 DeserializeArchetypeError::Decode(e)
96 }
97}
98
99/// Simple statistics for a semantic domain.
100#[derive(Debug, Default, Clone, Copy)]
101pub struct DomainStats {
102 /// Total number of entities in this domain.
103 pub entity_count: u32,
104 /// Total number of pages allocated for this domain.
105 pub page_count: u32,
106}
107
108/// The central container for the entire ECS, holding all entities, components, and metadata.
109pub struct World {
110 /// Manages entity IDs and metadata.
111 pub(crate) entities: EntityStore,
112 /// Manages component storage and pages.
113 pub(crate) storage: StorageManager,
114 /// Manages query planning and caching.
115 pub(crate) planner: QueryPlanner,
116 /// The type registry for serialization purposes.
117 type_registry: TypeRegistry,
118 /// Monotonic per-domain change counters ("epochs"), indexed by
119 /// [`SemanticDomain::index`]. Every entry point that can change a
120 /// domain's *semantic* content (spawn/despawn, component add/remove,
121 /// mutable access) bumps the matching epoch in O(1). Flows compare
122 /// epochs across frames to decide whether a cached View is still
123 /// valid, so over-bumping is harmless while a missed bump would mean
124 /// stale Views. Representation-only changes (AGDF layout) do NOT bump.
125 domain_epochs: [u64; SemanticDomain::COUNT],
126 /// Process-unique id of this `World` instance. Folded into Flow cache
127 /// keys so a freshly created World (whose epochs restart at zero, e.g.
128 /// a play-mode snapshot restore) can never alias a previous World's
129 /// epoch values and serve a stale cached View.
130 instance_id: u64,
131}
132
133/// Source of process-unique [`World::instance_id`] values.
134static WORLD_INSTANCE_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
135
136impl World {
137 /// (Internal) Allocates a new or recycled `EntityId` and reserves its metadata slot.
138 fn create_entity(&mut self) -> EntityId {
139 self.entities.create_entity()
140 }
141
142 /// (Internal) Finds a page suitable for the given `ComponentBundle`, or creates one if none exists.
143 fn find_or_create_page_for_bundle<B: ComponentBundle>(&mut self) -> u32 {
144 self.storage.find_or_create_page_for_bundle::<B>()
145 }
146
147 /// (Internal) Removes a single physical row from a page via `swap_remove`,
148 /// patching the moved entity's metadata so every domain that referenced the
149 /// vacated slot now points at it.
150 ///
151 /// A mixed-domain bundle is stored in one page but registered under several
152 /// domain keys in [`EntityMetadata::locations`], all addressing the same
153 /// `(page_id, row_index)`. The moved (last-row) entity may likewise reference
154 /// this page under more than one domain, so every one of its locations that
155 /// pointed at the page's old last row is repointed at `location` — patching a
156 /// single domain would leave the others dangling. O(domains) per page, no scan.
157 fn remove_from_page(&mut self, entity_to_despawn: EntityId, location: PageIndex) {
158 let page = &mut self.storage.pages[location.page_id as usize];
159 if page.entities.is_empty() {
160 return;
161 }
162
163 let old_last_row = (page.entities.len() - 1) as u32;
164 let last_entity_in_page = page.entities[old_last_row as usize];
165 page.swap_remove_row(location.row_index);
166
167 // The last row moved into the vacated slot. Repoint every location of the
168 // moved entity that addressed this page's old last row at the new slot.
169 // (When the despawned entity *is* the last row, nothing moved.)
170 if last_entity_in_page != entity_to_despawn {
171 let metadata = self.entities.get_metadata_mut(last_entity_in_page).unwrap();
172 for loc in metadata.locations.values_mut() {
173 if loc.page_id == location.page_id && loc.row_index == old_last_row {
174 *loc = location;
175 }
176 }
177 }
178 }
179
180 /// Finds or creates a page for the given signature of component `TypeId`s.
181 fn find_or_create_page_for_signature(&mut self, signature: &[TypeId]) -> u32 {
182 self.storage.find_or_create_page_for_signature(signature)
183 }
184
185 /// Creates a new, empty `World` with pre-registered internal component types.
186 pub fn new() -> Self {
187 let mut world = Self {
188 entities: EntityStore::new(),
189 storage: StorageManager::new(ComponentRegistry::default()),
190 planner: QueryPlanner::new(),
191 type_registry: TypeRegistry::default(),
192 domain_epochs: [0; SemanticDomain::COUNT],
193 instance_id: WORLD_INSTANCE_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
194 };
195 // Generic and hand-implemented components can't self-register via the
196 // derive (generics have no single `TypeId`; `MaterialRef` holds a trait
197 // object and has a manual `Component` impl), so they stay explicit.
198 // CollisionPairs is **not** an ECS component — it lives in `Resources`
199 // as `Arc<Mutex<CollisionPairs>>`.
200 world.register_component::<HandleComponent<Mesh>>(SemanticDomain::Render);
201 world.register_component::<HandleComponent<GpuMesh>>(SemanticDomain::Render);
202 world.register_component::<HandleComponent<GpuMaterial>>(SemanticDomain::Render);
203 world.register_component::<HandleComponent<Box<dyn Material>>>(SemanticDomain::Render);
204 world.register_component::<MaterialRef>(SemanticDomain::Render);
205 world.register_component::<MeshRef>(SemanticDomain::Render);
206
207 // Auto-register every component that declares its domain via
208 // `#[derive(Component)]` + `#[component(domain = ...)]`. Idempotent with the
209 // explicit calls above (same TypeId → same vtable) during migration.
210 for reg in inventory::iter::<crate::ecs::ComponentDomainRegistration> {
211 (reg.register)(&mut world);
212 }
213
214 world
215 }
216
217 /// Returns the [`SemanticDomain`] a component type was registered with,
218 /// or `None` if it isn't registered (yet) on this world. Used by the
219 /// editor to categorise components in the "Add Component" menu and the
220 /// inspector without hard-coding a per-type table.
221 pub fn component_domain(&self, type_id: TypeId) -> Option<SemanticDomain> {
222 self.storage.registry.get_domain(type_id)
223 }
224
225 /// The [`LayoutPolicy`] a component type is currently stored with. Defaults
226 /// to `Soa`; the layout-adaptation pass may change it.
227 pub fn component_layout(&self, type_id: TypeId) -> Option<LayoutPolicy> {
228 self.storage.registry.layout_of(type_id)
229 }
230
231 /// Online access stats `(query_count, rows_scanned)` for a component type —
232 /// the DCC / telemetry read these to drive memory-layout adaptation. The DCC
233 /// only *observes*; it never mutates the layout (Data self-optimizes).
234 pub fn component_access_stats(&self, type_id: TypeId) -> Option<(u64, u64)> {
235 self.storage.registry.access_stats(type_id)
236 }
237
238 /// The number of live entities — the coarse workload size `n` the DCC's
239 /// cost model fits agent execution time against.
240 pub fn entity_count(&self) -> usize {
241 self.entities.len()
242 }
243
244 /// A snapshot of every registered component's access pattern as
245 /// `(type_name, size_bytes, query_count, rows_scanned)`. The hot path
246 /// samples this at a low rate and publishes it through the observation
247 /// tunnel; the DCC turns it into a read-only layout recommendation.
248 pub fn component_access_snapshot(&self) -> Vec<(String, usize, u64, u64)> {
249 self.storage
250 .registry
251 .access_snapshot()
252 .into_iter()
253 .map(|(tid, size, qc, rows)| {
254 let name = self
255 .type_registry
256 .get_name_of(&tid)
257 .unwrap_or("<unknown>")
258 .to_string();
259 (name, size, qc, rows)
260 })
261 .collect()
262 }
263
264 /// Current change epoch of `domain` — a monotonic counter bumped by
265 /// every mutation entry point that can affect the domain's semantic
266 /// content. Equal epochs across two reads guarantee the domain's data
267 /// (and its query iteration order) is unchanged; a different value
268 /// only means "possibly changed" (bumps are conservative). Flows use
269 /// this to validate cached Views.
270 pub fn domain_epoch(&self, domain: SemanticDomain) -> u64 {
271 self.domain_epochs[domain.index()]
272 }
273
274 /// Process-unique identifier of this `World` instance. Cache keys
275 /// derived from [`domain_epoch`](Self::domain_epoch) must include it so
276 /// that epochs from two different World instances never compare equal.
277 pub fn instance_id(&self) -> u64 {
278 self.instance_id
279 }
280
281 /// (Internal) Marks `domain` as semantically changed. O(1).
282 pub(crate) fn bump_domain_epoch(&mut self, domain: SemanticDomain) {
283 self.domain_epochs[domain.index()] = self.domain_epochs[domain.index()].wrapping_add(1);
284 }
285
286 /// (Internal) Marks every domain as semantically changed — used by bulk
287 /// paths (deserialization, compaction) where per-domain attribution is
288 /// not worth the bookkeeping. Over-invalidation is always safe.
289 pub(crate) fn bump_all_domain_epochs(&mut self) {
290 for epoch in &mut self.domain_epochs {
291 *epoch = epoch.wrapping_add(1);
292 }
293 }
294
295 /// Spawns a new entity with the given bundle of components.
296 ///
297 /// This is the primary method for creating entities. It orchestrates the entire process:
298 /// 1. Allocates a new `EntityId`.
299 /// 2. Finds or creates a suitable `ComponentPage` for the component bundle.
300 /// 3. Pushes the component data into the page's columns.
301 /// 4. Updates the entity's metadata to point to the new data's location.
302 /// 5. Updates domain bitsets and stats for the newly created entity components.
303 ///
304 /// Returns the `EntityId` of the newly created entity.
305 pub fn spawn<B: ComponentBundle>(&mut self, bundle: B) -> EntityId {
306 // Step 1: Allocate a new EntityId.
307 let entity_id = self.create_entity();
308
309 // Step 2: Find or create a page for this bundle.
310 let page_id = self.find_or_create_page_for_bundle::<B>();
311
312 // --- Step 3: Push component data into the page. ---
313 let row_index;
314 {
315 let page = &mut self.storage.pages[page_id as usize];
316 row_index = page.entities.len() as u32;
317 // SAFETY: `page` was just resolved by `find_or_create_page_for_bundle::<B>()`
318 // (line above), which guarantees its column layout matches `B::component_types()`.
319 // `bundle.add_to_page` requires that every column type in the bundle has a
320 // matching column on the page; that invariant is upheld by the page-discovery
321 // step. Exclusive access is held via `&mut page`.
322 unsafe {
323 bundle.add_to_page(page);
324 }
325 page.add_entity(entity_id);
326 }
327
328 // --- Step 4: Update the entity's metadata. ---
329 let location = PageIndex { page_id, row_index };
330 let metadata = self.entities.get_metadata_mut(entity_id).unwrap();
331 B::update_metadata(metadata, location, &self.storage.registry);
332
333 // --- Step 5: Update domain bitsets and stats for the newly created entity components. ---
334 for domain in metadata.locations.keys() {
335 self.storage
336 .domain_bitsets
337 .entry(*domain)
338 .or_default()
339 .set(entity_id.index);
340
341 self.storage
342 .domain_stats
343 .entry(*domain)
344 .or_default()
345 .entity_count += 1;
346
347 // A new entity appeared in this domain — invalidate cached
348 // Views. Inline field access: `metadata` still borrows
349 // `self.entities`, so `bump_domain_epoch(&mut self)` can't be
350 // called here.
351 self.domain_epochs[domain.index()] = self.domain_epochs[domain.index()].wrapping_add(1);
352 }
353
354 entity_id
355 }
356
357 /// Despawns an entity, removing all its components and freeing its ID for recycling.
358 ///
359 /// This method performs the following steps:
360 /// 1. Verifies that the `EntityId` is valid by checking its index and generation.
361 /// 2. Removes the entity's component data from all pages where it is stored.
362 /// 3. Marks the entity's metadata slot as vacant and adds its index to the free list.
363 ///
364 /// Returns `true` if the entity was valid and despawned, `false` otherwise.
365 pub fn despawn(&mut self, entity_id: EntityId) -> bool {
366 // Step 1: Validate the EntityId.
367 // First, check if the index is even valid for our entities Vec.
368 if entity_id.index as usize >= self.entities.len() {
369 return false;
370 }
371
372 // Get the data at the slot.
373 let (id_in_world, metadata_slot) = self.entities.get(entity_id.index as usize).unwrap();
374
375 // An ID is valid if its generation matches the one in the world,
376 // AND if the metadata slot is currently occupied (`is_some`).
377 if id_in_world.generation != entity_id.generation || metadata_slot.is_none() {
378 return false;
379 }
380
381 // --- At this point, the ID is valid. ---
382
383 // Step 2: Take the metadata out of the slot, leaving it `None`.
384 // This is what officially "kills" the entity.
385 let metadata = self
386 .entities
387 .get_mut(entity_id.index as usize)
388 .unwrap()
389 .1
390 .take()
391 .unwrap();
392 self.entities.freed_entities.push(entity_id.index);
393
394 // --- Step 3: Remove the entity's data and update per-domain bookkeeping. ---
395 // A mixed-domain bundle lives in one page but is registered under several
396 // domain keys that all address the same `(page_id, row_index)`. The
397 // physical row must be `swap_remove`d exactly once — calling it per domain
398 // key would remove the moved survivor's data on the second pass — while the
399 // bitset/stats/epoch bookkeeping still runs for every domain the entity
400 // belonged to.
401 let mut removed_rows: HashSet<PageIndex> = HashSet::new();
402 for (domain, location) in metadata.locations {
403 if removed_rows.insert(location) {
404 self.remove_from_page(entity_id, location);
405 }
406
407 // Clear the entity's bit in the domain bitset and update stats.
408 if let Some(bitset) = self.storage.domain_bitsets.get_mut(&domain) {
409 bitset.clear(entity_id.index);
410 }
411 if let Some(stats) = self.storage.domain_stats.get_mut(&domain) {
412 stats.entity_count = stats.entity_count.saturating_sub(1);
413 }
414
415 // An entity left this domain (and `remove_from_page` may have
416 // reordered the page) — invalidate cached Views.
417 self.bump_domain_epoch(domain);
418 }
419 true
420 }
421
422 /// Creates an iterator that queries the world for entities matching a set of components and filters.
423 ///
424 /// This is the primary method for reading and writing data in the ECS. The query `Q`
425 /// is specified as a tuple via turbofish syntax. It can include component references
426 /// (e.g., `&Position`, `&mut Velocity`) and filters (e.g., `Without<Parent>`).
427 ///
428 /// This method is very cheap to call. It performs an efficient search to identify all
429 /// `ComponentPage`s that satisfy the query's criteria. The returned iterator then
430 /// efficiently iterates over the data in only those pages.
431 ///
432 /// # Examples
433 ///
434 /// ```rust,ignore
435 /// // Find all entities with a `Transform` and `GlobalTransform`.
436 /// for (transform, global) in world.query::<(&Transform, &GlobalTransform)>() {
437 /// // ...
438 /// }
439 ///
440 /// // Find all root entities (those with a `Transform` but without a `Parent`).
441 /// for (transform,) in world.query::<(&Transform, Without<Parent>)>() {
442 /// // ...
443 /// }
444 /// ```
445 pub fn query<'a, Q: WorldQuery>(&'a self) -> Query<'a, Q> {
446 let type_ids = Q::type_ids();
447
448 // 1. Try to fetch the strategy plan from the cache.
449 // We cache the execution logic (Native vs Transversal), not the page indices.
450 let plan = {
451 let cache = self
452 .planner
453 .query_cache
454 .read()
455 .unwrap_or_else(|e| e.into_inner());
456 if let Some(plan) = cache.get(&type_ids) {
457 plan.clone()
458 } else {
459 drop(cache);
460 let new_plan = self.analyze_query(&type_ids);
461 let mut cache = self
462 .planner
463 .query_cache
464 .write()
465 .unwrap_or_else(|e| e.into_inner());
466 cache.insert(type_ids.clone(), new_plan.clone());
467 new_plan
468 }
469 };
470
471 // 2. Dynamically find matching pages for this call.
472 // This ensures the query is correct even if new archetypes were created
473 // in a different domain since the last call.
474 let matching_page_indices =
475 self.find_matching_pages(&plan.driver_signature, &Q::without_type_ids());
476
477 // Record one access observation per queried component (coarse, off the
478 // per-element path): count the query and the rows it scans. The DCC reads
479 // these to drive adaptive memory layout — observation only, never control.
480 let rows_scanned: u64 = matching_page_indices
481 .iter()
482 .map(|&pid| self.storage.pages[pid as usize].row_count() as u64)
483 .sum();
484 self.storage.registry.record_access(&type_ids, rows_scanned);
485
486 // 3. Return the query with the plan and the current matching pages.
487 Query::new(self, plan, matching_page_indices)
488 }
489
490 /// Creates a mutable iterator that queries the world for entities matching a set of components and filters.
491 ///
492 /// This method is similar to `query`, but it allows mutable access to the components.
493 /// It uses the same dynamic plan re-finding to ensure thread-safe consistency.
494 pub fn query_mut<'a, Q: WorldQuery>(&'a mut self) -> QueryMut<'a, Q> {
495 let type_ids = Q::type_ids();
496
497 // 1. Get strategy from cache
498 let plan = {
499 let cache = self
500 .planner
501 .query_cache
502 .read()
503 .unwrap_or_else(|e| e.into_inner());
504 if let Some(plan) = cache.get(&type_ids) {
505 plan.clone()
506 } else {
507 drop(cache);
508 let new_plan = self.analyze_query(&type_ids);
509 let mut cache = self
510 .planner
511 .query_cache
512 .write()
513 .unwrap_or_else(|e| e.into_inner());
514 cache.insert(type_ids.clone(), new_plan.clone());
515 new_plan
516 }
517 };
518
519 // 2. Dynamically find pages
520 let matching_page_indices =
521 self.find_matching_pages(&plan.driver_signature, &Q::without_type_ids());
522
523 // Record one access observation per queried component (see `query`).
524 let rows_scanned: u64 = matching_page_indices
525 .iter()
526 .map(|&pid| self.storage.pages[pid as usize].row_count() as u64)
527 .sum();
528 self.storage.registry.record_access(&type_ids, rows_scanned);
529
530 // The caller may write through every `&mut` term of the query:
531 // mark those domains changed ONCE here, at construction — never
532 // per row, which would put the bump on the iteration hot path.
533 for type_id in Q::mutable_type_ids() {
534 if let Some(domain) = self.storage.registry.get_domain(type_id) {
535 self.bump_domain_epoch(domain);
536 }
537 }
538
539 // 3. Construct the iterator
540 QueryMut::new(self, plan, matching_page_indices)
541 }
542
543 /// Registers a component type with a specific semantic domain.
544 ///
545 /// This is a crucial setup step. Before a component of type `T` can be used
546 /// in a bundle, it must be registered with the world to define which semantic
547 /// page group its data will be stored in.
548 pub fn register_component<T: Component>(&mut self, domain: SemanticDomain) {
549 self.storage.registry.register::<T>(domain);
550 self.type_registry.register::<T>();
551 }
552
553 /// Analyzes a query's component signature to create an optimized execution plan.
554 ///
555 /// This method identifies if a query is transversal (spanning multiple domains)
556 /// and selects the most efficient "Driver Domain" based on entity density.
557 pub(crate) fn analyze_query(&self, type_ids: &[TypeId]) -> QueryPlan {
558 let mut domains = HashSet::new();
559 for type_id in type_ids {
560 if let Some(domain) = self.storage.registry.get_domain(*type_id) {
561 domains.insert(domain);
562 }
563 }
564
565 if domains.len() <= 1 {
566 // NATIVE MODE: All components belong to the same semantic domain (or none).
567 // This is the fastest execution path as it avoids any cross-domain joins.
568 let first_domain = domains.into_iter().next();
569 let plan = QueryPlan::new(false, first_domain, HashSet::new(), type_ids.to_vec());
570 return plan;
571 }
572
573 // TRANSVERSAL MODE
574 // Select the domain with the highest density of entities as the driver domain.
575 // Highest density = most entities per page => page_A_count * entity_B_count < page_B_count * entity_A_count
576 let driver_domain = domains
577 .iter()
578 .min_by(|&&a, &&b| {
579 let stats_a = self
580 .storage
581 .domain_stats
582 .get(&a)
583 .copied()
584 .unwrap_or_default();
585 let stats_b = self
586 .storage
587 .domain_stats
588 .get(&b)
589 .copied()
590 .unwrap_or_default();
591 let score_a = (stats_a.page_count as u64) * (stats_b.entity_count as u64);
592 let score_b = (stats_b.page_count as u64) * (stats_a.entity_count as u64);
593 score_a.cmp(&score_b)
594 })
595 .copied()
596 .unwrap(); // Should always be Some if domains.len() > 1
597
598 let mut peer_domains = domains;
599 peer_domains.remove(&driver_domain);
600
601 // Calculate driver signature (subset of type_ids in the driver domain)
602 let mut driver_signature = Vec::new();
603 for type_id in type_ids {
604 if self.storage.registry.get_domain(*type_id) == Some(driver_domain) {
605 driver_signature.push(*type_id);
606 }
607 }
608 driver_signature.sort();
609
610 // Initialize the final plan.
611 // In transversal mode, the driver signature is the subset of components
612 // that belong to the driver domain.
613 QueryPlan::new(true, Some(driver_domain), peer_domains, driver_signature)
614 }
615
616 /// Internal helper to find pages matching a signature and filter.
617 fn find_matching_pages(&self, type_ids: &[TypeId], without_type_ids: &[TypeId]) -> Vec<u32> {
618 let mut matching_page_indices = Vec::new();
619 'page_loop: for (page_id, page) in self.storage.pages.iter().enumerate() {
620 for required_type in type_ids {
621 if page.type_ids.binary_search(required_type).is_err() {
622 continue 'page_loop;
623 }
624 }
625 for excluded_type in without_type_ids {
626 if page.type_ids.binary_search(excluded_type).is_ok() {
627 continue 'page_loop;
628 }
629 }
630 matching_page_indices.push(page_id as u32);
631 }
632 matching_page_indices
633 }
634
635 /// (Internal) Computes a bitset that represents the intersection of all domains
636 /// involved in a transversal query. This is used to speed up joins by skipping
637 /// metadata lookups for entities that are guaranteed to not satisfy the query.
638 pub(crate) fn compute_query_bitset(&self, plan: &QueryPlan) -> Option<DomainBitset> {
639 if plan.mode == crate::ecs::QueryMode::Native {
640 return None;
641 }
642
643 let driver_domain = plan.driver_domain?;
644
645 // Start with the driver domain's bitset.
646 let mut bitset = self.storage.domain_bitsets.get(&driver_domain)?.clone();
647
648 // Intersect with all peer domains.
649 for peer_domain in &plan.peer_domains {
650 if let Some(peer_bitset) = self.storage.domain_bitsets.get(peer_domain) {
651 bitset.intersect(peer_bitset);
652 } else {
653 // If a required peer domain has NO components at all, the intersection is empty.
654 return Some(DomainBitset::new());
655 }
656 }
657
658 Some(bitset)
659 }
660
661 /// This operation is designed to be fast. It performs the necessary data
662 /// migration to move the entity's components for the given `SemanticDomain`
663 /// to a new `ComponentPage` that matches the new layout.
664 ///
665 /// Crucially, it does NOT clean up the "hole" left in the old page. Instead,
666 /// it returns the location of the orphaned data, delegating the cleanup task
667 /// to an asynchronous garbage collection system.
668 ///
669 /// # Returns
670 ///
671 /// - `Ok(Option<PageIndex>)`: On success. The `Option` contains the location of
672 /// orphaned data if a migration occurred, which should be sent to a garbage collector.
673 /// It is `None` if no migration was needed (e.g., adding to a new domain).
674 /// - `Err(AddComponentError)`: If the operation failed (e.g., entity not alive,
675 /// component not registered, or component already present).
676 pub fn add_component<C: Component>(
677 &mut self,
678 entity_id: EntityId,
679 component: C,
680 ) -> Result<Option<PageIndex>, AddComponentError> {
681 // 1. Validate EntityId and get metadata
682 let Some((id_in_world, Some(_))) = self.entities.get(entity_id.index as usize) else {
683 return Err(AddComponentError::EntityNotFound);
684 };
685
686 if id_in_world.generation != entity_id.generation {
687 return Err(AddComponentError::EntityNotFound);
688 }
689
690 let Some(domain) = self.storage.registry.get_domain(TypeId::of::<C>()) else {
691 return Err(AddComponentError::ComponentNotRegistered);
692 };
693
694 let mut metadata = self
695 .entities
696 .get_mut(entity_id.index as usize)
697 .unwrap()
698 .1
699 .take()
700 .unwrap();
701 let old_location_opt = metadata.locations.get(&domain).copied();
702
703 // 2. Determine old and new page signatures
704 let old_type_ids = old_location_opt.map_or(Vec::new(), |loc| {
705 self.storage.pages[loc.page_id as usize].type_ids.clone()
706 });
707 let mut new_type_ids = old_type_ids.clone();
708 new_type_ids.push(TypeId::of::<C>());
709 new_type_ids.sort();
710 new_type_ids.dedup();
711
712 if new_type_ids == old_type_ids {
713 self.entities.get_mut(entity_id.index as usize).unwrap().1 = Some(metadata); // Put it back
714 return Err(AddComponentError::ComponentAlreadyExists);
715 }
716
717 // 3. Find or create the destination page
718 let dest_page_id = self.find_or_create_page_for_signature(&new_type_ids);
719
720 // 4. Perform the migration
721 let dest_row_index;
722 unsafe {
723 // SAFETY: when an old location exists it lives on a different page
724 // than `dest_page_id` (the equal-page case is unreachable — a
725 // differing signature guarantees a different page), so `src_page`
726 // and `dest_page` are borrowed disjointly through raw pointers into
727 // `storage.pages`.
728 let (src_page_opt, dest_page) = if let Some(loc) = old_location_opt {
729 if loc.page_id == dest_page_id {
730 unreachable!(
731 "same-page migration must be caught by the earlier signature check"
732 );
733 } else {
734 let all_pages_ptr = self.storage.pages.as_mut_ptr();
735 let dest_page = &mut *all_pages_ptr.add(dest_page_id as usize);
736 let src_page = &*all_pages_ptr.add(loc.page_id as usize);
737 (Some(src_page), dest_page)
738 }
739 } else {
740 (None, &mut self.storage.pages[dest_page_id as usize])
741 };
742
743 dest_row_index = dest_page.entities.len() as u32;
744
745 if let Some(src_page) = src_page_opt {
746 let src_row = old_location_opt.unwrap().row_index as usize;
747 for type_id in &old_type_ids {
748 let copier = self.storage.registry.get_row_copier(type_id).unwrap();
749 let src_col = src_page.columns.get(type_id).unwrap();
750 let dest_col = dest_page.columns.get_mut(type_id).unwrap();
751 copier(src_col.as_ref(), src_row, dest_col.as_mut());
752 }
753 }
754
755 dest_page
756 .columns
757 .get_mut(&TypeId::of::<C>())
758 .unwrap()
759 .as_any_mut()
760 .downcast_mut::<Vec<C>>()
761 .unwrap()
762 .push(component);
763
764 dest_page.add_entity(entity_id);
765 }
766
767 // 5. Update metadata and put it back.
768 //
769 // The migration copied the entity's *whole* archetype row (every
770 // component in the source page, across all its domains) into the
771 // destination page. A multi-domain entity is stored in one page under
772 // several domain keys all addressing the same `(page, row)` (see
773 // `remove_from_page`), so repoint EVERY co-located domain — not just the
774 // added component's — to keep the entity in one page (the CRPECS
775 // archetype model) and leave the old row fully dead (reclaimable by
776 // compaction) instead of a partial orphan with duplicated columns.
777 let new_location = PageIndex {
778 page_id: dest_page_id,
779 row_index: dest_row_index,
780 };
781 match old_location_opt {
782 Some(old) => {
783 for loc in metadata.locations.values_mut() {
784 if *loc == old {
785 *loc = new_location;
786 }
787 }
788 }
789 // First component in this domain — no prior row to migrate from.
790 None => {
791 metadata.locations.insert(domain, new_location);
792 }
793 }
794
795 // Update the domain bitset for the entity.
796 self.storage
797 .domain_bitsets
798 .entry(domain)
799 .or_default()
800 .set(entity_id.index);
801
802 self.entities.get_mut(entity_id.index as usize).unwrap().1 = Some(metadata);
803
804 // The entity gained a component in this domain — invalidate cached Views.
805 self.bump_domain_epoch(domain);
806
807 // 6. Record the abandoned source page so maintenance compacts its
808 // now-orphaned row later (see `StorageManager::dirty_pages`). The
809 // `None` case adds the entity to a domain for the first time, leaving
810 // no orphan behind.
811 if let Some(old) = old_location_opt {
812 self.storage.dirty_pages.insert(old.page_id);
813 }
814
815 // 7. Return the old location for cleanup, without performing swap_remove
816 Ok(old_location_opt)
817 }
818
819 /// Removes a **single** component `C` from `entity`, preserving every
820 /// other component the entity carries (in any domain).
821 ///
822 /// Mirrors [`add_component`](Self::add_component) in reverse: rebuilds
823 /// the entity's domain page signature without `C`, finds-or-creates a
824 /// page matching the new signature, and copies the surviving
825 /// same-domain components there. The old slot is orphaned and reported
826 /// for the GC, exactly like an add migration.
827 ///
828 /// Use this — not [`remove_component_domain`](Self::remove_component_domain) —
829 /// for surgical component removal (editor "delete component" button,
830 /// `set_parent` unparenting, AGDF demotion). `remove_component_domain`
831 /// is a low-level primitive that drops the entire domain bucket and
832 /// should be reserved for entity teardown / GC paths.
833 ///
834 /// # Returns
835 ///
836 /// - `Ok(Some(PageIndex))` — old location to send to the GC.
837 /// - `Ok(None)` — should not happen in practice (kept for symmetry with
838 /// `add_component`).
839 /// - `Err(RemoveComponentError::EntityNotFound)` — entity dead or stale.
840 /// - `Err(RemoveComponentError::ComponentNotRegistered)` — type unknown.
841 /// - `Err(RemoveComponentError::ComponentNotPresent)` — entity didn't
842 /// carry this component to begin with.
843 pub fn remove_component<C: Component>(
844 &mut self,
845 entity_id: EntityId,
846 ) -> Result<Option<PageIndex>, RemoveComponentError> {
847 // 1. Validate the entity.
848 let Some((id_in_world, Some(_))) = self.entities.get(entity_id.index as usize) else {
849 return Err(RemoveComponentError::EntityNotFound);
850 };
851 if id_in_world.generation != entity_id.generation {
852 return Err(RemoveComponentError::EntityNotFound);
853 }
854
855 // 2. Resolve the component's domain.
856 let Some(domain) = self.storage.registry.get_domain(TypeId::of::<C>()) else {
857 return Err(RemoveComponentError::ComponentNotRegistered);
858 };
859
860 // Take metadata out so we can mutate `self.storage` freely.
861 let mut metadata = self
862 .entities
863 .get_mut(entity_id.index as usize)
864 .unwrap()
865 .1
866 .take()
867 .unwrap();
868
869 let Some(loc) = metadata.locations.get(&domain).copied() else {
870 // Entity isn't in this domain at all.
871 self.entities.get_mut(entity_id.index as usize).unwrap().1 = Some(metadata);
872 return Err(RemoveComponentError::ComponentNotPresent);
873 };
874
875 let target_type = TypeId::of::<C>();
876 let old_type_ids = self.storage.pages[loc.page_id as usize].type_ids.clone();
877 if !old_type_ids.contains(&target_type) {
878 // Entity is in this domain but doesn't have C specifically.
879 self.entities.get_mut(entity_id.index as usize).unwrap().1 = Some(metadata);
880 return Err(RemoveComponentError::ComponentNotPresent);
881 }
882
883 // 3. Build the new domain signature (sans C).
884 let new_type_ids: Vec<TypeId> = old_type_ids
885 .iter()
886 .copied()
887 .filter(|t| *t != target_type)
888 .collect();
889
890 // 4. If C was the last component in this domain, just drop the
891 // domain location and bitset bit. No page migration needed.
892 if new_type_ids.is_empty() {
893 metadata.locations.remove(&domain);
894 if let Some(bitset) = self.storage.domain_bitsets.get_mut(&domain) {
895 bitset.clear(entity_id.index);
896 }
897 self.entities.get_mut(entity_id.index as usize).unwrap().1 = Some(metadata);
898 // The entity left this domain entirely — invalidate cached Views.
899 self.bump_domain_epoch(domain);
900 // The row is orphaned (metadata no longer references it) — schedule
901 // its page for compaction.
902 self.storage.dirty_pages.insert(loc.page_id);
903 return Ok(Some(loc));
904 }
905
906 // 5. Find or create the destination page for the reduced signature.
907 let dest_page_id = self.find_or_create_page_for_signature(&new_type_ids);
908
909 // 6. Migrate every surviving same-domain component from src → dest.
910 let dest_row_index;
911 unsafe {
912 // SAFETY: src and dest are different pages (signatures differ),
913 // so we can borrow them disjointly through raw pointers.
914 let all_pages_ptr = self.storage.pages.as_mut_ptr();
915 let dest_page = &mut *all_pages_ptr.add(dest_page_id as usize);
916 let src_page = &*all_pages_ptr.add(loc.page_id as usize);
917 assert_ne!(
918 loc.page_id, dest_page_id,
919 "remove_component: src/dest aliased"
920 );
921
922 dest_row_index = dest_page.entities.len() as u32;
923 let src_row = loc.row_index as usize;
924
925 for type_id in &new_type_ids {
926 let copier = self.storage.registry.get_row_copier(type_id).unwrap();
927 let src_col = src_page.columns.get(type_id).unwrap();
928 let dest_col = dest_page.columns.get_mut(type_id).unwrap();
929 copier(src_col.as_ref(), src_row, dest_col.as_mut());
930 }
931
932 dest_page.add_entity(entity_id);
933 }
934
935 // 7. Update entity metadata to point at the new (page, row). As in
936 // `add_component`, the whole archetype row migrated, so repoint every
937 // co-located domain (not just this one) to keep the entity in one page
938 // and leave the old row fully dead.
939 let new_location = PageIndex {
940 page_id: dest_page_id,
941 row_index: dest_row_index,
942 };
943 for l in metadata.locations.values_mut() {
944 if *l == loc {
945 *l = new_location;
946 }
947 }
948 // The bitset stays set — other components remain in this domain.
949 self.entities.get_mut(entity_id.index as usize).unwrap().1 = Some(metadata);
950
951 // The entity lost a component in this domain — invalidate cached Views.
952 self.bump_domain_epoch(domain);
953
954 // 8. Record the abandoned source page so maintenance compacts its
955 // now-orphaned row later.
956 self.storage.dirty_pages.insert(loc.page_id);
957
958 // 9. Hand the old location off to the GC.
959 Ok(Some(loc))
960 }
961
962 /// Logically removes all components belonging to a specific `SemanticDomain` from an entity.
963 ///
964 /// This is an extremely fast, O(1) operation that only modifies the entity's
965 /// metadata. It does not immediately deallocate or move any component data.
966 /// The component data is "orphaned" and will be cleaned up later by a
967 /// garbage collection process.
968 ///
969 /// This method is generic over a component `C` to determine which domain to remove.
970 ///
971 /// # Returns
972 ///
973 /// - `Some(PageIndex)`: Contains the location of the orphaned data if the
974 /// components were successfully removed. This can be sent to a garbage collector.
975 /// - `None`: If the entity is not alive or did not have any components in the
976 /// specified `SemanticDomain`.
977 pub fn remove_component_domain<C: Component>(
978 &mut self,
979 entity_id: EntityId,
980 ) -> Option<PageIndex> {
981 // 1. Validate the entity ID to ensure we're acting on a live entity.
982 let (id_in_world, metadata_slot) = self.entities.get_mut(entity_id.index as usize)?;
983 if id_in_world.generation != entity_id.generation || metadata_slot.is_none() {
984 return None;
985 }
986
987 // 2. Use the registry to find the component's domain.
988 let domain = self.storage.registry.get_domain(TypeId::of::<C>())?;
989
990 // 3. Remove the location entry from the entity's metadata.
991 // `HashMap::remove` returns the value that was at that key, which is exactly what we need.
992 let metadata = metadata_slot.as_mut().unwrap();
993 let location = metadata.locations.remove(&domain);
994
995 // Clear the domain bitset if a component was removed.
996 if let Some(loc) = location {
997 if let Some(bitset) = self.storage.domain_bitsets.get_mut(&domain) {
998 bitset.clear(entity_id.index);
999 }
1000 // The entity left this domain — invalidate cached Views.
1001 self.bump_domain_epoch(domain);
1002 // The row is orphaned (metadata no longer references it) — schedule
1003 // its page for compaction.
1004 self.storage.dirty_pages.insert(loc.page_id);
1005 }
1006
1007 location
1008 }
1009
1010 /// Gets a mutable reference to a single component `T` for a given entity.
1011 ///
1012 /// This provides direct, "random" access to a component, which can be less
1013 /// performant than querying but is useful for targeted modifications.
1014 ///
1015 /// # Returns
1016 ///
1017 /// `None` if the entity is not alive or does not have the requested component.
1018 pub fn get_mut<T: Component>(&mut self, entity_id: EntityId) -> Option<&mut T> {
1019 // 1. Validate the entity ID. An out-of-range index means "not alive"
1020 // (e.g. a stale or malformed EntityId), so return None rather than panic.
1021 let (id_in_world, metadata_opt) = self.entities.get(entity_id.index as usize)?;
1022 if id_in_world.generation != entity_id.generation || metadata_opt.is_none() {
1023 return None;
1024 }
1025 let metadata = metadata_opt.as_ref().unwrap();
1026
1027 // 2. Use the registry to find the component's domain and its location.
1028 let domain = self.storage.registry.get_domain(TypeId::of::<T>())?;
1029 let location = metadata.locations.get(&domain)?;
1030
1031 // Handing out `&mut T` means T may change — invalidate cached
1032 // Views. Inline field access: `metadata` still borrows
1033 // `self.entities`, so `bump_domain_epoch(&mut self)` can't be
1034 // called here.
1035 self.domain_epochs[domain.index()] = self.domain_epochs[domain.index()].wrapping_add(1);
1036
1037 // 3. Get the component data from the page.
1038 let type_id = TypeId::of::<T>();
1039 let page = self.storage.pages.get_mut(location.page_id as usize)?;
1040 let column = page.columns.get_mut(&type_id)?;
1041 let vec = column.as_any_mut().downcast_mut::<Vec<T>>()?;
1042
1043 vec.get_mut(location.row_index as usize)
1044 }
1045
1046 /// Gets mutable references to components of type `T` for multiple entities simultaneously.
1047 ///
1048 /// This is safer than `get_mut` in a loop because it allows retrieving multiple
1049 /// disjoint mutable references to components of the same type.
1050 ///
1051 /// # Returns
1052 ///
1053 /// An array of `Option<&mut T>`. If any entity is not found, does not have the component,
1054 /// or if there are duplicate requests for the same component instance, that entry will be `None`.
1055 pub fn get_many_mut<T: Component, const N: usize>(
1056 &mut self,
1057 ids: [EntityId; N],
1058 ) -> [Option<&mut T>; N] {
1059 let mut results: [Option<&mut T>; N] = std::array::from_fn(|_| None);
1060
1061 let type_id = TypeId::of::<T>();
1062 let domain = match self.storage.registry.get_domain(type_id) {
1063 Some(d) => d,
1064 None => return results,
1065 };
1066
1067 // Handing out `&mut T` references means T may change — invalidate
1068 // cached Views (conservative: bumped even if no entity resolves).
1069 self.bump_domain_epoch(domain);
1070
1071 // 1. Collect locations and check for duplicates
1072 let mut locations = [(0u32, 0u32); N];
1073 let mut found_mask = [false; N];
1074
1075 for i in 0..N {
1076 if let Some((stored_id, Some(metadata))) = self.entities.get(ids[i].index as usize) {
1077 // Ensure the EntityId matches (including generation)
1078 if *stored_id == ids[i] {
1079 if let Some(loc) = metadata.locations.get(&domain) {
1080 locations[i] = (loc.page_id, loc.row_index);
1081 found_mask[i] = true;
1082 }
1083 }
1084 }
1085 }
1086
1087 // 2. Check for collisions in (page, row) to prevent aliasing
1088 for i in 0..N {
1089 if !found_mask[i] {
1090 continue;
1091 }
1092 for j in (i + 1)..N {
1093 if found_mask[j] && locations[i] == locations[j] {
1094 // Collision detected: return all None for safety
1095 return std::array::from_fn(|_| None);
1096 }
1097 }
1098 }
1099
1100 // 3. Retrieve references using unsafe to bypass split_at_mut complexity.
1101 // SAFETY: We have verified that all (page, row) pairs are unique,
1102 // so we are not creating multiple mutable references to the same data.
1103 for i in 0..N {
1104 if found_mask[i] {
1105 let (page_id, row_index) = locations[i];
1106 unsafe {
1107 // We can't borrow self.pages multiple times mutably in the loop,
1108 // but we know the indices are disjoint or the data is disjoint.
1109 let world_ptr = self as *mut Self;
1110 if let Some(page) = (&mut *world_ptr).storage.pages.get_mut(page_id as usize) {
1111 if let Some(column) = page.columns.get_mut(&type_id) {
1112 if let Some(vec) = column.as_any_mut().downcast_mut::<Vec<T>>() {
1113 results[i] = Some(vec.get_unchecked_mut(row_index as usize));
1114 }
1115 }
1116 }
1117 }
1118 }
1119 }
1120
1121 results
1122 }
1123
1124 /// Gets an immutable reference to a single component `T` for a given entity.
1125 ///
1126 /// This provides direct, "random" access to a component.
1127 ///
1128 /// # Returns
1129 ///
1130 /// `None` if the entity is not alive or does not have the requested component.
1131 pub fn get<T: Component>(&self, entity_id: EntityId) -> Option<&T> {
1132 // 1. Validate the entity ID. An out-of-range index means "not alive"
1133 // (e.g. a stale or malformed EntityId), so return None rather than panic.
1134 let (id_in_world, metadata_opt) = self.entities.get(entity_id.index as usize)?;
1135 if id_in_world.generation != entity_id.generation || metadata_opt.is_none() {
1136 return None;
1137 }
1138 let metadata = metadata_opt.as_ref().unwrap();
1139
1140 // 2. Use the registry to find the component's domain and its location.
1141 let domain = self.storage.registry.get_domain(TypeId::of::<T>())?;
1142 let location = metadata.locations.get(&domain)?;
1143
1144 // 3. Get the component data from the page.
1145 let type_id = TypeId::of::<T>();
1146 let page = self.storage.pages.get(location.page_id as usize)?;
1147
1148 // 4. Return the immutable reference.
1149 let vec = page
1150 .columns
1151 .get(&type_id)?
1152 .as_any()
1153 .downcast_ref::<Vec<T>>()?;
1154 vec.get(location.row_index as usize)
1155 }
1156
1157 /// Reads a component by **value**, working for *any* physical layout (AoS or
1158 /// field-SoA). This is the layout-agnostic read path: a field-SoA component
1159 /// can't hand out `&T` (its bytes aren't a contiguous `T`), so callers that
1160 /// must work regardless of layout — the serialization recipe, the `Soa<T>`
1161 /// query — go through here. For AoS it simply clones the `&T`.
1162 ///
1163 /// `None` if the entity is not alive or lacks the component.
1164 pub fn clone_component<T: Component>(&self, entity_id: EntityId) -> Option<T> {
1165 let (id_in_world, metadata_opt) = self.entities.get(entity_id.index as usize)?;
1166 if id_in_world.generation != entity_id.generation {
1167 return None;
1168 }
1169 let metadata = metadata_opt.as_ref()?;
1170 let domain = self.storage.registry.get_domain(TypeId::of::<T>())?;
1171 let location = metadata.locations.get(&domain)?;
1172 let page = self.storage.pages.get(location.page_id as usize)?;
1173 let column = page.columns.get(&TypeId::of::<T>())?;
1174 Some(T::clone_from_column(
1175 column.as_ref(),
1176 location.row_index as usize,
1177 ))
1178 }
1179
1180 /// Writes a component by **value**, working for any physical layout. The
1181 /// layout-agnostic write path (AoS assigns the slot; field-SoA scatters into
1182 /// its lanes). Returns `false` if the entity is not alive or lacks the
1183 /// component (nothing is written).
1184 pub fn set_component<T: Component>(&mut self, entity_id: EntityId, value: T) -> bool {
1185 let Some((id_in_world, metadata_opt)) = self.entities.get(entity_id.index as usize) else {
1186 return false;
1187 };
1188 if id_in_world.generation != entity_id.generation {
1189 return false;
1190 }
1191 let Some(metadata) = metadata_opt.as_ref() else {
1192 return false;
1193 };
1194 let Some(domain) = self.storage.registry.get_domain(TypeId::of::<T>()) else {
1195 return false;
1196 };
1197 let Some(location) = metadata.locations.get(&domain).copied() else {
1198 return false;
1199 };
1200 let Some(page) = self.storage.pages.get_mut(location.page_id as usize) else {
1201 return false;
1202 };
1203 let Some(column) = page.columns.get_mut(&TypeId::of::<T>()) else {
1204 return false;
1205 };
1206 value.set_in_column(column.as_mut(), location.row_index as usize);
1207 // The component's value changed — invalidate cached Views.
1208 self.bump_domain_epoch(domain);
1209 true
1210 }
1211
1212 /// Runs `f` over every field-SoA column of component `T` in the world — the
1213 /// bulk SIMD entry point. Each call hands the kernel a [`FieldSoaColumn`]
1214 /// whose per-field `f32` lanes are contiguous, so it can tile them into
1215 /// `f32x8` without gather (the resident layout that reaches ~4×).
1216 ///
1217 /// Iterates per page so each lane slice is a single archetype's run. A
1218 /// no-op for any page whose `T` column is not field-SoA.
1219 pub fn for_each_soa_column_mut<T: crate::ecs::SoaLayout>(
1220 &mut self,
1221 mut f: impl FnMut(&mut crate::ecs::FieldSoaColumn<T>),
1222 ) {
1223 let type_id = TypeId::of::<T>();
1224 // Bulk mutable access to T's lanes — invalidate cached Views once,
1225 // up front (never inside the per-page/per-element loop).
1226 if let Some(domain) = self.storage.registry.get_domain(type_id) {
1227 self.bump_domain_epoch(domain);
1228 }
1229 for page in self.storage.pages.iter_mut() {
1230 if let Some(column) = page.columns.get_mut(&type_id) {
1231 if let Some(soa) = column
1232 .as_any_mut()
1233 .downcast_mut::<crate::ecs::FieldSoaColumn<T>>()
1234 {
1235 f(soa);
1236 }
1237 }
1238 }
1239 }
1240
1241 /// Returns an iterator over all currently living `EntityId`s in the world.
1242 pub fn iter_entities(&self) -> impl Iterator<Item = EntityId> + '_ {
1243 self.entities
1244 .iter()
1245 .filter_map(|(id, metadata_opt)| metadata_opt.as_ref().map(|_| *id))
1246 }
1247
1248 /// Serializes the entire World state using a direct memory layout strategy.
1249 ///
1250 /// This method is highly unsafe as it reads raw component memory.
1251 pub fn serialize_archetype(&self) -> Result<Vec<u8>, bincode::error::EncodeError> {
1252 let mut serialized_pages = Vec::with_capacity(self.storage.pages.len());
1253 for page in &self.storage.pages {
1254 let mut serialized_columns = HashMap::new();
1255
1256 // Use the TypeRegistry to get the stable string name for each TypeId.
1257 let type_names: Vec<String> = page
1258 .type_ids
1259 .iter()
1260 .map(|id| self.type_registry.get_name_of(id).unwrap().to_string())
1261 .collect();
1262
1263 for type_id in &page.type_ids {
1264 let type_name = self.type_registry.get_name_of(type_id).unwrap();
1265 let column = &page.columns[type_id];
1266 // The column owns its byte format (AoS raw bytes, or field-major
1267 // for a field-SoA column) — round-tripped by `set_from_bytes`.
1268 serialized_columns.insert(type_name.to_string(), column.to_bytes());
1269 }
1270
1271 serialized_pages.push(SerializedPage {
1272 type_names,
1273 entities: page.entities.clone(),
1274 columns: serialized_columns,
1275 });
1276 }
1277 let layout = SceneMemoryLayout {
1278 entities: self.entities.entities.clone(),
1279 freed_entities: self.entities.freed_entities.clone(),
1280 pages: serialized_pages,
1281 };
1282 bincode::encode_to_vec(layout, config::standard())
1283 }
1284
1285 /// Deserializes and completely replaces the World state from a memory layout.
1286 ///
1287 /// This method is highly unsafe as it writes raw bytes into component vectors.
1288 pub fn deserialize_archetype(&mut self, data: &[u8]) -> Result<(), DeserializeArchetypeError> {
1289 let (layout, _): (SceneMemoryLayout, _) =
1290 bincode::decode_from_slice(data, config::standard())?;
1291
1292 self.entities.entities = layout.entities;
1293 self.entities.freed_entities = layout.freed_entities;
1294 self.storage.pages.clear();
1295
1296 for serialized_page in layout.pages {
1297 // Use the TypeRegistry to convert string names back to TypeIds. A
1298 // name absent from the registry comes from an untrusted/foreign
1299 // scene, so fail gracefully instead of panicking.
1300 let type_ids: Vec<TypeId> = serialized_page
1301 .type_names
1302 .iter()
1303 .map(|name| {
1304 self.type_registry
1305 .get_id_of(name)
1306 .ok_or_else(|| DeserializeArchetypeError::UnknownComponent(name.clone()))
1307 })
1308 .collect::<Result<_, _>>()?;
1309
1310 let mut new_page = ComponentPage {
1311 type_ids,
1312 entities: serialized_page.entities,
1313 columns: HashMap::new(),
1314 };
1315
1316 for (type_name, bytes) in &serialized_page.columns {
1317 let type_id = self.type_registry.get_id_of(type_name).ok_or_else(|| {
1318 DeserializeArchetypeError::UnknownComponent(type_name.clone())
1319 })?;
1320 let constructor = self
1321 .storage
1322 .registry
1323 .get_column_constructor(&type_id)
1324 .ok_or_else(|| {
1325 DeserializeArchetypeError::UnknownComponent(type_name.clone())
1326 })?;
1327 let mut column = constructor();
1328 // SAFETY: `constructor` is the registered column factory for
1329 // `type_id`, so it produces a column whose element type matches
1330 // the bytes serialized for that same type. `set_from_bytes`
1331 // additionally validates the byte length before any allocation,
1332 // returning an error (propagated here) on a hostile or
1333 // mismatched length rather than aborting.
1334 unsafe {
1335 column
1336 .set_from_bytes(bytes)
1337 .map_err(DeserializeArchetypeError::InvalidColumn)?;
1338 }
1339 new_page.columns.insert(type_id, column);
1340 }
1341 self.storage.pages.push(new_page);
1342 }
1343
1344 // The entire World content was replaced by raw storage writes —
1345 // every domain may have changed, so invalidate all cached Views.
1346 self.bump_all_domain_epochs();
1347
1348 Ok(())
1349 }
1350}
1351
1352impl World {
1353 /// Returns `true` if any of `entity`'s live metadata locations references
1354 /// `(page_id, row)`. Domain-**agnostic** on purpose: in a multi-domain page a
1355 /// physical row is dead only when *no* domain still points at it, so a
1356 /// per-domain check (like the query layer's `is_live_row`) would wrongly
1357 /// classify a row still live for another domain as an orphan and destroy it.
1358 ///
1359 /// A dead/recycled entity (generation mismatch or vacated metadata) counts as
1360 /// not referencing the row, so its leftover row is reclaimable.
1361 fn entity_references_row(&self, entity: EntityId, page_id: u32, row: usize) -> bool {
1362 let Some((slot_id, metadata_opt)) = self.entities.get(entity.index as usize) else {
1363 return false;
1364 };
1365 if slot_id.generation != entity.generation {
1366 return false;
1367 }
1368 let Some(metadata) = metadata_opt.as_ref() else {
1369 return false;
1370 };
1371 metadata
1372 .locations
1373 .values()
1374 .any(|loc| loc.page_id == page_id && loc.row_index as usize == row)
1375 }
1376
1377 /// Compacts a single page: physically drops every row no live entity
1378 /// references (a migration orphan), preserving order for the surviving rows.
1379 ///
1380 /// Reuses [`remove_from_page`](Self::remove_from_page) as the removal
1381 /// primitive, so the survivor moved into each hole has its metadata repaired
1382 /// across **all** its domains — the validation the former `cleanup_orphan_at`
1383 /// lacked. Representation-only, but reordering rows changes query iteration
1384 /// order, so the page's domain epochs are bumped when at least one row is
1385 /// removed (order-sensitive Views — index-aligned light/audio lists — depend
1386 /// on the bump). No bump when nothing was removed.
1387 pub(crate) fn compact_page(&mut self, page_id: u32) {
1388 match self.storage.pages.get(page_id as usize) {
1389 Some(page) if !page.entities.is_empty() => {}
1390 _ => return,
1391 }
1392
1393 let mut removed_any = false;
1394 let mut row = 0usize;
1395 loop {
1396 let len = self.storage.pages[page_id as usize].entities.len();
1397 if row >= len {
1398 break;
1399 }
1400 let entity = self.storage.pages[page_id as usize].entities[row];
1401 if self.entity_references_row(entity, page_id, row) {
1402 // Live for some domain — keep it and advance.
1403 row += 1;
1404 } else {
1405 // Orphan: `remove_from_page` swap-removes it and repoints the
1406 // survivor moved into the slot (across all its domains). The
1407 // swapped-in row now sits at `row`, so re-check the same index.
1408 self.remove_from_page(
1409 entity,
1410 PageIndex {
1411 page_id,
1412 row_index: row as u32,
1413 },
1414 );
1415 removed_any = true;
1416 }
1417 }
1418
1419 if removed_any {
1420 // Iteration order for every domain this page participates in changed.
1421 let mut domains: Vec<SemanticDomain> = {
1422 let page = &self.storage.pages[page_id as usize];
1423 page.type_ids
1424 .iter()
1425 .filter_map(|t| self.storage.registry.get_domain(*t))
1426 .collect()
1427 };
1428 domains.sort_by_key(|d| d.index());
1429 domains.dedup();
1430 for domain in domains {
1431 self.bump_domain_epoch(domain);
1432 }
1433
1434 // If compaction drained the page completely, recycle its slot so a
1435 // later allocation reuses it instead of growing the pages vec.
1436 if self.storage.pages[page_id as usize].entities.is_empty() {
1437 self.storage.mark_page_free(page_id);
1438 }
1439 }
1440 }
1441
1442 /// Drains up to `budget` dirty pages and compacts each, returning the number
1443 /// of pages processed. Called once per frame by
1444 /// [`EcsMaintenance`](crate::ecs::EcsMaintenance) in `TickPhase::Maintenance`.
1445 /// Pages beyond the budget stay queued for the next frame — harmless, since
1446 /// the query layer already skips orphan rows via `is_live_row`.
1447 pub(crate) fn run_compaction(&mut self, budget: usize) -> usize {
1448 if budget == 0 || self.storage.dirty_pages.is_empty() {
1449 return 0;
1450 }
1451 let take: Vec<u32> = self
1452 .storage
1453 .dirty_pages
1454 .iter()
1455 .copied()
1456 .take(budget)
1457 .collect();
1458 for &page_id in &take {
1459 self.storage.dirty_pages.remove(&page_id);
1460 }
1461 for &page_id in &take {
1462 self.compact_page(page_id);
1463 }
1464 take.len()
1465 }
1466}
1467
1468impl Default for World {
1469 /// Creates a new, empty `World` via `World::new()`.
1470 fn default() -> Self {
1471 Self::new()
1472 }
1473}