Skip to main content

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}