anput/
view.rs

1use crate::{
2    archetype::{Archetype, ArchetypeColumnInfo, ArchetypeView},
3    bundle::BundleColumns,
4    component::Component,
5    entity::Entity,
6    query::{
7        DynamicLookupAccess, DynamicLookupIter, DynamicQueryFilter, DynamicQueryIter,
8        TypedLookupAccess, TypedLookupFetch, TypedLookupIter, TypedQueryFetch, TypedQueryIter,
9    },
10    world::World,
11};
12use std::{
13    marker::PhantomData,
14    ops::{Bound, Deref, RangeBounds},
15};
16
17pub struct TypedWorldView<B: BundleColumns> {
18    view: WorldView,
19    _phantom: PhantomData<B>,
20}
21
22impl<B: BundleColumns> TypedWorldView<B> {
23    pub fn new(world: &World) -> Self {
24        Self {
25            view: WorldView::new::<B>(world),
26            _phantom: PhantomData,
27        }
28    }
29
30    pub fn new_raw(view: WorldView) -> Option<Self> {
31        for column in B::columns_static() {
32            if !view.views.iter().any(|v| v.has_column(&column)) {
33                return None;
34            }
35        }
36        Some(Self {
37            view,
38            _phantom: PhantomData,
39        })
40    }
41
42    pub fn into_inner(self) -> WorldView {
43        self.view
44    }
45}
46
47impl<B: BundleColumns> Clone for TypedWorldView<B> {
48    fn clone(&self) -> Self {
49        Self {
50            view: self.view.clone(),
51            _phantom: PhantomData,
52        }
53    }
54}
55
56impl<B: BundleColumns> Deref for TypedWorldView<B> {
57    type Target = WorldView;
58
59    fn deref(&self) -> &Self::Target {
60        &self.view
61    }
62}
63
64#[derive(Default, Clone)]
65pub struct WorldView {
66    views: Vec<ArchetypeView>,
67}
68
69impl WorldView {
70    pub fn new<B: BundleColumns>(world: &World) -> Self {
71        Self::default().with::<B>(world)
72    }
73
74    pub fn with<B: BundleColumns>(mut self, world: &World) -> Self {
75        self.include::<B>(world);
76        self
77    }
78
79    pub fn with_raw(mut self, world: &World, columns: &[ArchetypeColumnInfo]) -> Self {
80        self.include_raw(world, columns);
81        self
82    }
83
84    pub fn include<B: BundleColumns>(&mut self, world: &World) {
85        for archetype in world.archetypes() {
86            if let Some(view) = archetype.view::<B>() {
87                self.views.push(view);
88            }
89        }
90    }
91
92    pub fn include_raw(&mut self, world: &World, columns: &[ArchetypeColumnInfo]) {
93        for archetype in world.archetypes() {
94            if let Some(view) = archetype.view_raw(columns) {
95                self.views.push(view);
96            }
97        }
98    }
99
100    #[inline]
101    pub fn is_empty(&self) -> bool {
102        self.len() == 0
103    }
104
105    #[inline]
106    pub fn len(&self) -> usize {
107        self.archetypes().map(|archetype| archetype.len()).sum()
108    }
109
110    #[inline]
111    pub fn archetypes(&self) -> impl Iterator<Item = &Archetype> {
112        self.views.iter().map(|view| view.archetype())
113    }
114
115    #[inline]
116    pub fn entities(&self) -> impl Iterator<Item = Entity> + '_ {
117        self.views.iter().flat_map(|view| view.entities().iter())
118    }
119
120    #[inline]
121    pub fn entity_by_index(&self, mut index: usize) -> Option<Entity> {
122        for archetype in self.archetypes() {
123            if index >= archetype.len() {
124                index -= archetype.len();
125                continue;
126            }
127            return archetype.entities().get(index);
128        }
129        None
130    }
131
132    #[inline]
133    pub fn entities_range(
134        &self,
135        range: impl RangeBounds<usize>,
136    ) -> impl Iterator<Item = Entity> + '_ {
137        WorldViewEntityRangeIter::new(self, range)
138    }
139
140    #[inline]
141    pub fn entities_work_group(
142        &self,
143        group_index: usize,
144        mut groups_count: usize,
145        mut min_items_per_group: usize,
146    ) -> impl Iterator<Item = Entity> + '_ {
147        groups_count = groups_count.max(1);
148        min_items_per_group = min_items_per_group.max(1);
149        let group_size = (self.len() / groups_count).max(min_items_per_group);
150        let start = group_index * group_size;
151        let end = start + group_size;
152        self.entities_range(start..end)
153    }
154
155    pub fn find_by<const LOCKING: bool, T: Component + PartialEq>(
156        &self,
157        data: &T,
158    ) -> Option<Entity> {
159        for (entity, component) in self.query::<LOCKING, (Entity, &T)>() {
160            if component == data {
161                return Some(entity);
162            }
163        }
164        None
165    }
166
167    pub fn find_with<const LOCKING: bool, T: Component>(
168        &self,
169        f: impl Fn(&T) -> bool,
170    ) -> Option<Entity> {
171        for (entity, component) in self.query::<LOCKING, (Entity, &T)>() {
172            if f(component) {
173                return Some(entity);
174            }
175        }
176        None
177    }
178
179    pub fn entity<'a, const LOCKING: bool, Fetch: TypedLookupFetch<'a, LOCKING>>(
180        &'a self,
181        entity: Entity,
182    ) -> Option<Fetch::Value> {
183        // TODO: this might be fucked up here, i believe we could potentially extend
184        // fetched references lifetimes, which can lead to memory corruption - INVESTIGATE!
185        self.lookup_access::<LOCKING, Fetch>().access(entity)
186    }
187
188    pub fn query<'a, const LOCKING: bool, Fetch: TypedQueryFetch<'a, LOCKING>>(
189        &'a self,
190    ) -> TypedQueryIter<'a, LOCKING, Fetch> {
191        TypedQueryIter::new_view(self)
192    }
193
194    pub fn dynamic_query<'a, const LOCKING: bool>(
195        &'a self,
196        filter: &DynamicQueryFilter,
197    ) -> DynamicQueryIter<'a, LOCKING> {
198        DynamicQueryIter::new_view(filter, self)
199    }
200
201    pub fn lookup<'a, const LOCKING: bool, Fetch: TypedLookupFetch<'a, LOCKING>>(
202        &'a self,
203        entities: impl IntoIterator<Item = Entity> + 'a,
204    ) -> TypedLookupIter<'a, LOCKING, Fetch> {
205        TypedLookupIter::new_view(self, entities)
206    }
207
208    pub fn lookup_access<'a, const LOCKING: bool, Fetch: TypedLookupFetch<'a, LOCKING>>(
209        &'a self,
210    ) -> TypedLookupAccess<'a, LOCKING, Fetch> {
211        TypedLookupAccess::new_view(self)
212    }
213
214    pub fn dynamic_lookup<'a, const LOCKING: bool>(
215        &'a self,
216        filter: &DynamicQueryFilter,
217        entities: impl IntoIterator<Item = Entity> + 'a,
218    ) -> DynamicLookupIter<'a, LOCKING> {
219        DynamicLookupIter::new_view(filter, self, entities)
220    }
221
222    pub fn dynamic_lookup_access<'a, const LOCKING: bool>(
223        &'a self,
224        filter: &DynamicQueryFilter,
225    ) -> DynamicLookupAccess<'a, LOCKING> {
226        DynamicLookupAccess::new_view(filter, self)
227    }
228}
229
230pub struct WorldViewEntityRangeIter<'a> {
231    views: &'a [ArchetypeView],
232    index: usize,
233    offset: usize,
234    left: usize,
235}
236
237impl<'a> WorldViewEntityRangeIter<'a> {
238    pub fn new(view: &'a WorldView, range: impl RangeBounds<usize>) -> Self {
239        let size = view.len();
240        let start = match range.start_bound() {
241            Bound::Included(start) => *start,
242            Bound::Excluded(start) => start.saturating_add(1),
243            Bound::Unbounded => 0,
244        }
245        .min(size);
246        let end = match range.end_bound() {
247            Bound::Included(end) => end.saturating_add(1),
248            Bound::Excluded(end) => *end,
249            Bound::Unbounded => size,
250        }
251        .max(start)
252        .min(size);
253        let mut offset = start;
254        let mut index = 0;
255        for view in &view.views {
256            if offset >= view.len() {
257                offset -= view.len();
258                index += 1;
259            } else {
260                break;
261            }
262        }
263        let left = end - start;
264        Self {
265            views: &view.views,
266            index,
267            offset,
268            left,
269        }
270    }
271}
272
273impl Iterator for WorldViewEntityRangeIter<'_> {
274    type Item = Entity;
275
276    fn next(&mut self) -> Option<Self::Item> {
277        while self.left > 0 {
278            let view = self.views.get(self.index)?;
279            if let Some(entity) = view.entities().get(self.offset) {
280                self.offset += 1;
281                self.left -= 1;
282                return Some(entity);
283            } else {
284                self.offset = 0;
285                self.index += 1;
286            }
287        }
288        None
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use moirai::jobs::Jobs;
296    use std::{
297        thread::{sleep, spawn},
298        time::Duration,
299    };
300
301    fn is_async<T: Send + Sync>() {}
302
303    #[test]
304    fn test_world_view_threads() {
305        is_async::<WorldView>();
306
307        let mut world = World::default();
308        for index in 0..20usize {
309            world.spawn((index, index % 2 == 0)).unwrap();
310        }
311
312        let view = WorldView::new::<(usize,)>(&world);
313        let join = spawn(move || {
314            view.query::<true, &usize>()
315                .inspect(|value| {
316                    println!("Value: {value}");
317                    sleep(Duration::from_millis(10));
318                })
319                .copied()
320                .sum::<usize>()
321        });
322
323        sleep(Duration::from_millis(50));
324        println!("Try spawn SDIR locked columns");
325        // View has SDIR locked `usize` column, so we can't spawn anything
326        // with those columns.
327        assert!(world.spawn((42usize, false)).is_err());
328
329        sleep(Duration::from_millis(50));
330        println!("Spawn SDIR unlocked columns");
331        // Neighter `bool`, nor `i32` column has SDIR lock in view, so we
332        // are safe to spawn those.
333        world.spawn((true, 42i32)).unwrap();
334
335        sleep(Duration::from_millis(50));
336        println!("Wait for job result");
337        let sum = join.join().unwrap();
338        println!("Sum: {sum}");
339        assert_eq!(sum, world.query::<true, &usize>().copied().sum::<usize>());
340
341        // View no longer exists, so no more SDIR lock on columns.
342        println!("Spawn previously SDIR locked columns");
343        world.spawn((42usize, false)).unwrap();
344    }
345
346    #[test]
347    fn test_world_view_parallel() {
348        const N: usize = if cfg!(miri) { 10 } else { 1000 };
349        let jobs = Jobs::default();
350
351        let mut world = World::default();
352        for index in 0..N {
353            world.spawn((index, index % 2 == 0)).unwrap();
354        }
355
356        // World view over selected columns.
357        let view = WorldView::new::<(usize,)>(&world);
358
359        // Process view with parallelized execution of view work groups.
360        let sum = jobs
361            .broadcast(move |ctx| {
362                let entities =
363                    view.entities_work_group(ctx.work_group_index, ctx.work_groups_count, 10);
364                view.lookup::<true, &usize>(entities)
365                    .copied()
366                    .sum::<usize>()
367            })
368            .wait()
369            .unwrap()
370            .into_iter()
371            .sum::<usize>();
372
373        assert_eq!(sum, world.query::<true, &usize>().copied().sum());
374    }
375}