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 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 assert!(world.spawn((42usize, false)).is_err());
328
329 sleep(Duration::from_millis(50));
330 println!("Spawn SDIR unlocked columns");
331 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 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 let view = WorldView::new::<(usize,)>(&world);
358
359 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}